| <!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> |