\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 "" (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