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

Catmull Rom to Bezier SVG

  1. // from: 
  2. //   https://github.com/DmitryBaranovskiy/raphael/blob/b5fdbdc9850827eb49622cd0bc81b26025f8128a/dev/raphael.core.js#L1019
  3. // and: 
  4. //   http://schepers.cc/getting-to-the-point
  5.  
  6. function catmullRom2bezier(crp, close) {
  7.   const d = [];
  8.   for (let i = 0, iLen = crp.length; iLen - 2 * !close > i; i += 2) {
  9.     const p = [
  10.       { x: crp[i - 2], y: crp[i - 1] },
  11.       { x: crp[i], y: crp[i + 1] },
  12.       { x: crp[i + 2], y: crp[i + 3] },
  13.       { x: crp[i + 4], y: crp[i + 5] }
  14.     ];
  15.     if (close) {
  16.       if (!i) {
  17.         p[0] = { x: crp[iLen - 2], y: crp[iLen - 1] };
  18.       } else if (iLen - 4 == i) {
  19.         p[3] = { x: crp[0], y: crp[1] };
  20.       } else if (iLen - 2 == i) {
  21.         p[2] = { x: crp[0], y: crp[1] };
  22.         p[3] = { x: crp[2], y: crp[3] };
  23.       }
  24.     } else {
  25.       if (iLen - 4 == i) {
  26.         p[3] = p[2];
  27.       } else if (!i) {
  28.         p[0] = { x: crp[i], y: crp[i + 1] };
  29.       }
  30.     }
  31.     d.push([
  32.       'C',
  33.       (-p[0].x + 6 * p[1].x + p[2].x) / 6,
  34.       (-p[0].y + 6 * p[1].y + p[2].y) / 6,
  35.       (p[1].x + 6 * p[2].x - p[3].x) / 6,
  36.       (p[1].y + 6 * p[2].y - p[3].y) / 6,
  37.       p[2].x,
  38.       p[2].y
  39.     ]);
  40.   }
  41.  
  42.   // really for demo purposes:
  43.  
  44.   // draw a tiny rect if less than 2 points
  45.   console.log(d.length);
  46.   if (d.length < 2) {
  47.     const x = crp[0];
  48.     const y = crp[1];
  49.     return `M ${x} ${y} h 1 v 1 h -1 v-1`;
  50.  
  51.     // draw a line if 2 points
  52.   } else if (d.length === 2) {
  53.     return `M ${crp[0]} ${crp[1]} L ` + crp.flat().join` `;
  54.   }
  55.   // draw curves if 3 or more (raphael only ever did this, other numbers of points
  56.   // were considered invalid)
  57.   return  `M ${crp[0]} ${crp[1]}` + d.flat().join` `;
  58. }
  59.  
  60. // try it out:
  61.  
  62. const pathCommands = catmullRom2bezier([
  63.   100, 100, 200, 100, 200, 300, 170, 150, 120, 180, 120, 120
  64.   ], true)
  65.  
  66. const svg = `
  67.   click 3 or more times anywhere<br>
  68.   <button>clear</button>
  69.   <svg>
  70.     <path d="M 100 100 ${pathCommands}" stroke="blue" fill="none" />
  71.   </svg>
  72.   <style>
  73.   svg {
  74.     position:absolute;
  75.     left:0;top:0;
  76.     overflow:visible;
  77.     pointer-events:none;
  78.   }
  79.   </style>
  80. `;
  81.  
  82. const div = document.body.appendChild(
  83.   document.createElement('div')
  84. );
  85. div.innerHTML = svg;
  86. const path = div.querySelector('path')
  87.  
  88. let pnts = [];
  89.  
  90. function isButton(e) {
  91.   if (e.target.tagName === 'BUTTON') {
  92.     pnts = []
  93.     path.setAttribute('d', 'M 0 0L0 0');
  94.     return true;
  95.   }
  96. }
  97.  
  98. document.addEventListener('click', e => {
  99.   if (isButton(e)) return;
  100.   pnts.push(e.clientX, e.clientY);
  101.   path.setAttribute('d', catmullRom2bezier(pnts, true))
  102. });
  103.  
  104. document.addEventListener('touchend', e => {
  105.   if (isButton(e)) return;
  106.   pnts.push(e.touchs[0].clientX, e.touchs[0].e.clientY);
  107.   path.setAttribute('d', catmullRom2bezier(pnts, true))
  108. });

Raphaël was my first deep dive into SVG. Back then, only VML was supported in older IE versions, so using Raphaël was key for tons of commercial work I did (Raphaël generated VML as well as SVG). This snippet is the Catmull Rom code from Raphaël. I’m pretty sure I didn’t know what a Catmull Rom spline was until I found out that using `R` in a path wasn’t supported natively in SVG… and was actually some magic thing that existed in Raphaël alone.

// curves // javascript // math // svg

Distance Between Two Points (SVG)

  1. const dist = (x1, y1, x2, y2) => 
  2.   Math.sqrt(
  3.     (x1 - x2) ** 2 + 
  4.     (y1 - y2) ** 2);
  5.  
  6. const el = document.body.appendChild(
  7.   document.createElement('div')
  8. );
  9.  
  10. el.innerHTML = `
  11.   <svg style="overflow:visible;">
  12.     <circle id="circA" cx="150" cy="100" r="50" fill="gray" />
  13.     <circle id="circB" cx="150" cy="200" r="50" fill="blue" />
  14.     <text id="text" dy="20" dx="20">move mouse</text>
  15.   </svg>
  16.   <style>
  17.     body, html, svg {
  18.       width: 100%;
  19.       height: 100%;
  20.     }
  21.   </style>
  22. `;
  23.  
  24. function touch(e) {
  25.   const touches = e.touches;
  26.   let x, y;
  27.   if (touches != null && touches.length > 0) {
  28.     x = touches[0].clientX;
  29.     y = touches[0].clientY;
  30.   } else {
  31.     x = e.clientX;
  32.     y = e.clientY;
  33.   }
  34.   return { x, y };
  35. }
  36.  
  37. const hasTouch = navigator.maxTouchPoints > 0;
  38. const move = hasTouch ? 'touchmove' : 'mousemove';
  39. document.addEventListener(move, e => {
  40.   const { x, y } = touch(e);
  41.  
  42.   // using global ids :D
  43.   circB.cx.baseVal.value = x;
  44.   circB.cy.baseVal.value = y;
  45.  
  46.   const distance = dist(
  47.     circA.cx.baseVal.value, 
  48.     circA.cy.baseVal.value, x, y
  49.   );
  50.   text.innerHTML = 'move mouse, distance: ' + distance;
  51.  
  52.   circA.r.baseVal.value = distance - circB.r.baseVal.value;
  53. });

This snippet shows how to calculate the distance between two points. The dist function uses the pythagorean theorem:

  1. const dist = (x1, y1, x2, y2) => 
  2.   Math.sqrt(
  3.     (x1 - x2) ** 2 + 
  4.     (y1 - y2) ** 2);
// dom // graphics // javascript // math // svg

Line to Line Intersection

  1. // line to line intersection https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection
  2. function lineIntersection(p1, p2, p3, p4) {
  3.   let ip = { x: 0, y: 0 };
  4.   // rare case where I use `_` in a non-constant variable name
  5.   // to make this easy to read to with the wikipedia info
  6.   const x4_x3 = p4.x - p3.x;
  7.   const y4_y3 = p4.y - p3.y;
  8.   const x2_x1 = p2.x - p1.x;
  9.   const y2_y1 = p2.y - p1.y;
  10.   const x1_x3 = p1.x - p3.x;
  11.   const y1_y3 = p1.y - p3.y;
  12.   let nx, ny, dn;
  13.  
  14.   nx = x4_x3 * y1_y3 - y4_y3 * x1_x3;
  15.   ny = x2_x1 * y1_y3 - y2_y1 * x1_x3;
  16.   dn = y4_y3 * x2_x1 - x4_x3 * y2_y1;
  17.  
  18.   nx /= dn;
  19.   ny /= dn;
  20.  
  21.   // has intersection
  22.   if (nx >= 0 && nx <= 1 && ny >= 0 && ny <= 1) {
  23.     ny = p1.y + nx * y2_y1;
  24.     nx = p1.x + nx * x2_x1;
  25.     ip.x = nx;
  26.     ip.y = ny;
  27.   } else {
  28.     // no intersection
  29.     ip = null;
  30.   }
  31.   return ip;
  32. }
  33.  
  34. const el = document.body.appendChild(
  35.   document.createElement('div'));
  36.  
  37. // hard coding line values for simplicity and ease of understanding
  38. el.innerHTML = `
  39.   <svg width="100%" height="100%" viewBox="0 0 550 496">
  40.     <path id='path' d="M 10 10 L 300 300 M 100 10 L 160 320" stroke="black" fill='none' vector-effect="non-scaling-stroke"/>
  41.     <rect id="intersecton" x="0" y="0" width="10" height="10" fill="red" />
  42.   </svg>
  43.   <style>
  44.     svg, div, body, html {
  45.       overflow: visible; 
  46.       height: 100%; 
  47.       width: 100%;
  48.       margin: 0; padding: 0;
  49.     }
  50.   </style>
  51. `;
  52.  
  53. const loc = lineIntersection(
  54.   {x: 10, y: 10}, {x: 300, y:300},
  55.   {x: 100, y: 10}, {x: 160, y:320}
  56. );
  57.  
  58. // subtract half the size of the rect from both axis to center it
  59. intersecton.x.baseVal.value = loc.x - 5;
  60.  
  61. // @NOTE: using the `id` global `window.intersection` 
  62. // is just good for demos - little risky for real stuff 
  63. // since it lends itself to easy collision
  64. window.intersecton.y.baseVal.value = loc.y - 5;

Line to line intersection rendered with SVG.

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

Rectangle Path with SVG

  1. const rectPath = rect =>
  2.   `M ${rect.left} ${rect.top}
  3.   L ${rect.right} ${rect.top}
  4.     ${rect.right} ${rect.bottom}
  5.     ${rect.left} ${rect.bottom}
  6.     ${rect.left} ${rect.top} `;
  7.  
  8. const el = document.body.appendChild(
  9.   document.createElement('div'));
  10.  
  11. el.innerHTML = `
  12.   <svg width="100%" height="100%" viewBox="0 0 550 496">
  13.     <path d="
  14.     ${rectPath({left: 20, top: 10, right: 100, bottom: 100})}
  15.     ${rectPath({left: 50, top: 50, right: 200, bottom: 200})}
  16.     ${rectPath({left: 150, top: 20, right: 250, bottom: 100})}
  17.     ${rectPath({left: 150, top: 120, right: 250, bottom: 230})}
  18.     " stroke="black" fill="red" fill-rule="evenodd" vector-effect="non-scaling-stroke"/>
  19.  
  20.     <path d="
  21.     ${rectPath({left: 10, top: 220, right: 100, bottom: 300})}
  22.     ${rectPath({left: 20, top: 250, right: 150, bottom: 350})}
  23.     " stroke="white" fill="#64a7ff" fill-rule="nonzero" vector-effect="non-scaling-stroke"/>
  24.  
  25.     <path d="
  26.     ${rectPath({left: 350, top: 10, right: 450, bottom: 150})}
  27.     " fill="#2e9997" />
  28.   </svg>
  29.   <style>
  30.     svg, div, body, html {
  31.       overflow: visible; 
  32.       height: 100%; 
  33.       width: 100%;
  34.       margin: 0; padding: 0;
  35.     }
  36.   </style>
  37. `;

In this snippet rectPath creates a rectangle with “move to” and “line to” commands to be used in conjunction with SVG paths.

// javascript // paths // svg
snippet.zone ~ 2021-24 /// {s/z}