mirror of
https://github.com/alibaba/flutter-go.git
synced 2025-07-15 03:04:25 +08:00
1070 lines
28 KiB
JavaScript
1070 lines
28 KiB
JavaScript
// Copyright 2018 The Chromium Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
// TODO(yjbanov): Consider the following optimizations:
|
|
// - Switch from JSON to typed arrays. See:
|
|
// https://github.com/w3c/css-houdini-drafts/issues/136
|
|
// - When there is no DOM-rendered content, then clipping in the canvas is more
|
|
// efficient than DOM-rendered clipping.
|
|
// - When DOM-rendered clip is the only option, then clipping _again_ in the
|
|
// canvas is superfluous.
|
|
// - When transform is a 2D transform and there is no DOM-rendered content, then
|
|
// canvas transform is more efficient than DOM-rendered transform.
|
|
// - If a transform must be DOM-rendered, then clipping in the canvas _again_ is
|
|
// superfluous.
|
|
|
|
/**
|
|
* Applies paint commands to CSS Paint API (a.k.a. Houdini).
|
|
*
|
|
* This painter is driven by houdini_canvas.dart. This painter and the
|
|
* HoudiniCanvas class must be kept in sync with each other.
|
|
*/
|
|
class FlutterPainter {
|
|
/**
|
|
* Properties used by this painter.
|
|
*
|
|
* @return {string[]} list of CSS properties this painter depends on.
|
|
*/
|
|
static get inputProperties() {
|
|
return ['--flt'];
|
|
}
|
|
|
|
/**
|
|
* Implements the painter interface.
|
|
*/
|
|
paint(ctx, geom, properties) {
|
|
let fltProp = properties.get('--flt').toString();
|
|
if (!fltProp) {
|
|
// Nothing to paint.
|
|
return;
|
|
}
|
|
const commands = JSON.parse(fltProp);
|
|
for (let i = 0; i < commands.length; i++) {
|
|
let command = commands[i];
|
|
// TODO(yjbanov): we should probably move command identifiers into an enum
|
|
switch (command[0]) {
|
|
case 1:
|
|
this._save(ctx, geom, command);
|
|
break;
|
|
case 2:
|
|
this._restore(ctx, geom, command);
|
|
break;
|
|
case 3:
|
|
this._translate(ctx, geom, command);
|
|
break;
|
|
case 4:
|
|
this._scale(ctx, geom, command);
|
|
break;
|
|
case 5:
|
|
this._rotate(ctx, geom, command);
|
|
break;
|
|
// Skip case 6: implemented in the DOM for now.
|
|
case 7:
|
|
this._skew(ctx, geom, command);
|
|
break;
|
|
case 8:
|
|
this._clipRect(ctx, geom, command);
|
|
break;
|
|
case 9:
|
|
this._clipRRect(ctx, geom, command);
|
|
break;
|
|
case 10:
|
|
this._clipPath(ctx, geom, command);
|
|
break;
|
|
case 11:
|
|
this._drawColor(ctx, geom, command);
|
|
break;
|
|
case 12:
|
|
this._drawLine(ctx, geom, command);
|
|
break;
|
|
case 13:
|
|
this._drawPaint(ctx, geom, command);
|
|
break;
|
|
case 14:
|
|
this._drawRect(ctx, geom, command);
|
|
break;
|
|
case 15:
|
|
this._drawRRect(ctx, geom, command);
|
|
break;
|
|
case 16:
|
|
this._drawDRRect(ctx, geom, command);
|
|
break;
|
|
case 17:
|
|
this._drawOval(ctx, geom, command);
|
|
break;
|
|
case 18:
|
|
this._drawCircle(ctx, geom, command);
|
|
break;
|
|
case 19:
|
|
this._drawPath(ctx, geom, command);
|
|
break;
|
|
case 20:
|
|
this._drawShadow(ctx, geom, command);
|
|
break;
|
|
default:
|
|
throw new Error(`Unsupported command ID: ${command[0]}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
_applyPaint(ctx, paint) {
|
|
let blendMode = _stringForBlendMode(paint.blendMode);
|
|
ctx.globalCompositeOperation = blendMode ? blendMode : 'source-over';
|
|
ctx.lineWidth = paint.strokeWidth ? paint.strokeWidth : 1.0;
|
|
|
|
let strokeCap = _stringForStrokeCap(paint.strokeCap);
|
|
ctx.lineCap = strokeCap ? strokeCap : 'butt';
|
|
|
|
if (paint.shader != null) {
|
|
let paintStyle = paint.shader.createPaintStyle(ctx);
|
|
ctx.fillStyle = paintStyle;
|
|
ctx.strokeStyle = paintStyle;
|
|
} else if (paint.color != null) {
|
|
let colorString = paint.color;
|
|
ctx.fillStyle = colorString;
|
|
ctx.strokeStyle = colorString;
|
|
}
|
|
if (paint.maskFilter != null) {
|
|
ctx.filter = `blur(${paint.maskFilter[1]}px)`;
|
|
}
|
|
}
|
|
|
|
_strokeOrFill(ctx, paint, resetPaint) {
|
|
switch (paint.style) {
|
|
case PaintingStyle.stroke:
|
|
ctx.stroke();
|
|
break;
|
|
case PaintingStyle.fill:
|
|
default:
|
|
ctx.fill();
|
|
break;
|
|
}
|
|
if (resetPaint) {
|
|
this._resetPaint(ctx);
|
|
}
|
|
}
|
|
|
|
_resetPaint(ctx) {
|
|
ctx.globalCompositeOperation = 'source-over';
|
|
ctx.lineWidth = 1.0;
|
|
ctx.lineCap = 'butt';
|
|
ctx.filter = 'none';
|
|
ctx.fillStyle = null;
|
|
ctx.strokeStyle = null;
|
|
}
|
|
|
|
_save(ctx, geom, command) {
|
|
ctx.save();
|
|
}
|
|
|
|
_restore(ctx, geom, command) {
|
|
ctx.restore();
|
|
}
|
|
|
|
_translate(ctx, geom, command) {
|
|
ctx.translate(command[1], command[2]);
|
|
}
|
|
|
|
_scale(ctx, geom, command) {
|
|
ctx.translate(command[1], command[2]);
|
|
}
|
|
|
|
_rotate(ctx, geom, command) {
|
|
ctx.rotate(command[1]);
|
|
}
|
|
|
|
_skew(ctx, geom, command) {
|
|
ctx.translate(command[1], command[2]);
|
|
}
|
|
|
|
_drawRect(ctx, geom, command) {
|
|
let scanner = _scanCommand(command);
|
|
let rect = scanner.scanRect();
|
|
let paint = scanner.scanPaint();
|
|
this._applyPaint(ctx, paint);
|
|
ctx.beginPath();
|
|
ctx.rect(rect.left, rect.top, rect.width(), rect.height());
|
|
this._strokeOrFill(ctx, paint, true);
|
|
}
|
|
|
|
_drawRRect(ctx, geom, command) {
|
|
let scanner = _scanCommand(command);
|
|
let rrect = scanner.scanRRect();
|
|
let paint = scanner.scanPaint();
|
|
|
|
this._applyPaint(ctx, paint);
|
|
this._drawRRectPath(ctx, rrect, true);
|
|
this._strokeOrFill(ctx, paint, true);
|
|
}
|
|
|
|
_drawDRRect(ctx, geom, command) {
|
|
let scanner = _scanCommand(command);
|
|
let outer = scanner.scanRRect();
|
|
let inner = scanner.scanRRect();
|
|
let paint = scanner.scanPaint();
|
|
this._applyPaint(ctx, paint);
|
|
this._drawRRectPath(ctx, outer, true);
|
|
this._drawRRectPathReverse(ctx, inner, false);
|
|
this._strokeOrFill(ctx, paint, true);
|
|
}
|
|
|
|
_drawRRectPath(ctx, rrect, startNewPath) {
|
|
// TODO(mdebbar): there's a bug in this code, it doesn't correctly handle
|
|
// the case when the radius is greater than the width of the
|
|
// rect. When we fix that in BitmapCanvas, we need to fix it
|
|
// here too.
|
|
// To draw the rounded rectangle, perform the following 8 steps:
|
|
// 1. draw the line for the top
|
|
// 2. draw the arc for the top-right corner
|
|
// 3. draw the line for the right side
|
|
// 4. draw the arc for the bottom-right corner
|
|
// 5. draw the line for the bottom of the rectangle
|
|
// 6. draw the arc for the bottom-left corner
|
|
// 7. draw the line for the left side
|
|
// 8. draw the arc for the top-left corner
|
|
//
|
|
// After drawing, the current point will be the left side of the top of the
|
|
// rounded rectangle (after the corner).
|
|
// TODO(het): Confirm that this is the end point in Flutter for RRect
|
|
|
|
ctx.moveTo(rrect.left + rrect.trRadiusX, rrect.top);
|
|
|
|
if (startNewPath) {
|
|
ctx.beginPath();
|
|
}
|
|
|
|
// Top side and top-right corner
|
|
ctx.lineTo(rrect.right - rrect.trRadiusX, rrect.top);
|
|
ctx.ellipse(
|
|
rrect.right - rrect.trRadiusX,
|
|
rrect.top + rrect.trRadiusY,
|
|
rrect.trRadiusX,
|
|
rrect.trRadiusY,
|
|
0,
|
|
1.5 * Math.PI,
|
|
2.0 * Math.PI,
|
|
false,
|
|
);
|
|
|
|
// Right side and bottom-right corner
|
|
ctx.lineTo(rrect.right, rrect.bottom - rrect.brRadiusY);
|
|
ctx.ellipse(
|
|
rrect.right - rrect.brRadiusX,
|
|
rrect.bottom - rrect.brRadiusY,
|
|
rrect.brRadiusX,
|
|
rrect.brRadiusY,
|
|
0,
|
|
0,
|
|
0.5 * Math.PI,
|
|
false,
|
|
);
|
|
|
|
// Bottom side and bottom-left corner
|
|
ctx.lineTo(rrect.left + rrect.blRadiusX, rrect.bottom);
|
|
ctx.ellipse(
|
|
rrect.left + rrect.blRadiusX,
|
|
rrect.bottom - rrect.blRadiusY,
|
|
rrect.blRadiusX,
|
|
rrect.blRadiusY,
|
|
0,
|
|
0.5 * Math.PI,
|
|
Math.PI,
|
|
false,
|
|
);
|
|
|
|
// Left side and top-left corner
|
|
ctx.lineTo(rrect.left, rrect.top + rrect.tlRadiusY);
|
|
ctx.ellipse(
|
|
rrect.left + rrect.tlRadiusX,
|
|
rrect.top + rrect.tlRadiusY,
|
|
rrect.tlRadiusX,
|
|
rrect.tlRadiusY,
|
|
0,
|
|
Math.PI,
|
|
1.5 * Math.PI,
|
|
false,
|
|
);
|
|
}
|
|
|
|
_drawRRectPathReverse(ctx, rrect, startNewPath) {
|
|
// Draw the rounded rectangle, counterclockwise.
|
|
ctx.moveTo(rrect.right - rrect.trRadiusX, rrect.top);
|
|
|
|
if (startNewPath) {
|
|
ctx.beginPath();
|
|
}
|
|
|
|
// Top side and top-left corner
|
|
ctx.lineTo(rrect.left + rrect.tlRadiusX, rrect.top);
|
|
ctx.ellipse(
|
|
rrect.left + rrect.tlRadiusX,
|
|
rrect.top + rrect.tlRadiusY,
|
|
rrect.tlRadiusX,
|
|
rrect.tlRadiusY,
|
|
0,
|
|
1.5 * Math.PI,
|
|
Math.PI,
|
|
true,
|
|
);
|
|
|
|
// Left side and bottom-left corner
|
|
ctx.lineTo(rrect.left, rrect.bottom - rrect.blRadiusY);
|
|
ctx.ellipse(
|
|
rrect.left + rrect.blRadiusX,
|
|
rrect.bottom - rrect.blRadiusY,
|
|
rrect.blRadiusX,
|
|
rrect.blRadiusY,
|
|
0,
|
|
Math.PI,
|
|
0.5 * Math.PI,
|
|
true,
|
|
);
|
|
|
|
// Bottom side and bottom-right corner
|
|
ctx.lineTo(rrect.right - rrect.brRadiusX, rrect.bottom);
|
|
ctx.ellipse(
|
|
rrect.right - rrect.brRadiusX,
|
|
rrect.bottom - rrect.brRadiusY,
|
|
rrect.brRadiusX,
|
|
rrect.brRadiusY,
|
|
0,
|
|
0.5 * Math.PI,
|
|
0,
|
|
true,
|
|
);
|
|
|
|
// Right side and top-right corner
|
|
ctx.lineTo(rrect.right, rrect.top + rrect.trRadiusY);
|
|
ctx.ellipse(
|
|
rrect.right - rrect.trRadiusX,
|
|
rrect.top + rrect.trRadiusY,
|
|
rrect.trRadiusX,
|
|
rrect.trRadiusY,
|
|
0,
|
|
0,
|
|
1.5 * Math.PI,
|
|
true,
|
|
);
|
|
}
|
|
|
|
_clipRect(ctx, geom, command) {
|
|
let scanner = _scanCommand(command);
|
|
let rect = scanner.scanRect();
|
|
ctx.beginPath();
|
|
ctx.rect(rect.left, rect.top, rect.width(), rect.height());
|
|
ctx.clip();
|
|
}
|
|
|
|
_clipRRect(ctx, geom, command) {
|
|
let path = new Path([]);
|
|
let commands = [new RRectCommand(command[1])];
|
|
path.subpaths.push(new Subpath(commands));
|
|
this._runPath(ctx, path);
|
|
ctx.clip();
|
|
}
|
|
|
|
_clipPath(ctx, geom, command) {
|
|
let scanner = _scanCommand(command);
|
|
let path = scanner.scanPath();
|
|
this._runPath(ctx, path);
|
|
ctx.clip();
|
|
}
|
|
|
|
_drawCircle(ctx, geom, command) {
|
|
let scanner = _scanCommand(command);
|
|
let dx = scanner.scanNumber();
|
|
let dy = scanner.scanNumber();
|
|
let radius = scanner.scanNumber();
|
|
let paint = scanner.scanPaint();
|
|
|
|
this._applyPaint(ctx, paint);
|
|
ctx.beginPath();
|
|
ctx.ellipse(dx, dy, radius, radius, 0, 0, 2.0 * Math.PI, false);
|
|
this._strokeOrFill(ctx, paint, true);
|
|
}
|
|
|
|
_drawOval(ctx, geom, command) {
|
|
let scanner = _scanCommand(command);
|
|
let rect = scanner.scanRect();
|
|
let paint = scanner.scanPaint();
|
|
|
|
this._applyPaint(ctx, paint);
|
|
ctx.beginPath();
|
|
ctx.ellipse(
|
|
(rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2,
|
|
rect.width / 2, rect.height / 2, 0, 0, 2.0 * Math.PI, false);
|
|
this._strokeOrFill(ctx, paint, true);
|
|
}
|
|
|
|
_drawPath(ctx, geom, command) {
|
|
let scanner = _scanCommand(command);
|
|
let path = scanner.scanPath();
|
|
let paint = scanner.scanPaint();
|
|
this._applyPaint(ctx, paint);
|
|
this._runPath(ctx, path);
|
|
this._strokeOrFill(ctx, paint, true);
|
|
}
|
|
|
|
_drawShadow(ctx, geom, command) {
|
|
// TODO: this is mostly a stub; implement properly.
|
|
let scanner = _scanCommand(command);
|
|
let path = scanner.scanPath();
|
|
let color = scanner.scanArray();
|
|
let elevation = scanner.scanNumber();
|
|
let transparentOccluder = scanner.scanBool();
|
|
|
|
let shadows = _computeShadowsForElevation(elevation, color);
|
|
for (let i = 0; i < shadows.length; i++) {
|
|
let shadow = shadows[i];
|
|
|
|
let paint = new Paint(
|
|
null, // blendMode
|
|
PaintingStyle.fill, // style
|
|
1.0, // strokeWidth
|
|
null, // strokeCap
|
|
true, // isAntialias
|
|
shadow.color, // color
|
|
null, // shader
|
|
[BlurStyle.normal, shadow.blur], // maskFilter
|
|
null, // filterQuality
|
|
null // colorFilter
|
|
);
|
|
|
|
ctx.save();
|
|
ctx.translate(shadow.offsetX, shadow.offsetY);
|
|
this._applyPaint(ctx, paint);
|
|
this._runPath(ctx, path, true);
|
|
this._strokeOrFill(ctx, paint, false);
|
|
ctx.restore();
|
|
}
|
|
this._resetPaint(ctx);
|
|
}
|
|
|
|
_runPath(ctx, path) {
|
|
ctx.beginPath();
|
|
for (let i = 0; i < path.subpaths.length; i++) {
|
|
let subpath = path.subpaths[i];
|
|
for (let j = 0; j < subpath.commands.length; j++) {
|
|
let command = subpath.commands[j];
|
|
switch (command.type()) {
|
|
case PathCommandType.bezierCurveTo:
|
|
ctx.bezierCurveTo(
|
|
command.x1, command.y1, command.x2, command.y2, command.x3,
|
|
command.y3);
|
|
break;
|
|
case PathCommandType.close:
|
|
ctx.closePath();
|
|
break;
|
|
case PathCommandType.ellipse:
|
|
ctx.ellipse(
|
|
command.x, command.y, command.radiusX, command.radiusY,
|
|
command.rotation, command.startAngle, command.endAngle,
|
|
command.anticlockwise);
|
|
break;
|
|
case PathCommandType.lineTo:
|
|
ctx.lineTo(command.x, command.y);
|
|
break;
|
|
case PathCommandType.moveTo:
|
|
ctx.moveTo(command.x, command.y);
|
|
break;
|
|
case PathCommandType.rrect:
|
|
this._drawRRectPath(ctx, command.rrect, false);
|
|
break;
|
|
case PathCommandType.rect:
|
|
ctx.rect(command.x, command.y, command.width, command.height);
|
|
break;
|
|
case PathCommandType.quadraticCurveTo:
|
|
ctx.quadraticCurveTo(
|
|
command.x1, command.y1, command.x2, command.y2);
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown path command ${command.type()}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_drawColor(ctx, geom, command) {
|
|
ctx.globalCompositeOperation = _stringForBlendMode(command[2]);
|
|
ctx.fillStyle = command[1];
|
|
|
|
// Fill a virtually infinite rect with the color.
|
|
//
|
|
// We can't use (0, 0, width, height) because the current transform can
|
|
// cause it to not fill the entire clip.
|
|
ctx.fillRect(-10000, -10000, 20000, 20000);
|
|
this._resetPaint(ctx);
|
|
}
|
|
|
|
_drawLine(ctx, geom, command) {
|
|
let scanner = _scanCommand(command);
|
|
let p1dx = scanner.scanNumber();
|
|
let p1dy = scanner.scanNumber();
|
|
let p2dx = scanner.scanNumber();
|
|
let p2dy = scanner.scanNumber();
|
|
let paint = scanner.scanPaint();
|
|
this._applyPaint(ctx, paint);
|
|
ctx.beginPath();
|
|
ctx.moveTo(p1dx, p1dy);
|
|
ctx.lineTo(p2dx, p2dy);
|
|
ctx.stroke();
|
|
this._resetPaint(ctx);
|
|
}
|
|
|
|
_drawPaint(ctx, geom, command) {
|
|
let scanner = _scanCommand(command);
|
|
let paint = scanner.scanPaint();
|
|
this._applyPaint(ctx, paint);
|
|
ctx.beginPath();
|
|
|
|
// Fill a virtually infinite rect with the color.
|
|
//
|
|
// We can't use (0, 0, width, height) because the current transform can
|
|
// cause it to not fill the entire clip.
|
|
ctx.fillRect(-10000, -10000, 20000, 20000);
|
|
this._resetPaint(ctx);
|
|
}
|
|
}
|
|
|
|
function _scanCommand(command) {
|
|
return new CommandScanner(command);
|
|
}
|
|
|
|
const PaintingStyle = {
|
|
fill: 0,
|
|
stroke: 1,
|
|
};
|
|
|
|
/// A singleton used to parse serialized commands.
|
|
class CommandScanner {
|
|
constructor(command) {
|
|
// Skip the first element, which is always the command ID.
|
|
this.index = 1;
|
|
this.command = command;
|
|
}
|
|
|
|
scanRect() {
|
|
let rect = this.command[this.index++];
|
|
return new Rect(rect[0], rect[1], rect[2], rect[3]);
|
|
}
|
|
|
|
scanRRect() {
|
|
let rrect = this.command[this.index++];
|
|
return new RRect(
|
|
rrect[0], rrect[1], rrect[2], rrect[3], rrect[4], rrect[5], rrect[6],
|
|
rrect[7], rrect[8], rrect[9], rrect[10], rrect[11]);
|
|
}
|
|
|
|
scanPaint() {
|
|
let paint = this.command[this.index++];
|
|
return new Paint(
|
|
paint[0], paint[1], paint[2], paint[3], paint[4], paint[5], paint[6],
|
|
paint[7], paint[8], paint[9]);
|
|
}
|
|
|
|
scanNumber() {
|
|
return this.command[this.index++];
|
|
}
|
|
|
|
scanString() {
|
|
return this.command[this.index++];
|
|
}
|
|
|
|
scanBool() {
|
|
return this.command[this.index++];
|
|
}
|
|
|
|
scanPath() {
|
|
let subpaths = this.command[this.index++];
|
|
return new Path(subpaths);
|
|
}
|
|
|
|
scanArray() {
|
|
return this.command[this.index++];
|
|
}
|
|
}
|
|
|
|
class Rect {
|
|
constructor(left, top, right, bottom) {
|
|
this.left = left;
|
|
this.top = top;
|
|
this.right = right;
|
|
this.bottom = bottom;
|
|
}
|
|
|
|
width() {
|
|
return this.right - this.left;
|
|
}
|
|
|
|
height() {
|
|
return this.bottom - this.top;
|
|
}
|
|
}
|
|
|
|
class RRect {
|
|
constructor(
|
|
left, top, right, bottom, tlRadiusX, tlRadiusY, trRadiusX, trRadiusY,
|
|
brRadiusX, brRadiusY, blRadiusX, blRadiusY) {
|
|
this.left = left;
|
|
this.top = top;
|
|
this.right = right;
|
|
this.bottom = bottom;
|
|
this.tlRadiusX = tlRadiusX;
|
|
this.tlRadiusY = tlRadiusY;
|
|
this.trRadiusX = trRadiusX;
|
|
this.trRadiusY = trRadiusY;
|
|
this.brRadiusX = brRadiusX;
|
|
this.brRadiusY = brRadiusY;
|
|
this.blRadiusX = blRadiusX;
|
|
this.blRadiusY = blRadiusY;
|
|
}
|
|
|
|
tallMiddleRect() {
|
|
let leftRadius = Math.max(this.blRadiusX, this.tlRadiusX);
|
|
let rightRadius = Math.max(this.trRadiusX, this.brRadiusX);
|
|
return new Rect(
|
|
this.left + leftRadius, this.top, this.right - rightRadius,
|
|
this.bottom);
|
|
}
|
|
|
|
middleRect() {
|
|
let leftRadius = Math.max(this.blRadiusX, this.tlRadiusX);
|
|
let topRadius = Math.max(this.tlRadiusY, this.trRadiusY);
|
|
let rightRadius = Math.max(this.trRadiusX, this.brRadiusX);
|
|
let bottomRadius = Math.max(this.brRadiusY, this.blRadiusY);
|
|
return new Rect(
|
|
this.left + leftRadius, this.top + topRadius, this.right - rightRadius,
|
|
this.bottom - bottomRadius);
|
|
}
|
|
|
|
wideMiddleRect() {
|
|
let topRadius = Math.max(this.tlRadiusY, this.trRadiusY);
|
|
let bottomRadius = Math.max(this.brRadiusY, this.blRadiusY);
|
|
return new Rect(
|
|
this.left, this.top + topRadius, this.right,
|
|
this.bottom - bottomRadius);
|
|
}
|
|
}
|
|
|
|
class Paint {
|
|
constructor(
|
|
blendMode, style, strokeWidth, strokeCap, isAntialias, color, shader,
|
|
maskFilter, filterQuality, colorFilter) {
|
|
this.blendMode = blendMode;
|
|
this.style = style;
|
|
this.strokeWidth = strokeWidth;
|
|
this.strokeCap = strokeCap;
|
|
this.isAntialias = isAntialias;
|
|
this.color = color;
|
|
this.shader = _deserializeShader(shader); // TODO: deserialize
|
|
this.maskFilter = maskFilter;
|
|
this.filterQuality = filterQuality;
|
|
this.colorFilter = colorFilter; // TODO: deserialize
|
|
}
|
|
}
|
|
|
|
function _deserializeShader(data) {
|
|
if (!data) {
|
|
return null;
|
|
}
|
|
|
|
switch (data[0]) {
|
|
case 1:
|
|
return new GradientLinear(data);
|
|
default:
|
|
throw new Error(`Shader type not supported: ${data}`);
|
|
}
|
|
}
|
|
|
|
class GradientLinear {
|
|
constructor(data) {
|
|
this.fromX = data[1];
|
|
this.fromY = data[2];
|
|
this.toX = data[3];
|
|
this.toY = data[4];
|
|
this.colors = data[5];
|
|
this.colorStops = data[6];
|
|
this.tileMode = data[7];
|
|
}
|
|
|
|
createPaintStyle(ctx) {
|
|
let gradient =
|
|
ctx.createLinearGradient(this.fromX, this.fromY, this.toX, this.toY);
|
|
if (this.colorStops == null) {
|
|
gradient.addColorStop(0, this.colors[0]);
|
|
gradient.addColorStop(1, this.colors[1]);
|
|
return gradient;
|
|
}
|
|
for (let i = 0; i < this.colors.length; i++) {
|
|
gradient.addColorStop(this.colorStops[i], this.colors[i]);
|
|
}
|
|
return gradient;
|
|
}
|
|
}
|
|
|
|
const BlendMode = {
|
|
clear: 0,
|
|
src: 1,
|
|
dst: 2,
|
|
srcOver: 3,
|
|
dstOver: 4,
|
|
srcIn: 5,
|
|
dstIn: 6,
|
|
srcOut: 7,
|
|
dstOut: 8,
|
|
srcATop: 9,
|
|
dstATop: 10,
|
|
xor: 11,
|
|
plus: 12,
|
|
modulate: 13,
|
|
screen: 14,
|
|
overlay: 15,
|
|
darken: 16,
|
|
lighten: 17,
|
|
colorDodge: 18,
|
|
colorBurn: 19,
|
|
hardLight: 20,
|
|
softLight: 21,
|
|
difference: 22,
|
|
exclusion: 23,
|
|
multiply: 24,
|
|
hue: 25,
|
|
saturation: 26,
|
|
color: 27,
|
|
luminosity: 28,
|
|
};
|
|
|
|
function _stringForBlendMode(blendMode) {
|
|
if (blendMode == null) return null;
|
|
switch (blendMode) {
|
|
case BlendMode.srcOver:
|
|
return 'source-over';
|
|
case BlendMode.srcIn:
|
|
return 'source-in';
|
|
case BlendMode.srcOut:
|
|
return 'source-out';
|
|
case BlendMode.srcATop:
|
|
return 'source-atop';
|
|
case BlendMode.dstOver:
|
|
return 'destination-over';
|
|
case BlendMode.dstIn:
|
|
return 'destination-in';
|
|
case BlendMode.dstOut:
|
|
return 'destination-out';
|
|
case BlendMode.dstATop:
|
|
return 'destination-atop';
|
|
case BlendMode.plus:
|
|
return 'lighten';
|
|
case BlendMode.src:
|
|
return 'copy';
|
|
case BlendMode.xor:
|
|
return 'xor';
|
|
case BlendMode.multiply:
|
|
// Falling back to multiply, ignoring alpha channel.
|
|
// TODO(flutter_web): only used for debug, find better fallback for web.
|
|
case BlendMode.modulate:
|
|
return 'multiply';
|
|
case BlendMode.screen:
|
|
return 'screen';
|
|
case BlendMode.overlay:
|
|
return 'overlay';
|
|
case BlendMode.darken:
|
|
return 'darken';
|
|
case BlendMode.lighten:
|
|
return 'lighten';
|
|
case BlendMode.colorDodge:
|
|
return 'color-dodge';
|
|
case BlendMode.colorBurn:
|
|
return 'color-burn';
|
|
case BlendMode.hardLight:
|
|
return 'hard-light';
|
|
case BlendMode.softLight:
|
|
return 'soft-light';
|
|
case BlendMode.difference:
|
|
return 'difference';
|
|
case BlendMode.exclusion:
|
|
return 'exclusion';
|
|
case BlendMode.hue:
|
|
return 'hue';
|
|
case BlendMode.saturation:
|
|
return 'saturation';
|
|
case BlendMode.color:
|
|
return 'color';
|
|
case BlendMode.luminosity:
|
|
return 'luminosity';
|
|
default:
|
|
throw new Error(
|
|
'Flutter web does not support the blend mode: $blendMode');
|
|
}
|
|
}
|
|
|
|
const StrokeCap = {
|
|
butt: 0,
|
|
round: 1,
|
|
square: 2,
|
|
};
|
|
|
|
function _stringForStrokeCap(strokeCap) {
|
|
if (strokeCap == null) return null;
|
|
switch (strokeCap) {
|
|
case StrokeCap.butt:
|
|
return 'butt';
|
|
case StrokeCap.round:
|
|
return 'round';
|
|
case StrokeCap.square:
|
|
default:
|
|
return 'square';
|
|
}
|
|
}
|
|
|
|
class Path {
|
|
constructor(serializedSubpaths) {
|
|
this.subpaths = [];
|
|
for (let i = 0; i < serializedSubpaths.length; i++) {
|
|
let subpath = serializedSubpaths[i];
|
|
let pathCommands = [];
|
|
for (let j = 0; j < subpath.length; j++) {
|
|
let pathCommand = subpath[j];
|
|
switch (pathCommand[0]) {
|
|
case 1:
|
|
pathCommands.push(new MoveTo(pathCommand));
|
|
break;
|
|
case 2:
|
|
pathCommands.push(new LineTo(pathCommand));
|
|
break;
|
|
case 3:
|
|
pathCommands.push(new Ellipse(pathCommand));
|
|
break;
|
|
case 4:
|
|
pathCommands.push(new QuadraticCurveTo(pathCommand));
|
|
break;
|
|
case 5:
|
|
pathCommands.push(new BezierCurveTo(pathCommand));
|
|
break;
|
|
case 6:
|
|
pathCommands.push(new RectCommand(pathCommand));
|
|
break;
|
|
case 7:
|
|
pathCommands.push(new RRectCommand(pathCommand));
|
|
break;
|
|
case 8:
|
|
pathCommands.push(new CloseCommand());
|
|
break;
|
|
default:
|
|
throw new Error(`Unsupported path command: ${pathCommand}`);
|
|
}
|
|
}
|
|
|
|
this.subpaths.push(new Subpath(pathCommands));
|
|
}
|
|
}
|
|
}
|
|
|
|
class Subpath {
|
|
constructor(commands) {
|
|
this.commands = commands;
|
|
}
|
|
}
|
|
|
|
class MoveTo {
|
|
constructor(data) {
|
|
this.x = data[1];
|
|
this.y = data[2];
|
|
}
|
|
|
|
type() {
|
|
return PathCommandType.moveTo;
|
|
}
|
|
}
|
|
|
|
class LineTo {
|
|
constructor(data) {
|
|
this.x = data[1];
|
|
this.y = data[2];
|
|
}
|
|
|
|
type() {
|
|
return PathCommandType.lineTo;
|
|
}
|
|
}
|
|
|
|
class Ellipse {
|
|
constructor(data) {
|
|
this.x = data[1];
|
|
this.y = data[2];
|
|
this.radiusX = data[3];
|
|
this.radiusY = data[4];
|
|
this.rotation = data[5];
|
|
this.startAngle = data[6];
|
|
this.endAngle = data[7];
|
|
this.anticlockwise = data[8];
|
|
}
|
|
|
|
type() {
|
|
return PathCommandType.ellipse;
|
|
}
|
|
}
|
|
|
|
class QuadraticCurveTo {
|
|
constructor(data) {
|
|
this.x1 = data[1];
|
|
this.y1 = data[2];
|
|
this.x2 = data[3];
|
|
this.y2 = data[4];
|
|
}
|
|
|
|
type() {
|
|
return PathCommandType.quadraticCurveTo;
|
|
}
|
|
}
|
|
|
|
class BezierCurveTo {
|
|
constructor(data) {
|
|
this.x1 = data[1];
|
|
this.y1 = data[2];
|
|
this.x2 = data[3];
|
|
this.y2 = data[4];
|
|
this.x3 = data[5];
|
|
this.y3 = data[6];
|
|
}
|
|
|
|
type() {
|
|
return PathCommandType.bezierCurveTo;
|
|
}
|
|
}
|
|
|
|
class RectCommand {
|
|
constructor(data) {
|
|
this.x = data[1];
|
|
this.y = data[2];
|
|
this.width = data[3];
|
|
this.height = data[4];
|
|
}
|
|
|
|
type() {
|
|
return PathCommandType.rect;
|
|
}
|
|
}
|
|
|
|
class RRectCommand {
|
|
constructor(data) {
|
|
let scanner = _scanCommand(data);
|
|
this.rrect = scanner.scanRRect();
|
|
}
|
|
|
|
type() {
|
|
return PathCommandType.rrect;
|
|
}
|
|
}
|
|
|
|
class CloseCommand {
|
|
type() {
|
|
return PathCommandType.close;
|
|
}
|
|
}
|
|
|
|
class CanvasShadow {
|
|
constructor(offsetX, offsetY, blur, spread, color) {
|
|
this.offsetX = offsetX;
|
|
this.offsetY = offsetY;
|
|
this.blur = blur;
|
|
this.spread = spread;
|
|
this.color = color;
|
|
}
|
|
}
|
|
|
|
const _noShadows = [];
|
|
|
|
function _computeShadowsForElevation(elevation, color) {
|
|
if (elevation <= 0.0) {
|
|
return _noShadows;
|
|
} else if (elevation <= 1.0) {
|
|
return _computeShadowElevation(2, color);
|
|
} else if (elevation <= 2.0) {
|
|
return _computeShadowElevation(4, color);
|
|
} else if (elevation <= 3.0) {
|
|
return _computeShadowElevation(6, color);
|
|
} else if (elevation <= 4.0) {
|
|
return _computeShadowElevation(8, color);
|
|
} else if (elevation <= 5.0) {
|
|
return _computeShadowElevation(16, color);
|
|
} else {
|
|
return _computeShadowElevation(24, color);
|
|
}
|
|
}
|
|
|
|
function _computeShadowElevation(dp, color) {
|
|
// TODO(yjbanov): multiple shadows are very expensive. Find a more efficient
|
|
// method to render them.
|
|
let red = color[1];
|
|
let green = color[2];
|
|
let blue = color[3];
|
|
|
|
// let penumbraColor = `rgba(${red}, ${green}, ${blue}, 0.14)`;
|
|
// let ambientShadowColor = `rgba(${red}, ${green}, ${blue}, 0.12)`;
|
|
let umbraColor = `rgba(${red}, ${green}, ${blue}, 0.2)`;
|
|
|
|
let result = [];
|
|
if (dp === 2) {
|
|
// result.push(new CanvasShadow(0.0, 2.0, 1.0, 0.0, penumbraColor));
|
|
// result.push(new CanvasShadow(0.0, 3.0, 0.5, -2.0, ambientShadowColor));
|
|
result.push(new CanvasShadow(0.0, 1.0, 2.5, 0.0, umbraColor));
|
|
} else if (dp === 3) {
|
|
// result.push(new CanvasShadow(0.0, 1.5, 4.0, 0.0, penumbraColor));
|
|
// result.push(new CanvasShadow(0.0, 3.0, 1.5, -2.0, ambientShadowColor));
|
|
result.push(new CanvasShadow(0.0, 1.0, 4.0, 0.0, umbraColor));
|
|
} else if (dp === 4) {
|
|
// result.push(new CanvasShadow(0.0, 4.0, 2.5, 0.0, penumbraColor));
|
|
// result.push(new CanvasShadow(0.0, 1.0, 5.0, 0.0, ambientShadowColor));
|
|
result.push(new CanvasShadow(0.0, 2.0, 2.0, -1.0, umbraColor));
|
|
} else if (dp === 6) {
|
|
// result.push(new CanvasShadow(0.0, 6.0, 5.0, 0.0, penumbraColor));
|
|
// result.push(new CanvasShadow(0.0, 1.0, 9.0, 0.0, ambientShadowColor));
|
|
result.push(new CanvasShadow(0.0, 3.0, 2.5, -1.0, umbraColor));
|
|
} else if (dp === 8) {
|
|
// result.push(new CanvasShadow(0.0, 4.0, 10.0, 1.0, penumbraColor));
|
|
// result.push(new CanvasShadow(0.0, 3.0, 7.0, 2.0, ambientShadowColor));
|
|
result.push(new CanvasShadow(0.0, 5.0, 2.5, -3.0, umbraColor));
|
|
} else if (dp === 12) {
|
|
// result.push(new CanvasShadow(0.0, 12.0, 8.5, 2.0, penumbraColor));
|
|
// result.push(new CanvasShadow(0.0, 5.0, 11.0, 4.0, ambientShadowColor));
|
|
result.push(new CanvasShadow(0.0, 7.0, 4.0, -4.0, umbraColor));
|
|
} else if (dp === 16) {
|
|
// result.push(new CanvasShadow(0.0, 16.0, 12.0, 2.0, penumbraColor));
|
|
// result.push(new CanvasShadow(0.0, 6.0, 15.0, 5.0, ambientShadowColor));
|
|
result.push(new CanvasShadow(0.0, 0.0, 5.0, -5.0, umbraColor));
|
|
} else {
|
|
// result.push(new CanvasShadow(0.0, 24.0, 18.0, 3.0, penumbraColor));
|
|
// result.push(new CanvasShadow(0.0, 9.0, 23.0, 8.0, ambientShadowColor));
|
|
result.push(new CanvasShadow(0.0, 11.0, 7.5, -7.0, umbraColor));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
const PathCommandType = {
|
|
moveTo: 0,
|
|
lineTo: 1,
|
|
ellipse: 2,
|
|
close: 3,
|
|
quadraticCurveTo: 4,
|
|
bezierCurveTo: 5,
|
|
rect: 6,
|
|
rrect: 7,
|
|
};
|
|
|
|
const TileMode = {
|
|
clamp: 0,
|
|
repeated: 1,
|
|
};
|
|
|
|
const BlurStyle = {
|
|
normal: 0,
|
|
solid: 1,
|
|
outer: 2,
|
|
inner: 3,
|
|
};
|
|
|
|
/// This makes the painter available as "background-image: paint(flt)".
|
|
registerPaint('flt', FlutterPainter);
|