| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- /* 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('<br />');
- }
- const noteEventsBuffer = [];
- function updateNote(type, { x, y }) {
- x = Math.floor(x);
- y = Math.floor(y);
- noteEventsBuffer.unshift(`${type}: (${x}, ${y})`);
- noteEventsDiv.innerHTML = noteEventsBuffer.join('<br />');
- }
|