SVG: Allow setting filters using JSX (#5449)

This commit is contained in:
zubiden 2025-01-21 18:21:10 +01:00 committed by Alexander Zinchuk
parent af472d0fd4
commit 8dc935d0b8
9 changed files with 184 additions and 201 deletions

View File

@ -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 = ({
<DeleteFolderDialog folder={deleteFolderDialog} />
<ReactionPicker isOpen={isReactionPickerOpen} />
<DeleteMessageModal isOpen={isDeleteMessageModalOpen} />
<SvgController />
</div>
);
};

View File

@ -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<string, React.ReactElement>();
const CALLBACK_MANAGER = createCallbackManager();
const SvgController = () => {
const forceUpdate = useForceUpdate();
useEffect(() => {
return CALLBACK_MANAGER.addCallback(forceUpdate);
}, []);
return (
<Portal>
<svg width="0" height="0" viewBox="0 0 1 1" className="svg-definitions">
<defs>
{Array.from(DEFINITION_MAP.values())}
</defs>
</svg>
</Portal>
);
};
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();
}

View File

@ -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 = (
<svg
className={styles.ghost}
width={width}
height={height}
style={`left: ${x}px; top: ${y}px;`}
viewBox={`0 0 ${width} ${height}`}
>
<defs>
{createFilter(filterId, Math.min(width, height), seed)}
</defs>
<g filter={`url(#${filterId})`}>
<foreignObject className={styles.elementContainer} width={width} height={height} />
</g>
</svg>
);
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 (
<filter
xmlns={SVG_NAMESPACE}
id={id}
x="-150%"
y="-150%"
width="400%"
height="400%"
color-interpolation-filters="sRGB"
>
<feTurbulence type="fractalNoise" baseFrequency="0.5" numOctaves="1" result="dustNoise" seed={baseSeed} />
<feComponentTransfer in="dustNoise" result="dustNoiseMask">
<feFuncA type="linear" slope="5" intercept="0">
<animate attributeName="slope" values="5; 2; 1; 0" dur={`${DURATION}ms`} fill="freeze" />
</feFuncA>
</feComponentTransfer>
<feComposite in="SourceGraphic" in2="dustNoiseMask" operator="in" result="dustySource" />
<feTurbulence
type="fractalNoise"
baseFrequency="0.015"
numOctaves="1"
result="displacementNoise1"
seed={baseSeed + 1}
/>
<feTurbulence
type="fractalNoise"
baseFrequency="1"
numOctaves="2"
result="displacementNoise2"
seed={baseSeed + 2}
/>
<feMerge result="combinedNoise">
<feMergeNode in="displacementNoise1" />
<feMergeNode in="displacementNoise2" />
</feMerge>
<feDisplacementMap in="dustySource" in2="combinedNoise" scale="0" xChannelSelector="R" yChannelSelector="G">
<animate attributeName="scale" values={`0; ${smallestSide * 3}`} dur={`${DURATION}ms`} fill="freeze" />
</feDisplacementMap>
</filter>
);
}

View File

@ -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 x="0" y="0" width="1" height="1" color-interpolation-filters="sRGB" xmlns={SVG_NAMESPACE}>
<feImage href={waveRipple} result="waveImage" />
<feDisplacementMap
in="SourceGraphic"
in2="waveImage"
scale={FILTER_SCALE}
xChannelSelector="R"
yChannelSelector="B"
/>
</filter>,
FILTER_ID,
);
return () => {
removeSvgDefinition(FILTER_ID);

View File

@ -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';

View File

@ -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<string, SvgColorFilter>();
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(
<filter color-interpolation-filters="sRGB" xmlns={SVG_NAMESPACE}>
<feColorMatrix
type="matrix"
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`}
/>
</filter>,
this.filterId,
);
filter.appendChild(feColorMatrix);
}
public getFilterId() {

View File

@ -39,6 +39,9 @@ type CurrentContext = Record<string, Signal<unknown>>;
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<T extends VirtualElement | undefined>(
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<T extends VirtualElement | undefined>(
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<T extends VirtualElement | undefined>(
}
mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, currentContext, {
nextSibling, fragment, isSvg,
nextSibling, fragment, namespace,
});
} else {
const canSetTextContent = !fragment
@ -157,7 +161,7 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
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<T extends VirtualElement | undefined>(
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<T extends VirtualElement | undefined>(
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;
}

View File

@ -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;
}

View File

@ -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<string, SVGElement>();
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);
}
}