1<!DOCTYPE html> 2<title>CanvasKit Paragraph (with & without ICU)</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<script type="text/javascript" src="/build/canvaskit.js"></script> 8 9<style> 10 canvas { 11 border: 1px dashed #AAA; 12 } 13 #withICU { 14 border-color: red; 15 } 16 #withoutICU { 17 border-color: green; 18 } 19 #sampleText { 20 width: 400px; 21 height: 200px; 22 } 23</style> 24 25<table> 26 <thead> 27 <th><h2 style="color: red;">With ICU</h2></th> 28 <th></th> 29 <th><h2 style="color: green;">Without ICU</h2></th> 30 </thead> 31 <tr> 32 <td><canvas id="withICU" width=600 height=600></canvas></td> 33 <td style="width: 20px;"></td> 34 <td><canvas id="withoutICU" width=600 height=600 tabindex='-1'></canvas></td> 35 </tr> 36</table> 37 38<textarea id="sampleText">The لاquick (brown) fox 39واحد (اثنان) ثلاثة 40ate a hamburger. 41</textarea> 42 43<script type="text/javascript" charset="utf-8"> 44 45 var CanvasKit = null; 46 var fonts = null; 47 var sampleText = null; 48 49 var cdn = 'https://storage.googleapis.com/skia-cdn/misc/'; 50 51 const ckLoaded = CanvasKitInit({locateFile: (file) => '/build/'+file}); 52 53 const loadFonts = [ 54 fetch(cdn + 'Roboto-Regular.ttf').then((response) => response.arrayBuffer()), 55 fetch('https://fonts.gstatic.com/s/notoemoji/v26/bMrnmSyK7YY-MEu6aWjPDs-ar6uWaGWuob-r0jwvS-FGJCMY.ttf').then((response) => response.arrayBuffer()), 56 fetch('https://fonts.gstatic.com/s/notosansarabic/v18/nwpxtLGrOAZMl5nJ_wfgRg3DrWFZWsnVBJ_sS6tlqHHFlhQ5l3sQWIHPqzCfyGyvu3CBFQLaig.ttf').then((response) => response.arrayBuffer()), 57 ]; 58 59 let paragraphWithICU; 60 let paragraphWithoutICU; 61 62 Promise.all([ckLoaded, ...loadFonts]).then(([_CanvasKit, ..._fonts]) => { 63 CanvasKit = _CanvasKit; 64 fonts = _fonts; 65 66 const textarea = document.getElementById('sampleText'); 67 sampleText = textarea.value; 68 textarea.addEventListener('input', (e) => { 69 sampleText = e.target.value; 70 paragraphWithICU = ParagraphWithICU(); 71 paragraphWithoutICU = ParagraphWithoutICU(); 72 }); 73 74 paragraphWithICU = ParagraphWithICU(); 75 paragraphWithoutICU = ParagraphWithoutICU(); 76 77 continuousRendering('withICU', () => paragraphWithICU); 78 continuousRendering('withoutICU', () => paragraphWithoutICU); 79 }); 80 81 const fontFamilies = [ 82 'Roboto', 83 'Noto Emoji', 84 'Noto Sans Arabic', 85 ]; 86 87 function continuousRendering(elementId, getParagraph) { 88 const surface = CanvasKit.MakeCanvasSurface(elementId); 89 if (!surface) { 90 throw new Error('Could not make surface'); 91 } 92 93 function drawFrame(canvas) { 94 drawParagraph(canvas, getParagraph()); 95 surface.requestAnimationFrame(drawFrame); 96 } 97 surface.requestAnimationFrame(drawFrame); 98 } 99 100 function ParagraphWithICU() { 101 if (!CanvasKit || !fonts) { 102 throw new Error('CanvasKit or fonts not loaded'); 103 } 104 105 const fontMgr = CanvasKit.FontMgr.FromData(fonts); 106 107 const paraStyle = new CanvasKit.ParagraphStyle({ 108 textStyle: { 109 color: CanvasKit.BLACK, 110 fontFamilies: fontFamilies, 111 fontSize: 50, 112 }, 113 textAlign: CanvasKit.TextAlign.Left, 114 maxLines: 4, 115 }); 116 117 const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr); 118 builder.addText(sampleText); 119 const paragraph = builder.build(); 120 121 fontMgr.delete(); 122 123 return paragraph; 124 } 125 126 function ParagraphWithoutICU() { 127 if (!CanvasKit || !fonts) { 128 throw new Error('CanvasKit or fonts not loaded'); 129 } 130 131 const fontMgr = CanvasKit.FontMgr.FromData(fonts); 132 133 const paraStyle = new CanvasKit.ParagraphStyle({ 134 textStyle: { 135 color: CanvasKit.BLACK, 136 fontFamilies: fontFamilies, 137 fontSize: 50, 138 }, 139 maxLines: 4, 140 textAlign: CanvasKit.TextAlign.Left, 141 }); 142 143 const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr); 144 builder.addText(sampleText); 145 146 const text = sampleText; 147 148 // Pass the entire text as one word. It's only used for the method 149 // getWords. 150 const mallocedWords = CanvasKit.Malloc(Uint32Array, 2); 151 mallocedWords.toTypedArray().set([0, text.length]); 152 153 const graphemeBoundaries = getGraphemeBoundaries(text); 154 const mallocedGraphemes = CanvasKit.Malloc(Uint32Array, graphemeBoundaries.length); 155 mallocedGraphemes.toTypedArray().set(graphemeBoundaries); 156 157 const lineBreaks = getLineBreaks(text); 158 const mallocedLineBreaks = CanvasKit.Malloc(Uint32Array, lineBreaks.length); 159 mallocedLineBreaks.toTypedArray().set(lineBreaks); 160 161 console.log('RequiresClientICU:', CanvasKit.ParagraphBuilder.RequiresClientICU()); 162 163 builder.setWordsUtf16(mallocedWords); 164 builder.setGraphemeBreaksUtf16(mallocedGraphemes); 165 builder.setLineBreaksUtf16(mallocedLineBreaks); 166 const paragraph = builder.build(); 167 168 fontMgr.delete(); 169 170 return paragraph; 171 } 172 173 function drawParagraph(canvas, paragraph) { 174 const fontPaint = new CanvasKit.Paint(); 175 fontPaint.setStyle(CanvasKit.PaintStyle.Fill); 176 fontPaint.setAntiAlias(true); 177 178 canvas.clear(CanvasKit.WHITE); 179 const wrapTo = 350 + 150 * Math.sin(Date.now() / 4000); 180 paragraph.layout(wrapTo); 181 182 const rects = [ 183 ...paragraph.getRectsForRange(2, 8, CanvasKit.RectHeightStyle.Tight, CanvasKit.RectWidthStyle.Tight), 184 ...paragraph.getRectsForRange(12, 16, CanvasKit.RectHeightStyle.Tight, CanvasKit.RectWidthStyle.Tight), 185 ]; 186 const rectPaint = new CanvasKit.Paint(); 187 const colors = [CanvasKit.CYAN, CanvasKit.MAGENTA, CanvasKit.BLUE, CanvasKit.YELLOW]; 188 for (const rect of rects) { 189 rectPaint.setColor(colors.shift() || CanvasKit.RED); 190 canvas.drawRect(rect, rectPaint); 191 } 192 193 canvas.drawParagraph(paragraph, 0, 0); 194 195 canvas.drawLine(wrapTo, 0, wrapTo, 400, fontPaint); 196 } 197 198 const SOFT = 0; 199 const HARD = 1; 200 201 function getLineBreaks(text) { 202 const breaks = [0, SOFT]; 203 204 const iterator = new Intl.v8BreakIterator(['en'], {type: 'line'}); 205 iterator.adoptText(text); 206 iterator.first(); 207 208 while (iterator.next() != -1) { 209 breaks.push(iterator.current(), getBreakType(iterator.breakType())); 210 } 211 212 return breaks; 213 } 214 215 function getBreakType(v8BreakType) { 216 return v8BreakType == 'none' ? SOFT : HARD; 217 } 218 219 function getGraphemeBoundaries(text) { 220 const segmenter = new Intl.Segmenter(['en'], {type: 'grapheme'}); 221 const segments = segmenter.segment(text); 222 223 const graphemeBoundaries = []; 224 for (const segment of segments) { 225 graphemeBoundaries.push(segment.index); 226 } 227 graphemeBoundaries.push(text.length); 228 return graphemeBoundaries; 229 } 230</script> 231