xref: /aosp_15_r20/external/skia/demos.skia.org/demos/spreadsheet/index.html (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1<!DOCTYPE html>
2<title>Spreadsheet Demo</title>
3<meta charset="utf-8" />
4<meta http-equiv="X-UA-Compatible" content="IE=edge">
5<meta name="viewport" content="width=device-width, initial-scale=1.0">
6<script type="text/javascript" src="https://unpkg.com/[email protected]/bin/full/canvaskit.js"></script>
7
8<style>
9  body {
10      background-color: black;
11  }
12  h1 {
13      color: white;
14  }
15  .hidden {
16      display: none;
17  }
18</style>
19
20<body>
21  <h1>Large canvas with many numbers, like a spreadsheet</h1>
22  <select id="numbers_impl">
23    <option value="fillText">&lt;canvas&gt; fillText</option>
24    <option value="drawGlyphs">CK drawGlyphs (tuned)</option>
25    <option value="drawText">CK drawText (naive)</option>
26  </select>
27
28  <canvas id=ck_canvas width=3840 height=2160 class="hidden"></canvas>
29  <canvas id=canvas_2d width=3840 height=2160></canvas>
30</body>
31
32<script type="text/javascript" charset="utf-8">
33  const ckLoaded = CanvasKitInit({ locateFile: (file) => 'https://unpkg.com/[email protected]/bin/full/' + file });
34
35  // This is the dimensions of a 4k screen.
36  const WIDTH = 3840, HEIGHT = 2160;
37  const ROWS = 144;
38  const ROW_HEIGHT = 15;
39  const COLS = 77;
40  const COL_WIDTH = 50;
41  const canvas2DEle = document.getElementById('canvas_2d');
42  const ckEle = document.getElementById('ck_canvas');
43
44  ckLoaded.then((CanvasKit) => {
45    const canvas2dCtx = canvas2DEle.getContext('2d');
46    const surface = CanvasKit.MakeCanvasSurface('ck_canvas');
47    if (!surface) {
48      throw 'Could not make surface';
49    }
50
51    const colorPaints = {
52      "grey": CanvasKit.Color(76, 76, 76),
53      "black": CanvasKit.BLACK,
54      "white": CanvasKit.WHITE,
55      "springGreen": CanvasKit.Color(0, 255, 127),
56      "tomato": CanvasKit.Color(255, 99, 71),
57    };
58    for (const name in colorPaints) {
59      const color = colorPaints[name];
60      const paint = new CanvasKit.Paint();
61      paint.setColor(color);
62      colorPaints[name] = paint;
63    }
64
65    const data = [];
66    for (let row = 0; row < ROWS; row++) {
67      data[row] = [];
68      for (let col = 0; col < COLS; col++) {
69        data[row][col] = Math.random() * Math.PI;
70      }
71    }
72
73    // Maybe use https://storage.googleapis.com/skia-cdn/google-web-fonts/NotoSans-Regular.ttf
74    const textFont = new CanvasKit.Font(null, 12);
75
76    const choice = document.getElementById("numbers_impl");
77
78    let frames = [];
79    const framesToMeasure = 10;
80    choice.addEventListener("change", () => {
81      frames = [];
82      if (choice.selectedIndex === 0) {
83        canvas2DEle.classList.remove('hidden');
84        ckEle.classList.add('hidden');
85      } else {
86        canvas2DEle.classList.add('hidden');
87        ckEle.classList.remove('hidden');
88      }
89    })
90    function drawFrame(canvas) {
91      if (frames && frames.length === framesToMeasure) {
92        // It is important to measure frame to frame time to account for the time spent by the
93        // GPU after our flush calls.
94        const deltas = [];
95        for (let i = 0; i< frames.length-1;i++) {
96          deltas.push(frames[i+1] - frames[i]);
97        }
98        console.log(`First ${framesToMeasure} frames`, deltas);
99        console.log((frames[framesToMeasure-1] - frames[0]) / framesToMeasure);
100        frames = null;
101      } else if (frames) {
102        frames.push(performance.now());
103      }
104
105      if (choice.selectedIndex === 2) {
106        canvas.clear(CanvasKit.BLACK);
107        drawTextImpl(canvas);
108      } else if (choice.selectedIndex === 1) {
109        canvas.clear(CanvasKit.BLACK);
110        drawGlyphsImpl(canvas);
111      } else {
112        fillTextImpl(canvas2dCtx);
113      }
114
115      surface.requestAnimationFrame(drawFrame)
116    }
117
118    function drawTextImpl(canvas) {
119      const timer = performance.now() / 10000;
120      for (let row = 0; row < ROWS; row++) {
121        if (row % 2) {
122          canvas.drawRect(CanvasKit.XYWHRect(0, row * ROW_HEIGHT + 2, WIDTH, ROW_HEIGHT), colorPaints["grey"]);
123        }
124        for (let col = 0; col < COLS; col++) {
125          let n = Math.abs(Math.sin(timer + data[row][col]));
126          let useWhiteFont = true;
127          if (n < 0.05) {
128            canvas.drawRect(CanvasKit.XYWHRect(col * COL_WIDTH - 2, (row - 1) * ROW_HEIGHT + 2, COL_WIDTH, ROW_HEIGHT), colorPaints["tomato"]);
129            useWhiteFont = false;
130          } else if (n > 0.95) {
131            canvas.drawRect(CanvasKit.XYWHRect(col * COL_WIDTH - 2, (row - 1) * ROW_HEIGHT + 2, COL_WIDTH, ROW_HEIGHT), colorPaints["springGreen"]);
132            useWhiteFont = false;
133          }
134          const str = n.toFixed(4);
135          canvas.drawText(str, col * COL_WIDTH, row * ROW_HEIGHT,
136            useWhiteFont ? colorPaints["white"] : colorPaints["black"], textFont);
137        }
138      }
139    }
140
141    //====================================================================================
142    const alphabet = "0.123456789 ";
143    const glyphIDs = textFont.getGlyphIDs(alphabet);
144    // These are all 7 with current font and size
145    const advances = textFont.getGlyphWidths(glyphIDs);
146
147
148    const charsPerDataPoint = 6; // leading 0, period, 4 decimal places
149    const glyphIDsMObj = CanvasKit.MallocGlyphIDs(ROWS * COLS * charsPerDataPoint);
150    let wasmGlyphIDArr = glyphIDsMObj.toTypedArray();
151    const glyphLocationsMObj = CanvasKit.Malloc(Float32Array, glyphIDsMObj.length * 2);
152    let wasmGlyphLocations = glyphLocationsMObj.toTypedArray();
153
154    function dataToGlyphs(n, outputBuffer, offset) {
155      const s = n.toFixed(4);
156      outputBuffer[offset] = glyphIDs[0]; // Always a leading 0
157      outputBuffer[offset+1] = glyphIDs[1]; // Always a decimal place
158      for (let i = 2; i< charsPerDataPoint; i++) {
159        outputBuffer[offset+i] = glyphIDs[alphabet.indexOf(s[i])];
160      }
161    }
162    const spaceIndex = alphabet.length - 1;
163    function blankOut(outputBuffer, offset) {
164      for (let i = 0; i< charsPerDataPoint; i++) {
165        outputBuffer[offset+i] = glyphIDs[spaceIndex];
166      }
167    }
168
169    for (let row = 0; row < ROWS; row++) {
170      for (let col = 0; col < COLS; col++) {
171        for (let i = 0; i < charsPerDataPoint; i++) {
172          const offset = (col + row * COLS) * charsPerDataPoint * 2;
173          wasmGlyphLocations[offset + i * 2] = col * COL_WIDTH + i * advances[0];
174          wasmGlyphLocations[offset + i * 2 + 1] = row * ROW_HEIGHT;
175        }
176      }
177    }
178
179    const greyGlyphIDsMObj = CanvasKit.MallocGlyphIDs(charsPerDataPoint);
180
181    function drawGlyphsImpl(canvas) {
182      wasmGlyphIDArr = glyphIDsMObj.toTypedArray();
183      let wasmGreyGlyphIDsArr = greyGlyphIDsMObj.toTypedArray();
184
185      const timer = performance.now() / 10000;
186      for (let row = 0; row < ROWS; row++) {
187        if (row % 2) {
188          canvas.drawRect(CanvasKit.XYWHRect(0, row * ROW_HEIGHT + 2, WIDTH, ROW_HEIGHT), colorPaints["grey"]);
189        }
190        for (let col = 0; col < COLS; col++) {
191          const n = Math.abs(Math.sin(timer + data[row][col]));
192          let useWhiteFont = true;
193          if (n < 0.05) {
194            canvas.drawRect(CanvasKit.XYWHRect(col * COL_WIDTH - 2, (row - 1) * ROW_HEIGHT + 2, COL_WIDTH, ROW_HEIGHT), colorPaints["tomato"]);
195            useWhiteFont = false;
196          } else if (n > 0.95) {
197            canvas.drawRect(CanvasKit.XYWHRect(col * COL_WIDTH - 2, (row - 1) * ROW_HEIGHT + 2, COL_WIDTH, ROW_HEIGHT), colorPaints["springGreen"]);
198            useWhiteFont = false;
199          }
200
201          const offset = (col + row * COLS) * charsPerDataPoint;
202          if (useWhiteFont) {
203            dataToGlyphs(n, wasmGlyphIDArr, offset);
204          } else {
205            blankOut(wasmGlyphIDArr, offset);
206            dataToGlyphs(n, wasmGreyGlyphIDsArr, 0);
207            canvas.drawGlyphs(wasmGreyGlyphIDsArr,
208              glyphLocationsMObj.subarray(offset*2, (offset + charsPerDataPoint) * 2),
209              0, 0, textFont, colorPaints["grey"]
210            );
211          }
212        }
213      }
214      canvas.drawGlyphs(wasmGlyphIDArr, glyphLocationsMObj, 0, 0, textFont, colorPaints["white"]);
215    }
216
217    function fillTextImpl(ctx) {
218      ctx.font = '12px monospace';
219      ctx.fillStyle = 'black';
220      ctx.fillRect(0, 0, WIDTH, HEIGHT);
221      const timer = performance.now() / 10000;
222      for (let row = 0; row < ROWS; row++) {
223        if (row % 2) {
224          ctx.fillStyle = 'rgb(76,76,76)';
225          ctx.fillRect(0, row * ROW_HEIGHT + 2, WIDTH, ROW_HEIGHT);
226        }
227        for (let col = 0; col < COLS; col++) {
228          let n = Math.abs(Math.sin(timer + data[row][col]));
229          let useWhiteFont = true;
230          if (n < 0.05) {
231            ctx.fillStyle = 'rgb(255, 99, 71)';
232            ctx.fillRect(col * COL_WIDTH - 2, (row - 1) * ROW_HEIGHT + 2, COL_WIDTH, ROW_HEIGHT);
233            useWhiteFont = false;
234          } else if (n > 0.95) {
235            ctx.fillStyle = 'rgb(0, 255, 127)';
236            ctx.fillRect(col * COL_WIDTH - 2, (row - 1) * ROW_HEIGHT + 2, COL_WIDTH, ROW_HEIGHT);
237            useWhiteFont = false;
238          }
239          const str = n.toFixed(4);
240          if (useWhiteFont) {
241            ctx.fillStyle = 'white';
242          } else {
243            ctx.fillStyle = 'black';
244          }
245          ctx.fillText(str, col * COL_WIDTH, row * ROW_HEIGHT);
246        }
247      }
248    }
249
250    surface.requestAnimationFrame(drawFrame);
251  });
252</script>