Expose glyphIntercepts to CK
bug: skia:12000
Change-Id: I40f130b0eab21ef4a53ee88eed8f62c4f3a577c1
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/408899
Commit-Queue: Mike Reed <reed@google.com>
Reviewed-by: Yegor Jbanov <yjbanov@google.com>
diff --git a/modules/canvaskit/WasmCommon.h b/modules/canvaskit/WasmCommon.h
index 9bc7a1b..b6e3ace 100644
--- a/modules/canvaskit/WasmCommon.h
+++ b/modules/canvaskit/WasmCommon.h
@@ -41,4 +41,29 @@
return jarray;
}
+/**
+ * Reads a JS array, and returns a copy of it in the std::vector.
+ *
+ * It is up to the caller to ensure the T and arrayType-string match
+ * e.g. T == uint16_t, arrayType == "Uint16Array"
+ *
+ * Note, this checks the JS type, and if it does not match, it will still attempt
+ * to return a copy, just by performing a 1-element-at-a-time copy
+ * via emscripten::vecFromJSArray().
+ */
+template <typename T>
+std::vector<T> CopyTypedArray(JSArray src, const char arrayType[]) {
+ if (src.instanceof(emscripten::val::global(arrayType))) {
+ const size_t len = src["length"].as<size_t>();
+ std::vector<T> dst;
+ dst.resize(len);
+ auto dst_view = emscripten::val(typed_memory_view(len, dst.data()));
+ dst_view.call<void>("set", src);
+ return dst;
+ } else {
+ // copies one at a time
+ return emscripten::vecFromJSArray<T>(src);
+ }
+}
+
#endif
diff --git a/modules/canvaskit/canvaskit_bindings.cpp b/modules/canvaskit/canvaskit_bindings.cpp
index 5d7a3d7..67ee1ec 100644
--- a/modules/canvaskit/canvaskit_bindings.cpp
+++ b/modules/canvaskit/canvaskit_bindings.cpp
@@ -1209,6 +1209,19 @@
}
return j;
}))
+ .function("getGlyphIntercepts", optional_override([](SkFont& self,
+ JSArray jglyphs,
+ JSArray jpos,
+ float top, float bottom) -> JSArray {
+ auto glyphs = CopyTypedArray<uint16_t>(jglyphs, "Uint16Array");
+ auto pos = CopyTypedArray<float>(jpos, "Float32Array");
+ if (glyphs.size() > (pos.size() >> 1)) {
+ return emscripten::val("Not enough x,y position pairs for glyphs");
+ }
+ auto sects = self.getIntercepts(glyphs.data(), SkToInt(glyphs.size()),
+ (const SkPoint*)pos.data(), top, bottom);
+ return MakeTypedArray(sects.size(), (const float*)sects.data(), "Float32Array");
+ }), allow_raw_pointers())
.function("getScaleX", &SkFont::getScaleX)
.function("getSize", &SkFont::getSize)
.function("getSkewX", &SkFont::getSkewX)
diff --git a/modules/canvaskit/externs.js b/modules/canvaskit/externs.js
index b3e968b..ed4a462 100644
--- a/modules/canvaskit/externs.js
+++ b/modules/canvaskit/externs.js
@@ -358,6 +358,7 @@
getGlyphBounds: function() {},
getGlyphIDs: function() {},
getGlyphWidths: function() {},
+ getGlyphIntercepts: function() {},
},
// private API (from C++ bindings)
diff --git a/modules/canvaskit/npm_build/extra.html b/modules/canvaskit/npm_build/extra.html
index e12ad20..40ad87c 100644
--- a/modules/canvaskit/npm_build/extra.html
+++ b/modules/canvaskit/npm_build/extra.html
@@ -368,6 +368,7 @@
case 'i': editor.applyStyleToSelection({italic:'toggle'}); return;
case 'b': editor.applyStyleToSelection({bold:'toggle'}); return;
+ case 'l': editor.applyStyleToSelection({underline:'toggle'}); return;
case ']': editor.applyStyleToSelection({size_add:1}); return;
case '[': editor.applyStyleToSelection({size_add:-1}); return;
diff --git a/modules/canvaskit/npm_build/textapi_utils.js b/modules/canvaskit/npm_build/textapi_utils.js
index f2e429f..9f46889 100644
--- a/modules/canvaskit/npm_build/textapi_utils.js
+++ b/modules/canvaskit/npm_build/textapi_utils.js
@@ -205,6 +205,7 @@
color: null,
bold: null,
italic: null,
+ underline: null,
_check_toggle: function(src, dst) {
if (src == 'toggle') {
@@ -234,6 +235,9 @@
if (src.italic) {
this.italic = this._check_toggle(src.italic, this.italic);
}
+ if (src.underline) {
+ this.underline = this._check_toggle(src.underline, this.underline);
+ }
if (src.size_add) {
this.size += src.size_add;
@@ -496,12 +500,32 @@
}
LOG(' glyph subrange', glyph_start, glyph_end);
gly = gly.slice(glyph_start, glyph_end);
- pos = pos.slice(glyph_start*2, glyph_end*2);
+ // +2 at the end so we can see the trailing position (esp. for underlines)
+ pos = pos.slice(glyph_start*2, glyph_end*2 + 2);
} else {
LOG(' use entire glyph run');
}
canvas.drawGlyphs(gly, pos, 0, 0, f, p);
+ if (s.underline) {
+ const gap = 2;
+ const Y = pos[1]; // first Y
+ const lastX = pos[gly.length*2];
+ const sects = f.getGlyphIntercepts(gly, pos, Y+2, Y+4);
+
+ let x = pos[0];
+ for (let i = 0; i < sects.length; i += 2) {
+ const end = sects[i] - gap;
+ if (x < end) {
+ canvas.drawRect([x, Y+2, end, Y+4], p);
+ }
+ x = sects[i+1] + gap;
+ }
+ if (x < lastX) {
+ canvas.drawRect([x, Y+2, lastX, Y+4], p);
+ }
+ }
+
start = end;
}
diff --git a/modules/canvaskit/npm_build/types/canvaskit-wasm-tests.ts b/modules/canvaskit/npm_build/types/canvaskit-wasm-tests.ts
index 8c75898..e14573cf 100644
--- a/modules/canvaskit/npm_build/types/canvaskit-wasm-tests.ts
+++ b/modules/canvaskit/npm_build/types/canvaskit-wasm-tests.ts
@@ -344,6 +344,22 @@
font.setSubpixel(true);
font.setTypeface(null);
font.setTypeface(face);
+
+ // try unittest for intercepts
+ {
+ font.setTypeface(null);
+ font.setSize(100);
+ const ids = font.getGlyphIDs('I');
+ console.assert(ids.length == 1, "");
+
+ // aim for the middle of the I at 100 point, expecting a hit
+ let sects = font.getGlyphIntercepts(ids, [0, 0], -60, -40);
+ console.assert(sects.length === 2, "expected one pair of intercepts");
+
+ // aim below the baseline where we expect no intercepts
+ sects = font.getGlyphIntercepts(ids, [0, 0], 20, 30);
+ console.assert(ids.length === 0, "expected no intercepts");
+ }
}
function fontMgrTests(CK: CanvasKit) {
diff --git a/modules/canvaskit/npm_build/types/index.d.ts b/modules/canvaskit/npm_build/types/index.d.ts
index 966a35d..de62a4d 100644
--- a/modules/canvaskit/npm_build/types/index.d.ts
+++ b/modules/canvaskit/npm_build/types/index.d.ts
@@ -1660,6 +1660,26 @@
output?: Float32Array): Float32Array;
/**
+ * Computes any intersections of a thick "line" and a run of positionsed glyphs.
+ * The thick line is represented as a top and bottom coordinate (positive for
+ * below the baseline, negative for above). If there are no intersections
+ * (e.g. if this is intended as an underline, and there are no "collisions")
+ * then the returned array will be empty. If there are intersections, the array
+ * will contain pairs of X coordinates [start, end] for each segment that
+ * intersected with a glyph.
+ *
+ * @param glyphs the glyphs to intersect with
+ * @param positions x,y coordinates (2 per glyph) for each glyph
+ * @param top top of the thick "line" to use for intersection testing
+ * @param bottom bottom of the thick "line" to use for intersection testing
+ * @param paint optional (can be null) in case the paint affects the
+ * "thickness" of the glyphs (e.g. patheffect, stroking, maskfilter)
+ * @return array of [start, end] x-coordinate pairs. Maybe be empty.
+ */
+ getGlyphIntercepts(glyphs: InputGlyphIDArray, positions: Float32Array,
+ top: number, bottom: number): Float32Array;
+
+ /**
* Returns text scale on x-axis. Default value is 1.
*/
getScaleX(): number;