[canvaskit] support globalAlpha and dashed strokes

This involves a refactor of how we deal with paint.
Now, we have a source-of-truth paint that is copied
and modified for each of fill/stroke/shadow.

Adds preliminary support for blend modes.

Most of what remains is text, images, and gradients.

Bug: skia:
Change-Id: I41806adeb7de4faa6c98a580c4f1de4e4a34a37d
Reviewed-on: https://skia-review.googlesource.com/c/173223
Reviewed-by: Kevin Lubick <kjlubick@google.com>
This commit is contained in:
Kevin Lubick 2018-11-28 12:51:56 -05:00
parent 572fdcf08a
commit 12c0e50fda
6 changed files with 576 additions and 74 deletions

View File

@ -10,7 +10,7 @@
}
#patheffect,#paths,#sk_drinks,#sk_party, #sk_legos, #sk_onboarding,
#api1_c, #api2_c {
img, canvas {
width: 300px;
height: 300px;
}
@ -26,6 +26,8 @@
<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>
<img id=api5 width=300 height=300>
<canvas id=api5_c width=300 height=300></canvas>
<h2> CanvasKit draws Paths to the browser</h2>
<canvas id=vertex1 width=300 height=300></canvas>
@ -48,8 +50,6 @@
<canvas id=nima_example width=300 height=300></canvas>
<script type="text/javascript" src="/node_modules/canvaskit/bin/canvaskit.js"></script>
<script type="text/javascript" charset="utf-8">
@ -94,6 +94,7 @@
CanvasAPI2(CanvasKit);
CanvasAPI3(CanvasKit);
CanvasAPI4(CanvasKit);
CanvasAPI5(CanvasKit);
VertexAPI1(CanvasKit);
VertexAPI2(CanvasKit, bonesImage);
@ -607,11 +608,66 @@
ctx.fillStyle = 'green';
ctx.font = '16pt Arial';
ctx.fillText('This should be shadowed', 20, 80);
}
document.getElementById('api4').src = skcanvas.toDataURL();
}
function CanvasAPI5(CanvasKit) {
let skcanvas = CanvasKit.MakeCanvas(600, 600);
let realCanvas = document.getElementById('api5_c');
realCanvas.width = 600;
realCanvas.height = 600;
for (let canvas of [skcanvas, realCanvas]) {
let ctx = canvas.getContext('2d');
ctx.scale(1.1, 1.1);
ctx.translate(10, 10);
// Shouldn't impact the fillRect calls
ctx.setLineDash([5, 3]);
ctx.fillStyle = 'rgba(200, 0, 100, 0.81)';
ctx.fillRect(20, 30, 100, 100);
ctx.globalAlpha = 0.81;
ctx.fillStyle = 'rgba(200, 0, 100, 1.0)';
ctx.fillRect(120, 30, 100, 100);
// This shouldn't do anything
ctx.globalAlpha = 0.1;
ctx.fillStyle = 'rgba(200, 0, 100, 0.9)';
ctx.globalAlpha = 0.9;
// Intentional no-op to check ordering
ctx.clearRect(220, 30, 100, 100);
ctx.fillRect(220, 30, 100, 100);
ctx.fillRect(320, 30, 100, 100);
ctx.clearRect(330, 40, 80, 80);
ctx.strokeStyle = 'blue';
ctx.lineWidth = 3;
ctx.setLineDash([5, 3]);
ctx.strokeRect(20, 150, 100, 100);
ctx.setLineDash([50, 30]);
ctx.strokeRect(125, 150, 100, 100);
ctx.lineDashOffset = 25;
ctx.strokeRect(230, 150, 100, 100);
ctx.setLineDash([2, 5, 9]);
ctx.strokeRect(335, 150, 100, 100);
ctx.setLineDash([5, 2]);
ctx.moveTo(336, 400);
ctx.quadraticCurveTo(366, 488, 120, 450);
ctx.lineTo(300, 400);
ctx.stroke();
ctx.font = '36pt Arial';
ctx.strokeText('Dashed', 20, 350);
ctx.fillText('Not Dashed', 20, 400);
}
document.getElementById('api5').src = skcanvas.toDataURL();
}
function NimaExample(CanvasKit, nimaFile, nimaTexture) {
if (!CanvasKit || !nimaFile || !nimaTexture) {
return;

View File

@ -17,7 +17,6 @@
#include "SkColor.h"
#include "SkCornerPathEffect.h"
#include "SkDashPathEffect.h"
#include "SkDashPathEffect.h"
#include "SkData.h"
#include "SkDiscretePathEffect.h"
#include "SkEncodedImageFormat.h"
@ -496,6 +495,7 @@ EMSCRIPTEN_BINDINGS(Skia) {
SkPaint p(self);
return p;
}))
.function("getBlendMode", &SkPaint::getBlendMode)
.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.
@ -513,6 +513,7 @@ EMSCRIPTEN_BINDINGS(Skia) {
return self.measureText(text.c_str(), text.length());
}))
.function("setAntiAlias", &SkPaint::setAntiAlias)
.function("setBlendMode", &SkPaint::setBlendMode)
.function("setColor", optional_override([](SkPaint& self, JSColor color)->void {
// JS side gives us a signed int instead of an unsigned int for color
// Add a optional_override to change it out.
@ -601,12 +602,14 @@ EMSCRIPTEN_BINDINGS(Skia) {
.function("bounds", &SkVertices::bounds)
.function("mode", &SkVertices::mode)
.function("uniqueID", &SkVertices::uniqueID)
#ifdef SK_DEBUG
.function("dumpPositions", optional_override([](SkVertices& self)->void {
auto pos = self.positions();
for(int i = 0; i< self.vertexCount(); i++) {
SkDebugf("position[%d] = (%f, %f)\n", i, pos[i].x(), pos[i].y());
}
}))
#endif
.function("vertexCount", &SkVertices::vertexCount);

View File

@ -40,9 +40,10 @@ var CanvasKit = {
MakeSurface: function() {},
MakeWebGLCanvasSurface: function() {},
currentContext: function() {},
getSkDataBytes: function() {},
getColorComponents: function() {},
getSkDataBytes: function() {},
initFonts: function() {},
multiplyByAlpha: function() {},
setCurrentContext: function() {},
// private API (i.e. things declared in the bindings that we use
@ -79,8 +80,10 @@ var CanvasKit = {
clear: function() {},
drawPaint: function() {},
drawPath: function() {},
drawText: function() {},
drawRect: function() {},
drawShadow: function() {},
drawText: function() {},
drawVertices: function() {},
flush: function() {},
rotate: function() {},
save: function() {},
@ -113,6 +116,7 @@ var CanvasKit = {
// public API (from C++ bindings)
/** @return {CanvasKit.SkPaint} */
copy: function() {},
getBlendMode: function() {},
getColor: function() {},
getStrokeCap: function() {},
getStrokeJoin: function() {},
@ -121,6 +125,7 @@ var CanvasKit = {
getTextSize: function() {},
measureText: function() {},
setAntiAlias: function() {},
setBlendMode: function() {},
setColor: function() {},
setMaskFilter: function() {},
setPathEffect: function() {},
@ -196,8 +201,10 @@ var CanvasKit = {
SkVertices: {
// public API (from C++ bindings)
/** @return {CanvasKit.SkVertices} */
applyBones: function() {},
bounds: function() {},
mode: function() {},
uniqueID: function() {},
vertexCount: function() {},
// private API
/** @return {CanvasKit.SkVertices} */
@ -337,6 +344,7 @@ CanvasKit.SkPath.prototype.trim = function() {};
CanvasKit.SkSurface.prototype.flush = function() {};
CanvasKit.SkSurface.prototype.dispose = function() {};
/** @return {CanvasKit.SkVertices} */
CanvasKit.SkVertices.prototype.applyBones = function() {};
CanvasKit.SkImage.prototype.encodeToData = function() {};
@ -360,11 +368,14 @@ CanvasRenderingContext2D.prototype.arcTo = function() {};
CanvasRenderingContext2D.prototype.beginPath = function() {};
CanvasRenderingContext2D.prototype.bezierCurveTo = function() {};
CanvasRenderingContext2D.prototype.clearHitRegions = function() {};
CanvasRenderingContext2D.prototype.clearRect = function() {};
CanvasRenderingContext2D.prototype.closePath = function() {};
CanvasRenderingContext2D.prototype.drawFocusIfNeeded = function() {};
CanvasRenderingContext2D.prototype.ellipse = function() {};
CanvasRenderingContext2D.prototype.fill = function() {};
CanvasRenderingContext2D.prototype.fillRect = function() {};
CanvasRenderingContext2D.prototype.fillText = function() {};
CanvasRenderingContext2D.prototype.getLineDash = function() {};
CanvasRenderingContext2D.prototype.lineTo = function() {};
CanvasRenderingContext2D.prototype.measureText = function() {};
CanvasRenderingContext2D.prototype.moveTo = function() {};
@ -377,12 +388,15 @@ CanvasRenderingContext2D.prototype.rotate = function() {};
CanvasRenderingContext2D.prototype.save = function() {};
CanvasRenderingContext2D.prototype.scale = function() {};
CanvasRenderingContext2D.prototype.scrollPathIntoView = function() {};
CanvasRenderingContext2D.prototype.setLineDash = function() {};
CanvasRenderingContext2D.prototype.setTransform = function() {};
CanvasRenderingContext2D.prototype.stroke = function() {};
CanvasRenderingContext2D.prototype.strokeRect = function() {};
CanvasRenderingContext2D.prototype.strokeText = function() {};
CanvasRenderingContext2D.prototype.transform = function() {};
CanvasRenderingContext2D.prototype.translate = function() {};
// Not sure why this is needed - might be a bug in emsdk that this isn't properly declared.
function loadWebAssemblyModule() {};
var DOMMatrix = {};

View File

@ -28,4 +28,23 @@
((color >> 24) & 0xFF) / 255,
]
}
CanvasKit.multiplyByAlpha = function(color, alpha) {
if (alpha === 1) {
return color;
}
// extract as int from 0 to 255
var a = (color >> 24) & 0xFF;
a *= alpha;
// mask off the old alpha
color &= 0xFFFFFF;
return clamp(a) << 24 | color;
}
}(Module)); // When this file is loaded in, the high level object is "Module";
// See https://stackoverflow.com/a/31090240
// This contraption keeps closure from minifying away the check
// if btoa is defined *and* prevents runtime "btoa" or "window" is not defined.
// Defined outside any scopes to make it available in all files.
var isNode = !(new Function("try {return this===window;}catch(e){ return false;}")());

View File

@ -4,14 +4,9 @@
// SkCanvas).
(function(CanvasKit) {
// See https://stackoverflow.com/a/31090240
// This contraption keeps closure from minifying away the check
// if btoa is defined *and* prevents runtime "btoa" or "window" is not defined.
var isNode = !(new Function("try {return this===window;}catch(e){ return false;}")());
function argsAreFinite(args) {
function allAreFinite(args) {
for (var i = 0; i < args.length; i++) {
if (!Number.isFinite(args[0])) {
if (!Number.isFinite(args[i])) {
return false;
}
}
@ -75,17 +70,26 @@
this._canvas = skcanvas;
this._paint = new CanvasKit.SkPaint();
this._paint.setAntiAlias(true);
this._paint.setStrokeWidth(1);
this._paint.setStrokeMiter(10);
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._strokeColor = CanvasKit.BLACK;
this._fillColor = CanvasKit.BLACK;
this._shadowBlur = 0;
this._shadowColor = CanvasKit.TRANSPARENT;
this._shadowOffsetX = 0;
this._shadowOffsetY = 0;
this._globalAlpha = 1;
this._strokeWidth = 1;
this._lineDashOffset = 0;
this._lineDashList = [];
// aka SkBlendMode
this._globalCompositeOperation = CanvasKit.BlendMode.SrcOver;
this._paint.setStrokeWidth(this._strokeWidth);
this._paint.setBlendMode(this._globalCompositeOperation);
this._currentPath = new CanvasKit.SkPath();
this._currentSubpath = null;
@ -101,6 +105,44 @@
// by the surface of which it is based.
}
// This always accepts DOMMatrix/SVGMatrix or any other
// object that has properties a,b,c,d,e,f defined.
// It will return DOMMatrix if the constructor is defined
// (e.g. we are in a browser), otherwise it will return a
// flattened 9-element matrix. That 9 element matrix
// can also be accepted on all platforms (somewhat
// contrary to the specification, but at least keeps it usable
// on Node)
Object.defineProperty(this, 'currentTransform', {
enumerable: true,
get: function() {
if (isNode) {
return this._currentTransform.slice();
} else {
// a-f aren't in the order as we have them. a-f are in "SVG order".
var m = new DOMMatrix();
m.a = this._currentTransform[0];
m.c = this._currentTransform[1];
m.e = this._currentTransform[2];
m.b = this._currentTransform[3];
m.d = this._currentTransform[4];
m.f = this._currentTransform[5];
return m;
}
},
set: function(matrix) {
if (matrix.a) {
// if we see a property named 'a', guess that b-f will
// also be there.
this._currentTransform = [matrix.a, matrix.c, matrix.e,
matrix.b, matrix.d, matrix.f,
0, 0, 1];
} else if (matrix.length === 9) {
this._currentTransform = matrix;
}
}
});
Object.defineProperty(this, 'fillStyle', {
enumerable: true,
get: function() {
@ -124,6 +166,185 @@
}
});
Object.defineProperty(this, 'globalAlpha', {
enumerable: true,
get: function() {
return this._globalAlpha;
},
set: function(newAlpha) {
// ignore invalid values, as per the spec
if (!isFinite(newAlpha) || newAlpha < 0 || newAlpha > 1) {
return;
}
this._globalAlpha = newAlpha;
}
});
Object.defineProperty(this, 'globalCompositeOperation', {
enumerable: true,
get: function() {
switch (this._globalCompositeOperation) {
// composite-mode
case CanvasKit.BlendMode.SrcOver:
return 'source-over';
case CanvasKit.BlendMode.DstOver:
return 'destination-over';
case CanvasKit.BlendMode.Src:
return 'copy';
case CanvasKit.BlendMode.Dst:
return 'destination';
case CanvasKit.BlendMode.Clear:
return 'clear';
case CanvasKit.BlendMode.SrcIn:
return 'source-in';
case CanvasKit.BlendMode.DstIn:
return 'destination-in';
case CanvasKit.BlendMode.SrcOut:
return 'source-out';
case CanvasKit.BlendMode.DstOut:
return 'destination-out';
case CanvasKit.BlendMode.SrcATop:
return 'source-atop';
case CanvasKit.BlendMode.DstATop:
return 'destination-atop';
case CanvasKit.BlendMode.Xor:
return 'xor';
case CanvasKit.BlendMode.Plus:
return 'lighter';
case CanvasKit.BlendMode.Multiply:
return 'multiply';
case CanvasKit.BlendMode.Screen:
return 'screen';
case CanvasKit.BlendMode.Overlay:
return 'overlay';
case CanvasKit.BlendMode.Darken:
return 'darken';
case CanvasKit.BlendMode.Lighten:
return 'lighten';
case CanvasKit.BlendMode.ColorDodge:
return 'color-dodge';
case CanvasKit.BlendMode.ColorBurn:
return 'color-burn';
case CanvasKit.BlendMode.HardLight:
return 'hard-light';
case CanvasKit.BlendMode.SoftLight:
return 'soft-light';
case CanvasKit.BlendMode.Difference:
return 'difference';
case CanvasKit.BlendMode.Exclusion:
return 'exclusion';
case CanvasKit.BlendMode.Hue:
return 'hue';
case CanvasKit.BlendMode.Saturation:
return 'saturation';
case CanvasKit.BlendMode.Color:
return 'color';
case CanvasKit.BlendMode.Luminosity:
return 'luminosity';
}
},
set: function(newMode) {
switch (newMode) {
// composite-mode
case 'source-over':
this._globalCompositeOperation = CanvasKit.BlendMode.SrcOver;
break;
case 'destination-over':
this._globalCompositeOperation = CanvasKit.BlendMode.DstOver;
break;
case 'copy':
this._globalCompositeOperation = CanvasKit.BlendMode.Src;
break;
case 'destination':
this._globalCompositeOperation = CanvasKit.BlendMode.Dst;
break;
case 'clear':
this._globalCompositeOperation = CanvasKit.BlendMode.Clear;
break;
case 'source-in':
this._globalCompositeOperation = CanvasKit.BlendMode.SrcIn;
break;
case 'destination-in':
this._globalCompositeOperation = CanvasKit.BlendMode.DstIn;
break;
case 'source-out':
this._globalCompositeOperation = CanvasKit.BlendMode.SrcOut;
break;
case 'destination-out':
this._globalCompositeOperation = CanvasKit.BlendMode.DstOut;
break;
case 'source-atop':
this._globalCompositeOperation = CanvasKit.BlendMode.SrcATop;
break;
case 'destination-atop':
this._globalCompositeOperation = CanvasKit.BlendMode.DstATop;
break;
case 'xor':
this._globalCompositeOperation = CanvasKit.BlendMode.Xor;
break;
case 'lighter':
this._globalCompositeOperation = CanvasKit.BlendMode.Plus;
break;
case 'plus-lighter':
this._globalCompositeOperation = CanvasKit.BlendMode.Plus;
break;
case 'plus-darker':
throw 'plus-darker is not supported';
// blend-mode
case 'multiply':
this._globalCompositeOperation = CanvasKit.BlendMode.Multiply;
break;
case 'screen':
this._globalCompositeOperation = CanvasKit.BlendMode.Screen;
break;
case 'overlay':
this._globalCompositeOperation = CanvasKit.BlendMode.Overlay;
break;
case 'darken':
this._globalCompositeOperation = CanvasKit.BlendMode.Darken;
break;
case 'lighten':
this._globalCompositeOperation = CanvasKit.BlendMode.Lighten;
break;
case 'color-dodge':
this._globalCompositeOperation = CanvasKit.BlendMode.ColorDodge;
break;
case 'color-burn':
this._globalCompositeOperation = CanvasKit.BlendMode.ColorBurn;
break;
case 'hard-light':
this._globalCompositeOperation = CanvasKit.BlendMode.HardLight;
break;
case 'soft-light':
this._globalCompositeOperation = CanvasKit.BlendMode.SoftLight;
break;
case 'difference':
this._globalCompositeOperation = CanvasKit.BlendMode.Difference;
break;
case 'exclusion':
this._globalCompositeOperation = CanvasKit.BlendMode.Exclusion;
break;
case 'hue':
this._globalCompositeOperation = CanvasKit.BlendMode.Hue;
break;
case 'saturation':
this._globalCompositeOperation = CanvasKit.BlendMode.Saturation;
break;
case 'color':
this._globalCompositeOperation = CanvasKit.BlendMode.Color;
break;
case 'luminosity':
this._globalCompositeOperation = CanvasKit.BlendMode.Luminosity;
break;
default:
return;
}
this._paint.setBlendMode(this._globalCompositeOperation);
}
});
Object.defineProperty(this, 'lineCap', {
enumerable: true,
get: function() {
@ -151,6 +372,19 @@
}
});
Object.defineProperty(this, 'lineDashOffset', {
enumerable: true,
get: function() {
return this._lineDashOffset;
},
set: function(newOffset) {
if (!isFinite(newOffset)) {
return;
}
this._lineDashOffset = newOffset;
}
});
Object.defineProperty(this, 'lineJoin', {
enumerable: true,
get: function() {
@ -188,6 +422,7 @@
// Spec says to ignore NaN/Inf/0/negative values
return;
}
this._strokeWidth = newWidth;
this._paint.setStrokeWidth(newWidth);
}
});
@ -273,7 +508,7 @@
}
this.arcTo = function(x1, y1, x2, y2, radius) {
if (!argsAreFinite(arguments)) {
if (!allAreFinite(arguments)) {
return;
}
if (radius < 0) {
@ -301,7 +536,7 @@
}
this.bezierCurveTo = function(cp1x, cp1y, cp2x, cp2y, x, y) {
if (!argsAreFinite(arguments)) {
if (!allAreFinite(arguments)) {
return;
}
var pts = [cp1x, cp1y, cp2x, cp2y, x, y];
@ -318,6 +553,15 @@
this._currentSubpath.cubicTo(cp1x, cp1y, cp2x, cp2y, x, y);
}
this.clearRect = function(x, y, width, height) {
this._canvas.setMatrix(this._currentTransform);
this._paint.setStyle(CanvasKit.PaintStyle.Fill);
this._paint.setBlendMode(CanvasKit.BlendMode.Clear);
this._canvas.drawRect(CanvasKit.LTRBRect(x, y, x+width, y+height), this._paint);
this._canvas.setMatrix(CanvasKit.SkMatrix.identity());
this._paint.setBlendMode(this._globalCompositeOperation);
}
this.closePath = function() {
if (this._currentSubpath) {
this._currentSubpath.close();
@ -336,7 +580,7 @@
this.ellipse = function(x, y, radiusX, radiusY, rotation,
startAngle, endAngle, ccw) {
if (!argsAreFinite(arguments)) {
if (!allAreFinite(arguments)) {
return;
}
if (radiusX < 0 || radiusY < 0) {
@ -362,17 +606,29 @@
temp.delete();
}
// A helper to copy the current paint, ready for filling
// This applies the global alpha.
// Call dispose() after to clean up.
this._fillPaint = function() {
var paint = this._paint.copy();
paint.setStyle(CanvasKit.PaintStyle.Fill);
var alphaColor = CanvasKit.multiplyByAlpha(this._fillColor, this._globalAlpha);
paint.setColor(alphaColor);
paint.dispose = function() {
// If there are some helper effects in the future, clean them up
// here. In any case, we have .dispose() to make _fillPaint behave
// like _strokePaint and _shadowPaint.
this.delete();
}
return paint;
}
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 fillPaint = this._fillPaint();
var shadowPaint = this._shadowPaint();
var shadowPaint = this._shadowPaint(fillPaint);
if (shadowPaint) {
var offsetMatrix = CanvasKit.SkMatrix.multiply(
this._currentTransform,
@ -384,16 +640,22 @@
shadowPaint.dispose();
}
this._canvas.drawPath(this._currentPath, this._paint);
// set stroke width back to original size:
this._paint.setStrokeWidth(orig);
this._canvas.drawPath(this._currentPath, fillPaint);
fillPaint.dispose();
}
this.fillRect = function(x, y, width, height) {
var fillPaint = this._fillPaint();
this._canvas.setMatrix(this._currentTransform);
this._canvas.drawRect(CanvasKit.LTRBRect(x, y, x+width, y+height), fillPaint);
this._canvas.setMatrix(CanvasKit.SkMatrix.identity());
fillPaint.dispose();
}
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();
var fillPaint = this._fillPaint()
var shadowPaint = this._shadowPaint(fillPaint);
if (shadowPaint) {
var offsetMatrix = CanvasKit.SkMatrix.multiply(
this._currentTransform,
@ -405,12 +667,17 @@
// 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.drawText(text, x, y, fillPaint);
this._canvas.setMatrix(CanvasKit.SkMatrix.identity());
fillPaint.dispose();
}
this.getLineDash = function() {
return this._lineDashList.slice();
}
this.lineTo = function(x, y) {
if (!argsAreFinite(arguments)) {
if (!allAreFinite(arguments)) {
return;
}
var pts = [x, y];
@ -433,7 +700,7 @@
}
this.moveTo = function(x, y) {
if (!argsAreFinite(arguments)) {
if (!allAreFinite(arguments)) {
return;
}
var pts = [x, y];
@ -450,7 +717,7 @@
}
this.quadraticCurveTo = function(cpx, cpy, x, y) {
if (!argsAreFinite(arguments)) {
if (!allAreFinite(arguments)) {
return;
}
var pts = [cpx, cpy, x, y];
@ -466,7 +733,7 @@
}
this.rect = function(x, y, width, height) {
if (!argsAreFinite(arguments)) {
if (!allAreFinite(arguments)) {
return;
}
var pts = [x, y];
@ -490,8 +757,9 @@
}
this._currentTransform = newState.ctm;
// TODO(kjlubick): clipping region
// TODO(kjlubick): dash list
this._paint.setStrokeWidth(newState.sw);
this._lineDashList = newState.ldl;
this._strokeWidth = newState.sw;
this._paint.setStrokeWidth(this._strokeWidth);
this._strokeColor = newState.sc;
this._fillColor = newState.fc;
this._paint.setStrokeCap(newState.cap);
@ -501,7 +769,11 @@
this._shadowOffsetY = newState.soy;
this._shadowBlur = newState.sb;
this._shadowColor = newState.shc;
//TODO: globalAlpha, lineDashOffset, filter, globalCompositeOperation, font, textAlign, textBaseline, direction, imageSmoothingEnabled, imageSmoothingQuality.
this._globalAlpha = newState.ga;
this._globalCompositeOperation = newState.gco;
this._paint.setBlendMode(this._globalCompositeOperation);
this._lineDashOffset = newState.ldo;
//TODO: filter, font, textAlign, textBaseline, direction, imageSmoothingEnabled, imageSmoothingQuality.
}
this.rotate = function(radians, px, py) {
@ -514,8 +786,8 @@
this._canvasStateStack.push({
ctm: this._currentTransform.slice(),
// TODO(kjlubick): clipping region
// TODO(kjlubick): dash list
sw: this._paint.getStrokeWidth(),
ldl: this._lineDashList.slice(),
sw: this._strokeWidth,
sc: this._strokeColor,
fc: this._fillColor,
cap: this._paint.getStrokeCap(),
@ -525,7 +797,10 @@
soy: this._shadowOffsetY,
sb: this._shadowBlur,
shc: this._shadowColor,
//TODO: globalAlpha, lineDashOffset, filter, globalCompositeOperation, font, textAlign, textBaseline, direction, imageSmoothingEnabled, imageSmoothingQuality.
ga: this._globalAlpha,
ldo: this._lineDashOffset,
gco: this._globalCompositeOperation,
//TODO: filter, font, textAlign, textBaseline, direction, imageSmoothingEnabled, imageSmoothingQuality.
});
}
@ -544,6 +819,21 @@
return (Math.abs(sx) + Math.abs(sy))/2;
}
this.setLineDash = function(dashes) {
for (var i = 0; i < dashes.length; i++) {
if (!Number.isFinite(dashes[i]) || dashes[i] < 0) {
SkDebug('dash list must have positive, finite values');
return;
}
}
if (dashes.length % 2 === 1) {
// as per the spec, concatenate 2 copies of dashes
// to give it an even number of elements.
Array.prototype.push.apply(dashes, dashes);
}
this._lineDashList = dashes;
}
this.setTransform = function(a, b, c, d, e, f) {
this._currentTransform = [a, c, e,
b, d, f,
@ -551,11 +841,13 @@
}
// 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
// should be no shadow. This ends up being a copy of the given
// paint with a blur maskfilter and the correct color.
this._shadowPaint = function() {
this._shadowPaint = function(basePaint) {
// multiply first to see if the alpha channel goes to 0 after multiplication.
var alphaColor = CanvasKit.multiplyByAlpha(this._shadowColor, this._globalAlpha);
// if alpha is zero, no shadows
if (!CanvasKit.getColorComponents(this._shadowColor)[3]) {
if (!CanvasKit.getColorComponents(alphaColor)[3]) {
return null;
}
// one of these must also be non-zero (otherwise the shadow is
@ -563,8 +855,8 @@
if (!(this._shadowBlur || this._shadowOffsetY || this._shadowOffsetX)) {
return null;
}
var shadowPaint = this._paint.copy();
shadowPaint.setColor(this._shadowColor);
var shadowPaint = basePaint.copy();
shadowPaint.setColor(alphaColor);
var blurEffect = CanvasKit.MakeBlurMaskFilter(CanvasKit.BlurStyle.Normal,
Math.max(1, this._shadowBlur/2), // very little blur when < 1
false);
@ -579,18 +871,36 @@
return shadowPaint;
}
this.stroke = function() {
this._commitSubpath();
this._paint.setStyle(CanvasKit.PaintStyle.Stroke);
this._paint.setColor(this._strokeColor);
var orig = this._paint.getStrokeWidth();
// A helper to get a copy of the current paint, ready for stroking.
// This applies the global alpha and the dashedness.
// Call dispose() after to clean up.
this._strokePaint = function() {
var paint = this._paint.copy();
paint.setStyle(CanvasKit.PaintStyle.Stroke);
var alphaColor = CanvasKit.multiplyByAlpha(this._strokeColor, this._globalAlpha);
paint.setColor(alphaColor);
// 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 scaledWidth = this._strokeWidth * this._scalefactor();
paint.setStrokeWidth(scaledWidth);
var shadowPaint = this._shadowPaint();
if (this._lineDashList.length) {
var dashedEffect = CanvasKit.MakeSkDashPathEffect(this._lineDashList, this._lineDashOffset);
paint.setPathEffect(dashedEffect);
}
paint.dispose = function() {
dashedEffect && dashedEffect.delete();
this.delete();
}
return paint;
}
this.stroke = function() {
this._commitSubpath();
var strokePaint = this._strokePaint();
var shadowPaint = this._shadowPaint(strokePaint);
if (shadowPaint) {
var offsetMatrix = CanvasKit.SkMatrix.multiply(
this._currentTransform,
@ -602,16 +912,23 @@
shadowPaint.dispose();
}
this._canvas.drawPath(this._currentPath, this._paint);
// set stroke width back to original size:
this._paint.setStrokeWidth(orig);
this._canvas.drawPath(this._currentPath, strokePaint);
strokePaint.dispose();
}
this.strokeRect = function(x, y, width, height) {
var strokePaint = this._strokePaint();
this._canvas.setMatrix(this._currentTransform);
this._canvas.drawRect(CanvasKit.LTRBRect(x, y, x+width, y+height), strokePaint);
this._canvas.setMatrix(CanvasKit.SkMatrix.identity());
strokePaint.dispose();
}
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();
var strokePaint = this._strokePaint();
var shadowPaint = this._shadowPaint(strokePaint);
if (shadowPaint) {
var offsetMatrix = CanvasKit.SkMatrix.multiply(
this._currentTransform,
@ -623,8 +940,9 @@
// 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.drawText(text, x, y, strokePaint);
this._canvas.setMatrix(CanvasKit.SkMatrix.identity());
strokePaint.dispose();
}
this.translate = function(dx, dy) {

View File

@ -91,6 +91,43 @@ describe('CanvasKit\'s Canvas 2d Behavior', function() {
done();
}));
});
it('can multiply colors by alpha', function(done) {
LoadCanvasKit.then(catchException(done, () => {
const multiplyByAlpha = CanvasKit.multiplyByAlpha;
const testCases = [
{
inColor: CanvasKit.Color(102, 51, 153, 1.0),
inAlpha: 1.0,
outColor: CanvasKit.Color(102, 51, 153, 1.0),
},
{
inColor: CanvasKit.Color(102, 51, 153, 1.0),
inAlpha: 0.8,
outColor: CanvasKit.Color(102, 51, 153, 0.8),
},
{
inColor: CanvasKit.Color(102, 51, 153, 0.8),
inAlpha: 0.7,
outColor: CanvasKit.Color(102, 51, 153, 0.56),
},
{
inColor: CanvasKit.Color(102, 51, 153, 0.8),
inAlpha: 1000,
outColor: CanvasKit.Color(102, 51, 153, 1.0),
},
];
for (let tc of testCases) {
// Print out the test case if the two don't match.
expect(multiplyByAlpha(tc.inColor, tc.inAlpha))
.toBe(tc.outColor, JSON.stringify(tc));
}
done();
}));
});
}); // end describe('color string parsing')
function multipleCanvasTest(testname, done, test) {
@ -253,6 +290,57 @@ describe('CanvasKit\'s Canvas 2d Behavior', function() {
}));
});
it('fills/strokes rects and supports some global settings', function(done) {
LoadCanvasKit.then(catchException(done, () => {
multipleCanvasTest('global_dashed_rects', done, (canvas) => {
let ctx = canvas.getContext('2d');
ctx.scale(1.1, 1.1);
ctx.translate(10, 10);
// Shouldn't impact the fillRect calls
ctx.setLineDash([5, 3]);
ctx.fillStyle = 'rgba(200, 0, 100, 0.81)';
ctx.fillRect(20, 30, 100, 100);
ctx.globalAlpha = 0.81;
ctx.fillStyle = 'rgba(200, 0, 100, 1.0)';
ctx.fillRect(120, 30, 100, 100);
// This shouldn't do anything
ctx.globalAlpha = 0.1;
ctx.fillStyle = 'rgba(200, 0, 100, 0.9)';
ctx.globalAlpha = 0.9;
// Intentional no-op to check ordering
ctx.clearRect(220, 30, 100, 100);
ctx.fillRect(220, 30, 100, 100);
ctx.fillRect(320, 30, 100, 100);
ctx.clearRect(330, 40, 80, 80);
ctx.strokeStyle = 'blue';
ctx.lineWidth = 3;
ctx.setLineDash([5, 3]);
ctx.strokeRect(20, 150, 100, 100);
ctx.setLineDash([50, 30]);
ctx.strokeRect(125, 150, 100, 100);
ctx.lineDashOffset = 25;
ctx.strokeRect(230, 150, 100, 100);
ctx.setLineDash([2, 5, 9]);
ctx.strokeRect(335, 150, 100, 100);
ctx.setLineDash([5, 2]);
ctx.moveTo(336, 400);
ctx.quadraticCurveTo(366, 488, 120, 450);
ctx.lineTo(300, 400);
ctx.stroke();
ctx.font = '36pt Arial';
ctx.strokeText('Dashed', 20, 350);
ctx.fillText('Not Dashed', 20, 400);
});
}));
});
it('can read default properties', function(done) {
LoadCanvasKit.then(catchException(done, () => {
const skcanvas = CanvasKit.MakeCanvas(CANVAS_WIDTH, CANVAS_HEIGHT);
@ -265,8 +353,12 @@ describe('CanvasKit\'s Canvas 2d Behavior', function() {
const toTest = ['font', 'lineWidth', 'strokeStyle', 'lineCap',
'lineJoin', 'miterLimit', 'shadowOffsetY',
'shadowBlur', 'shadowColor', 'shadowOffsetX'];
'shadowBlur', 'shadowColor', 'shadowOffsetX',
'globalAlpha', 'globalCompositeOperation',
'lineDashOffset'];
// Compare all the default values of the properties of skcanvas
// to the default values on the properties of a real canvas.
for( let attr of toTest) {
expect(skcontext[attr]).toBe(realContext[attr], attr);
}