/**
|
* @file Manages SVG shadow elements.
|
* @author Zhang Wenli
|
*/
|
|
import Definable from './Definable';
|
import Displayable from '../../graphic/Displayable';
|
import { Dictionary } from '../../core/types';
|
import { getIdURL, getShadowKey, hasShadow, normalizeColor } from '../../svg/helper';
|
import { createElement } from '../../svg/core';
|
|
type DisplayableExtended = Displayable & {
|
_shadowDom: SVGElement
|
}
|
/**
|
* Manages SVG shadow elements.
|
*
|
*/
|
export default class ShadowManager extends Definable {
|
|
private _shadowDomMap: Dictionary<SVGFilterElement> = {}
|
private _shadowDomPool: SVGFilterElement[] = []
|
|
constructor(zrId: number, svgRoot: SVGElement) {
|
super(zrId, svgRoot, ['filter'], '__filter_in_use__', '_shadowDom');
|
}
|
|
/**
|
* Add a new shadow tag in <defs>
|
*
|
* @param displayable zrender displayable element
|
* @return created DOM
|
*/
|
private _getFromPool(): SVGFilterElement {
|
let shadowDom = this._shadowDomPool.pop(); // Try to get one from trash.
|
if (!shadowDom) {
|
shadowDom = createElement('filter') as SVGFilterElement;
|
shadowDom.setAttribute('id', 'zr' + this._zrId + '-shadow-' + this.nextId++);
|
const domChild = createElement('feDropShadow');
|
shadowDom.appendChild(domChild);
|
this.addDom(shadowDom);
|
}
|
|
return shadowDom;
|
}
|
|
|
/**
|
* Update shadow.
|
*/
|
update(svgElement: SVGElement, displayable: Displayable) {
|
const style = displayable.style;
|
if (hasShadow(style)) {
|
// Try getting shadow from cache.
|
const shadowKey = getShadowKey(displayable);
|
let shadowDom = (displayable as DisplayableExtended)._shadowDom = this._shadowDomMap[shadowKey];
|
if (!shadowDom) {
|
shadowDom = this._getFromPool();
|
this._shadowDomMap[shadowKey] = shadowDom;
|
}
|
this.updateDom(svgElement, displayable, shadowDom);
|
}
|
else {
|
// Remove shadow
|
this.remove(svgElement, displayable);
|
}
|
}
|
|
|
/**
|
* Remove DOM and clear parent filter
|
*/
|
remove(svgElement: SVGElement, displayable: Displayable) {
|
if ((displayable as DisplayableExtended)._shadowDom != null) {
|
(displayable as DisplayableExtended)._shadowDom = null;
|
svgElement.removeAttribute('filter');
|
}
|
}
|
|
|
/**
|
* Update shadow dom
|
*
|
* @param displayable zrender displayable element
|
* @param shadowDom DOM to update
|
*/
|
updateDom(svgElement: SVGElement, displayable: Displayable, shadowDom: SVGElement) {
|
let domChild = shadowDom.children[0];
|
|
const style = displayable.style;
|
const globalScale = displayable.getGlobalScale();
|
const scaleX = globalScale[0];
|
const scaleY = globalScale[1];
|
if (!scaleX || !scaleY) {
|
return;
|
}
|
|
// TODO: textBoxShadowBlur is not supported yet
|
const offsetX = style.shadowOffsetX || 0;
|
const offsetY = style.shadowOffsetY || 0;
|
const blur = style.shadowBlur;
|
const normalizedColor = normalizeColor(style.shadowColor);
|
|
domChild.setAttribute('dx', offsetX / scaleX + '');
|
domChild.setAttribute('dy', offsetY / scaleY + '');
|
domChild.setAttribute('flood-color', normalizedColor.color);
|
domChild.setAttribute('flood-opacity', normalizedColor.opacity + '');
|
|
// Divide by two here so that it looks the same as in canvas
|
// See: https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-shadowblur
|
const stdDx = blur / 2 / scaleX;
|
const stdDy = blur / 2 / scaleY;
|
const stdDeviation = stdDx + ' ' + stdDy;
|
domChild.setAttribute('stdDeviation', stdDeviation);
|
|
// Fix filter clipping problem
|
shadowDom.setAttribute('x', '-100%');
|
shadowDom.setAttribute('y', '-100%');
|
shadowDom.setAttribute('width', '300%');
|
shadowDom.setAttribute('height', '300%');
|
|
// Store dom element in shadow, to avoid creating multiple
|
// dom instances for the same shadow element
|
(displayable as DisplayableExtended)._shadowDom = shadowDom;
|
|
svgElement.setAttribute('filter', getIdURL(shadowDom.getAttribute('id')));
|
}
|
|
removeUnused() {
|
const defs = this.getDefs(false);
|
if (!defs) {
|
// Nothing to remove
|
return;
|
}
|
let shadowDomsPool = this._shadowDomPool;
|
|
// let currentUsedShadow = 0;
|
const shadowDomMap = this._shadowDomMap;
|
for (let key in shadowDomMap) {
|
if (shadowDomMap.hasOwnProperty(key)) {
|
shadowDomsPool.push(shadowDomMap[key]);
|
}
|
// currentUsedShadow++;
|
}
|
|
// Reset the map.
|
this._shadowDomMap = {};
|
}
|
}
|