diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index e86ffb97942ff..a475d4c617285 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -102,6 +102,8 @@ export 'engine/html/shaders/shader_builder.dart'; export 'engine/html/shaders/vertex_shaders.dart'; export 'engine/html/surface.dart'; export 'engine/html/surface_stats.dart'; +export 'engine/html/svg/svg_canvas.dart'; +export 'engine/html/svg/svg_picture.dart'; export 'engine/html/transform.dart'; export 'engine/html_image_codec.dart'; export 'engine/initialization.dart'; @@ -146,6 +148,7 @@ export 'engine/services/message_codecs.dart'; export 'engine/services/serialization.dart'; export 'engine/shadow.dart'; export 'engine/svg.dart'; +export 'engine/svg_filter.dart'; export 'engine/test_embedding.dart'; export 'engine/text/canvas_paragraph.dart'; export 'engine/text/font_collection.dart'; diff --git a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart index 27694946d5a23..86d217ac153e5 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart @@ -10,6 +10,7 @@ import '../dom.dart'; import '../html/path_to_svg_clip.dart'; import '../platform_views/slots.dart'; import '../svg.dart'; +import '../svg_filter.dart'; import '../util.dart'; import '../vector_math.dart'; import '../window.dart'; diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart index e982f04707538..0cc531b4cf409 100644 --- a/lib/web_ui/lib/src/engine/dom.dart +++ b/lib/web_ui/lib/src/engine/dom.dart @@ -321,6 +321,16 @@ extension DomCSSStyleDeclarationExtension on DomCSSStyleDeclaration { set transformOrigin(String value) => setProperty('transform-origin', value); set opacity(String value) => setProperty('opacity', value); set color(String value) => setProperty('color', value); + + // SVG only + set fill(String value) => setProperty('fill', value); + // SVG only + set stroke(String value) => setProperty('stroke', value); + + // Skip this: not supported in Safari, use `stroke-width` element attribute instead + // set strokeWidth(String value) => setProperty('strokeWidth', value); + + set vectorEffect(String value) => setProperty('vectorEffect', value); set top(String value) => setProperty('top', value); set left(String value) => setProperty('left', value); set right(String value) => setProperty('right', value); @@ -381,6 +391,7 @@ extension DomCSSStyleDeclarationExtension on DomCSSStyleDeclaration { set alignContent(String value) => setProperty('align-content', value); set textAlign(String value) => setProperty('text-align', value); set font(String value) => setProperty('font', value); + set dominantBaseline(String value) => setProperty('dominant-baseline', value); String get width => getPropertyValue('width'); String get height => getPropertyValue('height'); String get position => getPropertyValue('position'); @@ -390,6 +401,16 @@ extension DomCSSStyleDeclarationExtension on DomCSSStyleDeclaration { String get transformOrigin => getPropertyValue('transform-origin'); String get opacity => getPropertyValue('opacity'); String get color => getPropertyValue('color'); + + // SVG only + String get fill => getPropertyValue('fill'); + // SVG only + String get stroke => getPropertyValue('stroke'); + + // Skip this: not supported in Safari + // String get strokeWidth => getPropertyValue('strokeWidth'); + + String get vectorEffect => getPropertyValue('vectorEffect'); String get top => getPropertyValue('top'); String get left => getPropertyValue('left'); String get right => getPropertyValue('right'); @@ -445,6 +466,7 @@ extension DomCSSStyleDeclarationExtension on DomCSSStyleDeclaration { String get alignContent => getPropertyValue('align-content'); String get textAlign => getPropertyValue('text-align'); String get font => getPropertyValue('font'); + String get dominantBaseline => getPropertyValue('dominant-baseline'); external String getPropertyValue(String property); void setProperty(String propertyName, String value, [String? priority]) { @@ -1740,3 +1762,215 @@ DomV8BreakIterator createV8BreakIterator() { ], ); } + +@JS() +@staticInterop +class DOMPoint {} +extension DOMPointExtension on DOMPoint { + external double get x; + external double get y; + external double get z; + external double get w; +} + +@JS() +@anonymous +@staticInterop +class DOMPointInit { + external factory DOMPointInit({ + double x = 0, + double y = 0, + double z = 0, + double w = 1, + }); +} + +@JS() +@staticInterop +class DOMMatrixReadOnly {} +extension DOMMatrixReadOnlyExtension on DOMMatrixReadOnly { + external static DOMMatrixReadOnly fromMatrix(DOMMatrixInit other); + external static DOMMatrixReadOnly fromFloat32Array(Float32List array32); + external static DOMMatrixReadOnly fromFloat64Array(Float64List array64); + + external double get m11; + external double get m12; + external double get m13; + external double get m14; + external double get m21; + external double get m22; + external double get m23; + external double get m24; + external double get m31; + external double get m32; + external double get m33; + external double get m34; + external double get m41; + external double get m42; + external double get m43; + external double get m44; + + external bool get is2D; + external bool get isIdentity; + + // Immutable transform methods + external DOMMatrix translate([double tx = 0, double ty = 0, double tz = 0]); + + external DOMMatrix scale( + double scaleX, + double scaleY, [ + double scaleZ = 1, + double originX = 0, + double originY = 0, + double originZ = 0, + ]); + + external DOMMatrix scaleNonUniform( + double scaleX, + double scaleY, + ); + + external DOMMatrix scale3d( + double scale, [ + double originX = 0, + double originY = 0, + double originZ = 0, + ]); + + external DOMMatrix rotate([ + double rotX = 0, + double? rotY, + double? rotZ, + ]); + + external DOMMatrix rotateFromVector([ + double x = 0, + double y = 0, + ]); + + external DOMMatrix rotateAxisAngle([ + double x = 0, + double y = 0, + double z = 0, + double angle = 0, + ]); + + external DOMMatrix skewX([double sx = 0]); + external DOMMatrix skewY([double sy = 0]); + external DOMMatrix multiply([DOMMatrixInit other]); + external DOMMatrix flipX(); + external DOMMatrix flipY(); + external DOMMatrix inverse(); + + external DOMPoint transformPoint([DOMPointInit point]); + external Float32List toFloat32Array(); + external Float64List toFloat64Array(); +} + +@JS() +@staticInterop +class DOMMatrix extends DOMMatrixReadOnly {} +extension DOMMatrixExtension on DOMMatrix { + external static DOMMatrix fromMatrix(DOMMatrixInit other); + external static DOMMatrix fromFloat32Array(Float32List array32); + external static DOMMatrix fromFloat64Array(Float64List array64); + + external set m11(double value); + external set m12(double value); + external set m13(double value); + external set m14(double value); + external set m21(double value); + external set m22(double value); + external set m23(double value); + external set m24(double value); + external set m31(double value); + external set m32(double value); + external set m33(double value); + external set m34(double value); + external set m41(double value); + external set m42(double value); + external set m43(double value); + external set m44(double value); + + // Mutable transform methods + external DOMMatrix multiplySelf(DOMMatrixInit other); + external DOMMatrix preMultiplySelf(DOMMatrixInit other); + external DOMMatrix translateSelf([ + double tx = 0, + double ty = 0, + double tz = 0, + ]); + external DOMMatrix scaleSelf([ + double scaleX = 1, + double scaleY = 1, + double scaleZ = 1, + double originX = 0, + double originY = 0, + double originZ = 0, + ]); + external DOMMatrix scale3dSelf([ + double scale = 1, + double originX = 0, + double originY = 0, + double originZ = 0, + ]); + external DOMMatrix rotateSelf([ + double rotX = 0, + double? rotY, + double? rotZ, + ]); + external DOMMatrix rotateFromVectorSelf([ + double x = 0, + double y = 0, + ]); + external DOMMatrix rotateAxisAngleSelf([ + double x = 0, + double y = 0, + double z = 0, + double angle = 0, + ]); + external DOMMatrix skewXSelf(double sx); + external DOMMatrix skewYSelf(double sy); + external DOMMatrix invertSelf(); + + external DOMMatrix setMatrixValue(String transformList); +} + +@JS() +@anonymous +@staticInterop +class DOMMatrix2DInit { + external factory DOMMatrix2DInit({ + required double m11, + required double m12, + required double m21, + required double m22, + required double m41, + required double m42, + }); +} + +@JS() +@anonymous +@staticInterop +class DOMMatrixInit { + external factory DOMMatrixInit({ + required double m11, + required double m12, + required double m21, + required double m22, + required double m41, + required double m42, + double? m13 = 0, + double? m14 = 0, + double? m23 = 0, + double? m24 = 0, + double? m31 = 0, + double? m32 = 0, + double? m33 = 1, + double? m34 = 0, + double? m43 = 0, + double? m44 = 1, + required bool is2D, + }); +} diff --git a/lib/web_ui/lib/src/engine/embedder.dart b/lib/web_ui/lib/src/engine/embedder.dart index db28eac7672c9..58292392f36be 100644 --- a/lib/web_ui/lib/src/engine/embedder.dart +++ b/lib/web_ui/lib/src/engine/embedder.dart @@ -171,8 +171,9 @@ class FlutterViewEmbedder { _glassPaneShadow = glassPaneElementHostNode; // Don't allow the scene to receive pointer events. - _sceneHostElement = domDocument.createElement('flt-scene-host') - ..style.pointerEvents = 'none'; + _sceneHostElement = domDocument.createElement('flt-scene-host'); + // DO_NOT_SUBMIT: revert this before submitting. + // ..style.pointerEvents = 'none'; renderer.reset(this); diff --git a/lib/web_ui/lib/src/engine/engine_canvas.dart b/lib/web_ui/lib/src/engine/engine_canvas.dart index 297319a082371..4a5e41fbff3de 100644 --- a/lib/web_ui/lib/src/engine/engine_canvas.dart +++ b/lib/web_ui/lib/src/engine/engine_canvas.dart @@ -87,22 +87,6 @@ abstract class EngineCanvas { void endOfPaint(); } -/// Adds an [offset] transformation to a [transform] matrix and returns the -/// combined result. -/// -/// If the given offset is zero, returns [transform] matrix as is. Otherwise, -/// returns a new [Matrix4] object representing the combined transformation. -Matrix4 transformWithOffset(Matrix4 transform, ui.Offset offset) { - if (offset == ui.Offset.zero) { - return transform; - } - - // Clone to avoid mutating transform. - final Matrix4 effectiveTransform = transform.clone(); - effectiveTransform.translate(offset.dx, offset.dy); - return effectiveTransform; -} - class SaveStackEntry { SaveStackEntry({ required this.transform, diff --git a/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart b/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart index 5468b0bb066ea..ed4c8b9a2ca17 100644 --- a/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart +++ b/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart @@ -14,6 +14,7 @@ import '../engine_canvas.dart'; import '../frame_reference.dart'; import '../html_image_codec.dart'; import '../platform_dispatcher.dart'; +import '../svg_filter.dart'; import '../text/canvas_paragraph.dart'; import '../util.dart'; import '../vector_math.dart'; @@ -1200,140 +1201,6 @@ String? blendModeToCssMixBlendMode(ui.BlendMode? blendMode) { } } -// Source: https://www.w3.org/TR/SVG11/filters.html#InterfaceSVGFEBlendElement -// These constant names deviate from Dart's camelCase convention on purpose to -// make it easier to search for them in W3 specs and in Chromium sources. -const int SVG_FEBLEND_MODE_UNKNOWN = 0; -const int SVG_FEBLEND_MODE_NORMAL = 1; -const int SVG_FEBLEND_MODE_MULTIPLY = 2; -const int SVG_FEBLEND_MODE_SCREEN = 3; -const int SVG_FEBLEND_MODE_DARKEN = 4; -const int SVG_FEBLEND_MODE_LIGHTEN = 5; -const int SVG_FEBLEND_MODE_OVERLAY = 6; -const int SVG_FEBLEND_MODE_COLOR_DODGE = 7; -const int SVG_FEBLEND_MODE_COLOR_BURN = 8; -const int SVG_FEBLEND_MODE_HARD_LIGHT = 9; -const int SVG_FEBLEND_MODE_SOFT_LIGHT = 10; -const int SVG_FEBLEND_MODE_DIFFERENCE = 11; -const int SVG_FEBLEND_MODE_EXCLUSION = 12; -const int SVG_FEBLEND_MODE_HUE = 13; -const int SVG_FEBLEND_MODE_SATURATION = 14; -const int SVG_FEBLEND_MODE_COLOR = 15; -const int SVG_FEBLEND_MODE_LUMINOSITY = 16; - -// Source: https://github.com/chromium/chromium/blob/e1e495b29e1178a451f65980a6c4ae017c34dc94/third_party/blink/renderer/platform/graphics/graphics_types.cc#L55 -const String kCompositeClear = 'clear'; -const String kCompositeCopy = 'copy'; -const String kCompositeSourceOver = 'source-over'; -const String kCompositeSourceIn = 'source-in'; -const String kCompositeSourceOut = 'source-out'; -const String kCompositeSourceAtop = 'source-atop'; -const String kCompositeDestinationOver = 'destination-over'; -const String kCompositeDestinationIn = 'destination-in'; -const String kCompositeDestinationOut = 'destination-out'; -const String kCompositeDestinationAtop = 'destination-atop'; -const String kCompositeXor = 'xor'; -const String kCompositeLighter = 'lighter'; - -/// Compositing and blending operation in SVG. -/// -/// Flutter's [BlendMode] flattens what SVG expresses as two orthogonal -/// properties, a composite operator and blend mode. Instances of this class -/// are returned from [blendModeToSvgEnum] by mapping Flutter's [BlendMode] -/// enum onto the SVG equivalent. -/// -/// See also: -/// -/// * https://www.w3.org/TR/compositing-1 -/// * https://github.com/chromium/chromium/blob/e1e495b29e1178a451f65980a6c4ae017c34dc94/third_party/blink/renderer/platform/graphics/graphics_types.cc#L55 -/// * https://github.com/chromium/chromium/blob/e1e495b29e1178a451f65980a6c4ae017c34dc94/third_party/blink/renderer/modules/canvas/canvas2d/base_rendering_context_2d.cc#L725 -class SvgBlendMode { - const SvgBlendMode(this.compositeOperator, this.blendMode); - - /// The name of the SVG composite operator. - /// - /// If this mode represents a blend mode, this is set to [kCompositeSourceOver]. - final String compositeOperator; - - /// The identifier of the SVG blend mode. - /// - /// This is mode represents a compositing operation, this is set to [SVG_FEBLEND_MODE_UNKNOWN]. - final int blendMode; -} - -/// Converts Flutter's [ui.BlendMode] to SVG's pair. -SvgBlendMode? blendModeToSvgEnum(ui.BlendMode? blendMode) { - if (blendMode == null) { - return null; - } - switch (blendMode) { - case ui.BlendMode.clear: - return const SvgBlendMode(kCompositeClear, SVG_FEBLEND_MODE_UNKNOWN); - case ui.BlendMode.srcOver: - return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_UNKNOWN); - case ui.BlendMode.srcIn: - return const SvgBlendMode(kCompositeSourceIn, SVG_FEBLEND_MODE_UNKNOWN); - case ui.BlendMode.srcOut: - return const SvgBlendMode(kCompositeSourceOut, SVG_FEBLEND_MODE_UNKNOWN); - case ui.BlendMode.srcATop: - return const SvgBlendMode(kCompositeSourceAtop, SVG_FEBLEND_MODE_UNKNOWN); - case ui.BlendMode.dstOver: - return const SvgBlendMode(kCompositeDestinationOver, SVG_FEBLEND_MODE_UNKNOWN); - case ui.BlendMode.dstIn: - return const SvgBlendMode(kCompositeDestinationIn, SVG_FEBLEND_MODE_UNKNOWN); - case ui.BlendMode.dstOut: - return const SvgBlendMode(kCompositeDestinationOut, SVG_FEBLEND_MODE_UNKNOWN); - case ui.BlendMode.dstATop: - return const SvgBlendMode(kCompositeDestinationAtop, SVG_FEBLEND_MODE_UNKNOWN); - case ui.BlendMode.plus: - return const SvgBlendMode(kCompositeLighter, SVG_FEBLEND_MODE_UNKNOWN); - case ui.BlendMode.src: - return const SvgBlendMode(kCompositeCopy, SVG_FEBLEND_MODE_UNKNOWN); - case ui.BlendMode.xor: - return const SvgBlendMode(kCompositeXor, SVG_FEBLEND_MODE_UNKNOWN); - case ui.BlendMode.multiply: - // Falling back to multiply, ignoring alpha channel. - // TODO(ferhat): only used for debug, find better fallback for web. - case ui.BlendMode.modulate: - return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_MULTIPLY); - case ui.BlendMode.screen: - return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_SCREEN); - case ui.BlendMode.overlay: - return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_OVERLAY); - case ui.BlendMode.darken: - return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_DARKEN); - case ui.BlendMode.lighten: - return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_LIGHTEN); - case ui.BlendMode.colorDodge: - return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_COLOR_DODGE); - case ui.BlendMode.colorBurn: - return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_COLOR_BURN); - case ui.BlendMode.hardLight: - return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_HARD_LIGHT); - case ui.BlendMode.softLight: - return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_SOFT_LIGHT); - case ui.BlendMode.difference: - return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_DIFFERENCE); - case ui.BlendMode.exclusion: - return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_EXCLUSION); - case ui.BlendMode.hue: - return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_HUE); - case ui.BlendMode.saturation: - return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_SATURATION); - case ui.BlendMode.color: - return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_COLOR); - case ui.BlendMode.luminosity: - return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_LUMINOSITY); - default: - assert( - false, - 'Flutter Web does not support the blend mode: $blendMode', - ); - - return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_NORMAL); - } -} - String? stringForStrokeCap(ui.StrokeCap? strokeCap) { if (strokeCap == null) { return null; diff --git a/lib/web_ui/lib/src/engine/html/clip.dart b/lib/web_ui/lib/src/engine/html/clip.dart index 892bc88403825..59c359379b022 100644 --- a/lib/web_ui/lib/src/engine/html/clip.dart +++ b/lib/web_ui/lib/src/engine/html/clip.dart @@ -340,13 +340,13 @@ class PersistedPhysicalShape extends PersistedContainerSurface /// we size the inner container to cover full pathBounds instead of sizing /// to clipping rect bounds (which is the case for elevation == 0.0 where /// we shift outer/inner clip area instead to position clip-path). - final SVGSVGElement svgClipPath = elevation == 0.0 - ? pathToSvgClipPath(path, + final SvgClip svgClip = elevation == 0.0 + ? SvgClip.fromPath(path, offsetX: -pathBounds.left, offsetY: -pathBounds.top, scaleX: 1.0 / pathBounds.width, scaleY: 1.0 / pathBounds.height) - : pathToSvgClipPath(path, + : SvgClip.fromPath(path, scaleX: 1.0 / pathBounds.right, scaleY: 1.0 / pathBounds.bottom); @@ -354,10 +354,10 @@ class PersistedPhysicalShape extends PersistedContainerSurface /// svg clip and render elements. _clipElement?.remove(); _svgElement?.remove(); - _clipElement = svgClipPath; + _clipElement = svgClip.wrapForHtml().host; rootElement!.append(_clipElement!); if (elevation == 0.0) { - setClipPath(rootElement!, createSvgClipUrl()); + setClipPath(rootElement!, svgClip.url); final DomCSSStyleDeclaration rootElementStyle = rootElement!.style; rootElementStyle ..overflow = '' @@ -372,7 +372,7 @@ class PersistedPhysicalShape extends PersistedContainerSurface return; } - setClipPath(childContainer!, createSvgClipUrl()); + setClipPath(childContainer!, svgClip.url); final DomCSSStyleDeclaration rootElementStyle = rootElement!.style; rootElementStyle ..overflow = '' @@ -509,9 +509,10 @@ class PersistedClipPath extends PersistedContainerSurface /// Creates an svg clipPath and applies it to [element]. SVGSVGElement createSvgClipDef(DomElement element, ui.Path clipPath) { final ui.Rect pathBounds = clipPath.getBounds(); - final SVGSVGElement svgClipPath = pathToSvgClipPath(clipPath, + final SvgClip clip = SvgClip.fromPath(clipPath, scaleX: 1.0 / pathBounds.right, scaleY: 1.0 / pathBounds.bottom); - setClipPath(element, createSvgClipUrl()); + final SVGSVGElement svgClipPath = clip.wrapForHtml().host; + setClipPath(element, clip.url); // We need to set width and height for the clipElement to cover the // bounds of the path since browsers such as Safari and Edge // seem to incorrectly intersect the element bounding rect with diff --git a/lib/web_ui/lib/src/engine/html/color_filter.dart b/lib/web_ui/lib/src/engine/html/color_filter.dart index 4961d7c8dec6d..2079c8a3266b6 100644 --- a/lib/web_ui/lib/src/engine/html/color_filter.dart +++ b/lib/web_ui/lib/src/engine/html/color_filter.dart @@ -5,13 +5,8 @@ import 'package:ui/ui.dart' as ui; import '../../engine/color_filter.dart'; -import '../browser_detection.dart'; import '../dom.dart'; import '../embedder.dart'; -import '../svg.dart'; -import '../util.dart'; -import 'bitmap_canvas.dart'; -import 'path_to_svg_clip.dart'; import 'shaders/shader.dart'; import 'surface.dart'; @@ -113,395 +108,3 @@ class PersistedColorFilter extends PersistedContainerSurface } } } - -SvgFilter svgFilterFromBlendMode( - ui.Color? filterColor, ui.BlendMode colorFilterBlendMode) { - final SvgFilter svgFilter; - switch (colorFilterBlendMode) { - case ui.BlendMode.srcIn: - case ui.BlendMode.srcATop: - svgFilter = _srcInColorFilterToSvg(filterColor); - break; - case ui.BlendMode.srcOut: - svgFilter = _srcOutColorFilterToSvg(filterColor); - break; - case ui.BlendMode.dstATop: - svgFilter = _dstATopColorFilterToSvg(filterColor); - break; - case ui.BlendMode.xor: - svgFilter = _xorColorFilterToSvg(filterColor); - break; - case ui.BlendMode.plus: - // Porter duff source + destination. - svgFilter = _compositeColorFilterToSvg(filterColor, 0, 1, 1, 0); - break; - case ui.BlendMode.modulate: - // Porter duff source * destination but preserves alpha. - svgFilter = _modulateColorFilterToSvg(filterColor!); - break; - case ui.BlendMode.overlay: - // Since overlay is the same as hard-light by swapping layers, - // pass hard-light blend function. - svgFilter = _blendColorFilterToSvg( - filterColor, - blendModeToSvgEnum(ui.BlendMode.hardLight)!, - swapLayers: true, - ); - break; - // Several of the filters below (although supported) do not render the - // same (close but not exact) as native flutter when used as blend mode - // for a background-image with a background color. They only look - // identical when feBlend is used within an svg filter definition. - // - // Saturation filter uses destination when source is transparent. - // cMax = math.max(r, math.max(b, g)); - // cMin = math.min(r, math.min(b, g)); - // delta = cMax - cMin; - // lightness = (cMax + cMin) / 2.0; - // saturation = delta / (1.0 - (2 * lightness - 1.0).abs()); - case ui.BlendMode.saturation: - case ui.BlendMode.colorDodge: - case ui.BlendMode.colorBurn: - case ui.BlendMode.hue: - case ui.BlendMode.color: - case ui.BlendMode.luminosity: - case ui.BlendMode.multiply: - case ui.BlendMode.screen: - case ui.BlendMode.darken: - case ui.BlendMode.lighten: - case ui.BlendMode.hardLight: - case ui.BlendMode.softLight: - case ui.BlendMode.difference: - case ui.BlendMode.exclusion: - svgFilter = _blendColorFilterToSvg( - filterColor, blendModeToSvgEnum(colorFilterBlendMode)!); - break; - case ui.BlendMode.src: - case ui.BlendMode.dst: - case ui.BlendMode.dstIn: - case ui.BlendMode.dstOut: - case ui.BlendMode.dstOver: - case ui.BlendMode.clear: - case ui.BlendMode.srcOver: - throw UnimplementedError( - 'Blend mode not supported in HTML renderer: $colorFilterBlendMode', - ); - } - return svgFilter; -} - -// See: https://www.w3.org/TR/SVG11/types.html#InterfaceSVGUnitTypes -const int kObjectBoundingBox = 2; - -// See: https://www.w3.org/TR/SVG11/filters.html#InterfaceSVGFEColorMatrixElement -const int kMatrixType = 1; - -// See: https://www.w3.org/TR/SVG11/filters.html#InterfaceSVGFECompositeElement -const int kOperatorOut = 3; -const int kOperatorAtop = 4; -const int kOperatorXor = 5; -const int kOperatorArithmetic = 6; - -/// Builds an [SvgFilter]. -class SvgFilterBuilder { - SvgFilterBuilder() : id = '_fcf${++_filterIdCounter}' { - filter.id = id; - - // SVG filters that contain `` will fail on several browsers - // (e.g. Firefox) if bounds are not specified. - filter.filterUnits!.baseVal = kObjectBoundingBox; - - // On Firefox percentage width/height 100% works however fails in Chrome 88. - filter.x!.baseVal!.valueAsString = '0%'; - filter.y!.baseVal!.valueAsString = '0%'; - filter.width!.baseVal!.valueAsString = '100%'; - filter.height!.baseVal!.valueAsString = '100%'; - } - - static int _filterIdCounter = 0; - - final String id; - final SVGSVGElement root = kSvgResourceHeader.cloneNode(false) as - SVGSVGElement; - final SVGFilterElement filter = createSVGFilterElement(); - - set colorInterpolationFilters(String filters) { - filter.setAttribute('color-interpolation-filters', filters); - } - - void setFeColorMatrix(List matrix, { required String result }) { - final SVGFEColorMatrixElement element = createSVGFEColorMatrixElement(); - element.type!.baseVal = kMatrixType; - element.result!.baseVal = result; - final SVGNumberList value = element.values!.baseVal!; - for (int i = 0; i < matrix.length; i++) { - value.appendItem(root.createSVGNumber()..value = matrix[i]); - } - filter.append(element); - } - - void setFeFlood({ - required String floodColor, - required String floodOpacity, - required String result, - }) { - final SVGFEFloodElement element = createSVGFEFloodElement(); - element.setAttribute('flood-color', floodColor); - element.setAttribute('flood-opacity', floodOpacity); - element.result!.baseVal = result; - filter.append(element); - } - - void setFeBlend({ - required String in1, - required String in2, - required int mode, - }) { - final SVGFEBlendElement element = createSVGFEBlendElement(); - element.in1!.baseVal = in1; - element.in2!.baseVal = in2; - element.mode!.baseVal = mode; - filter.append(element); - } - - void setFeComposite({ - required String in1, - required String in2, - required int operator, - num? k1, - num? k2, - num? k3, - num? k4, - required String result, - }) { - final SVGFECompositeElement element = createSVGFECompositeElement(); - element.in1!.baseVal = in1; - element.in2!.baseVal = in2; - element.operator!.baseVal = operator; - if (k1 != null) { - element.k1!.baseVal = k1; - } - if (k2 != null) { - element.k2!.baseVal = k2; - } - if (k3 != null) { - element.k3!.baseVal = k3; - } - if (k4 != null) { - element.k4!.baseVal = k4; - } - element.result!.baseVal = result; - filter.append(element); - } - - void setFeImage({ - required String href, - required String result, - required double width, - required double height, - }) { - final SVGFEImageElement element = createSVGFEImageElement(); - element.href!.baseVal = href; - element.result!.baseVal = result; - - // WebKit will not render if x/y/width/height is specified. So we return - // explicit size here unless running on WebKit. - if (browserEngine != BrowserEngine.webkit) { - element.x!.baseVal!.newValueSpecifiedUnits(svgLengthTypeNumber, 0); - element.y!.baseVal!.newValueSpecifiedUnits(svgLengthTypeNumber, 0); - element.width!.baseVal!.newValueSpecifiedUnits(svgLengthTypeNumber, width); - element.height!.baseVal!.newValueSpecifiedUnits(svgLengthTypeNumber, height); - } - filter.append(element); - } - - SvgFilter build() { - root.append(filter); - return SvgFilter._(id, root); - } -} - -class SvgFilter { - SvgFilter._(this.id, this.element); - - final String id; - final SVGSVGElement element; -} - -SvgFilter svgFilterFromColorMatrix(List matrix) { - final SvgFilterBuilder builder = SvgFilterBuilder(); - builder.setFeColorMatrix(matrix, result: 'comp'); - return builder.build(); -} - -// The color matrix for feColorMatrix element changes colors based on -// the following: -// -// | R' | | r1 r2 r3 r4 r5 | | R | -// | G' | | g1 g2 g3 g4 g5 | | G | -// | B' | = | b1 b2 b3 b4 b5 | * | B | -// | A' | | a1 a2 a3 a4 a5 | | A | -// | 1 | | 0 0 0 0 1 | | 1 | -// -// R' = r1*R + r2*G + r3*B + r4*A + r5 -// G' = g1*R + g2*G + g3*B + g4*A + g5 -// B' = b1*R + b2*G + b3*B + b4*A + b5 -// A' = a1*R + a2*G + a3*B + a4*A + a5 -SvgFilter _srcInColorFilterToSvg(ui.Color? color) { - final SvgFilterBuilder builder = SvgFilterBuilder(); - builder.colorInterpolationFilters = 'sRGB'; - builder.setFeColorMatrix( - const [ - 0, 0, 0, 0, 1, - 0, 0, 0, 0, 1, - 0, 0, 0, 0, 1, - 0, 0, 0, 1, 0, - ], - result: 'destalpha', - ); - builder.setFeFlood( - floodColor: colorToCssString(color) ?? '', - floodOpacity: '1', - result: 'flood', - ); - builder.setFeComposite( - in1: 'flood', - in2: 'destalpha', - operator: kOperatorArithmetic, - k1: 1, - k2: 0, - k3: 0, - k4: 0, - result: 'comp', - ); - return builder.build(); -} - -/// The destination that overlaps the source is composited with the source and -/// replaces the destination. dst-atop CR = CB*αB*αA+CA*αA*(1-αB) αR=αA -SvgFilter _dstATopColorFilterToSvg(ui.Color? color) { - final SvgFilterBuilder builder = SvgFilterBuilder(); - builder.setFeFlood( - floodColor: colorToCssString(color) ?? '', - floodOpacity: '1', - result: 'flood', - ); - builder.setFeComposite( - in1: 'SourceGraphic', - in2: 'flood', - operator: kOperatorAtop, - result: 'comp', - ); - return builder.build(); -} - -SvgFilter _srcOutColorFilterToSvg(ui.Color? color) { - final SvgFilterBuilder builder = SvgFilterBuilder(); - builder.setFeFlood( - floodColor: colorToCssString(color) ?? '', - floodOpacity: '1', - result: 'flood', - ); - builder.setFeComposite( - in1: 'flood', - in2: 'SourceGraphic', - operator: kOperatorOut, - result: 'comp', - ); - return builder.build(); -} - -SvgFilter _xorColorFilterToSvg(ui.Color? color) { - final SvgFilterBuilder builder = SvgFilterBuilder(); - builder.setFeFlood( - floodColor: colorToCssString(color) ?? '', - floodOpacity: '1', - result: 'flood', - ); - builder.setFeComposite( - in1: 'flood', - in2: 'SourceGraphic', - operator: kOperatorXor, - result: 'comp', - ); - return builder.build(); -} - -// The source image and color are composited using : -// result = k1 *in*in2 + k2*in + k3*in2 + k4. -SvgFilter _compositeColorFilterToSvg( - ui.Color? color, double k1, double k2, double k3, double k4) { - final SvgFilterBuilder builder = SvgFilterBuilder(); - builder.setFeFlood( - floodColor: colorToCssString(color) ?? '', - floodOpacity: '1', - result: 'flood', - ); - builder.setFeComposite( - in1: 'flood', - in2: 'SourceGraphic', - operator: kOperatorArithmetic, - k1: k1, - k2: k2, - k3: k3, - k4: k4, - result: 'comp', - ); - return builder.build(); -} - -// Porter duff source * destination , keep source alpha. -// First apply color filter to source to change it to [color], then -// composite using multiplication. -SvgFilter _modulateColorFilterToSvg(ui.Color color) { - final double r = color.red / 255.0; - final double b = color.blue / 255.0; - final double g = color.green / 255.0; - - final SvgFilterBuilder builder = SvgFilterBuilder(); - builder.setFeColorMatrix( - [ - 0, 0, 0, 0, r, - 0, 0, 0, 0, g, - 0, 0, 0, 0, b, - 0, 0, 0, 1, 0, - ], - result: 'recolor', - ); - builder.setFeComposite( - in1: 'recolor', - in2: 'SourceGraphic', - operator: kOperatorArithmetic, - k1: 1, - k2: 0, - k3: 0, - k4: 0, - result: 'comp', - ); - return builder.build(); -} - -// Uses feBlend element to blend source image with a color. -SvgFilter _blendColorFilterToSvg(ui.Color? color, SvgBlendMode svgBlendMode, - {bool swapLayers = false}) { - final SvgFilterBuilder builder = SvgFilterBuilder(); - builder.setFeFlood( - floodColor: colorToCssString(color) ?? '', - floodOpacity: '1', - result: 'flood', - ); - if (swapLayers) { - builder.setFeBlend( - in1: 'SourceGraphic', - in2: 'flood', - mode: svgBlendMode.blendMode, - ); - } else { - builder.setFeBlend( - in1: 'flood', - in2: 'SourceGraphic', - mode: svgBlendMode.blendMode, - ); - } - return builder.build(); -} diff --git a/lib/web_ui/lib/src/engine/html/dom_canvas.dart b/lib/web_ui/lib/src/engine/html/dom_canvas.dart index 3e63a0b99ff03..2b5612dbc4698 100644 --- a/lib/web_ui/lib/src/engine/html/dom_canvas.dart +++ b/lib/web_ui/lib/src/engine/html/dom_canvas.dart @@ -156,7 +156,7 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking { /// actually be applied when rendering the element. /// /// Returns a color for box-shadow based on blur filter at sigma. -ui.Color blurColor(ui.Color color, double sigma) { +ui.Color _blurColor(ui.Color color, double sigma) { final double strength = math.min(math.sqrt(sigma) / (math.pi * 2.0), 1.0); final int reducedAlpha = ((1.0 - strength) * color.alpha).round(); return ui.Color((reducedAlpha & 0xff) << 24 | (color.value & 0x00ffffff)); @@ -265,7 +265,7 @@ DomHTMLElement buildDrawRectElement( // A bug in webkit leaves artifacts when this element is animated // with filter: blur, we use boxShadow instead. style.boxShadow = '0px 0px ${sigma * 2.0}px $cssColor'; - cssColor = colorToCssString(blurColor(ui.Color(paint.color), sigma))!; + cssColor = colorToCssString(_blurColor(ui.Color(paint.color), sigma))!; } else { style.filter = 'blur(${sigma}px)'; } diff --git a/lib/web_ui/lib/src/engine/html/path/path.dart b/lib/web_ui/lib/src/engine/html/path/path.dart index 066836901f7e6..ac33f8190163f 100644 --- a/lib/web_ui/lib/src/engine/html/path/path.dart +++ b/lib/web_ui/lib/src/engine/html/path/path.dart @@ -1002,6 +1002,11 @@ class SurfacePath implements ui.Path { _addRRect(rrect, SPathDirection.kCW, 6); } + void addDRRect(ui.RRect outer, ui.RRect inner) { + _addRRect(outer, SPathDirection.kCW, 6); + _addRRect(inner, SPathDirection.kCCW, 6); + } + void _addRRect(ui.RRect rrect, int direction, int startIndex) { assert(rrectIsValid(rrect)); assert(direction != SPathDirection.kUnknown); diff --git a/lib/web_ui/lib/src/engine/html/path_to_svg_clip.dart b/lib/web_ui/lib/src/engine/html/path_to_svg_clip.dart index 24f5fe5eb044e..7b1a667ec4473 100644 --- a/lib/web_ui/lib/src/engine/html/path_to_svg_clip.dart +++ b/lib/web_ui/lib/src/engine/html/path_to_svg_clip.dart @@ -2,63 +2,113 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// TODO(yjbanov): rename this to svg_clip.dart import 'package:ui/ui.dart' as ui; import '../browser_detection.dart'; import '../dom.dart'; import '../svg.dart'; +import '../svg_filter.dart'; import 'path/path.dart'; import 'path/path_to_svg.dart'; /// Counter used for generating clip path id inside an svg tag. int _clipIdCounter = 0; -/// Used for clipping and filter svg resources. -/// -/// Position needs to be absolute since these svgs are sandwiched between -/// canvas elements and can cause layout shifts otherwise. -final SVGSVGElement kSvgResourceHeader = createSVGSVGElement() - ..setAttribute('width', 0) - ..setAttribute('height', 0) - ..style.position = 'absolute'; - -/// Converts Path to svg element that contains a clip-path definition. -/// -/// Calling this method updates [_clipIdCounter]. The HTML id of the generated -/// clip is set to "svgClip${_clipIdCounter}", e.g. "svgClip123". -SVGSVGElement pathToSvgClipPath(ui.Path path, - {double offsetX = 0, +class SvgClip { + factory SvgClip.fromPath(ui.Path path, { + double offsetX = 0, double offsetY = 0, double scaleX = 1.0, - double scaleY = 1.0}) { - _clipIdCounter += 1; - final SVGSVGElement root = kSvgResourceHeader.cloneNode(false) as SVGSVGElement; - final SVGDefsElement defs = createSVGDefsElement(); - root.append(defs); + double scaleY = 1.0, + }) { + final SVGPathElement svgPath = createSVGPathElement(); + if (scaleX != 1 || scaleY != 1) { + svgPath.setAttribute('transform', 'scale($scaleX, $scaleY)'); + } + svgPath.setAttribute('d', pathToSvg((path as SurfacePath).pathRef, offsetX: offsetX, offsetY: offsetY)); - final String clipId = 'svgClip$_clipIdCounter'; - final SVGClipPathElement clipPath = createSVGClipPathElement(); - defs.append(clipPath); - clipPath.id = clipId; - - final SVGPathElement svgPath = createSVGPathElement(); - clipPath.append(svgPath); - svgPath.setAttribute('fill', '#FFFFFF'); - - // Firefox objectBoundingBox fails to scale to 1x1 units, instead use - // no clipPathUnits but write the path in target units. - if (browserEngine != BrowserEngine.firefox) { - clipPath.setAttribute('clipPathUnits', 'objectBoundingBox'); - svgPath.setAttribute('transform', 'scale($scaleX, $scaleY)'); + final SvgClipBuilder builder = SvgClipBuilder(); + builder.addGeometry(svgPath); + return builder.build(); + } + + factory SvgClip.fromRect(ui.Rect rect) { + final SVGRectElement svgRect = createSVGRectElement(); + svgRect.setX(rect.left); + svgRect.setY(rect.top); + svgRect.setWidth(rect.width); + svgRect.setHeight(rect.height); + + final SvgClipBuilder builder = SvgClipBuilder(); + builder.addGeometry(svgRect); + return builder.build(); + } + + SvgClip._({ + required this.id, + required this.clipPath, + }) : url = 'url(#$id)'; + + final String id; + final SVGClipPathElement clipPath; + final String url; + + /// Wraps the clip to be usable for clipping HTML elements. + /// + /// `` cannot be directly added to an HTML node, so this function + /// wraps it in an `...` structure. `clipPathUnits` is + /// applied for compatibility with HTML's coordinate system. + SvgHtmlClip wrapForHtml() { + // Firefox objectBoundingBox fails to scale to 1x1 units, instead use + // no clipPathUnits but write the path in target units. + if (browserEngine != BrowserEngine.firefox) { + clipPath.setAttribute('clipPathUnits', 'objectBoundingBox'); + } + + final SVGSVGElement host = kSvgResourceHeader.cloneNode(false) as SVGSVGElement; + final SVGDefsElement defs = createSVGDefsElement(); + host.append(defs); + defs.append(clipPath); + + return SvgHtmlClip( + host: host, + clip: this, + ); } +} + +class SvgHtmlClip { + SvgHtmlClip({ + required this.host, + required this.clip, + }); - svgPath.setAttribute('d', pathToSvg((path as SurfacePath).pathRef, offsetX: offsetX, offsetY: offsetY)); - return root; + final SVGSVGElement host; + final SvgClip clip; } -String createSvgClipUrl() => 'url(#svgClip$_clipIdCounter)'; +class SvgClipBuilder { + SvgClipBuilder() : _id = 'svgClip${_clipIdCounter++}' { + clipPath.id = _id; + } + + final String _id; + final SVGClipPathElement clipPath = createSVGClipPathElement(); + + void addGeometry(SVGGeometryElement geometry) { + clipPath.append(geometry); + } + + SvgClip build() { + return SvgClip._( + id: _id, + clipPath: clipPath, + ); + } +} /// Resets clip ids. Used for testing by [debugForgetFrameScene] API. -void resetSvgClipIds() { +void debugResetSvgClipIds() { _clipIdCounter = 0; } diff --git a/lib/web_ui/lib/src/engine/html/scene_builder.dart b/lib/web_ui/lib/src/engine/html/scene_builder.dart index bd84aeaf9a992..5d9a847123dc8 100644 --- a/lib/web_ui/lib/src/engine/html/scene_builder.dart +++ b/lib/web_ui/lib/src/engine/html/scene_builder.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; +import 'package:ui/src/engine/html/svg/svg_picture.dart'; import 'package:ui/ui.dart' as ui; import '../../engine.dart' show kProfileApplyFrame, kProfilePrerollFrame; @@ -397,8 +398,13 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { if (willChangeHint) { hints |= 2; } - _addSurface(PersistedPicture( - offset.dx, offset.dy, picture as EnginePicture, hints)); + if (kUseSvgPicture) { + _addSurface(SvgPicture( + offset.dx, offset.dy, picture as EnginePicture, hints)); + } else { + _addSurface(PersistedPicture( + offset.dx, offset.dy, picture as EnginePicture, hints)); + } } /// Adds a backend texture to the scene. @@ -526,7 +532,7 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { static void debugForgetFrameScene() { _lastFrameScene?.rootElement?.remove(); _lastFrameScene = null; - resetSvgClipIds(); + debugResetSvgClipIds(); recycledCanvases.clear(); } diff --git a/lib/web_ui/lib/src/engine/html/shader_mask.dart b/lib/web_ui/lib/src/engine/html/shader_mask.dart index 83988c294df09..7498d91a4322f 100644 --- a/lib/web_ui/lib/src/engine/html/shader_mask.dart +++ b/lib/web_ui/lib/src/engine/html/shader_mask.dart @@ -7,8 +7,7 @@ import 'package:ui/ui.dart' as ui; import '../browser_detection.dart'; import '../dom.dart'; import '../embedder.dart'; -import 'bitmap_canvas.dart'; -import 'color_filter.dart'; +import '../svg_filter.dart'; import 'shaders/shader.dart'; import 'surface.dart'; @@ -181,243 +180,3 @@ class PersistedShaderMask extends PersistedContainerSurface } } } - -SvgFilter svgMaskFilterFromImageAndBlendMode( - String imageUrl, ui.BlendMode blendMode, double width, double height) { - final SvgFilter svgFilter; - switch (blendMode) { - case ui.BlendMode.src: - svgFilter = _srcImageToSvg(imageUrl, width, height); - break; - case ui.BlendMode.srcIn: - case ui.BlendMode.srcATop: - svgFilter = _srcInImageToSvg(imageUrl, width, height); - break; - case ui.BlendMode.srcOut: - svgFilter = _srcOutImageToSvg(imageUrl, width, height); - break; - case ui.BlendMode.xor: - svgFilter = _xorImageToSvg(imageUrl, width, height); - break; - case ui.BlendMode.plus: - // Porter duff source + destination. - svgFilter = _compositeImageToSvg(imageUrl, 0, 1, 1, 0, width, height); - break; - case ui.BlendMode.modulate: - // Porter duff source * destination but preserves alpha. - svgFilter = _modulateImageToSvg(imageUrl, width, height); - break; - case ui.BlendMode.overlay: - // Since overlay is the same as hard-light by swapping layers, - // pass hard-light blend function. - svgFilter = _blendImageToSvg( - imageUrl, - blendModeToSvgEnum(ui.BlendMode.hardLight)!, - width, - height, - swapLayers: true, - ); - break; - // Several of the filters below (although supported) do not render the - // same (close but not exact) as native flutter when used as blend mode - // for a background-image with a background color. They only look - // identical when feBlend is used within an svg filter definition. - // - // Saturation filter uses destination when source is transparent. - // cMax = math.max(r, math.max(b, g)); - // cMin = math.min(r, math.min(b, g)); - // delta = cMax - cMin; - // lightness = (cMax + cMin) / 2.0; - // saturation = delta / (1.0 - (2 * lightness - 1.0).abs()); - case ui.BlendMode.saturation: - case ui.BlendMode.colorDodge: - case ui.BlendMode.colorBurn: - case ui.BlendMode.hue: - case ui.BlendMode.color: - case ui.BlendMode.luminosity: - case ui.BlendMode.multiply: - case ui.BlendMode.screen: - case ui.BlendMode.darken: - case ui.BlendMode.lighten: - case ui.BlendMode.hardLight: - case ui.BlendMode.softLight: - case ui.BlendMode.difference: - case ui.BlendMode.exclusion: - svgFilter = _blendImageToSvg( - imageUrl, blendModeToSvgEnum(blendMode)!, width, height); - break; - case ui.BlendMode.dst: - case ui.BlendMode.dstATop: - case ui.BlendMode.dstIn: - case ui.BlendMode.dstOut: - case ui.BlendMode.dstOver: - case ui.BlendMode.clear: - case ui.BlendMode.srcOver: - throw UnsupportedError( - 'Invalid svg filter request for blend-mode $blendMode'); - } - return svgFilter; -} - -// The color matrix for feColorMatrix element changes colors based on -// the following: -// -// | R' | | r1 r2 r3 r4 r5 | | R | -// | G' | | g1 g2 g3 g4 g5 | | G | -// | B' | = | b1 b2 b3 b4 b5 | * | B | -// | A' | | a1 a2 a3 a4 a5 | | A | -// | 1 | | 0 0 0 0 1 | | 1 | -// -// R' = r1*R + r2*G + r3*B + r4*A + r5 -// G' = g1*R + g2*G + g3*B + g4*A + g5 -// B' = b1*R + b2*G + b3*B + b4*A + b5 -// A' = a1*R + a2*G + a3*B + a4*A + a5 -SvgFilter _srcInImageToSvg(String imageUrl, double width, double height) { - final SvgFilterBuilder builder = SvgFilterBuilder(); - builder.setFeColorMatrix( - const [ - 0, 0, 0, 0, 1, - 0, 0, 0, 0, 1, - 0, 0, 0, 0, 1, - 0, 0, 0, 1, 0, - ], - result: 'destalpha', - ); - builder.setFeImage( - href: imageUrl, - result: 'image', - width: width, - height: height, - ); - builder.setFeComposite( - in1: 'image', - in2: 'destalpha', - operator: kOperatorArithmetic, - k1: 1, - k2: 0, - k3: 0, - k4: 0, - result: 'comp', - ); - return builder.build(); -} - -SvgFilter _srcImageToSvg(String imageUrl, double width, double height) { - final SvgFilterBuilder builder = SvgFilterBuilder(); - builder.setFeImage( - href: imageUrl, - result: 'comp', - width: width, - height: height, - ); - return builder.build(); -} - -SvgFilter _srcOutImageToSvg(String imageUrl, double width, double height) { - final SvgFilterBuilder builder = SvgFilterBuilder(); - builder.setFeImage( - href: imageUrl, - result: 'image', - width: width, - height: height, - ); - builder.setFeComposite( - in1: 'image', - in2: 'SourceGraphic', - operator: kOperatorOut, - result: 'comp', - ); - return builder.build(); -} - -SvgFilter _xorImageToSvg(String imageUrl, double width, double height) { - final SvgFilterBuilder builder = SvgFilterBuilder(); - builder.setFeImage( - href: imageUrl, - result: 'image', - width: width, - height: height, - ); - builder.setFeComposite( - in1: 'image', - in2: 'SourceGraphic', - operator: kOperatorXor, - result: 'comp', - ); - return builder.build(); -} - -// The source image and color are composited using : -// result = k1 *in*in2 + k2*in + k3*in2 + k4. -SvgFilter _compositeImageToSvg(String imageUrl, double k1, double k2, double k3, - double k4, double width, double height) { - final SvgFilterBuilder builder = SvgFilterBuilder(); - builder.setFeImage( - href: imageUrl, - result: 'image', - width: width, - height: height, - ); - builder.setFeComposite( - in1: 'image', - in2: 'SourceGraphic', - operator: kOperatorArithmetic, - k1: k1, - k2: k2, - k3: k3, - k4: k4, - result: 'comp', - ); - return builder.build(); -} - -// Porter duff source * destination , keep source alpha. -// First apply color filter to source to change it to [color], then -// composite using multiplication. -SvgFilter _modulateImageToSvg(String imageUrl, double width, double height) { - final SvgFilterBuilder builder = SvgFilterBuilder(); - builder.setFeImage( - href: imageUrl, - result: 'image', - width: width, - height: height, - ); - builder.setFeComposite( - in1: 'image', - in2: 'SourceGraphic', - operator: kOperatorArithmetic, - k1: 1, - k2: 0, - k3: 0, - k4: 0, - result: 'comp', - ); - return builder.build(); -} - -// Uses feBlend element to blend source image with a color. -SvgFilter _blendImageToSvg( - String imageUrl, SvgBlendMode svgBlendMode, double width, double height, - {bool swapLayers = false}) { - final SvgFilterBuilder builder = SvgFilterBuilder(); - builder.setFeImage( - href: imageUrl, - result: 'image', - width: width, - height: height, - ); - if (swapLayers) { - builder.setFeBlend( - in1: 'SourceGraphic', - in2: 'image', - mode: svgBlendMode.blendMode, - ); - } else { - builder.setFeBlend( - in1: 'image', - in2: 'SourceGraphic', - mode: svgBlendMode.blendMode, - ); - } - return builder.build(); -} diff --git a/lib/web_ui/lib/src/engine/html/shaders/shader.dart b/lib/web_ui/lib/src/engine/html/shaders/shader.dart index e438165383486..cf165be7c8955 100644 --- a/lib/web_ui/lib/src/engine/html/shaders/shader.dart +++ b/lib/web_ui/lib/src/engine/html/shaders/shader.dart @@ -12,10 +12,10 @@ import '../../color_filter.dart'; import '../../dom.dart'; import '../../embedder.dart'; import '../../safe_browser_api.dart'; +import '../../svg_filter.dart'; import '../../util.dart'; import '../../validators.dart'; import '../../vector_math.dart'; -import '../color_filter.dart'; import '../path/path_utils.dart'; import '../render_vertices.dart'; import 'normalized_gradient.dart'; diff --git a/lib/web_ui/lib/src/engine/html/svg/svg_canvas.dart b/lib/web_ui/lib/src/engine/html/svg/svg_canvas.dart new file mode 100644 index 0000000000000..acf6cbf117319 --- /dev/null +++ b/lib/web_ui/lib/src/engine/html/svg/svg_canvas.dart @@ -0,0 +1,330 @@ +// 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. + +import 'dart:math' as math; +import 'dart:typed_data'; + +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui; + +/// A canvas that renders to DOM elements and CSS properties. +class SvgCanvas extends EngineCanvas with SaveElementStackTracking { + SvgCanvas() : _id = _idCounter++ { + rootElement.setAttribute('flt-id', _id); + rootElement.style.overflow = 'visible'; + rootElement.append(_defs); + } + + static int _idCounter = 1; + final int _id; + + @override + final SVGSVGElement rootElement = createSVGSVGElement(); + + final SVGDefsElement _defs = createSVGDefsElement(); + + void discard() { + rootElement.remove(); + } + + void draw(EnginePicture picture) { + picture.recordingCanvas!.apply(this, ui.Rect.largest); + } + + @override + void clear() { + throw UnsupportedError('SvgCanvas must not be reused.'); + } + + @override + void clipRect(ui.Rect rect, ui.ClipOp clipOp) { + _clip(SvgClip.fromRect(rect), debugLabel: 'clipRect'); + } + + @override + void clipRRect(ui.RRect rrect) { + final SurfacePath path = SurfacePath(); + path.addRRect(rrect); + _clip(SvgClip.fromPath(path), debugLabel: 'clipRRect'); + } + + @override + void clipPath(ui.Path path) { + _clip(SvgClip.fromPath(path), debugLabel: 'clipPath'); + } + + void _clip(SvgClip clip, { required String debugLabel }) { + _defs.append(clip.clipPath); + clip.clipPath.setAttribute('flt-debug', debugLabel); + _applyCurrentTransform(clip.clipPath); + final SVGGElement clipGroup = createSVGGElement(); + clipGroup.setAttribute('clip-path', clip.url); + currentElement.append(clipGroup); + + // Push the element to the stack to make it the currentElement. This way the + // clip will apply to any `draw` method until `restore` is called. + pushElement(clipGroup); + } + + @override + void drawColor(ui.Color color, ui.BlendMode blendMode) { + drawPaint( + SurfacePaintData() + ..color = color.value + ..blendMode = blendMode, + ); + } + + @override + void drawLine(ui.Offset p1, ui.Offset p2, SurfacePaintData paint) { + final SVGLineElement element = createSVGLineElement(); + element.setX1(p1.dx); + element.setY1(p1.dy); + element.setX2(p2.dx); + element.setY2(p2.dy); + _applyPaintToDrawing(element, ui.Rect.fromPoints(p1, p2), paint); + _applyCurrentTransform(element); + currentElement.append(element); + } + + @override + void drawPaint(SurfacePaintData paint) { + drawRect(const ui.Rect.fromLTRB(-10000, -10000, 10000, 10000), paint); + } + + @override + void drawRect(ui.Rect rect, SurfacePaintData paint) { + final SVGRectElement element = createSVGRectElement(); + element.setX(rect.left); + element.setY(rect.top); + element.setWidth(rect.width); + element.setHeight(rect.height); + _applyPaintToDrawing(element, rect, paint); + _applyCurrentTransform(element); + currentElement.append(element); + } + + void _applyCurrentTransform(DomElement element) { + setElementTransform(element, currentTransform.storage); + } + + void _applyCurrentTransformAndOffset(DomElement element, ui.Offset offset) { + if (!currentTransform.isIdentity()) { + setElementTransform( + element, + transformWithOffset(currentTransform, offset).storage, + ); + } else if (offset != ui.Offset.zero) { + setElementTranslation(element, offset); + } + } + + @override + void drawRRect(ui.RRect rrect, SurfacePaintData paint) { + // TODO(yjbanov): this could be faster if specialized for rrect. + final SurfacePath path = SurfacePath(); + path.addRRect(rrect); + _drawPath(path, paint, debugLabel: 'drawRRect'); + } + + @override + void drawDRRect(ui.RRect outer, ui.RRect inner, SurfacePaintData paint) { + final SurfacePath path = SurfacePath(); + path.addDRRect(outer, inner); + _drawPath(path, paint, debugLabel: 'drawDRRect'); + } + + @override + void drawOval(ui.Rect rect, SurfacePaintData paint) { + final SurfacePath path = SurfacePath(); + path.addOval(rect); + _drawPath(path, paint, debugLabel: 'drawOval'); + } + + @override + void drawCircle(ui.Offset c, double radius, SurfacePaintData paint) { + final SurfacePath path = SurfacePath(); + path.addOval(ui.Rect.fromCircle(center: c, radius: radius)); + _drawPath(path, paint, debugLabel: 'drawCircle'); + } + + @override + void drawPath(ui.Path path, SurfacePaintData paint, { String? debugLabel }) { + _drawPath(path, paint, debugLabel: debugLabel); + } + + SVGPathElement _drawPath(ui.Path path, SurfacePaintData paint, { String? debugLabel, ui.Offset? offset }) { + path as SurfacePath; + final SVGPathElement element = createSVGPathElement(); + element.setAttribute('fld-debug', debugLabel ?? 'drawPath'); + element.setAttribute('d', pathToSvg(path.pathRef)); + _applyPaintToDrawing(element, path.getBounds(), paint); + _applyCurrentTransform(element); + currentElement.append(element); + return element; + } + + @override + void drawShadow(ui.Path path, ui.Color color, double elevation, + bool transparentOccluder) { + final SurfaceShadowData? shadow = computeShadow(path.getBounds(), elevation); + if (shadow != null) { + final SVGPathElement element = _drawPath( + path, + SurfacePaintData() + ..style = ui.PaintingStyle.fill + ..color = toShadowColor(color).value, + offset: shadow.offset, + debugLabel: 'drawShadow', + ); + final ui.Rect pathBounds = path.getBounds(); + _applyBlurFilter( + element: element, + sigmaX: shadow.blurX / 3, + sigmaY: shadow.blurY / 3, + areaOfEffect: ui.Rect.fromLTRB( + pathBounds.left - shadow.blurX, + pathBounds.top - shadow.blurY, + pathBounds.right + shadow.blurX, + pathBounds.bottom + shadow.blurY, + ), + ); + } + } + + @override + void drawImage(ui.Image image, ui.Offset p, SurfacePaintData paint) { + final SVGImageElement imageElement = _drawUnpositionedImage(image, paint); + _applyCurrentTransformAndOffset(imageElement, p); + currentElement.append(imageElement); + } + + SVGImageElement _drawUnpositionedImage(ui.Image image, SurfacePaintData paint) { + image as HtmlImage; + final SVGImageElement imageElement = image.cloneSVGImageElement(); + _applyPaintToDrawing( + imageElement, + ui.Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()), + paint, + ); + return imageElement; + } + + @override + void drawImageRect( + ui.Image image, ui.Rect src, ui.Rect dst, SurfacePaintData paint) { + final bool needsDestinationRectangle = + src.left != 0 || + src.top != 0 || + src.width != image.width || + src.height != image.height || + dst.width != image.width || + dst.height != image.height; + if (!needsDestinationRectangle) { + // There's no effect from the `src` or `dst` rectangles, so the image can + // be painted using the simpler and faster route. + drawImage(image, dst.topLeft, paint); + } else { + final SvgClip clip = SvgClip.fromRect(src); + _defs.append(clip.clipPath); + + final SVGImageElement imageElement = _drawUnpositionedImage(image, paint); + imageElement.setAttribute('clip-path', clip.url); + imageElement.style + ..transformOrigin = '${dst.left}px ${dst.top}px 0' + ..transform = 'scale(calc(${dst.width / src.width}), calc(${dst.height / src.height})) ' + 'translate(${dst.left - src.left}px, ${dst.top - src.top}px)'; + + if (currentTransform.isIdentity()) { + // There's no extra transform active in the save stack, so just add the + // element with its own transform. + currentElement.append(imageElement); + } else { + // There's a non-trivial transform in the save stack. However, it cannot + // be applied directly on the image element because the image element + // has a non-zero transform-origin, which would apply the save stack + // transform incorrectly. So instead, the image is wrapped in a + // element and the save stack transform is applied to it instead. + final SVGGElement transformElement = createSVGGElement(); + _applyCurrentTransform(transformElement); + transformElement.append(imageElement); + currentElement.append(transformElement); + } + } + } + + @override + void drawParagraph(ui.Paragraph paragraph, ui.Offset offset) { + paragraph as CanvasParagraph; + assert(paragraph.isLaidOut); + final SVGGElement paragraphElement = paragraph.toSvgElement(); + _applyCurrentTransformAndOffset(paragraphElement, offset); + currentElement.append(paragraphElement); + } + + @override + void drawVertices( + ui.Vertices vertices, ui.BlendMode blendMode, SurfacePaintData paint) { + print('Unimplemented drawVertices'); + } + + @override + void drawPoints( + ui.PointMode pointMode, Float32List points, SurfacePaintData paint) { + print('Unimplemented drawPoints'); + } + + @override + void endOfPaint() { + // TODO(yjbanov): maybe need to restore active saves and saveLayers here. + } + + void _applyBlurFilter({ + required SVGElement element, + required ui.Rect areaOfEffect, + required double sigmaX, + required double sigmaY, + }) { + final SvgFilterBuilder filterBuilder = SvgFilterBuilder(targetType: SvgFilterTargetType.svg) + ..setFeGaussianBlur( + sigmaX: sigmaX, + sigmaY: sigmaY, + areaOfEffect: areaOfEffect, + ); + final SvgFilter filter = filterBuilder.build(_defs); + filter.applyToSvg(element); + } + + void _applyPaintToDrawing(SVGElement element, ui.Rect drawableBounds, SurfacePaintData paint) { + final bool isStroke = paint.style == ui.PaintingStyle.stroke; + final double strokeWidth = paint.strokeWidth ?? 0.0; + final DomCSSStyleDeclaration style = element.style; + final String cssColor = paint.color == null ? '#000000' : colorValueToCssString(paint.color)!; + + final ui.MaskFilter? maskFilter = paint.maskFilter; + if (maskFilter != null) { + _applyBlurFilter( + element: element, + sigmaX: maskFilter.webOnlySigma, + sigmaY: maskFilter.webOnlySigma, + areaOfEffect: drawableBounds.inflate(maskFilter.webOnlySigma * 5), + ); + } + + if (isStroke) { + element.setAttribute('stroke', cssColor); + element.setAttribute('fill', 'none'); + if (strokeWidth == 0) { + // In Flutter 0 means "hairline", i.e. exactly 1 pixel wide line, which in + // SVG is expressed as a non-scaling vector effect. + style.vectorEffect = 'non-scaling-stroke'; + } else { + element.setAttribute('stroke-width', '${strokeWidth.toStringAsFixed(3)}px'); + } + } else { + element.setAttribute('fill', cssColor); + element.setAttribute('stroke', 'none'); + } + } +} diff --git a/lib/web_ui/lib/src/engine/html/svg/svg_picture.dart b/lib/web_ui/lib/src/engine/html/svg/svg_picture.dart new file mode 100644 index 0000000000000..74a4813de5e91 --- /dev/null +++ b/lib/web_ui/lib/src/engine/html/svg/svg_picture.dart @@ -0,0 +1,121 @@ +// 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. + +import 'package:ui/ui.dart' as ui; + +import '../../dom.dart'; +import '../../picture.dart'; +import '../surface.dart'; +import 'svg_canvas.dart'; + +const bool kUseSvgPicture = true; + +class SvgPicture extends PersistedLeafSurface { + SvgPicture(this.dx, this.dy, this.picture, this.hints) + : localPaintBounds = picture.recordingCanvas!.pictureBounds, + _id = _pictureCounter++; + + static int _pictureCounter = 1; + + final int _id; + final double dx; + final double dy; + final EnginePicture picture; + final ui.Rect? localPaintBounds; + final int hints; + + SvgCanvas? _canvas; + + @override + void adoptElements(SvgPicture oldSurface) { + super.adoptElements(oldSurface); + _canvas = oldSurface._canvas; + } + + @override + DomElement createElement() { + final DomElement element = defaultCreateElement('flt-svg-picture'); + element.setAttribute('flt-id', _id); + + // The DOM elements used to render pictures are used purely to put pixels on + // the screen. They have no semantic information. If an assistive technology + // attempts to scan picture content it will look like garbage and confuse + // users. UI semantics are exported as a separate DOM tree rendered parallel + // to pictures. + // + // Why are layer and scene elements not hidden from ARIA? Because those + // elements may contain platform views, and platform views must be + // accessible. + element.setAttribute('aria-hidden', 'true'); + + return element; + } + + @override + void preroll(PrerollSurfaceContext prerollContext) { + if (prerollContext.activeShaderMaskCount != 0 || + prerollContext.activeColorFilterCount != 0) { + picture.recordingCanvas?.renderStrategy.isInsideSvgFilterTree = true; + } + super.preroll(prerollContext); + } + + @override + void recomputeTransformAndClip() { + transform = parent!.transform; + if (dx != 0.0 || dy != 0.0) { + transform = transform!.clone(); + transform!.translate2D(dx, dy); + } + } + + @override + double matchForUpdate(SvgPicture existingSurface) { + if (existingSurface.picture == picture) { + // Picture is the same, return perfect score. + return 0.0; + } + return 1.0; + } + + @override + void apply() { + _applyTranslate(); + _redraw(); + } + + @override + void update(SvgPicture oldSurface) { + super.update(oldSurface); + + if (dx != oldSurface.dx || dy != oldSurface.dy) { + _applyTranslate(); + } + + if (identical(picture, oldSurface.picture)) { + _canvas = oldSurface._canvas; + } else { + _redraw(); + } + } + + void _applyTranslate() { + rootElement!.style.transform = 'translate(${dx}px, ${dy}px)'; + } + + void _redraw() { + _canvas?.discard(); + final SvgCanvas canvas = SvgCanvas(); + _canvas = canvas; + canvas.draw(picture); + rootElement!.append(canvas.rootElement); + } + + @override + void discard() { + super.discard(); + _canvas?.discard(); + _canvas = null; + } +} diff --git a/lib/web_ui/lib/src/engine/html_image_codec.dart b/lib/web_ui/lib/src/engine/html_image_codec.dart index d5db33fcbffc1..9c0aa8b3399a5 100644 --- a/lib/web_ui/lib/src/engine/html_image_codec.dart +++ b/lib/web_ui/lib/src/engine/html_image_codec.dart @@ -10,6 +10,7 @@ import 'package:ui/ui.dart' as ui; import 'browser_detection.dart'; import 'dom.dart'; import 'safe_browser_api.dart'; +import 'svg.dart'; import 'util.dart'; Object? get _jsImageDecodeFunction => getJsProperty( @@ -42,7 +43,7 @@ class HtmlCodec implements ui.Codec { // Currently there is no way to watch decode progress, so // we add 0/100 , 100/100 progress callbacks to enable loading progress // builders to create UI. - chunkCallback?.call(0, 100); + chunkCallback?.call(0, 100); if (_supportsDecode) { final DomHTMLImageElement imgElement = createDomHTMLImageElement(); imgElement.src = src; @@ -212,6 +213,14 @@ class HtmlImage implements ui.Image { return imgElement.cloneNode(true) as DomHTMLImageElement; } + SVGImageElement cloneSVGImageElement() { + final SVGImageElement result = createSVGImageElement(); + result.href.baseVal = imgElement.src; + result.width.baseVal.newValueSpecifiedUnits(width); + result.height.baseVal.newValueSpecifiedUnits(height); + return result; + } + @override String toString() => '[$width\u00D7$height]'; } diff --git a/lib/web_ui/lib/src/engine/shadow.dart b/lib/web_ui/lib/src/engine/shadow.dart index a4a79244ed95a..ca0f98f434826 100644 --- a/lib/web_ui/lib/src/engine/shadow.dart +++ b/lib/web_ui/lib/src/engine/shadow.dart @@ -86,10 +86,13 @@ ui.Rect computePenumbraBounds(ui.Rect shape, double elevation) { class SurfaceShadowData { const SurfaceShadowData({ required this.blurWidth, + required this.blurX, + required this.blurY, required this.offset, }); - /// The length in pixels of the shadow. + /// The length in pixels of the shadow to use when per-axis shadow blurs are + /// not supported. /// /// This is different from the `sigma` used by blur filters. This value /// contains the entire shadow, so, for example, to compute the shadow @@ -97,6 +100,12 @@ class SurfaceShadowData { /// that casts it. final double blurWidth; + /// The length in pixels of the shadow along the X axis. + final double blurX; + + /// The length in pixels of the shadow along the Y axis. + final double blurY; + /// The offset of the shadow relative to the shape as computed by /// [computeShadowOffset]. final ui.Offset offset; @@ -122,10 +131,13 @@ SurfaceShadowData? computeShadow(ui.Rect shape, double elevation) { final double penumbraWidth = elevation * penumbraTangentX; final double penumbraHeight = elevation * penumbraTangentY; return SurfaceShadowData( - // There's no way to express different blur along different dimensions, so - // we use the narrower of the two to prevent the shadow blur from being longer - // than the shape itself, using min instead of average of penumbra values. + // In canvas 2D there's no way to express different blur along different + // dimensions, so we use the narrower of the two to prevent the shadow blur + // from being longer than the shape itself, using min instead of average of + // penumbra values. blurWidth: math.min(penumbraWidth, penumbraHeight), + blurX: penumbraWidth, + blurY: penumbraHeight, offset: computeShadowOffset(elevation), ); } diff --git a/lib/web_ui/lib/src/engine/svg.dart b/lib/web_ui/lib/src/engine/svg.dart index 8f0ffbc743b90..73600f36cebe4 100644 --- a/lib/web_ui/lib/src/engine/svg.dart +++ b/lib/web_ui/lib/src/engine/svg.dart @@ -2,10 +2,24 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// Because JS binding classes are empty shells, any static members can stand +// alone. +// ignore_for_file: avoid_classes_with_only_static_members + import 'package:js/js.dart'; +import 'package:js/js_util.dart' as js_util; +import 'package:ui/src/engine/vector_math.dart'; import 'dom.dart'; +// Because SVG is silly and doesn't have direct constructors for simple values, +// such as `SVGNumber` and `SVGLength`, we need a dummy `` element whose +// methods we can use to create such values. See all methods that begin with +// "create" on MDN: +// +// https://developer.mozilla.org/en-US/docs/Web/API/SVGSVGElement +final SVGSVGElement _svgValueFactory = createSVGSVGElement(); + @JS() @staticInterop class SVGElement extends DomElement {} @@ -17,7 +31,45 @@ SVGElement createSVGElement(String tag) => @JS() @staticInterop class SVGGraphicsElement extends SVGElement {} +extension SVGGraphicsElementExtension on SVGGraphicsElement { + external SVGAnimatedTransformList get transform; +} + +@JS() +@staticInterop +class SVGAnimatedTransformList extends SVGElement {} +extension SVGAnimatedTransformListExtension on SVGAnimatedTransformList { + external SVGTransformList get baseVal; +} +@JS() +@staticInterop +class SVGTransform extends SVGElement {} +extension SVGTransformExtension on SVGTransform { + /// The matrix representing this transform. + /// + /// The returned object is live, meaning that any changes made to the + /// [SVGTransform] object are immediately reflected in the matrix object and + /// vice versa. + external SVGMatrix get matrix; + + external void setMatrix(SVGMatrix matrix); + external void setTranslate(double tx, double ty); + external void setScale(double sx, double sy); + external void setRotate(double angle, double cx, double cy); + external void setSkewX(double angle); + external void setSkewY(double angle); +} + +@JS() +@staticInterop +class SVGTransformList extends SVGElement {} +extension SVGTransformListExtension on SVGTransformList { + external SVGTransform appendItem(SVGTransform newItem); + external SVGTransform getItem(int index); +} + +/// The `` element containing an SVG picture. @JS() @staticInterop class SVGSVGElement extends SVGGraphicsElement {} @@ -30,8 +82,204 @@ SVGSVGElement createSVGSVGElement() { extension SVGSVGElementExtension on SVGSVGElement { external SVGNumber createSVGNumber(); - external SVGAnimatedLength? get height; - external SVGAnimatedLength? get width; + external SVGLength createSVGLength(); + external SVGMatrix createSVGMatrix(); + + external SVGAnimatedLength get height; + void setHeight(double height) { + this.height.baseVal.newValueSpecifiedUnits(height); + } + + external SVGAnimatedLength get width; + void setWidth(double width) { + this.width.baseVal.newValueSpecifiedUnits(width); + } +} + +@JS() +@staticInterop +class SVGGElement extends SVGGraphicsElement { +} + +SVGGElement createSVGGElement() { + return createSVGElement('g') as SVGGElement; +} + +@JS() +@staticInterop +class SVGImageElement extends SVGGraphicsElement {} +extension SVGImageElementExtension on SVGImageElement { + external SVGAnimatedString get href; + external SVGAnimatedLength get width; + external SVGAnimatedLength get height; +} + +SVGImageElement createSVGImageElement() { + return createSVGElement('image') as SVGImageElement; +} + +@JS() +@staticInterop +class SVGGeometryElement extends SVGGraphicsElement {} + + +@JS() +@staticInterop +class SVGForeignObjectElement extends SVGGraphicsElement {} + +SVGForeignObjectElement createSVGForeignObjectElement() { + return createSVGElement('foreignObject') as SVGForeignObjectElement; +} + +extension SVGForeignObjectElementExtension on SVGForeignObjectElement { + external SVGAnimatedLength get height; + void setHeight(double height) { + this.height.baseVal.newValueSpecifiedUnits(height); + } + + external SVGAnimatedLength get width; + void setWidth(double width) { + this.width.baseVal.newValueSpecifiedUnits(width); + } + + external SVGAnimatedLength get x; + void setX(double x) { + this.x.baseVal.newValueSpecifiedUnits(x); + } + + external SVGAnimatedLength get y; + void setY(double y) { + this.y.baseVal.newValueSpecifiedUnits(y); + } +} + +@JS() +@staticInterop +class SVGTextContentElement extends SVGGraphicsElement { +} + +extension SVGTextContentElementExtension on SVGTextContentElement { + external SVGAnimatedLength get textLength; + external SVGAnimatedEnumeration get lengthAdjust; +} + +@JS() +@staticInterop +class SVGTextPositioningElement extends SVGTextContentElement {} + +extension SVGTextPositioningElementExtension on SVGTextPositioningElement { + external SVGAnimatedLengthList get x; + void addX(double x) { + this.x.baseVal.appendItem(SVGLength.create(x)); + } + + external SVGAnimatedLengthList get y; + void addY(double y) { + this.y.baseVal.appendItem(SVGLength.create(y)); + } + + external SVGAnimatedLengthList get dx; + void addDx(double dx) { + this.dx.baseVal.appendItem(SVGLength.create(dx)); + } + + external SVGAnimatedLengthList get dy; + void addDy(double dy) { + this.dy.baseVal.appendItem(SVGLength.create(dy)); + } +} + +@JS() +@staticInterop +class SVGTextElement extends SVGTextPositioningElement {} + +extension SVGTextElementExtension on SVGTextElement { + external SVGAnimatedLength get textLength; +} + +SVGTextElement createSVGTextElement() { + return createSVGElement('text') as SVGTextElement; +} + +@JS() +@staticInterop +class SVGTSpanElement extends SVGTextPositioningElement {} + +SVGTSpanElement createSVGTSpanElement() { + return createSVGElement('tspan') as SVGTSpanElement; +} + +extension SVGTSpanElementExtension on SVGTSpanElement { + external SVGAnimatedLength get textLength; +} + +/// Bindings for https://developer.mozilla.org/en-US/docs/Web/API/SVGRectElement. +@JS() +@staticInterop +class SVGRectElement extends SVGGeometryElement {} +extension SVGRectElementExtension on SVGRectElement { + external SVGAnimatedLength get height; + void setHeight(double height) { + this.height.baseVal.newValueSpecifiedUnits(height); + } + + external SVGAnimatedLength get width; + void setWidth(double width) { + this.width.baseVal.newValueSpecifiedUnits(width); + } + + external SVGAnimatedLength get x; + void setX(double x) { + this.x.baseVal.newValueSpecifiedUnits(x); + } + + external SVGAnimatedLength get y; + void setY(double y) { + this.y.baseVal.newValueSpecifiedUnits(y); + } + + external SVGAnimatedLength get rx; + void setRx(double rx) { + this.rx.baseVal.newValueSpecifiedUnits(rx); + } + + external SVGAnimatedLength get ry; + void setRy(double ry) { + this.ry.baseVal.newValueSpecifiedUnits(ry); + } +} + +SVGRectElement createSVGRectElement() { + return createSVGElement('rect') as SVGRectElement; +} + +@JS() +@staticInterop +class SVGLineElement extends SVGGeometryElement {} +extension SVGLineElementExtension on SVGLineElement { + external SVGAnimatedLength get x1; + void setX1(double x1) { + this.x1.baseVal.newValueSpecifiedUnits(x1); + } + + external SVGAnimatedLength get y1; + void setY1(double y1) { + this.y1.baseVal.newValueSpecifiedUnits(y1); + } + + external SVGAnimatedLength get x2; + void setX2(double x2) { + this.x2.baseVal.newValueSpecifiedUnits(x2); + } + + external SVGAnimatedLength get y2; + void setY2(double y2) { + this.y2.baseVal.newValueSpecifiedUnits(y2); + } +} + +SVGLineElement createSVGLineElement() { + return createSVGElement('line') as SVGLineElement; } @JS() @@ -50,10 +298,6 @@ SVGDefsElement createSVGDefsElement() => domDocument.createElementNS('http://www.w3.org/2000/svg', 'defs') as SVGDefsElement; -@JS() -@staticInterop -class SVGGeometryElement extends SVGGraphicsElement {} - @JS() @staticInterop class SVGPathElement extends SVGGeometryElement {} @@ -67,11 +311,27 @@ SVGPathElement createSVGPathElement() => class SVGFilterElement extends SVGElement {} extension SVGFilterElementExtension on SVGFilterElement { - external SVGAnimatedEnumeration? get filterUnits; - external SVGAnimatedLength? get height; - external SVGAnimatedLength? get width; - external SVGAnimatedLength? get x; - external SVGAnimatedLength? get y; + external SVGAnimatedEnumeration get filterUnits; + + external SVGAnimatedLength get height; + void setHeight(double height) { + this.height.baseVal.newValueSpecifiedUnits(height); + } + + external SVGAnimatedLength get width; + void setWidth(double width) { + this.width.baseVal.newValueSpecifiedUnits(width); + } + + external SVGAnimatedLength get x; + void setX(double x) { + this.x.baseVal.newValueSpecifiedUnits(x); + } + + external SVGAnimatedLength get y; + void setY(double y) { + this.y.baseVal.newValueSpecifiedUnits(y); + } } SVGFilterElement createSVGFilterElement() => @@ -81,21 +341,69 @@ SVGFilterElement createSVGFilterElement() => @JS() @staticInterop class SVGAnimatedLength {} - extension SVGAnimatedLengthExtension on SVGAnimatedLength { - external SVGLength? get baseVal; + external SVGLength get baseVal; } @JS() @staticInterop -class SVGLength {} +class SVGAnimatedLengthList {} +extension SVGAnimatedLengthListExtension on SVGAnimatedLengthList { + external SVGLengthList get baseVal; +} + +@JS() +@staticInterop +class SVGLengthList {} + +extension SVGLengthListExtension on SVGLengthList { + external SVGLength appendItem(SVGLength newItem); +} + +@JS() +@staticInterop +class SVGLength { + static SVGLength create(num valueInSpecifiedUnits, [SVGLengthUnitType unitType = SVGLengthUnitType.number]) { + return _svgValueFactory.createSVGLength() + ..newValueSpecifiedUnits(valueInSpecifiedUnits, unitType); + } +} extension SVGLengthExtension on SVGLength { external set valueAsString(String? value); - external void newValueSpecifiedUnits(int unitType, num valueInSpecifiedUnits); + + /// A type-safe wrapper for the underlying `newValueSpecifiedUnits` method with + /// reasonable defaults. + void newValueSpecifiedUnits(num valueInSpecifiedUnits, [SVGLengthUnitType unitType = SVGLengthUnitType.number]) { + _newValueSpecifiedUnits(this, unitType._svgValue, valueInSpecifiedUnits); + } +} + +class SVGLengthUnitType { + const SVGLengthUnitType(this._svgValue); + + static const SVGLengthUnitType unknown = SVGLengthUnitType(0); + static const SVGLengthUnitType number = SVGLengthUnitType(1); + static const SVGLengthUnitType percentage = SVGLengthUnitType(2); + static const SVGLengthUnitType ems = SVGLengthUnitType(3); + static const SVGLengthUnitType exs = SVGLengthUnitType(4); + static const SVGLengthUnitType px = SVGLengthUnitType(5); + static const SVGLengthUnitType cm = SVGLengthUnitType(6); + static const SVGLengthUnitType mm = SVGLengthUnitType(7); + static const SVGLengthUnitType inch = SVGLengthUnitType(8); + static const SVGLengthUnitType pt = SVGLengthUnitType(9); + static const SVGLengthUnitType pc = SVGLengthUnitType(10); + + final int _svgValue; } -const int svgLengthTypeNumber = 1; +void _newValueSpecifiedUnits(SVGLength length, int unitType, num valueInSpecifiedUnits) { + js_util.callMethod( + length, + 'newValueSpecifiedUnits', + [unitType, valueInSpecifiedUnits], + ); +} @JS() @staticInterop @@ -108,23 +416,36 @@ extension SVGAnimatedEnumerationExtenson on SVGAnimatedEnumeration { @JS() @staticInterop class SVGFEColorMatrixElement extends SVGElement {} - extension SVGFEColorMatrixElementExtension on SVGFEColorMatrixElement { - external SVGAnimatedEnumeration? get type; - external SVGAnimatedString? get result; - external SVGAnimatedNumberList? get values; + external SVGAnimatedEnumeration get type; + external SVGAnimatedString get result; + external SVGAnimatedNumberList get values; } SVGFEColorMatrixElement createSVGFEColorMatrixElement() => domDocument.createElementNS('http://www.w3.org/2000/svg', 'feColorMatrix') as SVGFEColorMatrixElement; +@JS() +@staticInterop +class SVGFEGaussianBlurElement extends SVGElement {} +extension SVGFEGaussianBlurElementExtension on SVGFEGaussianBlurElement { + external SVGAnimatedString get in1; + external SVGAnimatedNumber get stdDeviationX; + external SVGAnimatedNumber get stdDeviationY; + external SVGAnimatedString get result; +} + +SVGFEGaussianBlurElement createSVGFEGaussianBlurElement() => + domDocument.createElementNS('http://www.w3.org/2000/svg', 'feGaussianBlur') + as SVGFEGaussianBlurElement; + @JS() @staticInterop class SVGFEFloodElement extends SVGElement {} extension SVGFEFloodElementExtension on SVGFEFloodElement { - external SVGAnimatedString? get result; + external SVGAnimatedString get result; } SVGFEFloodElement createSVGFEFloodElement() => @@ -136,9 +457,9 @@ SVGFEFloodElement createSVGFEFloodElement() => class SVGFEBlendElement extends SVGElement {} extension SVGFEBlendElementExtension on SVGFEBlendElement { - external SVGAnimatedString? get in1; - external SVGAnimatedString? get in2; - external SVGAnimatedEnumeration? get mode; + external SVGAnimatedString get in1; + external SVGAnimatedString get in2; + external SVGAnimatedEnumeration get mode; } SVGFEBlendElement createSVGFEBlendElement() => @@ -150,12 +471,35 @@ SVGFEBlendElement createSVGFEBlendElement() => class SVGFEImageElement extends SVGElement {} extension SVGFEImageElementExtension on SVGFEImageElement { - external SVGAnimatedLength? get height; - external SVGAnimatedLength? get width; - external SVGAnimatedString? get result; - external SVGAnimatedLength? get x; - external SVGAnimatedLength? get y; - external SVGAnimatedString? get href; + external SVGAnimatedLength get height; + void setHeight(double height) { + this.height.baseVal.newValueSpecifiedUnits(height); + } + + external SVGAnimatedLength get width; + void setWidth(double width) { + this.width.baseVal.newValueSpecifiedUnits(width); + } + + external SVGAnimatedString get result; + void setResult(String result) { + this.result.baseVal = result; + } + + external SVGAnimatedLength get x; + void setX(double x) { + this.x.baseVal.newValueSpecifiedUnits(x); + } + + external SVGAnimatedLength get y; + void setY(double y) { + this.y.baseVal.newValueSpecifiedUnits(y); + } + + external SVGAnimatedString get href; + void setHref(String href) { + this.href.baseVal = href; + } } SVGFEImageElement createSVGFEImageElement() => @@ -171,14 +515,14 @@ SVGFECompositeElement createSVGFECompositeElement() => as SVGFECompositeElement; extension SVGFEBlendCompositeExtension on SVGFECompositeElement { - external SVGAnimatedString? get in1; - external SVGAnimatedString? get in2; - external SVGAnimatedNumber? get k1; - external SVGAnimatedNumber? get k2; - external SVGAnimatedNumber? get k3; - external SVGAnimatedNumber? get k4; - external SVGAnimatedEnumeration? get operator; - external SVGAnimatedString? get result; + external SVGAnimatedString get in1; + external SVGAnimatedString get in2; + external SVGAnimatedNumber get k1; + external SVGAnimatedNumber get k2; + external SVGAnimatedNumber get k3; + external SVGAnimatedNumber get k4; + external SVGAnimatedEnumeration get operator; + external SVGAnimatedString get result; } @JS() @@ -215,8 +559,95 @@ extension SVGNumberListExtension on SVGNumberList { @JS() @staticInterop -class SVGNumber {} +class SVGNumber { + static SVGNumber create(num value) { + return _svgValueFactory.createSVGNumber()..value = value; + } +} extension SVGNumberExtension on SVGNumber { external set value(num? value); } + +/// An SVG-specific 2x3 transform matrix. +/// +/// This is not sufficient to represent arbitrary 4x4 matrices the Flutter uses, +/// but many basic things can be expressed. See [fromMatrix4]. +/// +/// W3C aims to replace this type with a more capable [DOMMatrix] in the future, +/// but right now many SVG APIs are still using it, and so must we. See: +/// +/// - https://developer.mozilla.org/en-US/docs/Web/API/SVGMatrix +/// - https://github.com/w3c/svgwg/issues/706 +@JS() +@staticInterop +class SVGMatrix { + static SVGMatrix identity() => _svgValueFactory.createSVGMatrix(); + + static SVGMatrix fromComponents({ + required double a, + required double b, + required double c, + required double d, + required double e, + required double f, + }) { + final SVGMatrix matrix = _svgValueFactory.createSVGMatrix(); + matrix.a = a; + matrix.b = b; + matrix.c = c; + matrix.d = d; + matrix.e = e; + matrix.f = f; + return matrix; + } + + /// SVGMatrix is a 2x3 matrix. + /// + /// In 3x3 matrix form assuming vector representation of (x, y, 1): + /// + /// a c e + /// b d f + /// 0 0 1 + /// + /// This translates to 4x4 matrix with vector representation of (x, y, z, 1) + /// as: + /// + /// a c 0 e + /// b d 0 f + /// 0 0 1 0 + /// 0 0 0 1 + /// + /// This matrix is sufficient to represent 2D rotates, translates, scales, + /// and skews. + static SVGMatrix fromMatrix4(Matrix4 other) { + return SVGMatrix.fromComponents( + a: other[0], + b: other[1], + c: other[4], + d: other[5], + e: other[12], + f: other[13], + ); + } +} +extension SVGMatrixExtension on SVGMatrix { + external double a; + external double b; + external double c; + external double d; + external double e; + external double f; + + external SVGMatrix multiply(SVGMatrix secondMatrix); + external SVGMatrix inverse(); + external SVGMatrix translate(double x, double y); + external SVGMatrix scale(double scaleFactor); + external SVGMatrix scaleNonUniform(double scaleFactorX, double scaleFactorY); + external SVGMatrix rotate(double angle); + external SVGMatrix rotateFromVector(double x, double y); + external SVGMatrix flipX(); + external SVGMatrix flipY(); + external SVGMatrix skewX(double angle); + external SVGMatrix skewY(double angle); +} diff --git a/lib/web_ui/lib/src/engine/svg_filter.dart b/lib/web_ui/lib/src/engine/svg_filter.dart new file mode 100644 index 0000000000000..44fb09b0592fa --- /dev/null +++ b/lib/web_ui/lib/src/engine/svg_filter.dart @@ -0,0 +1,831 @@ +// 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. + +import 'package:ui/ui.dart' as ui; + +import 'browser_detection.dart'; +import 'dom.dart'; +import 'svg.dart'; +import 'util.dart'; + +/// Used for clipping and filter svg resources. +/// +/// Position needs to be absolute since these svgs are sandwiched between +/// canvas elements and can cause layout shifts otherwise. +final SVGSVGElement kSvgResourceHeader = createSVGSVGElement() + ..setAttribute('width', 0) + ..setAttribute('height', 0) + ..style.position = 'absolute'; + +SvgFilter svgFilterFromBlendMode( + ui.Color? filterColor, ui.BlendMode colorFilterBlendMode) { + final SvgFilter svgFilter; + switch (colorFilterBlendMode) { + case ui.BlendMode.srcIn: + case ui.BlendMode.srcATop: + svgFilter = _srcInColorFilterToSvg(filterColor); + break; + case ui.BlendMode.srcOut: + svgFilter = _srcOutColorFilterToSvg(filterColor); + break; + case ui.BlendMode.dstATop: + svgFilter = _dstATopColorFilterToSvg(filterColor); + break; + case ui.BlendMode.xor: + svgFilter = _xorColorFilterToSvg(filterColor); + break; + case ui.BlendMode.plus: + // Porter duff source + destination. + svgFilter = _compositeColorFilterToSvg(filterColor, 0, 1, 1, 0); + break; + case ui.BlendMode.modulate: + // Porter duff source * destination but preserves alpha. + svgFilter = _modulateColorFilterToSvg(filterColor!); + break; + case ui.BlendMode.overlay: + // Since overlay is the same as hard-light by swapping layers, + // pass hard-light blend function. + svgFilter = _blendColorFilterToSvg( + filterColor, + blendModeToSvgEnum(ui.BlendMode.hardLight)!, + swapLayers: true, + ); + break; + // Several of the filters below (although supported) do not render the + // same (close but not exact) as native flutter when used as blend mode + // for a background-image with a background color. They only look + // identical when feBlend is used within an svg filter definition. + // + // Saturation filter uses destination when source is transparent. + // cMax = math.max(r, math.max(b, g)); + // cMin = math.min(r, math.min(b, g)); + // delta = cMax - cMin; + // lightness = (cMax + cMin) / 2.0; + // saturation = delta / (1.0 - (2 * lightness - 1.0).abs()); + case ui.BlendMode.saturation: + case ui.BlendMode.colorDodge: + case ui.BlendMode.colorBurn: + case ui.BlendMode.hue: + case ui.BlendMode.color: + case ui.BlendMode.luminosity: + case ui.BlendMode.multiply: + case ui.BlendMode.screen: + case ui.BlendMode.darken: + case ui.BlendMode.lighten: + case ui.BlendMode.hardLight: + case ui.BlendMode.softLight: + case ui.BlendMode.difference: + case ui.BlendMode.exclusion: + svgFilter = _blendColorFilterToSvg( + filterColor, blendModeToSvgEnum(colorFilterBlendMode)!); + break; + case ui.BlendMode.src: + case ui.BlendMode.dst: + case ui.BlendMode.dstIn: + case ui.BlendMode.dstOut: + case ui.BlendMode.dstOver: + case ui.BlendMode.clear: + case ui.BlendMode.srcOver: + throw UnimplementedError( + 'Blend mode not supported in HTML renderer: $colorFilterBlendMode', + ); + } + return svgFilter; +} + +// See: https://www.w3.org/TR/SVG11/types.html#InterfaceSVGUnitTypes +const int kUserSpaceOnUse = 1; +const int kObjectBoundingBox = 2; + +// See: https://www.w3.org/TR/SVG11/filters.html#InterfaceSVGFEColorMatrixElement +const int kMatrixType = 1; + +// See: https://www.w3.org/TR/SVG11/filters.html#InterfaceSVGFECompositeElement +const int kOperatorOut = 3; +const int kOperatorAtop = 4; +const int kOperatorXor = 5; +const int kOperatorArithmetic = 6; + +/// Configures an SVG filter for a specific target type. +/// +/// SVG filters needs to be configured differently depending on whether they are +/// applied to HTML or SVG. This enum communicates to [SvgFilterBuilder], +/// [SvgFilter], and other code constructing SVG filters what the indended +/// target is. +enum SvgFilterTargetType { + /// The target of the SVG filter is an SVG element. + svg, + + /// The target of the SVG filter is an HTML element. + html, +} + +/// Builds an [SvgFilter]. +class SvgFilterBuilder { + SvgFilterBuilder({ required SvgFilterTargetType targetType }) : id = '_fcf${++_filterIdCounter}' { + filter.id = id; + + switch (targetType) { + case SvgFilterTargetType.svg: + filter.filterUnits.baseVal = kUserSpaceOnUse; + break; + case SvgFilterTargetType.html: + // SVG filters that contain `` will fail on several browsers + // (e.g. Firefox) if bounds are not specified. + filter.filterUnits.baseVal = kObjectBoundingBox; + + // On Firefox percentage width/height 100% works however fails in Chrome 88. + filter.x.baseVal.valueAsString = '0%'; + filter.y.baseVal.valueAsString = '0%'; + filter.width.baseVal.valueAsString = '100%'; + filter.height.baseVal.valueAsString = '100%'; + break; + } + } + + static int _filterIdCounter = 0; + + final String id; + final SVGFilterElement filter = createSVGFilterElement(); + + set colorInterpolationFilters(String filters) { + filter.setAttribute('color-interpolation-filters', filters); + } + + void setFeGaussianBlur({ + required double sigmaX, + required double sigmaY, + required ui.Rect areaOfEffect, + String in1 = 'SourceGraphic', + String result = 'comp', + }) { + final SVGFEGaussianBlurElement element = createSVGFEGaussianBlurElement(); + element.in1.baseVal = in1; + element.stdDeviationX.baseVal = sigmaX; + element.stdDeviationY.baseVal = sigmaY; + element.result.baseVal = result; + filter.x.baseVal.valueAsString = '${areaOfEffect.left}'; + filter.y.baseVal.valueAsString = '${areaOfEffect.top}'; + filter.width.baseVal.valueAsString = '${areaOfEffect.width}'; + filter.height.baseVal.valueAsString = '${areaOfEffect.height}'; + filter.append(element); + } + + void setFeColorMatrix(List matrix, { required String result }) { + final SVGFEColorMatrixElement element = createSVGFEColorMatrixElement(); + element.type.baseVal = kMatrixType; + element.result.baseVal = result; + final SVGNumberList value = element.values.baseVal!; + for (int i = 0; i < matrix.length; i++) { + value.appendItem(SVGNumber.create(matrix[i])); + } + filter.append(element); + } + + void setFeFlood({ + required String floodColor, + required String floodOpacity, + required String result, + }) { + final SVGFEFloodElement element = createSVGFEFloodElement(); + element.setAttribute('flood-color', floodColor); + element.setAttribute('flood-opacity', floodOpacity); + element.result.baseVal = result; + filter.append(element); + } + + void setFeBlend({ + required String in1, + required String in2, + required int mode, + }) { + final SVGFEBlendElement element = createSVGFEBlendElement(); + element.in1.baseVal = in1; + element.in2.baseVal = in2; + element.mode.baseVal = mode; + filter.append(element); + } + + void setFeComposite({ + required String in1, + required String in2, + required int operator, + num? k1, + num? k2, + num? k3, + num? k4, + required String result, + }) { + final SVGFECompositeElement element = createSVGFECompositeElement(); + element.in1.baseVal = in1; + element.in2.baseVal = in2; + element.operator.baseVal = operator; + if (k1 != null) { + element.k1.baseVal = k1; + } + if (k2 != null) { + element.k2.baseVal = k2; + } + if (k3 != null) { + element.k3.baseVal = k3; + } + if (k4 != null) { + element.k4.baseVal = k4; + } + element.result.baseVal = result; + filter.append(element); + } + + void setFeImage({ + required String href, + required String result, + required double width, + required double height, + }) { + final SVGFEImageElement element = createSVGFEImageElement(); + element.href.baseVal = href; + element.result.baseVal = result; + + // WebKit will not render if x/y/width/height is specified. So we return + // explicit size here unless running on WebKit. + if (browserEngine != BrowserEngine.webkit) { + element.setX(0); + element.setY(0); + element.setWidth(width); + element.setHeight(height); + } + filter.append(element); + } + + SvgFilter build([SVGElement? host]) { + host ??= kSvgResourceHeader.cloneNode(false) as SVGSVGElement; + host.append(filter); + return SvgFilter._(id, host); + } +} + +class SvgFilter { + SvgFilter._(this.id, this.element); + + final String id; + final SVGElement element; + + void applyToSvg(SVGElement target) { + target.setAttribute('filter', 'url(#$id)'); + } +} + +SvgFilter svgFilterFromColorMatrix(List matrix) { + final SvgFilterBuilder builder = SvgFilterBuilder(targetType: SvgFilterTargetType.html); + builder.setFeColorMatrix(matrix, result: 'comp'); + return builder.build(); +} + +// The color matrix for feColorMatrix element changes colors based on +// the following: +// +// | R' | | r1 r2 r3 r4 r5 | | R | +// | G' | | g1 g2 g3 g4 g5 | | G | +// | B' | = | b1 b2 b3 b4 b5 | * | B | +// | A' | | a1 a2 a3 a4 a5 | | A | +// | 1 | | 0 0 0 0 1 | | 1 | +// +// R' = r1*R + r2*G + r3*B + r4*A + r5 +// G' = g1*R + g2*G + g3*B + g4*A + g5 +// B' = b1*R + b2*G + b3*B + b4*A + b5 +// A' = a1*R + a2*G + a3*B + a4*A + a5 +SvgFilter _srcInColorFilterToSvg(ui.Color? color) { + final SvgFilterBuilder builder = SvgFilterBuilder(targetType: SvgFilterTargetType.html); + builder.colorInterpolationFilters = 'sRGB'; + builder.setFeColorMatrix( + const [ + 0, 0, 0, 0, 1, + 0, 0, 0, 0, 1, + 0, 0, 0, 0, 1, + 0, 0, 0, 1, 0, + ], + result: 'destalpha', + ); + builder.setFeFlood( + floodColor: colorToCssString(color) ?? '', + floodOpacity: '1', + result: 'flood', + ); + builder.setFeComposite( + in1: 'flood', + in2: 'destalpha', + operator: kOperatorArithmetic, + k1: 1, + k2: 0, + k3: 0, + k4: 0, + result: 'comp', + ); + return builder.build(); +} + +/// The destination that overlaps the source is composited with the source and +/// replaces the destination. dst-atop CR = CB*αB*αA+CA*αA*(1-αB) αR=αA +SvgFilter _dstATopColorFilterToSvg(ui.Color? color) { + final SvgFilterBuilder builder = SvgFilterBuilder(targetType: SvgFilterTargetType.html); + builder.setFeFlood( + floodColor: colorToCssString(color) ?? '', + floodOpacity: '1', + result: 'flood', + ); + builder.setFeComposite( + in1: 'SourceGraphic', + in2: 'flood', + operator: kOperatorAtop, + result: 'comp', + ); + return builder.build(); +} + +SvgFilter _srcOutColorFilterToSvg(ui.Color? color) { + final SvgFilterBuilder builder = SvgFilterBuilder(targetType: SvgFilterTargetType.html); + builder.setFeFlood( + floodColor: colorToCssString(color) ?? '', + floodOpacity: '1', + result: 'flood', + ); + builder.setFeComposite( + in1: 'flood', + in2: 'SourceGraphic', + operator: kOperatorOut, + result: 'comp', + ); + return builder.build(); +} + +SvgFilter _xorColorFilterToSvg(ui.Color? color) { + final SvgFilterBuilder builder = SvgFilterBuilder(targetType: SvgFilterTargetType.html); + builder.setFeFlood( + floodColor: colorToCssString(color) ?? '', + floodOpacity: '1', + result: 'flood', + ); + builder.setFeComposite( + in1: 'flood', + in2: 'SourceGraphic', + operator: kOperatorXor, + result: 'comp', + ); + return builder.build(); +} + +// The source image and color are composited using : +// result = k1 *in*in2 + k2*in + k3*in2 + k4. +SvgFilter _compositeColorFilterToSvg( + ui.Color? color, double k1, double k2, double k3, double k4) { + final SvgFilterBuilder builder = SvgFilterBuilder(targetType: SvgFilterTargetType.html); + builder.setFeFlood( + floodColor: colorToCssString(color) ?? '', + floodOpacity: '1', + result: 'flood', + ); + builder.setFeComposite( + in1: 'flood', + in2: 'SourceGraphic', + operator: kOperatorArithmetic, + k1: k1, + k2: k2, + k3: k3, + k4: k4, + result: 'comp', + ); + return builder.build(); +} + +// Porter duff source * destination , keep source alpha. +// First apply color filter to source to change it to [color], then +// composite using multiplication. +SvgFilter _modulateColorFilterToSvg(ui.Color color) { + final double r = color.red / 255.0; + final double b = color.blue / 255.0; + final double g = color.green / 255.0; + + final SvgFilterBuilder builder = SvgFilterBuilder(targetType: SvgFilterTargetType.html); + builder.setFeColorMatrix( + [ + 0, 0, 0, 0, r, + 0, 0, 0, 0, g, + 0, 0, 0, 0, b, + 0, 0, 0, 1, 0, + ], + result: 'recolor', + ); + builder.setFeComposite( + in1: 'recolor', + in2: 'SourceGraphic', + operator: kOperatorArithmetic, + k1: 1, + k2: 0, + k3: 0, + k4: 0, + result: 'comp', + ); + return builder.build(); +} + +// Uses feBlend element to blend source image with a color. +SvgFilter _blendColorFilterToSvg(ui.Color? color, SvgBlendMode svgBlendMode, + {bool swapLayers = false}) { + final SvgFilterBuilder builder = SvgFilterBuilder(targetType: SvgFilterTargetType.html); + builder.setFeFlood( + floodColor: colorToCssString(color) ?? '', + floodOpacity: '1', + result: 'flood', + ); + if (swapLayers) { + builder.setFeBlend( + in1: 'SourceGraphic', + in2: 'flood', + mode: svgBlendMode.blendMode, + ); + } else { + builder.setFeBlend( + in1: 'flood', + in2: 'SourceGraphic', + mode: svgBlendMode.blendMode, + ); + } + return builder.build(); +} + + +// Source: https://www.w3.org/TR/SVG11/filters.html#InterfaceSVGFEBlendElement +// These constant names deviate from Dart's camelCase convention on purpose to +// make it easier to search for them in W3 specs and in Chromium sources. +const int SVG_FEBLEND_MODE_UNKNOWN = 0; +const int SVG_FEBLEND_MODE_NORMAL = 1; +const int SVG_FEBLEND_MODE_MULTIPLY = 2; +const int SVG_FEBLEND_MODE_SCREEN = 3; +const int SVG_FEBLEND_MODE_DARKEN = 4; +const int SVG_FEBLEND_MODE_LIGHTEN = 5; +const int SVG_FEBLEND_MODE_OVERLAY = 6; +const int SVG_FEBLEND_MODE_COLOR_DODGE = 7; +const int SVG_FEBLEND_MODE_COLOR_BURN = 8; +const int SVG_FEBLEND_MODE_HARD_LIGHT = 9; +const int SVG_FEBLEND_MODE_SOFT_LIGHT = 10; +const int SVG_FEBLEND_MODE_DIFFERENCE = 11; +const int SVG_FEBLEND_MODE_EXCLUSION = 12; +const int SVG_FEBLEND_MODE_HUE = 13; +const int SVG_FEBLEND_MODE_SATURATION = 14; +const int SVG_FEBLEND_MODE_COLOR = 15; +const int SVG_FEBLEND_MODE_LUMINOSITY = 16; + +// Source: https://github.com/chromium/chromium/blob/e1e495b29e1178a451f65980a6c4ae017c34dc94/third_party/blink/renderer/platform/graphics/graphics_types.cc#L55 +const String kCompositeClear = 'clear'; +const String kCompositeCopy = 'copy'; +const String kCompositeSourceOver = 'source-over'; +const String kCompositeSourceIn = 'source-in'; +const String kCompositeSourceOut = 'source-out'; +const String kCompositeSourceAtop = 'source-atop'; +const String kCompositeDestinationOver = 'destination-over'; +const String kCompositeDestinationIn = 'destination-in'; +const String kCompositeDestinationOut = 'destination-out'; +const String kCompositeDestinationAtop = 'destination-atop'; +const String kCompositeXor = 'xor'; +const String kCompositeLighter = 'lighter'; + +/// Compositing and blending operation in SVG. +/// +/// Flutter's [BlendMode] flattens what SVG expresses as two orthogonal +/// properties, a composite operator and blend mode. Instances of this class +/// are returned from [blendModeToSvgEnum] by mapping Flutter's [BlendMode] +/// enum onto the SVG equivalent. +/// +/// See also: +/// +/// * https://www.w3.org/TR/compositing-1 +/// * https://github.com/chromium/chromium/blob/e1e495b29e1178a451f65980a6c4ae017c34dc94/third_party/blink/renderer/platform/graphics/graphics_types.cc#L55 +/// * https://github.com/chromium/chromium/blob/e1e495b29e1178a451f65980a6c4ae017c34dc94/third_party/blink/renderer/modules/canvas/canvas2d/base_rendering_context_2d.cc#L725 +class SvgBlendMode { + const SvgBlendMode(this.compositeOperator, this.blendMode); + + /// The name of the SVG composite operator. + /// + /// If this mode represents a blend mode, this is set to [kCompositeSourceOver]. + final String compositeOperator; + + /// The identifier of the SVG blend mode. + /// + /// This is mode represents a compositing operation, this is set to [SVG_FEBLEND_MODE_UNKNOWN]. + final int blendMode; +} + +/// Converts Flutter's [ui.BlendMode] to SVG's pair. +SvgBlendMode? blendModeToSvgEnum(ui.BlendMode? blendMode) { + if (blendMode == null) { + return null; + } + switch (blendMode) { + case ui.BlendMode.clear: + return const SvgBlendMode(kCompositeClear, SVG_FEBLEND_MODE_UNKNOWN); + case ui.BlendMode.srcOver: + return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_UNKNOWN); + case ui.BlendMode.srcIn: + return const SvgBlendMode(kCompositeSourceIn, SVG_FEBLEND_MODE_UNKNOWN); + case ui.BlendMode.srcOut: + return const SvgBlendMode(kCompositeSourceOut, SVG_FEBLEND_MODE_UNKNOWN); + case ui.BlendMode.srcATop: + return const SvgBlendMode(kCompositeSourceAtop, SVG_FEBLEND_MODE_UNKNOWN); + case ui.BlendMode.dstOver: + return const SvgBlendMode(kCompositeDestinationOver, SVG_FEBLEND_MODE_UNKNOWN); + case ui.BlendMode.dstIn: + return const SvgBlendMode(kCompositeDestinationIn, SVG_FEBLEND_MODE_UNKNOWN); + case ui.BlendMode.dstOut: + return const SvgBlendMode(kCompositeDestinationOut, SVG_FEBLEND_MODE_UNKNOWN); + case ui.BlendMode.dstATop: + return const SvgBlendMode(kCompositeDestinationAtop, SVG_FEBLEND_MODE_UNKNOWN); + case ui.BlendMode.plus: + return const SvgBlendMode(kCompositeLighter, SVG_FEBLEND_MODE_UNKNOWN); + case ui.BlendMode.src: + return const SvgBlendMode(kCompositeCopy, SVG_FEBLEND_MODE_UNKNOWN); + case ui.BlendMode.xor: + return const SvgBlendMode(kCompositeXor, SVG_FEBLEND_MODE_UNKNOWN); + case ui.BlendMode.multiply: + // Falling back to multiply, ignoring alpha channel. + // TODO(ferhat): only used for debug, find better fallback for web. + case ui.BlendMode.modulate: + return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_MULTIPLY); + case ui.BlendMode.screen: + return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_SCREEN); + case ui.BlendMode.overlay: + return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_OVERLAY); + case ui.BlendMode.darken: + return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_DARKEN); + case ui.BlendMode.lighten: + return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_LIGHTEN); + case ui.BlendMode.colorDodge: + return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_COLOR_DODGE); + case ui.BlendMode.colorBurn: + return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_COLOR_BURN); + case ui.BlendMode.hardLight: + return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_HARD_LIGHT); + case ui.BlendMode.softLight: + return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_SOFT_LIGHT); + case ui.BlendMode.difference: + return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_DIFFERENCE); + case ui.BlendMode.exclusion: + return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_EXCLUSION); + case ui.BlendMode.hue: + return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_HUE); + case ui.BlendMode.saturation: + return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_SATURATION); + case ui.BlendMode.color: + return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_COLOR); + case ui.BlendMode.luminosity: + return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_LUMINOSITY); + default: + assert( + false, + 'Flutter Web does not support the blend mode: $blendMode', + ); + + return const SvgBlendMode(kCompositeSourceOver, SVG_FEBLEND_MODE_NORMAL); + } +} + + +SvgFilter svgMaskFilterFromImageAndBlendMode( + String imageUrl, ui.BlendMode blendMode, double width, double height) { + final SvgFilter svgFilter; + switch (blendMode) { + case ui.BlendMode.src: + svgFilter = _srcImageToSvg(imageUrl, width, height); + break; + case ui.BlendMode.srcIn: + case ui.BlendMode.srcATop: + svgFilter = _srcInImageToSvg(imageUrl, width, height); + break; + case ui.BlendMode.srcOut: + svgFilter = _srcOutImageToSvg(imageUrl, width, height); + break; + case ui.BlendMode.xor: + svgFilter = _xorImageToSvg(imageUrl, width, height); + break; + case ui.BlendMode.plus: + // Porter duff source + destination. + svgFilter = _compositeImageToSvg(imageUrl, 0, 1, 1, 0, width, height); + break; + case ui.BlendMode.modulate: + // Porter duff source * destination but preserves alpha. + svgFilter = _modulateImageToSvg(imageUrl, width, height); + break; + case ui.BlendMode.overlay: + // Since overlay is the same as hard-light by swapping layers, + // pass hard-light blend function. + svgFilter = _blendImageToSvg( + imageUrl, + blendModeToSvgEnum(ui.BlendMode.hardLight)!, + width, + height, + swapLayers: true, + ); + break; + // Several of the filters below (although supported) do not render the + // same (close but not exact) as native flutter when used as blend mode + // for a background-image with a background color. They only look + // identical when feBlend is used within an svg filter definition. + // + // Saturation filter uses destination when source is transparent. + // cMax = math.max(r, math.max(b, g)); + // cMin = math.min(r, math.min(b, g)); + // delta = cMax - cMin; + // lightness = (cMax + cMin) / 2.0; + // saturation = delta / (1.0 - (2 * lightness - 1.0).abs()); + case ui.BlendMode.saturation: + case ui.BlendMode.colorDodge: + case ui.BlendMode.colorBurn: + case ui.BlendMode.hue: + case ui.BlendMode.color: + case ui.BlendMode.luminosity: + case ui.BlendMode.multiply: + case ui.BlendMode.screen: + case ui.BlendMode.darken: + case ui.BlendMode.lighten: + case ui.BlendMode.hardLight: + case ui.BlendMode.softLight: + case ui.BlendMode.difference: + case ui.BlendMode.exclusion: + svgFilter = _blendImageToSvg( + imageUrl, blendModeToSvgEnum(blendMode)!, width, height); + break; + case ui.BlendMode.dst: + case ui.BlendMode.dstATop: + case ui.BlendMode.dstIn: + case ui.BlendMode.dstOut: + case ui.BlendMode.dstOver: + case ui.BlendMode.clear: + case ui.BlendMode.srcOver: + throw UnsupportedError( + 'Invalid svg filter request for blend-mode $blendMode'); + } + return svgFilter; +} + +// The color matrix for feColorMatrix element changes colors based on +// the following: +// +// | R' | | r1 r2 r3 r4 r5 | | R | +// | G' | | g1 g2 g3 g4 g5 | | G | +// | B' | = | b1 b2 b3 b4 b5 | * | B | +// | A' | | a1 a2 a3 a4 a5 | | A | +// | 1 | | 0 0 0 0 1 | | 1 | +// +// R' = r1*R + r2*G + r3*B + r4*A + r5 +// G' = g1*R + g2*G + g3*B + g4*A + g5 +// B' = b1*R + b2*G + b3*B + b4*A + b5 +// A' = a1*R + a2*G + a3*B + a4*A + a5 +SvgFilter _srcInImageToSvg(String imageUrl, double width, double height) { + final SvgFilterBuilder builder = SvgFilterBuilder(targetType: SvgFilterTargetType.html); + builder.setFeColorMatrix( + const [ + 0, 0, 0, 0, 1, + 0, 0, 0, 0, 1, + 0, 0, 0, 0, 1, + 0, 0, 0, 1, 0, + ], + result: 'destalpha', + ); + builder.setFeImage( + href: imageUrl, + result: 'image', + width: width, + height: height, + ); + builder.setFeComposite( + in1: 'image', + in2: 'destalpha', + operator: kOperatorArithmetic, + k1: 1, + k2: 0, + k3: 0, + k4: 0, + result: 'comp', + ); + return builder.build(); +} + +SvgFilter _srcImageToSvg(String imageUrl, double width, double height) { + final SvgFilterBuilder builder = SvgFilterBuilder(targetType: SvgFilterTargetType.html); + builder.setFeImage( + href: imageUrl, + result: 'comp', + width: width, + height: height, + ); + return builder.build(); +} + +SvgFilter _srcOutImageToSvg(String imageUrl, double width, double height) { + final SvgFilterBuilder builder = SvgFilterBuilder(targetType: SvgFilterTargetType.html); + builder.setFeImage( + href: imageUrl, + result: 'image', + width: width, + height: height, + ); + builder.setFeComposite( + in1: 'image', + in2: 'SourceGraphic', + operator: kOperatorOut, + result: 'comp', + ); + return builder.build(); +} + +SvgFilter _xorImageToSvg(String imageUrl, double width, double height) { + final SvgFilterBuilder builder = SvgFilterBuilder(targetType: SvgFilterTargetType.html); + builder.setFeImage( + href: imageUrl, + result: 'image', + width: width, + height: height, + ); + builder.setFeComposite( + in1: 'image', + in2: 'SourceGraphic', + operator: kOperatorXor, + result: 'comp', + ); + return builder.build(); +} + +// The source image and color are composited using : +// result = k1 *in*in2 + k2*in + k3*in2 + k4. +SvgFilter _compositeImageToSvg(String imageUrl, double k1, double k2, double k3, + double k4, double width, double height) { + final SvgFilterBuilder builder = SvgFilterBuilder(targetType: SvgFilterTargetType.html); + builder.setFeImage( + href: imageUrl, + result: 'image', + width: width, + height: height, + ); + builder.setFeComposite( + in1: 'image', + in2: 'SourceGraphic', + operator: kOperatorArithmetic, + k1: k1, + k2: k2, + k3: k3, + k4: k4, + result: 'comp', + ); + return builder.build(); +} + +// Porter duff source * destination , keep source alpha. +// First apply color filter to source to change it to [color], then +// composite using multiplication. +SvgFilter _modulateImageToSvg(String imageUrl, double width, double height) { + final SvgFilterBuilder builder = SvgFilterBuilder(targetType: SvgFilterTargetType.html); + builder.setFeImage( + href: imageUrl, + result: 'image', + width: width, + height: height, + ); + builder.setFeComposite( + in1: 'image', + in2: 'SourceGraphic', + operator: kOperatorArithmetic, + k1: 1, + k2: 0, + k3: 0, + k4: 0, + result: 'comp', + ); + return builder.build(); +} + +// Uses feBlend element to blend source image with a color. +SvgFilter _blendImageToSvg( + String imageUrl, SvgBlendMode svgBlendMode, double width, double height, + {bool swapLayers = false}) { + final SvgFilterBuilder builder = SvgFilterBuilder(targetType: SvgFilterTargetType.html); + builder.setFeImage( + href: imageUrl, + result: 'image', + width: width, + height: height, + ); + if (swapLayers) { + builder.setFeBlend( + in1: 'SourceGraphic', + in2: 'image', + mode: svgBlendMode.blendMode, + ); + } else { + builder.setFeBlend( + in1: 'image', + in2: 'SourceGraphic', + mode: svgBlendMode.blendMode, + ); + } + return builder.build(); +} diff --git a/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart b/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart index 272781147c78d..0240a3c55028a 100644 --- a/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart +++ b/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart @@ -2,12 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; import '../dom.dart'; import '../embedder.dart'; import '../html/bitmap_canvas.dart'; import '../profiler.dart'; +import '../svg.dart'; import '../util.dart'; import 'layout_fragmenter.dart'; import 'layout_service.dart'; @@ -185,6 +187,66 @@ class CanvasParagraph implements ui.Paragraph { return rootElement; } + + SVGGElement? _cachedSvgElement; + + /// Returns a SVG element that represents the entire paragraph and its + /// children. + /// + /// Generates a new SVG element on every invocation. + SVGGElement toSvgElement() { + assert(isLaidOut); + final SVGGElement? svgElement = _cachedSvgElement; + if (svgElement == null) { + return _cachedSvgElement ??= _createSvgElement(); + } + return svgElement.cloneNode(true) as SVGGElement; + } + + SVGGElement _createSvgElement() { + final SVGGElement rootElement = createSVGGElement(); + + // Prevent the browser from doing any line breaks in the paragraph. We want + // to have full control of the paragraph layout. + rootElement.style.whiteSpace = 'pre'; + + // Append all spans to the paragraph. + for (int i = 0; i < lines.length; i++) { + final ParagraphLine line = lines[i]; + for (final LayoutFragment fragment in line.fragments) { + if (fragment.isPlaceholder) { + continue; + } + + final String text = fragment.getText(this); + if (text.trim().isEmpty) { + continue; + } + + final SVGTextElement spanElement = createSVGTextElement(); + applyTextStyleToSvgElement(element: spanElement, style: fragment.style); + + final ui.Rect boxRect = fragment.toPaintingTextBox().toRect(); + spanElement.addX(boxRect.left); + // SVG places elements such that the text baseline coincides with + // the Y coordinate. However, Flutter positions the top edge of the text + // line at the Y coordinate. For the SVG text to match what Flutter + // expects, the baseline is added to the Y coordinate. + spanElement.addY(boxRect.top + line.baseline); + spanElement.style + // This is needed for space-only spans that are used to justify the paragraph. + ..width = '${boxRect.width}px' + // Makes sure the baseline of each span is positioned as expected. + ..lineHeight = '${boxRect.height}px'; + + spanElement.appendText(text); + rootElement.append(spanElement); + } + } + + return rootElement; + } + @override List getBoxesForPlaceholders() { return _layoutService.getBoxesForPlaceholders(); diff --git a/lib/web_ui/lib/src/engine/text/paragraph.dart b/lib/web_ui/lib/src/engine/text/paragraph.dart index c24b476290049..a73130af9dbca 100644 --- a/lib/web_ui/lib/src/engine/text/paragraph.dart +++ b/lib/web_ui/lib/src/engine/text/paragraph.dart @@ -867,6 +867,105 @@ void applyTextStyleToElement({ } } +/// Same as [applyTextStyleToSvgElement] but applies text style to text rendered +/// in SVG. +/// +/// There are some differences how text styles work between HTML and SVG. For +/// example, in HTML to specify font color you use `color`, but in SVG you use +/// `fill`. +void applyTextStyleToSvgElement({ + required DomElement element, + required EngineTextStyle style, +}) { + assert(element != null); + assert(style != null); + bool updateDecoration = false; + final DomCSSStyleDeclaration cssStyle = element.style; + + final ui.Color? color = style.foreground?.color ?? style.color; + if (style.foreground?.style == ui.PaintingStyle.stroke) { + // When comparing the outputs of the Bitmap Canvas and the DOM + // implementation, we have found, that we need to set the background color + // of the text to transparent to achieve the same effect as in the Bitmap + // Canvas and the Skia Engine where only the text stroke is painted. + // If we don't set it here to transparent, the text will inherit the color + // of it's parent element. + cssStyle.fill = 'transparent'; + // Use hairline (device pixel when strokeWidth is not specified). + final double? strokeWidth = style.foreground?.strokeWidth; + final double adaptedWidth = strokeWidth != null && strokeWidth > 0 + ? strokeWidth + : 1.0 / ui.window.devicePixelRatio; + cssStyle.textStroke = '${adaptedWidth}px ${colorToCssString(color)}'; + } else if (color != null) { + cssStyle.fill = colorToCssString(color)!; + } + final ui.Color? background = style.background?.color; + if (background != null) { + cssStyle.backgroundColor = colorToCssString(background)!; + } + final double? fontSize = style.fontSize; + if (fontSize != null) { + cssStyle.fontSize = '${fontSize.floor()}px'; + } + if (style.fontWeight != null) { + cssStyle.fontWeight = fontWeightToCss(style.fontWeight)!; + } + if (style.fontStyle != null) { + cssStyle.fontStyle = + style.fontStyle == ui.FontStyle.normal ? 'normal' : 'italic'; + } + // For test environment use effectiveFontFamily since we need to + // consistently use Ahem font. + if (ui.debugEmulateFlutterTesterEnvironment) { + cssStyle.fontFamily = canonicalizeFontFamily(style.effectiveFontFamily)!; + } else { + cssStyle.fontFamily = canonicalizeFontFamily(style.fontFamily)!; + } + if (style.letterSpacing != null) { + cssStyle.letterSpacing = '${style.letterSpacing}px'; + } + if (style.wordSpacing != null) { + cssStyle.wordSpacing = '${style.wordSpacing}px'; + } + if (style.decoration != null) { + updateDecoration = true; + } + final List? shadows = style.shadows; + if (shadows != null) { + cssStyle.textShadow = _shadowListToCss(shadows); + } + + if (updateDecoration) { + if (style.decoration != null) { + final String? textDecoration = + _textDecorationToCssString(style.decoration, style.decorationStyle); + if (textDecoration != null) { + if (browserEngine == BrowserEngine.webkit) { + setElementStyle(element, '-webkit-text-decoration', textDecoration); + } else { + cssStyle.textDecoration = textDecoration; + } + final ui.Color? decorationColor = style.decorationColor; + if (decorationColor != null) { + cssStyle.textDecorationColor = colorToCssString(decorationColor)!; + } + } + } + } + + final List? fontFeatures = style.fontFeatures; + if (fontFeatures != null && fontFeatures.isNotEmpty) { + cssStyle.fontFeatureSettings = _fontFeatureListToCss(fontFeatures); + } + + final List? fontVariations = style.fontVariations; + if (fontVariations != null && fontVariations.isNotEmpty) { + cssStyle.setProperty( + 'font-variation-settings', _fontVariationListToCss(fontVariations)); + } +} + String _shadowListToCss(List shadows) { if (shadows.isEmpty) { return ''; diff --git a/lib/web_ui/lib/src/engine/util.dart b/lib/web_ui/lib/src/engine/util.dart index 3f78e859e8286..cc57867220b0e 100644 --- a/lib/web_ui/lib/src/engine/util.dart +++ b/lib/web_ui/lib/src/engine/util.dart @@ -84,6 +84,17 @@ void setElementTransform(DomElement element, Float32List matrix4) { ..transform = float64ListToCssTransform(matrix4); } +/// Same as [setElementTransform] but specialized for translation along x and y. +void setElementTranslation(DomElement element, ui.Offset offset) { + if (offset == ui.Offset.zero) { + element.style.removeProperty('transform'); + } else { + element.style + ..transformOrigin = '0 0 0' + ..transform = 'translate(${offset.dx}px, ${offset.dy}px)'; + } +} + /// Converts [matrix] to CSS transform value. /// /// To avoid blurry text on some screens this function uses a 2D CSS transform @@ -170,6 +181,22 @@ TransformKind transformKindOf(List matrix) { } } +/// Adds an [offset] transformation to a [transform] matrix and returns the +/// combined result. +/// +/// If the given offset is zero, returns [transform] matrix as is. Otherwise, +/// returns a new [Matrix4] object representing the combined transformation. +Matrix4 transformWithOffset(Matrix4 transform, ui.Offset offset) { + if (offset == ui.Offset.zero) { + return transform; + } + + // Clone to avoid mutating transform. + final Matrix4 effectiveTransform = transform.clone(); + effectiveTransform.translate(offset.dx, offset.dy); + return effectiveTransform; +} + /// Returns `true` is the [matrix] describes an identity transformation. bool isIdentityFloat32ListTransform(Float32List matrix) { assert(matrix.length == 16); diff --git a/lib/web_ui/lib/src/engine/vector_math.dart b/lib/web_ui/lib/src/engine/vector_math.dart index 28fb5f70c6000..444c37dff5136 100644 --- a/lib/web_ui/lib/src/engine/vector_math.dart +++ b/lib/web_ui/lib/src/engine/vector_math.dart @@ -249,6 +249,25 @@ class Matrix4 { _m4storage[15] = t4; } + void translate2D(double x, double y) { + final double t1 = _m4storage[0] * x + + _m4storage[4] * y + + _m4storage[12]; + final double t2 = _m4storage[1] * x + + _m4storage[5] * y + + _m4storage[13]; + final double t3 = _m4storage[2] * x + + _m4storage[6] * y + + _m4storage[14]; + final double t4 = _m4storage[3] * x + + _m4storage[7] * y + + _m4storage[15]; + _m4storage[12] = t1; + _m4storage[13] = t2; + _m4storage[14] = t3; + _m4storage[15] = t4; + } + /// Scale this matrix by a [Vector3], [Vector4], or x,y,z void scale(double x, [double? y, double? z]) { final double sx = x; diff --git a/lib/web_ui/test/html/path_to_svg_golden_test.dart b/lib/web_ui/test/html/path_to_svg_golden_test.dart index 0356f1f1d30b8..6bb47027ee0eb 100644 --- a/lib/web_ui/test/html/path_to_svg_golden_test.dart +++ b/lib/web_ui/test/html/path_to_svg_golden_test.dart @@ -187,8 +187,8 @@ DomElement pathToSvgElement(Path path, Paint paint, bool enableFill) { final SVGSVGElement root = createSVGSVGElement(); root.style.transform = 'translate(200px, 0px)'; root.setAttribute('viewBox', '0 0 ${bounds.right} ${bounds.bottom}'); - root.width!.baseVal!.newValueSpecifiedUnits(svgLengthTypeNumber, bounds.right); - root.height!.baseVal!.newValueSpecifiedUnits(svgLengthTypeNumber, bounds.bottom); + root.setWidth(bounds.right); + root.setHeight(bounds.bottom); final SVGPathElement pathElement = createSVGPathElement(); root.append(pathElement);