[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>
This commit is contained in:
Kevin Lubick 2019-03-15 15:36:29 -04:00
parent e1271700ad
commit d3cfbcae10
7 changed files with 373 additions and 11 deletions

View File

@ -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`)

View File

@ -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) {

View File

@ -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<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 @@ EMSCRIPTEN_BINDINGS(Skia) {
#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 @@ EMSCRIPTEN_BINDINGS(Skia) {
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 @@ EMSCRIPTEN_BINDINGS(Skia) {
.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)

View File

@ -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;

View File

@ -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;
}
}

View File

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

View File

@ -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
});