const DEFAULT_METHOD = 'SVG'; const worker = new Worker('worker.js'); const svgObjectElement = document.getElementById('svg'); document.getElementById('svg').addEventListener('load', () => { const svgElement = svgObjectElement.contentDocument; const svgData = svgToPathStringAndFillColorPairs(svgElement); // Send svgData and transfer an offscreenCanvas to the worker for Path2D and CanvasKit rendering const path2dCanvas = document.getElementById('Path2D-canvas').transferControlToOffscreen(); worker.postMessage({ svgData: svgData, offscreenCanvas: path2dCanvas, type: 'Path2D' }, [path2dCanvas]); const canvasKitCanvas = document.getElementById('CanvasKit-canvas').transferControlToOffscreen(); worker.postMessage({ svgData: svgData, offscreenCanvas: canvasKitCanvas, type: 'CanvasKit' }, [canvasKitCanvas]); // The Canvas2D and CanvasKit rendering methods are executed in a web worker to avoid blocking // the main thread. The SVG rendering method is executed in the main thread. SVG rendering is // not in a worker because it is not possible - the DOM cannot be accessed from a web worker. const svgAnimator = new Animator(); svgAnimator.renderer = new SVGRenderer(svgObjectElement); switchRenderMethodCallback(DEFAULT_METHOD)(); // Listen to framerate reports from the worker, and update framerate text worker.addEventListener('message', ({ data: {renderMethod, framesCount, totalFramesMs} }) => { const fps = fpsFromFramesInfo(framesCount, totalFramesMs); let textEl; if (renderMethod === 'Path2D') { textEl = document.getElementById('Path2D-fps'); } if (renderMethod === 'CanvasKit') { textEl = document.getElementById('CanvasKit-fps'); } textEl.innerText = `${fps.toFixed(2)} fps over ${framesCount} frames`; }); // Update framerate text every second setInterval(() => { if (svgAnimator.framesCount > 0) { const fps = fpsFromFramesInfo(svgAnimator.framesCount, svgAnimator.totalFramesMs); document.getElementById('SVG-fps').innerText = `${fps.toFixed(2)} fps over ${svgAnimator.framesCount} frames`; } }, 1000); document.getElementById('SVG-input') .addEventListener('click', switchRenderMethodCallback('SVG')); document.getElementById('Path2D-input') .addEventListener('click', switchRenderMethodCallback('Path2D')); document.getElementById('CanvasKit-input') .addEventListener('click', switchRenderMethodCallback('CanvasKit')); function switchRenderMethodCallback(switchMethod) { return () => { // Hide all renderer elements and stop svgAnimator document.getElementById('CanvasKit-canvas').style.visibility = 'hidden'; document.getElementById('Path2D-canvas').style.visibility = 'hidden'; for (const svgEl of svgAnimator.renderer.svgElArray) { svgEl.style.visibility = 'hidden'; } svgAnimator.stop(); // Show only the active renderer element if (switchMethod === 'SVG') { svgAnimator.start(); for (const svgEl of svgAnimator.renderer.svgElArray) { svgEl.style.visibility = 'visible'; } } if (switchMethod === 'CanvasKit') { document.getElementById('CanvasKit-canvas').style.visibility = 'visible'; } if (switchMethod === 'Path2D') { document.getElementById('Path2D-canvas').style.visibility = 'visible'; } worker.postMessage({ switchMethod }); }; } }); // Add .data after the load listener so that the listener always fires an event svgObjectElement.data = 'garbage.svg'; const EMPTY_SVG_PATH_STRING = 'M 0 0'; const COLOR_WHITE = '#000000'; function svgToPathStringAndFillColorPairs(svgElement) { const pathElements = Array.from(svgElement.getElementsByTagName('path')); return pathElements.map((path) => [ path.getAttribute('d') ?? EMPTY_SVG_PATH_STRING, path.getAttribute('fill') ?? COLOR_WHITE ]); } const MS_IN_A_SECOND = 1000; function fpsFromFramesInfo(framesCount, totalFramesMs) { const averageFrameTime = totalFramesMs / framesCount; return (1 / averageFrameTime) * MS_IN_A_SECOND; }