2

In the canvas 2D API, we can first define a subpath using one context's transformation and then change that context's transformation for only the fill() or stroke() calls, which would have effect on the stylings, like fillStyle, lineWidth and other visible properties, but which will leave the sub-path as defined. This is quite convenient when we want to zoom in vector-shapes while keeping the same stroke-width.

Here is a simple example where only the lineWidth is affected by the variable zoom transformation:

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

let zoom = 1;
let speed = 0.1;
requestAnimationFrame(update);

function update() {
  if( zoom >= 10 || zoom <= 0.1 ) speed *= -1;
  zoom += speed;
  draw();
  requestAnimationFrame(update);
}

function draw() {
  ctx.setTransform(1, 0, 0, 1, 0, 0);
  ctx.clearRect(0,0,canvas.width,canvas.height);
  // define the subpath at identity matrix
  ctx.beginPath();
  ctx.moveTo(10 ,80);
  ctx.quadraticCurveTo(52.5,10,95,80);
  ctx.quadraticCurveTo(137.5,150,180,80);
  // stroke zoomed
  ctx.setTransform(zoom, 0, 0, zoom, 0, 0);
  ctx.stroke();
}
<canvas id="canvas"></canvas>

With the Path2D API, we have to pass this subpath directly in either ctx.fill(path) or ctx.stroke(path) methods.
This means we can't separate the stylings from the subpath declaration like we did before:

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

let zoom = 1;
let speed = 0.1;
requestAnimationFrame(update);

function update() {
  if( zoom >= 10 || zoom <= 0.1 ) speed *= -1;
  zoom += speed;
  draw();
  requestAnimationFrame(update);
}

function draw() {
  ctx.setTransform(1, 0, 0, 1, 0, 0);
  ctx.clearRect(0,0,canvas.width,canvas.height);
  // define the subpath at identity matrix
  // (declared in 'draw' just for the example, would be the same anyway outside)
  const path = new Path2D("M 10 80 Q 52.5 10, 95 80 T 180 80");
  // stroke zoomed
  ctx.setTransform(zoom, 0, 0, zoom, 0, 0);
  ctx.stroke(path);
}
<canvas id="canvas"></canvas>

Is there no way of doing this while using this otherwise convenient Path2D API?

Kaiido
  • 123,334
  • 13
  • 219
  • 285

1 Answers1

4

There is a way to transform a Path2D object by passing a DOMMatrix1 to the Path2D.prototype.addPath method.

So we can actually achieve the same result by passing a transformed copy of our Path2d:

const transformPath = (path, matrix) => {
  const copy = new Path2D();
  copy.addPath(path, matrix);
  return copy;
};
// ...
ctx.stroke( transformPath( path, {a: 1/zoom, d: 1/zoom } );

However, you'll notice that we have to make our path-matrix relatively from the styling one.
The new DOMMatrix API eases matrix transforms a lot2, but it makes this approach definitely more convoluted than the beginPath() way, that's quite unfortunate we can't act on the Path2D object itself or even just have this transform parameter on the constructor too, but that's the only way I know of...

const transformPath = (path, matrix) => {
  const copy = new Path2D();
  copy.addPath(path, matrix);
  return copy;
};

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// define the subpath
const path = new Path2D("M 10 80 Q 52.5 10, 95 80 T 180 80");

let zoom = 1;
let speed = 0.1;
requestAnimationFrame(update);

function update() {
  if( zoom >= 10 || zoom <= 0.1 ) speed *= -1;
  zoom += speed;
  draw();
  requestAnimationFrame(update);
}

function draw() {
  ctx.setTransform(1, 0, 0, 1, 0, 0);
  ctx.clearRect(0,0,canvas.width,canvas.height);  
  // zoom the stylings
  ctx.setTransform(zoom, 0, 0, zoom, 0, 0);
  // create our transformed path
  const invertMatrix = {a: 1/zoom, d: 1/zoom};
  ctx.stroke(transformPath(path, invertMatrix));
}
<canvas id="canvas"></canvas>

1. Actually it doesn't need to be an actual DOMMatrix, any object with its properties will do
2. We can now even use such objects in ctx.setTransform(matrix).

Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • 3
    Total awesome now creating self similar paths for 2D context is a breeze as `addPath` can add to its self `p.addPath(p)` Useless on its own, but with matrix all sorts of fractal like / repeating paths are possible. Simple and fast.. Example, a spiral containing 6000+ rects created with `p = new Path2D; p.rect(0,-5,10,10); i = 12; while (i--) { p.addPath(p, m.multiplySelf(m)) }` with an init matrix `m = new DOMMatrix([0.99,0,0,0.99,6,0]).rotateSelf(0.03);` – Blindman67 Jan 12 '20 at 02:40