| /** | 
|  * @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 = {}; | 
|     } | 
| } |