1

I used Path2D to draw a negative axis path, offset it with translate, and then created a gradient with createPattern. I found that the fill has an offset, and there is an offset on the Chrome browser, which is normal on the Firefox browser.Why are they different???


Open this demo on Chrome and Firefox https://codepen.io/xiechengjian/pen/vVbNXv

my Chrome version:70.0.3538.67 my Firefox version:63.0

let p = new Path2D();
ctx.translate(100,100)
p.moveTo(-100,-100)
p.lineTo(100,-100);
p.lineTo(100,100);
p.lineTo(-100,100);
p.closePath()
var offCanvas = document.createElement('canvas');
offCanvas.width = 200;
offCanvas.height = 200;
var offCtx=offCanvas.getContext("2d")

offCtx.fillStyle = "red"
offCtx.fillRect(0,0,200,200);
let gradient = ctx.createPattern(offCanvas, "no-repeat");
ctx.fillStyle = gradient
ctx.fill(p)
ctx.stroke(p)

1 Answers1

1

This a Firefox bug.


First I'd like to clear up what I fear is a misconception here. The Path2D object you created is not connected to your 2D context anyhow before you use it with ctx.fill(p); ctx.stroke(p);.
It is only inside these methods that the coordinates of its sub-paths will get transformed to the context's current matrix.
So the ctx.translate(100,100); has no effect on the Path2D object, it will have on both context's methods though.

Also, your Path2D is actually just p.rect(-100, -100, 200, 200). So if we replace all this Path2D code to a simple ctx.rect() call, we can see that the discrepancy you noticed is not related to the Path2D API:

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");

ctx.fillStyle = generatePattern();
ctx.translate(100, 100);
ctx.rect(-100, -100, 200, 200);
ctx.fill();
ctx.stroke();

function generatePattern() {
  var offCanvas = document.createElement('canvas');
  offCanvas.width = 200;
  offCanvas.height = 200;
  var offCtx = offCanvas.getContext("2d");
  offCtx.fillStyle = "red";
  offCtx.fillRect(0, 0, 200, 200);
  return ctx.createPattern(offCanvas, "no-repeat");
}
<canvas id="canvas" width="500" height="250"></canvas>

So what happens, is that the fillStyle (and strokeStyle) property is like an infinitely big layer, itself relative to the context's transform matrix. When you set it to either a CanvasPattern, or a CanvasGradient, this matters since the position of your bitmap will get ruled by this transformation matrix.

Here is a simple example showing how this can be used, to create a moving gradient, while the sub-path remains the same.

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
// we define the gradient only once
ctx.fillStyle = generateGradient();
// we define the sub-path only once
ctx.lineTo(20, 20);
ctx.lineTo(190, 50);
ctx.lineTo(130, 190);
ctx.closePath();

draw({clientX:50, clientY:50});
canvas.onmousemove = draw;

function draw(evt) {
  var rect = canvas.getBoundingClientRect();
  var x = evt.clientX - rect.left - 50;
  var y = evt.clientY - rect.top - 50;
  // clear
  ctx.setTransform(1,0,0,1,0,0);
  ctx.clearRect(0,0,canvas.width,canvas.height);
  // here, all that moves is the fillStyle layer
  ctx.translate(x, y);
  ctx.fill();
  ctx.stroke();
}


function generateGradient() {
  var grad = ctx.createRadialGradient(
    50,
    50,
    0,
    50,
    50,
    25
  ); // a circle whose center is at 50,50, and rad is 25
  grad.addColorStop(0, 'red');
  grad.addColorStop(1, 'green');
  return grad;
}
move your mouse over the triangle
<canvas id="canvas" width="500" height="250"></canvas>

In your case, the pattern you created will get moved by 100px on both x and y axis, hence the top-left corner of your offCanvas will be rendered at pixel 100,100 and since your Path only renders until pixel 200,200, it will should get cropped.

But why does Firefox renders it like there is no offset?
It is entirely a bug of how Firefox renders non-repeating CanvasPatterns.
Using a bitmap image will make it obvious:

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var img = new Image();
img.onload = function() {

  ctx.fillStyle = ctx.createPattern(img, "no-repeat");
  ctx.translate(100, 100);
  ctx.rect(-100, -100, 200, 200);
  ctx.fill();
  ctx.stroke();

};
img.src = 'https://mdn.mozillademos.org/files/222/Canvas_createpattern.png';
<canvas id="canvas" width="500" height="250"></canvas>

Results in my Firefox 64:
The image gets stretched in all directions

So when you used a solid red color as pattern, this bug would actually produce a fully red bitmap, while the correct behavior according to specs is to render transparent black pixels where the pattern algorithm created no results.


For a fix, we'll have to wait for FF to provide it.
But note that in your case, using fillRect will workaround the bug.

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var img = new Image();
img.onload = function() {

  ctx.fillStyle = ctx.createPattern(img, "no-repeat");
  ctx.fillRect(0, 0, 200, 200);
  ctx.strokeRect(0, 0, 200, 200);

};
img.src = 'https://mdn.mozillademos.org/files/222/Canvas_createpattern.png';
<canvas id="canvas" width="500" height="250"></canvas>
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • Thank you for your patience.But in practice the path2D I need to fill contains the negative axis path both x and y axis. so I have to `translate()` to draw all the paths.I can fill it up with `createRadialGradient()` or `createLinearGradient()`,but not with `createPattern()`.In this case ,How can I fill it up with `createPattern()`.Thank you again – xiechengjian Oct 31 '18 at 02:52
  • As I explained, this is a browser bug, and can not be fixed. There is certainly a workaround, but that would be completely dependant on your exact use case. I invite you to write a new question where you would expose the exact case (e.g why it needs to be a non-repeating pattern) in search for a workaround. But for now, I think I gave the answer to "why it differs between Chrome and FF". – Kaiido Oct 31 '18 at 10:11
  • Well,Thank you for solving this problem – xiechengjian Nov 01 '18 at 06:28
  • Let me know when you'll write up your new question, I'd be glad to search for a specific workaround if I've got time. – Kaiido Nov 01 '18 at 06:31
  • Maybe I've found a solution, [see this](https://stackoverflow.com/questions/20253210/canvas-pattern-offset) – xiechengjian Nov 01 '18 at 06:34
  • That will unfortunately not fix FF's bug. Even when the transform is the one of the CanvasPattern rather than the one of the context, they use GL_CLAMP_TO_EDGE, so you'll have the same discrepancy. – Kaiido Nov 01 '18 at 06:46