/* global document window */
const isDragSymbol = Symbol('dragging');
const isOutSymbol = Symbol('isOut');
const windowListeners = Symbol('windowListeners');
const listeners = Symbol('listener');
/**
* A class to enable touch & mouse interactions on SVGs. Translates browser coordinates to
* SVG coordinates.
*
* Call SVGInteraction.makeInteractive(svg), then override this class's methods to use.
*/
class SVGInteraction {
constructor(svg, svgPt) {
if (svgPt) this.svgPt = svgPt;
this.svg = svg;
this.makeInteractive();
}
addEventListener(type, callback) {
this[listeners].push([type, callback]);
}
removeEventListener(type, callback) {
const index = this[listeners].findIndex(([listenerType, listenerCallback]) => {
return type === listenerType && callback === listenerCallback;
});
if (index !== -1) this[listeners].splice(index, 1);
return index !== -1;
}
callListeners(type, e, coords) {
console.log('calling listeners ' + type);
this[listeners].forEach(([listenerType, callback]) => {
console.log('checking listener ', listenerType);
if (type === listenerType) {
console.log('calling!');
callback(e, coords);
}
});
}
/* eslint-disable no-unused-vars */
// These are here as holders -- override whichever you need when you inherit this class.
touchStart(e, coords) { this.callListeners('touchStart', e, coords); }
touchEnd(e, coords) { this.callListeners('touchEnd', e, coords); }
drag(e, coords) { this.callListeners('drag', e, coords); }
hover(e, coords) { this.callListeners('hover', e, coords); }
mouseOut(e, coords) { this.callListeners('mouseOut', e, coords); }
/* eslint-enable no-unused-vars */
makeInteractive(svg = this.svg) {
console.log('making interactive');
this[listeners] = [];
// We will add listeners to the SVG bounding box itself:
svg.style.pointerEvents = 'bounding-box';
// An SVG point is used to translate div space to SVG space. See Alexander Jank's solution at:
// https://stackoverflow.com/questions/29261304/how-to-get-the-click-coordinates-relative-to-svg-element-holding-the-onclick-lis
this.svgPt = this.svgPt || svg.createSVGPoint();
/** ==== Condition Testing & Setting Functions ====
*
* These functions are used to translate UI events from the browser into more helpful
* events from a programatic perspective. They take care of tracking mousestate &
* touchstate (down or up) to fire drag vs. hover. If a user drags outside of the SVG,
* they continue to fire the drag event, so that (if desired) things like drag & drop
* among elements can be implemented.
*
*/
// A not-combinator
const not = condition => () => !condition();
// Get whether we're dragging
const isDragging = () => this[isDragSymbol];
// Set whether the mouse is down or up.
const down = () => { this[isDragSymbol] = true; return true; };
const up = () => { this[isDragSymbol] = false; return true; };
// Get whether we're outside of the SVG bounds
const isOutside = () => this[isOutSymbol];
// Set whether we're in our out; if we move out while dragging we need window listeners briefly.
const inside = () => {
this[isOutSymbol] = false;
this[windowListeners].forEach(([eventType, listener]) => {
window.removeEventListener(eventType, listener, false);
});
return true;
};
const outside = () => {
this[isOutSymbol] = true;
this[windowListeners].forEach(([eventType, listener]) => {
window.addEventListener(eventType, listener);
});
return true;
};
// We'll hold the window listeners here so we can add & remove them as needed.
this[windowListeners] = [];
// Utility function to check event conditions & fire class events if needed
const addListener = ([eventType, callback, ifTrue, el]) => {
el = el || svg;
const listener = (evt) => {
if (ifTrue()) {
const coords = getCoords(evt, svg, this.svgPt, this);
callback.call(this, evt, coords);
}
};
if (el !== window) el.addEventListener(eventType, listener);
else this[windowListeners].push([eventType, listener]);
};
// [ type, listener, testFunction (fire listener if true), EventTarget (default this.svg)]
[
/* events occuring within SVG */
['mousedown', this.touchStart, down], // Touch is started
['touchstart', this.touchStart, down], // Touch is started
['mouseup', this.touchEnd, up], // Touch ends or mouse up
['touchend', this.touchEnd, up], // Touch ends or mouse up
['touchmove', this.drag, isDragging], // Dragging
['mousemove', this.drag, isDragging], // Dragging
['mousemove', this.hover, not(isDragging)], // Hover
['mouseout', this.mouseOut, not(isDragging)], // Mouseout
['touchcancel', this.mouseOut, not(isDragging)], // Mouseout (touch interrupt)
['mouseout', () => {}, () => isDragging() && outside()], // goes out of bounds isDown & set
['touchcancel', () => {}, () => isDragging() && outside()], // goes out of bounds isDown & set
['touchmove', inside, isOutside], // comes back inside
['mousemove', inside, isOutside], // comes back inside
['mousedown', inside, isOutside], // comes back inside, this shouldn't happen, but just in case
['touchstart', inside, isOutside], // comes back inside, this shouldn't happen, but just in case
/* out of bounds events */
['touchmove', this.drag, () => isOutside() && isDragging(), window], // if outside in window & dragging
['mousemove', this.drag, () => isOutside() && isDragging(), window], // if outside in window & dragging
['mouseup', this.touchEnd, () => isOutside() && isDragging() && up() && inside(), window], // if outside in window & dragging & released
['touchend', this.touchEnd, () => isOutside() && isDragging() && up() && inside(), window], // if outside in window & dragging & released
]
.forEach(addListener);
}
}
/**
* Returns coordinates for an event
* @param {Event} e touch or mouse event
* @param {SVGPoint} svgPt an SVG point
* @param {SVGElement} svg the SVG's client bounding rectangle
*
* @returns {Object} { x, y, [touches] }
*/
function getCoords(e, svg, svgPt) {
if ('changedTouches' in e) {
const length = e.changedTouches.length;
const touches = [];
for (let i = 0; i < length; i++) {
touches.push(getCoords(e.changedTouches.item(i), svg, svgPt));
}
return { x: touches[0].x, y: touches[0].y, touches };
}
svgPt.x = e.clientX;
svgPt.y = e.clientY;
const svgCoords = svgPt.matrixTransform(svg.getScreenCTM().inverse());
return { x: svgCoords.x, y: svgCoords.y };
}
function drawMusic() {
// Basic setup boilerplate for using VexFlow with the SVG rendering context:
VF = Vex.Flow;
// Create an SVG renderer and attach it to the DIV element named "boo".
var div = document.getElementById("boo")
var renderer = new VF.Renderer(div, VF.Renderer.Backends.SVG);
// Configure the rendering context.
renderer.resize(500, 500);
var context = renderer.getContext();
var notes = [
// A quarter-note C.
new VF.StaveNote({clef: "treble", keys: ["c/4"], duration: "q" }),
// A quarter-note D.
new VF.StaveNote({clef: "treble", keys: ["d/4"], duration: "q" }),
// A quarter-note rest. Note that the key (b/4) specifies the vertical
// position of the rest.
new VF.StaveNote({clef: "treble", keys: ["b/4"], duration: "qr" }),
// A C-Major chord.
new VF.StaveNote({clef: "treble", keys: ["c/4", "e/4", "g/4"], duration: "q" })
];
// Create a voice in 4/4 and add above notes
var voice = new VF.Voice({num_beats: 4, beat_value: 4});
voice.addTickables(notes);
// Format and justify the notes to 400 pixels.
var formatter = new VF.Formatter().joinVoices([voice]).format([voice], 400);
// Create a stave of width 400 at position 10, 40 on the canvas.
var stave = new VF.Stave(10, 40, 400);
// Add a clef and time signature.
stave.addClef("treble").addTimeSignature("4/4");
// Connect it to the rendering context and draw!
stave.setContext(context).draw();
// Render voice
voice.draw(context, stave);
return { notes, context }
}
const { notes, context } = drawMusic();
context.resize(500, 200);
context.setViewBox(0, 0, 500, 200);
const svg = context.svg;
svg.removeAttribute('width');
svg.removeAttribute('height');
svg.style.border = "black solid 1px";
const interaction = new SVGInteraction(svg);
const eventsDiv = document.getElementById('eventsDiv');
const noteEventsDiv = document.getElementById('noteEventsDiv');
const events = [ 'touchStart', 'touchEnd', 'drag', 'hover', 'mouseOut' ];
events.forEach((type) => {
interaction.addEventListener(type, (e, coords) => {
update(type, coords);
})
})
notes.forEach( (note, index) => {
const noteInteraction = new SVGInteraction(note.attrs.el, interaction.svgPt);
events.forEach((type) =>
noteInteraction.addEventListener(type, (e, coords) => { updateNote(`Note ${index}: ${type}`, e, coords); })
);
})
const eventsBuffer = [];
function update(type, { x, y }) {
x = Math.floor(x);
y = Math.floor(y);
eventsBuffer.unshift(`${type}: (${x}, ${y})`);
eventsDiv.innerHTML = eventsBuffer.join('
');
}
const noteEventsBuffer = [];
function updateNote(type, { x, y }) {
x = Math.floor(x);
y = Math.floor(y);
noteEventsBuffer.unshift(`${type}: (${x}, ${y})`);
noteEventsDiv.innerHTML = noteEventsBuffer.join('
');
}