[prev in list] [next in list] [prev in thread] [next in thread] 

List:       lilypond-user
Subject:    Re: Slur with left and/or right arrow head
From:       Thomas Morley <thomasmorley65 () gmail ! com>
Date:       2019-04-19 21:03:31
Message-ID: CABsfGyVY5+X5BsYq085TF6R0XuhBB5eo_yfLfXuzpHErsuqieA () mail ! gmail ! com
[Download RAW message or body]

Am Di., 16. Apr. 2019 um 23:45 Uhr schrieb Aaron Hill
<lilypond@hillvisions.com>:
>
> On 2019-04-16 10:37 am, Thomas Morley wrote:
> > Am Mo., 15. Apr. 2019 um 19:26 Uhr schrieb Lukas-Fabian Moser
> > <lfm@gmx.de>:
> >>
> >> Folks,
> >>
> >> in
> >> https://archiv.lilypondforum.de/index.php?topic=1744.msg9669#msg9669,
> >> Harm invented a truly wonderful new feature allowing to add an arrow
> >> head to the right end of a Slur (or, for that matter, a Tie,
> >> PhrasingSlur etc.). I reproduce it here with only trivial changes
> >> (mainly omitting parser/location).
> >>
> >> Now I also need slurs with arrows pointing to the left (and ideally,
> >> also the option to have an arrow tip at both ends of the Slur). At
> >> first
> >> glance the asymmetry favoring the right hand side of a Slur seems to
> >> be
> >> hard-coded pretty deeply in Harm's code. Is there a cheap way to add a
> >> choice of "left or right end" (if not even the "or/and" possibility)?
> >>
> >> Best
> >> Lukas
> >
> > Hi Lukas,
> >
> > I started to implement the functionality, finally I more or less
> > rewrote anything.
> > As David K once said: rewriting all means at least knowing where the
> > bugs are...
>
> Harm,
>
> There is an annoying optical issue where using the angle of the curve at
> the end points does not work well for an arrow head that partially
> overlaps the curve.  Instead, one needs to consider the slope of the
> curve a little inwards from the ends so that the arrow appears to be
> aligned properly.
>
> I took a stab at patching your code to address this.  This involved some
> additional computational work for various metrics of a Bezier curve.
> See the attached files.
>
> Among the things I changed is that the code that adds the arrows to the
> ends of the curve no longer applies an offset.  This offset was strictly
> horizontal which did not work well for more heavily rotated arrows.
> Instead, the offset is done within the code that computes and rotates
> the arrow, so that the center of rotation is properly defined.

Hi Aaron,

meanwhile I think I understand more about Beziers, many thanks for
your and David's explanations.
Also, I looked entirely through your code and probably understood how
you do things.

As already said I stumbled across some procedures being called over
and over, also I asked myself why we need the entire length of the
Bezier, if we are interested only in a short part at start/end.
So I wrote a procedure (relying on `split-bezier ´ from
bezier-tools.scm), where the Bezier is splitted, i.e. two sets of new
control-points are returned.
For those new control-points the direct line between first and last
point is calculated. If this is lower than a certain treshold, we have
control-points for a Bezier where we can calculate the angle,
otherwise it continues to recurse until the goal is reached. Relying
on start/end of the original Bezier it's first or last of the new
points, which now can serve for calculating the angle.

Function-calls are drastically reduced, performance time is reduced
and code simplified imho.

One thing I noticed are not so nice printed arrows for short Beziers
like Repeat/LaissezVibrerTie.
Likely due to the width of the arrowhead, _within_ this width the
Bezier is "too active", so to speak.


In general I've found your function to calculate a point on the Bezier
and to calculate an angle at a certain point of the Bezier _very_
helpful.
What do you think adding it to bezier-tools.scm?

Attached the newest code.

WYT?

Thanks,
  Harm

@ Lukas
Up to now I didn't tackle the arrow-left/right LEFT/RIGHT thing, I
first wanted to fight my way through the code for Beziers.
Probably tomorrow, hopefuly ...

["arrow-slur-04.ly" (text/x-lilypond)]

\version "2.19.82"

%% Thanks to Aaron Hill
%% http://lists.gnu.org/archive/html/lilypond-user/2019-04/msg00240.html 

%% Does not work for 2.18.2 because of
%%   - grob::name (could be replaced by grob-name, see p.e. LSR)
%%   - minimum-length-after-break (no replacement possible, only used in
%%     the examples, though)
  
#(ly:load "bezier-tools.scm")

#(define (note-column-bounded? dir grob)
"Checks wether @var{grob} is a spanner and whether the spanner is bounded in
@var{dir}-direction by a note-column."
  (if (ly:spanner? grob)
      (grob::has-interface (ly:spanner-bound grob dir) 'note-column-interface)
      #f))

#(define (offset-number-pair-list l1 l2)
"Offset the number-pairs of @var{l1} by the matching number-pairs of @var{l2}"
;; NB no type-checking or checking for equal lengths is done here
  (map (lambda (p1 p2) (offset-add p1 p2)) l1 l2))

#(define (bezier::point control-points t)
"Given a Bezier curve of arbitrary degree specified by @var{control-points},
compute the point at the specified position @var{t}."
  (if (< 1 (length control-points))
      (let ((q0 (bezier::point (drop-right control-points 1) t))
            (q1 (bezier::point (drop control-points 1) t)))
        (cons
          (+ (* (car q0) (- 1 t)) (* (car q1) t))
          (+ (* (cdr q0) (- 1 t)) (* (cdr q1) t))))
      (car control-points)))

#(define (bezier::angle control-points t)
"Given a Bezier curve of arbitrary degree specified by @var{control-points},
compute the slope at the specified position @var{t}."
  (let ((q0 (bezier::point (drop-right control-points 1) t))
        (q1 (bezier::point (drop control-points 1) t)))
    (ly:angle (- (car q1) (car q0)) (- (cdr q1) (cdr q0)))))

#(define* 
  (bezier::approx-control-points-to-length 
    control-points dir length 
    #:optional (precision 0.01) (right-t 0.2) (left-t 0.8))
"Given a Bezier curve specified by @var{control-points}, return 
new control-points where the length of the Bezier specified by them is approx
@var{length}.
The procedure returns if difference of the new calculated length and the given
@var{length} is lower than optional @var{precision}.
The optional @var{left-t} and @var{right-t} represent the steps where new
control-points are calculated relying on @var{dir}."
  ;; TODO
  ;; Do the values for precision, left-t, right-t cover all cases?
  (let*  ((frst-cp (car control-points))
          (last-cp (last control-points))
          (actual-length
            (ly:length 
              (- (car frst-cp) (car last-cp))
              (- (cdr frst-cp) (cdr last-cp))))
          (diff (- (abs actual-length) (abs length))))
      (if (< diff precision)
          control-points
          (bezier::approx-control-points-to-length
            (if (positive? dir)
                (cdr (split-bezier control-points right-t))
                (car (split-bezier control-points left-t)))
            dir
            length))))

#(define (curve-adjusted-arrow-head dir control-points)
(lambda (curve)
"Returns a stencil build from an arrowhead-glyph, adjusted to fit at start/end
of a curve looking at the curve's @var{control-points}.
Relying on @var{dir} for looking at left or right side of the curve."
  (if (not dir)
      empty-stencil
      (let* ((staff-space (ly:staff-symbol-staff-space curve))
             ;; reducing fs-from-staff-space a bit looks nicer
             (fs-from-staff-space (1- (magnification->font-size staff-space)))
             (grob-font
               (ly:paper-get-font
                 (ly:grob-layout curve)
                 `(((font-encoding . fetaMusic)
                    (font-size . ,fs-from-staff-space)))))
             (arrowhead-stil
               (ly:font-get-glyph grob-font
                 (format #f "arrowheads.open.0~a1"
                   (if (positive? dir) "" "M"))))
             (arrowhead-width 
               (interval-length (ly:stencil-extent arrowhead-stil X)))
             (offset-stil
               (ly:stencil-translate
                 arrowhead-stil
                 (cons (* dir 0.4 arrowhead-width) 0)))
             (arrowhead-end 
               (interval-bound (ly:stencil-extent offset-stil X) (- dir)))
             (offset (* 0.33 arrowhead-end))
             (angle 
               (bezier::angle 
                 (bezier::approx-control-points-to-length 
                   control-points dir offset)
                 (if (positive? dir) 0 1))))
        (ly:stencil-rotate-absolute offset-stil angle 0 0)))))
        
#(define modify-control-points-for-arrows
(lambda (grob)
"Returns a number-pair-list suitable for setting @code{control-points}-property.
The values are modified with respect to a probably printed arrowhead, which
is done by looking at the subproperties of @code{details}:
@code{arrow-left} and @code{arrow-right}."
  (let* ((curve-dir (ly:grob-property grob 'direction))
         (details (ly:grob-property grob 'details))
         (arrow-left (assoc-get 'arrow-left details #f))
         (arrow-right (assoc-get 'arrow-right details #f))
         (nc-right-bound?
           (note-column-bounded? RIGHT grob))
         (nc-left-bound?
           (note-column-bounded? LEFT grob))
         (c-ps (ly:grob-property grob 'control-points)))
    (cond ((and (not arrow-left) (not arrow-right))
            c-ps)
          ((eq? (grob::name grob) 'LaissezVibrerTie)
            (if arrow-left ;; move a little to right
                (offset-number-pair-list
                  c-ps
                  '((0.3 . 0) (0.3 . 0) (0.3 . 0) (0.3 . 0)))
                 c-ps))
          ((eq? (grob::name grob) 'RepeatTie)
            (if arrow-right ;; move a little to left
                (offset-number-pair-list
                  c-ps
                  '((-0.3 . 0) (-0.3 . 0) (-0.3 . 0) (-0.3 . 0)))
                c-ps))
          (else ;; Tie, Slur, PhrasingSlur
            (let ((move-this-to-left
                    (if arrow-left
                        (if nc-left-bound? 0.4 0.5)
                        0))
                  (move-this-to-right
                    (if arrow-right
                        (if nc-right-bound? -0.4 -0.5)
                        0))
                  ;; For Ties we want to keep a horizontal look
                  (move-Y-at-left
                    (if (or arrow-left
                            (grob::has-interface grob 'tie-interface))
                        (* 0.2 curve-dir)
                        0))
                  (move-Y-at-right
                    (if (or arrow-right
                            (grob::has-interface grob 'tie-interface))
                        (* 0.2 curve-dir)
                        0)))
              (offset-number-pair-list
                c-ps
                (list
                  (cons move-this-to-left  move-Y-at-left)
                  (cons move-this-to-left  move-Y-at-left)
                  (cons move-this-to-right move-Y-at-right)
                  (cons move-this-to-right move-Y-at-right)))))))))

#(define add-arrow-head-to-curve
(lambda (grob)
"Returns a curve stencil with optional arrowheads at start/end.
Whether to print arrowheads is decided by looking at the subproperties of
@code{details}: @code{arrow-left} and @code{arrow-right}."
  (let* ((orig (if (ly:spanner? grob)
                   (ly:grob-original grob)
                   #f))
         (siblings (if (ly:grob? orig)
                       (ly:spanner-broken-into orig)
                       '()))
         (control-points (modify-control-points-for-arrows grob))
         (details (ly:grob-property grob 'details))
         (arrow-left (assoc-get 'arrow-left details #f))
         (arrow-right (assoc-get 'arrow-right details #f))
         (function
           (assoc-get 'stencil (reverse (ly:grob-basic-properties grob))))

         (stil ;; Ugh, is there no better way to test that a grob has no
               ;; 'stencil and that no other previous procedure assigned
               ;; a stencil-value to said grob?
               (if (and (procedure? function)
                        (not (eq? (procedure-name function)
                                  'add-arrow-head-to-curve)))
                   (begin
                     (ly:grob-set-property! grob 'control-points control-points)
                     (function grob))
                   (begin
                     (ly:warning "~a has no stencil. Ignoring." grob)
                     #f))))
     (cond ((and (not arrow-left) (not arrow-right))
            stil)
           (#t
            ;; TODO
            ;; For now arrowheads are printed as specified even for each
            ;; broken curve, should possibilities to limit behaviour at
            ;; line-break be implemented?
            ;(or (null? siblings)
            ;    #t
            ;    (equal? grob (car (last-pair siblings))))
            (let* (;(control-points (modify-control-points-for-arrows grob))
                   (frst (car control-points))
                   (frth (cadddr control-points))

                   (arrow-right
                     ((curve-adjusted-arrow-head arrow-right control-points)
                       grob))
                   (arrow-left
                     ((curve-adjusted-arrow-head arrow-left control-points)
                       grob)))
              (ly:stencil-add
                (ly:stencil-translate arrow-left frst)
                (ly:stencil-translate arrow-right frth)
                (stencil-with-color stil red))))))))

pointing-curve =
#(define-music-function (p l curve) (string?)
"Set property @code{after-line-breaking} for grob @code{curve}. Finally setting
the @code{stencil} to @code{arrowed-curve}.
It's needed to go for @code{after-line-breaking}, otherwise changes to
@code{control-points} done by @code{shape} wouldn't be respected.
Whether or not arrows are printed should done by applying, p.e.
@lilypond[verbatim,quote]
  \\override Tie.details.arrow-left = #LEFT
  \\override Slur.details.arrow-left = #LEFT
@end lilypond
separately."
  #{
    \temporary \override $curve . after-line-breaking =
      #(lambda (grob)
        (ly:grob-set-property! grob 'stencil (add-arrow-head-to-curve grob)))
  #})

revert-pointing-curve =
#(define-music-function (p l curve) (string?)
"Revert the setting for @code{after-line-breaking} of grob @var{curve}."
  #{
    \revert $curve . after-line-breaking
  #})

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% EXAMPLES
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%


\layout {
  \override Tie.details.arrow-left = #LEFT
  \override Slur.details.arrow-left = #LEFT
  \override PhrasingSlur.details.arrow-left = #LEFT
  \override RepeatTie.details.arrow-left = #LEFT
  \override LaissezVibrerTie.details.arrow-left = #LEFT

  \override Tie.details.arrow-right = #RIGHT
  \override Slur.details.arrow-right = #RIGHT
  \override PhrasingSlur.details.arrow-right = #RIGHT
  \override RepeatTie.details.arrow-right = #RIGHT
  \override LaissezVibrerTie.details.arrow-right = #RIGHT
}

% {
\new Staff \with { instrumentName = "Slurs" }
\relative c'' {
	\pointing-curve Slur
	c'( c 
	\bar "" \break  
	c c)

	\slurDown
	c( c c c)

	\voiceOne
	c,,4( c c c'')

	<>^"default"
	\revert-pointing-curve Slur
	\oneVoice
	c( c c c)
}
%}
% {
m = { c4( d e f e d des c) }

testI = {
  \relative c \m
  \relative c' \m
  \relative c'' \m
  \relative c''' \m
}

\new Staff \with { instrumentName = "Slurs" }
{
  \pointing-curve Slur
  <>^"no Slur-Stem-direction"
  \testI
  \break


  <>^"Slur down, Stem up"
  \slurDown
  \stemUp
  \testI
  \break

  <>^"Slur up, Stem down"
  \slurUp
  \stemDown
  \testI
  \break

  <>^"Slur up, Stem up"
  \slurUp
  \stemUp
  \testI
  \break

  <>^"Slur down, Stem down"
  \slurDown
  \stemDown
  \testI
  \break

  <>^"default"
  \stemNeutral
  \slurNeutral
  \revert-pointing-curve Slur
  \testI
  \break
}
%}
% {
\new Staff \with { instrumentName = "Ties" }
\relative c' {
	\pointing-curve Tie
	%% overriding TieColumn.tie-configuration works
	<c e g c>1~
    \once \override TieColumn.tie-configuration =
      #'((3.0 . 1) (-1.0 . 1) (-5.0 . -1) (-8.0 . -1))
	q
	\once \override Tie.minimum-length-after-break = 8

	<c e g c>1~
	\break
	q
	<>^"default"
	\revert-pointing-curve Tie
	<c e g c>1~ q
}
%}
% {
\new Staff \with { instrumentName = "PhrasingSlur" }
\relative c' {
	\pointing-curve PhrasingSlur
	<c e g c>1^\( q q <g d' g b g'>\)
	<>^"default"
	\revert-pointing-curve PhrasingSlur
	<c e g c>1^\( q q <g d' g b g'>\)
}
%}
% {
%% \shape works
\new Staff \with { instrumentName = "RepeatTie" }
\relative c' {
  \pointing-curve RepeatTie
  c1\repeatTie
  %% If left _and_ right arrow is wished, the RepeatTie may be too
  %% short, use \shape then
  <>^"shaped"
  \shape #'((-0.6 . 0) (-0.6 . -0.1) (0 . -0.1) (0 . 0)) RepeatTie
  c1\repeatTie
  <>^"default"
  \revert-pointing-curve RepeatTie
  c1\repeatTie
}
%}
% {
\new Staff \with { instrumentName = "LaissezVibrerTie" }
\relative c' {
  \pointing-curve LaissezVibrerTie
  c1\laissezVibrer
  %% If left _and_ right arrow is wished, the LaissezVibrerTie may be too
  %% short, use \shape then
  <>^"shaped"
  c1-\shape #'((0 . 0) (0 . -0.1) (0.6 . -0.1) (0.6 . 0))-\laissezVibrer
  <>^"default"
  \revert-pointing-curve LaissezVibrerTie
  c1\laissezVibrer
}
%}
\paper { indent = 30 }

#(set-global-staff-size 18)

%% arrow-slur-04.ly
%% real	0m3,880s
%% user	0m3,595s
%% sys	0m0,286s

%% arrow-slur-03.ly
%% real	0m3,540s
%% user	0m3,323s
%% sys	0m0,216s

%% arrow-slur-03-patch.ly
%% real	0m4,191s
%% user	0m3,776s
%% sys	0m0,414s



_______________________________________________
lilypond-user mailing list
lilypond-user@gnu.org
https://lists.gnu.org/mailman/listinfo/lilypond-user


[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic