// Returns an [x, y] point on a circle, with given origin and radius, at a given angle // counter-clockwise from the positive horizontal axis. function circleCoordinates(origin, radius, radians) { return [ origin[0] + Math.cos(radians) * radius, origin[1] + Math.sin(radians) * radius ]; } // Animator handles calling and stopping requestAnimationFrame and keeping track of framerate. class Animator { framesCount = 0; totalFramesMs = 0; animating = false; renderer = null; start() { if (this.animating === false) { this.animating = true; this.framesCount = 0; const frameStartMs = performance.now(); const drawFrame = () => { if (this.animating && this.renderer) { requestAnimationFrame(drawFrame); this.framesCount++; const [x, y] = circleCoordinates([-70, -70], 50, this.framesCount/100); this.renderer.render(x, y); const frameTimeMs = performance.now() - frameStartMs; this.totalFramesMs = frameTimeMs; } }; requestAnimationFrame(drawFrame); } } stop() { this.animating = false; } } // The following three renderers draw a repeating pattern of paths. // The approximate height and width of this repeated pattern is given by PATTERN_BOUNDS: const PATTERN_BOUNDS = 600; // And the spacing of the pattern (distance between repeated paths) is given by PATTERN_SPACING: const PATTERN_SPACING = 70; class SVGRenderer { constructor(svgObjectElement) { this.svgObjectElement = svgObjectElement; this.svgElArray = []; // Create an SVG element for every position in the pattern for (let xo = 0; xo < PATTERN_BOUNDS; xo += PATTERN_SPACING) { for (let yo = 0; yo < PATTERN_BOUNDS; yo += PATTERN_SPACING) { const clonedSVG = svgObjectElement.cloneNode(true); this.svgElArray.push(clonedSVG); svgObjectElement.parentElement.appendChild(clonedSVG); } } } render(x, y) { let i = 0; for (let xo = 0; xo < PATTERN_BOUNDS; xo += PATTERN_SPACING) { for (let yo = 0; yo < PATTERN_BOUNDS; yo += PATTERN_SPACING) { this.svgElArray[i].style.transform = `translate(${x + xo}px, ${y + yo}px)`; i++; } } } } class Path2dRenderer { constructor(svgData, offscreenCanvas) { this.data = svgData.map(([pathString, fillColor]) => [new Path2D(pathString), fillColor]); this.ctx = offscreenCanvas.getContext('2d'); } render(x, y) { const ctx = this.ctx; ctx.clearRect(0, 0, 500, 500); for (let xo = 0; xo < PATTERN_BOUNDS; xo += PATTERN_SPACING) { for (let yo = 0; yo < PATTERN_BOUNDS; yo += PATTERN_SPACING) { ctx.save(); ctx.translate(x + xo, y + yo); for (const [path, fillColor] of this.data) { ctx.fillStyle = fillColor; ctx.fill(path); } ctx.restore(); } } } } class CanvasKitRenderer { constructor(svgData, offscreenCanvas, CanvasKit) { this.CanvasKit = CanvasKit; this.data = svgData.map(([pathString, fillColor]) => [ CanvasKit.Path.MakeFromSVGString(pathString), CanvasKit.parseColorString(fillColor) ]); this.surface = CanvasKit.MakeWebGLCanvasSurface(offscreenCanvas, null); if (!this.surface) { throw 'Could not make canvas surface'; } this.canvas = this.surface.getCanvas(); this.paint = new CanvasKit.Paint(); this.paint.setAntiAlias(true); this.paint.setStyle(CanvasKit.PaintStyle.Fill); } render(x, y) { const canvas = this.canvas; canvas.clear(this.CanvasKit.WHITE); for (let xo = 0; xo < PATTERN_BOUNDS; xo += PATTERN_SPACING) { for (let yo = 0; yo < PATTERN_BOUNDS; yo += PATTERN_SPACING) { canvas.save(); canvas.translate(x + xo, y + yo); for (const [path, color] of this.data) { this.paint.setColor(color); canvas.drawPath(path, this.paint); } canvas.restore(); } } this.surface.flush(); } }