[Top][All Lists]

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: Questions about HorizontalBracket

From: Lukas-Fabian Moser
Subject: Re: Questions about HorizontalBracket
Date: Tue, 15 Sep 2020 12:58:19 +0200
User-agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Thunderbird/68.10.0

Hi Francesco,

Thank you very much, Lukas! :-) Your answer goes far beyond my expectations:
using slurs is a very good point. Actually I had thought about using them, but
I had no clue on how to modify their shape. Besides that, Scheme language
still looks a bit intimidating to me, so I had to resort to use

Anyway, let’s see if I understand the big picture of your code:
1. you define some pure Scheme functions that deal about creating the basic
“shapes” (actually the vectors that will be used by the “path” command);
2. then you define the commands that tweak the stencil of a Slur grob;
3. and, like a “tweak” command, you call it just before the parentheses,
letting the magic happen, right?

Yes, that basically is the picture. I created a second version that not only should have a clearer structure (because now the creation of the shapes is factored out into two custom-made markup functions) but is also heavily commented.

I have a plan to create some examples of "LilyPond programming with annotations" (for the dual purpose of enlightening myself and helping others on their path), so feel free to comment and ask questions.

By the way, the code does not compile with my old 2.19.83 version (included in
my Linux installation), I had to download the latest version.

My bad: I hadn't realized that the invaluable \=... mechanism for simultaneous slurs (added by David K. in 2015 wasn't contained in 2.19.83. Sorry.


\version "2.21.0"

% -------------- Scheme functions for dealing with 2D vectors -------------
% A vector is represented as a pair v = (x . y), hence
% x is (car v) and y is (cdr v).

% Some routines for calculating with 2D vectors (given as scheme pairs)
#(define (vector-add v w) ; Calculate v+w
   (cons (+ (car v) (car w))
         (+ (cdr v) (cdr w))))
% Remember that (cons a b) by definition constructs the pair (a . b).

#(define (vector-substract v w) ; Calculate v-w
   (cons (- (car v) (car w))
         (- (cdr v) (cdr w))))

#(define (vector-stretch factor v) ; Calculate factor * v.
   ; (The mathematician in me wanted to write lambda * v, but lambda
   ; is a sacred reserved word in scheme :-).)
   (cons (* (car v) factor)
         (* (cdr v) factor)))

#(define (scalar-product v w) ; Euclidean scalar product of vectors
   (+ (* (car v) (car w))
      (* (cdr v) (cdr w))))

#(define (midpoint v w) ; calculate (v+w)/2
   (vector-stretch 1/2 (vector-add v w)))

#(define (rotate-90-ccw v)
   ; rotate vector 90° counter-clockwise
   ; (mathematically positive direction)
   (cons (- (cdr v)) (car v)))

#(define (normal p q)
   ; constructs a normal vector to the line from p to q.
   ; the length of the normal vector will be proportional to
   ; the distance [pq].
   (rotate-90-ccw (vector-substract q p)))

#(define (on-which-halfplane v normal start)
   ; A line through "start" with fixed normal vector "normal" cuts the plane
   ; into two half-planes. This function returns
   ; 0 if v lies on the line itself,
   ; +1 if v lies in the half plane that the normal vector points to,
   ; -1 otherwise.
   (let ((dist (- (scalar-product normal v)
                  (scalar-product normal start))))
     (cond ((> dist 0) 1) ; is there no "sgn" function in guile?!
           ((< dist 0) -1)
           (else 0))

% -------------- Markup functions replacing slur shapes -------------
% The following markup functions expect a list of control points
% like they are used for slurs:
% (start 1st-directional-point 2nd-directional-point stop)

% First, we define shortcuts for easy construction of \path's.
% [ Background: We want to construct a markup using \path. As you can
%   see in,
%   \path expects a list of commands of the form
%     '((moveto 0 0)
%       (lineto -1 1)
%       (lineto 1 1)
%       (lineto 1 -1)
%       (curveto -5 -5 -5 5 -1 0)
%       (closepath)),
%   i.e. each element is itself a list, starting with a command symbol like
%   'lineto followed by numeric parameters.
%   But for us, points/vectors are _pairs_ of numbers.
%   So, we define a "moveto" command does the necessary conversion: It combines
%   - the symbol "moveto",
%   - the x-coordinate of point p (i.e. (car p))
%   - the y coordinate of point p (i.e. (cdr p))
%   into a list. ]
#(define (moveto p) (list 'moveto (car p) (cdr p)))
#(define (lineto p) (list 'lineto (car p) (cdr p)))

% Markup functions are defined by #(define-markup-command ...), see
% [ Note that the syntax for defining new markup commands differs from
%   the syntax for defining new music functions
% ( syntax is different from the way you define new music functions): %   #(define-markup-command (name-of-new-command layout props arguments...) (type-predictes...) ...)
%   vs.
%   name-of-new-music-function = #(define-music-function (arguments...) (type-predicates...) ...)
%   One of the reasons is that #(define-markup-command) defines not only
%   \name-of-new-command but also make-(name-of-new-music-function)-markup
%   to be used in scheme; see below. ]

  (slurReplacementVShape layout props control-points)
  (number-pair-list?) ; The only actual parameter (control-points) should be a list of (we expect: four) number pairs
  (let* ((start (first control-points))
         (1st-directional-point (second control-points))
         (2nd-directional-point (third control-points))
         (stop (fourth control-points)))
    (interpret-markup layout props #{
      \markup {
        \path #0.1 #(list (moveto start)
                          (lineto (midpoint 1st-directional-point 2nd-directional-point))
                          (lineto stop))
                      } #})))

  (slurReplacementBracket layout props control-points)
  (let* ((start (first control-points))
         (stop (fourth control-points))
         (1st-directional-point (second control-points))
         ; We have to find out the direction in which the bracket should
         ; protrude. The idea here is to use the first directional
         ; control point of the slur and draw the bracket such that
         ; it lies in the half-plane (with respect to the line through
         ; "start" and "stop" in which that control point lies.)
         (normal (normal start stop))
           (* 0.075 (on-which-halfplane 1st-directional-point normal start))
    (interpret-markup layout props #{
      \markup {
        \path #0.1
        #(list (moveto start)
               (lineto (vector-add start scaled-normal))
               (lineto (vector-add stop scaled-normal))
               (lineto stop))
                      } #})))

% Might as well test them right away:

\markup {
  Test of markup command "\slurReplacementVShape:"
  \slurReplacementVShape #'((0 . 0) (2 . 2) (5 . 3) (8 . 1))

\markup {
  Test of markup command "\slurReplacementBracket:"
  \slurReplacementBracket #'((0 . 0) (2 . 2) (5 . 3) (8 . 1))

% -------------- Macros for tweaking slurs -------------

VShapeSlur = \tweak stencil
#(lambda (grob)
    ; Interpret as a markup (see bonus consideration below) ...
    grob ; ... using the layout/properties settings active for the current grob ...
    ; ... the following markup:
     ; What you would write inside a LilyPond \markup as
     ; \slurReplacementVShape {parameters},
     ; in scheme becomes:
     ; (make-slurReplacementVShape-markup parameters)
     ; As parameters, we take the control-points property of the
     ; current grob (which we know will be a slur, because that's where
     ; we intend to use the \VShapeSlur command).
     ; The fact that slurs do have a property 'control-points can be
     ; found in
     (ly:grob-property grob 'control-points))))

bracketSlur = \tweak stencil
#(lambda (grob)
    grob (make-slurReplacementBracket-markup
          (ly:grob-property grob 'control-points))))

% -------------- Tests -------------

\relative c' {
  c1 \bracketSlur ( d) e \bracketSlur( f g a g f')
  c,1 \VShapeSlur ( d) e \VShapeSlur( f g a g f')

\markup {
  The "\=..." mechanism for distinguishing arbitrarily many (maybe overlapping)
  slurs also can be used:
\relative c' {
  c4 \VShapeSlur \=0_( \VShapeSlur \=1^( d e \bracketSlur \=2_( f\=1) e^( d\=2) c b\=0))

\markup \wordwrap {
  Nowhere in our definition of "\\VShapeSlur" do we actually require the
  "\\tweak"ed grob to be a slur. Everything that has suitable 'control-points
  should work - for example, a tie! And behold:
\relative c' {
  c1 \VShapeSlur ~ c
  d'1 \bracketSlur ~ d
\markup \wordwrap {
  (Turns out that naming our tweak macros "\\VShapeSlur" and "\\bracketSlur"
  might have been a little bit rash.)

% ----- Bonus consideration ("what I learned while writing this") ----------------
% What's the deal with
% (interpret-markup layout props ...) vs. (grob-interpret-markup grob ...) ?
% Both turn a markup into a stencil ("they draw the markup"). For this,
% they need context: For instance, settings about the current font-size etc.,
% which are stored in the layout and props variables. So the question
% becomes: What does grob-interpret-markup do?
% To find out, I simply looked up its definition in LilyPond itself. In
% scm/output-lib.scm, we find:
%   (define-public (grob-interpret-markup grob text)
%     (let* ((layout (ly:grob-layout grob))
%            (defs (ly:output-def-lookup layout 'text-font-defaults))
%            (props (ly:grob-alist-chain grob defs)))
%       (ly:text-interface::interpret-markup layout props text)))
% To wit, grob-interpret-markup asks the grob to reveal the suitable
% layout and props that are needed to call ly:text-interface::interpret-markup.
% BUT: What about the prefix "ly:text-interface::"?
% The answer may be found in scm/markup.scm:
%   (define-public interpret-markup ly:text-interface::interpret-markup)
% which means: ly:text-interface::interpret-markup is the full name,
% for which interpret-markup is just a shorthand.

reply via email to

[Prev in Thread] Current Thread [Next in Thread]