[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 <bungeman@google.com>
diff --git a/modules/canvaskit/CHANGELOG.md b/modules/canvaskit/CHANGELOG.md
index 8238784..5488786 100644
--- a/modules/canvaskit/CHANGELOG.md
+++ b/modules/canvaskit/CHANGELOG.md
@@ -6,6 +6,10 @@
## [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 bd75ded..273859d 100644
--- a/modules/canvaskit/canvaskit/example.html
+++ b/modules/canvaskit/canvaskit/example.html
@@ -47,6 +47,7 @@
<h2> CanvasKit can allow for text shaping (e.g. breaking, kerning)</h2>
<canvas id=shape1 width=600 height=600></canvas>
<canvas id=shape2 width=600 height=600></canvas>
+<canvas id=textonpath width=300 height=300></canvas>
<h2> Skottie </h2>
<canvas id=sk_legos width=300 height=300></canvas>
@@ -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 172cc2f..19b82a5 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 @@
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 @@
.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<char*>(sptr);
+ SkScalar* widths = reinterpret_cast<SkScalar*>(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 @@
#endif
;
+ class_<SkPathMeasure>("SkPathMeasure")
+ .constructor<const SkPath&, bool, SkScalar>()
+ .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>("SkShader")
.smart_ptr<sk_sp<SkShader>>("sk_sp<SkShader>");
@@ -988,6 +1028,17 @@
class_<SkTextBlob>("SkTextBlob")
.smart_ptr<sk_sp<SkTextBlob>>("sk_sp<SkTextBlob>>")
+ .class_function("_MakeFromRSXform", optional_override([](uintptr_t /* char* */ sptr,
+ size_t strBtyes,
+ uintptr_t /* SkRSXform* */ xptr,
+ const SkFont& font,
+ SkTextEncoding encoding)->sk_sp<SkTextBlob> {
+ // See comment above for uintptr_t explanation
+ const char* str = reinterpret_cast<const char*>(sptr);
+ const SkRSXform* xforms = reinterpret_cast<const SkRSXform*>(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<SkTextBlob> {
@@ -1174,6 +1225,13 @@
.element(&SkPoint3::fY)
.element(&SkPoint3::fZ);
+ // PosTan can be represented by [px, py, tx, ty]
+ value_array<PosTan>("PosTan")
+ .element(&PosTan::px)
+ .element(&PosTan::py)
+ .element(&PosTan::tx)
+ .element(&PosTan::ty);
+
// {"w": Number, "h", Number}
value_object<SkSize>("SkSize")
.field("w", &SkSize::fWidth)
diff --git a/modules/canvaskit/externs.js b/modules/canvaskit/externs.js
index 0b38b3b..a5294fa 100644
--- a/modules/canvaskit/externs.js
+++ b/modules/canvaskit/externs.js
@@ -84,6 +84,8 @@
// Objects and properties on CanvasKit
+ RSXFormBuilder: function() {},
+
ShapedText: {
// public API (from C++ bindings)
getBounds: function() {},
@@ -138,6 +140,8 @@
setSize: function() {},
setSkewX: function() {},
setTypeface: function() {},
+ // private API (from C++ bindings)
+ _getWidths: function() {},
},
SkFontMgr: {
@@ -241,6 +245,13 @@
dumpHex: function() {},
},
+ SkPathMeasure: {
+ getLength: function() {},
+ getPosTan: function() {},
+ isClosed: function() {},
+ nextContour: function() {},
+ },
+
SkRect: {
fLeft: {},
fTop: {},
@@ -263,7 +274,12 @@
},
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.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 16be49f..c12f117 100644
--- a/modules/canvaskit/helper.js
+++ b/modules/canvaskit/helper.js
@@ -161,3 +161,40 @@
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 038d4f1..ccb8c9d 100644
--- a/modules/canvaskit/interface.js
+++ b/modules/canvaskit/interface.js
@@ -452,6 +452,36 @@
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 @@
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 8bc9f52..dbe0e23 100644
--- a/modules/canvaskit/tests/font.spec.js
+++ b/modules/canvaskit/tests/font.spec.js
@@ -14,18 +14,17 @@
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 @@
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 @@
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
});