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="/build/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) => '/build/'+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 typeface = CanvasKit.Typeface.MakeTypefaceFromData(fontData); 53 54 const paint = new CanvasKit.Paint(); 55 56 paint.setColor(CanvasKit.BLUE); 57 paint.setStyle(CanvasKit.PaintStyle.Stroke); 58 59 const textPaint = new CanvasKit.Paint(); 60 const textFont = new CanvasKit.Font(typeface, 20); 61 textFont.setLinearMetrics(true); 62 textFont.setSubpixel(true); 63 textFont.setHinting(CanvasKit.FontHinting.Slight); 64 65 66 // Only care about these characters for now. If we get any unknown characters, we'll replace 67 // them with the first glyph here (the replacement glyph). 68 // We put the family code point second to make sure we handle >16 bit codes correctly. 69 const alphabet = "�abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 _.,?!æ‽"; 70 const ids = textFont.getGlyphIDs(alphabet); 71 const unknownCharacterGlyphID = ids[0]; 72 // char here means "string version of unicode code point". This makes the code below a bit more 73 // readable than just integers. We just have to take care when reading these in that we don't 74 // grab the second half of a 32 bit code unit. 75 const charsToGlyphIDs = {}; 76 // Indexes in JS correspond to a 16 bit or 32 bit code unit. If a code point is wider than 77 // 16 bits, it overflows into the next index. codePointAt will return a >16 bit value if the 78 // given index overflows. We need to check for this and skip the next index lest we get a 79 // garbage value (the second half of the Unicode code point. 80 let glyphIdx = 0; 81 for (let i = 0; i < alphabet.length; i++) { 82 charsToGlyphIDs[alphabet[i]] = ids[glyphIdx]; 83 if (alphabet.codePointAt(i) > 65535) { 84 i++; // skip the next index because that will be the second half of the code point. 85 } 86 glyphIdx++; 87 } 88 89 // TODO(kjlubick): linear metrics so we get "correct" data (e.g. floats). 90 const bounds = textFont.getGlyphBounds(ids, textPaint); 91 const widths = textFont.getGlyphWidths(ids, textPaint); 92 // See https://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html 93 // Note that in Skia, y-down is positive, so it is common to see yMax below be negative. 94 const glyphMetricsByGlyphID = {}; 95 for (let i = 0; i < ids.length; i++) { 96 glyphMetricsByGlyphID[ids[i]] = { 97 xMin: bounds[i*4], 98 yMax: bounds[i*4 + 1], 99 xMax: bounds[i*4 + 2], 100 yMin: bounds[i*4 + 3], 101 xAdvance: widths[i], 102 }; 103 } 104 105 const shapeAndDrawText = (str, canvas, x, y, maxWidth, font, paint) => { 106 const LINE_SPACING = 20; 107 108 // This is a conservative estimate - it can be shorter if we have ligatures code points 109 // that span multiple 16bit words. 110 const glyphs = CanvasKit.MallocGlyphIDs(str.length); 111 let glyphArr = glyphs.toTypedArray(); 112 113 // Turn the code points into glyphs, accounting for up to 2 ligatures. 114 let shapedGlyphIdx = -1; 115 for (let i = 0; i < str.length; i++) { 116 const char = str[i]; 117 shapedGlyphIdx++; 118 // POC Ligature support. 119 if (charsToGlyphIDs['æ'] && char === 'a' && str[i+1] === 'e') { 120 glyphArr[shapedGlyphIdx] = charsToGlyphIDs['æ']; 121 i++; // skip next code point 122 continue; 123 } 124 if (charsToGlyphIDs['‽'] && ( 125 (char === '?' && str[i+1] === '!') || (char === '!' && str[i+1] === '?' ))) { 126 glyphArr[shapedGlyphIdx] = charsToGlyphIDs['‽']; 127 i++; // skip next code point 128 continue; 129 } 130 glyphArr[shapedGlyphIdx] = charsToGlyphIDs[char] || unknownCharacterGlyphID; 131 if (str.codePointAt(i) > 65535) { 132 i++; // skip the next index because that will be the second half of the code point. 133 } 134 } 135 // Trim down our array of glyphs to only the amount we have after ligatures and code points 136 // that are > 16 bits. 137 glyphArr = glyphs.subarray(0, shapedGlyphIdx+1); 138 139 // Break our glyphs into runs based on the maxWidth and the xAdvance. 140 const glyphRuns = []; 141 let currentRunStartIdx = 0; 142 let currentWidth = 0; 143 for (let i = 0; i < glyphArr.length; i++) { 144 const nextGlyphWidth = glyphMetricsByGlyphID[glyphArr[i]].xAdvance; 145 if (currentWidth + nextGlyphWidth > maxWidth) { 146 glyphRuns.push(glyphs.subarray(currentRunStartIdx, i)); 147 currentRunStartIdx = i; 148 currentWidth = 0; 149 } 150 currentWidth += nextGlyphWidth; 151 } 152 glyphRuns.push(glyphs.subarray(currentRunStartIdx, glyphArr.length)); 153 154 // Draw all those runs. 155 for (let i = 0; i < glyphRuns.length; i++) { 156 const blob = CanvasKit.TextBlob.MakeFromGlyphs(glyphRuns[i], font); 157 if (blob) { 158 canvas.drawTextBlob(blob, x, y + LINE_SPACING*i, paint); 159 } 160 blob.delete(); 161 } 162 CanvasKit.Free(glyphs); 163 } 164 165 const drawFrame = (canvas) => { 166 canvas.clear(CanvasKit.WHITE); 167 canvas.drawText('a + e = ae (no ligature)', 168 5, 30, textPaint, textFont); 169 canvas.drawText('a + e = æ (hard-coded ligature)', 170 5, 50, textPaint, textFont); 171 172 canvas.drawRect(CanvasKit.LTRBRect(10, 80, 280, 290), paint); 173 shapeAndDrawText(document.getElementById('input').value, canvas, 15, 100, 265, textFont, textPaint); 174 175 surface.requestAnimationFrame(drawFrame) 176 }; 177 surface.requestAnimationFrame(drawFrame); 178 } 179</script> 180