[canvaskit] Add shadow and save/restore support

This also exposes the ShadowUtils::drawShadow on Canvas,
even though it wasn't what was needed to duplicate
the Canvas effect.

Bug: skia:
Change-Id: I12276ef106244218e4827b7fcd7949c83cf13e5f
Reviewed-on: https://skia-review.googlesource.com/c/172967
Reviewed-by: Kevin Lubick <kjlubick@google.com>
This commit is contained in:
Kevin Lubick 2018-11-27 13:26:59 -05:00
parent 2a1848d2aa
commit 61ef7b2589
6 changed files with 576 additions and 30 deletions

View File

@ -24,6 +24,8 @@
<canvas id=api2_c width=300 height=300></canvas>
<img id=api3 width=300 height=300>
<canvas id=api3_c width=300 height=300></canvas>
<img id=api4 width=300 height=300>
<canvas id=api4_c width=300 height=300></canvas>
<h2> CanvasKit draws Paths to the browser</h2>
<canvas id=vertex1 width=300 height=300></canvas>
@ -91,6 +93,7 @@
CanvasAPI1(CanvasKit);
CanvasAPI2(CanvasKit);
CanvasAPI3(CanvasKit);
CanvasAPI4(CanvasKit);
VertexAPI1(CanvasKit);
VertexAPI2(CanvasKit, bonesImage);
@ -556,6 +559,59 @@
document.getElementById('api3').src = skcanvas.toDataURL();
}
function CanvasAPI4(CanvasKit) {
let skcanvas = CanvasKit.MakeCanvas(300, 300);
let realCanvas = document.getElementById('api4_c');
realCanvas.width = 300;
realCanvas.height = 300;
for (let canvas of [skcanvas, realCanvas]) {
let ctx = canvas.getContext('2d');
ctx.strokeStyle = '#000';
ctx.fillStyle = '#CCC';
ctx.shadowColor = 'rebeccapurple';
ctx.shadowBlur = 1;
ctx.shadowOffsetX = 3;
ctx.shadowOffsetY = -8;
ctx.rect(10, 10, 30, 30);
ctx.save();
ctx.strokeStyle = '#C00';
ctx.fillStyle = '#00C';
ctx.shadowBlur = 0;
ctx.shadowColor = 'transparent';
ctx.stroke();
ctx.restore();
ctx.fill();
ctx.beginPath();
ctx.moveTo(36, 148);
ctx.quadraticCurveTo(66, 188, 120, 136);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.shadowColor = '#993366AA';
ctx.shadowOffsetX = 8;
ctx.shadowBlur = 5;
ctx.setTransform(2, 0, -.5, 2.5, -40, 120);
ctx.rect(110, 10, 20, 20);
ctx.lineTo(110, 0);
ctx.resetTransform();
ctx.lineTo(220, 120);
ctx.stroke();
ctx.fillStyle = 'green';
ctx.font = '16pt Arial';
ctx.fillText('This should be shadowed', 20, 80);
}
document.getElementById('api4').src = skcanvas.toDataURL();
}
function NimaExample(CanvasKit, nimaFile, nimaTexture) {
if (!CanvasKit || !nimaFile || !nimaTexture) {
return;

View File

@ -22,9 +22,11 @@
#include "SkDiscretePathEffect.h"
#include "SkEncodedImageFormat.h"
#include "SkFontMgr.h"
#include "SkBlurTypes.h"
#include "SkFontMgrPriv.h"
#include "SkGradientShader.h"
#include "SkImageShader.h"
#include "SkMaskFilter.h"
#include "SkPaint.h"
#include "SkParsePath.h"
#include "SkPath.h"
@ -32,6 +34,7 @@
#include "SkPathOps.h"
#include "SkScalar.h"
#include "SkShader.h"
#include "SkShadowUtils.h"
#include "SkString.h"
#include "SkStrokeRec.h"
#include "SkSurface.h"
@ -349,6 +352,10 @@ EMSCRIPTEN_BINDINGS(Skia) {
function("getSkDataBytes", &getSkDataBytes, allow_raw_pointers());
function("MakeSkCornerPathEffect", &SkCornerPathEffect::Make, allow_raw_pointers());
function("MakeSkDiscretePathEffect", &SkDiscretePathEffect::Make, allow_raw_pointers());
function("MakeBlurMaskFilter", optional_override([](SkBlurStyle style, SkScalar sigma, bool respectCTM)->sk_sp<SkMaskFilter> {
// Adds a little helper because emscripten doesn't expose default params.
return SkMaskFilter::MakeBlur(style, sigma, respectCTM);
}), allow_raw_pointers());
function("MakePathFromOp", &MakePathFromOp);
// These won't be called directly, there's a JS helper to deal with typed arrays.
@ -445,7 +452,16 @@ EMSCRIPTEN_BINDINGS(Skia) {
.function("drawPaint", &SkCanvas::drawPaint)
.function("drawPath", &SkCanvas::drawPath)
.function("drawRect", &SkCanvas::drawRect)
.function("drawText", optional_override([](SkCanvas& self, std::string text, SkScalar x, SkScalar y, const SkPaint& p) {
.function("drawShadow", optional_override([](SkCanvas& self, const SkPath& path,
const SkPoint3& zPlaneParams,
const SkPoint3& lightPos, SkScalar lightRadius,
JSColor ambientColor, JSColor spotColor,
uint32_t flags) {
SkShadowUtils::DrawShadow(&self, path, zPlaneParams, lightPos, lightRadius,
SkColor(ambientColor), SkColor(spotColor), flags);
}))
.function("drawText", optional_override([](SkCanvas& self, std::string text, SkScalar x,
SkScalar y, const SkPaint& p) {
// TODO(kjlubick): This does not work well for non-ascii
// Need to maybe add a helper in interface.js that supports UTF-8
// Otherwise, go with std::wstring and set UTF-32 encoding.
@ -471,12 +487,20 @@ EMSCRIPTEN_BINDINGS(Skia) {
.function("_encodeToData", select_overload<sk_sp<SkData>()const>(&SkImage::encodeToData))
.function("_encodeToDataWithFormat", select_overload<sk_sp<SkData>(SkEncodedImageFormat encodedImageFormat, int quality)const>(&SkImage::encodeToData));
class_<SkMaskFilter>("SkMaskFilter")
.smart_ptr<sk_sp<SkMaskFilter>>("sk_sp<SkMaskFilter>");
class_<SkPaint>("SkPaint")
.constructor<>()
.function("copy", optional_override([](const SkPaint& self)->SkPaint {
SkPaint p(self);
return p;
}))
.function("getColor", optional_override([](SkPaint& self)->JSColor {
// JS side gives us a signed int instead of an unsigned int for color
// Add a optional_override to change it out.
return JSColor(self.getColor());
}))
.function("getStrokeWidth", &SkPaint::getStrokeWidth)
.function("getStrokeMiter", &SkPaint::getStrokeMiter)
.function("getStrokeCap", &SkPaint::getStrokeCap)
@ -494,6 +518,7 @@ EMSCRIPTEN_BINDINGS(Skia) {
// Add a optional_override to change it out.
self.setColor(SkColor(color));
}))
.function("setMaskFilter", &SkPaint::setMaskFilter)
.function("setPathEffect", &SkPaint::setPathEffect)
.function("setShader", &SkPaint::setShader)
.function("setStrokeWidth", &SkPaint::setStrokeWidth)
@ -616,10 +641,11 @@ EMSCRIPTEN_BINDINGS(Skia) {
.value("Color", SkBlendMode::kColor)
.value("Luminosity", SkBlendMode::kLuminosity);
enum_<SkPaint::Style>("PaintStyle")
.value("Fill", SkPaint::Style::kFill_Style)
.value("Stroke", SkPaint::Style::kStroke_Style)
.value("StrokeAndFill", SkPaint::Style::kStrokeAndFill_Style);
enum_<SkBlurStyle>("BlurStyle")
.value("Normal", SkBlurStyle::kNormal_SkBlurStyle)
.value("Solid", SkBlurStyle::kSolid_SkBlurStyle)
.value("Outer", SkBlurStyle::kOuter_SkBlurStyle)
.value("Inner", SkBlurStyle::kInner_SkBlurStyle);
enum_<SkPath::FillType>("FillType")
.value("Winding", SkPath::FillType::kWinding_FillType)
@ -627,6 +653,15 @@ EMSCRIPTEN_BINDINGS(Skia) {
.value("InverseWinding", SkPath::FillType::kInverseWinding_FillType)
.value("InverseEvenOdd", SkPath::FillType::kInverseEvenOdd_FillType);
enum_<SkEncodedImageFormat>("ImageFormat")
.value("PNG", SkEncodedImageFormat::kPNG)
.value("JPEG", SkEncodedImageFormat::kJPEG);
enum_<SkPaint::Style>("PaintStyle")
.value("Fill", SkPaint::Style::kFill_Style)
.value("Stroke", SkPaint::Style::kStroke_Style)
.value("StrokeAndFill", SkPaint::Style::kStrokeAndFill_Style);
enum_<SkPathOp>("PathOp")
.value("Difference", SkPathOp::kDifference_SkPathOp)
.value("Intersect", SkPathOp::kIntersect_SkPathOp)
@ -660,10 +695,6 @@ EMSCRIPTEN_BINDINGS(Skia) {
.value("TrianglesStrip", SkVertices::VertexMode::kTriangleStrip_VertexMode)
.value("TriangleFan", SkVertices::VertexMode::kTriangleFan_VertexMode);
enum_<SkEncodedImageFormat>("ImageFormat")
.value("PNG", SkEncodedImageFormat::kPNG)
.value("JPEG", SkEncodedImageFormat::kJPEG);
// A value object is much simpler than a class - it is returned as a JS
// object and does not require delete().
@ -685,6 +716,12 @@ EMSCRIPTEN_BINDINGS(Skia) {
.element(&SkPoint::fX)
.element(&SkPoint::fY);
// SkPoint3s can be represented by [x, y, z]
value_array<SkPoint3>("SkPoint3")
.element(&SkPoint3::fX)
.element(&SkPoint3::fY)
.element(&SkPoint3::fZ);
// {"w": Number, "h", Number}
value_object<SkSize>("SkSize")
.field("w", &SkSize::fWidth)
@ -717,6 +754,7 @@ EMSCRIPTEN_BINDINGS(Skia) {
constant("BLUE", (JSColor) SK_ColorBLUE);
constant("YELLOW", (JSColor) SK_ColorYELLOW);
constant("CYAN", (JSColor) SK_ColorCYAN);
constant("BLACK", (JSColor) SK_ColorBLACK);
// TODO(?)
#if SK_INCLUDE_SKOTTIE

View File

@ -27,19 +27,21 @@ var CanvasKit = {
Color: function() {},
/** @return {CanvasKit.SkRect} */
LTRBRect: function() {},
MakeBlurMaskFilter: function() {},
MakeCanvas: function() {},
MakeCanvasSurface: function() {},
MakeSWCanvasSurface: function() {},
MakeWebGLCanvasSurface: function() {},
MakeImageShader: function() {},
MakeLinearGradientShader: function() {},
MakeRadialGradientShader: function() {},
MakeNimaActor: function() {},
MakeRadialGradientShader: function() {},
MakeSWCanvasSurface: function() {},
MakeSkDashPathEffect: function() {},
MakeSkVertices: function() {},
MakeSurface: function() {},
MakeWebGLCanvasSurface: function() {},
currentContext: function() {},
getSkDataBytes: function() {},
getColorComponents: function() {},
initFonts: function() {},
setCurrentContext: function() {},
@ -78,6 +80,7 @@ var CanvasKit = {
drawPaint: function() {},
drawPath: function() {},
drawText: function() {},
drawShadow: function() {},
flush: function() {},
rotate: function() {},
save: function() {},
@ -110,6 +113,7 @@ var CanvasKit = {
// public API (from C++ bindings)
/** @return {CanvasKit.SkPaint} */
copy: function() {},
getColor: function() {},
getStrokeCap: function() {},
getStrokeJoin: function() {},
getStrokeMiter: function() {},
@ -118,6 +122,7 @@ var CanvasKit = {
measureText: function() {},
setAntiAlias: function() {},
setColor: function() {},
setMaskFilter: function() {},
setPathEffect: function() {},
setShader: function() {},
setStrokeCap: function() {},
@ -203,6 +208,52 @@ var CanvasKit = {
gpu: {},
skottie: {},
TRANSPARENT: {},
RED: {},
BLUE: {},
YELLOW: {},
CYAN: {},
BLACK: {},
BlendMode: {
Clear: {},
Src: {},
Dst: {},
SrcOver: {},
DstOver: {},
SrcIn: {},
DstIn: {},
SrcOut: {},
DstOut: {},
SrcATop: {},
DstATop: {},
Xor: {},
Plus: {},
Modulate: {},
Screen: {},
Overlay: {},
Darken: {},
Lighten: {},
ColorDodge: {},
ColorBurn: {},
HardLight: {},
SoftLight: {},
Difference: {},
Exclusion: {},
Multiply: {},
Hue: {},
Saturation: {},
Color: {},
Luminosity: {},
},
BlurStyle: {
Normal: {},
Solid: {},
Outer: {},
Inner: {},
},
FillType: {
Winding: {},
EvenOdd: {},
@ -312,6 +363,7 @@ CanvasRenderingContext2D.prototype.clearHitRegions = function() {};
CanvasRenderingContext2D.prototype.closePath = function() {};
CanvasRenderingContext2D.prototype.drawFocusIfNeeded = function() {};
CanvasRenderingContext2D.prototype.ellipse = function() {};
CanvasRenderingContext2D.prototype.fill = function() {};
CanvasRenderingContext2D.prototype.fillText = function() {};
CanvasRenderingContext2D.prototype.lineTo = function() {};
CanvasRenderingContext2D.prototype.measureText = function() {};
@ -320,7 +372,9 @@ CanvasRenderingContext2D.prototype.quadraticCurveTo = function() {};
CanvasRenderingContext2D.prototype.rect = function() {};
CanvasRenderingContext2D.prototype.removeHitRegion = function() {};
CanvasRenderingContext2D.prototype.resetTransform = function() {};
CanvasRenderingContext2D.prototype.restore = function() {};
CanvasRenderingContext2D.prototype.rotate = function() {};
CanvasRenderingContext2D.prototype.save = function() {};
CanvasRenderingContext2D.prototype.scale = function() {};
CanvasRenderingContext2D.prototype.scrollPathIntoView = function() {};
CanvasRenderingContext2D.prototype.setTransform = function() {};

View File

@ -17,4 +17,15 @@
}
return (clamp(a*255) << 24) | (clamp(r) << 16) | (clamp(g) << 8) | (clamp(b) << 0);
}
// returns [r, g, b, a] from a color
// where a is scaled between 0 and 1.0
CanvasKit.getColorComponents = function(color) {
return [
(color >> 16) & 0xFF,
(color >> 8) & 0xFF,
(color >> 0) & 0xFF,
((color >> 24) & 0xFF) / 255,
]
}
}(Module)); // When this file is loaded in, the high level object is "Module";

View File

@ -80,10 +80,19 @@
this._paint.setStrokeCap(CanvasKit.StrokeCap.Butt);
this._paint.setStrokeJoin(CanvasKit.StrokeJoin.Miter);
this._strokeColor = CanvasKit.BLACK;
this._fillColor = CanvasKit.BLACK;
this._shadowBlur = 0;
this._shadowColor = CanvasKit.TRANSPARENT;
this._shadowOffsetX = 0;
this._shadowOffsetY = 0;
this._currentPath = new CanvasKit.SkPath();
this._currentSubpath = null;
this._currentTransform = CanvasKit.SkMatrix.identity();
this._canvasStateStack = [];
this._dispose = function() {
this._currentPath.delete();
this._currentSubpath && this._currentSubpath.delete();
@ -92,17 +101,88 @@
// by the surface of which it is based.
}
Object.defineProperty(this, 'fillStyle', {
enumerable: true,
get: function() {
return colorToString(this._fillColor);
},
set: function(newStyle) {
this._fillColor = parseColor(newStyle);
}
});
Object.defineProperty(this, 'font', {
enumerable: true,
get: function(newStyle) {
// TODO generate this
return '10px sans-serif';
},
set: function(newStyle) {
var size = parseFontSize(newStyle);
// TODO styles
// TODO(kjlubick) styles, font name
this._paint.setTextSize(size);
}
});
Object.defineProperty(this, 'lineCap', {
enumerable: true,
get: function() {
switch (this._paint.getStrokeCap()) {
case CanvasKit.StrokeCap.Butt:
return 'butt';
case CanvasKit.StrokeCap.Round:
return 'round';
case CanvasKit.StrokeCap.Square:
return 'square';
}
},
set: function(newCap) {
switch (newCap) {
case 'butt':
this._paint.setStrokeCap(CanvasKit.StrokeCap.Butt);
return;
case 'round':
this._paint.setStrokeCap(CanvasKit.StrokeCap.Round);
return;
case 'square':
this._paint.setStrokeCap(CanvasKit.StrokeCap.Square);
return;
}
}
});
Object.defineProperty(this, 'lineJoin', {
enumerable: true,
get: function() {
switch (this._paint.getStrokeJoin()) {
case CanvasKit.StrokeJoin.Miter:
return 'miter';
case CanvasKit.StrokeJoin.Round:
return 'round';
case CanvasKit.StrokeJoin.Bevel:
return 'bevel';
}
},
set: function(newJoin) {
switch (newJoin) {
case 'miter':
this._paint.setStrokeJoin(CanvasKit.StrokeJoin.Miter);
return;
case 'round':
this._paint.setStrokeJoin(CanvasKit.StrokeJoin.Round);
return;
case 'bevel':
this._paint.setStrokeJoin(CanvasKit.StrokeJoin.Bevel);
return;
}
}
});
Object.defineProperty(this, 'lineWidth', {
enumerable: true,
get: function() {
return this._paint.getStrokeWidth();
},
set: function(newWidth) {
if (newWidth <= 0 || !newWidth) {
// Spec says to ignore NaN/Inf/0/negative values
@ -112,10 +192,77 @@
}
});
Object.defineProperty(this, 'miterLimit', {
enumerable: true,
get: function() {
return this._paint.getStrokeMiter();
},
set: function(newLimit) {
if (newLimit <= 0 || !newLimit) {
// Spec says to ignore NaN/Inf/0/negative values
return;
}
this._paint.setStrokeMiter(newLimit);
}
});
Object.defineProperty(this, 'shadowBlur', {
enumerable: true,
get: function() {
return this._shadowBlur;
},
set: function(newBlur) {
// ignore negative, inf and NAN (but not 0) as per the spec.
if (newBlur < 0 || !isFinite(newBlur)) {
return;
}
this._shadowBlur = newBlur;
}
});
Object.defineProperty(this, 'shadowColor', {
enumerable: true,
get: function() {
return colorToString(this._shadowColor);
},
set: function(newColor) {
this._shadowColor = parseColor(newColor);
}
});
Object.defineProperty(this, 'shadowOffsetX', {
enumerable: true,
get: function() {
return this._shadowOffsetX;
},
set: function(newOffset) {
if (!isFinite(newOffset)) {
return;
}
this._shadowOffsetX = newOffset;
}
});
Object.defineProperty(this, 'shadowOffsetY', {
enumerable: true,
get: function() {
return this._shadowOffsetY;
},
set: function(newOffset) {
if (!isFinite(newOffset)) {
return;
}
this._shadowOffsetY = newOffset;
}
});
Object.defineProperty(this, 'strokeStyle', {
enumerable: true,
get: function() {
return colorToString(this._strokeColor);
},
set: function(newStyle) {
this._paint.setColor(parseColor(newStyle));
this._strokeColor = parseColor(newStyle);
}
});
@ -215,8 +362,48 @@
temp.delete();
}
this.fill = function() {
this._commitSubpath();
this._paint.setStyle(CanvasKit.PaintStyle.Fill);
this._paint.setColor(this._fillColor);
var orig = this._paint.getStrokeWidth();
// This is not in the spec, but it appears Chrome scales up
// the line width by some amount when stroking (and filling?).
var scaledWidth = orig * this._scalefactor();
this._paint.setStrokeWidth(scaledWidth);
var shadowPaint = this._shadowPaint();
if (shadowPaint) {
var offsetMatrix = CanvasKit.SkMatrix.multiply(
this._currentTransform,
CanvasKit.SkMatrix.translated(this._shadowOffsetX, this._shadowOffsetY)
);
this._canvas.setMatrix(offsetMatrix);
this._canvas.drawPath(this._currentPath, shadowPaint);
this._canvas.setMatrix(CanvasKit.SkMatrix.identity());
shadowPaint.dispose();
}
this._canvas.drawPath(this._currentPath, this._paint);
// set stroke width back to original size:
this._paint.setStrokeWidth(orig);
}
this.fillText = function(text, x, y, maxWidth) {
// TODO do something with maxWidth, probably involving measure
this._paint.setStyle(CanvasKit.PaintStyle.Fill);
this._paint.setColor(this._fillColor);
var shadowPaint = this._shadowPaint();
if (shadowPaint) {
var offsetMatrix = CanvasKit.SkMatrix.multiply(
this._currentTransform,
CanvasKit.SkMatrix.translated(this._shadowOffsetX, this._shadowOffsetY)
);
this._canvas.setMatrix(offsetMatrix);
this._canvas.drawText(text, x, y, shadowPaint);
shadowPaint.dispose();
// Don't need to setMatrix back, it will be handled by the next few lines.
}
this._canvas.setMatrix(this._currentTransform);
this._canvas.drawText(text, x, y, this._paint);
this._canvas.setMatrix(CanvasKit.SkMatrix.identity());
@ -296,12 +483,52 @@
this._currentTransform = CanvasKit.SkMatrix.identity();
}
this.restore = function() {
var newState = this._canvasStateStack.pop();
if (!newState) {
return;
}
this._currentTransform = newState.ctm;
// TODO(kjlubick): clipping region
// TODO(kjlubick): dash list
this._paint.setStrokeWidth(newState.sw);
this._strokeColor = newState.sc;
this._fillColor = newState.fc;
this._paint.setStrokeCap(newState.cap);
this._paint.setStrokeJoin(newState.jn);
this._paint.setStrokeMiter(newState.mtr);
this._shadowOffsetX = newState.sox;
this._shadowOffsetY = newState.soy;
this._shadowBlur = newState.sb;
this._shadowColor = newState.shc;
//TODO: globalAlpha, lineDashOffset, filter, globalCompositeOperation, font, textAlign, textBaseline, direction, imageSmoothingEnabled, imageSmoothingQuality.
}
this.rotate = function(radians, px, py) {
this._currentTransform = CanvasKit.SkMatrix.multiply(
this._currentTransform,
CanvasKit.SkMatrix.rotated(radians, px, py));
}
this.save = function() {
this._canvasStateStack.push({
ctm: this._currentTransform.slice(),
// TODO(kjlubick): clipping region
// TODO(kjlubick): dash list
sw: this._paint.getStrokeWidth(),
sc: this._strokeColor,
fc: this._fillColor,
cap: this._paint.getStrokeCap(),
jn: this._paint.getStrokeJoin(),
mtr: this._paint.getStrokeMiter(),
sox: this._shadowOffsetX,
soy: this._shadowOffsetY,
sb: this._shadowBlur,
shc: this._shadowColor,
//TODO: globalAlpha, lineDashOffset, filter, globalCompositeOperation, font, textAlign, textBaseline, direction, imageSmoothingEnabled, imageSmoothingQuality.
});
}
this.scale = function(sx, sy) {
this._currentTransform = CanvasKit.SkMatrix.multiply(
this._currentTransform,
@ -323,24 +550,78 @@
0, 0, 1];
}
this.stroke = function() {
if (this._currentSubpath) {
this._commitSubpath();
this._paint.setStyle(CanvasKit.PaintStyle.Stroke);
var orig = this._paint.getStrokeWidth();
// This is not in the spec, but it appears Chrome scales up
// the line width by some amount when stroking (and filling?).
var scaledWidth = orig * this._scalefactor();
this._paint.setStrokeWidth(scaledWidth);
this._canvas.drawPath(this._currentPath, this._paint);
// set stroke width back to original size:
this._paint.setStrokeWidth(orig);
// Returns the shadow paint for the current settings or null if there
// should be no shadow. This ends up being a copy of the current
// paint with a blur maskfilter and the correct color.
this._shadowPaint = function() {
// if alpha is zero, no shadows
if (!CanvasKit.getColorComponents(this._shadowColor)[3]) {
return null;
}
// one of these must also be non-zero (otherwise the shadow is
// completely hidden. And the spec says so).
if (!(this._shadowBlur || this._shadowOffsetY || this._shadowOffsetX)) {
return null;
}
var shadowPaint = this._paint.copy();
shadowPaint.setColor(this._shadowColor);
var blurEffect = CanvasKit.MakeBlurMaskFilter(CanvasKit.BlurStyle.Normal,
Math.max(1, this._shadowBlur/2), // very little blur when < 1
false);
shadowPaint.setMaskFilter(blurEffect);
// hack up a "destructor" which also cleans up the blurEffect. Otherwise,
// we leak the blurEffect (since smart pointers don't help us in JS land).
shadowPaint.dispose = function() {
blurEffect.delete();
this.delete();
};
return shadowPaint;
}
this.stroke = function() {
this._commitSubpath();
this._paint.setStyle(CanvasKit.PaintStyle.Stroke);
this._paint.setColor(this._strokeColor);
var orig = this._paint.getStrokeWidth();
// This is not in the spec, but it appears Chrome scales up
// the line width by some amount when stroking (and filling?).
var scaledWidth = orig * this._scalefactor();
this._paint.setStrokeWidth(scaledWidth);
var shadowPaint = this._shadowPaint();
if (shadowPaint) {
var offsetMatrix = CanvasKit.SkMatrix.multiply(
this._currentTransform,
CanvasKit.SkMatrix.translated(this._shadowOffsetX, this._shadowOffsetY)
);
this._canvas.setMatrix(offsetMatrix);
this._canvas.drawPath(this._currentPath, shadowPaint);
this._canvas.setMatrix(CanvasKit.SkMatrix.identity());
shadowPaint.dispose();
}
this._canvas.drawPath(this._currentPath, this._paint);
// set stroke width back to original size:
this._paint.setStrokeWidth(orig);
}
this.strokeText = function(text, x, y, maxWidth) {
// TODO do something with maxWidth, probably involving measure
this._paint.setStyle(CanvasKit.PaintStyle.Stroke);
this._paint.setColor(this._strokeColor);
var shadowPaint = this._shadowPaint();
if (shadowPaint) {
var offsetMatrix = CanvasKit.SkMatrix.multiply(
this._currentTransform,
CanvasKit.SkMatrix.translated(this._shadowOffsetX, this._shadowOffsetY)
);
this._canvas.setMatrix(offsetMatrix);
this._canvas.drawText(text, x, y, shadowPaint);
shadowPaint.dispose();
// Don't need to setMatrix back, it will be handled by the next few lines.
}
this._canvas.setMatrix(this._currentTransform);
this._canvas.drawText(text, x, y, this._paint);
this._canvas.setMatrix(CanvasKit.SkMatrix.identity());
@ -421,6 +702,28 @@
}
}
function colorToString(skcolor) {
// https://html.spec.whatwg.org/multipage/canvas.html#serialisation-of-a-color
var components = CanvasKit.getColorComponents(skcolor);
var r = components[0];
var g = components[1];
var b = components[2];
var a = components[3];
if (a === 1.0) {
// hex
r = r.toString(16).toLowerCase();
g = g.toString(16).toLowerCase();
b = b.toString(16).toLowerCase();
r = (r.length === 1 ? '0'+r: r);
g = (g.length === 1 ? '0'+g: g);
b = (b.length === 1 ? '0'+b: b);
return '#'+r+g+b;
} else {
a = (a === 0 || a === 1) ? a : a.toFixed(8);
return 'rgba('+r+', '+g+', '+b+', '+a+')';
}
}
function valueOrPercent(aStr) {
var a = parseFloat(aStr) || 1;
if (aStr && aStr.indexOf('%') !== -1) {
@ -484,6 +787,7 @@
}
CanvasKit._testing['parseColor'] = parseColor;
CanvasKit._testing['colorToString'] = colorToString;
// Create the following with
// node ./htmlcanvas/_namedcolors.js --expose-wasm

View File

@ -33,14 +33,14 @@ describe('CanvasKit\'s Canvas 2d Behavior', function() {
container.innerHTML = '';
});
describe('color string parsing', function() {
describe('color strings', function() {
function hex(s) {
return parseInt(s, 16);
}
it('parses hex color strings', function(done) {
LoadCanvasKit.then(catchException(done, () => {
let parseColor = CanvasKit._testing.parseColor;
const parseColor = CanvasKit._testing.parseColor;
expect(parseColor('#FED')).toEqual(
CanvasKit.Color(hex('FF'), hex('EE'), hex('DD'), 1));
expect(parseColor('#FEDC')).toEqual(
@ -54,7 +54,7 @@ describe('CanvasKit\'s Canvas 2d Behavior', function() {
});
it('parses rgba color strings', function(done) {
LoadCanvasKit.then(catchException(done, () => {
let parseColor = CanvasKit._testing.parseColor;
const parseColor = CanvasKit._testing.parseColor;
expect(parseColor('rgba(117, 33, 64, 0.75)')).toEqual(
CanvasKit.Color(117, 33, 64, 0.75));
expect(parseColor('rgb(117, 33, 64, 0.75)')).toEqual(
@ -68,7 +68,7 @@ describe('CanvasKit\'s Canvas 2d Behavior', function() {
});
it('parses named color strings', function(done) {
LoadCanvasKit.then(catchException(done, () => {
let parseColor = CanvasKit._testing.parseColor;
const parseColor = CanvasKit._testing.parseColor;
expect(parseColor('grey')).toEqual(
CanvasKit.Color(128, 128, 128, 1.0));
expect(parseColor('blanchedalmond')).toEqual(
@ -78,6 +78,19 @@ describe('CanvasKit\'s Canvas 2d Behavior', function() {
done();
}));
});
it('properly produces color strings', function(done) {
LoadCanvasKit.then(catchException(done, () => {
const colorToString = CanvasKit._testing.colorToString;
expect(colorToString(CanvasKit.Color(102, 51, 153, 1.0))).toEqual('#663399');
expect(colorToString(CanvasKit.Color(255, 235, 205, 0.5))).toEqual(
'rgba(255, 235, 205, 0.50196078)');
done();
}));
});
}); // end describe('color string parsing')
function multipleCanvasTest(testname, done, test) {
@ -192,6 +205,76 @@ describe('CanvasKit\'s Canvas 2d Behavior', function() {
});
}));
});
it('properly saves and restores states, even when drawing shadows', function(done) {
LoadCanvasKit.then(catchException(done, () => {
multipleCanvasTest('shadows_and_save_restore', done, (canvas) => {
let ctx = canvas.getContext('2d');
ctx.strokeStyle = '#000';
ctx.fillStyle = '#CCC';
ctx.shadowColor = 'rebeccapurple';
ctx.shadowBlur = 1;
ctx.shadowOffsetX = 3;
ctx.shadowOffsetY = -8;
ctx.rect(10, 10, 30, 30);
ctx.save();
ctx.strokeStyle = '#C00';
ctx.fillStyle = '#00C';
ctx.shadowBlur = 0;
ctx.shadowColor = 'transparent';
ctx.stroke();
ctx.restore();
ctx.fill();
ctx.beginPath();
ctx.moveTo(36, 148);
ctx.quadraticCurveTo(66, 188, 120, 136);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.shadowColor = '#993366AA';
ctx.shadowOffsetX = 8;
ctx.shadowBlur = 5;
ctx.setTransform(2, 0, -.5, 2.5, -40, 120);
ctx.rect(110, 10, 20, 20);
ctx.lineTo(110, 0);
ctx.resetTransform();
ctx.lineTo(220, 120);
ctx.stroke();
ctx.fillStyle = 'green';
ctx.font = '16pt Arial';
ctx.fillText('This should be shadowed', 20, 80);
});
}));
});
it('can read default properties', function(done) {
LoadCanvasKit.then(catchException(done, () => {
const skcanvas = CanvasKit.MakeCanvas(CANVAS_WIDTH, CANVAS_HEIGHT);
const realCanvas = document.getElementById('test');
realCanvas.width = CANVAS_WIDTH;
realCanvas.height = CANVAS_HEIGHT;
const skcontext = skcanvas.getContext('2d');
const realContext = realCanvas.getContext('2d');
const toTest = ['font', 'lineWidth', 'strokeStyle', 'lineCap',
'lineJoin', 'miterLimit', 'shadowOffsetY',
'shadowBlur', 'shadowColor', 'shadowOffsetX'];
for( let attr of toTest) {
expect(skcontext[attr]).toBe(realContext[attr], attr);
}
skcanvas.dispose();
done();
}));
});
}); // end describe('Path drawing API')