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