Skip to content

Commit d4c1c17

Browse files
committed
Fixed transition on Safari
1 parent 561c204 commit d4c1c17

File tree

2 files changed

+107
-12
lines changed

2 files changed

+107
-12
lines changed

website/src/components/VisualElements/UShapeAttentionCurve.module.css

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@
3737
stroke-dasharray: 1000;
3838
stroke-dashoffset: 1000;
3939
animation: drawCurve 1.5s cubic-bezier(0.4, 0, 0.2, 1) forwards;
40-
/* Smooth transition for path morphing */
41-
transition: d 0.6s cubic-bezier(0.4, 0, 0.2, 1);
4240
}
4341

4442
@keyframes drawCurve {
@@ -51,8 +49,7 @@
5149
.areaFill {
5250
opacity: 0;
5351
animation: fadeInArea 0.8s ease-in 0.5s forwards;
54-
/* Smooth transition for path morphing */
55-
transition: d 0.6s cubic-bezier(0.4, 0, 0.2, 1);
52+
/* Path morphing handled by JavaScript */
5653
}
5754

5855
@keyframes fadeInArea {
@@ -120,9 +117,7 @@
120117
font-weight: 600;
121118
fill: var(--ifm-color-emphasis-700);
122119
/* Smooth transition for position changes */
123-
transition:
124-
x 0.6s cubic-bezier(0.4, 0, 0.2, 1),
125-
y 0.6s cubic-bezier(0.4, 0, 0.2, 1);
120+
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
126121
}
127122

128123
.axisLabelSmall {

website/src/components/VisualElements/UShapeAttentionCurve.tsx

Lines changed: 105 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from 'react';
1+
import React, { useState, useEffect, useRef } from 'react';
22
import styles from './UShapeAttentionCurve.module.css';
33

44
interface UShapeAttentionCurveProps {
@@ -9,11 +9,14 @@ export default function UShapeAttentionCurve({
99
initialContextFill = 60,
1010
}: UShapeAttentionCurveProps): JSX.Element {
1111
const [contextFill, setContextFill] = useState(initialContextFill);
12+
const [animatedPath, setAnimatedPath] = useState<string>('');
13+
const previousPathRef = useRef<string>('');
14+
const animationFrameRef = useRef<number | null>(null);
1215

1316
// SVG dimensions
1417
const width = 800;
1518
const height = 300;
16-
const padding = 60;
19+
const padding = 70;
1720

1821
// Calculate curve parameters based on context fill percentage
1922
// More context = deeper U (worse middle attention)
@@ -35,9 +38,105 @@ export default function UShapeAttentionCurve({
3538
C ${middleX + 100},${middleY} ${endX - 100},${startY} ${endX},${endY}
3639
`.trim();
3740

41+
// Animate path morphing for Safari compatibility
42+
useEffect(() => {
43+
// Initialize on first render
44+
if (!previousPathRef.current) {
45+
previousPathRef.current = curvePath;
46+
setAnimatedPath(curvePath);
47+
return;
48+
}
49+
50+
// If path hasn't changed, skip animation
51+
if (previousPathRef.current === curvePath) {
52+
return;
53+
}
54+
55+
// Parse path coordinates using regex
56+
const parsePathCoords = (path: string): number[] => {
57+
const matches = path.match(/[\d.]+/g);
58+
return matches ? matches.map(Number) : [];
59+
};
60+
61+
const startCoords = parsePathCoords(previousPathRef.current);
62+
const endCoords = parsePathCoords(curvePath);
63+
64+
// Animation parameters
65+
const duration = 600; // 600ms to match CSS timing
66+
const startTime = performance.now();
67+
68+
// Cubic bezier easing function matching CSS cubic-bezier(0.4, 0, 0.2, 1)
69+
const cubicBezier = (
70+
p1x: number,
71+
p1y: number,
72+
p2x: number,
73+
p2y: number
74+
) => {
75+
// Binary search to find t for given x
76+
const getTForX = (x: number): number => {
77+
let t = x;
78+
for (let i = 0; i < 8; i++) {
79+
const slope =
80+
3 * p1x * (1 - t) ** 2 +
81+
6 * (p2x - p1x) * t * (1 - t) +
82+
3 * (1 - p2x) * t ** 2;
83+
if (slope === 0) break;
84+
const currentX =
85+
3 * (1 - t) ** 2 * t * p1x + 3 * (1 - t) * t ** 2 * p2x + t ** 3;
86+
t -= (currentX - x) / slope;
87+
}
88+
return t;
89+
};
90+
91+
return (x: number): number => {
92+
if (x === 0 || x === 1) return x;
93+
const t = getTForX(x);
94+
return 3 * (1 - t) ** 2 * t * p1y + 3 * (1 - t) * t ** 2 * p2y + t ** 3;
95+
};
96+
};
97+
98+
const easing = cubicBezier(0.4, 0, 0.2, 1);
99+
100+
// Animation loop
101+
const animate = (currentTime: number) => {
102+
const elapsed = currentTime - startTime;
103+
const progress = Math.min(elapsed / duration, 1);
104+
const easedProgress = easing(progress);
105+
106+
// Interpolate coordinates
107+
const interpolatedCoords = startCoords.map((start, i) => {
108+
const end = endCoords[i];
109+
return start + (end - start) * easedProgress;
110+
});
111+
112+
// Reconstruct path string
113+
const [m1, m2, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12] =
114+
interpolatedCoords;
115+
const interpolatedPath = `M ${m1},${m2} C ${c1},${c2} ${c3},${c4} ${c5},${c6} C ${c7},${c8} ${c9},${c10} ${c11},${c12}`;
116+
117+
setAnimatedPath(interpolatedPath);
118+
119+
if (progress < 1) {
120+
animationFrameRef.current = requestAnimationFrame(animate);
121+
} else {
122+
previousPathRef.current = curvePath;
123+
}
124+
};
125+
126+
// Start animation
127+
animationFrameRef.current = requestAnimationFrame(animate);
128+
129+
// Cleanup on unmount or when curvePath changes
130+
return () => {
131+
if (animationFrameRef.current !== null) {
132+
cancelAnimationFrame(animationFrameRef.current);
133+
}
134+
};
135+
}, [curvePath]);
136+
38137
// Area fill path (curve + bottom edge for filled area)
39138
const areaPath = `
40-
${curvePath}
139+
${animatedPath || curvePath}
41140
L ${endX},${height - padding}
42141
L ${startX},${height - padding}
43142
Z
@@ -124,7 +223,7 @@ export default function UShapeAttentionCurve({
124223

125224
{/* The U-shaped curve line */}
126225
<path
127-
d={curvePath}
226+
d={animatedPath || curvePath}
128227
fill="none"
129228
stroke={`url(#${gradientId})`}
130229
strokeWidth="4"
@@ -223,9 +322,10 @@ export default function UShapeAttentionCurve({
223322
</text>
224323
<text
225324
x={padding - 40}
226-
y={middleY}
325+
y={padding}
227326
textAnchor="middle"
228327
className={styles.axisLabel}
328+
style={{ transform: `translateY(${middleY - padding}px)` }}
229329
>
230330
Low
231331
</text>

0 commit comments

Comments
 (0)