180 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			HTML
		
	
	
	
			
		
		
	
	
			180 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			HTML
		
	
	
	
| <!DOCTYPE html>
 | ||
| <title>WIP Shaping in JS Demo</title>
 | ||
| <meta charset="utf-8" />
 | ||
| <meta http-equiv="X-UA-Compatible" content="IE=edge">
 | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | ||
| 
 | ||
| <style>
 | ||
|   canvas {
 | ||
|     border: 1px dashed #AAA;
 | ||
|   }
 | ||
| 
 | ||
|   #input {
 | ||
|     height: 300px;
 | ||
|   }
 | ||
| 
 | ||
| </style>
 | ||
| 
 | ||
| <h2> (Really Bad) Shaping in JS </h2>
 | ||
| <textarea id=input></textarea>
 | ||
| <canvas id=shaped_text width=300 height=300></canvas>
 | ||
| 
 | ||
| <script type="text/javascript" src="/build/canvaskit.js"></script>
 | ||
| 
 | ||
| <script type="text/javascript" charset="utf-8">
 | ||
| 
 | ||
|   let CanvasKit = null;
 | ||
|   const cdn = 'https://storage.googleapis.com/skia-cdn/misc/';
 | ||
| 
 | ||
|   const ckLoaded = CanvasKitInit({locateFile: (file) => '/build/'+file});
 | ||
|   const loadFont = fetch(cdn + 'Roboto-Regular.ttf').then((response) => response.arrayBuffer());
 | ||
|   // This font works with interobang.
 | ||
|   //const loadFont = fetch('https://storage.googleapis.com/skia-cdn/google-web-fonts/SourceSansPro-Regular.ttf').then((response) => response.arrayBuffer());
 | ||
| 
 | ||
|   document.getElementById('input').value = 'An aegis protected the fox!?';
 | ||
| 
 | ||
|   // Examples requiring external resources.
 | ||
|   Promise.all([ckLoaded, loadFont]).then((results) => {
 | ||
|     ShapingJS(...results);
 | ||
|   });
 | ||
| 
 | ||
|   function ShapingJS(CanvasKit, fontData) {
 | ||
|     if (!CanvasKit || !fontData) {
 | ||
|       return;
 | ||
|     }
 | ||
| 
 | ||
|     const surface = CanvasKit.MakeCanvasSurface('shaped_text');
 | ||
|     if (!surface) {
 | ||
|       console.error('Could not make surface');
 | ||
|       return;
 | ||
|     }
 | ||
| 
 | ||
|     const typeface = CanvasKit.Typeface.MakeFreeTypeFaceFromData(fontData);
 | ||
| 
 | ||
|     const paint = new CanvasKit.Paint();
 | ||
| 
 | ||
|     paint.setColor(CanvasKit.BLUE);
 | ||
|     paint.setStyle(CanvasKit.PaintStyle.Stroke);
 | ||
| 
 | ||
|     const textPaint = new CanvasKit.Paint();
 | ||
|     const textFont = new CanvasKit.Font(typeface, 20);
 | ||
|     textFont.setLinearMetrics(true);
 | ||
|     textFont.setSubpixel(true);
 | ||
|     textFont.setHinting(CanvasKit.FontHinting.Slight);
 | ||
| 
 | ||
| 
 | ||
|     // Only care about these characters for now. If we get any unknown characters, we'll replace
 | ||
|     // them with the first glyph here (the replacement glyph).
 | ||
|     // We put the family code point second to make sure we handle >16 bit codes correctly.
 | ||
|     const alphabet = "<22>👪abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 _.,?!æ‽";
 | ||
|     const ids = textFont.getGlyphIDs(alphabet);
 | ||
|     const unknownCharacterGlyphID = ids[0];
 | ||
|     // char here means "string version of unicode code point". This makes the code below a bit more
 | ||
|     // readable than just integers. We just have to take care when reading these in that we don't
 | ||
|     // grab the second half of a 32 bit code unit.
 | ||
|     const charsToGlyphIDs = {};
 | ||
|     // Indexes in JS correspond to a 16 bit or 32 bit code unit. If a code point is wider than
 | ||
|     // 16 bits, it overflows into the next index. codePointAt will return a >16 bit value if the
 | ||
|     // given index overflows. We need to check for this and skip the next index lest we get a
 | ||
|     // garbage value (the second half of the Unicode code point.
 | ||
|     let glyphIdx = 0;
 | ||
|     for (let i = 0; i < alphabet.length; i++) {
 | ||
|       charsToGlyphIDs[alphabet[i]] = ids[glyphIdx];
 | ||
|       if (alphabet.codePointAt(i) > 65535) {
 | ||
|         i++; // skip the next index because that will be the second half of the code point.
 | ||
|       }
 | ||
|       glyphIdx++;
 | ||
|     }
 | ||
| 
 | ||
|     // TODO(kjlubick): linear metrics so we get "correct" data (e.g. floats).
 | ||
|     const bounds = textFont.getGlyphBounds(ids, textPaint);
 | ||
|     const widths = textFont.getGlyphWidths(ids, textPaint);
 | ||
|     // See https://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html
 | ||
|     // Note that in Skia, y-down is positive, so it is common to see yMax below be negative.
 | ||
|     const glyphMetricsByGlyphID = {};
 | ||
|     for (let i = 0; i < ids.length; i++) {
 | ||
|       glyphMetricsByGlyphID[ids[i]] = {
 | ||
|         xMin: bounds[i*4],
 | ||
|         yMax: bounds[i*4 + 1],
 | ||
|         xMax: bounds[i*4 + 2],
 | ||
|         yMin: bounds[i*4 + 3],
 | ||
|         xAdvance: widths[i],
 | ||
|       };
 | ||
|     }
 | ||
| 
 | ||
|     const shapeAndDrawText = (str, canvas, x, y, maxWidth, font, paint) => {
 | ||
|       const LINE_SPACING = 20;
 | ||
| 
 | ||
|       // This is a conservative estimate - it can be shorter if we have ligatures code points
 | ||
|       // that span multiple 16bit words.
 | ||
|       const glyphs = CanvasKit.MallocGlyphIDs(str.length);
 | ||
|       let glyphArr = glyphs.toTypedArray();
 | ||
| 
 | ||
|       // Turn the code points into glyphs, accounting for up to 2 ligatures.
 | ||
|       let shapedGlyphIdx = -1;
 | ||
|       for (let i = 0; i < str.length; i++) {
 | ||
|         const char = str[i];
 | ||
|         shapedGlyphIdx++;
 | ||
|         // POC Ligature support.
 | ||
|         if (charsToGlyphIDs['æ'] && char === 'a' && str[i+1] === 'e') {
 | ||
|           glyphArr[shapedGlyphIdx] = charsToGlyphIDs['æ'];
 | ||
|           i++; // skip next code point
 | ||
|           continue;
 | ||
|         }
 | ||
|         if (charsToGlyphIDs['‽'] && (
 | ||
|             (char === '?' && str[i+1] === '!') || (char === '!' && str[i+1] === '?' ))) {
 | ||
|           glyphArr[shapedGlyphIdx] = charsToGlyphIDs['‽'];
 | ||
|           i++; // skip next code point
 | ||
|           continue;
 | ||
|         }
 | ||
|         glyphArr[shapedGlyphIdx] = charsToGlyphIDs[char] || unknownCharacterGlyphID;
 | ||
|         if (str.codePointAt(i) > 65535) {
 | ||
|           i++; // skip the next index because that will be the second half of the code point.
 | ||
|         }
 | ||
|       }
 | ||
|       // Trim down our array of glyphs to only the amount we have after ligatures and code points
 | ||
|       // that are > 16 bits.
 | ||
|       glyphArr = glyphs.subarray(0, shapedGlyphIdx+1);
 | ||
| 
 | ||
|       // Break our glyphs into runs based on the maxWidth and the xAdvance.
 | ||
|       const glyphRuns = [];
 | ||
|       let currentRunStartIdx = 0;
 | ||
|       let currentWidth = 0;
 | ||
|       for (let i = 0; i < glyphArr.length; i++) {
 | ||
|         const nextGlyphWidth = glyphMetricsByGlyphID[glyphArr[i]].xAdvance;
 | ||
|         if (currentWidth + nextGlyphWidth > maxWidth) {
 | ||
|           glyphRuns.push(glyphs.subarray(currentRunStartIdx, i));
 | ||
|           currentRunStartIdx = i;
 | ||
|           currentWidth = 0;
 | ||
|         }
 | ||
|         currentWidth += nextGlyphWidth;
 | ||
|       }
 | ||
|       glyphRuns.push(glyphs.subarray(currentRunStartIdx, glyphArr.length));
 | ||
| 
 | ||
|       // Draw all those runs.
 | ||
|       for (let i = 0; i < glyphRuns.length; i++) {
 | ||
|         const blob = CanvasKit.TextBlob.MakeFromGlyphs(glyphRuns[i], font);
 | ||
|         if (blob) {
 | ||
|           canvas.drawTextBlob(blob, x, y + LINE_SPACING*i, paint);
 | ||
|         }
 | ||
|         blob.delete();
 | ||
|       }
 | ||
|       CanvasKit.Free(glyphs);
 | ||
|     }
 | ||
| 
 | ||
|     const drawFrame = (canvas) => {
 | ||
|       canvas.clear(CanvasKit.WHITE);
 | ||
|       canvas.drawText('a + e = ae (no ligature)',
 | ||
|         5, 30, textPaint, textFont);
 | ||
|       canvas.drawText('a + e = æ (hard-coded ligature)',
 | ||
|         5, 50, textPaint, textFont);
 | ||
| 
 | ||
|       canvas.drawRect(CanvasKit.LTRBRect(10, 80, 280, 290), paint);
 | ||
|       shapeAndDrawText(document.getElementById('input').value, canvas, 15, 100, 265, textFont, textPaint);
 | ||
| 
 | ||
|       surface.requestAnimationFrame(drawFrame)
 | ||
|     };
 | ||
|     surface.requestAnimationFrame(drawFrame);
 | ||
|   }
 | ||
| </script>
 |