Skip to content

Commit d4c50c4

Browse files
committed
Add animation support to all Curve components
1 parent be7d875 commit d4c50c4

File tree

5 files changed

+93
-95
lines changed

5 files changed

+93
-95
lines changed

src/components/CompositeCurve/CompositeCurve.tsx

Lines changed: 19 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,28 @@
1-
import {
2-
useEffect,
3-
useMemo,
4-
useState,
5-
useCallback,
6-
type CSSProperties
7-
} from 'react'
1+
import { useEffect, useMemo, useState, useCallback } from 'react'
82
import {
93
calcBiQuadCoefficients,
104
calcCompositeMagnitudes,
115
calcMagnitudes
126
} from '../../math'
137
import { type GraphFilter, type Magnitude } from '../../types'
148
import { useGraph, FrequencyResponseCurve } from '..'
9+
import type { DefaultCurveProps } from '../types'
1510

1611
const getFilterKey = (filter: GraphFilter) =>
1712
`${filter.type}_${filter.freq}_${filter.q}_${filter.gain}`
1813

19-
type CompositeCurveProps = {
14+
type CompositeCurveProps = DefaultCurveProps & {
2015
/**
2116
* Array of filters to combine into a single frequency response curve
2217
*/
2318
filters: GraphFilter[]
24-
/**
25-
* Whether to render the curve with a dotted/dashed line style
26-
* @default false
27-
*/
28-
dotted?: boolean
29-
/**
30-
* Composite curve color
31-
* @default theme.curve.color || '#FFFFFF'
32-
*/
33-
color?: string
34-
/**
35-
* Composite curve opacity
36-
* @default theme.curve.opacity || 1
37-
*/
38-
opacity?: number
39-
/**
40-
* Composite curve line width
41-
* @default theme.curve.width || 1.5
42-
*/
43-
lineWidth?: number
4419
/**
4520
* Adjusts the resolution of the curve by reducing the number of points based on the graph's width.
4621
* Lower values = more points = smoother curve but slower performance.
4722
* Recommended to increase this value when rendering more than 10 filters.
4823
* @default 2
4924
*/
5025
resolutionFactor?: number
51-
/**
52-
* Optional gradient ID to fill the curve with a gradient
53-
* The gradient must be defined by `FilterGradient` component and referenced by its ID
54-
* @default undefined
55-
*/
56-
gradientId?: string
57-
/**
58-
* Additional CSS classes to apply to the curve path
59-
*/
60-
className?: string
61-
/**
62-
* Additional inline styles to apply to the curve path
63-
*/
64-
style?: CSSProperties
6526
}
6627
/**
6728
* Renders a composite frequency response curve by combining multiple filter responses.
@@ -72,14 +33,19 @@ type CompositeCurveProps = {
7233
*/
7334
export const CompositeCurve = ({
7435
filters,
75-
dotted = false,
36+
resolutionFactor = 2,
37+
7638
color,
39+
dotted,
7740
opacity,
7841
lineWidth,
79-
resolutionFactor = 2,
8042
gradientId,
81-
className,
82-
style
43+
44+
style,
45+
easing,
46+
animate,
47+
duration, // ms
48+
className
8349
}: CompositeCurveProps) => {
8450
const { scale, width } = useGraph()
8551
const { minFreq, maxFreq, sampleRate } = scale
@@ -128,18 +94,23 @@ export const CompositeCurve = ({
12894
return calcCompositeMagnitudes(allMags)
12995
}, [magnitudesCache])
13096

97+
if (!compositeMagnitudes.length) return null
98+
13199
return (
132100
<>
133101
<use href="#centerLine" />
134102
<FrequencyResponseCurve
135103
magnitudes={compositeMagnitudes}
136-
dotted={dotted}
137104
color={color}
105+
dotted={dotted}
138106
opacity={opacity}
139107
lineWidth={lineWidth}
140108
gradientId={gradientId}
141-
className={className}
142109
style={style}
110+
easing={easing}
111+
animate={animate}
112+
duration={duration}
113+
className={className}
143114
/>
144115
</>
145116
)

src/components/FilterCurve/FilterCurve.tsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -99,24 +99,28 @@ export type FilterCurveProps = DefaultCurveProps &
9999
**/
100100
export const FilterCurve = ({
101101
filter,
102-
color,
103102
index = -1,
104-
lineWidth,
105-
opacity,
106-
107103
resolutionFactor = 2,
108104

105+
color,
106+
dotted,
107+
opacity,
108+
lineWidth,
109109
gradientId,
110+
110111
showPin = false,
111112
showBypass = false,
112-
active = false,
113113

114+
active = false,
114115
activeColor,
115-
activeLineWidth,
116116
activeOpacity,
117+
activeLineWidth,
117118

118-
className,
119119
style,
120+
easing,
121+
animate,
122+
duration, // ms
123+
className,
120124

121125
onChange
122126
}: FilterCurveProps) => {
@@ -169,17 +173,21 @@ export const FilterCurve = ({
169173
filter={filter}
170174
color={curveColor}
171175
opacity={curveOpacity}
172-
width={curveWidth}
176+
lineWidth={curveWidth}
173177
/>
174178
)}
175179
<FrequencyResponseCurve
176180
magnitudes={magnitudes}
181+
dotted={dotted}
177182
color={curveColor}
178183
opacity={curveOpacity}
179184
lineWidth={curveWidth}
180185
gradientId={gradientId}
181-
className={className}
182186
style={style}
187+
easing={easing}
188+
animate={animate}
189+
duration={duration}
190+
className={className}
183191
/>
184192
</>
185193
)

src/components/FilterCurve/FilterPin.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ import { useGraph } from '../..'
1212
export type FilterPinProps = {
1313
filter: GraphFilter
1414
vars: BiQuadCoefficients
15-
width?: number
15+
lineWidth?: number
1616
opacity?: CSSProperties['opacity']
1717
color?: CSSProperties['color']
1818
}
1919
export const FilterPin = ({
2020
filter,
2121
vars,
2222
opacity,
23-
width,
23+
lineWidth,
2424
color
2525
}: FilterPinProps) => {
2626
const { scale, height, logScale } = useGraph()
@@ -71,7 +71,7 @@ export const FilterPin = ({
7171
y1={pointY}
7272
y2={magnitudeY}
7373
stroke={color}
74-
strokeWidth={width}
74+
strokeWidth={lineWidth}
7575
strokeOpacity={opacity}
7676
/>
7777
)

src/components/FrequencyResponseCurve/FrequencyResponseCurve.tsx

Lines changed: 44 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useRef, useState } from 'react'
1+
import { useLayoutEffect, useMemo, useRef } from 'react'
22

33
import { type DefaultCurveProps, easingSplines } from '../types'
44
import { plotCurve, scaleMagnitudes } from '../../math'
@@ -40,37 +40,53 @@ export const FrequencyResponseCurve = ({
4040
theme: { curve }
4141
} = useGraph()
4242

43-
const previousPathRef = useRef<string>('')
44-
const [initialized, setInitialized] = useState(false)
43+
// Track first mount and previous path
44+
const firstMount = useRef(true)
45+
const prevPathRef = useRef<string>('')
46+
// Reference to animate element
47+
const animateRef = useRef<SVGAnimateElement>(null)
4548

46-
const points = scaleMagnitudes(magnitudes, scale, width, height)
47-
const path = plotCurve(points, scale, width, height)
49+
// Memoize paths calculation
50+
const { currentPath, initialPath } = useMemo(() => {
51+
const points = scaleMagnitudes(magnitudes, scale, width, height)
52+
const flatPoints = points.map((p) => ({ x: p.x, y: height / 2 }))
4853

49-
// Single effect to handle both initialization and updates
50-
useEffect(() => {
51-
if (!initialized) {
52-
// First time - create flat line with same number of points
53-
const zeroes = new Array(magnitudes.length).fill(0)
54-
const initialPoints = scaleMagnitudes(zeroes, scale, width, height)
55-
previousPathRef.current = plotCurve(initialPoints, scale, width, height)
56-
setInitialized(true)
57-
} else {
58-
previousPathRef.current = path
54+
const currentPath = plotCurve(points, scale, width, height)
55+
const initialPath = plotCurve(flatPoints, scale, width, height)
56+
57+
return {
58+
currentPath,
59+
initialPath
60+
}
61+
}, [magnitudes, scale, width, height])
62+
63+
// Handle initial mount state
64+
useLayoutEffect(() => {
65+
if (firstMount.current) {
66+
prevPathRef.current = initialPath
67+
firstMount.current = false
5968
}
60-
}, [path, magnitudes.length, scale, width, height])
69+
}, [initialPath])
6170

62-
// Don't render animation until initialized
63-
const showAnimation = animate && initialized && previousPathRef.current
71+
// Update previous path and trigger animation
72+
useLayoutEffect(() => {
73+
if (!firstMount.current && animate && animateRef.current) {
74+
prevPathRef.current = currentPath
75+
animateRef.current.beginElement()
76+
}
77+
}, [magnitudes, animate])
78+
79+
// Determine which path to display
80+
const displayPath = animate && firstMount.current ? initialPath : currentPath
81+
const fromPath = prevPathRef.current || initialPath
6482

6583
const curveColor = color || curve.color
6684
const curveWidth = lineWidth || curve.width
6785
const curveOpacity = opacity || curve.opacity
6886

69-
// NOTE: center line should be rendered on top of all filter curves but behind the final, resulting one
70-
7187
return (
7288
<path
73-
d={path}
89+
d={displayPath}
7490
stroke={curveColor}
7591
strokeWidth={curveWidth}
7692
strokeOpacity={curveOpacity}
@@ -80,17 +96,17 @@ export const FrequencyResponseCurve = ({
8096
className={className}
8197
style={style}
8298
>
83-
{showAnimation && (
99+
{animate && (
84100
<animate
85-
key={path} // Forces new animation when path changes
86-
attributeName="d"
87-
from={previousPathRef.current}
88-
to={path}
89-
dur={`${duration}ms`}
101+
ref={animateRef}
102+
from={fromPath}
103+
to={currentPath}
90104
fill="freeze"
105+
repeatCount="1"
91106
calcMode="spline"
107+
attributeName="d"
108+
dur={`${duration}ms`}
92109
keySplines={easingSplines[easing]}
93-
repeatCount="1"
94110
/>
95111
)}
96112
</path>

src/components/types.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import type { CSSProperties } from 'react'
22

33
export const easingSplines = {
4+
// Standard CSS easing values translated to SVG keySplines format
45
linear: '0 0 1 1',
5-
easeIn: '0.4 0 1 1',
6-
easeOut: '0 0 0.2 1',
7-
easeInOut: '0.4 0 0.2 1',
8-
elastic: '0.6 0.5 0.15 0.95'
6+
easeIn: '0.42 0 1 1',
7+
easeOut: '0 0 0.58 1',
8+
easeInOut: '0.42 0 0.58 1',
9+
// Cubic bezier curves for more dramatic effects
10+
elastic: '0.64 0 0.78 1.5',
11+
bounce: '0.32 1.5 0.67 0.78'
912
} as const
1013

1114
export type EasingType = keyof typeof easingSplines
@@ -43,17 +46,17 @@ export type DefaultCurveProps = CurveStyleProps &
4346
CurveAnimationProps & {
4447
/**
4548
* Color override for the curve stroke
46-
* @default theme.curve.color
49+
* @default theme.curve.color || '#FFFFFF'
4750
*/
4851
color?: CSSProperties['color']
4952
/**
5053
* Opacity override for the curve stroke
51-
* @default theme.curve.opacity
54+
* @default theme.curve.opacity || 1
5255
*/
5356
opacity?: CSSProperties['opacity']
5457
/**
5558
* Line width override for the curve stroke
56-
* @default theme.curve.width
59+
* @default theme.curve.width || 1.5
5760
*/
5861
lineWidth?: number
5962
/**

0 commit comments

Comments
 (0)