// 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);