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