Import Cobalt 25.master.0.1033734
diff --git a/third_party/skia/demos.skia.org/demos/hello_world/index.html b/third_party/skia/demos.skia.org/demos/hello_world/index.html
new file mode 100644
index 0000000..5a0a9ca
--- /dev/null
+++ b/third_party/skia/demos.skia.org/demos/hello_world/index.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<title>Hello World Demo</title>
+<meta charset="utf-8" />
+<meta http-equiv="X-UA-Compatible" content="IE=edge">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<script type="text/javascript" src="https://unpkg.com/canvaskit-wasm@0.25.0/bin/full/canvaskit.js"></script>
+
+<style>
+ canvas {
+ border: 1px dashed grey;
+ }
+</style>
+
+<body>
+ <h1>Hello world</h1>
+
+ <canvas id=draw width=500 height=500></canvas>
+</body>
+
+<script type="text/javascript" charset="utf-8">
+ const ckLoaded = CanvasKitInit({ locateFile: (file) => 'https://unpkg.com/canvaskit-wasm@0.25.0/bin/full/' + file });
+
+ ckLoaded.then((CanvasKit) => {
+ const surface = CanvasKit.MakeCanvasSurface('draw');
+ if (!surface) {
+ throw 'Could not make surface';
+ }
+
+ const paint = new CanvasKit.Paint();
+ paint.setColor(CanvasKit.RED);
+
+ const textPaint = new CanvasKit.Paint();
+ const textFont = new CanvasKit.Font(null, 20);
+
+ function drawFrame(canvas) {
+ canvas.drawRect(CanvasKit.LTRBRect(10, 10, 50, 50), paint);
+ canvas.drawText('If you see this, CanvasKit loaded!!', 5, 100, textPaint, textFont);
+ }
+ surface.requestAnimationFrame(drawFrame);
+ });
+
+</script>
\ No newline at end of file
diff --git a/third_party/skia/demos.skia.org/demos/image_decode_web_worker/index.html b/third_party/skia/demos.skia.org/demos/image_decode_web_worker/index.html
new file mode 100644
index 0000000..36636cb
--- /dev/null
+++ b/third_party/skia/demos.skia.org/demos/image_decode_web_worker/index.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<title>Image Decoding Demo</title>
+<meta charset="utf-8" />
+<meta http-equiv="X-UA-Compatible" content="IE=edge">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+
+<style>
+ canvas {
+ border: 1px dashed grey;
+ }
+
+ .canvas-container {
+ float: left;
+ }
+</style>
+
+<body>
+ <h1>CanvasKit loading images in a webworker (using browser-based decoders)</h1>
+ <p>NOTE: this demo currently only works in chromium-based browsers, where
+ <a href="https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas#Browser_compatibility">
+ Offscreen Canvas
+ </a>
+ is supported.
+ </p>
+
+ <div class="canvas-container">
+ <h2>Decoding on main thread</h2>
+ <canvas id="main-thread-canvas" width=500 height=500></canvas>
+ <div>
+ <button id="load-button-main">Decode Image on Main Thread</button>
+ <button id="load-button-web">Decode Image with Web Worker</button>
+ <button id="clear-button">Clear Image</button>
+ </div>
+ <p>
+ Notice that decoding the image on the main thread pauses the circle animation until the
+ image is ready to be drawn, where as decoding it in a webworker does not have this pause
+ (or at least not as drastic a pause). You may want to reload the page, as browsers are
+ smart enough to not have to re-decode the image on subsequent requests.
+ </p>
+ </div>
+
+</body>
+<script type="text/javascript" src="https://unpkg.com/canvaskit-wasm@0.25.0/bin/full/canvaskit.js"></script>
+<script type="text/javascript" src="main.js"></script>
\ No newline at end of file
diff --git a/third_party/skia/demos.skia.org/demos/image_decode_web_worker/main.js b/third_party/skia/demos.skia.org/demos/image_decode_web_worker/main.js
new file mode 100644
index 0000000..aef94bc
--- /dev/null
+++ b/third_party/skia/demos.skia.org/demos/image_decode_web_worker/main.js
@@ -0,0 +1,82 @@
+// Inspired by https://gist.github.com/ahem/d19ee198565e20c6f5e1bcd8f87b3408
+const worker = new Worker('worker.js');
+
+const canvasKitInitPromise =
+ CanvasKitInit({locateFile: (file) => 'https://unpkg.com/canvaskit-wasm@0.25.0/bin/full/'+file});
+
+const bigImagePromise =
+ fetch('https://upload.wikimedia.org/wikipedia/commons/3/30/Large_Gautama_Buddha_statue_in_Buddha_Park_of_Ravangla%2C_Sikkim.jpg')
+ .then((response) => response.blob());
+
+Promise.all([
+ canvasKitInitPromise,
+ bigImagePromise
+]).then(([
+ CanvasKit,
+ imageBlob
+]) => {
+ const surface = CanvasKit.MakeWebGLCanvasSurface('main-thread-canvas', null);
+ if (!surface) {
+ throw 'Could not make main thread canvas surface';
+ }
+
+ const paint = new CanvasKit.Paint();
+ paint.setColor(CanvasKit.RED);
+
+ let decodedImage;
+ // This animation draws a red circle oscillating from the center of the canvas.
+ // It is there to show the lag introduced by decoding the image on the main
+ // thread.
+ const drawFrame = (canvas) => {
+ canvas.clear(CanvasKit.WHITE);
+
+ if (decodedImage) {
+ canvas.drawImageRect(decodedImage,
+ CanvasKit.LTRBRect(0, 0, 3764, 5706), // original size of the image
+ CanvasKit.LTRBRect(0, 0, 500, 800), // scaled down
+ null); // no paint needed
+ }
+ canvas.drawCircle(250, 250, 200 * Math.abs(Math.sin(Date.now() / 1000)), paint);
+ surface.requestAnimationFrame(drawFrame);
+ };
+ surface.requestAnimationFrame(drawFrame);
+
+
+ document.getElementById('load-button-main').addEventListener('click', () => {
+ if (decodedImage) {
+ decodedImage.delete();
+ decodedImage = null;
+ }
+ const imgBitmapPromise = createImageBitmap(imageBlob);
+ imgBitmapPromise.then((imgBitmap) => {
+ decodedImage = CanvasKit.MakeImageFromCanvasImageSource(imgBitmap);
+ });
+ });
+
+ document.getElementById('load-button-web').addEventListener('click', () => {
+ if (decodedImage) {
+ decodedImage.delete();
+ decodedImage = null;
+ }
+ worker.postMessage(imageBlob);
+ });
+ worker.addEventListener('message', (e) => {
+ const decodedBuffer = e.data.decodedArrayBuffer;
+ const pixels = new Uint8Array(decodedBuffer);
+ decodedImage = CanvasKit.MakeImage({
+ width: e.data.width,
+ height: e.data.height,
+ alphaType: CanvasKit.AlphaType.Unpremul,
+ colorType: CanvasKit.ColorType.RGBA_8888,
+ colorSpace: CanvasKit.ColorSpace.SRGB
+ }, pixels, 4 * e.data.width);
+ });
+ document.getElementById('clear-button').addEventListener('click', () => {
+ if (decodedImage) {
+ decodedImage.delete();
+ decodedImage = null;
+ }
+ });
+});
+
+
diff --git a/third_party/skia/demos.skia.org/demos/image_decode_web_worker/worker.js b/third_party/skia/demos.skia.org/demos/image_decode_web_worker/worker.js
new file mode 100644
index 0000000..c5e17c3
--- /dev/null
+++ b/third_party/skia/demos.skia.org/demos/image_decode_web_worker/worker.js
@@ -0,0 +1,23 @@
+// This worker listens for a message that is the blob of data that is an encoded image.
+// In principle, this worker could also load the image, but I didn't want to introduce
+// network lag in this comparison. When it has decoded the image and converted it into
+// unpremul bytes, it returns the width, height, and the pixels in an array buffer via
+// a worker message.
+self.addEventListener('message', (e) => {
+ const blob = e.data;
+ createImageBitmap(blob).then((bitmap) => {
+ const oCanvas = new OffscreenCanvas(bitmap.width, bitmap.height);
+ const ctx2d = oCanvas.getContext('2d');
+ ctx2d.drawImage(bitmap, 0, 0);
+
+ const imageData = ctx2d.getImageData(0, 0, bitmap.width, bitmap.height);
+ const arrayBuffer = imageData.data.buffer;
+ self.postMessage({
+ width: bitmap.width,
+ height: bitmap.height,
+ decodedArrayBuffer: arrayBuffer
+ }, [
+ arrayBuffer // give up ownership of this object
+ ]);
+ });
+});
\ No newline at end of file
diff --git a/third_party/skia/demos.skia.org/demos/image_sampling/index.html b/third_party/skia/demos.skia.org/demos/image_sampling/index.html
new file mode 100644
index 0000000..6626630
--- /dev/null
+++ b/third_party/skia/demos.skia.org/demos/image_sampling/index.html
@@ -0,0 +1,143 @@
+<!DOCTYPE html>
+<title>Custom Image Upscaling</title>
+<meta charset="utf-8" />
+<meta http-equiv="X-UA-Compatible" content="IE=edge">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<script type="text/javascript" src="https://unpkg.com/canvaskit-wasm@0.25.0/bin/full/canvaskit.js"></script>
+
+<style>
+ figcaption {
+ max-width: 800px;
+ }
+</style>
+
+<body>
+ <h1>Custom Image Upscaling</h1>
+
+ <div class="slidecontainer">
+ <input type="range" min="0" max="1" value="0" step="0.01" class="slider" id="sharpen"
+ title="sharpen coefficient: 0 means nearest neighbor.">
+ <input type="range" min="0" max="1" value="0.3" step="0.01" class="slider" id="cubic_B"
+ title="cubic B">
+ <input type="range" min="0" max="1" value="0.3" step="0.01" class="slider" id="cubic_C"
+ title="cubic C">
+ </div>
+
+ <figure>
+ <canvas id=draw width=820 height=820></canvas>
+ <figcaption>
+ This demo shows off a custom image upscaling algorithm written in SkSL. The algorithm
+ can be between nearest neighbor and linear interpolation, depending if the value of the
+ sharpen (i.e. the first) slider is 0 or 1, respectively. The upper left quadrant shows
+ the results of a 100x zoom in on a 4 pixel by 4 pixel image of random colors with this
+ algorithm. The lower left is the same algorithm with a smoothing curve applied.
+ <br>
+ For comparison, the upper right shows a stock linear interpolation and the lower right
+ shows a cubic interpolation with the B and C values controlled by the two remaining
+ sliders.
+ </figcaption>
+ </figure>
+
+</body>
+
+<script type="text/javascript" charset="utf-8">
+ const ckLoaded = CanvasKitInit({ locateFile: (file) => 'https://unpkg.com/canvaskit-wasm@0.25.0/bin/full/' + file });
+
+ ckLoaded.then((CanvasKit) => {
+ if (!CanvasKit.RuntimeEffect) {
+ throw 'Need RuntimeEffect';
+ }
+ const surface = CanvasKit.MakeCanvasSurface('draw');
+ if (!surface) {
+ throw 'Could not make surface';
+ }
+
+ const prog = `
+ uniform shader image;
+ uniform float sharp; // 1/m 0 --> NN, 1 --> Linear
+ uniform float do_smooth; // bool
+
+ float2 smooth(float2 t) {
+ return t * t * (3.0 - 2.0 * t);
+ }
+
+ float2 sharpen(float2 w) {
+ return saturate(sharp * (w - 0.5) + 0.5);
+ }
+
+ half4 main(float2 p) {
+ half4 pa = image.eval(float2(p.x-0.5, p.y-0.5));
+ half4 pb = image.eval(float2(p.x+0.5, p.y-0.5));
+ half4 pc = image.eval(float2(p.x-0.5, p.y+0.5));
+ half4 pd = image.eval(float2(p.x+0.5, p.y+0.5));
+ float2 w = sharpen(fract(p + 0.5));
+ if (do_smooth > 0) {
+ w = smooth(w);
+ }
+ return mix(mix(pa, pb, w.x), mix(pc, pd, w.x), w.y);
+ }
+ `;
+ const effect = CanvasKit.RuntimeEffect.Make(prog);
+
+ const paint = new CanvasKit.Paint();
+ // image is a 4x4 image of 16 random colors. This very small image will be upscaled
+ // through various techniques.
+ const image = function() {
+ const surf = CanvasKit.MakeSurface(4, 4);
+ const c = surf.getCanvas();
+ for (let y = 0; y < 4; y++) {
+ for (let x = 0; x < 4; x++) {
+ paint.setColor([Math.random(), Math.random(), Math.random(), 1]);
+ c.drawRect(CanvasKit.LTRBRect(x, y, x+1, y+1), paint);
+ }
+ }
+ return surf.makeImageSnapshot();
+ }();
+
+ const imageShader = image.makeShaderOptions(CanvasKit.TileMode.Clamp,
+ CanvasKit.TileMode.Clamp,
+ CanvasKit.FilterMode.Nearest,
+ CanvasKit.MipmapMode.None);
+
+ sharpen.oninput = () => { surface.requestAnimationFrame(drawFrame); };
+ cubic_B.oninput = () => { surface.requestAnimationFrame(drawFrame); };
+ cubic_C.oninput = () => { surface.requestAnimationFrame(drawFrame); };
+
+ const drawFrame = function(canvas) {
+ const v = sharpen.valueAsNumber;
+ const m = 1/Math.max(v, 0.00001);
+ const B = cubic_B.valueAsNumber;
+ const C = cubic_C.valueAsNumber;
+
+ canvas.save();
+ // Upscale all drawing by 100x; This is big enough to make the differences in technique
+ // more obvious.
+ const scale = 100;
+ canvas.scale(scale, scale);
+
+ // Upper left, draw image using an algorithm (written in SkSL) between nearest neighbor and
+ // linear interpolation with no smoothing.
+ paint.setShader(effect.makeShaderWithChildren([m, 0], [imageShader], null));
+ canvas.drawRect(CanvasKit.LTRBRect(0, 0, 4, 4), paint);
+
+ // Lower left, draw image using an algorithm (written in SkSL) between nearest neighbor and
+ // linear interpolation with smoothing enabled.
+ canvas.save();
+ canvas.translate(0, 4.1);
+ paint.setShader(effect.makeShaderWithChildren([m, 1], [imageShader], null));
+ canvas.drawRect(CanvasKit.LTRBRect(0, 0, 4, 4), paint);
+ canvas.restore();
+
+ // Upper right, draw image with built-in linear interpolation.
+ canvas.drawImageOptions(image, 4.1, 0, CanvasKit.FilterMode.Linear, CanvasKit.MipmapMode.None, null);
+
+ // Lower right, draw image with configurable cubic interpolation.
+ canvas.drawImageCubic(image, 4.1, 4.1, B, C, null);
+
+ canvas.restore();
+ };
+
+ surface.requestAnimationFrame(drawFrame);
+ });
+
+</script>
diff --git a/third_party/skia/demos.skia.org/demos/path_performance/garbage.svg b/third_party/skia/demos.skia.org/demos/path_performance/garbage.svg
new file mode 100644
index 0000000..7ccc45a
--- /dev/null
+++ b/third_party/skia/demos.skia.org/demos/path_performance/garbage.svg
@@ -0,0 +1,21 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
+ <path
+ fill="#3A5"
+ d="
+ M 7, 50
+ a 43,43 0 1,0 86,0
+ a 43,43 0 1,0 -86,0
+ "
+ />
+ <path
+ d="
+ M 53, 23
+ a 6,6 0 1,0 12,0
+ a 6,6 0 1,0 -12,0
+ "
+ />
+ <path d="M36,36c5,0,3,2,8-1c1,2,1,3,3,2c3,0-6,7-3,8c-4-2-9,2-14-2c4-3,4-4,5-7c5,0,8,2,12,1"/>
+ <path fill="#000" d="M34,29h31c2,5,7,10,7,16l-8,1l8,1l-3,31l-5,-18l-11,18l5-34l-3-8z"/>
+ <path stroke-width="2" d="M27,48h23M28,49h21l-3,28h-14l-4,-28h5l3,28h3v-28h5l-2,28m3-4h-13m-1-5h16m0-5h-16m-1-5h18m0-5h-19"/>
+ <path stroke="#F00" stroke-width="1"/>
+</svg>
diff --git a/third_party/skia/demos.skia.org/demos/path_performance/index.html b/third_party/skia/demos.skia.org/demos/path_performance/index.html
new file mode 100644
index 0000000..b96d465
--- /dev/null
+++ b/third_party/skia/demos.skia.org/demos/path_performance/index.html
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<title>CanvasKit Path Rendering Performance Demo</title>
+<meta charset="utf-8" />
+<meta http-equiv="X-UA-Compatible" content="IE=edge">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+
+<style>
+ html, body {
+ max-width: 700px;
+ }
+
+ #result-container {
+ border: 1px dashed grey;
+ height: 500px;
+ width: 500px;
+ position: relative;
+ overflow: hidden;
+ }
+
+ canvas {
+ visibility: hidden;
+ position: absolute;
+ left: 0;
+ top: 0;
+ }
+ object {
+ visibility: hidden;
+ position: absolute;
+ height: 500px;
+ width: 500px;
+ left: 0;
+ top: 0;
+ }
+
+ th {
+ text-align: left;
+ width: 33%;
+ }
+ td {
+ padding: 12px;
+ color: #555;
+ font-style: italic;
+ height: 80px;
+ }
+ table {
+ width: 100%;
+ }
+</style>
+
+<body>
+ <h1>CanvasKit Path Rendering Performance Demo</h1>
+ <p>NOTE: this demo currently only works in chromium-based browsers, where
+ <a href="https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas#Browser_compatibility">
+ Offscreen Canvas
+ </a>
+ is supported.
+ </p>
+
+
+ <h2>1. Choose a rendering method</h2>
+ <table>
+ <tr>
+ <th>
+ <input type="radio" id="SVG-input" name="rendermethod" checked>
+ <label for="SVG-input">SVG</label>
+ </th>
+ <th>
+ <input type="radio" id="Path2D-input" name="rendermethod">
+ <label for="Path2D-input">
+ <a href="https://developer.mozilla.org/en-US/docs/Web/API/Path2D">Path2D API</a>
+ </label>
+ </th>
+ <th>
+ <input type="radio" id="CanvasKit-input" name="rendermethod">
+ <label for="CanvasKit-input">CanvasKit</label>
+ </th>
+ </tr>
+ <tr>
+ <td id="SVG-fps">Choose this rendering method to collect data on its performance...</td>
+ <td id="Path2D-fps">Choose this rendering method to collect data on its performance...</td>
+ <td id="CanvasKit-fps">Choose this rendering method to collect data on its performance...</td>
+ </tr>
+ </table>
+
+ <h2>2. View the result</h2>
+ <div id="result-container">
+ <!-- Arbitrary svg for testing. Source: https://dev.w3.org/SVG/tools/svgweb/samples/svg-files-->
+ <object type="image/svg+xml" id="svg">
+ Garbage pictograph
+ </object>
+ <canvas id="Path2D-canvas" height=500 width=500></canvas>
+ <canvas id="CanvasKit-canvas" height=500 width=500></canvas>
+ </div>
+</body>
+<script type="text/javascript" src="https://unpkg.com/canvaskit-wasm@0.25.0/bin/full/canvaskit.js"></script>
+<script type="text/javascript" src="shared.js"></script>
+<script type="text/javascript" src="main.js"></script>
diff --git a/third_party/skia/demos.skia.org/demos/path_performance/main.js b/third_party/skia/demos.skia.org/demos/path_performance/main.js
new file mode 100644
index 0000000..d618bfb
--- /dev/null
+++ b/third_party/skia/demos.skia.org/demos/path_performance/main.js
@@ -0,0 +1,106 @@
+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;
+}
diff --git a/third_party/skia/demos.skia.org/demos/path_performance/shared.js b/third_party/skia/demos.skia.org/demos/path_performance/shared.js
new file mode 100644
index 0000000..134ad04
--- /dev/null
+++ b/third_party/skia/demos.skia.org/demos/path_performance/shared.js
@@ -0,0 +1,140 @@
+// 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();
+ }
+}
diff --git a/third_party/skia/demos.skia.org/demos/path_performance/worker.js b/third_party/skia/demos.skia.org/demos/path_performance/worker.js
new file mode 100644
index 0000000..692c055
--- /dev/null
+++ b/third_party/skia/demos.skia.org/demos/path_performance/worker.js
@@ -0,0 +1,54 @@
+importScripts('https://unpkg.com/canvaskit-wasm@0.25.0/bin/full/canvaskit.js');
+importScripts('shared.js');
+
+const CanvasKitPromise =
+ CanvasKitInit({locateFile: (file) => 'https://unpkg.com/canvaskit-wasm@0.25.0/bin/full/'+file});
+
+const path2dAnimator = new Animator();
+const canvasKitAnimator = new Animator();
+addEventListener('message', async ({ data: {svgData, offscreenCanvas, type, switchMethod} }) => {
+ // The worker expect to receive 2 messages after initialization: One with an offscreenCanvas
+ // for Path2D rendering, and one with an offscreenCanvas for CanvasKit rendering.
+ if (svgData && offscreenCanvas && type) {
+ if (type === 'Path2D') {
+ path2dAnimator.renderer =
+ new Path2dRenderer(svgData, offscreenCanvas);
+ }
+ if (type === 'CanvasKit') {
+ const CanvasKit = await CanvasKitPromise;
+ canvasKitAnimator.renderer =
+ new CanvasKitRenderer(svgData, offscreenCanvas, CanvasKit);
+ }
+ }
+ // The worker receives a "switchMethod" message whenever the user clicks a rendering
+ // method button on the web page.
+ if (switchMethod) {
+ // Pause other renderers and start correct renderer
+ canvasKitAnimator.stop();
+ path2dAnimator.stop();
+
+ if (switchMethod === 'Path2D') {
+ path2dAnimator.start();
+ } else if (switchMethod === 'CanvasKit') {
+ canvasKitAnimator.start();
+ }
+ }
+});
+
+// Report framerates of Path2D and CanvasKit rendering back to main.js
+setInterval(() => {
+ if (path2dAnimator.framesCount > 0) {
+ postMessage({
+ renderMethod: 'Path2D',
+ framesCount: path2dAnimator.framesCount,
+ totalFramesMs: path2dAnimator.totalFramesMs
+ });
+ }
+ if (canvasKitAnimator.framesCount > 0) {
+ postMessage({
+ renderMethod: 'CanvasKit',
+ framesCount: canvasKitAnimator.framesCount,
+ totalFramesMs: canvasKitAnimator.totalFramesMs
+ });
+ }
+}, 1000);
diff --git a/third_party/skia/demos.skia.org/demos/sampling_types/index.html b/third_party/skia/demos.skia.org/demos/sampling_types/index.html
new file mode 100644
index 0000000..c9f35c7
--- /dev/null
+++ b/third_party/skia/demos.skia.org/demos/sampling_types/index.html
@@ -0,0 +1,195 @@
+<!DOCTYPE html>
+<title>Image sampling techniques</title>
+<meta charset="utf-8" />
+<meta http-equiv="X-UA-Compatible" content="IE=edge">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<script type="text/javascript" src="https://unpkg.com/canvaskit-wasm@0.25.0/bin/full/canvaskit.js"></script>
+
+<style>
+ figcaption {
+ max-width: 800px;
+ }
+</style>
+
+<body>
+ <h1>Image sampling techniques</h1>
+
+ <div class="slidecontainer">
+ <input style="width:400px;" type="range" min="-10" max="10" value="0" step="0.01"
+ class="slider" id="scale_slider" title="Scale">
+ </div>
+
+ <figure>
+ <div ondrop="dropHandler(event);" ondragover="dragOverHandler(event);">
+ <canvas id="draw" width=2048 height=1600></canvas>
+ </div>
+ <figcaption>
+ Drop an Image onto the page (above)
+ </figcaption>
+ </figure>
+
+</body>
+
+<script type="text/javascript" charset="utf-8">
+ function preventScrolling(elem) {
+ elem.addEventListener('touchmove', (e) => {
+ // Prevents touch events in the element from scrolling.
+ e.preventDefault();
+ e.stopPropagation();
+ });
+ }
+
+ function dragOverHandler(ev) { ev.preventDefault(); }
+
+ // these can be changed with click/drag on the primary rectangle
+ let cell_width = 256,
+ cell_height = 256;
+
+ let image = null;
+ let CK = null;
+ let surface = null;
+ let drawFrame = null;
+
+ function dropHandler(ev) {
+ ev.preventDefault();
+
+ let file = null;
+ if (ev.dataTransfer.items) {
+ // Use DataTransferItemList interface to access the file(s)
+ for (item of ev.dataTransfer.items) {
+ // If dropped items aren't files, reject them
+ if (item.kind === 'file') {
+ file = item.getAsFile();
+ break;
+ }
+ }
+ } else {
+ // Use DataTransfer interface to access the file(s)
+ for (f of ev.dataTransfer.files) {
+ file = f;
+ break;
+ }
+ }
+ if (file) {
+ file.arrayBuffer().then(buffer => {
+ image = CK.MakeImageFromEncoded(buffer);
+ surface.requestAnimationFrame(drawFrame);
+ });
+ }
+ }
+
+ const ckLoaded = CanvasKitInit({ locateFile: (file) => 'https://unpkg.com/canvaskit-wasm@0.25.0/bin/full/' + file });
+
+ ckLoaded.then((_CK) => {
+ CK = _CK;
+ surface = CK.MakeCanvasSurface('draw');
+ if (!surface) {
+ throw 'Could not make surface';
+ }
+
+ const font = new CK.Font(null, 15);
+ const paint = new CK.Paint();
+ const textPaint = new CK.Paint();
+ const TX = 20,
+ TY = 30; // where we start the first rectangle
+
+ scale_slider.oninput = () => { surface.requestAnimationFrame(drawFrame); };
+
+ m33_scaled = function(sx, sy) {
+ if (!sy) sy = sx;
+ let m = new Float32Array(9);
+ m[0] = sx; m[4] = sy; m[8] = 1;
+ return m;
+ }
+
+ drawFrame = function(canvas) {
+ if (!image) return;
+
+ const scale = scale_slider.valueAsNumber < 0 ? 1 / (1 - scale_slider.valueAsNumber)
+ : (1 + scale_slider.valueAsNumber);
+
+ const bounds = [0, 0, cell_width, cell_height];
+
+ const lm = m33_scaled(scale, scale);
+
+ const samplings = [
+ [CK.FilterMode.Nearest, CK.MipmapMode.None, 0, 0, "Nearest"],
+ [CK.FilterMode.Linear, CK.MipmapMode.None, 0, 0, "Bilerp"],
+ [CK.FilterMode.Linear, CK.MipmapMode.Linear, 0, 0, "Trilerp"],
+ [null, null, 0, 0.5, "CatmullRom"],
+ [null, null, 1/3.0, 1/3.0, "Mitchell"],
+ ];
+
+ const tile = CK.TileMode.Repeat;
+
+ canvas.save();
+ canvas.translate(TX, TY);
+ canvas.save();
+ for (i in samplings) {
+ if (i == 3) {
+ canvas.restore();
+ canvas.translate(0, bounds[3] - bounds[1] + 30);
+ canvas.save();
+ }
+
+ const s = samplings[i];
+ const shader = s[0] ? image.makeShaderOptions(tile, tile, s[0], s[1], lm)
+ : image.makeShaderCubic( tile, tile, s[2], s[3], lm);
+ paint.setShader(shader);
+ canvas.drawRect(bounds, paint);
+ shader.delete();
+
+ canvas.drawText(s[4], 20, -8, textPaint, font);
+
+ canvas.translate(bounds[2] - bounds[0] + 30, 0);
+ }
+ canvas.restore();
+ canvas.restore();
+ // draw the drag handle
+ if (true) {
+ canvas.save();
+ paint.setShader(null);
+ paint.setColor(CK.Color4f(1, 0, 0));
+ paint.setStrokeWidth(2);
+ canvas.translate(TX + cell_width + 4, TY + cell_height + 4);
+ canvas.drawLine(-12, 0, 0, 0, paint);
+ canvas.drawLine( 0, -12, 0, 0, paint);
+ canvas.restore()
+ }
+ };
+
+ // register our mouse handler
+ {
+ function len2(x, y) {
+ return x*x + y*y;
+ }
+ function hit_test(x,y, x1,y1) {
+ return len2(x-x1, y-y1) <= 10*10;
+ }
+
+ let do_drag = false;
+ function pointer_up(e) {
+ do_drag = false;
+ }
+ function pointer_down(e) {
+ do_drag = hit_test(TX+cell_width, TY+cell_height, e.offsetX, e.offsetY);
+ }
+ function pointer_move(e) {
+ if (e.pressure && do_drag) {
+ cell_width = Math.max(e.offsetX - TX, 32);
+ cell_height = Math.max(e.offsetY - TY, 32);
+ surface.requestAnimationFrame(drawFrame);
+ }
+ }
+
+ const elem = document.getElementById('draw');
+ elem.addEventListener('pointermove', pointer_move);
+ elem.addEventListener('pointerdown', pointer_down);
+ elem.addEventListener('pointerup', pointer_up);
+ preventScrolling(elem);
+ }
+
+ surface.requestAnimationFrame(drawFrame);
+ });
+
+</script>
diff --git a/third_party/skia/demos.skia.org/demos/spreadsheet/index.html b/third_party/skia/demos.skia.org/demos/spreadsheet/index.html
new file mode 100644
index 0000000..0a5ce27
--- /dev/null
+++ b/third_party/skia/demos.skia.org/demos/spreadsheet/index.html
@@ -0,0 +1,252 @@
+<!DOCTYPE html>
+<title>Spreadsheet Demo</title>
+<meta charset="utf-8" />
+<meta http-equiv="X-UA-Compatible" content="IE=edge">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<script type="text/javascript" src="https://unpkg.com/canvaskit-wasm@0.33.0/bin/full/canvaskit.js"></script>
+
+<style>
+ body {
+ background-color: black;
+ }
+ h1 {
+ color: white;
+ }
+ .hidden {
+ display: none;
+ }
+</style>
+
+<body>
+ <h1>Large canvas with many numbers, like a spreadsheet</h1>
+ <select id="numbers_impl">
+ <option value="fillText"><canvas> fillText</option>
+ <option value="drawGlyphs">CK drawGlyphs (tuned)</option>
+ <option value="drawText">CK drawText (naive)</option>
+ </select>
+
+ <canvas id=ck_canvas width=3840 height=2160 class="hidden"></canvas>
+ <canvas id=canvas_2d width=3840 height=2160></canvas>
+</body>
+
+<script type="text/javascript" charset="utf-8">
+ const ckLoaded = CanvasKitInit({ locateFile: (file) => 'https://unpkg.com/canvaskit-wasm@0.33.0/bin/full/' + file });
+
+ // This is the dimensions of a 4k screen.
+ const WIDTH = 3840, HEIGHT = 2160;
+ const ROWS = 144;
+ const ROW_HEIGHT = 15;
+ const COLS = 77;
+ const COL_WIDTH = 50;
+ const canvas2DEle = document.getElementById('canvas_2d');
+ const ckEle = document.getElementById('ck_canvas');
+
+ ckLoaded.then((CanvasKit) => {
+ const canvas2dCtx = canvas2DEle.getContext('2d');
+ const surface = CanvasKit.MakeCanvasSurface('ck_canvas');
+ if (!surface) {
+ throw 'Could not make surface';
+ }
+
+ const colorPaints = {
+ "grey": CanvasKit.Color(76, 76, 76),
+ "black": CanvasKit.BLACK,
+ "white": CanvasKit.WHITE,
+ "springGreen": CanvasKit.Color(0, 255, 127),
+ "tomato": CanvasKit.Color(255, 99, 71),
+ };
+ for (const name in colorPaints) {
+ const color = colorPaints[name];
+ const paint = new CanvasKit.Paint();
+ paint.setColor(color);
+ colorPaints[name] = paint;
+ }
+
+ const data = [];
+ for (let row = 0; row < ROWS; row++) {
+ data[row] = [];
+ for (let col = 0; col < COLS; col++) {
+ data[row][col] = Math.random() * Math.PI;
+ }
+ }
+
+ // Maybe use https://storage.googleapis.com/skia-cdn/google-web-fonts/NotoSans-Regular.ttf
+ const textFont = new CanvasKit.Font(null, 12);
+
+ const choice = document.getElementById("numbers_impl");
+
+ let frames = [];
+ const framesToMeasure = 10;
+ choice.addEventListener("change", () => {
+ frames = [];
+ if (choice.selectedIndex === 0) {
+ canvas2DEle.classList.remove('hidden');
+ ckEle.classList.add('hidden');
+ } else {
+ canvas2DEle.classList.add('hidden');
+ ckEle.classList.remove('hidden');
+ }
+ })
+ function drawFrame(canvas) {
+ if (frames && frames.length === framesToMeasure) {
+ // It is important to measure frame to frame time to account for the time spent by the
+ // GPU after our flush calls.
+ const deltas = [];
+ for (let i = 0; i< frames.length-1;i++) {
+ deltas.push(frames[i+1] - frames[i]);
+ }
+ console.log(`First ${framesToMeasure} frames`, deltas);
+ console.log((frames[framesToMeasure-1] - frames[0]) / framesToMeasure);
+ frames = null;
+ } else if (frames) {
+ frames.push(performance.now());
+ }
+
+ if (choice.selectedIndex === 2) {
+ canvas.clear(CanvasKit.BLACK);
+ drawTextImpl(canvas);
+ } else if (choice.selectedIndex === 1) {
+ canvas.clear(CanvasKit.BLACK);
+ drawGlyphsImpl(canvas);
+ } else {
+ fillTextImpl(canvas2dCtx);
+ }
+
+ surface.requestAnimationFrame(drawFrame)
+ }
+
+ function drawTextImpl(canvas) {
+ const timer = performance.now() / 10000;
+ for (let row = 0; row < ROWS; row++) {
+ if (row % 2) {
+ canvas.drawRect(CanvasKit.XYWHRect(0, row * ROW_HEIGHT + 2, WIDTH, ROW_HEIGHT), colorPaints["grey"]);
+ }
+ for (let col = 0; col < COLS; col++) {
+ let n = Math.abs(Math.sin(timer + data[row][col]));
+ let useWhiteFont = true;
+ if (n < 0.05) {
+ canvas.drawRect(CanvasKit.XYWHRect(col * COL_WIDTH - 2, (row - 1) * ROW_HEIGHT + 2, COL_WIDTH, ROW_HEIGHT), colorPaints["tomato"]);
+ useWhiteFont = false;
+ } else if (n > 0.95) {
+ canvas.drawRect(CanvasKit.XYWHRect(col * COL_WIDTH - 2, (row - 1) * ROW_HEIGHT + 2, COL_WIDTH, ROW_HEIGHT), colorPaints["springGreen"]);
+ useWhiteFont = false;
+ }
+ const str = n.toFixed(4);
+ canvas.drawText(str, col * COL_WIDTH, row * ROW_HEIGHT,
+ useWhiteFont ? colorPaints["white"] : colorPaints["black"], textFont);
+ }
+ }
+ }
+
+ //====================================================================================
+ const alphabet = "0.123456789 ";
+ const glyphIDs = textFont.getGlyphIDs(alphabet);
+ // These are all 7 with current font and size
+ const advances = textFont.getGlyphWidths(glyphIDs);
+
+
+ const charsPerDataPoint = 6; // leading 0, period, 4 decimal places
+ const glyphIDsMObj = CanvasKit.MallocGlyphIDs(ROWS * COLS * charsPerDataPoint);
+ let wasmGlyphIDArr = glyphIDsMObj.toTypedArray();
+ const glyphLocationsMObj = CanvasKit.Malloc(Float32Array, glyphIDsMObj.length * 2);
+ let wasmGlyphLocations = glyphLocationsMObj.toTypedArray();
+
+ function dataToGlyphs(n, outputBuffer, offset) {
+ const s = n.toFixed(4);
+ outputBuffer[offset] = glyphIDs[0]; // Always a leading 0
+ outputBuffer[offset+1] = glyphIDs[1]; // Always a decimal place
+ for (let i = 2; i< charsPerDataPoint; i++) {
+ outputBuffer[offset+i] = glyphIDs[alphabet.indexOf(s[i])];
+ }
+ }
+ const spaceIndex = alphabet.length - 1;
+ function blankOut(outputBuffer, offset) {
+ for (let i = 0; i< charsPerDataPoint; i++) {
+ outputBuffer[offset+i] = glyphIDs[spaceIndex];
+ }
+ }
+
+ for (let row = 0; row < ROWS; row++) {
+ for (let col = 0; col < COLS; col++) {
+ for (let i = 0; i < charsPerDataPoint; i++) {
+ const offset = (col + row * COLS) * charsPerDataPoint * 2;
+ wasmGlyphLocations[offset + i * 2] = col * COL_WIDTH + i * advances[0];
+ wasmGlyphLocations[offset + i * 2 + 1] = row * ROW_HEIGHT;
+ }
+ }
+ }
+
+ const greyGlyphIDsMObj = CanvasKit.MallocGlyphIDs(charsPerDataPoint);
+
+ function drawGlyphsImpl(canvas) {
+ wasmGlyphIDArr = glyphIDsMObj.toTypedArray();
+ let wasmGreyGlyphIDsArr = greyGlyphIDsMObj.toTypedArray();
+
+ const timer = performance.now() / 10000;
+ for (let row = 0; row < ROWS; row++) {
+ if (row % 2) {
+ canvas.drawRect(CanvasKit.XYWHRect(0, row * ROW_HEIGHT + 2, WIDTH, ROW_HEIGHT), colorPaints["grey"]);
+ }
+ for (let col = 0; col < COLS; col++) {
+ const n = Math.abs(Math.sin(timer + data[row][col]));
+ let useWhiteFont = true;
+ if (n < 0.05) {
+ canvas.drawRect(CanvasKit.XYWHRect(col * COL_WIDTH - 2, (row - 1) * ROW_HEIGHT + 2, COL_WIDTH, ROW_HEIGHT), colorPaints["tomato"]);
+ useWhiteFont = false;
+ } else if (n > 0.95) {
+ canvas.drawRect(CanvasKit.XYWHRect(col * COL_WIDTH - 2, (row - 1) * ROW_HEIGHT + 2, COL_WIDTH, ROW_HEIGHT), colorPaints["springGreen"]);
+ useWhiteFont = false;
+ }
+
+ const offset = (col + row * COLS) * charsPerDataPoint;
+ if (useWhiteFont) {
+ dataToGlyphs(n, wasmGlyphIDArr, offset);
+ } else {
+ blankOut(wasmGlyphIDArr, offset);
+ dataToGlyphs(n, wasmGreyGlyphIDsArr, 0);
+ canvas.drawGlyphs(wasmGreyGlyphIDsArr,
+ glyphLocationsMObj.subarray(offset*2, (offset + charsPerDataPoint) * 2),
+ 0, 0, textFont, colorPaints["grey"]
+ );
+ }
+ }
+ }
+ canvas.drawGlyphs(wasmGlyphIDArr, glyphLocationsMObj, 0, 0, textFont, colorPaints["white"]);
+ }
+
+ function fillTextImpl(ctx) {
+ ctx.font = '12px monospace';
+ ctx.fillStyle = 'black';
+ ctx.fillRect(0, 0, WIDTH, HEIGHT);
+ const timer = performance.now() / 10000;
+ for (let row = 0; row < ROWS; row++) {
+ if (row % 2) {
+ ctx.fillStyle = 'rgb(76,76,76)';
+ ctx.fillRect(0, row * ROW_HEIGHT + 2, WIDTH, ROW_HEIGHT);
+ }
+ for (let col = 0; col < COLS; col++) {
+ let n = Math.abs(Math.sin(timer + data[row][col]));
+ let useWhiteFont = true;
+ if (n < 0.05) {
+ ctx.fillStyle = 'rgb(255, 99, 71)';
+ ctx.fillRect(col * COL_WIDTH - 2, (row - 1) * ROW_HEIGHT + 2, COL_WIDTH, ROW_HEIGHT);
+ useWhiteFont = false;
+ } else if (n > 0.95) {
+ ctx.fillStyle = 'rgb(0, 255, 127)';
+ ctx.fillRect(col * COL_WIDTH - 2, (row - 1) * ROW_HEIGHT + 2, COL_WIDTH, ROW_HEIGHT);
+ useWhiteFont = false;
+ }
+ const str = n.toFixed(4);
+ if (useWhiteFont) {
+ ctx.fillStyle = 'white';
+ } else {
+ ctx.fillStyle = 'black';
+ }
+ ctx.fillText(str, col * COL_WIDTH, row * ROW_HEIGHT);
+ }
+ }
+ }
+
+ surface.requestAnimationFrame(drawFrame);
+ });
+</script>
\ No newline at end of file
diff --git a/third_party/skia/demos.skia.org/demos/textedit/index.html b/third_party/skia/demos.skia.org/demos/textedit/index.html
new file mode 100644
index 0000000..d6852c8
--- /dev/null
+++ b/third_party/skia/demos.skia.org/demos/textedit/index.html
@@ -0,0 +1,165 @@
+<!DOCTYPE html>
+<title>TextEdit demo in CanvasKit</title>
+<meta charset="utf-8" />
+<meta http-equiv="X-UA-Compatible" content="IE=edge">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<script type="text/javascript" src="https://particles.skia.org/dist/canvaskit.js"></script>
+<script type="text/javascript" src="textapi_utils.js"></script>
+<script type="text/javascript" src="spiralshader.js"></script>
+
+<style>
+canvas {
+ border: 1px dashed grey;
+}
+</style>
+
+<body>
+ <h1>TextEdit in CanvasKit</h1>
+
+ <canvas id=para2 width=600 height=600 tabindex='-1'></canvas>
+</body>
+
+<script type="text/javascript" charset="utf-8">
+ let CanvasKit;
+ onload = async () => {
+ CanvasKit = await CanvasKitInit({ locateFile: (file) => 'https://particles.skia.org/dist/'+file });
+ ParagraphAPI2();
+ };
+
+ function ParagraphAPI2() {
+ const surface = CanvasKit.MakeCanvasSurface('para2');
+ if (!surface) {
+ console.error('Could not make surface');
+ return;
+ }
+
+ const mouse = MakeMouse();
+ const cursor = MakeCursor(CanvasKit);
+ const canvas = surface.getCanvas();
+ const spiralEffect = MakeSpiralShaderEffect(CanvasKit);
+
+ const text0 = "In a hole in the ground there lived a hobbit. Not a nasty, dirty, " +
+ "wet hole full of worms and oozy smells. This was a hobbit-hole and " +
+ "that means good food, a warm hearth, and all the comforts of home.";
+ const LOC_X = 20,
+ LOC_Y = 20;
+
+ const bgPaint = new CanvasKit.Paint();
+ bgPaint.setColor([0.965, 0.965, 0.965, 1]);
+
+ const editor = MakeEditor(text0, {typeface:null, size:30}, cursor, 540);
+
+ editor.applyStyleToRange({size:130}, 0, 1);
+ editor.applyStyleToRange({italic:true}, 38, 38+6);
+ editor.applyStyleToRange({color:[1,0,0,1]}, 5, 5+4);
+
+ editor.setXY(LOC_X, LOC_Y);
+
+ function drawFrame(canvas) {
+ const lines = editor.getLines();
+
+ canvas.clear(CanvasKit.WHITE);
+
+ if (mouse.isActive()) {
+ const pos = mouse.getPos(-LOC_X, -LOC_Y);
+ const a = lines_pos_to_index(lines, pos[0], pos[1]);
+ const b = lines_pos_to_index(lines, pos[2], pos[3]);
+ if (a === b) {
+ editor.setIndex(a);
+ } else {
+ editor.setIndices(a, b);
+ }
+ }
+
+ canvas.drawRect(editor.bounds(), bgPaint);
+
+ {
+ // update our animated shaders
+ const rad_scale = Math.sin(Date.now() / 5000) / 2;
+ const shader0 = spiralEffect.makeShader([
+ rad_scale,
+ editor.width()/2, editor.width()/2,
+ 1,0,0,1, // color0
+ 0,0,1,1 // color1
+ ]);
+ editor.draw(canvas, [shader0]);
+ shader0.delete();
+ }
+
+ surface.requestAnimationFrame(drawFrame);
+ }
+ surface.requestAnimationFrame(drawFrame);
+
+ function interact(e) {
+ const type = e.type;
+ if (type === 'pointerup') {
+ mouse.setUp(e.offsetX, e.offsetY);
+ } else if (type === 'pointermove') {
+ mouse.setMove(e.offsetX, e.offsetY);
+ } else if (type === 'pointerdown') {
+ mouse.setDown(e.offsetX, e.offsetY);
+ }
+ };
+
+ function keyhandler(e) {
+ switch (e.key) {
+ case 'ArrowLeft': editor.moveDX(-1); return;
+ case 'ArrowRight': editor.moveDX(1); return;
+ case 'ArrowUp':
+ e.preventDefault();
+ editor.moveDY(-1);
+ return;
+ case 'ArrowDown':
+ e.preventDefault();
+ editor.moveDY(1);
+ return;
+ case 'Backspace':
+ editor.deleteSelection(-1);
+ return;
+ case 'Delete':
+ editor.deleteSelection(1);
+ return;
+ case 'Shift':
+ return;
+ case 'Tab': // todo: figure out how to handle...
+ e.preventDefault();
+ return;
+ }
+ if (e.ctrlKey) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ switch (e.key) {
+ case 'r': editor.applyStyleToSelection({color:[1,0,0,1]}); return;
+ case 'g': editor.applyStyleToSelection({color:[0,0.6,0,1]}); return;
+ case 'u': editor.applyStyleToSelection({color:[0,0,1,1]}); return;
+ case 'k': editor.applyStyleToSelection({color:[0,0,0,1]}); return;
+
+ case 's': editor.applyStyleToSelection({shaderIndex:0}); return;
+
+ case 'i': editor.applyStyleToSelection({italic:'toggle'}); return;
+ case 'b': editor.applyStyleToSelection({bold:'toggle'}); return;
+ case 'w': editor.applyStyleToSelection({wavy:'toggle'}); return;
+
+ case ']': editor.applyStyleToSelection({size_add:1}); return;
+ case '[': editor.applyStyleToSelection({size_add:-1}); return;
+ case '}': editor.applyStyleToSelection({size_add:10}); return;
+ case '{': editor.applyStyleToSelection({size_add:-10}); return;
+ }
+ }
+ if (!e.ctrlKey && !e.metaKey) {
+ if (e.key.length == 1) { // avoid keys like "Escape" for now
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ editor.insert(e.key);
+ }
+ }
+ }
+
+ document.getElementById('para2').addEventListener('pointermove', interact);
+ document.getElementById('para2').addEventListener('pointerdown', interact);
+ document.getElementById('para2').addEventListener('pointerup', interact);
+ document.getElementById('para2').addEventListener('keydown', keyhandler);
+ return surface;
+ }
+
+</script>
diff --git a/third_party/skia/demos.skia.org/demos/textedit/spiralshader.js b/third_party/skia/demos.skia.org/demos/textedit/spiralshader.js
new file mode 100644
index 0000000..3ad0c37
--- /dev/null
+++ b/third_party/skia/demos.skia.org/demos/textedit/spiralshader.js
@@ -0,0 +1,28 @@
+
+/*
+ * When calling makeShader, pass
+ * radius_scale 0.5
+ * center x, y
+ * color0 r, g, b, a
+ * color1 r, g, b, a
+ */
+function MakeSpiralShaderEffect(CanvasKit) {
+ const _spiralSkSL = `
+ uniform float rad_scale;
+ uniform float2 in_center;
+ uniform float4 in_colors0;
+ uniform float4 in_colors1;
+
+ half4 main(float2 p) {
+ float2 pp = p - in_center;
+ float radius = sqrt(dot(pp, pp));
+ radius = sqrt(radius);
+ float angle = atan(pp.y / pp.x);
+ float t = (angle + 3.1415926/2) / (3.1415926);
+ t += radius * rad_scale;
+ t = fract(t);
+ return half4(mix(in_colors0, in_colors1, t));
+ }`;
+
+ return CanvasKit.RuntimeEffect.Make(_spiralSkSL);
+}
diff --git a/third_party/skia/demos.skia.org/demos/textedit/textapi_utils.js b/third_party/skia/demos.skia.org/demos/textedit/textapi_utils.js
new file mode 100644
index 0000000..9e6df24
--- /dev/null
+++ b/third_party/skia/demos.skia.org/demos/textedit/textapi_utils.js
@@ -0,0 +1,638 @@
+
+function ASSERT(pred) {
+ console.assert(pred, 'assert failed');
+}
+
+function LOG(...args) {
+ // comment out for non-debugging
+// console.log(args);
+}
+
+function MakeCursor(CanvasKit) {
+ const linePaint = new CanvasKit.Paint();
+ linePaint.setColor([0,0,1,1]);
+ linePaint.setStyle(CanvasKit.PaintStyle.Stroke);
+ linePaint.setStrokeWidth(2);
+ linePaint.setAntiAlias(true);
+
+ const pathPaint = new CanvasKit.Paint();
+ pathPaint.setColor([0,0,1,0.25]);
+ linePaint.setAntiAlias(true);
+
+ return {
+ _line_paint: linePaint, // wrap in weak-ref so we can delete it?
+ _path_paint: pathPaint,
+ _x: 0,
+ _top: 0,
+ _bottom: 0,
+ _path: null, // only use x,top,bottom if path is null
+ _draws_per_sec: 2,
+
+ // pass 0 for no-draw, pass inf. for always on
+ setBlinkRate: function(blinks_per_sec) {
+ this._draws_per_sec = blinks_per_sec;
+ },
+ place: function(x, top, bottom) {
+ this._x = x;
+ this._top = top;
+ this._bottom = bottom;
+
+ this.setPath(null);
+ },
+ setPath: function(path) {
+ if (this._path) {
+ this._path.delete();
+ }
+ this._path = path;
+ },
+ draw_before: function(canvas) {
+ if (this._path) {
+ canvas.drawPath(this._path, this._path_paint);
+ }
+ },
+ draw_after: function(canvas) {
+ if (this._path) {
+ return;
+ }
+ if (Math.floor(Date.now() * this._draws_per_sec / 1000) & 1) {
+ canvas.drawLine(this._x, this._top, this._x, this._bottom, this._line_paint);
+ }
+ },
+ };
+}
+
+function MakeMouse() {
+ return {
+ _start_x: 0, _start_y: 0,
+ _curr_x: 0, _curr_y: 0,
+ _active: false,
+
+ isActive: function() {
+ return this._active;
+ },
+ setDown: function(x, y) {
+ this._start_x = this._curr_x = x;
+ this._start_y = this._curr_y = y;
+ this._active = true;
+ },
+ setMove: function(x, y) {
+ this._curr_x = x;
+ this._curr_y = y;
+ },
+ setUp: function(x, y) {
+ this._curr_x = x;
+ this._curr_y = y;
+ this._active = false;
+ },
+ getPos: function(dx, dy) {
+ return [ this._start_x + dx, this._start_y + dy, this._curr_x + dx, this._curr_y + dy ];
+ },
+ };
+}
+
+function runs_x_to_index(runs, x) {
+ for (const r of runs) {
+ for (let i = 1; i < r.offsets.length; i += 1) {
+ if (x < r.positions[i*2]) {
+ const mid = (r.positions[i*2-2] + r.positions[i*2]) * 0.5;
+ if (x <= mid) {
+ return r.offsets[i-1];
+ } else {
+ return r.offsets[i];
+ }
+ }
+ }
+ }
+ const r = runs[runs.length-1];
+ return r.offsets[r.offsets.length-1];
+}
+
+function lines_pos_to_index(lines, x, y) {
+ if (y < lines[0].top) {
+ return 0;
+ }
+ const l = lines.find((l) => y <= l.bottom);
+ return l ? runs_x_to_index(l.runs, x)
+ : lines[lines.length - 1].textRange.last;
+}
+
+function runs_index_to_run(runs, index) {
+ const r = runs.find((r) => index <= r.offsets[r.offsets.length-1]);
+ // return last if no run is found
+ return r ? r : runs[runs.length-1];
+}
+
+function runs_index_to_x(runs, index) {
+ const r = runs_index_to_run(runs, index);
+ const i = r.offsets.findIndex((offset) => index === offset);
+ return i >= 0 ? r.positions[i*2]
+ : r.positions[r.positions.length-2]; // last x
+}
+
+function lines_index_to_line_index(lines, index) {
+ const l = lines.findIndex((l) => index <= l.textRange.last);
+ return l >= 0 ? l : lines.length-1;
+}
+
+function lines_index_to_line(lines, index) {
+ return lines[lines_index_to_line_index(lines, index)];
+}
+
+function lines_indices_to_path(lines, a, b, width) {
+ if (a == b) {
+ return null;
+ }
+ if (a > b) { [a, b] = [b, a]; }
+
+ const path = new CanvasKit.Path();
+ const la = lines_index_to_line(lines, a);
+ const lb = lines_index_to_line(lines, b);
+ const ax = runs_index_to_x(la.runs, a);
+ const bx = runs_index_to_x(lb.runs, b);
+ if (la == lb) {
+ path.addRect([ax, la.top, bx, la.bottom]);
+ } else {
+ path.addRect([ax, la.top, width, la.bottom]);
+ path.addRect([0, lb.top, bx, lb.bottom]);
+ if (la.bottom < lb.top) {
+ path.addRect([0, la.bottom, width, lb.top]); // extra lines inbetween
+ }
+ }
+ return path;
+}
+
+function string_del(str, start, end) {
+ return str.slice(0, start) + str.slice(end, str.length);
+}
+
+function make_default_paint() {
+ const p = new CanvasKit.Paint();
+ p.setAntiAlias(true);
+ return p;
+}
+
+function make_default_font(tf) {
+ const font = new CanvasKit.Font(tf);
+ font.setSubpixel(true);
+ return font;
+}
+
+function MakeStyle(length) {
+ return {
+ _length: length,
+ typeface: null,
+ size: null,
+ color: null,
+ bold: null,
+ italic: null,
+
+ _check_toggle: function(src, dst) {
+ if (src == 'toggle') {
+ return !dst;
+ } else {
+ return src;
+ }
+ },
+
+ // returns true if we changed something affecting layout
+ mergeFrom: function(src) {
+ let layoutChanged = false;
+
+ if (src.typeface && this.typeface !== src.typeface) {
+ this.typeface = src.typeface;
+ layoutChanged = true;
+ }
+ if (src.size && this.size !== src.size) {
+ this.size = src.size;
+ layoutChanged = true;
+ }
+ if (src.color) {
+ this.color = src.color;
+ delete this.shaderIndex; // we implicitly delete shader if there is a color
+ }
+
+ if (src.bold) {
+ this.bold = this._check_toggle(src.bold, this.bold);
+ }
+ if (src.italic) {
+ this.italic = this._check_toggle(src.italic, this.italic);
+ }
+ if (src.wavy) {
+ this.wavy = this._check_toggle(src.wavy, this.wavy);
+ }
+
+ if (src.size_add) {
+ this.size += src.size_add;
+ layoutChanged = true;
+ }
+
+ if ('shaderIndex' in src) {
+ if (src.shaderIndex >= 0) {
+ this.shaderIndex = src.shaderIndex;
+ } else {
+ delete this.shaderIndex;
+ }
+ }
+ return layoutChanged;
+ }
+ };
+}
+
+function MakeEditor(text, style, cursor, width) {
+ const ed = {
+ _text: text,
+ _lines: null,
+ _cursor: cursor,
+ _width: width,
+ _index: { start: 0, end: 0 },
+ _styles: null,
+ // drawing
+ _X: 0,
+ _Y: 0,
+ _paint: make_default_paint(),
+ _font: make_default_font(style.typeface),
+
+ getLines: function() { return this._lines; },
+
+ width: function() {
+ return this._width;
+ },
+ height: function() {
+ return this._lines[this._lines.length-1].bottom;
+ },
+ bounds: function() {
+ return [this._X, this._Y, this._X + this.width(), this._Y + this.height()];
+ },
+ setXY: function(x, y) {
+ this._X = x;
+ this._Y = y;
+ },
+
+ _rebuild_selection: function() {
+ const a = this._index.start;
+ const b = this._index.end;
+ ASSERT(a >= 0 && a <= b && b <= this._text.length);
+ if (a === b) {
+ const l = lines_index_to_line(this._lines, a);
+ const x = runs_index_to_x(l.runs, a);
+ this._cursor.place(x, l.top, l.bottom);
+ } else {
+ this._cursor.setPath(lines_indices_to_path(this._lines, a, b, this._width));
+ }
+ },
+ setIndex: function(i) {
+ this._index.start = this._index.end = i;
+ this._rebuild_selection();
+ },
+ setIndices: function(a, b) {
+ if (a > b) { [a, b] = [b, a]; }
+ this._index.start = a;
+ this._index.end = b;
+ this._rebuild_selection();
+ },
+ moveDX: function(dx) {
+ let index;
+ if (this._index.start == this._index.end) {
+ // just adjust and pin
+ index = Math.max(Math.min(this._index.start + dx, this._text.length), 0);
+ } else {
+ // 'deselect' the region, and turn it into just a single index
+ index = dx < 0 ? this._index.start : this._index.end;
+ }
+ this.setIndex(index);
+ },
+ moveDY: function(dy) {
+ let index = (dy < 0) ? this._index.start : this._index.end;
+ const i = lines_index_to_line_index(this._lines, index);
+ if (dy < 0 && i === 0) {
+ index = 0;
+ } else if (dy > 0 && i == this._lines.length - 1) {
+ index = this._text.length;
+ } else {
+ const x = runs_index_to_x(this._lines[i].runs, index);
+ // todo: statefully track "original" x when an up/down sequence started,
+ // so we can avoid drift.
+ index = runs_x_to_index(this._lines[i+dy].runs, x);
+ }
+ this.setIndex(index);
+ },
+
+ _validateStyles: function() {
+ const len = this._styles.reduce((sum, style) => sum + style._length, 0);
+ ASSERT(len === this._text.length);
+ },
+ _validateBlocks: function(blocks) {
+ const len = blocks.reduce((sum, block) => sum + block.length, 0);
+ ASSERT(len === this._text.length);
+ },
+
+ _buildLines: function() {
+ this._validateStyles();
+
+ const blocks = [];
+ let block = null;
+ for (const s of this._styles) {
+ if (!block || (block.typeface === s.typeface && block.size === s.size)) {
+ if (!block) {
+ block = { length: 0, typeface: s.typeface, size: s.size };
+ }
+ block.length += s._length;
+ } else {
+ blocks.push(block);
+ block = { length: s._length, typeface: s.typeface, size: s.size };
+ }
+ }
+ blocks.push(block);
+ this._validateBlocks(blocks);
+
+ this._lines = CanvasKit.ParagraphBuilder.ShapeText(this._text, blocks, this._width);
+ this._rebuild_selection();
+
+ // add textRange to each run, to aid in drawing
+ this._runs = [];
+ for (const l of this._lines) {
+ for (const r of l.runs) {
+ r.textRange = { start: r.offsets[0], end: r.offsets[r.offsets.length-1] };
+ this._runs.push(r);
+ }
+ }
+ },
+
+ // note: this does not rebuild lines/runs, or update the cursor,
+ // but it does edit the text and styles
+ // returns true if it deleted anything
+ _deleteRange: function(start, end) {
+ ASSERT(start >= 0 && end <= this._text.length);
+ ASSERT(start <= end);
+ if (start === end) {
+ return false;
+ }
+
+ this._delete_style_range(start, end);
+ // Do this after shrink styles (we use text.length in an assert)
+ this._text = string_del(this._text, start, end);
+ },
+ deleteSelection: function(direction) {
+ let start = this._index.start;
+ if (start == this._index.end) {
+ if (direction < 0) {
+ if (start == 0) {
+ return; // nothing to do
+ }
+ this._deleteRange(start - 1, start);
+ start -= 1;
+ } else {
+ if (start >= this._text.length) {
+ return; // nothing to do
+ }
+ this._deleteRange(start, start + 1);
+ }
+ } else {
+ this._deleteRange(start, this._index.end);
+ }
+ this._index.start = this._index.end = start;
+ this._buildLines();
+ },
+ insert: function(charcode) {
+ const len = charcode.length;
+ if (this._index.start != this._index.end) {
+ this.deleteSelection();
+ }
+ const index = this._index.start;
+
+ // do this before edit the text (we use text.length in an assert)
+ const [i, prev_len] = this.find_style_index_and_prev_length(index);
+ this._styles[i]._length += len;
+
+ // now grow the text
+ this._text = this._text.slice(0, index) + charcode + this._text.slice(index);
+
+ this._index.start = this._index.end = index + len;
+ this._buildLines();
+ },
+
+ draw: function(canvas, shaders) {
+ canvas.save();
+ canvas.translate(this._X, this._Y);
+
+ this._cursor.draw_before(canvas);
+
+ const runs = this._runs;
+ const styles = this._styles;
+ const f = this._font;
+ const p = this._paint;
+
+ let s = styles[0];
+ let sindex = 0;
+ let s_start = 0;
+ let s_end = s._length;
+
+ let r = runs[0];
+ let rindex = 0;
+
+ let start = 0;
+ let end = 0;
+ while (start < this._text.length) {
+ while (r.textRange.end <= start) {
+ r = runs[++rindex];
+ if (!r) {
+ // ran out of runs, so the remaining text must just be WS
+ break;
+ }
+ }
+ if (!r) break;
+ while (s_end <= start) {
+ s = styles[++sindex];
+ s_start = s_end;
+ s_end += s._length;
+ }
+ end = Math.min(r.textRange.end, s_end);
+
+ LOG('New range: ', start, end,
+ 'from run', r.textRange.start, r.textRange.end,
+ 'style', s_start, s_end);
+
+ // check that we have anything to draw
+ if (r.textRange.start >= end) {
+ start = end;
+ continue; // could be a span of WS with no glyphs
+ }
+
+// f.setTypeface(r.typeface); // r.typeface is always null (for now)
+ f.setSize(r.size);
+ f.setEmbolden(s.bold);
+ f.setSkewX(s.italic ? -0.2 : 0);
+ p.setColor(s.color ? s.color : [0,0,0,1]);
+ p.setShader(s.shaderIndex >= 0 ? shaders[s.shaderIndex] : null);
+
+ let gly = r.glyphs;
+ let pos = r.positions;
+ if (start > r.textRange.start || end < r.textRange.end) {
+ // search for the subset of glyphs to draw
+ let glyph_start, glyph_end;
+ for (let i = 0; i < r.offsets.length; ++i) {
+ if (r.offsets[i] >= start) {
+ glyph_start = i;
+ break;
+ }
+ }
+ for (let i = glyph_start+1; i < r.offsets.length; ++i) {
+ if (r.offsets[i] >= end) {
+ glyph_end = i;
+ break;
+ }
+ }
+ LOG(' glyph subrange', glyph_start, glyph_end);
+ gly = gly.slice(glyph_start, glyph_end);
+ pos = pos.slice(glyph_start*2, glyph_end*2);
+ } else {
+ LOG(' use entire glyph run');
+ }
+
+ let working_pos = pos;
+ if (s.wavy) {
+ const xscale = 0.05;
+ const yscale = r.size * 0.125;
+ let wavy = [];
+ for (let i = 0; i < pos.length; i += 2) {
+ const x = pos[i + 0];
+ wavy.push(x);
+ wavy.push(pos[i + 1] + Math.sin(x * xscale) * yscale);
+ }
+ working_pos = new Float32Array(wavy);
+ }
+ canvas.drawGlyphs(gly, working_pos, 0, 0, f, p);
+
+ p.setShader(null); // in case our caller deletes their shader(s)
+
+ start = end;
+ }
+
+ this._cursor.draw_after(canvas);
+ canvas.restore();
+ },
+
+ // Styling
+
+ // returns [index, prev total length before this style]
+ find_style_index_and_prev_length: function(index) {
+ let len = 0;
+ for (let i = 0; i < this._styles.length; ++i) {
+ const l = this._styles[i]._length;
+ len += l;
+ // < favors the latter style if index is between two styles
+ if (index < len) {
+ return [i, len - l];
+ }
+ }
+ ASSERT(len === this._text.length);
+ return [this._styles.length-1, len];
+ },
+ _delete_style_range: function(start, end) {
+ // shrink/remove styles
+ //
+ // [.....][....][....][.....] styles
+ // [..................] start...end
+ //
+ // - trim the first style
+ // - remove the middle styles
+ // - trim the last style
+
+ let N = end - start;
+ let [i, prev_len] = this.find_style_index_and_prev_length(start);
+ let s = this._styles[i];
+ if (start > prev_len) {
+ // we overlap the first style (but not entirely
+ const skip = start - prev_len;
+ ASSERT(skip < s._length);
+ const shrink = Math.min(N, s._length - skip);
+ ASSERT(shrink > 0);
+ s._length -= shrink;
+ N -= shrink;
+ if (N === 0) {
+ return;
+ }
+ i += 1;
+ ASSERT(i < this._styles.length);
+ }
+ while (N > 0) {
+ s = this._styles[i];
+ if (N >= s._length) {
+ N -= s._length;
+ this._styles.splice(i, 1);
+ } else {
+ s._length -= N;
+ break;
+ }
+ }
+ },
+
+ applyStyleToRange: function(style, start, end) {
+ if (start > end) { [start, end] = [end, start]; }
+ ASSERT(start >= 0 && end <= this._text.length);
+ if (start === end) {
+ return;
+ }
+
+ LOG('trying to apply', style, start, end);
+ let i;
+ for (i = 0; i < this._styles.length; ++i) {
+ if (start <= this._styles[i]._length) {
+ break;
+ }
+ start -= this._styles[i]._length;
+ end -= this._styles[i]._length;
+ }
+
+ let s = this._styles[i];
+ // do we need to fission off a clean subset for the head of s?
+ if (start > 0) {
+ const ns = Object.assign({}, s);
+ s._length = start;
+ ns._length -= start;
+ LOG('initial splice', i, start, s._length, ns._length);
+ i += 1;
+ this._styles.splice(i, 0, ns);
+ end -= start;
+ // we don't use start any more
+ }
+ // merge into any/all whole styles we overlap
+ let layoutChanged = false;
+ while (end >= this._styles[i]._length) {
+ LOG('whole run merging for style index', i)
+ layoutChanged |= this._styles[i].mergeFrom(style);
+ end -= this._styles[i]._length;
+ i += 1;
+ if (end == 0) {
+ break;
+ }
+ }
+ // do we partially cover the last run
+ if (end > 0) {
+ s = this._styles[i];
+ const ns = Object.assign({}, s); // the new first half
+ ns._length = end;
+ s._length -= end; // trim the (unchanged) tail
+ LOG('merging tail', i, ns._length, s._length);
+ layoutChanged |= ns.mergeFrom(style);
+ this._styles.splice(i, 0, ns);
+ }
+
+ this._validateStyles();
+ LOG('after applying styles', this._styles);
+
+ if (layoutChanged) {
+ this._buildLines();
+ }
+ },
+ applyStyleToSelection: function(style) {
+ this.applyStyleToRange(style, this._index.start, this._index.end);
+ },
+ };
+
+ const s = MakeStyle(ed._text.length);
+ s.mergeFrom(style);
+ ed._styles = [ s ];
+ ed._buildLines();
+ return ed;
+}
diff --git a/third_party/skia/demos.skia.org/demos/textures/index.html b/third_party/skia/demos.skia.org/demos/textures/index.html
new file mode 100644
index 0000000..0a1f9b6
--- /dev/null
+++ b/third_party/skia/demos.skia.org/demos/textures/index.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<title>Texture demos</title>
+<meta charset="utf-8" />
+<meta http-equiv="X-UA-Compatible" content="IE=edge">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<script type="text/javascript" src="https://particles.skia.org/dist/canvaskit.js"></script>
+
+<style>
+ canvas {
+ border: 1px dashed grey;
+ }
+ #sourceImage, #sourceVideo {
+ width: 100px;
+ height: 100px;
+ border: 1px solid red;
+ cursor: pointer;
+ }
+</style>
+
+<body>
+ <h1>User Defined Textures</h1>
+
+ <p>Tap on one of the texture sources to switch to it.</p>
+
+ <canvas id=draw width=500 height=500></canvas>
+
+ <img id="sourceImage" src="/demos/textures/testimg.png">
+ <video id="sourceVideo" autoplay muted></video>
+</body>
+
+<script type="text/javascript" charset="utf-8">
+ const ckLoaded = CanvasKitInit({ locateFile: (file) => 'https://particles.skia.org/dist/' + file });
+ const canvasEle = document.getElementById('draw');
+ const imgEle = document.getElementById('sourceImage');
+ const videoEle = document.getElementById('sourceVideo');
+
+ // Links the web cam to the video element.
+ navigator.mediaDevices.getUserMedia({ video: true }).then((stream) => {
+ videoEle.srcObject = stream;
+ }).catch((err) => {
+ console.error("Could not set up video", err);
+ });
+
+ // Enables switching between texture sources.
+ let srcEle = imgEle;
+ imgEle.addEventListener('click', () => {
+ srcEle = imgEle;
+ });
+ videoEle.addEventListener('click', () => {
+ srcEle = videoEle;
+ });
+
+ Promise.all([
+ ckLoaded,
+ imgEle.decode(),
+]).then(([
+ CanvasKit,
+ shouldBeNull,
+]) => {
+ const surface = CanvasKit.MakeCanvasSurface('draw');
+ if (!surface) {
+ throw 'Could not make surface';
+ }
+ const paint = new CanvasKit.Paint();
+ // This image will have its underlying texture re-used once per frame.
+ const img = surface.makeImageFromTextureSource(srcEle);
+
+ let lastTS = Date.now();
+ function drawFrame(canvas) {
+ const now = Date.now();
+ canvas.rotate(10 * (now - lastTS) / 1000, 250, 250);
+ lastTS = now;
+ // Re-use the image's underlying texture, but replace the contents of the old texture
+ // with the contents of srcEle
+ surface.updateTextureFromSource(img, srcEle);
+ canvas.clear(CanvasKit.Color(200, 200, 200));
+ canvas.drawImage(img, 5, 5, paint);
+ surface.requestAnimationFrame(drawFrame);
+ }
+ surface.requestAnimationFrame(drawFrame);
+ });
+
+</script>
diff --git a/third_party/skia/demos.skia.org/demos/textures/testimg.png b/third_party/skia/demos.skia.org/demos/textures/testimg.png
new file mode 100644
index 0000000..c2efb81
--- /dev/null
+++ b/third_party/skia/demos.skia.org/demos/textures/testimg.png
Binary files differ
diff --git a/third_party/skia/demos.skia.org/demos/up_scaling/index.html b/third_party/skia/demos.skia.org/demos/up_scaling/index.html
new file mode 100644
index 0000000..c2f3f7f
--- /dev/null
+++ b/third_party/skia/demos.skia.org/demos/up_scaling/index.html
@@ -0,0 +1,183 @@
+<!doctype HTML>
+
+<!DOCTYPE html>
+<title>Custom Image Upscaling</title>
+<meta charset="utf-8" />
+<meta http-equiv="X-UA-Compatible" content="IE=edge">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<script type="text/javascript" src="https://unpkg.com/canvaskit-wasm@0.25.0/bin/full/canvaskit.js"></script>
+
+<style>
+canvas {
+ border: 1px dashed grey;
+}
+</style>
+
+<body>
+ <h1>Custom Image Upscaling</h1>
+
+ <div id=scale_text></div>
+ <div class="slidecontainer">
+ <input type="range" min="100" max="500" value="100" class="slider" id="scale_slider">
+ </div>
+
+ <canvas id=draw width=1000 height=400></canvas>
+</body>
+
+<script type="text/javascript" charset="utf-8">
+let CanvasKit;
+onload = async () => {
+ CanvasKit = await CanvasKitInit({ locateFile: (file) => "https://unpkg.com/canvaskit-wasm@0.25.0/bin/full/" + file });
+ init();
+};
+
+function init() {
+ if (!CanvasKit.RuntimeEffect) {
+ console.log(CanvasKit.RuntimeEffect);
+ throw "Need RuntimeEffect";
+ }
+ const surface = CanvasKit.MakeCanvasSurface('draw');
+ if (!surface) {
+ throw 'Could not make surface';
+ }
+
+ const prog = `
+ uniform shader image;
+ uniform float sharp; // slope of the lerp section of the kernel (steeper == sharper)
+
+ float2 sharpen(float2 w) {
+ // we think of sharp as a slope on a shifted line
+ // y = sharp * (w - 0.5) + 0.5
+ // Rewrite with mix needed for some GPUs to be correct
+ return saturate(mix(float2(0.5), w, sharp));
+ }
+
+ bool nearly_center(float2 p) {
+ float tolerance = 1/255.0;
+ p = abs(fract(p) - 0.5);
+ return p.x < tolerance && p.y < tolerance;
+ }
+
+ half4 main(float2 p) {
+ // p+1/2, p-1/2 can be numerically unstable when near the center, so we
+ // detect that case, and just sample at our center.
+ float h = nearly_center(p) ? 0.0 : 0.5;
+
+ // Manual bilerp logic
+ half4 pa = image.eval(float2(p.x-h, p.y-h));
+ half4 pb = image.eval(float2(p.x+h, p.y-h));
+ half4 pc = image.eval(float2(p.x-h, p.y+h));
+ half4 pd = image.eval(float2(p.x+h, p.y+h));
+
+ // Now 'sharpen' the weighting. This is the magic sauce where we different
+ // from a normal bilerp
+ float2 w = sharpen(fract(p + 0.5));
+ return mix(mix(pa, pb, w.x),
+ mix(pc, pd, w.x), w.y);
+ }
+ `;
+ const effect = CanvasKit.RuntimeEffect.Make(prog);
+
+ const size = 100;
+ const shader_paint = new CanvasKit.Paint();
+ const color_paint = new CanvasKit.Paint();
+
+ const image = function() {
+ let surf = CanvasKit.MakeSurface(size, size);
+ let c = surf.getCanvas();
+
+ color_paint.setColor([1, 1, 1, 1]);
+ c.drawRect([0, 0, size, size], color_paint);
+
+ color_paint.setColor([0, 0, 0, 1]);
+ for (let x = 0; x < size; x += 2) {
+ c.drawRect([x, 0, x+1, size], color_paint);
+ }
+ return surf.makeImageSnapshot();
+ }();
+
+ const imageShader = image.makeShaderOptions(CanvasKit.TileMode.Clamp,
+ CanvasKit.TileMode.Clamp,
+ CanvasKit.FilterMode.Nearest,
+ CanvasKit.MipmapMode.None);
+
+ scale_slider.oninput = () => { surface.requestAnimationFrame(drawFrame); }
+
+ const fract = function(value) {
+ return value - Math.floor(value);
+ }
+
+ // Uses custom sampling (4 sample points per-pixel)
+ draw_one_pass = function(canvas, y, scale) {
+ canvas.save();
+ canvas.scale(scale, 1.0);
+ shader_paint.setShader(effect.makeShaderWithChildren([Math.round(scale)], [imageShader], null));
+ canvas.drawRect([0, 0, size, y], shader_paint);
+ canvas.restore();
+ }
+
+ // First creates an upscaled image, and then bilerps it
+ draw_two_pass = function(canvas, y, scale) {
+ let intScale = Math.max(1, Math.floor(scale + 0.5));
+ let intImage = imageAtScale(intScale);
+
+ canvas.save();
+ canvas.scale(scale / intScale, 1);
+ canvas.drawImageOptions(intImage, 0, y, CanvasKit.FilterMode.Linear, CanvasKit.MipmapMode.None, null);
+ canvas.restore();
+ }
+
+ drawFrame = function(canvas) {
+ const scale = scale_slider.value / 100.0;
+ scale_text.innerText = scale
+
+ canvas.clear();
+
+ draw_one_pass(canvas, 100, scale);
+ drawMagnified(canvas, 0, 100);
+
+ draw_two_pass(canvas, 200, scale);
+ drawMagnified(canvas, 200, 300);
+ }
+
+ function drawMagnified(canvas, sampleY, dstY) {
+ let pixels = canvas.readPixels(
+ 0, sampleY,
+ { width: 50,
+ height: 1,
+ colorType: CanvasKit.ColorType.RGBA_8888,
+ alphaType: CanvasKit.AlphaType.Premul,
+ colorSpace: CanvasKit.ColorSpace.DISPLAY_P3
+ }
+ );
+
+ for (let i = 0; i < 50; i++) {
+ let color =
+ [ pixels[i*4 + 0] / 255.0,
+ pixels[i*4 + 1] / 255.0,
+ pixels[i*4 + 2] / 255.0,
+ pixels[i*4 + 3] / 255.0 ];
+ color_paint.setColor(color);
+ canvas.drawRect([i*20, dstY, (i+1)*20, dstY + 100], color_paint);
+ }
+ }
+
+ function imageAtScale(s) {
+ let surf = CanvasKit.MakeSurface(s * size, size);
+ let c = surf.getCanvas();
+
+ color_paint.setColor([1, 1, 1, 1]);
+ c.drawRect([0, 0, s * size, size], color_paint);
+
+ color_paint.setColor([0, 0, 0, 1]);
+ for (let x = 0; x < size; x += 2) {
+ c.drawRect([x * s, 0, (x+1) * s, size], color_paint);
+ }
+ return surf.makeImageSnapshot();
+ }
+
+ surface.requestAnimationFrame(drawFrame);
+}
+
+</script>
+
diff --git a/third_party/skia/demos.skia.org/demos/web_worker/index.html b/third_party/skia/demos.skia.org/demos/web_worker/index.html
new file mode 100644
index 0000000..13a3082
--- /dev/null
+++ b/third_party/skia/demos.skia.org/demos/web_worker/index.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<title>CanvasKit Web Worker Demo</title>
+<meta charset="utf-8" />
+<meta http-equiv="X-UA-Compatible" content="IE=edge">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+
+<style>
+ canvas {
+ border: 1px dashed grey;
+ }
+
+ .canvas-container {
+ float: left;
+ }
+</style>
+
+<body>
+ <h1>CanvasKit in a Web Worker demo</h1>
+ <p>NOTE: this demo currently only works in chromium-based browsers, where
+ <a href="https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas#Browser_compatibility">
+ Offscreen Canvas
+ </a>
+ is supported.
+ </p>
+
+ <div class="canvas-container">
+ <h2>Normal Canvas</h2>
+ <canvas id="onscreen-canvas" width=500 height=500></canvas>
+ <button id="busy-button">Make main thread busy</button>
+ </div>
+ <div class="canvas-container">
+ <h2>Web Worker Canvas</h2>
+ <canvas id="offscreen-canvas" width=500 height=500></canvas>
+ </div>
+</body>
+<script type="text/javascript" src="https://unpkg.com/canvaskit-wasm@0.25.0/bin/full/canvaskit.js"></script>
+<script type="text/javascript" src="shared.js"></script>
+<script type="text/javascript" src="main.js"></script>
diff --git a/third_party/skia/demos.skia.org/demos/web_worker/main.js b/third_party/skia/demos.skia.org/demos/web_worker/main.js
new file mode 100644
index 0000000..1c8a875
--- /dev/null
+++ b/third_party/skia/demos.skia.org/demos/web_worker/main.js
@@ -0,0 +1,34 @@
+// Set up worker and send it a message with the canvas to draw to.
+const offscreenCanvas = document.getElementById('offscreen-canvas').transferControlToOffscreen();
+const worker = new Worker('worker.js');
+worker.postMessage({ offscreenCanvas }, [offscreenCanvas]);
+
+const canvasKitInitPromise =
+ CanvasKitInit({locateFile: (file) => 'https://unpkg.com/canvaskit-wasm@0.25.0/bin/full/'+file});
+const skottieJsonPromise =
+ fetch('https://storage.googleapis.com/skia-cdn/misc/lego_loader.json')
+ .then((response) => response.text());
+
+Promise.all([
+ canvasKitInitPromise,
+ skottieJsonPromise
+]).then(([
+ CanvasKit,
+ jsonStr
+]) => {
+ const onscreenCanvas = document.getElementById('onscreen-canvas');
+ const surface = CanvasKit.MakeWebGLCanvasSurface(onscreenCanvas, null);
+ if (!surface) {
+ throw 'Could not make canvas surface';
+ }
+
+ SkottieExample(CanvasKit, surface, jsonStr);
+});
+
+document.getElementById('busy-button').addEventListener('click', () => {
+ const startTime = performance.now();
+ // This loop runs for 1300ms, emulating computation.
+ // 1300ms was chosen because it causes a visually obvious lag in the lego loader animation.
+ while (performance.now() - startTime < 1300) {
+ }
+});
diff --git a/third_party/skia/demos.skia.org/demos/web_worker/shared.js b/third_party/skia/demos.skia.org/demos/web_worker/shared.js
new file mode 100644
index 0000000..4f212f1
--- /dev/null
+++ b/third_party/skia/demos.skia.org/demos/web_worker/shared.js
@@ -0,0 +1,20 @@
+function SkottieExample(CanvasKit, surface, jsonStr, bounds) {
+ if (!CanvasKit || !jsonStr) {
+ return;
+ }
+ const animation = CanvasKit.MakeAnimation(jsonStr);
+ const duration = animation.duration() * 1000;
+
+ const firstFrame = performance.now();
+
+ function drawFrame(skcanvas) {
+ const now = performance.now();
+ const seek = ((now - firstFrame) / duration) % 1.0;
+
+ animation.seek(seek);
+ animation.render(skcanvas, [0, 0, 500, 500]);
+
+ surface.requestAnimationFrame(drawFrame);
+ }
+ surface.requestAnimationFrame(drawFrame);
+}
\ No newline at end of file
diff --git a/third_party/skia/demos.skia.org/demos/web_worker/worker.js b/third_party/skia/demos.skia.org/demos/web_worker/worker.js
new file mode 100644
index 0000000..c403654
--- /dev/null
+++ b/third_party/skia/demos.skia.org/demos/web_worker/worker.js
@@ -0,0 +1,27 @@
+importScripts('https://unpkg.com/canvaskit-wasm@0.25.0/bin/full/canvaskit.js');
+importScripts('shared.js');
+
+const transferCanvasToOffscreenPromise =
+ new Promise((resolve) => addEventListener('message', resolve));
+const canvasKitInitPromise =
+ CanvasKitInit({locateFile: (file) => 'https://unpkg.com/canvaskit-wasm@0.25.0/bin/full/'+file});
+const skottieJsonPromise =
+ fetch('https://storage.googleapis.com/skia-cdn/misc/lego_loader.json')
+ .then((response) => response.text());
+
+Promise.all([
+ transferCanvasToOffscreenPromise,
+ canvasKitInitPromise,
+ skottieJsonPromise
+]).then(([
+ { data: { offscreenCanvas } },
+ CanvasKit,
+ jsonStr
+]) => {
+ const surface = CanvasKit.MakeWebGLCanvasSurface(offscreenCanvas, null);
+ if (!surface) {
+ throw 'Could not make canvas surface';
+ }
+
+ SkottieExample(CanvasKit, surface, jsonStr);
+});
diff --git a/third_party/skia/demos.skia.org/demos/webgpu/index.html b/third_party/skia/demos.skia.org/demos/webgpu/index.html
new file mode 100644
index 0000000..049c30f
--- /dev/null
+++ b/third_party/skia/demos.skia.org/demos/webgpu/index.html
@@ -0,0 +1,144 @@
+<!DOCTYPE html>
+<title>Web GPU Demo</title>
+<meta charset="utf-8" />
+<meta http-equiv="X-UA-Compatible" content="IE=edge">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<!-- For *.skia.org https://developer.chrome.com/origintrials/#/registration/2983494015644598273
+ Expires Nov 19, 2021
+ -->
+<meta http-equiv="origin-trial" content="AnRs8mYss+Awd1DPUg2VfjXJbw2087/Dysaa3L7JmrbzTkwoEr87cX3y0zUfTGOFSLKJLRqNEmFAwfy+uumVXQsAAABbeyJvcmlnaW4iOiJodHRwczovL3NraWEub3JnOjQ0MyIsImZlYXR1cmUiOiJXZWJHUFUiLCJleHBpcnkiOjE2NDMxNTUxOTksImlzU3ViZG9tYWluIjp0cnVlfQ==">
+
+<!-- For localhost:8123 https://developer.chrome.com/origintrials/#/registration/6568359319031513089
+ Expires Nov 19, 2021
+ -->
+<meta http-equiv="origin-trial" content="ArQyw1ckz8lMOAcs5BbhOVJh2A6KMhYL6w/rTjPNnViqZyfFhlyJ5hnuHARoCkS1ZKiJi+YbsFvPWy23ePkFMQgAAABJeyJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjgxMjMiLCJmZWF0dXJlIjoiV2ViR1BVIiwiZXhwaXJ5IjoxNjQzMTU1MTk5fQ==">
+
+<style>
+ canvas {
+ border: 1px dashed grey;
+ }
+</style>
+
+<body>
+ <h1>WebGPU Test</h1>
+ <pre id="log"></pre>
+
+ <canvas id=draw width=500 height=500></canvas>
+</body>
+
+<script type="text/javascript" charset="utf-8">
+ if ("gpu" in navigator) {
+ log("WebGPU detected")
+ WebGPUDemo();
+ } else {
+ log("No WebGPU support.")
+ }
+
+ function log(s) {
+ document.getElementById("log").innerText = s;
+ }
+
+ async function WebGPUDemo() {
+ // Adapted from https://github.com/austinEng/webgpu-samples/blob/main/src/sample/helloTriangle/main.ts
+ const adapter = await navigator.gpu.requestAdapter();
+ if (!adapter) {
+ log("Could not load an adapter. For Chrome, try running with --enable-features=Vulkan --enable-unsafe-webgpu");
+ return;
+ }
+
+ const device = await adapter.requestDevice();
+ console.log(adapter, device);
+ const canvas = document.getElementById("draw");
+ const context = canvas.getContext('webgpu');
+ if (!context) {
+ log("Could not load webgpu context");
+ return;
+ }
+ console.log(context);
+
+ const devicePixelRatio = window.devicePixelRatio || 1;
+ const presentationSize = [
+ canvas.clientWidth * devicePixelRatio,
+ canvas.clientHeight * devicePixelRatio,
+ ];
+ const presentationFormat = context.getPreferredFormat(adapter);
+
+ context.configure({
+ device,
+ format: presentationFormat,
+ size: presentationSize,
+ });
+
+ const triangleVertWGSL = `[[stage(vertex)]]
+fn main([[builtin(vertex_index)]] VertexIndex : u32)
+ -> [[builtin(position)]] vec4<f32> {
+ var pos = array<vec2<f32>, 3>(
+ vec2<f32>(0.0, 0.5),
+ vec2<f32>(-0.5, -0.5),
+ vec2<f32>(0.5, -0.5));
+
+ return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
+}`;
+
+ const redFragWGSL = `[[stage(fragment)]]
+fn main() -> [[location(0)]] vec4<f32> {
+ return vec4<f32>(1.0, 0.0, 0.0, 1.0);
+}`;
+
+ const pipeline = device.createRenderPipeline({
+ vertex: {
+ module: device.createShaderModule({
+ code: triangleVertWGSL,
+ }),
+ entryPoint: 'main',
+ },
+ fragment: {
+ module: device.createShaderModule({
+ code: redFragWGSL,
+ }),
+ entryPoint: 'main',
+ targets: [
+ {
+ format: presentationFormat,
+ },
+ ],
+ },
+ primitive: {
+ topology: 'triangle-list',
+ },
+ });
+
+ console.log(pipeline);
+
+ const startTime = Date.now();
+ function frame() {
+ const now = Date.now();
+ const commandEncoder = device.createCommandEncoder();
+ const textureView = context.getCurrentTexture().createView();
+
+ const renderPassDescriptor = {
+ colorAttachments: [
+ {
+ view: textureView,
+ loadValue: {
+ r: Math.abs(Math.sin((startTime - now) / 500)),
+ g: Math.abs(Math.sin((startTime - now) / 600)),
+ b: Math.abs(Math.sin((startTime - now) / 700)),
+ a: 1.0 },
+ storeOp: 'store',
+ },
+ ],
+ };
+
+ const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
+ passEncoder.setPipeline(pipeline);
+ passEncoder.draw(3, 1, 0, 0);
+ passEncoder.endPass();
+
+ device.queue.submit([commandEncoder.finish()]);
+ requestAnimationFrame(frame);
+ }
+
+ requestAnimationFrame(frame);
+ }
+</script>
\ No newline at end of file