I have the following function that draws a canvas pattern. It works so slow when canvas size bigger then 512x512.
const defaultConfig: Divot = { step: 8, triangleWidth: 4, radius: 0.6, strokeStyle: '#000' };
export function DivotTemplate(divot: Partial<Divot> = defaultConfig, canvas: HTMLCanvasElement): HTMLCanvasElement {
const config = { ...defaultConfig, ...divot };
const { width, height } = canvas;
const { step, radius, strokeStyle, triangleWidth } = config;
const TAU = 2 * Math.PI;
let x = [1, 1, triangleWidth];
const y = [1, triangleWidth, triangleWidth / 2];
const context = canvas.getContext('2d');
if (!context) throw Error('Canvas context error');
context.strokeStyle = strokeStyle;
/* Рисуем три точки для первого треугольника */
for (let c = 0; c < height; c++) {
if (c % 2 === 0) {
x[0] = triangleWidth + 1;
x[1] = triangleWidth + 1;
x[2] = 1;
} else {
let s = step / 2;
x = [1 + s, 1 + s, triangleWidth + 1 + s];
}
for (let i = 0; i < width; i = i + step) {
context.beginPath();
context.arc(x[0], y[0], radius, 0, TAU);
context.stroke();
context.beginPath();
context.arc(x[1], y[1], radius, 0, TAU);
context.stroke();
context.beginPath();
context.arc(x[2], y[2], radius, 0, TAU);
context.stroke();
/* Соединяем две точки из трех */
context.beginPath();
context.moveTo(x[2], y[2]);
context.lineTo(x[0], y[0]);
context.stroke();
context.beginPath();
context.moveTo(x[2], y[2]);
context.lineTo(x[1], y[1]);
context.stroke();
x[0] = x[0] + step;
x[1] = x[1] + step;
x[2] = x[2] + step;
}
x = [1, 1, triangleWidth + 1];
y[0] = y[0] + step;
y[1] = y[1] + step;
y[2] = y[2] + step;
}
context.stroke();
return canvas;
}
As result I get a canvas pattern like:
I what can I optimize in this code according canvas and loops?
-
\$\begingroup\$ Would be great if you could turn this in to a working Js snippet \$\endgroup\$konijn– konijn2022年02月03日 16:28:27 +00:00Commented Feb 3, 2022 at 16:28
-
1\$\begingroup\$ I published full code there to test it: jsfiddle.net/nbut7jhr/5 \$\endgroup\$Nika– Nika2022年02月03日 16:40:19 +00:00Commented Feb 3, 2022 at 16:40
-
\$\begingroup\$ Also I have added a time start, time end to measure the time execution. jsfiddle.net/nbut7jhr/6 It takes 150 ms \$\endgroup\$Nika– Nika2022年02月03日 17:23:18 +00:00Commented Feb 3, 2022 at 17:23
-
\$\begingroup\$ Do you have any suggestions? \$\endgroup\$Nika– Nika2022年02月03日 17:40:53 +00:00Commented Feb 3, 2022 at 17:40
-
\$\begingroup\$ It's in TS, so it won't work for a stack snippet. \$\endgroup\$ggorlen– ggorlen2022年02月03日 20:53:42 +00:00Commented Feb 3, 2022 at 20:53
1 Answer 1
Optimizing rendering performance
Avoid unnecessary state changes.
beginPath
thenstroke
orfill
requires a GPU state change (move data from CPU controlled RAM to GPU controlled RAM). Reducing the number of times you call begin path will increase the performance of your rendering code.Don't
throw
in performance code. Code that can throw can flag the function asDo Not Optimize
meaning it will be ignored by the built in optimizer.Avoid array indexing in performance code. Accessing an array element is slower than accessing a variable directly.
Avoid repeating calculations inside a loop that can be done once outside the loop.
Basic rewrite.
The rewrite replaces the many beginPath
/ draw calls with one beginPath
and stroke outside the loops (because all the rendering is the same style). This will result in a significant performance increase.
To avoid the connecting strokes between arcs move to the beginning of the arc using moveTo(x + radius, y)
I have changed the signature of the function DivotTemplate
so that the default parameters is at the end.
const defaultConfig = { step: 8, triangleWidth: 4, radius: 0.6, strokeStyle: '#000' };
const now = performance.now();
bivotTemplate(canvas);
console.log("1024 by 1024 canvas in " + performance.now().toFixed(2) + "ms");
function bivotTemplate(canvas, divot = defaultConfig) {
const config = {...defaultConfig, ...divot };
const { width, height } = canvas;
const { step, radius, strokeStyle, triangleWidth } = config;
const TAU = 2 * Math.PI;
const halfStep = step / 2;
var c, i, x0 = 1, x1 = 1, x2 = triangleWidth;
var y0 = 1, y1 = triangleWidth, y2 = triangleWidth / 2;
const ctx = canvas.getContext('2d');
ctx.strokeStyle = strokeStyle;
ctx.beginPath();
for (c = 0; c < height; c++) {
if (c % 2 === 0) {
x0 = triangleWidth + 1;
x1 = triangleWidth + 1;
x2 = 1;
} else {
x0 = 1 + halfStep;
x1 = 1 + halfStep;
x2 = triangleWidth + 1 + halfStep;
}
for (i = 0; i < width; i = i + step) {
ctx.moveTo(x0 + radius, y0);
ctx.arc(x0, y0, radius, 0, TAU);
ctx.moveTo(x1 + radius, y1);
ctx.arc(x1, y1, radius, 0, TAU);
ctx.moveTo(x1 + radius, y1);
ctx.arc(x2, y2, radius, 0, TAU);
ctx.moveTo(x2, y2);
ctx.lineTo(x0, y0);
ctx.moveTo(x2, y2);
ctx.lineTo(x1, y1);
x0 += step;
x1 += step;
x2 += step;
}
y0 += step;
y1 += step;
y2 += step;
}
ctx.stroke();
return canvas;
}
<canvas id="canvas" width = "1024" height = "1024"></canvas>
Real-time
The above code will be much quicker then your original,. However it is still rather slow.
As the rendering is a repeated tile pattern you can increase the performance by 2 (maybe 3) orders of magnitude (real-time) using the 2D pattern.
Render the pattern to a canvas (not on the page) the size of one pattern. The use ctx.createPattern(canvas, repeat)
to create a repeating pattern.
Draw the pattern over the whole canvas using fillRect
. This will be almost as fast as drawing the pattern on a 8 by 8 canvas (in this case)