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