From 8dc935d0b86eaf8bf9cf3c01af8e72681d076f3b Mon Sep 17 00:00:00 2001 From: zubiden <19638254+zubiden@users.noreply.github.com> Date: Tue, 21 Jan 2025 18:21:10 +0100 Subject: [PATCH] SVG: Allow setting filters using JSX (#5449) --- src/components/main/Main.tsx | 2 + src/components/main/SvgController.tsx | 45 +++++ .../visualEffects/SnapEffectContainer.tsx | 171 +++++++----------- .../main/visualEffects/WaveContainer.tsx | 37 ++-- src/config.ts | 2 + .../{useColorFilter.ts => useColorFilter.tsx} | 26 ++- src/lib/teact/teact-dom.ts | 58 +++--- src/util/element/jsxToHtml.ts | 12 ++ src/util/svgController.ts | 32 ---- 9 files changed, 184 insertions(+), 201 deletions(-) create mode 100644 src/components/main/SvgController.tsx rename src/hooks/stickers/{useColorFilter.ts => useColorFilter.tsx} (68%) create mode 100644 src/util/element/jsxToHtml.ts delete mode 100644 src/util/svgController.ts diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index b212045e6..4ac2680fa 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -86,6 +86,7 @@ import GiveawayModal from './premium/GiveawayModal.async'; import PremiumMainModal from './premium/PremiumMainModal.async'; import StarsGiftingPickerModal from './premium/StarsGiftingPickerModal.async'; import SafeLinkModal from './SafeLinkModal.async'; +import SvgController from './SvgController'; import ConfettiContainer from './visualEffects/ConfettiContainer'; import SnapEffectContainer from './visualEffects/SnapEffectContainer'; import WaveContainer from './visualEffects/WaveContainer'; @@ -587,6 +588,7 @@ const Main = ({ + ); }; diff --git a/src/components/main/SvgController.tsx b/src/components/main/SvgController.tsx new file mode 100644 index 000000000..725c77aa6 --- /dev/null +++ b/src/components/main/SvgController.tsx @@ -0,0 +1,45 @@ +import React, { memo, useEffect } from '../../lib/teact/teact'; + +import { createCallbackManager } from '../../util/callbacks'; +import generateUniqueId from '../../util/generateUniqueId'; + +import useForceUpdate from '../../hooks/useForceUpdate'; + +import Portal from '../ui/Portal'; + +const DEFINITION_MAP = new Map(); +const CALLBACK_MANAGER = createCallbackManager(); + +const SvgController = () => { + const forceUpdate = useForceUpdate(); + + useEffect(() => { + return CALLBACK_MANAGER.addCallback(forceUpdate); + }, []); + + return ( + + + + {Array.from(DEFINITION_MAP.values())} + + + + ); +}; + +export default memo(SvgController); + +export function addSvgDefinition(element: React.ReactElement, id?: string) { + id ??= generateUniqueId(); + element.props.id = id; + + DEFINITION_MAP.set(element.props.id, element); + CALLBACK_MANAGER.runCallbacks(); + return id; +} + +export function removeSvgDefinition(id: string) { + DEFINITION_MAP.delete(id); + CALLBACK_MANAGER.runCallbacks(); +} diff --git a/src/components/main/visualEffects/SnapEffectContainer.tsx b/src/components/main/visualEffects/SnapEffectContainer.tsx index a960f9a49..4a811f611 100644 --- a/src/components/main/visualEffects/SnapEffectContainer.tsx +++ b/src/components/main/visualEffects/SnapEffectContainer.tsx @@ -1,10 +1,10 @@ import React, { memo } from '../../../lib/teact/teact'; import { getGlobal } from '../../../global'; -import { SNAP_EFFECT_CONTAINER_ID, SNAP_EFFECT_ID } from '../../../config'; +import { SNAP_EFFECT_CONTAINER_ID, SNAP_EFFECT_ID, SVG_NAMESPACE } from '../../../config'; import { selectCanAnimateSnapEffect } from '../../../global/selectors'; +import jsxToHtml from '../../../util/element/jsxToHtml'; import generateUniqueId from '../../../util/generateUniqueId'; -import { SVG_NAMESPACE } from '../../../util/svgController'; import styles from './SnapEffectContainer.module.scss'; @@ -39,29 +39,25 @@ export function animateSnap(element: HTMLElement) { const seed = Math.floor(Date.now() / 1000); const filterId = `${SNAP_EFFECT_ID}-${generateUniqueId()}`; - const svg = document.createElementNS(SVG_NAMESPACE, 'svg'); - svg.setAttribute('class', styles.ghost); - svg.setAttribute('width', `${width}px`); - svg.setAttribute('height', `${height}px`); - svg.setAttribute('style', `left: ${x}px; top: ${y}px;`); - svg.setAttribute('viewBox', `0 0 ${width} ${height}`); + const component = ( + + + {createFilter(filterId, Math.min(width, height), seed)} + + + + + + ); - const defs = document.createElementNS(SVG_NAMESPACE, 'defs'); - svg.appendChild(defs); - - const filter = createFilter(Math.min(width, height), seed); - filter.setAttribute('id', filterId); - defs.appendChild(filter); - - const g = document.createElementNS(SVG_NAMESPACE, 'g'); - g.setAttribute('filter', `url(#${filterId})`); - svg.appendChild(g); - - const foreignObject = document.createElementNS(SVG_NAMESPACE, 'foreignObject'); - foreignObject.setAttribute('class', styles.elementContainer); - foreignObject.setAttribute('width', `${width}px`); - foreignObject.setAttribute('height', `${height}px`); - g.appendChild(foreignObject); + const svg = jsxToHtml(component)[0] as HTMLElement; + const foreignObject = svg.querySelector('foreignObject')!; const computedStyle = window.getComputedStyle(element); const clone = element.cloneNode(true) as HTMLElement; @@ -83,90 +79,45 @@ export function animateSnap(element: HTMLElement) { return true; } -function createFilter(smallestSide: number, baseSeed: number = 42) { - const filter = document.createElementNS(SVG_NAMESPACE, 'filter'); - filter.setAttribute('x', '-150%'); - filter.setAttribute('y', '-150%'); - filter.setAttribute('width', '400%'); - filter.setAttribute('height', '400%'); - filter.setAttribute('color-interpolation-filters', 'sRGB'); - - const feTurbulence = document.createElementNS(SVG_NAMESPACE, 'feTurbulence'); - feTurbulence.setAttribute('type', 'fractalNoise'); - feTurbulence.setAttribute('baseFrequency', '0.5'); - feTurbulence.setAttribute('numOctaves', '1'); - feTurbulence.setAttribute('result', 'dustNoise'); - feTurbulence.setAttribute('seed', baseSeed.toString()); - filter.appendChild(feTurbulence); - - const feComponentTransfer = document.createElementNS(SVG_NAMESPACE, 'feComponentTransfer'); - feComponentTransfer.setAttribute('in', 'dustNoise'); - feComponentTransfer.setAttribute('result', 'dustNoiseMask'); - filter.appendChild(feComponentTransfer); - - const feFuncA = document.createElementNS(SVG_NAMESPACE, 'feFuncA'); - feFuncA.setAttribute('type', 'linear'); - feFuncA.setAttribute('slope', '5'); - feFuncA.setAttribute('intercept', '0'); - feComponentTransfer.appendChild(feFuncA); - - const feFuncAAnimate = document.createElementNS(SVG_NAMESPACE, 'animate'); - feFuncAAnimate.setAttribute('attributeName', 'slope'); - feFuncAAnimate.setAttribute('values', '5; 2; 1; 0'); - feFuncAAnimate.setAttribute('dur', `${DURATION}ms`); - feFuncAAnimate.setAttribute('fill', 'freeze'); - feFuncA.appendChild(feFuncAAnimate); - - const feComposite = document.createElementNS(SVG_NAMESPACE, 'feComposite'); - feComposite.setAttribute('in', 'SourceGraphic'); - feComposite.setAttribute('in2', 'dustNoiseMask'); - feComposite.setAttribute('operator', 'in'); - feComposite.setAttribute('result', 'dustySource'); - filter.appendChild(feComposite); - - const feTurbulence2 = document.createElementNS(SVG_NAMESPACE, 'feTurbulence'); - feTurbulence2.setAttribute('type', 'fractalNoise'); - feTurbulence2.setAttribute('baseFrequency', '0.015'); - feTurbulence2.setAttribute('numOctaves', '1'); - feTurbulence2.setAttribute('result', 'displacementNoice1'); - feTurbulence2.setAttribute('seed', (baseSeed + 1).toString()); - filter.appendChild(feTurbulence2); - - const feTurbulence3 = document.createElementNS(SVG_NAMESPACE, 'feTurbulence'); - feTurbulence3.setAttribute('type', 'fractalNoise'); - feTurbulence3.setAttribute('baseFrequency', '1'); - feTurbulence3.setAttribute('numOctaves', '2'); - feTurbulence3.setAttribute('result', 'displacementNoice2'); - feTurbulence3.setAttribute('seed', (baseSeed + 2).toString()); - filter.appendChild(feTurbulence3); - - const feMerge = document.createElementNS(SVG_NAMESPACE, 'feMerge'); - feMerge.setAttribute('result', 'combinedNoise'); - filter.appendChild(feMerge); - - const feMergeNode1 = document.createElementNS(SVG_NAMESPACE, 'feMergeNode'); - feMergeNode1.setAttribute('in', 'displacementNoice1'); - feMerge.appendChild(feMergeNode1); - - const feMergeNode2 = document.createElementNS(SVG_NAMESPACE, 'feMergeNode'); - feMergeNode2.setAttribute('in', 'displacementNoice2'); - feMerge.appendChild(feMergeNode2); - - const feDisplacementMap = document.createElementNS(SVG_NAMESPACE, 'feDisplacementMap'); - feDisplacementMap.setAttribute('in', 'dustySource'); - feDisplacementMap.setAttribute('in2', 'combinedNoise'); - feDisplacementMap.setAttribute('scale', '0'); - - feDisplacementMap.setAttribute('xChannelSelector', 'R'); - feDisplacementMap.setAttribute('yChannelSelector', 'G'); - filter.appendChild(feDisplacementMap); - - const feDisplacementMapAnimate = document.createElementNS(SVG_NAMESPACE, 'animate'); - feDisplacementMapAnimate.setAttribute('attributeName', 'scale'); - feDisplacementMapAnimate.setAttribute('values', `0; ${smallestSide * 3}`); - feDisplacementMapAnimate.setAttribute('dur', `${DURATION}ms`); - feDisplacementMapAnimate.setAttribute('fill', 'freeze'); - feDisplacementMap.appendChild(feDisplacementMapAnimate); - - return filter; +function createFilter(id: string, smallestSide: number, baseSeed: number = 42) { + return ( + + + + + + + + + + + + + + + + + + + ); } diff --git a/src/components/main/visualEffects/WaveContainer.tsx b/src/components/main/visualEffects/WaveContainer.tsx index 469f9254d..15623c1ac 100644 --- a/src/components/main/visualEffects/WaveContainer.tsx +++ b/src/components/main/visualEffects/WaveContainer.tsx @@ -5,14 +5,16 @@ import { withGlobal } from '../../../global'; import type { TabState } from '../../../global/types'; +import { SVG_NAMESPACE } from '../../../config'; import { selectTabState } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; import buildStyle from '../../../util/buildStyle'; -import { addSvgDefinition, removeSvgDefinition, SVG_NAMESPACE } from '../../../util/svgController'; import windowSize from '../../../util/windowSize'; import useLastCallback from '../../../hooks/useLastCallback'; +import { addSvgDefinition, removeSvgDefinition } from '../SvgController'; + import styles from './WaveContainer.module.scss'; import waveRipple from '../../../assets/wave_ripple.svg'; @@ -62,26 +64,19 @@ const WaveContainer = ({ waveInfo }: StateProps) => { }, [waveInfo]); useEffect(() => { - const filter = document.createElementNS(SVG_NAMESPACE, 'filter'); - filter.setAttribute('x', '0'); - filter.setAttribute('y', '0'); - filter.setAttribute('width', '1'); - filter.setAttribute('height', '1'); - filter.setAttribute('color-interpolation-filters', 'sRGB'); - addSvgDefinition(filter, FILTER_ID); - - const feImage = document.createElementNS(SVG_NAMESPACE, 'feImage'); - feImage.setAttribute('href', waveRipple); - feImage.setAttribute('result', 'waveImage'); - filter.appendChild(feImage); - - const feDisplacementMap = document.createElementNS(SVG_NAMESPACE, 'feDisplacementMap'); - feDisplacementMap.setAttribute('in', 'SourceGraphic'); - feDisplacementMap.setAttribute('in2', 'waveImage'); - feDisplacementMap.setAttribute('scale', FILTER_SCALE); - feDisplacementMap.setAttribute('xChannelSelector', 'R'); - feDisplacementMap.setAttribute('yChannelSelector', 'B'); - filter.appendChild(feDisplacementMap); + addSvgDefinition( + + + + , + FILTER_ID, + ); return () => { removeSvgDefinition(FILTER_ID); diff --git a/src/config.ts b/src/config.ts index 306a54007..6bb314f42 100644 --- a/src/config.ts +++ b/src/config.ts @@ -227,6 +227,8 @@ export const SLIDE_TRANSITION_DURATION = 450; export const BIRTHDAY_NUMBERS_SET = 'FestiveFontEmoji'; export const RESTRICTED_EMOJI_SET = 'RestrictedEmoji'; +export const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; + export const VIDEO_WEBM_TYPE = 'video/webm'; export const GIF_MIME_TYPE = 'image/gif'; diff --git a/src/hooks/stickers/useColorFilter.ts b/src/hooks/stickers/useColorFilter.tsx similarity index 68% rename from src/hooks/stickers/useColorFilter.ts rename to src/hooks/stickers/useColorFilter.tsx index 34e0901ff..e4ec3075e 100644 --- a/src/hooks/stickers/useColorFilter.ts +++ b/src/hooks/stickers/useColorFilter.tsx @@ -1,8 +1,10 @@ -import { useEffect } from '../../lib/teact/teact'; +import React, { useEffect } from '../../lib/teact/teact'; -import { addSvgDefinition, removeSvgDefinition, SVG_NAMESPACE } from '../../util/svgController'; +import { SVG_NAMESPACE } from '../../config'; import { hexToRgb } from '../../util/switchTheme'; +import { addSvgDefinition, removeSvgDefinition } from '../../components/main/SvgController'; + const SVG_MAP = new Map(); class SvgColorFilter { @@ -13,20 +15,16 @@ class SvgColorFilter { constructor(public color: string) { this.filterId = `color-filter-${color.slice(1)}`; - const filter = document.createElementNS(SVG_NAMESPACE, 'filter'); - filter.setAttribute('color-interpolation-filters', 'sRGB'); - addSvgDefinition(filter, this.filterId); - - const feColorMatrix = document.createElementNS(SVG_NAMESPACE, 'feColorMatrix'); - feColorMatrix.setAttribute('type', 'matrix'); - const rgbColor = hexToRgb(color); - feColorMatrix.setAttribute( - 'values', - `0 0 0 0 ${rgbColor.r / 255} 0 0 0 0 ${rgbColor.g / 255} 0 0 0 0 ${rgbColor.b / 255} 0 0 0 1 0`, + addSvgDefinition( + + + , + this.filterId, ); - - filter.appendChild(feColorMatrix); } public getFilterId() { diff --git a/src/lib/teact/teact-dom.ts b/src/lib/teact/teact-dom.ts index 33e1a08e3..ddd69aa4b 100644 --- a/src/lib/teact/teact-dom.ts +++ b/src/lib/teact/teact-dom.ts @@ -39,6 +39,9 @@ type CurrentContext = Record>; type DOMElement = HTMLElement | SVGElement; +const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; +const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'; + const FILTERED_ATTRIBUTES = new Set(['key', 'ref', 'teactFastList', 'teactOrderKey']); const HTML_ATTRIBUTES = new Set(['dir', 'role', 'form']); const CONTROLLABLE_TAGS = ['INPUT', 'TEXTAREA', 'SELECT']; @@ -89,11 +92,11 @@ function renderWithVirtual( nextSibling?: ChildNode; forceMoveToEnd?: boolean; fragment?: DocumentFragment; - isSvg?: true; + namespace?: string; } = {}, ): T { const { skipComponentUpdate, fragment } = options; - let { nextSibling, isSvg } = options; + let { nextSibling, namespace } = options; const isCurrentComponent = $current?.type === VirtualType.Component; const isNewComponent = $new?.type === VirtualType.Component; @@ -102,8 +105,9 @@ function renderWithVirtual( const isCurrentFragment = !isCurrentComponent && $current?.type === VirtualType.Fragment; const isNewFragment = !isNewComponent && $new?.type === VirtualType.Fragment; - if (!isSvg && $new?.type === VirtualType.Tag && $new.tag === 'svg') { - isSvg = true; + if ($new?.type === VirtualType.Tag) { + if ($new.tag === 'svg') namespace = SVG_NAMESPACE; + if ($new.props.xmlns) namespace = $new.props.xmlns; } if ( @@ -144,7 +148,7 @@ function renderWithVirtual( } mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, currentContext, { - nextSibling, fragment, isSvg, + nextSibling, fragment, namespace, }); } else { const canSetTextContent = !fragment @@ -157,7 +161,7 @@ function renderWithVirtual( parentEl.textContent = $newAsReal.value; $newAsReal.target = parentEl.firstChild!; } else { - const node = createNode($newAsReal, currentContext, isSvg); + const node = createNode($newAsReal, currentContext, namespace); $newAsReal.target = node; insertBefore(fragment || parentEl, node, nextSibling); @@ -184,10 +188,10 @@ function renderWithVirtual( remount(parentEl, $current, currentContext, undefined); mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, currentContext, { - nextSibling, fragment, isSvg, + nextSibling, fragment, namespace, }); } else { - const node = createNode($newAsReal, currentContext, isSvg); + const node = createNode($newAsReal, currentContext, namespace); $newAsReal.target = node; remount(parentEl, $current, currentContext, node, nextSibling); @@ -226,8 +230,10 @@ function renderWithVirtual( insertBefore(parentEl, currentTarget, nextSibling); } - updateAttributes($current, $newAsTag, currentTarget as DOMElement, isSvg); - renderChildren($current, $newAsTag, currentContext, currentTarget as DOMElement, undefined, undefined, isSvg); + updateAttributes($current, $newAsTag, currentTarget as DOMElement, namespace); + renderChildren( + $current, $newAsTag, currentContext, currentTarget as DOMElement, undefined, undefined, namespace, + ); } } } @@ -290,7 +296,7 @@ function mountChildren( options: { nextSibling?: ChildNode; fragment?: DocumentFragment; - isSvg?: true; + namespace?: string; }, ) { const { children } = $element; @@ -311,7 +317,7 @@ function unmountChildren( } } -function createNode($element: VirtualElementReal, currentContext: CurrentContext, isSvg?: true): Node { +function createNode($element: VirtualElementReal, currentContext: CurrentContext, namespace = HTML_NAMESPACE): Node { if ($element.type === VirtualType.Empty) { return document.createTextNode(''); } @@ -321,7 +327,7 @@ function createNode($element: VirtualElementReal, currentContext: CurrentContext } const { tag, props, children } = $element; - const element = isSvg ? document.createElementNS('http://www.w3.org/2000/svg', tag) : document.createElement(tag); + const element = document.createElementNS(namespace, tag) as DOMElement; processControlled(tag, props); @@ -330,7 +336,7 @@ function createNode($element: VirtualElementReal, currentContext: CurrentContext if (!props.hasOwnProperty(key)) continue; if (props[key] !== undefined) { - setAttribute(element, key, props[key], isSvg); + setAttribute(element, key, props[key], namespace); } } @@ -338,7 +344,7 @@ function createNode($element: VirtualElementReal, currentContext: CurrentContext for (let i = 0, l = children.length; i < l; i++) { const $child = children[i]; - const $renderedChild = renderWithVirtual(element, undefined, $child, $element, currentContext, i, { isSvg }); + const $renderedChild = renderWithVirtual(element, undefined, $child, $element, currentContext, i, { namespace }); if ($renderedChild !== $child) { children[i] = $renderedChild; } @@ -424,7 +430,7 @@ function renderChildren( currentEl: DOMElement, nextSibling?: ChildNode, forceMoveToEnd = false, - isSvg?: true, + namespace?: string, ) { if (DEBUG) { DEBUG_checkKeyUniqueness($new.children); @@ -456,7 +462,7 @@ function renderChildren( $new, currentContext, i, - i >= currentChildrenLength ? { fragment, isSvg } : { nextSibling, forceMoveToEnd, isSvg }, + i >= currentChildrenLength ? { fragment, namespace } : { nextSibling, forceMoveToEnd, namespace }, ); if ($renderedChild && $renderedChild !== newChildren[i]) { @@ -691,7 +697,9 @@ function processUncontrolledOnMount(element: DOMElement, props: AnyLiteral) { } } -function updateAttributes($current: VirtualElementTag, $new: VirtualElementTag, element: DOMElement, isSvg?: true) { +function updateAttributes( + $current: VirtualElementTag, $new: VirtualElementTag, element: DOMElement, namespace?: string, +) { processControlled(element.tagName, $new.props); const currentEntries = Object.entries($current.props); @@ -715,14 +723,14 @@ function updateAttributes($current: VirtualElementTag, $new: VirtualElementTag, const currentValue = $current.props[key]; if (newValue !== undefined && newValue !== currentValue) { - setAttribute(element, key, newValue, isSvg); + setAttribute(element, key, newValue, namespace); } } } -function setAttribute(element: DOMElement, key: string, value: any, isSvg?: true) { +function setAttribute(element: DOMElement, key: string, value: any, namespace?: string) { if (key === 'className') { - updateClassName(element, value, isSvg); + updateClassName(element, value, namespace); } else if (key === 'value') { const inputEl = element as HTMLInputElement; @@ -749,7 +757,9 @@ function setAttribute(element: DOMElement, key: string, value: any, isSvg?: true element.innerHTML = value.__html; } else if (key.startsWith('on')) { addEventListener(element, key, value, key.endsWith('Capture')); - } else if (isSvg || key.startsWith('data-') || key.startsWith('aria-') || HTML_ATTRIBUTES.has(key)) { + } else if ( + namespace === SVG_NAMESPACE || key.startsWith('data-') || key.startsWith('aria-') || HTML_ATTRIBUTES.has(key) + ) { element.setAttribute(key, value); } else if (!FILTERED_ATTRIBUTES.has(key)) { (element as any)[MAPPED_ATTRIBUTES[key] || key] = value; @@ -772,8 +782,8 @@ function removeAttribute(element: DOMElement, key: string, value: any) { } } -function updateClassName(element: DOMElement, value: string, isSvg?: true) { - if (isSvg) { +function updateClassName(element: DOMElement, value: string, namespace?: string) { + if (namespace === SVG_NAMESPACE) { element.setAttribute('class', value); return; } diff --git a/src/util/element/jsxToHtml.ts b/src/util/element/jsxToHtml.ts new file mode 100644 index 000000000..c5d05146f --- /dev/null +++ b/src/util/element/jsxToHtml.ts @@ -0,0 +1,12 @@ +import type { VirtualElement } from '../../lib/teact/teact'; +import TeactDOM from '../../lib/teact/teact-dom'; + +export default function jsxToHtml(jsx: VirtualElement) { + const fragment = document.createElement('div'); + TeactDOM.render(jsx, fragment); + + const children = Array.from(fragment.children); + TeactDOM.render(undefined, fragment); + + return children; +} diff --git a/src/util/svgController.ts b/src/util/svgController.ts deleted file mode 100644 index 927f6b28d..000000000 --- a/src/util/svgController.ts +++ /dev/null @@ -1,32 +0,0 @@ -import generateUniqueId from './generateUniqueId'; - -export const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; - -const CONTAINER = document.createElementNS(SVG_NAMESPACE, 'svg'); -CONTAINER.setAttribute('width', '0'); -CONTAINER.setAttribute('height', '0'); -CONTAINER.setAttribute('viewBox', '0 0 1 1'); -CONTAINER.classList.add('svg-definitions'); -document.body.appendChild(CONTAINER); - -const DEFS = document.createElementNS(SVG_NAMESPACE, 'defs'); -CONTAINER.appendChild(DEFS); - -const DEFINITION_MAP = new Map(); - -export function addSvgDefinition(element: SVGElement, id?: string) { - id ??= generateUniqueId(); - element.id = id; - - DEFS.appendChild(element); - DEFINITION_MAP.set(element.id, element); - return id; -} - -export function removeSvgDefinition(id: string) { - const element = DEFINITION_MAP.get(id); - if (element) { - element.remove(); - DEFINITION_MAP.delete(id); - } -}