[canvaskit] Initial addition of SkParagraph
There are more parts of ParagraphStyle and TextStyle, but this should be a bulk of the components. Bug: skia:9469 Change-Id: I87fff6700f41cff49ecbee3a1339e84c36699c93 Reviewed-on: https://skia-review.googlesource.com/c/skia/+/244837 Reviewed-by: Julia Lavrova <jlavrova@google.com>
This commit is contained in:
parent
c95c4a69e1
commit
369f6a5ea2
1
BUILD.gn
1
BUILD.gn
@ -1072,6 +1072,7 @@ group("modules") {
|
||||
deps = [
|
||||
"modules/particles",
|
||||
"modules/skottie",
|
||||
"modules/skparagraph",
|
||||
"modules/skshaper",
|
||||
]
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- `CanvasKit.MakeAnimatedImageFromEncoded`, `SkCanvas.drawAnimatedImage`.
|
||||
- `CanvasKit.SkFontMgr.FromData` which takes several ArrayBuffers of font data, parses
|
||||
them, reading the metadata (e.g. family names) and stores them into a SkFontMgr.
|
||||
- SkParagraph as an optional set of APIs for dealing with text layout.
|
||||
|
||||
### Changed
|
||||
- The `no_font` compile option should strip out more dead code related to fonts.
|
||||
|
@ -19,5 +19,6 @@ using JSObject = emscripten::val;
|
||||
using JSString = emscripten::val;
|
||||
using SkPathOrNull = emscripten::val;
|
||||
using Uint8Array = emscripten::val;
|
||||
using Float32Array = emscripten::val;
|
||||
|
||||
#endif
|
||||
|
@ -26,6 +26,9 @@
|
||||
<h2> Particles </h2>
|
||||
<canvas id=particles width=500 height=500></canvas>
|
||||
|
||||
<h2> Paragraph </h2>
|
||||
<canvas id=para1 width=500 height=500></canvas>
|
||||
|
||||
<script type="text/javascript" src="/node_modules/canvaskit/bin/canvaskit.js"></script>
|
||||
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
@ -63,6 +66,8 @@
|
||||
});
|
||||
|
||||
ParticlesAPI1(CanvasKit);
|
||||
|
||||
ParagraphAPI1(CanvasKit, robotoData);
|
||||
});
|
||||
|
||||
fetch('https://storage.googleapis.com/skia-cdn/misc/lego_loader.json').then((resp) => {
|
||||
@ -127,6 +132,7 @@
|
||||
SkottieExample(CanvasKit, 'sk_webfont', webfontJSON, fullBounds, {
|
||||
'Roboto-Regular': robotoData,
|
||||
});
|
||||
ParagraphAPI1(CanvasKit, robotoData);
|
||||
});
|
||||
});
|
||||
|
||||
@ -312,4 +318,66 @@ const snowfall = {
|
||||
window.requestAnimationFrame(drawFrame);
|
||||
|
||||
}
|
||||
|
||||
function ParagraphAPI1(CanvasKit, fontData) {
|
||||
if (!CanvasKit || !fontData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const surface = CanvasKit.MakeCanvasSurface('para1');
|
||||
if (!surface) {
|
||||
console.error('Could not make surface');
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = surface.getCanvas();
|
||||
const fontMgr = CanvasKit.SkFontMgr.FromData([fontData]);
|
||||
|
||||
const paraStyle = new CanvasKit.ParagraphStyle({
|
||||
textStyle: {
|
||||
color: CanvasKit.BLACK,
|
||||
fontFamilies: ['Roboto'],
|
||||
fontSize: 50,
|
||||
},
|
||||
textAlign: CanvasKit.TextAlign.Left,
|
||||
maxLines: 5,
|
||||
});
|
||||
|
||||
const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr);
|
||||
builder.addText('The quick brown fox ate a hamburgerfons and got sick.');
|
||||
const paragraph = builder.build();
|
||||
|
||||
let wrapTo = 0;
|
||||
|
||||
let X = 100;
|
||||
let Y = 100;
|
||||
|
||||
const font = new CanvasKit.SkFont(null, 18);
|
||||
const fontPaint = new CanvasKit.SkPaint();
|
||||
fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
|
||||
fontPaint.setAntiAlias(true);
|
||||
|
||||
function drawFrame(canvas) {
|
||||
canvas.clear(CanvasKit.WHITE);
|
||||
wrapTo = 350 + 150 * Math.sin(Date.now() / 2000);
|
||||
paragraph.layout(wrapTo);
|
||||
canvas.drawParagraph(paragraph, 0, 0);
|
||||
|
||||
canvas.drawLine(wrapTo, 0, wrapTo, 400, fontPaint);
|
||||
|
||||
let posA = paragraph.getGlyphPositionAtCoordinate(X, Y);
|
||||
canvas.drawText(`At (${X}, ${Y}) glyph is ${posA.pos}`, 5, 450, fontPaint, font);
|
||||
|
||||
surface.requestAnimationFrame(drawFrame);
|
||||
}
|
||||
surface.requestAnimationFrame(drawFrame);
|
||||
|
||||
let interact = (e) => {
|
||||
X = e.offsetX;
|
||||
Y = e.offsetY;
|
||||
};
|
||||
|
||||
document.getElementById('para1').addEventListener('pointermove', interact);
|
||||
return surface;
|
||||
}
|
||||
</script>
|
||||
|
@ -68,6 +68,9 @@
|
||||
#include <emscripten/html5.h>
|
||||
#endif
|
||||
|
||||
#ifdef SK_INCLUDE_PARAGRAPH
|
||||
#include "modules/skparagraph/include/Paragraph.h"
|
||||
#endif
|
||||
// Aliases for less typing
|
||||
using BoneIndices = SkVertices::BoneIndices;
|
||||
using BoneWeights = SkVertices::BoneWeights;
|
||||
@ -846,6 +849,12 @@ EMSCRIPTEN_BINDINGS(Skia) {
|
||||
.function("drawLine", select_overload<void (SkScalar, SkScalar, SkScalar, SkScalar, const SkPaint&)>(&SkCanvas::drawLine))
|
||||
.function("drawOval", &SkCanvas::drawOval)
|
||||
.function("drawPaint", &SkCanvas::drawPaint)
|
||||
#ifdef SK_INCLUDE_PARAGRAPH
|
||||
.function("drawParagraph", optional_override([](SkCanvas& self, skia::textlayout::Paragraph* p,
|
||||
SkScalar x, SkScalar y) {
|
||||
p->paint(&self, x, y);
|
||||
}), allow_raw_pointers())
|
||||
#endif
|
||||
.function("drawPath", &SkCanvas::drawPath)
|
||||
// Of note, picture is *not* what is colloquially thought of as a "picture", what we call
|
||||
// a bitmap. An SkPicture is a series of draw commands.
|
||||
@ -909,8 +918,7 @@ EMSCRIPTEN_BINDINGS(Skia) {
|
||||
SkImageInfo dstInfo = toSkImageInfo(di);
|
||||
|
||||
return self.writePixels(dstInfo, pixels, srcRowBytes, dstX, dstY);
|
||||
}))
|
||||
;
|
||||
}));
|
||||
|
||||
class_<SkColorFilter>("SkColorFilter")
|
||||
.smart_ptr<sk_sp<SkColorFilter>>("sk_sp<SkColorFilter>>")
|
||||
|
@ -133,6 +133,11 @@ if [[ $@ == *primitive_shaper* ]]; then
|
||||
SHAPER_TARGETS=""
|
||||
fi
|
||||
|
||||
PARAGRAPH_JS="--pre-js $BASE_DIR/paragraph.js"
|
||||
PARAGRAPH_LIB="$BUILD_DIR/libskparagraph.a"
|
||||
PARAGRAPH_BINDINGS="-DSK_INCLUDE_PARAGRAPH=1 \
|
||||
$BASE_DIR/paragraph_bindings.cpp"
|
||||
|
||||
# Turn off exiting while we check for ninja (which may not be on PATH)
|
||||
set +e
|
||||
NINJA=`which ninja`
|
||||
@ -193,10 +198,12 @@ echo "Compiling bitcode"
|
||||
skia_enable_skshaper=true \
|
||||
skia_enable_ccpr=false \
|
||||
skia_enable_nvpr=false \
|
||||
skia_enable_skparagraph=true \
|
||||
skia_enable_pdf=false"
|
||||
|
||||
# Build all the libs, we'll link the appropriate ones down below
|
||||
${NINJA} -C ${BUILD_DIR} libskia.a libskottie.a libsksg.a libskshaper.a libparticles.a $SHAPER_TARGETS
|
||||
${NINJA} -C ${BUILD_DIR} libskia.a libskottie.a libsksg.a \
|
||||
libskparagraph.a libskshaper.a libparticles.a $SHAPER_TARGETS
|
||||
|
||||
export EMCC_CLOSURE_ARGS="--externs $BASE_DIR/externs.js "
|
||||
|
||||
@ -210,6 +217,7 @@ ${EMCXX} \
|
||||
-I. \
|
||||
-Ithird_party/icu \
|
||||
-Ithird_party/skcms \
|
||||
-Ithird_party/externals/icu/source/common/ \
|
||||
-DSK_DISABLE_READBUFFER \
|
||||
-DSK_DISABLE_AAA \
|
||||
$WASM_GPU \
|
||||
@ -219,6 +227,7 @@ ${EMCXX} \
|
||||
--pre-js $BASE_DIR/preamble.js \
|
||||
--pre-js $BASE_DIR/helper.js \
|
||||
--pre-js $BASE_DIR/interface.js \
|
||||
$PARAGRAPH_JS \
|
||||
$SKOTTIE_JS \
|
||||
$HTML_CANVAS_API \
|
||||
--pre-js $BASE_DIR/postamble.js \
|
||||
@ -228,8 +237,10 @@ ${EMCXX} \
|
||||
$PARTICLES_BINDINGS \
|
||||
$SKOTTIE_BINDINGS \
|
||||
$MANAGED_SKOTTIE_BINDINGS \
|
||||
$PARAGRAPH_BINDINGS \
|
||||
$SKOTTIE_LIB \
|
||||
$PARTICLES_LIB \
|
||||
$PARAGRAPH_LIB \
|
||||
$BUILD_DIR/libskshaper.a \
|
||||
$SHAPER_LIB \
|
||||
$BUILD_DIR/libskia.a \
|
||||
|
@ -96,6 +96,18 @@ var CanvasKit = {
|
||||
setResourceCacheLimitBytes: function() {},
|
||||
},
|
||||
|
||||
Paragraph: {
|
||||
// public API (from C++ bindings)
|
||||
getGlyphPositionAtCoordinate: function() {},
|
||||
layout: function() {},
|
||||
|
||||
// private API
|
||||
/** @return {Float32Array} */
|
||||
_getRectsForRange: function() {},
|
||||
},
|
||||
|
||||
ParagraphStyle: function() {},
|
||||
|
||||
RSXFormBuilder: function() {},
|
||||
SkColorBuilder: function() {},
|
||||
SkRectBuilder: function() {},
|
||||
@ -126,6 +138,7 @@ var CanvasKit = {
|
||||
drawLine: function() {},
|
||||
drawOval: function() {},
|
||||
drawPaint: function() {},
|
||||
drawParagraph: function() {},
|
||||
drawPath: function() {},
|
||||
drawPicture: function() {},
|
||||
drawRRect: function() {},
|
||||
@ -379,6 +392,8 @@ var CanvasKit = {
|
||||
texCoords: function() {},
|
||||
},
|
||||
|
||||
TextStyle: function() {},
|
||||
|
||||
// Constants and Enums
|
||||
gpu: {},
|
||||
skottie: {},
|
||||
@ -398,6 +413,16 @@ var CanvasKit = {
|
||||
CUBIC_VERB: {},
|
||||
CLOSE_VERB: {},
|
||||
|
||||
NoDecoration: {},
|
||||
UnderlineDecoration: {},
|
||||
OverlineDecoration: {},
|
||||
LineThroughDecoration: {},
|
||||
|
||||
Affinity: {
|
||||
Upstream: {},
|
||||
Downstream: {},
|
||||
},
|
||||
|
||||
AlphaType: {
|
||||
Opaque: {},
|
||||
Premul: {},
|
||||
@ -495,6 +520,16 @@ var CanvasKit = {
|
||||
ReverseDifference: {},
|
||||
},
|
||||
|
||||
RectHeightStyle: {
|
||||
Tight: {},
|
||||
Max: {},
|
||||
},
|
||||
|
||||
RectWidthStyle: {
|
||||
Tight: {},
|
||||
Max: {},
|
||||
},
|
||||
|
||||
StrokeCap: {
|
||||
Butt: {},
|
||||
Round: {},
|
||||
@ -507,6 +542,15 @@ var CanvasKit = {
|
||||
Bevel: {},
|
||||
},
|
||||
|
||||
TextAlign: {
|
||||
Left: {},
|
||||
Right: {},
|
||||
Center: {},
|
||||
Justify: {},
|
||||
Start: {},
|
||||
End: {},
|
||||
},
|
||||
|
||||
TextEncoding: {
|
||||
UTF8: {},
|
||||
UTF16: {},
|
||||
@ -561,6 +605,8 @@ var CanvasKit = {
|
||||
// Public API things that are newly declared in the JS should go here.
|
||||
// It's not enough to declare them above, because closure can still erase them
|
||||
// unless they go on the prototype.
|
||||
CanvasKit.Paragraph.prototype.getRectsForRange = function() {};
|
||||
|
||||
CanvasKit.SkPath.prototype.addArc = function() {};
|
||||
CanvasKit.SkPath.prototype.addOval = function() {};
|
||||
CanvasKit.SkPath.prototype.addPath = function() {};
|
||||
|
@ -704,14 +704,14 @@ CanvasKit.onRuntimeInitialized = function() {
|
||||
return retVal;
|
||||
}
|
||||
|
||||
// arguments should all be an arrayBuffer or be an array of arrayBuffers.
|
||||
// arguments should all be arrayBuffers or be an array of arrayBuffers.
|
||||
CanvasKit.SkFontMgr.FromData = function() {
|
||||
if (!arguments.length) {
|
||||
SkDebug('Could not make SkFontMgr from no font sources');
|
||||
return null;
|
||||
}
|
||||
var fonts = arguments;
|
||||
if (Array.isArray(fonts) && fonts.length === 1) {
|
||||
if (fonts.length === 1 && Array.isArray(fonts[0])) {
|
||||
fonts = arguments[0];
|
||||
}
|
||||
if (!fonts.length) {
|
||||
|
76
modules/canvaskit/paragraph.js
Normal file
76
modules/canvaskit/paragraph.js
Normal file
@ -0,0 +1,76 @@
|
||||
(function(CanvasKit){
|
||||
CanvasKit._extraInitializations = CanvasKit._extraInitializations || [];
|
||||
CanvasKit._extraInitializations.push(function() {
|
||||
|
||||
CanvasKit.Paragraph.prototype.getRectsForRange = function(start, end, hStyle, wStyle) {
|
||||
/**
|
||||
* This is bytes, but we'll want to think about them as float32s
|
||||
* @type {Float32Array}
|
||||
*/
|
||||
var floatArray = this._getRectsForRange(start, end, hStyle, wStyle);
|
||||
|
||||
if (!floatArray || !floatArray.length) {
|
||||
return [];
|
||||
}
|
||||
var ret = [];
|
||||
for (var i = 0; i < floatArray.length; i+=4) {
|
||||
ret.push(CanvasKit.LTRBRect(floatArray[i], floatArray[i+1], floatArray[i+2], floatArray[i+3]))
|
||||
}
|
||||
CanvasKit._free(floatArray.byteOffset);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// These helpers fill out all fields, because emscripten complains if we
|
||||
// have undefined and it expects, for example, a float.
|
||||
CanvasKit.ParagraphStyle = function(s) {
|
||||
// Use [''] to tell closure not to minify the names
|
||||
s['heightMultiplier'] = s['heightMultiplier'] || 0;
|
||||
s['maxLines'] = s['maxLines'] || 0;
|
||||
s['textAlign'] = s['textAlign'] || CanvasKit.TextAlign.Start;
|
||||
s['textStyle'] = CanvasKit.TextStyle(s['textStyle']);
|
||||
return s;
|
||||
}
|
||||
|
||||
CanvasKit.TextStyle = function(s) {
|
||||
// Use [''] to tell closure not to minify the names
|
||||
s['backgroundColor'] = s['backgroundColor'] || 0;
|
||||
s['color'] = s['color'] || 0;
|
||||
s['decoration'] = s['decoration'] || 0;
|
||||
s['decorationThickness'] = s['decorationThickness'] || 0;
|
||||
s['fontSize'] = s['fontSize'] || 0;
|
||||
if (Array.isArray(s['fontFamilies']) && s['fontFamilies'].length) {
|
||||
var sPtr = naiveCopyStrArray(s['fontFamilies']);
|
||||
s['_fontFamilies'] = sPtr;
|
||||
s['_numFontFamilies'] = s['fontFamilies'].length;
|
||||
} else {
|
||||
s['_fontFamilies'] = nullptr;
|
||||
s['_numFontFamilies'] = 0;
|
||||
}
|
||||
s['foregroundColor'] = s['foregroundColor'] || 0;
|
||||
return s;
|
||||
}
|
||||
|
||||
// returns a pointer to a place on the heap that has an array
|
||||
// of char* (effectively a char**). For now, this does the naive thing
|
||||
// and depends on the string being null-terminated. This should be used
|
||||
// for simple, well-formed things (e.g. font-families), not arbitrary
|
||||
// text that should be drawn. If we need this to handle more complex
|
||||
// strings, it should return two pointers, a pointer of the
|
||||
// string array and a pointer to an array of the strings byte lengths.
|
||||
function naiveCopyStrArray(strings) {
|
||||
if (!strings || !strings.length) {
|
||||
return nullptr;
|
||||
}
|
||||
var sPtrs = [];
|
||||
for (var i = 0; i < strings.length; i++) {
|
||||
var str = strings[i];
|
||||
// Add 1 for null terminator, which we need when copying/converting
|
||||
var strLen = lengthBytesUTF8(str) + 1;
|
||||
var strPtr = CanvasKit._malloc(strLen);
|
||||
stringToUTF8(str, strPtr, strLen);
|
||||
sPtrs.push(strPtr);
|
||||
}
|
||||
return copy1dArray(sPtrs, CanvasKit.HEAPU32);
|
||||
}
|
||||
});
|
||||
}(Module)); // When this file is loaded in, the high level object is "Module";
|
196
modules/canvaskit/paragraph_bindings.cpp
Normal file
196
modules/canvaskit/paragraph_bindings.cpp
Normal file
@ -0,0 +1,196 @@
|
||||
/*
|
||||
* Copyright 2019 Google LLC
|
||||
*
|
||||
* Use of this source code is governed by a BSD-style license that can be
|
||||
* found in the LICENSE file.
|
||||
*/
|
||||
|
||||
#include "include/core/SkColor.h"
|
||||
#include "include/core/SkString.h"
|
||||
|
||||
#include "modules/skparagraph/include/DartTypes.h"
|
||||
#include "modules/skparagraph/include/Paragraph.h"
|
||||
#include "modules/skparagraph/include/ParagraphBuilder.h"
|
||||
#include "modules/skparagraph/include/TextStyle.h"
|
||||
#include "modules/skparagraph/src/ParagraphBuilderImpl.h"
|
||||
#include "modules/skparagraph/src/ParagraphImpl.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <emscripten.h>
|
||||
#include <emscripten/bind.h>
|
||||
#include "modules/canvaskit/WasmAliases.h"
|
||||
|
||||
using namespace emscripten;
|
||||
|
||||
namespace para = skia::textlayout;
|
||||
|
||||
struct SimpleTextStyle {
|
||||
SkColor color;
|
||||
SkColor foregroundColor;
|
||||
SkColor backgroundColor;
|
||||
uint8_t decoration;
|
||||
SkScalar fontSize;
|
||||
SkScalar decorationThickness;
|
||||
uintptr_t /* const char** */ fontFamilies;
|
||||
int numFontFamilies;
|
||||
};
|
||||
|
||||
para::TextStyle toTextStyle(const SimpleTextStyle& s) {
|
||||
para::TextStyle ts;
|
||||
if (s.color != 0) {
|
||||
ts.setColor(s.color);
|
||||
}
|
||||
|
||||
if (s.foregroundColor != 0) {
|
||||
SkPaint p;
|
||||
p.setColor(s.foregroundColor);
|
||||
ts.setForegroundColor(p);
|
||||
}
|
||||
|
||||
if (s.backgroundColor != 0) {
|
||||
SkPaint p;
|
||||
p.setColor(s.backgroundColor);
|
||||
ts.setBackgroundColor(p);
|
||||
}
|
||||
|
||||
if (s.fontSize != 0) {
|
||||
ts.setFontSize(s.fontSize);
|
||||
}
|
||||
|
||||
ts.setDecoration(para::TextDecoration(s.decoration));
|
||||
if (s.decorationThickness != 0) {
|
||||
ts.setDecorationThicknessMultiplier(s.decorationThickness);
|
||||
}
|
||||
|
||||
const char** fontFamilies = reinterpret_cast<const char**>(s.fontFamilies);
|
||||
if (s.numFontFamilies > 0 && fontFamilies != nullptr) {
|
||||
std::vector<SkString> ff;
|
||||
for (int i = 0; i< s.numFontFamilies; i++) {
|
||||
ff.emplace_back(fontFamilies[i]);
|
||||
}
|
||||
ts.setFontFamilies(ff);
|
||||
}
|
||||
|
||||
return ts;
|
||||
}
|
||||
|
||||
struct SimpleParagraphStyle {
|
||||
SimpleTextStyle textStyle;
|
||||
SkScalar heightMultiplier;
|
||||
para::TextAlign textAlign;
|
||||
size_t maxLines;
|
||||
};
|
||||
|
||||
para::ParagraphStyle toParagraphStyle(const SimpleParagraphStyle& s) {
|
||||
para::ParagraphStyle ps;
|
||||
auto ts = toTextStyle(s.textStyle);
|
||||
ps.setTextStyle(ts);
|
||||
if (s.heightMultiplier != 0) {
|
||||
ps.setHeight(s.heightMultiplier);
|
||||
}
|
||||
ps.setTextAlign(s.textAlign);
|
||||
if (s.maxLines != 0) {
|
||||
ps.setMaxLines(s.maxLines);
|
||||
}
|
||||
return ps;
|
||||
}
|
||||
|
||||
Float32Array GetRectsForRange(para::ParagraphImpl& self, unsigned start, unsigned end,
|
||||
para::RectHeightStyle heightStyle, para::RectWidthStyle widthStyle) {
|
||||
std::vector<para::TextBox> boxes = self.getRectsForRange(start, end, heightStyle, widthStyle);
|
||||
// Pack these text boxes into an array of n groups of 4 SkScalar (floats)
|
||||
if (!boxes.size()) {
|
||||
return emscripten::val::null();
|
||||
}
|
||||
SkRect* rects = new SkRect[boxes.size()];
|
||||
for (int i = 0; i< boxes.size(); i++) {
|
||||
rects[i] = boxes[i].rect;
|
||||
}
|
||||
float* fPtr = reinterpret_cast<float*>(rects);
|
||||
// Of note: now that we have cast rects to float*, emscripten is smart enough to wrap this
|
||||
// into a Float32Array for us.
|
||||
return Float32Array(typed_memory_view(boxes.size()*4, fPtr));
|
||||
}
|
||||
|
||||
EMSCRIPTEN_BINDINGS(Paragraph) {
|
||||
|
||||
class_<para::Paragraph>("Paragraph");
|
||||
|
||||
// This "base<>" tells Emscripten that ParagraphImpl is a Paragraph and can get substituted
|
||||
// in properly in drawParagraph. However, Emscripten will not let us bind pure virtual methods
|
||||
// so we have to "expose" the ParagraphImpl and its methods.
|
||||
class_<para::ParagraphImpl, base<para::Paragraph>>("ParagraphImpl")
|
||||
.function("_getRectsForRange", &GetRectsForRange)
|
||||
.function("getGlyphPositionAtCoordinate", ¶::ParagraphImpl::getGlyphPositionAtCoordinate)
|
||||
.function("layout", ¶::ParagraphImpl::layout);
|
||||
|
||||
class_<para::ParagraphBuilderImpl>("ParagraphBuilder")
|
||||
.class_function("Make", optional_override([](SimpleParagraphStyle style,
|
||||
sk_sp<SkFontMgr> fontMgr)-> para::ParagraphBuilderImpl {
|
||||
auto fc = sk_make_sp<para::FontCollection>();
|
||||
fc->setDefaultFontManager(fontMgr);
|
||||
auto ps = toParagraphStyle(style);
|
||||
para::ParagraphBuilderImpl pbi(ps, fc);
|
||||
return pbi;
|
||||
}), allow_raw_pointers())
|
||||
.function("addText", optional_override([](para::ParagraphBuilderImpl& self, std::string text) {
|
||||
return self.addText(text.c_str(), text.length());
|
||||
}))
|
||||
.function("build", ¶::ParagraphBuilderImpl::Build, allow_raw_pointers())
|
||||
.function("pop", ¶::ParagraphBuilderImpl::pop)
|
||||
.function("pushStyle", optional_override([](para::ParagraphBuilderImpl& self,
|
||||
SimpleTextStyle textStyle) {
|
||||
auto ts = toTextStyle(textStyle);
|
||||
self.pushStyle(ts);
|
||||
}));
|
||||
|
||||
|
||||
enum_<para::Affinity>("Affinity")
|
||||
.value("Upstream", para::Affinity::kUpstream)
|
||||
.value("Downstream", para::Affinity::kDownstream);
|
||||
|
||||
enum_<para::RectHeightStyle>("RectHeightStyle")
|
||||
.value("Tight", para::RectHeightStyle::kTight)
|
||||
.value("Max", para::RectHeightStyle::kMax);
|
||||
|
||||
enum_<para::RectWidthStyle>("RectWidthStyle")
|
||||
.value("Tight", para::RectWidthStyle::kTight)
|
||||
.value("Max", para::RectWidthStyle::kMax);
|
||||
|
||||
enum_<para::TextAlign>("TextAlign")
|
||||
.value("Left", para::TextAlign::kLeft)
|
||||
.value("Right", para::TextAlign::kRight)
|
||||
.value("Center", para::TextAlign::kCenter)
|
||||
.value("Justify", para::TextAlign::kJustify)
|
||||
.value("Start", para::TextAlign::kStart)
|
||||
.value("End", para::TextAlign::kEnd);
|
||||
|
||||
|
||||
value_object<para::PositionWithAffinity>("PositionWithAffinity")
|
||||
.field("pos", ¶::PositionWithAffinity::position)
|
||||
.field("affinity", ¶::PositionWithAffinity::affinity);
|
||||
|
||||
value_object<SimpleParagraphStyle>("ParagraphStyle")
|
||||
.field("heightMultiplier", &SimpleParagraphStyle::heightMultiplier)
|
||||
.field("maxLines", &SimpleParagraphStyle::maxLines)
|
||||
.field("textAlign", &SimpleParagraphStyle::textAlign)
|
||||
.field("textStyle", &SimpleParagraphStyle::textStyle);
|
||||
|
||||
value_object<SimpleTextStyle>("TextStyle")
|
||||
.field("backgroundColor", &SimpleTextStyle::backgroundColor)
|
||||
.field("color", &SimpleTextStyle::color)
|
||||
.field("decoration", &SimpleTextStyle::decoration)
|
||||
.field("decorationThickness", &SimpleTextStyle::decorationThickness)
|
||||
.field("_fontFamilies", &SimpleTextStyle::fontFamilies)
|
||||
.field("fontSize", &SimpleTextStyle::fontSize)
|
||||
.field("foregroundColor", &SimpleTextStyle::foregroundColor)
|
||||
.field("_numFontFamilies", &SimpleTextStyle::numFontFamilies);
|
||||
|
||||
// TextDecoration should be a const because they can be combined
|
||||
constant("NoDecoration", int(para::TextDecoration::kNoDecoration));
|
||||
constant("UnderlineDecoration", int(para::TextDecoration::kUnderline));
|
||||
constant("OverlineDecoration", int(para::TextDecoration::kOverline));
|
||||
constant("LineThroughDecoration", int(para::TextDecoration::kLineThrough));
|
||||
}
|
BIN
modules/canvaskit/tests/assets/NotoColorEmoji.ttf
Normal file
BIN
modules/canvaskit/tests/assets/NotoColorEmoji.ttf
Normal file
Binary file not shown.
297
modules/canvaskit/tests/paragraph.spec.js
Normal file
297
modules/canvaskit/tests/paragraph.spec.js
Normal file
@ -0,0 +1,297 @@
|
||||
describe('CanvasKit\'s Path Behavior', function() {
|
||||
let container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
const CANVAS_WIDTH = 600;
|
||||
const CANVAS_HEIGHT = 600;
|
||||
|
||||
beforeEach(function() {
|
||||
container.innerHTML = `
|
||||
<canvas width=600 height=600 id=test></canvas>
|
||||
<canvas width=600 height=600 id=report></canvas>`;
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
container.innerHTML = '';
|
||||
});
|
||||
|
||||
let notoSerifFontBuffer = null;
|
||||
// This font is known to support kerning
|
||||
const notoSerifFontLoaded = fetch('/assets/NotoSerif-Regular.ttf').then(
|
||||
(response) => response.arrayBuffer()).then(
|
||||
(buffer) => {
|
||||
notoSerifFontBuffer = buffer;
|
||||
});
|
||||
|
||||
let emojiFontBuffer = null;
|
||||
const emojiFontLoaded = fetch('/assets/NotoColorEmoji.ttf').then(
|
||||
(response) => response.arrayBuffer()).then(
|
||||
(buffer) => {
|
||||
emojiFontBuffer = buffer;
|
||||
});
|
||||
|
||||
it('draws shaped text in a paragraph', function(done) {
|
||||
Promise.all([LoadCanvasKit, notoSerifFontLoaded]).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.setColor(CanvasKit.RED);
|
||||
paint.setStyle(CanvasKit.PaintStyle.Stroke);
|
||||
|
||||
const fontMgr = CanvasKit.SkFontMgr.FromData(notoSerifFontBuffer);
|
||||
|
||||
const wrapTo = 200;
|
||||
|
||||
const paraStyle = new CanvasKit.ParagraphStyle({
|
||||
textStyle: {
|
||||
color: CanvasKit.BLACK,
|
||||
fontFamilies: ['Noto Serif'],
|
||||
fontSize: 20,
|
||||
},
|
||||
textAlign: CanvasKit.TextAlign.Center,
|
||||
maxLines: 8,
|
||||
});
|
||||
|
||||
const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr);
|
||||
builder.addText('VAVAVAVAVAVAVA\nVAVA\n');
|
||||
|
||||
const blueText = new CanvasKit.TextStyle({
|
||||
backgroundColor: CanvasKit.Color(234, 208, 232), // light pink
|
||||
color: CanvasKit.Color(48, 37, 199),
|
||||
decoration: CanvasKit.LineThroughDecoration,
|
||||
decorationThickness: 1.5, // multiplier based on font size
|
||||
fontSize: 24,
|
||||
});
|
||||
builder.pushStyle(blueText)
|
||||
builder.addText(`Gosh I hope this wraps at some point, it is such a long line.`)
|
||||
builder.pop();
|
||||
builder.addText(` I'm done with the blue now. `)
|
||||
builder.addText(`Now I hope we should stop before we get 8 lines tall. `);
|
||||
const paragraph = builder.build();
|
||||
|
||||
paragraph.layout(wrapTo);
|
||||
|
||||
canvas.drawRect(CanvasKit.LTRBRect(10, 10, wrapTo+10, wrapTo+10), paint);
|
||||
canvas.drawParagraph(paragraph, 10, 10);
|
||||
|
||||
surface.flush();
|
||||
|
||||
paint.delete();
|
||||
fontMgr.delete();
|
||||
reportSurface(surface, 'paragraph_basic', done);
|
||||
}));
|
||||
});
|
||||
|
||||
it('provides rectangles around glyph ranges', function(done) {
|
||||
// loosely based on SkParagraph_GetRectsForRangeParagraph test in c++ code.
|
||||
Promise.all([LoadCanvasKit, notoSerifFontLoaded]).then(catchException(done, () => {
|
||||
const surface = CanvasKit.MakeCanvasSurface('test');
|
||||
expect(surface).toBeTruthy('Could not make surface')
|
||||
if (!surface) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
const canvas = surface.getCanvas();
|
||||
|
||||
const fontMgr = CanvasKit.SkFontMgr.FromData(notoSerifFontBuffer);
|
||||
|
||||
const wrapTo = 550;
|
||||
const hStyle = CanvasKit.RectHeightStyle.Max;
|
||||
const wStyle = CanvasKit.RectWidthStyle.Tight;
|
||||
|
||||
const paraStyle = new CanvasKit.ParagraphStyle({
|
||||
textStyle: {
|
||||
color: CanvasKit.BLACK,
|
||||
fontFamilies: ['Noto Serif'],
|
||||
fontSize: 50,
|
||||
// TODO(kjlubick): font style
|
||||
},
|
||||
textAlign: CanvasKit.TextAlign.Left,
|
||||
maxLines: 10,
|
||||
});
|
||||
const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr);
|
||||
builder.addText('12345, \"67890\" 12345 67890 12345 67890 12345 67890 12345 67890 12345 67890 12345');
|
||||
const paragraph = builder.build();
|
||||
|
||||
paragraph.layout(wrapTo);
|
||||
|
||||
const ranges = [
|
||||
{
|
||||
start: 0,
|
||||
end: 0,
|
||||
expectedNum: 0,
|
||||
},
|
||||
{
|
||||
start: 0,
|
||||
end: 1,
|
||||
expectedNum: 1,
|
||||
color: CanvasKit.Color(200, 0, 200),
|
||||
},
|
||||
{
|
||||
start: 2,
|
||||
end: 8,
|
||||
expectedNum: 1,
|
||||
color: CanvasKit.Color(255, 0, 0),
|
||||
},
|
||||
{
|
||||
start: 8,
|
||||
end: 21,
|
||||
expectedNum: 1,
|
||||
color: CanvasKit.Color(0, 255, 0),
|
||||
},
|
||||
{
|
||||
start: 30,
|
||||
end: 100,
|
||||
expectedNum: 4,
|
||||
color: CanvasKit.Color(0, 0, 255),
|
||||
},
|
||||
{
|
||||
start: 19,
|
||||
end: 22,
|
||||
expectedNum: 1,
|
||||
color: CanvasKit.Color(0, 200, 200),
|
||||
}
|
||||
];
|
||||
// Move it down a bit so we can see the rects that go above 0,0
|
||||
canvas.translate(10, 10);
|
||||
canvas.drawParagraph(paragraph, 0, 0);
|
||||
|
||||
for (const test of ranges) {
|
||||
let rects = paragraph.getRectsForRange(test.start, test.end, hStyle, wStyle);
|
||||
expect(Array.isArray(rects)).toEqual(true);
|
||||
expect(rects.length).toEqual(test.expectedNum);
|
||||
|
||||
for (const rect of rects) {
|
||||
const p = new CanvasKit.SkPaint();
|
||||
p.setColor(test.color);
|
||||
p.setStyle(CanvasKit.PaintStyle.Stroke);
|
||||
canvas.drawRect(rect, p);
|
||||
p.delete();
|
||||
}
|
||||
}
|
||||
|
||||
surface.flush();
|
||||
fontMgr.delete();
|
||||
reportSurface(surface, 'paragraph_rects', done);
|
||||
}));
|
||||
});
|
||||
|
||||
// Disabled until we can update CanvasKit's freetype.
|
||||
xit('can draw emojis', function(done) {
|
||||
Promise.all([LoadCanvasKit, notoSerifFontLoaded, emojiFontLoaded]).then(catchException(done, () => {
|
||||
const surface = CanvasKit.MakeCanvasSurface('test');
|
||||
expect(surface).toBeTruthy('Could not make surface')
|
||||
if (!surface) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
const canvas = surface.getCanvas();
|
||||
|
||||
const fontMgr = CanvasKit.SkFontMgr.FromData([notoSerifFontBuffer, emojiFontBuffer]);
|
||||
fontMgr.dumpFamilies();
|
||||
|
||||
const wrapTo = 450;
|
||||
|
||||
const paraStyle = new CanvasKit.ParagraphStyle({
|
||||
textStyle: {
|
||||
color: CanvasKit.BLACK,
|
||||
// Put emoji first, otherwise zero-space-joiner will be matched by serif,
|
||||
// and we don't get families or rainbow flags.
|
||||
fontFamilies: ['Noto Color Emoji', 'Noto Serif'],
|
||||
fontSize: 30,
|
||||
},
|
||||
textAlign: CanvasKit.TextAlign.Left,
|
||||
maxLines: 10,
|
||||
});
|
||||
const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr);
|
||||
// FIXME(kjlubick): We need one style that doesn't have emoji, otherwise the 4 will
|
||||
// be "emoji 4".
|
||||
builder.addText('4 flags on following line:\n');
|
||||
builder.addText(`🏳️🌈 🇮🇹 🇱🇷 🇺🇸\n`);
|
||||
builder.addText('Rainbow Italy Liberia USA\n\n');
|
||||
builder.addText('Emoji below should wrap:\n');
|
||||
builder.addText(`🍕🍔🍟🥝🍱🕶🎩👩👩👦👩👩👧👧👩👩👦👩👩👧👧👩👩👦👩👩👧👧👩👩👦👩👩👧👧👩👩👦👩👩👧👧👩👩👦👩👩👧👧👩👩👦👩👩👧👧`);
|
||||
const paragraph = builder.build();
|
||||
|
||||
paragraph.layout(wrapTo);
|
||||
|
||||
canvas.drawParagraph(paragraph, 10, 10);
|
||||
|
||||
surface.flush();
|
||||
fontMgr.delete();
|
||||
reportSurface(surface, 'paragraph_emoji', done);
|
||||
}));
|
||||
});
|
||||
|
||||
// A bit wrong, check after jlavrova's change lands.
|
||||
xit('can do hit detection on ascii', function(done) {
|
||||
Promise.all([LoadCanvasKit, notoSerifFontLoaded]).then(catchException(done, () => {
|
||||
const surface = CanvasKit.MakeCanvasSurface('test');
|
||||
expect(surface).toBeTruthy('Could not make surface')
|
||||
if (!surface) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
const canvas = surface.getCanvas();
|
||||
const fontMgr = CanvasKit.SkFontMgr.FromData([notoSerifFontBuffer]);
|
||||
|
||||
const wrapTo = 300;
|
||||
|
||||
const paraStyle = new CanvasKit.ParagraphStyle({
|
||||
textStyle: {
|
||||
color: CanvasKit.BLACK,
|
||||
fontFamilies: ['Noto Serif'],
|
||||
fontSize: 50,
|
||||
},
|
||||
textAlign: CanvasKit.TextAlign.Left,
|
||||
maxLines: 10,
|
||||
});
|
||||
const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr);
|
||||
builder.addText('UNCOPYRIGHTABLE');
|
||||
const paragraph = builder.build();
|
||||
|
||||
paragraph.layout(wrapTo);
|
||||
|
||||
canvas.translate(10, 10);
|
||||
canvas.drawParagraph(paragraph, 0, 0);
|
||||
|
||||
const paint = new CanvasKit.SkPaint();
|
||||
|
||||
paint.setColor(CanvasKit.Color(255, 0, 0));
|
||||
paint.setStyle(CanvasKit.PaintStyle.Fill);
|
||||
canvas.drawCircle(20, 30, 3, paint);
|
||||
|
||||
paint.setColor(CanvasKit.Color(0, 0, 255));
|
||||
canvas.drawCircle(80, 90, 3, paint);
|
||||
|
||||
paint.setColor(CanvasKit.Color(0, 255, 0));
|
||||
canvas.drawCircle(280, 2, 3, paint);
|
||||
|
||||
let posU = paragraph.getGlyphPositionAtCoordinate(20, 30);
|
||||
expect(posU).toEqual({
|
||||
pos: 1,
|
||||
affinity: CanvasKit.Affinity.Upstream
|
||||
});
|
||||
let posA = paragraph.getGlyphPositionAtCoordinate(80, 90);
|
||||
expect(posA).toEqual({
|
||||
pos: 11,
|
||||
affinity: CanvasKit.Affinity.Downstream
|
||||
});
|
||||
let posG = paragraph.getGlyphPositionAtCoordinate(280, 2);
|
||||
expect(posG).toEqual({
|
||||
pos: 9,
|
||||
affinity: CanvasKit.Affinity.Upstream
|
||||
});
|
||||
|
||||
surface.flush();
|
||||
fontMgr.delete();
|
||||
reportSurface(surface, 'paragraph_hits', done);
|
||||
}));
|
||||
});
|
||||
|
||||
});
|
@ -62,6 +62,7 @@ struct PositionWithAffinity {
|
||||
int32_t position;
|
||||
Affinity affinity;
|
||||
|
||||
PositionWithAffinity() : position(0), affinity(kDownstream) {}
|
||||
PositionWithAffinity(int32_t p, Affinity a) : position(p), affinity(a) {}
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user