skia2/modules/canvaskit/npm_build/shaping.html
Kevin Lubick ef3d6af042 [infra] Add POC Bazel rules for CanvasKit
Many things are not enabled currently (e.g. Skottie, Paragraph),
but we can render many APIs using WebGL.

To turn on Paragraph, etc, we'll need to tackle fonts, which
is a separate effort.

This also changes where the build artifacts go. ./build/ is
easier to deal with than the old way of sticking them in
./npm_build/bin

Change-Id: Ia377360af580a887d03630670438fea2e3157e90
Bug: skia:12541
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/470682
Reviewed-by: Leandro Lovisolo <lovisolo@google.com>
Reviewed-by: Ben Wagner <bungeman@google.com>
2021-11-12 21:38:46 +00:00

180 lines
6.7 KiB
HTML
Raw Blame History

<!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>