Hello,

the snippet attached to this mail allows the tuning with the mouse of the
curves generated by Lilypond so that the changes can be reflected on the
original .ly file.
The tuning is done on the generated SVG file, as explained in this thread
(but you can look at the code of the attached file, there's a mini how-to
and it's very simple to use):

https://lists.gnu.org/archive/html/lilypond-user/2019-12/msg00217.html

Since the snippet is incompatible with Lilypond 2.18 it cannot be included
in the snippets repository of that version, as explained by Thomas (it
would be bad to downgrade the code).
Is it possible to include it in the development version, then?

(I also ask Aaron to revise the indentation / length of lines of the Scheme
code)
The snippet has had very positive feedbacks, and should not be left without
a future, IMHO. I believe it can define a template for the tuning of many
other properties of graphic objects, in a very simple way both for the
Lilypond user and for those who want to extend the procedure through
Javascript.

In the Lilypond user ml they suggested to include it in Frescobaldi or
Denemo; but I don't think it's the best solution. Because, being the
snippet 100% lilypond native code, it does not need any additional tool to
work, and demonstrates the potential of Lilypond in generating interactive
SVG. To be more precise, given that it is 100% Lilypond, Frescobaldi and
Denemo should recognize it automatically.
It is also true, as Carl states, that LilyPond is not intended to be a
WYSIWYG software; but I believe that the "tuning" through GUI is not
opposed to this vision but integrates it, especially considering that the
GUI is generated by Lilypond itself.

Best,
Paolo
\version "2.19.45"

JSSVGSlurTuner = #(define-void-function (body) (string?)
   (let* ((mod (resolve-module '(scm framework-svg)))
          (svg-end (module-ref mod 'svg-end #f)))
     (if (procedure? svg-end)
       (module-define! mod 'svg-end (lambda () (string-join
         (list "<script type=\"text/javascript\"><![CDATA["
               body "]]></script>" (svg-end)) "\n"))))))

JSSVGSlurTunerScript = #"
rootNode = document.querySelector('svg')
pixelsX  = rootNode.getAttribute('width').replace('mm', '') * 96 / 25.4
pixelsY  = rootNode.getAttribute('height').replace('mm', '') * 96 / 25.4
scaleX   = rootNode.getAttribute('viewBox').split(' ')[2] / pixelsX
scaleY   = rootNode.getAttribute('viewBox').split(' ')[3] / pixelsY

var slurId = 0
var currCp = null

function setCpsOnPath(path, x1, y1, x2, y2, x3, y3, x4, y4) {

	path.setAttribute('d', 'M ' + x1 + ',' + y1 + ' ' +
										'C ' + x2 + ',' + y2 + ' ' +
										x3 + ',' + y3 + ' ' +
										x4 + ',' + y4)

}

function initSlur(node) {

	//already initialized
	if (node.hasAttribute('id'))
		return

	cpsCounter = 1
	lineCounter = 1

	for (n1 = node.firstChild; n1 !== null; n1 = n1.nextSibling) {
		if (n1.nodeName == 'g') {
			for (n2 = n1.firstChild; n2 !== null; n2 = n2.nextSibling) {
			
				if (n2.nodeName == 'circle') {
					if (!node.hasAttribute('cp' + cpsCounter + 'x')) {
						//TODO Ugly parsering, replace with a proper and safer one
						transf = n2.getAttribute('transform')
						node.setAttribute('cp' + cpsCounter + 'x',
															transf.replace('translate(', '').split(',')[0])
						node.setAttribute('cp' + cpsCounter + 'y',
															transf.split(',')[1].trim().replace(')', ''))
						n2.setAttribute('id', slurId + '_cp_' + cpsCounter)
						n2.setAttribute('onmousedown', 'selectCp(this)')
					}
					cpsCounter++
				}

				if (n2.nodeName == 'line') {
					n2.setAttribute('id', slurId + '_line_' + lineCounter)
					lineCounter++
				}
			}
		}

		//remove 'transform' attribute and set abs coords
		if (n1.nodeName == 'path') {
			if (n1.hasAttribute('transform'))
				n1.removeAttribute('transform')

			setCpsOnPath(n1,
                   node.getAttribute('cp1x'),
                   node.getAttribute('cp1y'),
                   node.getAttribute('cp2x'),
                   node.getAttribute('cp2y'),
                   node.getAttribute('cp3x'),
                   node.getAttribute('cp3y'),
                   node.getAttribute('cp4x'),
                   node.getAttribute('cp4y'))

			n1.setAttribute('id', slurId + '_path')
			n1.setAttribute('fill', 'none')
		}
	}

	node.setAttribute('id', slurId)

	coords = document.createElementNS('http://www.w3.org/2000/svg', 'text')
	coords.setAttribute('transform',
											'translate('+ node.getAttribute('cp1x') + ',' + 
																		node.getAttribute('cp1y') + ')')
	coords.setAttribute('font-size', '2')
	coords.setAttribute('class', 'lilySlurCoords')
	coords.setAttribute('id', slurId + '_coords')
	node.appendChild(coords)

	slurId++

}

function selectCp(cp) {

	if (!cp.hasAttribute('id'))
		return

	if (!detectLeftButton(event)) {
		event.preventDefault()
		showCoords(cp.getAttribute('id').split('_')[0])
		return
	}

	cp.setAttribute('color', 'cyan')
	currCp = cp

}

function unselectCp() {

	if (currCp)
		currCp.setAttribute('color', 'rgb(255, 127, 0)')

	currCp = null
	
}

function moveCp() {

	if (!currCp)
		return

	translateCoordsStr = event.pageX * scaleX + ',' + event.pageY * scaleY
	currCp.setAttribute('transform',
  										'translate(' + translateCoordsStr + ')')

	//get the associated path
	assocSlurId = currCp.getAttribute('id').split('_')[0]
	path = document.getElementById(assocSlurId + '_path')

	xs = []
	ys = []

	for (i = 0; i < 4; i++) {
		cpElem = document.getElementById(assocSlurId + '_cp_' + (i + 1))
		transf = cpElem.getAttribute('transform')

		xs[i] = transf.replace('translate(', '')
		xs[i] = xs[i].split(',')[0]
		
		ys[i] = transf.split(',')[1].trim().replace(')', '')
	}

	for (i = 0; i < 3; i++) {
		currLine = document.getElementById(assocSlurId + '_line_' + (i + 1))
		currLine.setAttribute('transform', 
													'translate(' + xs[i] + ',' + ys[i] + ')')
		currLine.setAttribute('x2', xs[i + 1] - xs[i])
		currLine.setAttribute('y2', ys[i + 1] - ys[i])
	}

	setCpsOnPath(path, xs[0], ys[0], xs[1], ys[1], xs[2], ys[2], xs[3], ys[3])

}

function showCoords(assocSlurId) {

	coordsToDisplay = '\\\\shape #\\'('
	assocSlur = document.getElementById(assocSlurId)

	for (q = 0; q < 4; q++) {
		newCp = document.getElementById(assocSlurId + '_cp_' + (q + 1))
		
		xsOrig = assocSlur.getAttribute('cp' + (q + 1) + 'x')
		
		xsNew = newCp.getAttribute('transform')
		xsNew = xsNew.replace('translate(', '').split(',')[0]
		
		ysOrig = assocSlur.getAttribute('cp' + (q + 1) + 'y')
		
		ysNew = newCp.getAttribute('transform').split(',')[1]
		ysNew = ysNew.trim().replace(')', '')
		
		diffX = (xsNew - xsOrig).toFixed(3)
		diffY = (ysOrig - ysNew).toFixed(3)
		
		coordsToDisplay += '(' + (+diffX) + ' . ' + (+diffY) + ') '
	}

	coordsToDisplay = coordsToDisplay.substring(0, coordsToDisplay.length - 1)
	coordsToDisplay += ') ' + assocSlur.getAttribute('slurtype')

	alert(coordsToDisplay)

}

function detectLeftButton(evt) {

	evt = evt || window.event
	if ('buttons' in evt) {
		return evt.buttons == 1
	}
	var button = evt.which || evt.button
	return button == 1

}

window.oncontextmenu = function (evt) {

	evt.preventDefault()

}

var as = document.querySelectorAll('a')

//Remove all 'a' tags
for (var i = 0; i < as.length; i++) {
	as[i].replaceWith(...as[i].childNodes)
}

slurs = document.querySelectorAll('svg .lilySlur')

for (i = 0; i < slurs.length; i++) {
	initSlur(slurs[i])
}

document.addEventListener('mouseup', unselectCp)
document.addEventListener('mousemove', moveCp)
"	

addJSSVGSlurTuner = \JSSVGSlurTuner \JSSVGSlurTunerScript

addControlPoints = #(grob-transformer 'stencil (lambda (grob orig)
  (define (draw-control-point pt)
#{ \markup \translate $pt \with-color #'(1 .7 .7) \draw-circle #0.6 #0 ##t #})
  (define (draw-control-segment pt1 pt2)
(let ((delta (cons (- (car pt2) (car pt1)) (- (cdr pt2) (cdr pt1))))) #{ \markup \translate $pt1 \with-color #'(1 .5 0) \draw-line $delta #}))
  (let* ((pts (ly:grob-property grob 'control-points))
         (dots (map (lambda (pt)
            (grob-interpret-markup grob (draw-control-point pt)))
            pts))
         (lines (map (lambda (pt1 pt2)
            (grob-interpret-markup grob (draw-control-segment pt1 pt2)))
            pts (cdr pts))))
    (ly:stencil-add
      (apply ly:stencil-add lines)
      (apply ly:stencil-add dots)
      orig))))

showControlPoints = {
	\override PhrasingSlur.stencil = #addControlPoints
	\override PhrasingSlur.output-attributes = #'((class . "lilySlur")(slurtype . "PhrasingSlur"))
	\override Slur.stencil = #addControlPoints
	\override Slur.output-attributes = #'((class . "lilySlur")(slurtype . "Slur"))
	\override Tie.stencil = #addControlPoints
	\override Tie.output-attributes = #'((class . "lilySlur")(slurtype . "Tie"))  
}

%% How it works: 
%% 
%% 1) Produce SVG output file(s) ( compile with -dbackend=svg ) 
%% 2) Open the SVG file(s) with any viewer (it works with Firefox and Chrome browsers too)
%% 3) Modify slurs/ties with the mouse by moving their control points
%% 4) Right click on one of the control points of a modified slur/tie, 
%%    and copy and paste the expression generated by the script to the .ly file, just before the corresponding slur/tie. Recompile.

\score
{
	{
		\showControlPoints

		g4_\( a' 

		b'2~ | 

		b'2( e''8 d'') c'4\) 
	}
}

\addJSSVGSlurTuner

Reply via email to