Skip to content

Commit b6b0dff

Browse files
committed
Добавлен компонент ElectricBorder с анимацией и стилями для создания электрической рамки
1 parent 537fd85 commit b6b0dff

File tree

2 files changed

+313
-0
lines changed

2 files changed

+313
-0
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
.electric-border {
2+
--electric-light-color: oklch(from var(--electric-border-color) l c h);
3+
--eb-border-width: 2px;
4+
position: relative;
5+
border-radius: inherit;
6+
overflow: visible;
7+
isolation: isolate;
8+
}
9+
10+
.eb-svg {
11+
position: fixed;
12+
left: -10000px;
13+
top: -10000px;
14+
width: 10px;
15+
height: 10px;
16+
opacity: 0.001;
17+
pointer-events: none;
18+
}
19+
20+
.eb-content {
21+
position: relative;
22+
border-radius: inherit;
23+
z-index: 1;
24+
}
25+
26+
.eb-layers {
27+
position: absolute;
28+
inset: 0;
29+
border-radius: inherit;
30+
pointer-events: none;
31+
z-index: 2;
32+
}
33+
34+
.eb-stroke,
35+
.eb-glow-1,
36+
.eb-glow-2,
37+
.eb-overlay-1,
38+
.eb-overlay-2,
39+
.eb-background-glow {
40+
position: absolute;
41+
inset: 0;
42+
border-radius: inherit;
43+
pointer-events: none;
44+
box-sizing: border-box;
45+
}
46+
47+
.eb-stroke {
48+
border: var(--eb-border-width) solid var(--electric-border-color);
49+
}
50+
51+
.eb-glow-1 {
52+
border: var(--eb-border-width) solid
53+
oklch(from var(--electric-border-color) l c h / 0.6);
54+
opacity: 0.5;
55+
filter: blur(calc(0.5px + (var(--eb-border-width) * 0.25)));
56+
}
57+
58+
.eb-glow-2 {
59+
border: var(--eb-border-width) solid var(--electric-light-color);
60+
opacity: 0.5;
61+
filter: blur(calc(2px + (var(--eb-border-width) * 0.5)));
62+
}
63+
64+
.eb-background-glow {
65+
z-index: -1;
66+
transform: scale(1.08);
67+
filter: blur(32px);
68+
opacity: 0.3;
69+
background: linear-gradient(
70+
-30deg,
71+
var(--electric-light-color),
72+
transparent,
73+
var(--electric-border-color)
74+
);
75+
}
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import "./ElectricBorder.css";
2+
3+
import {
4+
CSSProperties,
5+
PropsWithChildren,
6+
useCallback,
7+
useEffect,
8+
useId,
9+
useLayoutEffect,
10+
useRef,
11+
} from "react";
12+
13+
type ElectricBorderProps = PropsWithChildren<{
14+
color?: string;
15+
speed?: number;
16+
chaos?: number;
17+
thickness?: number;
18+
className?: string;
19+
style?: CSSProperties;
20+
}>;
21+
22+
const ElectricBorder: React.FC<ElectricBorderProps> = ({
23+
children,
24+
color = "#5227FF",
25+
speed = 1,
26+
chaos = 1,
27+
thickness = 2,
28+
className,
29+
style,
30+
}: ElectricBorderProps) => {
31+
const rawId = useId().replace(/[:]/g, "");
32+
const filterId = `turbulent-displace-${rawId}`;
33+
const svgRef = useRef<SVGSVGElement | null>(null);
34+
const rootRef = useRef<HTMLDivElement | null>(null);
35+
const strokeRef = useRef<HTMLDivElement | null>(null);
36+
37+
const updateAnim = useCallback(() => {
38+
const svg = svgRef.current;
39+
const host = rootRef.current;
40+
if (!svg || !host) return;
41+
42+
if (strokeRef.current) {
43+
strokeRef.current.style.filter = `url(#${filterId})`;
44+
}
45+
46+
const width = Math.max(
47+
1,
48+
Math.round(host.clientWidth || host.getBoundingClientRect().width || 0)
49+
);
50+
const height = Math.max(
51+
1,
52+
Math.round(host.clientHeight || host.getBoundingClientRect().height || 0)
53+
);
54+
55+
const dyAnims = Array.from(
56+
svg.querySelectorAll<SVGAnimateElement>(
57+
'feOffset > animate[attributeName="dy"]'
58+
)
59+
);
60+
if (dyAnims.length >= 2) {
61+
dyAnims[0].setAttribute("values", `${height}; 0`);
62+
dyAnims[1].setAttribute("values", `0; -${height}`);
63+
}
64+
65+
const dxAnims = Array.from(
66+
svg.querySelectorAll<SVGAnimateElement>(
67+
'feOffset > animate[attributeName="dx"]'
68+
)
69+
);
70+
if (dxAnims.length >= 2) {
71+
dxAnims[0].setAttribute("values", `${width}; 0`);
72+
dxAnims[1].setAttribute("values", `0; -${width}`);
73+
}
74+
75+
const baseDur = 6;
76+
const dur = Math.max(0.001, baseDur / (speed || 1));
77+
[...dyAnims, ...dxAnims].forEach(a => a.setAttribute("dur", `${dur}s`));
78+
79+
const disp = svg.querySelector("feDisplacementMap");
80+
if (disp) disp.setAttribute("scale", String(30 * (chaos || 1)));
81+
82+
const filterEl = svg.querySelector<SVGFilterElement>(
83+
"#" + CSS.escape(filterId)
84+
);
85+
if (filterEl) {
86+
filterEl.setAttribute("x", "-200%");
87+
filterEl.setAttribute("y", "-200%");
88+
filterEl.setAttribute("width", "500%");
89+
filterEl.setAttribute("height", "500%");
90+
}
91+
92+
requestAnimationFrame(() => {
93+
[...dyAnims, ...dxAnims].forEach((a: SVGAnimateElement) => {
94+
const anim = a as unknown as { beginElement?: () => void };
95+
if (typeof anim.beginElement === "function") {
96+
try {
97+
anim.beginElement();
98+
} catch {
99+
// ignore
100+
}
101+
}
102+
});
103+
});
104+
}, [chaos, filterId, speed]);
105+
106+
useEffect(() => {
107+
updateAnim();
108+
}, [updateAnim]);
109+
110+
useLayoutEffect(() => {
111+
if (!rootRef.current) return;
112+
const ro = new ResizeObserver(() => updateAnim());
113+
ro.observe(rootRef.current);
114+
updateAnim();
115+
return () => ro.disconnect();
116+
}, [updateAnim]);
117+
118+
const vars = {
119+
"--electric-border-color": color,
120+
"--eb-border-width": `${thickness}px`,
121+
} as CSSProperties;
122+
123+
return (
124+
<div
125+
ref={rootRef}
126+
className={`electric-border ${className ?? ""}`}
127+
style={{ ...vars, ...style }}
128+
>
129+
<svg ref={svgRef} className="eb-svg" aria-hidden focusable="false">
130+
<defs>
131+
<filter
132+
id={filterId}
133+
colorInterpolationFilters="sRGB"
134+
x="-20%"
135+
y="-20%"
136+
width="140%"
137+
height="140%"
138+
>
139+
<feTurbulence
140+
type="turbulence"
141+
baseFrequency="0.02"
142+
numOctaves="10"
143+
result="noise1"
144+
seed="1"
145+
/>
146+
<feOffset in="noise1" dx="0" dy="0" result="offsetNoise1">
147+
<animate
148+
attributeName="dy"
149+
values="700; 0"
150+
dur="6s"
151+
repeatCount="indefinite"
152+
calcMode="linear"
153+
/>
154+
</feOffset>
155+
156+
<feTurbulence
157+
type="turbulence"
158+
baseFrequency="0.02"
159+
numOctaves="10"
160+
result="noise2"
161+
seed="1"
162+
/>
163+
<feOffset in="noise2" dx="0" dy="0" result="offsetNoise2">
164+
<animate
165+
attributeName="dy"
166+
values="0; -700"
167+
dur="6s"
168+
repeatCount="indefinite"
169+
calcMode="linear"
170+
/>
171+
</feOffset>
172+
173+
<feTurbulence
174+
type="turbulence"
175+
baseFrequency="0.02"
176+
numOctaves="10"
177+
result="noise1"
178+
seed="2"
179+
/>
180+
<feOffset in="noise1" dx="0" dy="0" result="offsetNoise3">
181+
<animate
182+
attributeName="dx"
183+
values="490; 0"
184+
dur="6s"
185+
repeatCount="indefinite"
186+
calcMode="linear"
187+
/>
188+
</feOffset>
189+
190+
<feTurbulence
191+
type="turbulence"
192+
baseFrequency="0.02"
193+
numOctaves="10"
194+
result="noise2"
195+
seed="2"
196+
/>
197+
<feOffset in="noise2" dx="0" dy="0" result="offsetNoise4">
198+
<animate
199+
attributeName="dx"
200+
values="0; -490"
201+
dur="6s"
202+
repeatCount="indefinite"
203+
calcMode="linear"
204+
/>
205+
</feOffset>
206+
207+
<feComposite in="offsetNoise1" in2="offsetNoise2" result="part1" />
208+
<feComposite in="offsetNoise3" in2="offsetNoise4" result="part2" />
209+
<feBlend
210+
in="part1"
211+
in2="part2"
212+
mode="color-dodge"
213+
result="combinedNoise"
214+
/>
215+
<feDisplacementMap
216+
in="SourceGraphic"
217+
in2="combinedNoise"
218+
scale="30"
219+
xChannelSelector="R"
220+
yChannelSelector="B"
221+
/>
222+
</filter>
223+
</defs>
224+
</svg>
225+
226+
<div className="eb-layers">
227+
<div ref={strokeRef} className="eb-stroke" />
228+
<div className="eb-glow-1" />
229+
<div className="eb-glow-2" />
230+
<div className="eb-background-glow" />
231+
</div>
232+
233+
<div className="eb-content">{children}</div>
234+
</div>
235+
);
236+
};
237+
238+
export default ElectricBorder;

0 commit comments

Comments
 (0)