From 61ef7b25895775b2ac9ed1c7f99f6fbf4994266a Mon Sep 17 00:00:00 2001 From: Kevin Lubick Date: Tue, 27 Nov 2018 13:26:59 -0500 Subject: [PATCH] [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 --- experimental/canvaskit/canvaskit/example.html | 56 +++ experimental/canvaskit/canvaskit_bindings.cpp | 56 ++- experimental/canvaskit/externs.js | 60 +++- experimental/canvaskit/helper.js | 11 + experimental/canvaskit/htmlcanvas/canvas2d.js | 332 +++++++++++++++++- experimental/canvaskit/tests/canvas2d.spec.js | 91 ++++- 6 files changed, 576 insertions(+), 30 deletions(-) diff --git a/experimental/canvaskit/canvaskit/example.html b/experimental/canvaskit/canvaskit/example.html index 84bde8a3e2..0228865129 100644 --- a/experimental/canvaskit/canvaskit/example.html +++ b/experimental/canvaskit/canvaskit/example.html @@ -24,6 +24,8 @@ + +

CanvasKit draws Paths to the browser

@@ -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; diff --git a/experimental/canvaskit/canvaskit_bindings.cpp b/experimental/canvaskit/canvaskit_bindings.cpp index 04432f8051..64e37822a3 100644 --- a/experimental/canvaskit/canvaskit_bindings.cpp +++ b/experimental/canvaskit/canvaskit_bindings.cpp @@ -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 { + // 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()const>(&SkImage::encodeToData)) .function("_encodeToDataWithFormat", select_overload(SkEncodedImageFormat encodedImageFormat, int quality)const>(&SkImage::encodeToData)); + class_("SkMaskFilter") + .smart_ptr>("sk_sp"); + class_("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_("PaintStyle") - .value("Fill", SkPaint::Style::kFill_Style) - .value("Stroke", SkPaint::Style::kStroke_Style) - .value("StrokeAndFill", SkPaint::Style::kStrokeAndFill_Style); + enum_("BlurStyle") + .value("Normal", SkBlurStyle::kNormal_SkBlurStyle) + .value("Solid", SkBlurStyle::kSolid_SkBlurStyle) + .value("Outer", SkBlurStyle::kOuter_SkBlurStyle) + .value("Inner", SkBlurStyle::kInner_SkBlurStyle); enum_("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_("ImageFormat") + .value("PNG", SkEncodedImageFormat::kPNG) + .value("JPEG", SkEncodedImageFormat::kJPEG); + + enum_("PaintStyle") + .value("Fill", SkPaint::Style::kFill_Style) + .value("Stroke", SkPaint::Style::kStroke_Style) + .value("StrokeAndFill", SkPaint::Style::kStrokeAndFill_Style); + enum_("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_("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") + .element(&SkPoint3::fX) + .element(&SkPoint3::fY) + .element(&SkPoint3::fZ); + // {"w": Number, "h", Number} value_object("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 diff --git a/experimental/canvaskit/externs.js b/experimental/canvaskit/externs.js index c2758e4a62..d239325e3c 100644 --- a/experimental/canvaskit/externs.js +++ b/experimental/canvaskit/externs.js @@ -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() {}; diff --git a/experimental/canvaskit/helper.js b/experimental/canvaskit/helper.js index 4253c27ef7..5e0aee2faa 100644 --- a/experimental/canvaskit/helper.js +++ b/experimental/canvaskit/helper.js @@ -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"; diff --git a/experimental/canvaskit/htmlcanvas/canvas2d.js b/experimental/canvaskit/htmlcanvas/canvas2d.js index 0325b34e37..d164c8d1c4 100644 --- a/experimental/canvaskit/htmlcanvas/canvas2d.js +++ b/experimental/canvaskit/htmlcanvas/canvas2d.js @@ -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 diff --git a/experimental/canvaskit/tests/canvas2d.spec.js b/experimental/canvaskit/tests/canvas2d.spec.js index 03e744b275..d8d6e3853a 100644 --- a/experimental/canvaskit/tests/canvas2d.spec.js +++ b/experimental/canvaskit/tests/canvas2d.spec.js @@ -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')