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