xref: /aosp_15_r20/external/skia/modules/canvaskit/npm_build/shaping.html (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1*c8dee2aaSAndroid Build Coastguard Worker<!DOCTYPE html>
2*c8dee2aaSAndroid Build Coastguard Worker<title>WIP Shaping in JS Demo</title>
3*c8dee2aaSAndroid Build Coastguard Worker<meta charset="utf-8" />
4*c8dee2aaSAndroid Build Coastguard Worker<meta http-equiv="X-UA-Compatible" content="IE=edge">
5*c8dee2aaSAndroid Build Coastguard Worker<meta name="viewport" content="width=device-width, initial-scale=1.0">
6*c8dee2aaSAndroid Build Coastguard Worker
7*c8dee2aaSAndroid Build Coastguard Worker<style>
8*c8dee2aaSAndroid Build Coastguard Worker  canvas {
9*c8dee2aaSAndroid Build Coastguard Worker    border: 1px dashed #AAA;
10*c8dee2aaSAndroid Build Coastguard Worker  }
11*c8dee2aaSAndroid Build Coastguard Worker
12*c8dee2aaSAndroid Build Coastguard Worker  #input {
13*c8dee2aaSAndroid Build Coastguard Worker    height: 300px;
14*c8dee2aaSAndroid Build Coastguard Worker  }
15*c8dee2aaSAndroid Build Coastguard Worker
16*c8dee2aaSAndroid Build Coastguard Worker</style>
17*c8dee2aaSAndroid Build Coastguard Worker
18*c8dee2aaSAndroid Build Coastguard Worker<h2> (Really Bad) Shaping in JS </h2>
19*c8dee2aaSAndroid Build Coastguard Worker<textarea id=input></textarea>
20*c8dee2aaSAndroid Build Coastguard Worker<canvas id=shaped_text width=300 height=300></canvas>
21*c8dee2aaSAndroid Build Coastguard Worker
22*c8dee2aaSAndroid Build Coastguard Worker<script type="text/javascript" src="/build/canvaskit.js"></script>
23*c8dee2aaSAndroid Build Coastguard Worker
24*c8dee2aaSAndroid Build Coastguard Worker<script type="text/javascript" charset="utf-8">
25*c8dee2aaSAndroid Build Coastguard Worker
26*c8dee2aaSAndroid Build Coastguard Worker  let CanvasKit = null;
27*c8dee2aaSAndroid Build Coastguard Worker  const cdn = 'https://storage.googleapis.com/skia-cdn/misc/';
28*c8dee2aaSAndroid Build Coastguard Worker
29*c8dee2aaSAndroid Build Coastguard Worker  const ckLoaded = CanvasKitInit({locateFile: (file) => '/build/'+file});
30*c8dee2aaSAndroid Build Coastguard Worker  const loadFont = fetch(cdn + 'Roboto-Regular.ttf').then((response) => response.arrayBuffer());
31*c8dee2aaSAndroid Build Coastguard Worker  // This font works with interobang.
32*c8dee2aaSAndroid Build Coastguard Worker  //const loadFont = fetch('https://storage.googleapis.com/skia-cdn/google-web-fonts/SourceSansPro-Regular.ttf').then((response) => response.arrayBuffer());
33*c8dee2aaSAndroid Build Coastguard Worker
34*c8dee2aaSAndroid Build Coastguard Worker  document.getElementById('input').value = 'An aegis protected the fox!?';
35*c8dee2aaSAndroid Build Coastguard Worker
36*c8dee2aaSAndroid Build Coastguard Worker  // Examples requiring external resources.
37*c8dee2aaSAndroid Build Coastguard Worker  Promise.all([ckLoaded, loadFont]).then((results) => {
38*c8dee2aaSAndroid Build Coastguard Worker    ShapingJS(...results);
39*c8dee2aaSAndroid Build Coastguard Worker  });
40*c8dee2aaSAndroid Build Coastguard Worker
41*c8dee2aaSAndroid Build Coastguard Worker  function ShapingJS(CanvasKit, fontData) {
42*c8dee2aaSAndroid Build Coastguard Worker    if (!CanvasKit || !fontData) {
43*c8dee2aaSAndroid Build Coastguard Worker      return;
44*c8dee2aaSAndroid Build Coastguard Worker    }
45*c8dee2aaSAndroid Build Coastguard Worker
46*c8dee2aaSAndroid Build Coastguard Worker    const surface = CanvasKit.MakeCanvasSurface('shaped_text');
47*c8dee2aaSAndroid Build Coastguard Worker    if (!surface) {
48*c8dee2aaSAndroid Build Coastguard Worker      console.error('Could not make surface');
49*c8dee2aaSAndroid Build Coastguard Worker      return;
50*c8dee2aaSAndroid Build Coastguard Worker    }
51*c8dee2aaSAndroid Build Coastguard Worker
52*c8dee2aaSAndroid Build Coastguard Worker    const typeface = CanvasKit.Typeface.MakeTypefaceFromData(fontData);
53*c8dee2aaSAndroid Build Coastguard Worker
54*c8dee2aaSAndroid Build Coastguard Worker    const paint = new CanvasKit.Paint();
55*c8dee2aaSAndroid Build Coastguard Worker
56*c8dee2aaSAndroid Build Coastguard Worker    paint.setColor(CanvasKit.BLUE);
57*c8dee2aaSAndroid Build Coastguard Worker    paint.setStyle(CanvasKit.PaintStyle.Stroke);
58*c8dee2aaSAndroid Build Coastguard Worker
59*c8dee2aaSAndroid Build Coastguard Worker    const textPaint = new CanvasKit.Paint();
60*c8dee2aaSAndroid Build Coastguard Worker    const textFont = new CanvasKit.Font(typeface, 20);
61*c8dee2aaSAndroid Build Coastguard Worker    textFont.setLinearMetrics(true);
62*c8dee2aaSAndroid Build Coastguard Worker    textFont.setSubpixel(true);
63*c8dee2aaSAndroid Build Coastguard Worker    textFont.setHinting(CanvasKit.FontHinting.Slight);
64*c8dee2aaSAndroid Build Coastguard Worker
65*c8dee2aaSAndroid Build Coastguard Worker
66*c8dee2aaSAndroid Build Coastguard Worker    // Only care about these characters for now. If we get any unknown characters, we'll replace
67*c8dee2aaSAndroid Build Coastguard Worker    // them with the first glyph here (the replacement glyph).
68*c8dee2aaSAndroid Build Coastguard Worker    // We put the family code point second to make sure we handle >16 bit codes correctly.
69*c8dee2aaSAndroid Build Coastguard Worker    const alphabet = "���abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 _.,?!æ‽";
70*c8dee2aaSAndroid Build Coastguard Worker    const ids = textFont.getGlyphIDs(alphabet);
71*c8dee2aaSAndroid Build Coastguard Worker    const unknownCharacterGlyphID = ids[0];
72*c8dee2aaSAndroid Build Coastguard Worker    // char here means "string version of unicode code point". This makes the code below a bit more
73*c8dee2aaSAndroid Build Coastguard Worker    // readable than just integers. We just have to take care when reading these in that we don't
74*c8dee2aaSAndroid Build Coastguard Worker    // grab the second half of a 32 bit code unit.
75*c8dee2aaSAndroid Build Coastguard Worker    const charsToGlyphIDs = {};
76*c8dee2aaSAndroid Build Coastguard Worker    // Indexes in JS correspond to a 16 bit or 32 bit code unit. If a code point is wider than
77*c8dee2aaSAndroid Build Coastguard Worker    // 16 bits, it overflows into the next index. codePointAt will return a >16 bit value if the
78*c8dee2aaSAndroid Build Coastguard Worker    // given index overflows. We need to check for this and skip the next index lest we get a
79*c8dee2aaSAndroid Build Coastguard Worker    // garbage value (the second half of the Unicode code point.
80*c8dee2aaSAndroid Build Coastguard Worker    let glyphIdx = 0;
81*c8dee2aaSAndroid Build Coastguard Worker    for (let i = 0; i < alphabet.length; i++) {
82*c8dee2aaSAndroid Build Coastguard Worker      charsToGlyphIDs[alphabet[i]] = ids[glyphIdx];
83*c8dee2aaSAndroid Build Coastguard Worker      if (alphabet.codePointAt(i) > 65535) {
84*c8dee2aaSAndroid Build Coastguard Worker        i++; // skip the next index because that will be the second half of the code point.
85*c8dee2aaSAndroid Build Coastguard Worker      }
86*c8dee2aaSAndroid Build Coastguard Worker      glyphIdx++;
87*c8dee2aaSAndroid Build Coastguard Worker    }
88*c8dee2aaSAndroid Build Coastguard Worker
89*c8dee2aaSAndroid Build Coastguard Worker    // TODO(kjlubick): linear metrics so we get "correct" data (e.g. floats).
90*c8dee2aaSAndroid Build Coastguard Worker    const bounds = textFont.getGlyphBounds(ids, textPaint);
91*c8dee2aaSAndroid Build Coastguard Worker    const widths = textFont.getGlyphWidths(ids, textPaint);
92*c8dee2aaSAndroid Build Coastguard Worker    // See https://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html
93*c8dee2aaSAndroid Build Coastguard Worker    // Note that in Skia, y-down is positive, so it is common to see yMax below be negative.
94*c8dee2aaSAndroid Build Coastguard Worker    const glyphMetricsByGlyphID = {};
95*c8dee2aaSAndroid Build Coastguard Worker    for (let i = 0; i < ids.length; i++) {
96*c8dee2aaSAndroid Build Coastguard Worker      glyphMetricsByGlyphID[ids[i]] = {
97*c8dee2aaSAndroid Build Coastguard Worker        xMin: bounds[i*4],
98*c8dee2aaSAndroid Build Coastguard Worker        yMax: bounds[i*4 + 1],
99*c8dee2aaSAndroid Build Coastguard Worker        xMax: bounds[i*4 + 2],
100*c8dee2aaSAndroid Build Coastguard Worker        yMin: bounds[i*4 + 3],
101*c8dee2aaSAndroid Build Coastguard Worker        xAdvance: widths[i],
102*c8dee2aaSAndroid Build Coastguard Worker      };
103*c8dee2aaSAndroid Build Coastguard Worker    }
104*c8dee2aaSAndroid Build Coastguard Worker
105*c8dee2aaSAndroid Build Coastguard Worker    const shapeAndDrawText = (str, canvas, x, y, maxWidth, font, paint) => {
106*c8dee2aaSAndroid Build Coastguard Worker      const LINE_SPACING = 20;
107*c8dee2aaSAndroid Build Coastguard Worker
108*c8dee2aaSAndroid Build Coastguard Worker      // This is a conservative estimate - it can be shorter if we have ligatures code points
109*c8dee2aaSAndroid Build Coastguard Worker      // that span multiple 16bit words.
110*c8dee2aaSAndroid Build Coastguard Worker      const glyphs = CanvasKit.MallocGlyphIDs(str.length);
111*c8dee2aaSAndroid Build Coastguard Worker      let glyphArr = glyphs.toTypedArray();
112*c8dee2aaSAndroid Build Coastguard Worker
113*c8dee2aaSAndroid Build Coastguard Worker      // Turn the code points into glyphs, accounting for up to 2 ligatures.
114*c8dee2aaSAndroid Build Coastguard Worker      let shapedGlyphIdx = -1;
115*c8dee2aaSAndroid Build Coastguard Worker      for (let i = 0; i < str.length; i++) {
116*c8dee2aaSAndroid Build Coastguard Worker        const char = str[i];
117*c8dee2aaSAndroid Build Coastguard Worker        shapedGlyphIdx++;
118*c8dee2aaSAndroid Build Coastguard Worker        // POC Ligature support.
119*c8dee2aaSAndroid Build Coastguard Worker        if (charsToGlyphIDs['æ'] && char === 'a' && str[i+1] === 'e') {
120*c8dee2aaSAndroid Build Coastguard Worker          glyphArr[shapedGlyphIdx] = charsToGlyphIDs['æ'];
121*c8dee2aaSAndroid Build Coastguard Worker          i++; // skip next code point
122*c8dee2aaSAndroid Build Coastguard Worker          continue;
123*c8dee2aaSAndroid Build Coastguard Worker        }
124*c8dee2aaSAndroid Build Coastguard Worker        if (charsToGlyphIDs['‽'] && (
125*c8dee2aaSAndroid Build Coastguard Worker            (char === '?' && str[i+1] === '!') || (char === '!' && str[i+1] === '?' ))) {
126*c8dee2aaSAndroid Build Coastguard Worker          glyphArr[shapedGlyphIdx] = charsToGlyphIDs['‽'];
127*c8dee2aaSAndroid Build Coastguard Worker          i++; // skip next code point
128*c8dee2aaSAndroid Build Coastguard Worker          continue;
129*c8dee2aaSAndroid Build Coastguard Worker        }
130*c8dee2aaSAndroid Build Coastguard Worker        glyphArr[shapedGlyphIdx] = charsToGlyphIDs[char] || unknownCharacterGlyphID;
131*c8dee2aaSAndroid Build Coastguard Worker        if (str.codePointAt(i) > 65535) {
132*c8dee2aaSAndroid Build Coastguard Worker          i++; // skip the next index because that will be the second half of the code point.
133*c8dee2aaSAndroid Build Coastguard Worker        }
134*c8dee2aaSAndroid Build Coastguard Worker      }
135*c8dee2aaSAndroid Build Coastguard Worker      // Trim down our array of glyphs to only the amount we have after ligatures and code points
136*c8dee2aaSAndroid Build Coastguard Worker      // that are > 16 bits.
137*c8dee2aaSAndroid Build Coastguard Worker      glyphArr = glyphs.subarray(0, shapedGlyphIdx+1);
138*c8dee2aaSAndroid Build Coastguard Worker
139*c8dee2aaSAndroid Build Coastguard Worker      // Break our glyphs into runs based on the maxWidth and the xAdvance.
140*c8dee2aaSAndroid Build Coastguard Worker      const glyphRuns = [];
141*c8dee2aaSAndroid Build Coastguard Worker      let currentRunStartIdx = 0;
142*c8dee2aaSAndroid Build Coastguard Worker      let currentWidth = 0;
143*c8dee2aaSAndroid Build Coastguard Worker      for (let i = 0; i < glyphArr.length; i++) {
144*c8dee2aaSAndroid Build Coastguard Worker        const nextGlyphWidth = glyphMetricsByGlyphID[glyphArr[i]].xAdvance;
145*c8dee2aaSAndroid Build Coastguard Worker        if (currentWidth + nextGlyphWidth > maxWidth) {
146*c8dee2aaSAndroid Build Coastguard Worker          glyphRuns.push(glyphs.subarray(currentRunStartIdx, i));
147*c8dee2aaSAndroid Build Coastguard Worker          currentRunStartIdx = i;
148*c8dee2aaSAndroid Build Coastguard Worker          currentWidth = 0;
149*c8dee2aaSAndroid Build Coastguard Worker        }
150*c8dee2aaSAndroid Build Coastguard Worker        currentWidth += nextGlyphWidth;
151*c8dee2aaSAndroid Build Coastguard Worker      }
152*c8dee2aaSAndroid Build Coastguard Worker      glyphRuns.push(glyphs.subarray(currentRunStartIdx, glyphArr.length));
153*c8dee2aaSAndroid Build Coastguard Worker
154*c8dee2aaSAndroid Build Coastguard Worker      // Draw all those runs.
155*c8dee2aaSAndroid Build Coastguard Worker      for (let i = 0; i < glyphRuns.length; i++) {
156*c8dee2aaSAndroid Build Coastguard Worker        const blob = CanvasKit.TextBlob.MakeFromGlyphs(glyphRuns[i], font);
157*c8dee2aaSAndroid Build Coastguard Worker        if (blob) {
158*c8dee2aaSAndroid Build Coastguard Worker          canvas.drawTextBlob(blob, x, y + LINE_SPACING*i, paint);
159*c8dee2aaSAndroid Build Coastguard Worker        }
160*c8dee2aaSAndroid Build Coastguard Worker        blob.delete();
161*c8dee2aaSAndroid Build Coastguard Worker      }
162*c8dee2aaSAndroid Build Coastguard Worker      CanvasKit.Free(glyphs);
163*c8dee2aaSAndroid Build Coastguard Worker    }
164*c8dee2aaSAndroid Build Coastguard Worker
165*c8dee2aaSAndroid Build Coastguard Worker    const drawFrame = (canvas) => {
166*c8dee2aaSAndroid Build Coastguard Worker      canvas.clear(CanvasKit.WHITE);
167*c8dee2aaSAndroid Build Coastguard Worker      canvas.drawText('a + e = ae (no ligature)',
168*c8dee2aaSAndroid Build Coastguard Worker        5, 30, textPaint, textFont);
169*c8dee2aaSAndroid Build Coastguard Worker      canvas.drawText('a + e = æ (hard-coded ligature)',
170*c8dee2aaSAndroid Build Coastguard Worker        5, 50, textPaint, textFont);
171*c8dee2aaSAndroid Build Coastguard Worker
172*c8dee2aaSAndroid Build Coastguard Worker      canvas.drawRect(CanvasKit.LTRBRect(10, 80, 280, 290), paint);
173*c8dee2aaSAndroid Build Coastguard Worker      shapeAndDrawText(document.getElementById('input').value, canvas, 15, 100, 265, textFont, textPaint);
174*c8dee2aaSAndroid Build Coastguard Worker
175*c8dee2aaSAndroid Build Coastguard Worker      surface.requestAnimationFrame(drawFrame)
176*c8dee2aaSAndroid Build Coastguard Worker    };
177*c8dee2aaSAndroid Build Coastguard Worker    surface.requestAnimationFrame(drawFrame);
178*c8dee2aaSAndroid Build Coastguard Worker  }
179*c8dee2aaSAndroid Build Coastguard Worker</script>
180