)
}
}
)
(
}
{
)
)
(
)
(
(
{
}
)
(
)
}
)
)
{
(
(
)
)
}
)
(
}

SVG Relative Touch/Mouse Events

  1. const hasTouch =
  2.   navigator.maxTouchPoints != null && navigator.maxTouchPoints > 0;
  3.  
  4. const el = document.body.appendChild(document.createElement('div'));
  5.  
  6. el.innerHTML = `
  7.   <svg id="mainSvg" width="100%" height="100%" viewBox="0 0 800 800">
  8.     <g transform="translate(100, 100) scale(0.8, 0.8) rotate(25, 400, 400)">
  9.       <rect x="0" y="0" width="800" height="800" fill="#ccc" stroke="none"/>
  10.       <text x="10" y="30" font-size="30px">click/tap the box</text>
  11.     </g>
  12.   </svg>
  13.   <style>
  14.     svg, div, body, html {
  15.       height: 100%;
  16.       width: 100%;
  17.       margin: 0; padding: 0;
  18.     }
  19.  
  20.     svg {
  21.       overflow: hidden;
  22.     }
  23.   </style>
  24. `;
  25. function createSvg(type) {
  26.   return document.createElementNS('http://www.w3.org/2000/svg', type);
  27. }
  28.  
  29. // mouse or touch location is always relative to the svg
  30. // any css transformations on the svg are only accounted for in 
  31. // Chrome unfortunately
  32. function svgTouch(e, svgEl) {
  33.   const touches = e.touches;
  34.   let locX, locY;
  35.   if (touches != null && touches.length > 0) {
  36.     locX = touches[0].clientX;
  37.     locY = touches[0].clientY;
  38.   } else {
  39.     locX = e.clientX;
  40.     locY = e.clientY;
  41.   }
  42.  
  43.   const pt = svgEl.createSVGPoint();
  44.   pt.x = locX;
  45.   pt.y = locY;
  46.  
  47.   const newPnt = pt.matrixTransform(svgEl.getScreenCTM().inverse());
  48.   return { locX: newPnt.x, locY: newPnt.y };
  49. }
  50.  
  51. document.addEventListener(hasTouch ? 'touchstart' : 'mousedown', e => {
  52.   // global id `mainSvg` :P
  53.   const { locX, locY } = svgTouch(e, mainSvg);
  54.  
  55.   const circle = createSvg('circle');
  56.   circle.cx.baseVal.value = locX;
  57.   circle.cy.baseVal.value = locY;
  58.   circle.r.baseVal.value = 20;
  59.   circle.style.fill = 'blue';
  60.  
  61.   mainSvg.appendChild(circle);
  62. });

This snippet shows how to get relative mouse/touch coordinates within an SVG element. The main trick here is on line 50… pt.matrixTransform(svgEl.getScreenCTM().inverse()); – we get the inverse transformation matrix of the “mainSvg” node and transform the mouse/touch coordinates by it.

Beware, when this was posted, as noted in the comments – CSS transforms on the SVG element or any of its parents won’t be accounted for in the `getScreenCTM` call in any browsers other than Chrome. There is an open Firefox issue for this I believe…

// dom // graphics // javascript // math // matrix // svg // tricks

Quick Touch Events 2 (easing)

  1. // no scrolling on mobile
  2. document.addEventListener('touchmove', e => e.preventDefault(), {
  3.   passive: false
  4. });
  5.  
  6. const hasTouch =
  7.   navigator.maxTouchPoints != null && navigator.maxTouchPoints > 0;
  8. // looking forward to `navigator?.maxTouchPoints > 0`
  9. // being better supported
  10.  
  11. function touchify(e) {
  12.   const touch = [];
  13.   touch.x = touch.y = 0;
  14.  
  15.   if (e.touches != null && e.touches.length > 0) {
  16.     touch.x = e.touches[0].clientX;
  17.     touch.y = e.touches[0].clientY;
  18.     for (let i = 0; i < e.touches.length; i++) {
  19.       touch[i] = e.touches[i];
  20.     }
  21.   } else {
  22.     touch.x = e.clientX;
  23.     touch.y = e.clientY;
  24.     touch[0] = { clientX: e.clientX, clientY: e.clientY };
  25.   }
  26.   return touch;
  27. }
  28.  
  29. let moveOrTouchMove;
  30.  
  31. if (hasTouch) {
  32.   moveOrTouchMove = 'touchmove';
  33. } else {
  34.   moveOrTouchMove = 'mousemove';
  35. }
  36.  
  37. function dot(x, y, radius, color) {
  38.   const el = document.createElement('div');
  39.   const size = `${radius * 2}px`;
  40.   Object.assign(el.style, {
  41.     position: 'absolute',
  42.     left: `${x}px`,
  43.     top: `${y}px`,
  44.     width: size,
  45.     height: size,
  46.     transform: `translate(${-radius}px, ${-radius}px)`,
  47.     borderRadius: '50%',
  48.     background: color
  49.   });
  50.   el.classList.add('dot');
  51.   document.body.appendChild(el);
  52.   return el;
  53. }
  54.  
  55. let dotX = 100,
  56.   dotY = 100,
  57.   touchX = 200,
  58.   touchY = 150,
  59.   damp = 12,
  60.   dotEl = dot(dotX, dotY, 20, 'red');
  61.  
  62. document.addEventListener(moveOrTouchMove, e => {
  63.   const { x, y } = touchify(e);
  64.   touchX = x;
  65.   touchY = y;
  66. });
  67.  
  68. function loop() {
  69.   dotX += (touchX - dotX) / damp;
  70.   dotY += (touchY - dotY) / damp;
  71.   Object.assign(dotEl.style, {
  72.     left: `${dotX}px`,
  73.     top: `${dotY}px`
  74.   });
  75.   window.requestAnimationFrame(loop);
  76. }
  77. loop();

Move your mouse on desktop or your finger on mobile – watch the red dot follow…

This is a simpler version of some of the things used in yesterdays post – just a quick way to normalize touch events – just one of many ways to do this – for simple stuff this is my goto.

If you want to try it out on its own page – take a look here (specifically good for trying it on mobile).

You’ll probably want a to use a meta tag like this – for the full effect.

  1. <meta name="viewport" content="width=device-width, initial-scale=1.0">

Quick Touch Events

  1. // no scrolling on mobile
  2. document.addEventListener("touchmove", e => e.preventDefault(), {
  3.   passive: false
  4. });
  5.  
  6. // has more than one touch point
  7. const hasTouch =
  8.   navigator.maxTouchPoints != null && navigator.maxTouchPoints > 0;
  9.  
  10. function touchify(e) {
  11.   const touch = [];
  12.   touch.x = touch.y = 0;
  13.  
  14.   if (e.touches != null && e.touches.length > 0) {
  15.     touch.x = e.touches[0].clientX;
  16.     touch.y = e.touches[0].clientY;
  17.     for (let i = 0; i < e.touches.length; i++) {
  18.       touch[i] = e.touches[i];
  19.     }
  20.   } else {
  21.     touch.x = e.clientX;
  22.     touch.y = e.clientY;
  23.     touch[0] = { clientX: e.clientX, clientY: e.clientY };
  24.   }
  25.   return touch;
  26. }
  27.  
  28. let clickOrTouchEnd,
  29.   downOrTouchStart,
  30.   enterOrTouchStart,
  31.   moveOrTouchMove,
  32.   upOrTouchEnd,
  33.   leaveOrTouchEnd;
  34.  
  35. if (hasTouch) {
  36.   clickOrTouchEnd = "touchend";
  37.   downOrTouchStart = "touchstart";
  38.   enterOrTouchStart = "touchstart";
  39.   moveOrTouchMove = "touchmove";
  40.   upOrTouchEnd = "touchend";
  41.   leaveOrTouchEnd = "touchend";
  42. } else {
  43.   clickOrTouchEnd = "click";
  44.   downOrTouchStart = "mousedown";
  45.   enterOrTouchStart = "mouseenter";
  46.   moveOrTouchMove = "mousemove";
  47.   upOrTouchEnd = "mouseup";
  48.   leaveOrTouchEnd = "mouseleave";
  49. }
  50.  
  51. function dot(x, y, radius, color) {
  52.   const el = document.createElement("div");
  53.   const size = `${radius * 2}px`;
  54.   Object.assign(el.style, {
  55.     position: "absolute",
  56.     left: `${x}px`,
  57.     top: `${y}px`,
  58.     width: size,
  59.     height: size,
  60.     transform: `translate(${-radius}px, ${-radius}px)`,
  61.     borderRadius: "50%",
  62.     background: color
  63.   });
  64.   el.classList.add("dot");
  65.   document.body.appendChild(el);
  66.   return el;
  67. }
  68.  
  69. let down = false;
  70. let lastTouch;
  71. document.addEventListener(downOrTouchStart, e => {
  72.   const { x, y } = touchify(e);
  73.   dot(x, y, 30, `rgba(255, 0, 0, 0.2)`);
  74.   down = true;
  75. });
  76.  
  77. document.addEventListener(moveOrTouchMove, e => {
  78.   if (down) {
  79.     const touches = touchify(e);
  80.     // draw more than one touch
  81.     for (let i = 0; i < touches.length; i++) {
  82.       const touch = touches[i];
  83.       dot(touch.clientX, touch.clientY, 10, `rgba(0, 0, 155, 0.2)`);
  84.     }
  85.  
  86.     lastTouch = touches[0];
  87.   }
  88. });
  89.  
  90. document.addEventListener(upOrTouchEnd, e => {
  91.   if (down) {
  92.     dot(lastTouch.clientX, lastTouch.clientY, 20, `rgba(0, 155, 0, 0.2)`);
  93.     down = false;
  94.   }
  95. });

Draw with your mouse by clicking, holding down and dragging…

I often use some variation of this depending on the project… This snippet normalizes touch events in a simple way, prevents scrolling and gives an example of how to handle multi-touch.

You’ll probably want a to use a meta tag like this – for the full effect.

  1. <meta name="viewport" content="width=device-width, initial-scale=1.0">

Wobbling Ball With Canvas

  1. // same as yesterday but with canvas instead of svg
  2. const canvas = document.createElement('canvas'),
  3.       c = canvas.getContext('2d');
  4. document.body.appendChild(canvas);
  5. document.body.style.margin = 0;
  6.  
  7. let w = window.innerWidth,
  8.     h = window.innerHeight,
  9.     x = w / 2,
  10.     y = h / 2,
  11.     vx = vy = dx = dy = 0,
  12.     damp = 0.99, div = 8, ticks = 0, 
  13.     wobbleChance = 0.03,
  14.     startTick = 50;
  15.  
  16. function loop() {
  17.  
  18.   w = window.innerWidth;
  19.   h = window.innerHeight;
  20.   radius = w * 0.05;
  21.   diam = radius * 2;
  22.   diam2x = diam * 2;
  23.  
  24.   if (x > w){
  25.     vx *= -1;
  26.     dx *= -1;
  27.     x = w;
  28.   } else if (x < 0){
  29.     vx *= -1;
  30.     dx *= -1;
  31.     x = 0;
  32.   }
  33.  
  34.   if (y > h) {
  35.     vy *= -1;
  36.     dy *= -1;
  37.     y = h;
  38.   } else if (y < 0) {
  39.     vy *= -1;
  40.     dy *= -1;
  41.     y = 0
  42.   } 
  43.  
  44.   if (
  45.     Math.random() < wobbleChance || 
  46.     ticks === startTick) {
  47.       dx += Math.random() * 10 - 5;
  48.       dy += Math.random() * 10 - 5;
  49.   }
  50.  
  51.   dx *= damp;
  52.   dy *= damp;
  53.  
  54.   vx += (dx - vx) / div;
  55.   vy += (dy - vy) / div;
  56.  
  57.   x += vx;
  58.   y += vy;
  59.  
  60.   // in most cases these days you
  61.   // just clear the whole canvas, but for
  62.   // this example we clear a rectangle around 
  63.   // the circle 
  64.   c.clearRect(
  65.     x - diam, 
  66.     y - diam, 
  67.     diam2x, 
  68.     diam2x
  69.   );
  70.  
  71.   // draw the path
  72.   c.fillStyle = 'red'
  73.   c.beginPath();
  74.   c.arc(x, y, radius, 0, Math.PI * 2, false);
  75.   c.fill();
  76.  
  77.   ticks++;
  78.   window.requestAnimationFrame(loop);
  79. }
  80. loop();
  81.  
  82. function resize() {
  83.   canvas.width = window.innerWidth;
  84.   canvas.height = window.innerHeight;
  85. }
  86. resize();
  87. window.addEventListener('resize', resize);

A wobbling ball with canvas.

Wobbling Ball with SVG

  1. const el = document.body.appendChild(
  2.   document.createElement('div'));
  3.  
  4. el.innerHTML = `
  5.   <svg width="100%" height="100%">
  6.     <circle 
  7.      id="circ" 
  8.      cx="0" cy="0" r="50"
  9.      fill="red" style="will-change: transform;"/>
  10.   </svg>
  11.   <style>
  12.     svg, div, body, html {
  13.       overflow: visible; 
  14.       height: 100%; 
  15.       width: 100%;
  16.       margin: 0; padding: 0;
  17.     }
  18.   </style>
  19.   `;
  20.  
  21. let w = window.innerWidth,
  22.     h = window.innerHeight,
  23.     x = w / 2,
  24.     y = h / 2,
  25.     vx = vy = dx = dy = 0,
  26.     damp = 0.99, div = 8, ticks = 0, 
  27.     wobbleChance = 0.03,
  28.     startTick = 50;
  29.  
  30. function loop() {
  31.   w = window.innerWidth;
  32.   h = window.innerHeight;
  33.  
  34.   if (x > w){
  35.     vx *= -1;
  36.     dx *= -1;
  37.     x = w;
  38.   } else if (x < 0){
  39.     vx *= -1;
  40.     dx *= -1;
  41.     x = 0;
  42.   }
  43.  
  44.   if (y > h) {
  45.     vy *= -1;
  46.     dy *= -1;
  47.     y = h;
  48.   } else if (y < 0) {
  49.     vy *= -1;
  50.     dy *= -1;
  51.     y = 0
  52.   } 
  53.  
  54.   if (
  55.     Math.random() < wobbleChance || 
  56.     ticks === startTick) {
  57.       dx += Math.random() * 10 - 5;
  58.       dy += Math.random() * 10 - 5;
  59.   }
  60.  
  61.   dx *= damp;
  62.   dy *= damp;
  63.  
  64.   vx += (dx - vx) / div;
  65.   vy += (dy - vy) / div;
  66.  
  67.   x += vx;
  68.   y += vy;
  69.  
  70.   circ.setAttribute('transform', `translate(${x} ${y})`);
  71.  
  72.   ticks++;
  73.   window.requestAnimationFrame(loop);
  74. }
  75. loop();
  76.  
  77. function resize() {
  78.   const radius = Math.min(w, h) * .05;
  79.  
  80.   // `window.circ` is the global id (⌐■_■)
  81.   circ.r.baseVal.value = radius;
  82. }
  83. resize();
  84. window.addEventListener('resize', resize);

A wobbling ball with svg.

snippet.zone /// {s/z}