blob: 71beada45618f4fd3f8b28c02ad7211f9ff212fc [file] [log] [blame]
// 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 '../canvaskit/color_filter.dart';
import '../color_filter.dart';
import '../dom.dart';
import '../embedder.dart';
import '../svg.dart';
import '../util.dart';
import 'bitmap_canvas.dart';
import 'path_to_svg_clip.dart';
import 'surface.dart';
/// A surface that applies an [ColorFilter] to its children.
class PersistedColorFilter extends PersistedContainerSurface
implements ui.ColorFilterEngineLayer {
PersistedColorFilter(PersistedColorFilter? oldLayer, this.filter)
: super(oldLayer);
@override
DomElement? get childContainer => _childContainer;
/// The dedicated child container element that's separate from the
/// [rootElement] is used to compensate for the coordinate system shift
/// introduced by the [rootElement] translation.
DomElement? _childContainer;
/// Color filter to apply to this surface.
final ui.ColorFilter filter;
DomElement? _filterElement;
bool containerVisible = true;
@override
void adoptElements(PersistedColorFilter oldSurface) {
super.adoptElements(oldSurface);
_childContainer = oldSurface._childContainer;
_filterElement = oldSurface._filterElement;
oldSurface._childContainer = null;
}
@override
void preroll(PrerollSurfaceContext prerollContext) {
++prerollContext.activeColorFilterCount;
super.preroll(prerollContext);
--prerollContext.activeColorFilterCount;
}
@override
void discard() {
super.discard();
flutterViewEmbedder.removeResource(_filterElement as html.Element?);
// 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
DomElement createElement() {
final DomElement element = defaultCreateElement('flt-color-filter');
final DomElement container = createDomElement('flt-filter-interior');
container.style.position = 'absolute';
_childContainer = container;
element.append(_childContainer!);
return element;
}
@override
void apply() {
flutterViewEmbedder.removeResource(_filterElement as html.Element?);
_filterElement = null;
final EngineColorFilter? engineValue = filter as EngineColorFilter?;
if (engineValue == null) {
rootElement!.style.backgroundColor = '';
childContainer?.style.visibility = 'visible';
return;
}
if (engineValue is CkBlendModeColorFilter) {
_applyBlendModeFilter(engineValue);
} else if (engineValue is CkMatrixColorFilter) {
_applyMatrixColorFilter(engineValue);
} else {
childContainer?.style.visibility = 'visible';
}
}
void _applyBlendModeFilter(CkBlendModeColorFilter colorFilter) {
final ui.Color filterColor = colorFilter.color;
ui.BlendMode colorFilterBlendMode = colorFilter.blendMode;
final DomCSSStyleDeclaration style = childContainer!.style;
switch (colorFilterBlendMode) {
case ui.BlendMode.clear:
case ui.BlendMode.dstOut:
case ui.BlendMode.srcOut:
style.visibility = 'hidden';
return;
case ui.BlendMode.dst:
case ui.BlendMode.dstIn:
// Noop.
return;
case ui.BlendMode.src:
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.
colorFilterBlendMode = ui.BlendMode.srcIn;
break;
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;
}
// Use SVG filter for blend mode.
final SvgFilter svgFilter = svgFilterFromBlendMode(filterColor, colorFilterBlendMode);
_filterElement = svgFilter.element;
flutterViewEmbedder.addResource(_filterElement! as html.Element);
style.filter = 'url(#${svgFilter.id})';
if (colorFilterBlendMode == ui.BlendMode.saturation ||
colorFilterBlendMode == ui.BlendMode.multiply ||
colorFilterBlendMode == ui.BlendMode.modulate) {
style.backgroundColor = colorToCssString(filterColor)!;
}
}
void _applyMatrixColorFilter(CkMatrixColorFilter colorFilter) {
final SvgFilter svgFilter = svgFilterFromColorMatrix(colorFilter.matrix);
_filterElement = svgFilter.element;
flutterViewEmbedder.addResource(_filterElement! as html.Element);
childContainer!.style.filter = 'url(#${svgFilter.id})';
}
@override
void update(PersistedColorFilter oldSurface) {
super.update(oldSurface);
if (oldSurface.filter != filter) {
apply();
}
}
}
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 {
static int _filterIdCounter = 0;
SvgFilterBuilder() : id = '_fcf${++_filterIdCounter}' {
filter.id = id;
// SVG filters that contain `<feImage>` 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%';
}
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<double> 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<double> 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 <double>[
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(
<double>[
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();
}