| // Copyright 2013 The Flutter 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 |
| |
| if (startNewPath) { |
| ctx.beginPath(); |
| } |
| |
| ctx.moveTo(rrect.left + rrect.trRadiusX, rrect.top); |
| |
| // 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); |