From d3cfbcae10eb903841819cb6fbf0ad6e35f7f88a Mon Sep 17 00:00:00 2001 From: Kevin Lubick Date: Fri, 15 Mar 2019 15:36:29 -0400 Subject: [PATCH] [canvaskit] Add TextOnPath helper to TextBlob This adds the pieces needed to accomplish this, and although clients could do it, I figured it would be nice to expose as a universal tool (on TextBlob). Bug: skia: Change-Id: Id5d61744973de2da75049d33d40e1dc442c2442c Reviewed-on: https://skia-review.googlesource.com/c/skia/+/201601 Reviewed-by: Ben Wagner --- modules/canvaskit/CHANGELOG.md | 4 + modules/canvaskit/canvaskit/example.html | 38 ++++++++ modules/canvaskit/canvaskit_bindings.cpp | 58 ++++++++++++ modules/canvaskit/externs.js | 22 +++++ modules/canvaskit/helper.js | 37 ++++++++ modules/canvaskit/interface.js | 112 ++++++++++++++++++++++ modules/canvaskit/tests/font.spec.js | 113 ++++++++++++++++++++--- 7 files changed, 373 insertions(+), 11 deletions(-) diff --git a/modules/canvaskit/CHANGELOG.md b/modules/canvaskit/CHANGELOG.md index 82387848c7..5488786707 100644 --- a/modules/canvaskit/CHANGELOG.md +++ b/modules/canvaskit/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + - `SkPathMeasure`, `RSXFormBuilder`, `SkFont.getWidths`, `SkTextBlob.MakeFromRSXform` + which were needed to add the helper function `SkTextBlob.MakeOnPath`. + ### Changed - Location in Skia Git repo now `modules/canvaskit` (was `experimental/canvaskit`) diff --git a/modules/canvaskit/canvaskit/example.html b/modules/canvaskit/canvaskit/example.html index bd75ded210..273859dea0 100644 --- a/modules/canvaskit/canvaskit/example.html +++ b/modules/canvaskit/canvaskit/example.html @@ -47,6 +47,7 @@

CanvasKit can allow for text shaping (e.g. breaking, kerning)

+

Skottie

@@ -116,6 +117,7 @@ TextShapingAPI1(CanvasKit, notoserifData); TextShapingAPI2(CanvasKit, notoserifData); + TextOnPathAPI1(CanvasKit); ParticlesAPI1(CanvasKit); @@ -1279,6 +1281,42 @@ preventScrolling(document.getElementById('shape2')); } + function TextOnPathAPI1(CanvasKit) { + const surface = CanvasKit.MakeCanvasSurface('textonpath'); + if (!surface) { + console.error('Could not make surface'); + return; + } + const context = CanvasKit.currentContext(); + const canvas = surface.getCanvas(); + const paint = new CanvasKit.SkPaint(); + paint.setStyle(CanvasKit.PaintStyle.Stroke); + + const font = new CanvasKit.SkFont(null, 24); + const fontPaint = new CanvasKit.SkPaint(); + fontPaint.setStyle(CanvasKit.PaintStyle.Fill); + + + const arc = new CanvasKit.SkPath(); + arc.arcTo(CanvasKit.LTRBRect(20, 40, 280, 300), -160, 140, true); + arc.lineTo(210, 140); + arc.arcTo(CanvasKit.LTRBRect(20, 0, 280, 260), 160, -140, true); + + const str = 'This téxt should follow the curve across contours...'; + const textBlob = CanvasKit.SkTextBlob.MakeOnPath(str, arc, font); + + canvas.drawPath(arc, paint); + canvas.drawTextBlob(textBlob, 0, 0, fontPaint); + + surface.flush(); + + textBlob.delete(); + arc.delete(); + paint.delete(); + font.delete(); + fontPaint.delete(); + } + function ParticlesAPI1(CanvasKit) { const surface = CanvasKit.MakeCanvasSurface('particles'); if (!surface) { diff --git a/modules/canvaskit/canvaskit_bindings.cpp b/modules/canvaskit/canvaskit_bindings.cpp index 172cc2ff14..19b82a5ee5 100644 --- a/modules/canvaskit/canvaskit_bindings.cpp +++ b/modules/canvaskit/canvaskit_bindings.cpp @@ -29,6 +29,7 @@ #include "SkParsePath.h" #include "SkPath.h" #include "SkPathEffect.h" +#include "SkPathMeasure.h" #include "SkPathOps.h" #include "SkScalar.h" #include "SkShader.h" @@ -534,6 +535,11 @@ void drawShapedText(SkCanvas& canvas, ShapedText st, SkScalar x, canvas.drawTextBlob(st.blob(), x, y, paint); } +// This is simpler than dealing with an SkPoint and SkVector +struct PosTan { + SkScalar px, py, tx, ty; +}; + // These objects have private destructors / delete mthods - I don't think // we need to do anything other than tell emscripten to do nothing. namespace emscripten { @@ -831,6 +837,25 @@ EMSCRIPTEN_BINDINGS(Skia) { .function("getSize", &SkFont::getSize) .function("getSkewX", &SkFont::getSkewX) .function("getTypeface", &SkFont::getTypeface, allow_raw_pointers()) + .function("_getWidths", optional_override([](SkFont& self, uintptr_t /* char* */ sptr, + size_t strLen, size_t expectedCodePoints, + uintptr_t /* SkScalar* */ wptr) -> bool { + char* str = reinterpret_cast(sptr); + SkScalar* widths = reinterpret_cast(wptr); + + SkGlyphID* glyphStorage = new SkGlyphID[expectedCodePoints]; + int actualCodePoints = self.textToGlyphs(str, strLen, SkTextEncoding::kUTF8, + glyphStorage, expectedCodePoints); + if (actualCodePoints != expectedCodePoints) { + SkDebugf("Actually %d glyphs, expected only %d\n", + actualCodePoints, expectedCodePoints); + return false; + } + + self.getWidths(glyphStorage, actualCodePoints, widths); + delete[] glyphStorage; + return true; + })) .function("measureText", optional_override([](SkFont& self, std::string text) { // TODO(kjlubick): This does not work well for non-ascii // Need to maybe add a helper in interface.js that supports UTF-8 @@ -971,6 +996,21 @@ EMSCRIPTEN_BINDINGS(Skia) { #endif ; + class_("SkPathMeasure") + .constructor() + .function("getLength", &SkPathMeasure::getLength) + .function("getPosTan", optional_override([](SkPathMeasure& self, + SkScalar distance) -> PosTan { + SkPoint p{0, 0}; + SkVector v{0, 0}; + if (!self.getPosTan(distance, &p, &v)) { + SkDebugf("zero-length path in getPosTan\n"); + } + return PosTan{p.x(), p.y(), v.x(), v.y()}; + })) + .function("isClosed", &SkPathMeasure::isClosed) + .function("nextContour", &SkPathMeasure::nextContour); + class_("SkShader") .smart_ptr>("sk_sp"); @@ -988,6 +1028,17 @@ EMSCRIPTEN_BINDINGS(Skia) { class_("SkTextBlob") .smart_ptr>("sk_sp>") + .class_function("_MakeFromRSXform", optional_override([](uintptr_t /* char* */ sptr, + size_t strBtyes, + uintptr_t /* SkRSXform* */ xptr, + const SkFont& font, + SkTextEncoding encoding)->sk_sp { + // See comment above for uintptr_t explanation + const char* str = reinterpret_cast(sptr); + const SkRSXform* xforms = reinterpret_cast(xptr); + + return SkTextBlob::MakeFromRSXform(str, strBtyes, xforms, font, encoding); + }), allow_raw_pointers()) .class_function("_MakeFromText", optional_override([](uintptr_t /* char* */ sptr, size_t len, const SkFont& font, SkTextEncoding encoding)->sk_sp { @@ -1174,6 +1225,13 @@ EMSCRIPTEN_BINDINGS(Skia) { .element(&SkPoint3::fY) .element(&SkPoint3::fZ); + // PosTan can be represented by [px, py, tx, ty] + value_array("PosTan") + .element(&PosTan::px) + .element(&PosTan::py) + .element(&PosTan::tx) + .element(&PosTan::ty); + // {"w": Number, "h", Number} value_object("SkSize") .field("w", &SkSize::fWidth) diff --git a/modules/canvaskit/externs.js b/modules/canvaskit/externs.js index 0b38b3b3f6..a5294fae42 100644 --- a/modules/canvaskit/externs.js +++ b/modules/canvaskit/externs.js @@ -84,6 +84,8 @@ var CanvasKit = { // Objects and properties on CanvasKit + RSXFormBuilder: function() {}, + ShapedText: { // public API (from C++ bindings) getBounds: function() {}, @@ -138,6 +140,8 @@ var CanvasKit = { setSize: function() {}, setSkewX: function() {}, setTypeface: function() {}, + // private API (from C++ bindings) + _getWidths: function() {}, }, SkFontMgr: { @@ -241,6 +245,13 @@ var CanvasKit = { dumpHex: function() {}, }, + SkPathMeasure: { + getLength: function() {}, + getPosTan: function() {}, + isClosed: function() {}, + nextContour: function() {}, + }, + SkRect: { fLeft: {}, fTop: {}, @@ -263,7 +274,12 @@ var CanvasKit = { }, SkTextBlob: { + // public API (both C++ and JS bindings) + MakeFromRSXform: function() {}, MakeFromText: function() {}, + MakeOnPath: function() {}, + // private API (from C++ bindings) + _MakeFromRSXform: function() {}, _MakeFromText: function() {}, }, @@ -496,6 +512,12 @@ CanvasKit.SkCanvas.prototype.writePixels = function() {}; CanvasKit.SkFontMgr.prototype.MakeTypefaceFromData = function() {}; +CanvasKit.SkFont.prototype.getWidths = function() {}; + +CanvasKit.RSXFormBuilder.prototype.build = function() {}; +CanvasKit.RSXFormBuilder.prototype.delete = function() {}; +CanvasKit.RSXFormBuilder.prototype.push = function() {}; + // Define StrokeOpts object var StrokeOpts = {}; StrokeOpts.prototype.width; diff --git a/modules/canvaskit/helper.js b/modules/canvaskit/helper.js index 16be49fe2b..c12f117dde 100644 --- a/modules/canvaskit/helper.js +++ b/modules/canvaskit/helper.js @@ -161,3 +161,40 @@ function loadCmdsTypedArray(arr) { var ptr = copy1dArray(ta, CanvasKit.HEAPF32); return [ptr, len]; } + +// Helper for building an array of RSXForms (which are just structs +// of 4 floats) +CanvasKit.RSXFormBuilder = function() { + this._pts = []; + this._ptr = null; +} + +/** + * A compressed form of a rotation+scale matrix. + * + * [ scos -ssin tx ] + * [ ssin scos ty ] + * [ 0 0 1 ] + */ +CanvasKit.RSXFormBuilder.prototype.push = function(scos, ssin, tx, ty) { + if (this._ptr) { + SkDebug('Cannot push more points - already built'); + return; + } + this._pts.push(scos, ssin, tx, ty); +} + +CanvasKit.RSXFormBuilder.prototype.build = function() { + if (this._ptr) { + return this._ptr; + } + this._ptr = copy1dArray(this._pts, CanvasKit.HEAPF32); + return this._ptr; +} + +CanvasKit.RSXFormBuilder.prototype.delete = function() { + if (this._ptr) { + CanvasKit._free(this._ptr); + this._ptr = null; + } +} diff --git a/modules/canvaskit/interface.js b/modules/canvaskit/interface.js index 038d4f13d9..ccb8c9dd76 100644 --- a/modules/canvaskit/interface.js +++ b/modules/canvaskit/interface.js @@ -452,6 +452,36 @@ CanvasKit.onRuntimeInitialized = function() { return ok; } + // Returns an array of the widths of the glyphs in this string. + CanvasKit.SkFont.prototype.getWidths = function(str) { + // add 1 for null terminator + var codePoints = str.length + 1; + // lengthBytesUTF8 and stringToUTF8Array are defined in the emscripten + // JS. See https://kripken.github.io/emscripten-site/docs/api_reference/preamble.js.html#stringToUTF8 + // Add 1 for null terminator + var strBytes = lengthBytesUTF8(str) + 1; + var strPtr = CanvasKit._malloc(strBytes); + stringToUTF8(str, strPtr, strBytes); + + var bytesPerFloat = 4; + // allocate widths == numCodePoints + var widthPtr = CanvasKit._malloc(codePoints * bytesPerFloat); + if (!this._getWidths(strPtr, strBytes, codePoints, widthPtr)) { + SkDebug('Could not compute widths'); + CanvasKit._free(strPtr); + CanvasKit._free(widthPtr); + return null; + } + // reminder, this shouldn't copy the data, just is a nice way to + // wrap 4 bytes together into a float. + var widths = new Float32Array(CanvasKit.buffer, widthPtr, codePoints); + // This copies the data so we can free the CanvasKit memory + var retVal = Array.from(widths); + CanvasKit._free(strPtr); + CanvasKit._free(widthPtr); + return retVal; + } + // fontData should be an arrayBuffer CanvasKit.SkFontMgr.prototype.MakeTypefaceFromData = function(fontData) { var data = new Uint8Array(fontData); @@ -468,6 +498,88 @@ CanvasKit.onRuntimeInitialized = function() { return font; } + CanvasKit.SkTextBlob.MakeOnPath = function(str, path, font, initialOffset) { + if (!str || !str.length) { + SkDebug('ignoring 0 length string'); + return; + } + if (!path || !path.countPoints()) { + SkDebug('ignoring empty path'); + return; + } + if (path.countPoints() === 1) { + SkDebug('path has 1 point, returning normal textblob'); + return this.MakeFromText(str, font); + } + + if (!initialOffset) { + initialOffset = 0; + } + + var widths = font.getWidths(str); + + var rsx = new CanvasKit.RSXFormBuilder(); + var meas = new CanvasKit.SkPathMeasure(path, false, 1); + var dist = initialOffset; + for (var i = 0; i < str.length; i++) { + var width = widths[i]; + dist += width/2; + if (dist > meas.getLength()) { + // jump to next contour + if (!meas.nextContour()) { + // We have come to the end of the path - terminate the string + // right here. + str = str.substring(0, i); + break; + } + dist = width/2; + } + + // Gives us the (x, y) coordinates as well as the cos/sin of the tangent + // line at that position. + var xycs = meas.getPosTan(dist); + var cx = xycs[0]; + var cy = xycs[1]; + var cosT = xycs[2]; + var sinT = xycs[3]; + + var adjustedX = cx - (width/2 * cosT); + var adjustedY = cy - (width/2 * sinT); + + rsx.push(cosT, sinT, adjustedX, adjustedY); + dist += width/2; + } + var retVal = this.MakeFromRSXform(str, rsx, font); + rsx.delete(); + meas.delete(); + return retVal; + } + + CanvasKit.SkTextBlob.MakeFromRSXform = function(str, rsxBuilder, font) { + // lengthBytesUTF8 and stringToUTF8Array are defined in the emscripten + // JS. See https://kripken.github.io/emscripten-site/docs/api_reference/preamble.js.html#stringToUTF8 + // Add 1 for null terminator + var strLen = lengthBytesUTF8(str) + 1; + var strPtr = CanvasKit._malloc(strLen); + // Add 1 for the null terminator. + stringToUTF8(str, strPtr, strLen); + var rptr = rsxBuilder.build(); + + var blob = CanvasKit.SkTextBlob._MakeFromRSXform(strPtr, strLen - 1, + rptr, font, CanvasKit.TextEncoding.UTF8); + if (!blob) { + SkDebug('Could not make textblob from string "' + str + '"'); + return null; + } + + var origDelete = blob.delete.bind(blob); + blob.delete = function() { + CanvasKit._free(strPtr); + origDelete(); + } + return blob; + } + CanvasKit.SkTextBlob.MakeFromText = function(str, font) { // lengthBytesUTF8 and stringToUTF8Array are defined in the emscripten // JS. See https://kripken.github.io/emscripten-site/docs/api_reference/preamble.js.html#stringToUTF8 diff --git a/modules/canvaskit/tests/font.spec.js b/modules/canvaskit/tests/font.spec.js index 8bc9f52866..dbe0e231e3 100644 --- a/modules/canvaskit/tests/font.spec.js +++ b/modules/canvaskit/tests/font.spec.js @@ -14,18 +14,17 @@ describe('CanvasKit\'s Path Behavior', function() { container.innerHTML = ''; }); + let notSerifFontBuffer = null; + // This font is known to support kerning + const notoSerifFontLoaded = fetch('/assets/NotoSerif-Regular.ttf').then( + (response) => response.arrayBuffer()).then( + (buffer) => { + notSerifFontBuffer = buffer; + }); + it('can draw shaped and unshaped text', function(done) { - let fontBuffer = null; - - // This font is known to support kerning - const skFontLoaded = fetch('/assets/NotoSerif-Regular.ttf').then( - (response) => response.arrayBuffer()).then( - (buffer) => { - fontBuffer = buffer; - }); - LoadCanvasKit.then(catchException(done, () => { - skFontLoaded.then(() => { + notoSerifFontLoaded.then(() => { // This is taken from example.html const surface = CanvasKit.MakeCanvasSurface('test'); expect(surface).toBeTruthy('Could not make surface') @@ -40,7 +39,7 @@ describe('CanvasKit\'s Path Behavior', function() { paint.setStyle(CanvasKit.PaintStyle.Stroke); const fontMgr = CanvasKit.SkFontMgr.RefDefault(); - const notoSerif = fontMgr.MakeTypefaceFromData(fontBuffer); + const notoSerif = fontMgr.MakeTypefaceFromData(notSerifFontBuffer); const textPaint = new CanvasKit.SkPaint(); // use the built-in monospace typeface. @@ -90,10 +89,102 @@ describe('CanvasKit\'s Path Behavior', function() { shapedText.delete(); textFont2.delete(); shapedText2.delete(); + fontMgr.delete(); reportSurface(surface, 'text_shaping', done); }); })); }); + it('can draw text following a path', function(done) { + LoadCanvasKit.then(catchException(done, () => { + const surface = CanvasKit.MakeCanvasSurface('test'); + expect(surface).toBeTruthy('Could not make surface') + if (!surface) { + done(); + return; + } + const canvas = surface.getCanvas(); + const paint = new CanvasKit.SkPaint(); + paint.setAntiAlias(true); + paint.setStyle(CanvasKit.PaintStyle.Stroke); + + const font = new CanvasKit.SkFont(null, 24); + const fontPaint = new CanvasKit.SkPaint(); + fontPaint.setAntiAlias(true); + fontPaint.setStyle(CanvasKit.PaintStyle.Fill); + + + const arc = new CanvasKit.SkPath(); + arc.arcTo(CanvasKit.LTRBRect(20, 40, 280, 300), -160, 140, true); + arc.lineTo(210, 140); + arc.arcTo(CanvasKit.LTRBRect(20, 0, 280, 260), 160, -140, true); + + // Only 1 dot should show up in the image, because we run out of path. + const str = 'This téxt should follow the curve across contours...'; + const textBlob = CanvasKit.SkTextBlob.MakeOnPath(str, arc, font); + + canvas.drawPath(arc, paint); + canvas.drawTextBlob(textBlob, 0, 0, fontPaint); + + surface.flush(); + + textBlob.delete(); + arc.delete(); + paint.delete(); + font.delete(); + fontPaint.delete(); + + reportSurface(surface, 'monospace_text_on_path', done); + })); + }); + + it('can draw text following a path with a non-serif font', function(done) { + LoadCanvasKit.then(catchException(done, () => { + notoSerifFontLoaded.then(() => { + const surface = CanvasKit.MakeCanvasSurface('test'); + expect(surface).toBeTruthy('Could not make surface') + if (!surface) { + done(); + return; + } + const fontMgr = CanvasKit.SkFontMgr.RefDefault(); + const notoSerif = fontMgr.MakeTypefaceFromData(notSerifFontBuffer); + + const canvas = surface.getCanvas(); + const paint = new CanvasKit.SkPaint(); + paint.setAntiAlias(true); + paint.setStyle(CanvasKit.PaintStyle.Stroke); + + const font = new CanvasKit.SkFont(notoSerif, 24); + const fontPaint = new CanvasKit.SkPaint(); + fontPaint.setAntiAlias(true); + fontPaint.setStyle(CanvasKit.PaintStyle.Fill); + + + const arc = new CanvasKit.SkPath(); + arc.arcTo(CanvasKit.LTRBRect(20, 40, 280, 300), -160, 140, true); + arc.lineTo(210, 140); + arc.arcTo(CanvasKit.LTRBRect(20, 0, 280, 260), 160, -140, true); + + const str = 'This téxt should follow the curve across contours...'; + const textBlob = CanvasKit.SkTextBlob.MakeOnPath(str, arc, font, 60.5); + + canvas.drawPath(arc, paint); + canvas.drawTextBlob(textBlob, 0, 0, fontPaint); + + surface.flush(); + + textBlob.delete(); + arc.delete(); + paint.delete(); + notoSerif.delete(); + font.delete(); + fontPaint.delete(); + fontMgr.delete(); + reportSurface(surface, 'serif_text_on_path', done); + }); + })); + }); + // TODO more tests });