svg-event.ts 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. /* global document window */
  2. const isDragSymbol = Symbol('dragging');
  3. const isOutSymbol = Symbol('isOut');
  4. const windowListeners = Symbol('windowListeners');
  5. const listeners = Symbol('listener');
  6. /**
  7. * A class to enable touch & mouse interactions on SVGs. Translates browser coordinates to
  8. * SVG coordinates.
  9. *
  10. * Call SVGInteraction.makeInteractive(svg), then override this class's methods to use.
  11. */
  12. class SVGInteraction {
  13. constructor(svg, svgPt) {
  14. if (svgPt) this.svgPt = svgPt;
  15. this.svg = svg;
  16. this.makeInteractive();
  17. }
  18. addEventListener(type, callback) {
  19. this[listeners].push([type, callback]);
  20. }
  21. removeEventListener(type, callback) {
  22. const index = this[listeners].findIndex(([listenerType, listenerCallback]) => {
  23. return type === listenerType && callback === listenerCallback;
  24. });
  25. if (index !== -1) this[listeners].splice(index, 1);
  26. return index !== -1;
  27. }
  28. callListeners(type, e, coords) {
  29. console.log('calling listeners ' + type);
  30. this[listeners].forEach(([listenerType, callback]) => {
  31. console.log('checking listener ', listenerType);
  32. if (type === listenerType) {
  33. console.log('calling!');
  34. callback(e, coords);
  35. }
  36. });
  37. }
  38. /* eslint-disable no-unused-vars */
  39. // These are here as holders -- override whichever you need when you inherit this class.
  40. touchStart(e, coords) { this.callListeners('touchStart', e, coords); }
  41. touchEnd(e, coords) { this.callListeners('touchEnd', e, coords); }
  42. drag(e, coords) { this.callListeners('drag', e, coords); }
  43. hover(e, coords) { this.callListeners('hover', e, coords); }
  44. mouseOut(e, coords) { this.callListeners('mouseOut', e, coords); }
  45. /* eslint-enable no-unused-vars */
  46. makeInteractive(svg = this.svg) {
  47. console.log('making interactive');
  48. this[listeners] = [];
  49. // We will add listeners to the SVG bounding box itself:
  50. svg.style.pointerEvents = 'bounding-box';
  51. // An SVG point is used to translate div space to SVG space. See Alexander Jank's solution at:
  52. // https://stackoverflow.com/questions/29261304/how-to-get-the-click-coordinates-relative-to-svg-element-holding-the-onclick-lis
  53. this.svgPt = this.svgPt || svg.createSVGPoint();
  54. /** ==== Condition Testing & Setting Functions ====
  55. *
  56. * These functions are used to translate UI events from the browser into more helpful
  57. * events from a programatic perspective. They take care of tracking mousestate &
  58. * touchstate (down or up) to fire drag vs. hover. If a user drags outside of the SVG,
  59. * they continue to fire the drag event, so that (if desired) things like drag & drop
  60. * among elements can be implemented.
  61. *
  62. */
  63. // A not-combinator
  64. const not = condition => () => !condition();
  65. // Get whether we're dragging
  66. const isDragging = () => this[isDragSymbol];
  67. // Set whether the mouse is down or up.
  68. const down = () => { this[isDragSymbol] = true; return true; };
  69. const up = () => { this[isDragSymbol] = false; return true; };
  70. // Get whether we're outside of the SVG bounds
  71. const isOutside = () => this[isOutSymbol];
  72. // Set whether we're in our out; if we move out while dragging we need window listeners briefly.
  73. const inside = () => {
  74. this[isOutSymbol] = false;
  75. this[windowListeners].forEach(([eventType, listener]) => {
  76. window.removeEventListener(eventType, listener, false);
  77. });
  78. return true;
  79. };
  80. const outside = () => {
  81. this[isOutSymbol] = true;
  82. this[windowListeners].forEach(([eventType, listener]) => {
  83. window.addEventListener(eventType, listener);
  84. });
  85. return true;
  86. };
  87. // We'll hold the window listeners here so we can add & remove them as needed.
  88. this[windowListeners] = [];
  89. // Utility function to check event conditions & fire class events if needed
  90. const addListener = ([eventType, callback, ifTrue, el]) => {
  91. el = el || svg;
  92. const listener = (evt) => {
  93. if (ifTrue()) {
  94. const coords = getCoords(evt, svg, this.svgPt, this);
  95. callback.call(this, evt, coords);
  96. }
  97. };
  98. if (el !== window) el.addEventListener(eventType, listener);
  99. else this[windowListeners].push([eventType, listener]);
  100. };
  101. // [ type, listener, testFunction (fire listener if true), EventTarget (default this.svg)]
  102. [
  103. /* events occuring within SVG */
  104. ['mousedown', this.touchStart, down], // Touch is started
  105. ['touchstart', this.touchStart, down], // Touch is started
  106. ['mouseup', this.touchEnd, up], // Touch ends or mouse up
  107. ['touchend', this.touchEnd, up], // Touch ends or mouse up
  108. ['touchmove', this.drag, isDragging], // Dragging
  109. ['mousemove', this.drag, isDragging], // Dragging
  110. ['mousemove', this.hover, not(isDragging)], // Hover
  111. ['mouseout', this.mouseOut, not(isDragging)], // Mouseout
  112. ['touchcancel', this.mouseOut, not(isDragging)], // Mouseout (touch interrupt)
  113. ['mouseout', () => {}, () => isDragging() && outside()], // goes out of bounds isDown & set
  114. ['touchcancel', () => {}, () => isDragging() && outside()], // goes out of bounds isDown & set
  115. ['touchmove', inside, isOutside], // comes back inside
  116. ['mousemove', inside, isOutside], // comes back inside
  117. ['mousedown', inside, isOutside], // comes back inside, this shouldn't happen, but just in case
  118. ['touchstart', inside, isOutside], // comes back inside, this shouldn't happen, but just in case
  119. /* out of bounds events */
  120. ['touchmove', this.drag, () => isOutside() && isDragging(), window], // if outside in window & dragging
  121. ['mousemove', this.drag, () => isOutside() && isDragging(), window], // if outside in window & dragging
  122. ['mouseup', this.touchEnd, () => isOutside() && isDragging() && up() && inside(), window], // if outside in window & dragging & released
  123. ['touchend', this.touchEnd, () => isOutside() && isDragging() && up() && inside(), window], // if outside in window & dragging & released
  124. ]
  125. .forEach(addListener);
  126. }
  127. }
  128. /**
  129. * Returns coordinates for an event
  130. * @param {Event} e touch or mouse event
  131. * @param {SVGPoint} svgPt an SVG point
  132. * @param {SVGElement} svg the SVG's client bounding rectangle
  133. *
  134. * @returns {Object} { x, y, [touches] }
  135. */
  136. function getCoords(e, svg, svgPt) {
  137. if ('changedTouches' in e) {
  138. const length = e.changedTouches.length;
  139. const touches = [];
  140. for (let i = 0; i < length; i++) {
  141. touches.push(getCoords(e.changedTouches.item(i), svg, svgPt));
  142. }
  143. return { x: touches[0].x, y: touches[0].y, touches };
  144. }
  145. svgPt.x = e.clientX;
  146. svgPt.y = e.clientY;
  147. const svgCoords = svgPt.matrixTransform(svg.getScreenCTM().inverse());
  148. return { x: svgCoords.x, y: svgCoords.y };
  149. }
  150. function drawMusic() {
  151. // Basic setup boilerplate for using VexFlow with the SVG rendering context:
  152. VF = Vex.Flow;
  153. // Create an SVG renderer and attach it to the DIV element named "boo".
  154. var div = document.getElementById("boo")
  155. var renderer = new VF.Renderer(div, VF.Renderer.Backends.SVG);
  156. // Configure the rendering context.
  157. renderer.resize(500, 500);
  158. var context = renderer.getContext();
  159. var notes = [
  160. // A quarter-note C.
  161. new VF.StaveNote({clef: "treble", keys: ["c/4"], duration: "q" }),
  162. // A quarter-note D.
  163. new VF.StaveNote({clef: "treble", keys: ["d/4"], duration: "q" }),
  164. // A quarter-note rest. Note that the key (b/4) specifies the vertical
  165. // position of the rest.
  166. new VF.StaveNote({clef: "treble", keys: ["b/4"], duration: "qr" }),
  167. // A C-Major chord.
  168. new VF.StaveNote({clef: "treble", keys: ["c/4", "e/4", "g/4"], duration: "q" })
  169. ];
  170. // Create a voice in 4/4 and add above notes
  171. var voice = new VF.Voice({num_beats: 4, beat_value: 4});
  172. voice.addTickables(notes);
  173. // Format and justify the notes to 400 pixels.
  174. var formatter = new VF.Formatter().joinVoices([voice]).format([voice], 400);
  175. // Create a stave of width 400 at position 10, 40 on the canvas.
  176. var stave = new VF.Stave(10, 40, 400);
  177. // Add a clef and time signature.
  178. stave.addClef("treble").addTimeSignature("4/4");
  179. // Connect it to the rendering context and draw!
  180. stave.setContext(context).draw();
  181. // Render voice
  182. voice.draw(context, stave);
  183. return { notes, context }
  184. }
  185. const { notes, context } = drawMusic();
  186. context.resize(500, 200);
  187. context.setViewBox(0, 0, 500, 200);
  188. const svg = context.svg;
  189. svg.removeAttribute('width');
  190. svg.removeAttribute('height');
  191. svg.style.border = "black solid 1px";
  192. const interaction = new SVGInteraction(svg);
  193. const eventsDiv = document.getElementById('eventsDiv');
  194. const noteEventsDiv = document.getElementById('noteEventsDiv');
  195. const events = [ 'touchStart', 'touchEnd', 'drag', 'hover', 'mouseOut' ];
  196. events.forEach((type) => {
  197. interaction.addEventListener(type, (e, coords) => {
  198. update(type, coords);
  199. })
  200. })
  201. notes.forEach( (note, index) => {
  202. const noteInteraction = new SVGInteraction(note.attrs.el, interaction.svgPt);
  203. events.forEach((type) =>
  204. noteInteraction.addEventListener(type, (e, coords) => { updateNote(`Note ${index}: ${type}`, e, coords); })
  205. );
  206. })
  207. const eventsBuffer = [];
  208. function update(type, { x, y }) {
  209. x = Math.floor(x);
  210. y = Math.floor(y);
  211. eventsBuffer.unshift(`${type}: (${x}, ${y})`);
  212. eventsDiv.innerHTML = eventsBuffer.join('<br />');
  213. }
  214. const noteEventsBuffer = [];
  215. function updateNote(type, { x, y }) {
  216. x = Math.floor(x);
  217. y = Math.floor(y);
  218. noteEventsBuffer.unshift(`${type}: (${x}, ${y})`);
  219. noteEventsDiv.innerHTML = noteEventsBuffer.join('<br />');
  220. }