@djangocfg/ui-tools 2.1.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/LottiePlayer.client-LBEC2JKY.mjs +161 -0
- package/dist/LottiePlayer.client-LBEC2JKY.mjs.map +1 -0
- package/dist/LottiePlayer.client-WFMG2OOW.cjs +168 -0
- package/dist/LottiePlayer.client-WFMG2OOW.cjs.map +1 -0
- package/dist/Mermaid.client-4TU2TSH3.mjs +477 -0
- package/dist/Mermaid.client-4TU2TSH3.mjs.map +1 -0
- package/dist/Mermaid.client-SBYY364Q.cjs +483 -0
- package/dist/Mermaid.client-SBYY364Q.cjs.map +1 -0
- package/dist/PlaygroundLayout-3YVSAEAF.cjs +1003 -0
- package/dist/PlaygroundLayout-3YVSAEAF.cjs.map +1 -0
- package/dist/PlaygroundLayout-4DYBORAS.mjs +996 -0
- package/dist/PlaygroundLayout-4DYBORAS.mjs.map +1 -0
- package/dist/PrettyCode.client-LCBPPTIX.mjs +152 -0
- package/dist/PrettyCode.client-LCBPPTIX.mjs.map +1 -0
- package/dist/PrettyCode.client-PNPLXRH6.cjs +154 -0
- package/dist/PrettyCode.client-PNPLXRH6.cjs.map +1 -0
- package/dist/chunk-37ZI6VD4.mjs +12 -0
- package/dist/chunk-37ZI6VD4.mjs.map +1 -0
- package/dist/chunk-3HK2OE62.cjs +81 -0
- package/dist/chunk-3HK2OE62.cjs.map +1 -0
- package/dist/chunk-7DGDQVQW.cjs +591 -0
- package/dist/chunk-7DGDQVQW.cjs.map +1 -0
- package/dist/chunk-M6P2FU7L.mjs +572 -0
- package/dist/chunk-M6P2FU7L.mjs.map +1 -0
- package/dist/chunk-UQ3XI5MY.cjs +15 -0
- package/dist/chunk-UQ3XI5MY.cjs.map +1 -0
- package/dist/chunk-YFRNE2IR.mjs +79 -0
- package/dist/chunk-YFRNE2IR.mjs.map +1 -0
- package/dist/index.cjs +5042 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1591 -0
- package/dist/index.d.ts +1591 -0
- package/dist/index.mjs +4941 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +86 -0
- package/src/components/markdown/MarkdownMessage.tsx +340 -0
- package/src/components/markdown/index.ts +5 -0
- package/src/index.ts +26 -0
- package/src/stores/index.ts +9 -0
- package/src/stores/mediaCache.ts +534 -0
- package/src/tools/AudioPlayer/README.md +206 -0
- package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +216 -0
- package/src/tools/AudioPlayer/components/HybridSimplePlayer.tsx +280 -0
- package/src/tools/AudioPlayer/components/HybridWaveform.tsx +279 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +149 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +110 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +58 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +45 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +82 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +8 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +6 -0
- package/src/tools/AudioPlayer/components/index.ts +22 -0
- package/src/tools/AudioPlayer/context/HybridAudioProvider.tsx +158 -0
- package/src/tools/AudioPlayer/context/index.ts +16 -0
- package/src/tools/AudioPlayer/effects/index.ts +412 -0
- package/src/tools/AudioPlayer/hooks/index.ts +35 -0
- package/src/tools/AudioPlayer/hooks/useHybridAudio.ts +387 -0
- package/src/tools/AudioPlayer/hooks/useHybridAudioAnalysis.ts +95 -0
- package/src/tools/AudioPlayer/hooks/useVisualization.tsx +207 -0
- package/src/tools/AudioPlayer/index.ts +133 -0
- package/src/tools/AudioPlayer/types/effects.ts +73 -0
- package/src/tools/AudioPlayer/types/index.ts +27 -0
- package/src/tools/AudioPlayer/utils/debug.ts +14 -0
- package/src/tools/AudioPlayer/utils/formatTime.ts +10 -0
- package/src/tools/AudioPlayer/utils/index.ts +6 -0
- package/src/tools/ImageViewer/@refactoring/00-PLAN.md +71 -0
- package/src/tools/ImageViewer/@refactoring/01-TYPES.md +121 -0
- package/src/tools/ImageViewer/@refactoring/02-UTILS.md +143 -0
- package/src/tools/ImageViewer/@refactoring/03-HOOKS.md +261 -0
- package/src/tools/ImageViewer/@refactoring/04-COMPONENTS.md +427 -0
- package/src/tools/ImageViewer/@refactoring/05-EXECUTION-CHECKLIST.md +126 -0
- package/src/tools/ImageViewer/README.md +200 -0
- package/src/tools/ImageViewer/components/ImageInfo.tsx +44 -0
- package/src/tools/ImageViewer/components/ImageToolbar.tsx +145 -0
- package/src/tools/ImageViewer/components/ImageViewer.tsx +241 -0
- package/src/tools/ImageViewer/components/index.ts +7 -0
- package/src/tools/ImageViewer/hooks/index.ts +9 -0
- package/src/tools/ImageViewer/hooks/useImageLoading.ts +204 -0
- package/src/tools/ImageViewer/hooks/useImageTransform.ts +101 -0
- package/src/tools/ImageViewer/index.ts +60 -0
- package/src/tools/ImageViewer/types.ts +81 -0
- package/src/tools/ImageViewer/utils/constants.ts +59 -0
- package/src/tools/ImageViewer/utils/debug.ts +14 -0
- package/src/tools/ImageViewer/utils/index.ts +17 -0
- package/src/tools/ImageViewer/utils/lqip.ts +47 -0
- package/src/tools/JsonForm/JsonSchemaForm.tsx +197 -0
- package/src/tools/JsonForm/examples/BotConfigExample.tsx +249 -0
- package/src/tools/JsonForm/examples/RealBotConfigExample.tsx +161 -0
- package/src/tools/JsonForm/index.ts +46 -0
- package/src/tools/JsonForm/templates/ArrayFieldItemTemplate.tsx +47 -0
- package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +74 -0
- package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +107 -0
- package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +35 -0
- package/src/tools/JsonForm/templates/FieldTemplate.tsx +62 -0
- package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +116 -0
- package/src/tools/JsonForm/templates/index.ts +12 -0
- package/src/tools/JsonForm/types.ts +83 -0
- package/src/tools/JsonForm/utils.ts +213 -0
- package/src/tools/JsonForm/widgets/CheckboxWidget.tsx +37 -0
- package/src/tools/JsonForm/widgets/ColorWidget.tsx +219 -0
- package/src/tools/JsonForm/widgets/NumberWidget.tsx +89 -0
- package/src/tools/JsonForm/widgets/SelectWidget.tsx +97 -0
- package/src/tools/JsonForm/widgets/SliderWidget.tsx +148 -0
- package/src/tools/JsonForm/widgets/SwitchWidget.tsx +35 -0
- package/src/tools/JsonForm/widgets/TextWidget.tsx +96 -0
- package/src/tools/JsonForm/widgets/index.ts +14 -0
- package/src/tools/JsonTree/index.tsx +243 -0
- package/src/tools/LottiePlayer/LottiePlayer.client.tsx +213 -0
- package/src/tools/LottiePlayer/index.tsx +56 -0
- package/src/tools/LottiePlayer/types.ts +108 -0
- package/src/tools/LottiePlayer/useLottie.ts +164 -0
- package/src/tools/Mermaid/Mermaid.client.tsx +82 -0
- package/src/tools/Mermaid/components/MermaidCodeViewer.tsx +95 -0
- package/src/tools/Mermaid/components/MermaidFullscreenModal.tsx +103 -0
- package/src/tools/Mermaid/hooks/index.ts +4 -0
- package/src/tools/Mermaid/hooks/useMermaidCleanup.ts +73 -0
- package/src/tools/Mermaid/hooks/useMermaidFullscreen.ts +46 -0
- package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +226 -0
- package/src/tools/Mermaid/hooks/useMermaidValidation.ts +29 -0
- package/src/tools/Mermaid/index.tsx +44 -0
- package/src/tools/Mermaid/utils/mermaid-helpers.ts +33 -0
- package/src/tools/OpenapiViewer/components/EndpointInfo.tsx +149 -0
- package/src/tools/OpenapiViewer/components/EndpointsLibrary.tsx +263 -0
- package/src/tools/OpenapiViewer/components/PlaygroundLayout.tsx +125 -0
- package/src/tools/OpenapiViewer/components/PlaygroundStepper.tsx +100 -0
- package/src/tools/OpenapiViewer/components/RequestBuilder.tsx +157 -0
- package/src/tools/OpenapiViewer/components/RequestParametersForm.tsx +253 -0
- package/src/tools/OpenapiViewer/components/ResponseViewer.tsx +173 -0
- package/src/tools/OpenapiViewer/components/VersionSelector.tsx +68 -0
- package/src/tools/OpenapiViewer/components/index.ts +14 -0
- package/src/tools/OpenapiViewer/constants.ts +39 -0
- package/src/tools/OpenapiViewer/context/PlaygroundContext.tsx +337 -0
- package/src/tools/OpenapiViewer/hooks/index.ts +8 -0
- package/src/tools/OpenapiViewer/hooks/useMobile.ts +10 -0
- package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +199 -0
- package/src/tools/OpenapiViewer/index.tsx +37 -0
- package/src/tools/OpenapiViewer/types.ts +151 -0
- package/src/tools/OpenapiViewer/utils/apiKeyManager.ts +149 -0
- package/src/tools/OpenapiViewer/utils/formatters.ts +71 -0
- package/src/tools/OpenapiViewer/utils/index.ts +9 -0
- package/src/tools/OpenapiViewer/utils/versionManager.ts +161 -0
- package/src/tools/PrettyCode/PrettyCode.client.tsx +208 -0
- package/src/tools/PrettyCode/index.tsx +47 -0
- package/src/tools/VideoPlayer/@refactoring/00-PLAN.md +91 -0
- package/src/tools/VideoPlayer/@refactoring/01-TYPES.md +284 -0
- package/src/tools/VideoPlayer/@refactoring/02-UTILS.md +141 -0
- package/src/tools/VideoPlayer/@refactoring/03-HOOKS.md +178 -0
- package/src/tools/VideoPlayer/@refactoring/04-COMPONENTS.md +95 -0
- package/src/tools/VideoPlayer/@refactoring/05-EXECUTION-CHECKLIST.md +139 -0
- package/src/tools/VideoPlayer/README.md +264 -0
- package/src/tools/VideoPlayer/components/VideoControls.tsx +138 -0
- package/src/tools/VideoPlayer/components/VideoErrorFallback.tsx +172 -0
- package/src/tools/VideoPlayer/components/VideoPlayer.tsx +201 -0
- package/src/tools/VideoPlayer/components/index.ts +14 -0
- package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +52 -0
- package/src/tools/VideoPlayer/context/index.ts +8 -0
- package/src/tools/VideoPlayer/hooks/index.ts +12 -0
- package/src/tools/VideoPlayer/hooks/useVideoPlayerSettings.ts +70 -0
- package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +116 -0
- package/src/tools/VideoPlayer/index.ts +77 -0
- package/src/tools/VideoPlayer/providers/NativeProvider.tsx +284 -0
- package/src/tools/VideoPlayer/providers/StreamProvider.tsx +505 -0
- package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +400 -0
- package/src/tools/VideoPlayer/providers/index.ts +8 -0
- package/src/tools/VideoPlayer/types/index.ts +38 -0
- package/src/tools/VideoPlayer/types/player.ts +116 -0
- package/src/tools/VideoPlayer/types/provider.ts +93 -0
- package/src/tools/VideoPlayer/types/sources.ts +97 -0
- package/src/tools/VideoPlayer/utils/debug.ts +14 -0
- package/src/tools/VideoPlayer/utils/fileSource.ts +78 -0
- package/src/tools/VideoPlayer/utils/index.ts +12 -0
- package/src/tools/VideoPlayer/utils/resolvers.ts +75 -0
- package/src/tools/_shared.ts +29 -0
- package/src/tools/index.ts +172 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HybridWaveform - Real-time frequency visualization for hybrid player.
|
|
5
|
+
*
|
|
6
|
+
* Two modes:
|
|
7
|
+
* - 'frequency': Real-time frequency bars (default, requires playing audio)
|
|
8
|
+
* - 'static': Static progress bar (for when no analyser is available)
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Shows buffered regions
|
|
12
|
+
* - Click to seek
|
|
13
|
+
* - Responsive width
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useRef, useEffect, useCallback, memo } from 'react';
|
|
17
|
+
import { useHybridAudioContext } from '../context/HybridAudioProvider';
|
|
18
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// TYPES
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
export interface HybridWaveformProps {
|
|
25
|
+
/** Visualization mode */
|
|
26
|
+
mode?: 'frequency' | 'static';
|
|
27
|
+
/** Canvas height in pixels */
|
|
28
|
+
height?: number;
|
|
29
|
+
/** Bar width in pixels */
|
|
30
|
+
barWidth?: number;
|
|
31
|
+
/** Gap between bars in pixels */
|
|
32
|
+
barGap?: number;
|
|
33
|
+
/** Bar border radius */
|
|
34
|
+
barRadius?: number;
|
|
35
|
+
/** Color for played portion */
|
|
36
|
+
progressColor?: string;
|
|
37
|
+
/** Color for unplayed portion */
|
|
38
|
+
waveColor?: string;
|
|
39
|
+
/** Color for buffered regions indicator */
|
|
40
|
+
bufferedColor?: string;
|
|
41
|
+
/** Additional CSS class */
|
|
42
|
+
className?: string;
|
|
43
|
+
/** Callback when user seeks */
|
|
44
|
+
onSeek?: (time: number) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// =============================================================================
|
|
48
|
+
// COMPONENT
|
|
49
|
+
// =============================================================================
|
|
50
|
+
|
|
51
|
+
export const HybridWaveform = memo(function HybridWaveform({
|
|
52
|
+
mode = 'frequency',
|
|
53
|
+
height = 64,
|
|
54
|
+
barWidth = 3,
|
|
55
|
+
barGap = 2,
|
|
56
|
+
barRadius = 2,
|
|
57
|
+
progressColor = 'hsl(217 91% 60%)',
|
|
58
|
+
waveColor = 'hsl(217 91% 60% / 0.3)',
|
|
59
|
+
bufferedColor = 'hsl(217 91% 60% / 0.15)',
|
|
60
|
+
className,
|
|
61
|
+
onSeek,
|
|
62
|
+
}: HybridWaveformProps) {
|
|
63
|
+
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
64
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
65
|
+
const animationRef = useRef<number | null>(null);
|
|
66
|
+
const { state, controls, webAudio } = useHybridAudioContext();
|
|
67
|
+
|
|
68
|
+
// Handle click to seek
|
|
69
|
+
const handleClick = useCallback(
|
|
70
|
+
(e: React.MouseEvent<HTMLCanvasElement>) => {
|
|
71
|
+
const canvas = canvasRef.current;
|
|
72
|
+
if (!canvas || !state.duration) return;
|
|
73
|
+
|
|
74
|
+
const rect = canvas.getBoundingClientRect();
|
|
75
|
+
const x = e.clientX - rect.left;
|
|
76
|
+
const progress = x / rect.width;
|
|
77
|
+
const time = state.duration * progress;
|
|
78
|
+
|
|
79
|
+
controls.seek(time);
|
|
80
|
+
onSeek?.(time);
|
|
81
|
+
},
|
|
82
|
+
[state.duration, controls, onSeek]
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Render frequency bars (real-time visualization)
|
|
86
|
+
const renderFrequency = useCallback(() => {
|
|
87
|
+
const canvas = canvasRef.current;
|
|
88
|
+
const analyser = webAudio.analyser;
|
|
89
|
+
if (!canvas) return;
|
|
90
|
+
|
|
91
|
+
const ctx = canvas.getContext('2d');
|
|
92
|
+
if (!ctx) return;
|
|
93
|
+
|
|
94
|
+
const { width, height: canvasHeight } = canvas;
|
|
95
|
+
const dpr = window.devicePixelRatio || 1;
|
|
96
|
+
const displayWidth = width / dpr;
|
|
97
|
+
|
|
98
|
+
// Get frequency data if analyser is available
|
|
99
|
+
let dataArray: Uint8Array<ArrayBuffer> | null = null;
|
|
100
|
+
if (analyser) {
|
|
101
|
+
dataArray = new Uint8Array(analyser.frequencyBinCount) as Uint8Array<ArrayBuffer>;
|
|
102
|
+
analyser.getByteFrequencyData(dataArray);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
ctx.clearRect(0, 0, width, canvasHeight);
|
|
106
|
+
|
|
107
|
+
// Draw buffered regions at bottom
|
|
108
|
+
if (state.buffered && state.duration > 0) {
|
|
109
|
+
ctx.fillStyle = bufferedColor;
|
|
110
|
+
for (let i = 0; i < state.buffered.length; i++) {
|
|
111
|
+
const start = (state.buffered.start(i) / state.duration) * width;
|
|
112
|
+
const end = (state.buffered.end(i) / state.duration) * width;
|
|
113
|
+
ctx.fillRect(start, canvasHeight - 3 * dpr, end - start, 3 * dpr);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Calculate bar count based on available width
|
|
118
|
+
const barCount = Math.floor(displayWidth / (barWidth + barGap));
|
|
119
|
+
const progress = state.duration > 0 ? state.currentTime / state.duration : 0;
|
|
120
|
+
const progressX = width * progress;
|
|
121
|
+
|
|
122
|
+
// Draw bars
|
|
123
|
+
for (let i = 0; i < barCount; i++) {
|
|
124
|
+
let barHeight: number;
|
|
125
|
+
|
|
126
|
+
if (dataArray && state.isPlaying) {
|
|
127
|
+
// Real-time frequency data
|
|
128
|
+
const step = Math.floor(dataArray.length / barCount);
|
|
129
|
+
const value = dataArray[i * step] / 255;
|
|
130
|
+
barHeight = Math.max(4 * dpr, value * (canvasHeight - 6 * dpr) * 0.9);
|
|
131
|
+
} else {
|
|
132
|
+
// Static fallback - small bars
|
|
133
|
+
barHeight = 8 * dpr;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const x = i * (barWidth + barGap) * dpr;
|
|
137
|
+
const y = (canvasHeight - barHeight) / 2;
|
|
138
|
+
|
|
139
|
+
ctx.fillStyle = x < progressX ? progressColor : waveColor;
|
|
140
|
+
|
|
141
|
+
// Draw rounded rect
|
|
142
|
+
const radius = barRadius * dpr;
|
|
143
|
+
const rectWidth = barWidth * dpr;
|
|
144
|
+
ctx.beginPath();
|
|
145
|
+
ctx.roundRect(x, y, rectWidth, barHeight, radius);
|
|
146
|
+
ctx.fill();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Continue animation if playing
|
|
150
|
+
if (state.isPlaying) {
|
|
151
|
+
animationRef.current = requestAnimationFrame(renderFrequency);
|
|
152
|
+
}
|
|
153
|
+
}, [
|
|
154
|
+
webAudio.analyser,
|
|
155
|
+
state.buffered,
|
|
156
|
+
state.duration,
|
|
157
|
+
state.currentTime,
|
|
158
|
+
state.isPlaying,
|
|
159
|
+
barWidth,
|
|
160
|
+
barGap,
|
|
161
|
+
barRadius,
|
|
162
|
+
progressColor,
|
|
163
|
+
waveColor,
|
|
164
|
+
bufferedColor,
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
// Render static progress bar
|
|
168
|
+
const renderStatic = useCallback(() => {
|
|
169
|
+
const canvas = canvasRef.current;
|
|
170
|
+
if (!canvas) return;
|
|
171
|
+
|
|
172
|
+
const ctx = canvas.getContext('2d');
|
|
173
|
+
if (!ctx) return;
|
|
174
|
+
|
|
175
|
+
const { width, height: canvasHeight } = canvas;
|
|
176
|
+
const dpr = window.devicePixelRatio || 1;
|
|
177
|
+
|
|
178
|
+
ctx.clearRect(0, 0, width, canvasHeight);
|
|
179
|
+
|
|
180
|
+
// Draw buffered regions
|
|
181
|
+
if (state.buffered && state.duration > 0) {
|
|
182
|
+
ctx.fillStyle = bufferedColor;
|
|
183
|
+
for (let i = 0; i < state.buffered.length; i++) {
|
|
184
|
+
const start = (state.buffered.start(i) / state.duration) * width;
|
|
185
|
+
const end = (state.buffered.end(i) / state.duration) * width;
|
|
186
|
+
ctx.fillRect(start, 0, end - start, canvasHeight);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Draw progress bar
|
|
191
|
+
const progress = state.duration > 0 ? state.currentTime / state.duration : 0;
|
|
192
|
+
const progressWidth = width * progress;
|
|
193
|
+
|
|
194
|
+
// Background
|
|
195
|
+
ctx.fillStyle = waveColor;
|
|
196
|
+
ctx.fillRect(0, canvasHeight / 2 - 2 * dpr, width, 4 * dpr);
|
|
197
|
+
|
|
198
|
+
// Progress
|
|
199
|
+
ctx.fillStyle = progressColor;
|
|
200
|
+
ctx.fillRect(0, canvasHeight / 2 - 2 * dpr, progressWidth, 4 * dpr);
|
|
201
|
+
|
|
202
|
+
// Handle
|
|
203
|
+
if (progress > 0) {
|
|
204
|
+
ctx.beginPath();
|
|
205
|
+
ctx.arc(progressWidth, canvasHeight / 2, 6 * dpr, 0, Math.PI * 2);
|
|
206
|
+
ctx.fill();
|
|
207
|
+
}
|
|
208
|
+
}, [state.buffered, state.duration, state.currentTime, progressColor, waveColor, bufferedColor]);
|
|
209
|
+
|
|
210
|
+
// Resize canvas to match container
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
const container = containerRef.current;
|
|
213
|
+
const canvas = canvasRef.current;
|
|
214
|
+
if (!container || !canvas) return;
|
|
215
|
+
|
|
216
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
217
|
+
const entry = entries[0];
|
|
218
|
+
if (!entry) return;
|
|
219
|
+
|
|
220
|
+
const dpr = window.devicePixelRatio || 1;
|
|
221
|
+
const displayWidth = entry.contentRect.width;
|
|
222
|
+
const displayHeight = height;
|
|
223
|
+
|
|
224
|
+
canvas.width = displayWidth * dpr;
|
|
225
|
+
canvas.height = displayHeight * dpr;
|
|
226
|
+
canvas.style.width = `${displayWidth}px`;
|
|
227
|
+
canvas.style.height = `${displayHeight}px`;
|
|
228
|
+
|
|
229
|
+
// Re-render after resize
|
|
230
|
+
if (mode === 'frequency') {
|
|
231
|
+
renderFrequency();
|
|
232
|
+
} else {
|
|
233
|
+
renderStatic();
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
resizeObserver.observe(container);
|
|
238
|
+
return () => resizeObserver.disconnect();
|
|
239
|
+
}, [height, mode, renderFrequency, renderStatic]);
|
|
240
|
+
|
|
241
|
+
// Animation loop for frequency mode
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
if (mode === 'frequency') {
|
|
244
|
+
renderFrequency();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return () => {
|
|
248
|
+
if (animationRef.current) {
|
|
249
|
+
cancelAnimationFrame(animationRef.current);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
}, [mode, renderFrequency]);
|
|
253
|
+
|
|
254
|
+
// Re-render on time change for static mode
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
if (mode === 'static') {
|
|
257
|
+
renderStatic();
|
|
258
|
+
}
|
|
259
|
+
}, [mode, state.currentTime, renderStatic]);
|
|
260
|
+
|
|
261
|
+
// Re-render frequency when playback state changes
|
|
262
|
+
useEffect(() => {
|
|
263
|
+
if (mode === 'frequency' && !state.isPlaying) {
|
|
264
|
+
// One final render when stopped
|
|
265
|
+
renderFrequency();
|
|
266
|
+
}
|
|
267
|
+
}, [mode, state.isPlaying, renderFrequency]);
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<div ref={containerRef} className={cn('w-full', className)}>
|
|
271
|
+
<canvas
|
|
272
|
+
ref={canvasRef}
|
|
273
|
+
onClick={handleClick}
|
|
274
|
+
className="cursor-pointer"
|
|
275
|
+
style={{ width: '100%', height }}
|
|
276
|
+
/>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AudioReactiveCover - Album art with audio-reactive animations
|
|
5
|
+
*
|
|
6
|
+
* Uses effects utilities for clean data preparation before render.
|
|
7
|
+
* Click on cover to switch between effect variants.
|
|
8
|
+
*
|
|
9
|
+
* Must be used within HybridAudioProvider context.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { type ReactNode } from 'react';
|
|
13
|
+
import { cn } from '../../../_shared';
|
|
14
|
+
import { useHybridAudioLevels, useHybridAudioState } from '../../context/HybridAudioProvider';
|
|
15
|
+
import {
|
|
16
|
+
type EffectVariant,
|
|
17
|
+
type EffectIntensity,
|
|
18
|
+
type EffectColorScheme,
|
|
19
|
+
getEffectConfig,
|
|
20
|
+
prepareEffectColors,
|
|
21
|
+
calculateGlowLayers,
|
|
22
|
+
calculateOrbs,
|
|
23
|
+
calculateMeshGradients,
|
|
24
|
+
calculateSpotlight,
|
|
25
|
+
EFFECT_ANIMATIONS,
|
|
26
|
+
} from '../../effects';
|
|
27
|
+
import { GlowEffect, OrbsEffect, SpotlightEffect, MeshEffect, type GlowEffectData } from './effects';
|
|
28
|
+
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// TYPES
|
|
31
|
+
// =============================================================================
|
|
32
|
+
|
|
33
|
+
export interface AudioReactiveCoverProps {
|
|
34
|
+
children: ReactNode;
|
|
35
|
+
size?: 'sm' | 'md' | 'lg';
|
|
36
|
+
variant?: EffectVariant;
|
|
37
|
+
intensity?: EffectIntensity;
|
|
38
|
+
colorScheme?: EffectColorScheme;
|
|
39
|
+
onClick?: () => void;
|
|
40
|
+
className?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// =============================================================================
|
|
44
|
+
// CONSTANTS
|
|
45
|
+
// =============================================================================
|
|
46
|
+
|
|
47
|
+
const SIZES = {
|
|
48
|
+
sm: { container: 'w-32 h-32', orbBase: 40 },
|
|
49
|
+
md: { container: 'w-40 h-40', orbBase: 50 },
|
|
50
|
+
lg: { container: 'w-48 h-48', orbBase: 60 },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// =============================================================================
|
|
54
|
+
// COMPONENT
|
|
55
|
+
// =============================================================================
|
|
56
|
+
|
|
57
|
+
export function AudioReactiveCover({
|
|
58
|
+
children,
|
|
59
|
+
size = 'lg',
|
|
60
|
+
variant = 'spotlight',
|
|
61
|
+
intensity = 'medium',
|
|
62
|
+
colorScheme = 'primary',
|
|
63
|
+
onClick,
|
|
64
|
+
className,
|
|
65
|
+
}: AudioReactiveCoverProps) {
|
|
66
|
+
// Get audio state from HybridAudioProvider context
|
|
67
|
+
const { isPlaying } = useHybridAudioState();
|
|
68
|
+
const levels = useHybridAudioLevels();
|
|
69
|
+
|
|
70
|
+
// =========================================================================
|
|
71
|
+
// PREPARE DATA BEFORE RENDER
|
|
72
|
+
// =========================================================================
|
|
73
|
+
|
|
74
|
+
const sizeConfig = SIZES[size];
|
|
75
|
+
const effectConfig = getEffectConfig(intensity);
|
|
76
|
+
const { colors, hueShift } = prepareEffectColors(colorScheme, levels);
|
|
77
|
+
|
|
78
|
+
// Calculate scale based on overall level
|
|
79
|
+
const containerScale = 1 + levels.overall * effectConfig.scale;
|
|
80
|
+
|
|
81
|
+
// Prepare effect-specific data - NO memoization for real-time reactivity
|
|
82
|
+
const glowData: GlowEffectData | null = variant === 'glow' ? {
|
|
83
|
+
layers: calculateGlowLayers(levels, effectConfig, colors),
|
|
84
|
+
hueShift,
|
|
85
|
+
showPulseRings: levels.bass > 0.5,
|
|
86
|
+
showSparkle: levels.high > 0.4,
|
|
87
|
+
} : null;
|
|
88
|
+
|
|
89
|
+
const orbsData = variant === 'orbs'
|
|
90
|
+
? calculateOrbs(levels, effectConfig, colors, sizeConfig.orbBase)
|
|
91
|
+
: null;
|
|
92
|
+
|
|
93
|
+
const meshData = variant === 'mesh'
|
|
94
|
+
? calculateMeshGradients(levels, effectConfig, colors)
|
|
95
|
+
: null;
|
|
96
|
+
|
|
97
|
+
const spotlightData = variant === 'spotlight'
|
|
98
|
+
? calculateSpotlight(levels, effectConfig, colors, levels.mid * 360)
|
|
99
|
+
: null;
|
|
100
|
+
|
|
101
|
+
// =========================================================================
|
|
102
|
+
// RENDER
|
|
103
|
+
// =========================================================================
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div
|
|
107
|
+
className={cn('relative', sizeConfig.container, className)}
|
|
108
|
+
style={{
|
|
109
|
+
transform: `scale(${containerScale})`,
|
|
110
|
+
transition: 'transform 0.1s ease-out',
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
{/* Effect layers container - under cover, non-interactive */}
|
|
114
|
+
<div className="absolute inset-0 z-0 pointer-events-none overflow-visible">
|
|
115
|
+
{glowData && (
|
|
116
|
+
<GlowEffect data={glowData} colors={colors} isPlaying={isPlaying} />
|
|
117
|
+
)}
|
|
118
|
+
|
|
119
|
+
{orbsData && (
|
|
120
|
+
<OrbsEffect orbs={orbsData} blur={effectConfig.blur} isPlaying={isPlaying} />
|
|
121
|
+
)}
|
|
122
|
+
|
|
123
|
+
{spotlightData && (
|
|
124
|
+
<SpotlightEffect data={spotlightData} colors={colors} blur={effectConfig.blur} isPlaying={isPlaying} />
|
|
125
|
+
)}
|
|
126
|
+
|
|
127
|
+
{meshData && (
|
|
128
|
+
<MeshEffect gradients={meshData} blur={effectConfig.blur} isPlaying={isPlaying} />
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{/* Content (cover art) */}
|
|
133
|
+
<div
|
|
134
|
+
className="relative w-full h-full rounded-lg overflow-hidden shadow-2xl z-10 bg-background cursor-pointer"
|
|
135
|
+
onClick={onClick}
|
|
136
|
+
role={onClick ? 'button' : undefined}
|
|
137
|
+
tabIndex={onClick ? 0 : undefined}
|
|
138
|
+
onKeyDown={onClick ? (e) => e.key === 'Enter' && onClick() : undefined}
|
|
139
|
+
>
|
|
140
|
+
{children}
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
{/* Inject animations once */}
|
|
144
|
+
<style dangerouslySetInnerHTML={{ __html: EFFECT_ANIMATIONS }} />
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export default AudioReactiveCover;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GlowEffect - Multi-layer glow effect with pulse rings
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { cn } from '../../../../_shared';
|
|
8
|
+
import type { calculateGlowLayers } from '../../../effects';
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// TYPES
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
export interface GlowEffectData {
|
|
15
|
+
layers: ReturnType<typeof calculateGlowLayers>;
|
|
16
|
+
hueShift: number;
|
|
17
|
+
showPulseRings: boolean;
|
|
18
|
+
showSparkle: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface GlowEffectProps {
|
|
22
|
+
data: GlowEffectData;
|
|
23
|
+
colors: string[];
|
|
24
|
+
isPlaying: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// COMPONENT
|
|
29
|
+
// =============================================================================
|
|
30
|
+
|
|
31
|
+
export function GlowEffect({ data, colors, isPlaying }: GlowEffectProps) {
|
|
32
|
+
const { layers, hueShift, showPulseRings, showSparkle } = data;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<>
|
|
36
|
+
{/* Main glow layers */}
|
|
37
|
+
{layers.map((layer, i) => (
|
|
38
|
+
<div
|
|
39
|
+
key={i}
|
|
40
|
+
className={cn('absolute rounded-2xl -z-10', layer.blur)}
|
|
41
|
+
style={{
|
|
42
|
+
inset: `-${layer.inset}px`,
|
|
43
|
+
background: layer.background,
|
|
44
|
+
opacity: isPlaying ? layer.opacity : 0,
|
|
45
|
+
transform: i < 2 ? `scaleY(${layer.scale})` : `scale(${layer.scale})`,
|
|
46
|
+
animation: isPlaying && layer.animation ? layer.animation : 'none',
|
|
47
|
+
transition: 'opacity 0.3s',
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
))}
|
|
51
|
+
|
|
52
|
+
{/* Rotating color sweep */}
|
|
53
|
+
<div
|
|
54
|
+
className="absolute rounded-2xl blur-xl overflow-hidden -z-10"
|
|
55
|
+
style={{
|
|
56
|
+
inset: '-75px',
|
|
57
|
+
opacity: isPlaying ? 0.6 : 0,
|
|
58
|
+
transition: 'opacity 0.5s',
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
<div
|
|
62
|
+
className="absolute inset-0"
|
|
63
|
+
style={{
|
|
64
|
+
background: `conic-gradient(
|
|
65
|
+
from ${hueShift}deg at 50% 50%,
|
|
66
|
+
hsl(${colors[0]} / 0.4) 0deg,
|
|
67
|
+
hsl(${colors[1] || colors[0]} / 0.3) 90deg,
|
|
68
|
+
hsl(${colors[2] || colors[0]} / 0.3) 180deg,
|
|
69
|
+
hsl(${colors[3] || colors[0]} / 0.3) 270deg,
|
|
70
|
+
hsl(${colors[0]} / 0.4) 360deg
|
|
71
|
+
)`,
|
|
72
|
+
animation: isPlaying ? 'glow-rotate 6s linear infinite' : 'none',
|
|
73
|
+
}}
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
{/* Pulse rings on bass hits */}
|
|
78
|
+
{showPulseRings && (
|
|
79
|
+
<>
|
|
80
|
+
<div
|
|
81
|
+
className="absolute -inset-6 rounded-xl border-2 animate-ping -z-10"
|
|
82
|
+
style={{
|
|
83
|
+
borderColor: `hsl(${colors[0]} / 0.4)`,
|
|
84
|
+
animationDuration: '1s',
|
|
85
|
+
}}
|
|
86
|
+
/>
|
|
87
|
+
<div
|
|
88
|
+
className="absolute -inset-12 rounded-2xl border animate-ping -z-10"
|
|
89
|
+
style={{
|
|
90
|
+
borderColor: `hsl(${colors[1] || colors[0]} / 0.3)`,
|
|
91
|
+
animationDuration: '1.5s',
|
|
92
|
+
animationDelay: '0.2s',
|
|
93
|
+
}}
|
|
94
|
+
/>
|
|
95
|
+
</>
|
|
96
|
+
)}
|
|
97
|
+
|
|
98
|
+
{/* Sparkle on high frequencies */}
|
|
99
|
+
{showSparkle && (
|
|
100
|
+
<div
|
|
101
|
+
className="absolute -inset-18 rounded-3xl -z-10"
|
|
102
|
+
style={{
|
|
103
|
+
background: `radial-gradient(circle at 50% 30%, hsl(${colors[2] || colors[0]} / 0.5) 0%, transparent 30%)`,
|
|
104
|
+
animation: 'sparkle-move 0.5s ease-out',
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
107
|
+
)}
|
|
108
|
+
</>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MeshEffect - Mesh gradient blobs that react to audio
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { cn } from '../../../../_shared';
|
|
8
|
+
import type { calculateMeshGradients } from '../../../effects';
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// TYPES
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
interface MeshEffectProps {
|
|
15
|
+
gradients: ReturnType<typeof calculateMeshGradients>;
|
|
16
|
+
blur: string;
|
|
17
|
+
isPlaying: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// COMPONENT
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
export function MeshEffect({ gradients, isPlaying }: MeshEffectProps) {
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
{gradients.map((g, i) => {
|
|
28
|
+
const isCenter = 'isCenter' in g && g.isCenter;
|
|
29
|
+
const scale = 'scale' in g ? g.scale : 1;
|
|
30
|
+
const rotation = 'rotation' in g ? g.rotation : 0;
|
|
31
|
+
const itemBlur = 'blur' in g ? g.blur : 'blur-2xl';
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div
|
|
35
|
+
key={i}
|
|
36
|
+
className={cn('absolute rounded-full -z-10', itemBlur)}
|
|
37
|
+
style={{
|
|
38
|
+
width: g.width,
|
|
39
|
+
height: g.height,
|
|
40
|
+
top: 'top' in g ? g.top : undefined,
|
|
41
|
+
bottom: 'bottom' in g ? g.bottom : undefined,
|
|
42
|
+
left: 'left' in g ? g.left : undefined,
|
|
43
|
+
right: 'right' in g ? g.right : undefined,
|
|
44
|
+
background: isCenter
|
|
45
|
+
? `radial-gradient(circle, hsl(${g.color} / 0.6) 0%, hsl(${g.color} / 0.3) 30%, transparent 60%)`
|
|
46
|
+
: `radial-gradient(circle, hsl(${g.color}) 0%, hsl(${g.color} / 0.5) 30%, transparent 65%)`,
|
|
47
|
+
opacity: isPlaying ? g.opacity : 0,
|
|
48
|
+
transform: isCenter
|
|
49
|
+
? `translate(-50%, -50%) scale(${scale})`
|
|
50
|
+
: `scale(${scale}) rotate(${rotation}deg)`,
|
|
51
|
+
transition: 'all 0.08s ease-out',
|
|
52
|
+
}}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
})}
|
|
56
|
+
</>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OrbsEffect - Floating orb particles that react to audio
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { cn } from '../../../../_shared';
|
|
8
|
+
import type { calculateOrbs } from '../../../effects';
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// TYPES
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
interface OrbsEffectProps {
|
|
15
|
+
orbs: ReturnType<typeof calculateOrbs>;
|
|
16
|
+
blur: string;
|
|
17
|
+
isPlaying: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// COMPONENT
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
export function OrbsEffect({ orbs, blur, isPlaying }: OrbsEffectProps) {
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
{orbs.map((orb, i) => (
|
|
28
|
+
<div
|
|
29
|
+
key={i}
|
|
30
|
+
className={cn('absolute rounded-full -z-10', blur)}
|
|
31
|
+
style={{
|
|
32
|
+
width: orb.size,
|
|
33
|
+
height: orb.size,
|
|
34
|
+
left: `${orb.x}%`,
|
|
35
|
+
top: `${orb.y}%`,
|
|
36
|
+
background: `radial-gradient(circle at 30% 30%, hsl(${orb.color}) 0%, hsl(${orb.color} / 0.5) 40%, transparent 70%)`,
|
|
37
|
+
opacity: isPlaying ? orb.opacity : 0,
|
|
38
|
+
transform: `translate(-50%, -50%) scale(${orb.scale})`,
|
|
39
|
+
transition: 'all 0.08s ease-out',
|
|
40
|
+
}}
|
|
41
|
+
/>
|
|
42
|
+
))}
|
|
43
|
+
</>
|
|
44
|
+
);
|
|
45
|
+
}
|