srkleiman
srkleiman

Reputation: 727

scaling canvas without distorting lineWidth or destroying clip

I'd like to scale one axis in a drawing. As others have pointed out, naively doing this will also scale lineWidth when the path is stroked. Other solutions have also pointed out that you can do the following to avoid distorting the lineWidth:

ctx.save();
ctx.scale(1, 2);
ctx.beginPath();
// draw shape
ctx.restore();
ctx.stroke();

Unfortunately, I also need a complex clipping region and the restore() destroys the clipping region. I tried simply restoring the scale:

ctx.save();
ctx.scale(1, 2);
ctx.beginPath();
// draw shape
ctx.scale(1, 1);
ctx.stroke();

But that did not work and the lineWidth remained distorted.

This is modeling a physical system and the shapes involved are both arcs and lines. It is important that the intersections of the various shapes be accurate, so doing without a clip and computing all the possible intersections is much harder. Also simply doing without the scale would force elliptical curves, which I'm trying to avoid, but that's the last resort.

Any ideas?

Upvotes: 0

Views: 55

Answers (1)

Kaiido
Kaiido

Reputation: 136627

I'm not too clear as to what you are doing exactly, and I guess you could avoid clipping by using compositing instead, which offers better performance, and cleaner results most of the time.

But anyway, let's assume you absolutely want to clip...


ctx.save() will stack the saved states in an internal ArrayLike object. Each call to restore() will only restore the popped out element (i.e the newest one in the stack).

So you can very well restore the state before scaling but after clipping by simply calling ctx.save() in between these two operations.

ctx = c.getContext('2d');
ctx.save(); // stack a first state without clipping, just in case
// define our clipping region
ctx.arc(53,50,30,0,Math.PI*2);
ctx.stroke();
ctx.clip();

// draw once at normal scale
drawShape();
ctx.strokeStyle = 'blue';
ctx.stroke();

// save our context state before we scale
ctx.save();
// now draw scaled
ctx.scale(2, 1);
drawShape();
// restore to before we applied the scale
ctx.restore();
ctx.strokeStyle = 'red';
ctx.stroke();

// restore before clipping
ctx.restore();
// next drawings would be unclipped...

function drawShape() {
  ctx.beginPath();
  ctx.rect(25, 30, 20, 20);
}
<canvas id="c"></cavnas>



Now, you don't even have to manage this mess of context states, and can simply restore the context's transformation matrix.

In your code, you where using ctx.scale(1, 1). This is a no-op (won't do anything). ctx.scale(factor, factor) will multiply the current scale values by the factors you passed. So since n*1=n, that won't do anything.

So yes, you could calculate the inverse factor you'd need to pass in there (like in your example, it would be ctx.scale(1, 0.5)), but the easiest solution is to use the absolute setTransform(xScale, xSkew, ySkew, yScale, xTranslate, yTranslate) method.
To reset the context matrix to its defaults, you just need to remember ctx.setTransform(1,0,0,1,0,0);.

ctx = c.getContext('2d');
// as a bonus, we will use compositing instead of clipping
// but this has no incidence on the demo

// draw once at normal scale
drawShape();
ctx.strokeStyle = 'blue';
ctx.stroke();

// now draw scaled
ctx.scale(2, 1);
drawShape();
// restore the context's matrix
ctx.setTransform(1,0,0,1,0,0);
ctx.strokeStyle = 'red';
ctx.stroke();

// clipping (compositing)
// only the pixels that are currenlty on the context,
// and whose position will match with one of the to be drawn will be kept
ctx.globalCompositeOperation = 'destination-in';
ctx.beginPath();
ctx.arc(53,50,30,0,Math.PI*2);
ctx.fill();
// restore to default
ctx.globalCompositeOperation = 'source-over';
// just to show the clipped area
ctx.strokeStyle = '#000';
ctx.stroke();

function drawShape() {
  ctx.beginPath();
  ctx.rect(25, 30, 20, 20);
}
<canvas id="c"></canvas>

Upvotes: 1

Related Questions