[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:
Kevin Lubick 2019-10-03 11:22:08 -04:00
parent c95c4a69e1
commit 369f6a5ea2
13 changed files with 712 additions and 6 deletions

View File

@ -1072,6 +1072,7 @@ group("modules") {
deps = [ deps = [
"modules/particles", "modules/particles",
"modules/skottie", "modules/skottie",
"modules/skparagraph",
"modules/skshaper", "modules/skshaper",
] ]
} }

View File

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `CanvasKit.MakeAnimatedImageFromEncoded`, `SkCanvas.drawAnimatedImage`. - `CanvasKit.MakeAnimatedImageFromEncoded`, `SkCanvas.drawAnimatedImage`.
- `CanvasKit.SkFontMgr.FromData` which takes several ArrayBuffers of font data, parses - `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. 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 ### Changed
- The `no_font` compile option should strip out more dead code related to fonts. - The `no_font` compile option should strip out more dead code related to fonts.

View File

@ -19,5 +19,6 @@ using JSObject = emscripten::val;
using JSString = emscripten::val; using JSString = emscripten::val;
using SkPathOrNull = emscripten::val; using SkPathOrNull = emscripten::val;
using Uint8Array = emscripten::val; using Uint8Array = emscripten::val;
using Float32Array = emscripten::val;
#endif #endif

View File

@ -26,6 +26,9 @@
<h2> Particles </h2> <h2> Particles </h2>
<canvas id=particles width=500 height=500></canvas> <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" src="/node_modules/canvaskit/bin/canvaskit.js"></script>
<script type="text/javascript" charset="utf-8"> <script type="text/javascript" charset="utf-8">
@ -63,6 +66,8 @@
}); });
ParticlesAPI1(CanvasKit); ParticlesAPI1(CanvasKit);
ParagraphAPI1(CanvasKit, robotoData);
}); });
fetch('https://storage.googleapis.com/skia-cdn/misc/lego_loader.json').then((resp) => { fetch('https://storage.googleapis.com/skia-cdn/misc/lego_loader.json').then((resp) => {
@ -127,6 +132,7 @@
SkottieExample(CanvasKit, 'sk_webfont', webfontJSON, fullBounds, { SkottieExample(CanvasKit, 'sk_webfont', webfontJSON, fullBounds, {
'Roboto-Regular': robotoData, 'Roboto-Regular': robotoData,
}); });
ParagraphAPI1(CanvasKit, robotoData);
}); });
}); });
@ -312,4 +318,66 @@ const snowfall = {
window.requestAnimationFrame(drawFrame); 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> </script>

View File

@ -68,6 +68,9 @@
#include <emscripten/html5.h> #include <emscripten/html5.h>
#endif #endif
#ifdef SK_INCLUDE_PARAGRAPH
#include "modules/skparagraph/include/Paragraph.h"
#endif
// Aliases for less typing // Aliases for less typing
using BoneIndices = SkVertices::BoneIndices; using BoneIndices = SkVertices::BoneIndices;
using BoneWeights = SkVertices::BoneWeights; 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("drawLine", select_overload<void (SkScalar, SkScalar, SkScalar, SkScalar, const SkPaint&)>(&SkCanvas::drawLine))
.function("drawOval", &SkCanvas::drawOval) .function("drawOval", &SkCanvas::drawOval)
.function("drawPaint", &SkCanvas::drawPaint) .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) .function("drawPath", &SkCanvas::drawPath)
// Of note, picture is *not* what is colloquially thought of as a "picture", what we call // 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. // a bitmap. An SkPicture is a series of draw commands.
@ -909,8 +918,7 @@ EMSCRIPTEN_BINDINGS(Skia) {
SkImageInfo dstInfo = toSkImageInfo(di); SkImageInfo dstInfo = toSkImageInfo(di);
return self.writePixels(dstInfo, pixels, srcRowBytes, dstX, dstY); return self.writePixels(dstInfo, pixels, srcRowBytes, dstX, dstY);
})) }));
;
class_<SkColorFilter>("SkColorFilter") class_<SkColorFilter>("SkColorFilter")
.smart_ptr<sk_sp<SkColorFilter>>("sk_sp<SkColorFilter>>") .smart_ptr<sk_sp<SkColorFilter>>("sk_sp<SkColorFilter>>")

View File

@ -133,6 +133,11 @@ if [[ $@ == *primitive_shaper* ]]; then
SHAPER_TARGETS="" SHAPER_TARGETS=""
fi 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) # Turn off exiting while we check for ninja (which may not be on PATH)
set +e set +e
NINJA=`which ninja` NINJA=`which ninja`
@ -180,7 +185,7 @@ echo "Compiling bitcode"
skia_use_piex=false \ skia_use_piex=false \
skia_use_system_libpng=true \ skia_use_system_libpng=true \
skia_use_system_freetype2=true \ skia_use_system_freetype2=true \
skia_use_system_libjpeg_turbo = false \ skia_use_system_libjpeg_turbo=false \
skia_use_vulkan=false \ skia_use_vulkan=false \
skia_use_wuffs = true \ skia_use_wuffs = true \
skia_use_zlib=true \ skia_use_zlib=true \
@ -193,10 +198,12 @@ echo "Compiling bitcode"
skia_enable_skshaper=true \ skia_enable_skshaper=true \
skia_enable_ccpr=false \ skia_enable_ccpr=false \
skia_enable_nvpr=false \ skia_enable_nvpr=false \
skia_enable_skparagraph=true \
skia_enable_pdf=false" skia_enable_pdf=false"
# Build all the libs, we'll link the appropriate ones down below # 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 " export EMCC_CLOSURE_ARGS="--externs $BASE_DIR/externs.js "
@ -210,6 +217,7 @@ ${EMCXX} \
-I. \ -I. \
-Ithird_party/icu \ -Ithird_party/icu \
-Ithird_party/skcms \ -Ithird_party/skcms \
-Ithird_party/externals/icu/source/common/ \
-DSK_DISABLE_READBUFFER \ -DSK_DISABLE_READBUFFER \
-DSK_DISABLE_AAA \ -DSK_DISABLE_AAA \
$WASM_GPU \ $WASM_GPU \
@ -219,6 +227,7 @@ ${EMCXX} \
--pre-js $BASE_DIR/preamble.js \ --pre-js $BASE_DIR/preamble.js \
--pre-js $BASE_DIR/helper.js \ --pre-js $BASE_DIR/helper.js \
--pre-js $BASE_DIR/interface.js \ --pre-js $BASE_DIR/interface.js \
$PARAGRAPH_JS \
$SKOTTIE_JS \ $SKOTTIE_JS \
$HTML_CANVAS_API \ $HTML_CANVAS_API \
--pre-js $BASE_DIR/postamble.js \ --pre-js $BASE_DIR/postamble.js \
@ -228,8 +237,10 @@ ${EMCXX} \
$PARTICLES_BINDINGS \ $PARTICLES_BINDINGS \
$SKOTTIE_BINDINGS \ $SKOTTIE_BINDINGS \
$MANAGED_SKOTTIE_BINDINGS \ $MANAGED_SKOTTIE_BINDINGS \
$PARAGRAPH_BINDINGS \
$SKOTTIE_LIB \ $SKOTTIE_LIB \
$PARTICLES_LIB \ $PARTICLES_LIB \
$PARAGRAPH_LIB \
$BUILD_DIR/libskshaper.a \ $BUILD_DIR/libskshaper.a \
$SHAPER_LIB \ $SHAPER_LIB \
$BUILD_DIR/libskia.a \ $BUILD_DIR/libskia.a \

View File

@ -96,6 +96,18 @@ var CanvasKit = {
setResourceCacheLimitBytes: function() {}, setResourceCacheLimitBytes: function() {},
}, },
Paragraph: {
// public API (from C++ bindings)
getGlyphPositionAtCoordinate: function() {},
layout: function() {},
// private API
/** @return {Float32Array} */
_getRectsForRange: function() {},
},
ParagraphStyle: function() {},
RSXFormBuilder: function() {}, RSXFormBuilder: function() {},
SkColorBuilder: function() {}, SkColorBuilder: function() {},
SkRectBuilder: function() {}, SkRectBuilder: function() {},
@ -126,6 +138,7 @@ var CanvasKit = {
drawLine: function() {}, drawLine: function() {},
drawOval: function() {}, drawOval: function() {},
drawPaint: function() {}, drawPaint: function() {},
drawParagraph: function() {},
drawPath: function() {}, drawPath: function() {},
drawPicture: function() {}, drawPicture: function() {},
drawRRect: function() {}, drawRRect: function() {},
@ -379,6 +392,8 @@ var CanvasKit = {
texCoords: function() {}, texCoords: function() {},
}, },
TextStyle: function() {},
// Constants and Enums // Constants and Enums
gpu: {}, gpu: {},
skottie: {}, skottie: {},
@ -398,6 +413,16 @@ var CanvasKit = {
CUBIC_VERB: {}, CUBIC_VERB: {},
CLOSE_VERB: {}, CLOSE_VERB: {},
NoDecoration: {},
UnderlineDecoration: {},
OverlineDecoration: {},
LineThroughDecoration: {},
Affinity: {
Upstream: {},
Downstream: {},
},
AlphaType: { AlphaType: {
Opaque: {}, Opaque: {},
Premul: {}, Premul: {},
@ -495,6 +520,16 @@ var CanvasKit = {
ReverseDifference: {}, ReverseDifference: {},
}, },
RectHeightStyle: {
Tight: {},
Max: {},
},
RectWidthStyle: {
Tight: {},
Max: {},
},
StrokeCap: { StrokeCap: {
Butt: {}, Butt: {},
Round: {}, Round: {},
@ -507,6 +542,15 @@ var CanvasKit = {
Bevel: {}, Bevel: {},
}, },
TextAlign: {
Left: {},
Right: {},
Center: {},
Justify: {},
Start: {},
End: {},
},
TextEncoding: { TextEncoding: {
UTF8: {}, UTF8: {},
UTF16: {}, UTF16: {},
@ -561,6 +605,8 @@ var CanvasKit = {
// Public API things that are newly declared in the JS should go here. // 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 // It's not enough to declare them above, because closure can still erase them
// unless they go on the prototype. // unless they go on the prototype.
CanvasKit.Paragraph.prototype.getRectsForRange = function() {};
CanvasKit.SkPath.prototype.addArc = function() {}; CanvasKit.SkPath.prototype.addArc = function() {};
CanvasKit.SkPath.prototype.addOval = function() {}; CanvasKit.SkPath.prototype.addOval = function() {};
CanvasKit.SkPath.prototype.addPath = function() {}; CanvasKit.SkPath.prototype.addPath = function() {};

View File

@ -704,14 +704,14 @@ CanvasKit.onRuntimeInitialized = function() {
return retVal; 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() { CanvasKit.SkFontMgr.FromData = function() {
if (!arguments.length) { if (!arguments.length) {
SkDebug('Could not make SkFontMgr from no font sources'); SkDebug('Could not make SkFontMgr from no font sources');
return null; return null;
} }
var fonts = arguments; var fonts = arguments;
if (Array.isArray(fonts) && fonts.length === 1) { if (fonts.length === 1 && Array.isArray(fonts[0])) {
fonts = arguments[0]; fonts = arguments[0];
} }
if (!fonts.length) { if (!fonts.length) {

View 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";

View 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", &para::ParagraphImpl::getGlyphPositionAtCoordinate)
.function("layout", &para::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", &para::ParagraphBuilderImpl::Build, allow_raw_pointers())
.function("pop", &para::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", &para::PositionWithAffinity::position)
.field("affinity", &para::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));
}

Binary file not shown.

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

View File

@ -62,6 +62,7 @@ struct PositionWithAffinity {
int32_t position; int32_t position;
Affinity affinity; Affinity affinity;
PositionWithAffinity() : position(0), affinity(kDownstream) {}
PositionWithAffinity(int32_t p, Affinity a) : position(p), affinity(a) {} PositionWithAffinity(int32_t p, Affinity a) : position(p), affinity(a) {}
}; };