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