[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 13:04:01 +0200
User-agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Thunderbird/68.10.0

New version with some typos removed:

\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:
% (
%   #(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-command)-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)
                          ; from start ...
                          (lineto (midpoint 1st-directional-point 2nd-directional-point))                           ; ... draw line to the midpoint of the two directional control points  ...
                          ; ... and then to stop.
                          (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)
               ; draw lines:
               ; from start to start + scaled-normal
               ;            to stop + scaled-normal
               ;            to stop
               ; yielding a bracket-shaped path.
               (lineto (vector-add start scaled-normal))
               (lineto (vector-add stop scaled-normal))
               (lineto stop))
                      } #})))

% Might as well test the markup functions 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]