Sliding Ease

An interactive React component that adds a dynamic bubble effect, visually tracking cursor movement in real time.

1
"use client"
2
3
import { useEffect, useRef, useCallback } from "react"
4
5
interface SlidingEaseVerticalBarsProps {
6
backgroundColor?: string
7
lineColor?: string
8
barColor?: string
9
lineWidth?: number
10
animationSpeed?: number
11
removeWaveLine?: boolean
12
}
13
14
const SlidingEaseVerticalBars = ({
15
backgroundColor = "#F0EEE6",
16
lineColor = "#444",
17
barColor = "#5E5D59",
18
lineWidth = 1,
19
animationSpeed = 0.005,
20
removeWaveLine = true,
21
}: SlidingEaseVerticalBarsProps) => {
22
const canvasRef = useRef<HTMLCanvasElement>(null)
23
const timeRef = useRef<number>(0)
24
const animationFrameId = useRef<number | null>(null)
25
const mouseRef = useRef({ x: 0, y: 0, isDown: false })
26
const transitionBursts = useRef<Array<{ x: number; y: number; time: number; intensity: number }>>([])
27
const dprRef = useRef<number>(1)
28
29
interface Bar {
30
y: number
31
height: number
32
width: number
33
}
34
35
const noise = (x: number, y: number, t: number): number => {
36
const n = Math.sin(x * 0.02 + t) * Math.cos(y * 0.02 + t) + Math.sin(x * 0.03 - t) * Math.cos(y * 0.01 + t)
37
return (n + 1) / 2
38
}
39
40
const getMouseInfluence = (x: number, y: number): number => {
41
const dx = x - mouseRef.current.x
42
const dy = y - mouseRef.current.y
43
const distance = Math.sqrt(dx * dx + dy * dy)
44
const maxDistance = 180
45
return Math.max(0, 1 - distance / maxDistance)
46
}
47
48
const getTransitionBurstInfluence = (x: number, y: number, currentTime: number): number => {
49
let totalInfluence = 0
50
51
transitionBursts.current.forEach((burst) => {
52
const age = currentTime - burst.time
53
const maxAge = 2500
54
if (age < maxAge) {
55
const dx = x - burst.x
56
const dy = y - burst.y
57
const distance = Math.sqrt(dx * dx + dy * dy)
58
const burstRadius = (age / maxAge) * 300
59
const burstWidth = 60
60
if (Math.abs(distance - burstRadius) < burstWidth) {
61
const burstStrength = (1 - age / maxAge) * burst.intensity
62
const proximityToBurst = 1 - Math.abs(distance - burstRadius) / burstWidth
63
totalInfluence += burstStrength * proximityToBurst
64
}
65
}
66
})
67
68
return Math.min(totalInfluence, 1.5)
69
}
70
71
const generatePattern = (seed: number, width: number, height: number, numLines: number): Bar[][] => {
72
const pattern: Bar[][] = []
73
const lineSpacing = width / numLines
74
75
for (let i = 0; i < numLines; i++) {
76
const lineBars: Bar[] = []
77
let currentY = 0
78
79
while (currentY < height) {
80
const noiseVal = noise(i * lineSpacing, currentY, seed)
81
if (noiseVal > 0.5) {
82
const barLength = 10 + noiseVal * 30
83
const barWidth = 2 + noiseVal * 3
84
lineBars.push({
85
y: currentY + barLength / 2,
86
height: barLength,
87
width: barWidth,
88
})
89
currentY += barLength + 15
90
} else {
91
currentY += 15
92
}
93
}
94
pattern.push(lineBars)
95
}
96
97
return pattern
98
}
99
100
const resizeCanvas = useCallback(() => {
101
const canvas = canvasRef.current
102
if (!canvas) return
103
104
const dpr = window.devicePixelRatio || 1
105
dprRef.current = dpr
106
107
const displayWidth = window.innerWidth
108
const displayHeight = window.innerHeight
109
110
// Set the actual size in memory (scaled up for high DPI)
111
canvas.width = displayWidth * dpr
112
canvas.height = displayHeight * dpr
113
114
// Scale the canvas back down using CSS
115
canvas.style.width = displayWidth + "px"
116
canvas.style.height = displayHeight + "px"
117
118
// Scale the drawing context so everything draws at the correct size
119
const ctx = canvas.getContext("2d")
120
if (ctx) {
121
ctx.scale(dpr, dpr)
122
}
123
}, [])
124
125
const handleMouseMove = useCallback((e: MouseEvent) => {
126
const canvas = canvasRef.current
127
if (!canvas) return
128
129
const rect = canvas.getBoundingClientRect()
130
mouseRef.current.x = e.clientX - rect.left
131
mouseRef.current.y = e.clientY - rect.top
132
}, [])
133
134
const handleMouseDown = useCallback((e: MouseEvent) => {
135
mouseRef.current.isDown = true
136
const canvas = canvasRef.current
137
if (!canvas) return
138
139
const rect = canvas.getBoundingClientRect()
140
const x = e.clientX - rect.left
141
const y = e.clientY - rect.top
142
143
transitionBursts.current.push({
144
x,
145
y,
146
time: Date.now(),
147
intensity: 2,
148
})
149
150
const now = Date.now()
151
transitionBursts.current = transitionBursts.current.filter((burst) => now - burst.time < 2500)
152
}, [])
153
154
const handleMouseUp = useCallback(() => {
155
mouseRef.current.isDown = false
156
}, [])
157
158
const animate = useCallback(() => {
159
const canvas = canvasRef.current
160
if (!canvas) return
161
162
const ctx = canvas.getContext("2d")
163
if (!ctx) return
164
165
const currentTime = Date.now()
166
timeRef.current += animationSpeed
167
168
// Use CSS pixel dimensions for calculations
169
const width = canvas.clientWidth
170
const height = canvas.clientHeight
171
172
const numLines = Math.floor(width / 15)
173
const lineSpacing = width / numLines
174
175
// Generate patterns
176
const pattern1 = generatePattern(0, width, height, numLines)
177
const pattern2 = generatePattern(5, width, height, numLines)
178
179
// Create cycle with mouse influence
180
const baseCycleTime = timeRef.current % (Math.PI * 2)
181
const mouseInfluenceOnCycle = getMouseInfluence(width / 2, height / 2) * 0.5
182
183
let easingFactor: number
184
const adjustedCycleTime = baseCycleTime + mouseInfluenceOnCycle
185
186
if (adjustedCycleTime < Math.PI * 0.1) {
187
easingFactor = 0
188
} else if (adjustedCycleTime < Math.PI * 0.9) {
189
const transitionProgress = (adjustedCycleTime - Math.PI * 0.1) / (Math.PI * 0.8)
190
easingFactor = transitionProgress
191
} else if (adjustedCycleTime < Math.PI * 1.1) {
192
easingFactor = 1
193
} else if (adjustedCycleTime < Math.PI * 1.9) {
194
const transitionProgress = (adjustedCycleTime - Math.PI * 1.1) / (Math.PI * 0.8)
195
easingFactor = 1 - transitionProgress
196
} else {
197
easingFactor = 0
198
}
199
200
const smoothEasing =
201
easingFactor < 0.5 ? 4 * easingFactor * easingFactor * easingFactor : 1 - Math.pow(-2 * easingFactor + 2, 3) / 2
202
203
ctx.fillStyle = backgroundColor
204
ctx.fillRect(0, 0, width, height)
205
206
// Draw lines and interpolated bars
207
for (let i = 0; i < numLines; i++) {
208
const x = i * lineSpacing + lineSpacing / 2
209
const lineMouseInfluence = getMouseInfluence(x, height / 2)
210
211
// Draw vertical line with mouse influence
212
ctx.beginPath()
213
ctx.strokeStyle = lineColor
214
ctx.lineWidth = lineWidth + lineMouseInfluence * 2
215
ctx.moveTo(x, 0)
216
ctx.lineTo(x, height)
217
ctx.stroke()
218
219
// Interpolate between patterns
220
const bars1 = pattern1[i] || []
221
const bars2 = pattern2[i] || []
222
const maxBars = Math.max(bars1.length, bars2.length)
223
224
for (let j = 0; j < maxBars; j++) {
225
let bar1 = bars1[j]
226
let bar2 = bars2[j]
227
228
if (!bar1) bar1 = { y: bar2.y - 100, height: 0, width: 0 }
229
if (!bar2) bar2 = { y: bar1.y + 100, height: 0, width: 0 }
230
231
const barMouseInfluence = getMouseInfluence(x, bar1.y)
232
const burstInfluence = getTransitionBurstInfluence(x, bar1.y, currentTime)
233
234
// Enhanced wave motion with mouse and burst influence
235
const baseWaveOffset =
236
Math.sin(i * 0.3 + j * 0.5 + timeRef.current * 2) * 10 * (smoothEasing * (1 - smoothEasing) * 4)
237
238
const mouseWaveOffset = barMouseInfluence * Math.sin(timeRef.current * 3 + i * 0.2) * 15
239
const burstWaveOffset = burstInfluence * Math.sin(timeRef.current * 4 + j * 0.3) * 20
240
const totalWaveOffset = baseWaveOffset + mouseWaveOffset + burstWaveOffset
241
242
// Interpolate properties
243
const y = bar1.y + (bar2.y - bar1.y) * smoothEasing + totalWaveOffset
244
const height =
245
bar1.height + (bar2.height - bar1.height) * smoothEasing + barMouseInfluence * 5 + burstInfluence * 8
246
const width = bar1.width + (bar2.width - bar1.width) * smoothEasing + barMouseInfluence * 2 + burstInfluence * 3
247
248
// Draw bar with enhanced effects
249
if (height > 0.1 && width > 0.1) {
250
const intensity = Math.min(1, 0.8 + barMouseInfluence * 0.2 + burstInfluence * 0.3)
251
const red = Number.parseInt(barColor.slice(1, 3), 16)
252
const green = Number.parseInt(barColor.slice(3, 5), 16)
253
const blue = Number.parseInt(barColor.slice(5, 7), 16)
254
255
ctx.fillStyle = `rgba(${red}, ${green}, ${blue}, ${intensity})`
256
ctx.fillRect(x - width / 2, y - height / 2, width, height)
257
}
258
}
259
}
260
261
// Draw transition burst effects
262
if (!removeWaveLine) {
263
transitionBursts.current.forEach((burst) => {
264
const age = currentTime - burst.time
265
const maxAge = 2500
266
if (age < maxAge) {
267
const progress = age / maxAge
268
const radius = progress * 300
269
const alpha = (1 - progress) * 0.2 * burst.intensity
270
271
ctx.beginPath()
272
ctx.strokeStyle = `rgba(100, 100, 100, ${alpha})`
273
ctx.lineWidth = 2
274
ctx.arc(burst.x, burst.y, radius, 0, 2 * Math.PI)
275
ctx.stroke()
276
}
277
})
278
}
279
280
animationFrameId.current = requestAnimationFrame(animate)
281
}, [backgroundColor, lineColor, removeWaveLine, barColor, lineWidth, animationSpeed])
282
283
useEffect(() => {
284
const canvas = canvasRef.current
285
if (!canvas) return
286
287
resizeCanvas()
288
289
const handleResize = () => resizeCanvas()
290
291
window.addEventListener("resize", handleResize)
292
canvas.addEventListener("mousemove", handleMouseMove)
293
canvas.addEventListener("mousedown", handleMouseDown)
294
canvas.addEventListener("mouseup", handleMouseUp)
295
296
animate()
297
298
return () => {
299
window.removeEventListener("resize", handleResize)
300
canvas.removeEventListener("mousemove", handleMouseMove)
301
canvas.removeEventListener("mousedown", handleMouseDown)
302
canvas.removeEventListener("mouseup", handleMouseUp)
303
304
if (animationFrameId.current) {
305
cancelAnimationFrame(animationFrameId.current)
306
animationFrameId.current = null
307
}
308
timeRef.current = 0
309
transitionBursts.current = []
310
}
311
}, [animate, resizeCanvas, handleMouseMove, handleMouseDown, handleMouseUp])
312
313
return (
314
<div className="absolute inset-0 w-full h-full overflow-hidden" style={{ backgroundColor }}>
315
<canvas ref={canvasRef} className="block w-full h-full" />
316
</div>
317
)
318
}
319
320
export default SlidingEaseVerticalBars
321

Props

PropTypeDefaultDescription
backgroundColorstring'#F0EEE6'Background color of the canvas.
lineColorstring'#444'Color of the vertical lines.
barColorstring'#5E5D59'Color of the sliding bars.
lineWidthnumber1Width of the vertical lines.
animationSpeednumber0.005Speed of the sliding animation.
removeWaveLinebooleantrueWhether to remove the animated wave line (if true, the wave is not shown).