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