Return shapedlines for text api

Change-Id: I8f9aa6e0cffb3441706bd0efeb0886209cac012a
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/403256
Reviewed-by: Julia Lavrova <jlavrova@google.com>
Commit-Queue: Mike Reed <reed@google.com>
This commit is contained in:
Mike Reed 2021-05-02 17:47:09 -04:00
parent f1ff004a7a
commit a46fe7d587
7 changed files with 391 additions and 51 deletions

View File

@ -33,6 +33,7 @@
<h2> Paragraph </h2>
<canvas id=para1 width=600 height=600></canvas>
<canvas id=para2 width=600 height=600></canvas>
<canvas id=para3 width=600 height=600></canvas>
<h2> CanvasKit can serialize/deserialize .skp files</h2>
<canvas id=skp width=500 height=500></canvas>
@ -143,6 +144,7 @@
Promise.all([ckLoaded, loadFont]).then((results) => {
ParagraphAPI1(...results);
ParagraphAPI2(...results);
ParagraphAPI3(...results);
GlyphGame(...results)
});
Promise.all([ckLoaded, loadSkp]).then((results) => {SkpExample(...results)});
@ -329,10 +331,10 @@
setBlinkRate: function(blinks_per_sec) {
this._draws_per_sec = blinks_per_sec;
},
place: function(x, y, fontMetrics) {
place: function(x, top, bottom) {
this._x = x;
this._top = y + fontMetrics.ascent;
this._bottom = y + fontMetrics.descent;
this._top = top;
this._bottom = bottom;
this._path = null;
},
@ -355,31 +357,33 @@
};
}
let mouse = {
_start_x: 0, _start_y: 0,
_curr_x: 0, _curr_y: 0,
_active: false,
function MakeMouse() {
return {
_start_x: 0, _start_y: 0,
_curr_x: 0, _curr_y: 0,
_active: false,
isActive: function() {
return this._active;
},
setDown: function(x, y) {
this._start_x = this._curr_x = x;
this._start_y = this._curr_y = y;
this._active = true;
},
setMove: function(x, y) {
this._curr_x = x;
this._curr_y = y;
},
setUp: function(x, y) {
this._curr_x = x;
this._curr_y = y;
this._active = false;
},
getPos: function() {
return [ this._start_x, this._start_y, this._curr_x, this._curr_y ];
},
isActive: function() {
return this._active;
},
setDown: function(x, y) {
this._start_x = this._curr_x = x;
this._start_y = this._curr_y = y;
this._active = true;
},
setMove: function(x, y) {
this._curr_x = x;
this._curr_y = y;
},
setUp: function(x, y) {
this._curr_x = x;
this._curr_y = y;
this._active = false;
},
getPos: function() {
return [ this._start_x, this._start_y, this._curr_x, this._curr_y ];
},
};
}
function ParagraphAPI2(CanvasKit, fontData) {
@ -393,6 +397,7 @@
return;
}
const mouse = MakeMouse();
const cursor = MakeCursor(CanvasKit);
const canvas = surface.getCanvas();
const fontMgr = CanvasKit.FontMgr.FromData([fontData]);
@ -517,7 +522,7 @@
if (a == b) {
INDEX = a;
const p = index_to_pos(runs, INDEX);
cursor.place(p[0], p[1], fm);
cursor.place(p[0], p[1] + fm.ascent, p[1] + fm.descent);
} else {
cursor.setPath(indices_to_path(runs, a, b, fm, WIDTH));
}
@ -552,6 +557,211 @@
return surface;
}
function ParagraphAPI3(CanvasKit, fontData) {
if (!CanvasKit || !fontData) {
return;
}
const surface = CanvasKit.MakeCanvasSurface('para3');
if (!surface) {
console.error('Could not make surface');
return;
}
const mouse = MakeMouse();
const cursor = MakeCursor(CanvasKit);
const canvas = surface.getCanvas();
const fontMgr = CanvasKit.FontMgr.FromData([fontData]);
const paraStyle = new CanvasKit.ParagraphStyle({
textStyle: {
color: CanvasKit.GRAY,
fontFamilies: ['Roboto'],
fontSize: 40,
},
textAlign: CanvasKit.TextAlign.Left,
});
const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr);
const text = "In a hole in the ground there lived a hobbit. Not a nasty, dirty, " +
"wet hole full of worms and oozy smells. This was a hobbit-hole and " +
"that means good food, a warm hearth, and all the comforts of home.";
builder.addText(text);
const paragraph = builder.build();
const WIDTH = 600;
paragraph.layout(WIDTH);
const tf = fontMgr.MakeTypefaceFromData(fontData);
const font = new CanvasKit.Font(tf, 40);
const fm = font.getMetrics();
const fontPaint = new CanvasKit.Paint();
fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
fontPaint.setAntiAlias(true);
function mid(a, b) { return (a + b) * 0.5; }
function runs_pos_to_index(runs, x, y) {
for (const r of runs) {
for (let i = 1; i < r.offsets.length; i += 1) {
if (x < r.positions[i*2]) {
if (x <= mid(r.positions[i*2-2], r.positions[i*2])) {
return r.offsets[i-1];
} else {
return r.offsets[i];
}
}
}
}
const r = runs[runs.length-1];
return r.offsets[r.offsets.length-1];
}
function lines_pos_to_index(lines, x, y) {
if (y < lines[0].baseline_y + lines[0].metrics.ascent) {
return 0;
}
for (const l of lines) {
if (y <= l.baseline_y + l.metrics.descent) {
return runs_pos_to_index(l.runs, x, y);
}
}
return text.length;
}
function runs_index_to_run(runs, index) {
for (const r of runs) {
if (index <= r.offsets[r.offsets.length-1]) {
return r;
}
}
return null;
}
function runs_index_to_x(runs, index) {
const r = runs_index_to_run(runs, index);
for (const i in r.offsets) {
if (index == r.offsets[i]) {
return r.positions[i*2];
}
}
}
function lines_index_to_line(lines, index) {
for (const l of lines) {
if (index <= l.textRange.last) {
return l;
}
}
return lines[lines.length-1];
}
function lines_index_to_x(lines, index) {
for (const l of lines) {
if (index <= l.textRange.last) {
return runs_index_to_x(l.runs, index);
}
}
}
function line_top(line) {
return line.baseline_y + line.metrics.ascent;
}
function line_bottom(line) {
return line.baseline_y + line.metrics.descent;
}
function lines_indices_to_path(lines, a, b, fm, width) {
if (a == b) {
return null;
}
if (a > b) {
const tmp = a;
a = b;
b = tmp;
}
const path = new CanvasKit.Path();
const la = lines_index_to_line(lines, a);
const lb = lines_index_to_line(lines, b);
const ax = runs_index_to_x(la.runs, a);
const bx = runs_index_to_x(lb.runs, b);
if (la == lb) {
path.addRect([ax, line_top(la), bx, line_bottom(la)]);
} else {
const a_bot = line_bottom(la);
const b_top = line_top(lb);
path.addRect([ax, line_top(la), width, a_bot]);
path.addRect([0, b_top, bx, line_bottom(lb)]);
if (a_bot < b_top) {
path.addRect([0, a_bot, width, b_top]); // extra lines inbetween
}
}
return path;
}
let INDEX = 0;
let lines;
function drawFrame(canvas) {
canvas.clear(CanvasKit.WHITE);
canvas.drawParagraph(paragraph, 0, 0);
if (!lines) {
lines = paragraph.getShapedLines();
}
if (mouse.isActive()) {
const pos = mouse.getPos();
const a = lines_pos_to_index(lines, pos[0], pos[1]);
const b = lines_pos_to_index(lines, pos[2], pos[3]);
if (a == b) {
INDEX = a;
const l = lines_index_to_line(lines, INDEX);
const x = runs_index_to_x(l.runs, INDEX);
cursor.place(x, line_top(l), line_bottom(l));
} else {
cursor.setPath(lines_indices_to_path(lines, a, b, fm, WIDTH));
}
}
const bgcolors = [[1,0,0,0.05], [0,1,0,0.05]];
let lineNo = 0;
cursor.draw_before(canvas);
for (const l of lines) {
if (true) { // test line bounds
const bounds = [0, l.baseline_y + l.metrics.ascent,
WIDTH, l.baseline_y + l.metrics.descent];
fontPaint.setColor(bgcolors[lineNo & 1]);
canvas.drawRect(bounds, fontPaint);
fontPaint.setColor([0,0,0,1]);
}
for (let r of l.runs) {
canvas.drawGlyphs(r.glyphs, r.positions, 0, 0, font, fontPaint);
}
lineNo += 1;
}
cursor.draw_after(canvas);
surface.requestAnimationFrame(drawFrame);
}
surface.requestAnimationFrame(drawFrame);
function interact(e) {
const type = e.type;
if (type === 'pointerup') {
mouse.setUp(e.offsetX, e.offsetY);
} else if (type === 'pointermove') {
mouse.setMove(e.offsetX, e.offsetY);
} else if (type === 'pointerdown') {
mouse.setDown(e.offsetX, e.offsetY);
}
};
document.getElementById('para3').addEventListener('pointermove', interact);
document.getElementById('para3').addEventListener('pointerdown', interact);
document.getElementById('para3').addEventListener('pointerup', interact);
return surface;
}
function RTShaderAPI1(CanvasKit) {
if (!CanvasKit) {
return;

View File

@ -650,6 +650,17 @@ export interface LineMetrics {
lineNumber: number;
}
export interface Range {
first: number;
last: number;
}
export interface YMetrics {
ascent: number; // distance above the baseline (negative) to top of 'normal' letters
descent: number; // distance below the baseline (positive) to bottom of 'normal' letters
leading: number; // extra space recommended between lines (needed?)
}
/**
* Information for a run of shaped text. See Paragraph.getShapedRuns()
*
@ -667,6 +678,13 @@ export interface GlyphRun {
flags: number; // see GlyphRunFlags
}
export interface ShapedLine {
textRange: Range;
metrics: YMetrics;
baseline_y: number;
runs: GlyphRun[];
}
/**
* This object is a wrapper around a pointer to some memory on the WASM heap. The type of the
* pointer was determined at creation time.
@ -824,7 +842,9 @@ export interface Paragraph extends EmbindObject<Paragraph> {
*/
getWordBoundary(offset: number): URange;
getShapedRuns(): GlyphRun[];
getShapedRuns(): GlyphRun[]; // deprecated
getShapedLines(): ShapedLine[];
/**
* Lays out the text in the paragraph so it is wrapped to the given width.

View File

@ -323,22 +323,25 @@ JSArray GetShapedRuns(para::Paragraph& self) {
// where we accumulate our js output
JSArray jruns = emscripten::val::array();
self.visit([&](const para::Paragraph::VisitorInfo& info) {
const int N = info.count; // glyphs
self.visit([&](int, const para::Paragraph::VisitorInfo* info) {
if (!info) {
return;
}
const int N = info->count; // glyphs
const int N1 = N + 1; // positions, offsets have 1 extra (trailing) slot
JSObject jrun = emscripten::val::object();
jrun.set("flags", info.flags);
jrun.set("glyphs", MakeTypedArray(N, info.glyphs, "Uint16Array"));
jrun.set("offsets", MakeTypedArray(N1, info.utf8Starts, "Uint32Array"));
jrun.set("flags", info->flags);
jrun.set("glyphs", MakeTypedArray(N, info->glyphs, "Uint16Array"));
jrun.set("offsets", MakeTypedArray(N1, info->utf8Starts, "Uint32Array"));
// we need to modify the positions, so make a temp copy
SkAutoSTMalloc<32, SkPoint> positions(N1);
for (int i = 0; i < N; ++i) {
positions.get()[i] = info.positions[i] + info.origin;
positions.get()[i] = info->positions[i] + info->origin;
}
positions.get()[N] = { info.advanceX, positions.get()[N - 1].fY };
positions.get()[N] = { info->advanceX, positions.get()[N - 1].fY };
jrun.set("positions", MakeTypedArray(N1*2, (const float*)positions.get(), "Float32Array"));
jruns.call<void>("push", jrun);
@ -346,6 +349,96 @@ JSArray GetShapedRuns(para::Paragraph& self) {
return jruns;
}
/*
* Returns Lines[]
*/
JSArray GetShapedLines(para::Paragraph& self) {
struct LineAccumulate {
int lineNumber = -1; // deliberately -1 from starting value
uint32_t minOffset = 0xFFFFFFFF;
uint32_t maxOffset = 0;
float minAscent = 0;
float maxDescent = 0;
float maxLeading = 0;
void reset(int lineNumber) {
new (this) LineAccumulate;
this->lineNumber = lineNumber;
}
};
// where we accumulate our js output
JSArray jlines = emscripten::val::array();
JSObject jline = emscripten::val::null();
JSArray jruns = emscripten::val::null();
LineAccumulate accum;
self.visit([&](int lineNumber, const para::Paragraph::VisitorInfo* info) {
if (!info) {
// end of current line
JSObject range = emscripten::val::object();
range.set("first", accum.minOffset);
range.set("last", accum.maxOffset);
jline.set("textRange", range);
JSObject metrics = emscripten::val::object();
metrics.set("ascent", accum.minAscent);
metrics.set("descent", accum.maxDescent);
metrics.set("leading", accum.maxLeading);
jline.set("metrics", metrics);
return;
}
if (lineNumber != accum.lineNumber) {
SkASSERT(lineNumber == accum.lineNumber + 1); // assume monotonic
accum.reset(lineNumber);
jruns = emscripten::val::array();
jline = emscripten::val::array();
jline.set("baseline_y", info->origin.fY);
jline.set("runs", jruns);
// will assign textRange and metrics on end-of-line signal
jlines.call<void>("push", jline);
}
// append the run
const int N = info->count; // glyphs
const int N1 = N + 1; // positions, offsets have 1 extra (trailing) slot
JSObject jrun = emscripten::val::object();
jrun.set("flags", info->flags);
jrun.set("glyphs", MakeTypedArray(N, info->glyphs, "Uint16Array"));
jrun.set("offsets", MakeTypedArray(N1, info->utf8Starts, "Uint32Array"));
// we need to modify the positions, so make a temp copy
SkAutoSTMalloc<32, SkPoint> positions(N1);
for (int i = 0; i < N; ++i) {
positions.get()[i] = info->positions[i] + info->origin;
}
positions.get()[N] = { info->advanceX, positions.get()[N - 1].fY };
jrun.set("positions", MakeTypedArray(N1*2, (const float*)positions.get(), "Float32Array"));
jruns.call<void>("push", jrun);
// update accum
{ SkFontMetrics fm;
info->font.getMetrics(&fm);
accum.minAscent = std::min(accum.minAscent, fm.fAscent);
accum.maxDescent = std::max(accum.maxDescent, fm.fDescent);
accum.maxLeading = std::max(accum.maxLeading, fm.fLeading);
accum.minOffset = std::min(accum.minOffset, info->utf8Starts[0]);
accum.maxOffset = std::max(accum.maxOffset, info->utf8Starts[N]);
}
});
return jlines;
}
EMSCRIPTEN_BINDINGS(Paragraph) {
class_<para::Paragraph>("Paragraph")
@ -362,6 +455,7 @@ EMSCRIPTEN_BINDINGS(Paragraph) {
.function("_getRectsForPlaceholders", &GetRectsForPlaceholders)
.function("_getRectsForRange", &GetRectsForRange)
.function("getShapedRuns", &GetShapedRuns)
.function("getShapedLines", &GetShapedLines)
.function("getWordBoundary", &para::Paragraph::getWordBoundary)
.function("layout", &para::Paragraph::layout);

View File

@ -100,21 +100,25 @@ protected:
p2.setStrokeWidth(4);
p2.setStrokeCap(SkPaint::kSquare_Cap);
para->visit([&](const skia::textlayout::Paragraph::VisitorInfo& info) {
canvas->drawGlyphs(info.count, info.glyphs, info.positions, info.origin, info.font, p);
para->visit([&](int, const skia::textlayout::Paragraph::VisitorInfo* info) {
if (!info) {
return;
}
canvas->drawGlyphs(info->count, info->glyphs, info->positions, info->origin,
info->font, p);
if (info.utf8Starts && false) {
if (info->utf8Starts && false) {
SkString str;
for (int i = 0; i < info.count; ++i) {
str.appendUnichar(gSpeach[info.utf8Starts[i]]);
for (int i = 0; i < info->count; ++i) {
str.appendUnichar(gSpeach[info->utf8Starts[i]]);
}
SkDebugf("'%s'\n", str.c_str());
}
if (false) { // show position points
for (int i = 0; i < info.count; ++i) {
auto pos = info.positions[i];
canvas->drawPoint(pos.fX + info.origin.fX, pos.fY + info.origin.fY, p2);
for (int i = 0; i < info->count; ++i) {
auto pos = info->positions[i];
canvas->drawPoint(pos.fX + info->origin.fX, pos.fY + info->origin.fY, p2);
}
}
});

View File

@ -86,7 +86,9 @@ public:
const uint32_t* utf8Starts; // count+1 values
unsigned flags;
};
using Visitor = std::function<void(const VisitorInfo&)>;
// lineNumber begins at 0. If info is null, this signals the end of that line.
using Visitor = std::function<void(int lineNumber, const VisitorInfo*)>;
virtual void visit(const Visitor&) = 0;
protected:

View File

@ -3469,12 +3469,18 @@ protected:
paragraph->paint(canvas, 0, 0);
paragraph->visit([&](const skia::textlayout::Paragraph::VisitorInfo& info) {
paragraph->visit([&](int, const skia::textlayout::Paragraph::VisitorInfo* info) {
if (!info) {
return;
}
SkFontMetrics metrics;
info.font.getMetrics(&metrics);
info->font.getMetrics(&metrics);
auto first = info.positions[0]; first.offset(info.origin.fX, info.origin.fY);
SkRect rect = SkRect::MakeXYWH(first.fX, first.fY + metrics.fAscent, info.advanceX - first.fX, metrics.fDescent - metrics.fAscent);
auto first = info->positions[0]; first.offset(info->origin.fX, info->origin.fY);
SkRect rect = SkRect::MakeXYWH(first.fX,
first.fY + metrics.fAscent,
info->advanceX - first.fX,
metrics.fDescent - metrics.fAscent);
SkPaint paint;
paint.setColor(SK_ColorLTGRAY);
canvas->drawRect(rect, paint);

View File

@ -1051,6 +1051,7 @@ void ParagraphImpl::ensureUTF16Mapping() {
}
void ParagraphImpl::visit(const Visitor& visitor) {
int lineNumber = 0;
for (auto& line : fLines) {
for (auto& rec : line.fTextBlobCache) {
SkTextBlob::Iter iter(*rec.fBlob);
@ -1071,7 +1072,7 @@ void ParagraphImpl::visit(const Visitor& visitor) {
clusterPtr += rec.fVisitor_Pos;
while (iter.experimentalNext(&run)) {
visitor({
const Paragraph::VisitorInfo info = {
run.font,
rec.fOffset,
rec.fClipRect.fRight,
@ -1080,10 +1081,13 @@ void ParagraphImpl::visit(const Visitor& visitor) {
run.positions,
clusterPtr,
0, // flags
});
};
visitor(lineNumber, &info);
clusterPtr += run.count;
}
}
visitor(lineNumber, nullptr); // signal end of line
lineNumber += 1;
}
}