仅转换 Path2D 的样式

在 canvas 2D API 中,我们可以首先使用一个上下文的转换定义一个子路径,然后只为 fill()stroke() 调用更改该上下文的转换,这会对 stylings 产生影响,例如 fillStylelineWidth 和其他可见属性,但会保留已定义的子路径.当我们想要在保持相同笔划宽度的同时放大矢量形状时,这非常方便.

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.

这是一个简单的例子,其中只有 lineWidth 受变量 zoom 变换的影响:

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>

使用 Path2D API,我们必须通过此子路径直接在 ctx.fill(path)ctx.stroke(path) 方法中.
这意味着我们不能像以前那样将样式与子路径声明分开:

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>

在使用这个原本方便的 Path2D API 时有没有办法做到这一点?

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

推荐答案

有一种方法可以通过传递 DOMMatrix1Path2D.prototype.addPath 方法.

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

所以我们实际上可以通过传递我们的 Path2d 的转换副本来实现相同的结果:

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 } );

但是,您会注意到我们必须使我们的 path-matrix 相对于 样式 之一.
新的 DOMMatrix API 大大简化了矩阵转换2,但它使这种方法肯定比 beginPath() 方式更复杂,很不幸我们无法对Path2D 对象本身,甚至只是在构造函数上也有这个 transform 参数,但这是我知道的唯一方法......

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.实际上它不需要是一个实际的 DOMMatrix,任何具有其属性的对象都可以
2.我们现在甚至可以在 ctx.setTransform(matrix) 中使用这些对象.

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).

相关文章