| // 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:html' as html; |
| |
| 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 'shaders/shader.dart'; |
| import 'surface.dart'; |
| |
| /// A surface that applies a shader to its children. |
| /// |
| /// Currently there are 2 types of shaders: |
| /// - Gradients |
| /// - ImageShader |
| /// |
| /// Gradients |
| /// The gradients can be applied to the child tree by rendering the gradient |
| /// into an image and referencing the image in an svg filter to apply |
| /// to DOM tree. |
| /// |
| class PersistedShaderMask extends PersistedContainerSurface |
| implements ui.ShaderMaskEngineLayer { |
| PersistedShaderMask( |
| PersistedShaderMask? oldLayer, |
| this.shader, |
| this.maskRect, |
| this.blendMode, |
| this.filterQuality, |
| ) : super(oldLayer); |
| |
| DomElement? _childContainer; |
| final ui.Shader shader; |
| final ui.Rect maskRect; |
| final ui.BlendMode blendMode; |
| final ui.FilterQuality filterQuality; |
| html.Element? _shaderElement; |
| final bool isWebKit = browserEngine == BrowserEngine.webkit; |
| |
| @override |
| void adoptElements(PersistedShaderMask oldSurface) { |
| super.adoptElements(oldSurface); |
| _childContainer = oldSurface._childContainer; |
| _shaderElement = oldSurface._shaderElement; |
| oldSurface._childContainer = null; |
| oldSurface._shaderElement = null; |
| } |
| |
| @override |
| DomElement? get childContainer => _childContainer; |
| |
| @override |
| void discard() { |
| super.discard(); |
| flutterViewEmbedder.removeResource(_shaderElement); |
| // Do not detach the child container from the root. It is permanently |
| // attached. The elements are reused together and are detached from the DOM |
| // together. |
| _childContainer = null; |
| } |
| |
| @override |
| void preroll(PrerollSurfaceContext prerollContext) { |
| ++prerollContext.activeShaderMaskCount; |
| super.preroll(prerollContext); |
| --prerollContext.activeShaderMaskCount; |
| } |
| |
| @override |
| DomElement createElement() { |
| final DomElement element = defaultCreateElement('flt-shader-mask'); |
| final DomElement container = createDomElement('flt-mask-interior'); |
| container.style.position = 'absolute'; |
| _childContainer = container; |
| element.append(_childContainer!); |
| return element; |
| } |
| |
| @override |
| void apply() { |
| flutterViewEmbedder.removeResource(_shaderElement); |
| _shaderElement = null; |
| if (shader is ui.Gradient) { |
| rootElement!.style |
| ..left = '${maskRect.left}px' |
| ..top = '${maskRect.top}px' |
| ..width = '${maskRect.width}px' |
| ..height = '${maskRect.height}px'; |
| _childContainer!.style |
| ..left = '${-maskRect.left}px' |
| ..top = '${-maskRect.top}px'; |
| // Prevent ShaderMask from failing inside animations that size |
| // area to empty. |
| if (maskRect.width > 0 && maskRect.height > 0) { |
| _applyGradientShader(); |
| } |
| return; |
| } |
| // TODO(ferhat): Implement _applyImageShader(); |
| throw Exception('Shader type not supported for ShaderMask'); |
| } |
| |
| void _applyGradientShader() { |
| if (shader is EngineGradient) { |
| final EngineGradient gradientShader = shader as EngineGradient; |
| |
| // The gradient shader's bounds are in the context of the element itself, |
| // rather than the global position, so translate it back to the origin. |
| final ui.Rect translatedRect = |
| maskRect.translate(-maskRect.left, -maskRect.top); |
| final String imageUrl = |
| gradientShader.createImageBitmap(translatedRect, 1, true) as String; |
| ui.BlendMode blendModeTemp = blendMode; |
| switch (blendModeTemp) { |
| case ui.BlendMode.clear: |
| case ui.BlendMode.dstOut: |
| case ui.BlendMode.srcOut: |
| childContainer?.style.visibility = 'hidden'; |
| return; |
| case ui.BlendMode.dst: |
| case ui.BlendMode.dstIn: |
| // Noop. Should render existing destination. |
| rootElement!.style.filter = ''; |
| return; |
| case ui.BlendMode.srcOver: |
| // Uses source filter color. |
| // Since we don't have a size, we can't use background color. |
| // Use svg filter srcIn instead. |
| blendModeTemp = ui.BlendMode.srcIn; |
| break; |
| case ui.BlendMode.src: |
| case ui.BlendMode.dstOver: |
| case ui.BlendMode.srcIn: |
| case ui.BlendMode.srcATop: |
| case ui.BlendMode.dstATop: |
| case ui.BlendMode.xor: |
| case ui.BlendMode.plus: |
| case ui.BlendMode.modulate: |
| case ui.BlendMode.screen: |
| case ui.BlendMode.overlay: |
| case ui.BlendMode.darken: |
| case ui.BlendMode.lighten: |
| case ui.BlendMode.colorDodge: |
| case ui.BlendMode.colorBurn: |
| case ui.BlendMode.hardLight: |
| case ui.BlendMode.softLight: |
| case ui.BlendMode.difference: |
| case ui.BlendMode.exclusion: |
| case ui.BlendMode.multiply: |
| case ui.BlendMode.hue: |
| case ui.BlendMode.saturation: |
| case ui.BlendMode.color: |
| case ui.BlendMode.luminosity: |
| break; |
| } |
| |
| final SvgFilter svgFilter = svgMaskFilterFromImageAndBlendMode( |
| imageUrl, blendModeTemp, maskRect.width, maskRect.height); |
| _shaderElement = svgFilter.element as html.Element; |
| if (isWebKit) { |
| _childContainer!.style.filter = 'url(#${svgFilter.id})'; |
| } else { |
| rootElement!.style.filter = 'url(#${svgFilter.id})'; |
| } |
| flutterViewEmbedder.addResource(_shaderElement!); |
| } |
| } |
| |
| @override |
| void update(PersistedShaderMask oldSurface) { |
| super.update(oldSurface); |
| if (shader != oldSurface.shader || |
| maskRect != oldSurface.maskRect || |
| blendMode != oldSurface.blendMode) { |
| apply(); |
| } |
| } |
| } |
| |
| 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 <double>[ |
| 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(); |
| } |