fix: remplacement égaliseur à barres par waveform continue style Stitch

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 4.7 committed 2026-04-29 10:48:40 +02:00
1 parent 31eb884669
commit 318683b6c4
2 files changed
+121 -96

No files matched your search

+1 -1
View File
@@ -231,7 +231,7 @@ export default function AudioPlayer({ episode, onClose, userId, mode = 'floating
</div>
{/* Audio Visualizer */}
<div className="w-full flex justify-center my-3">
<div className="my-4">
<AudioVisualizer height={56} />
</div>
+120 -95
View File
@@ -1,26 +1,26 @@
import { useEffect, useRef, useState } from 'react';
import { useEffect, useRef } from 'react';
import { useAudio } from '../context/AudioContext';
interface AudioVisualizerProps {
width?: number;
height?: number;
barCount?: number;
}
export default function AudioVisualizer({
width,
height = 72,
barCount = 48,
}: AudioVisualizerProps) {
const { isPlaying, getFrequencyData } = useAudio();
const NUM_POINTS = 80;
const BASE_AMPLITUDE = 0.12;
const MAX_AMPLITUDE = 0.42;
export default function AudioVisualizer({ height = 80 }: AudioVisualizerProps) {
const { isPlaying, currentTime, duration, getFrequencyData } = useAudio();
const canvasRef = useRef<HTMLCanvasElement>(null);
const rafRef = useRef<number>(0);
const heightsRef = useRef<number[]>(Array(barCount).fill(3));
const heightsRef = useRef<number[]>(Array(NUM_POINTS).fill(BASE_AMPLITUDE));
const playingRef = useRef(isPlaying);
const getDataRef = useRef(getFrequencyData);
const timeRef = useRef({ currentTime, duration });
useEffect(() => { playingRef.current = isPlaying; }, [isPlaying]);
useEffect(() => { getDataRef.current = getFrequencyData; }, [getFrequencyData]);
useEffect(() => { timeRef.current = { currentTime, duration }; }, [currentTime, duration]);
useEffect(() => {
const canvas = canvasRef.current;
@@ -29,7 +29,9 @@ export default function AudioVisualizer({
if (!ctx) return;
const dpr = window.devicePixelRatio || 1;
const displayW = width || canvas.parentElement?.clientWidth || 300;
const parentW = canvas.parentElement?.clientWidth || 300;
const maxW = Math.min(parentW, 800);
const displayW = maxW;
const displayH = height;
canvas.width = displayW * dpr;
canvas.height = displayH * dpr;
@@ -37,9 +39,6 @@ export default function AudioVisualizer({
canvas.style.height = `${displayH}px`;
ctx.scale(dpr, dpr);
const half = barCount / 2;
const barW = Math.max(2, (displayW - barCount * 2) / barCount);
const gap = (displayW - barW * barCount) / (barCount + 1);
const midY = displayH / 2;
let frame = 0;
@@ -48,110 +47,136 @@ export default function AudioVisualizer({
if (!ctx || !canvas) return;
frame++;
const raw = getDataRef.current?.();
const currentHeights = heightsRef.current;
const hts = heightsRef.current;
const step = displayW / (NUM_POINTS - 1);
const { currentTime: ct, duration: dur } = timeRef.current;
const progress = dur > 0 ? ct / dur : 0;
// Update target heights from frequency data
if (raw && playingRef.current && frame % 2 === 0) {
for (let i = 0; i < barCount; i++) {
const distFromCenter = Math.abs(i - (barCount - 1) / 2);
const halfIdx = Math.floor(distFromCenter);
const dataIndex = Math.min(halfIdx, raw.length - 1);
const value = raw[dataIndex] ?? 0;
// Edge fade — center bars are taller
const edgeFade = 1 - (distFromCenter / half) * 0.25;
// Boost mid frequencies
const midBoost = distFromCenter < half * 0.5 ? 1.2 : 1;
const target = Math.max(4, Math.min(100, (value / 255) * 100 * edgeFade * midBoost));
// Smooth easing toward target
currentHeights[i] += (target - currentHeights[i]) * 0.35;
// Update heights from frequency data
if (raw && playingRef.current && frame % 3 === 0) {
for (let i = 0; i < NUM_POINTS; i++) {
const bucketSize = raw.length / NUM_POINTS;
const startIdx = Math.floor(i * bucketSize);
const endIdx = Math.floor((i + 1) * bucketSize);
let sum = 0;
let count = 0;
for (let j = startIdx; j < endIdx && j < raw.length; j++) {
sum += raw[j];
count++;
}
const avg = count > 0 ? sum / count : 0;
const target = BASE_AMPLITUDE + (avg / 255) * (MAX_AMPLITUDE - BASE_AMPLITUDE);
const rate = target > hts[i] ? 0.3 : 0.12;
hts[i] += (target - hts[i]) * rate;
}
} else if (!playingRef.current) {
// Gentle decay when paused
for (let i = 0; i < barCount; i++) {
currentHeights[i] += (3 - currentHeights[i]) * 0.08;
for (let i = 0; i < NUM_POINTS; i++) {
hts[i] += (BASE_AMPLITUDE - hts[i]) * 0.06;
}
}
// Clear
// Compute path points
const points: { x: number; y: number }[] = [];
for (let i = 0; i < NUM_POINTS; i++) {
const x = i * step;
const direction = i % 2 === 0 ? -1 : 1;
const y = midY + direction * hts[i] * midY;
points.push({ x, y });
}
ctx.clearRect(0, 0, displayW, displayH);
// Draw bars
for (let i = 0; i < barCount; i++) {
const h = currentHeights[i];
const barH = Math.max(2, (h / 100) * midY * 0.9);
const x = gap + i * (barW + gap);
const radius = barW / 2;
// ── Background muted waveform ──
drawSmoothPath(ctx, points, 'rgba(100, 116, 139, 0.25)', 2, displayW, midY);
// Glow on taller bars
if (h > 25) {
const glowAlpha = Math.min(0.35, h / 300);
const glowGrad = ctx.createRadialGradient(x + barW / 2, midY, barW, x + barW / 2, midY, barH + barW);
glowGrad.addColorStop(0, `rgba(108, 111, 247, ${glowAlpha})`);
glowGrad.addColorStop(1, 'rgba(108, 111, 247, 0)');
ctx.fillStyle = glowGrad;
ctx.fillRect(x - barW, midY - barH - barW, barW * 3, barH * 2 + barW * 2);
}
// ── Active glowing waveform ──
// Outer glow
ctx.save();
ctx.shadowColor = 'rgba(34, 211, 238, 0.6)';
ctx.shadowBlur = 14;
const grad = ctx.createLinearGradient(0, 0, displayW, 0);
grad.addColorStop(0, '#22d3ee');
grad.addColorStop(0.5, '#818cf8');
grad.addColorStop(1, '#c084fc');
drawSmoothPath(ctx, points, grad, 3, displayW, midY);
ctx.restore();
// Bar gradient (top half = cyan, bottom half = indigo)
const barGrad = ctx.createLinearGradient(x, midY - barH, x, midY + barH);
barGrad.addColorStop(0, '#0dc5e0');
barGrad.addColorStop(0.4, '#6c6ff7');
barGrad.addColorStop(0.6, '#6c6ff7');
barGrad.addColorStop(1, '#0dc5e0');
ctx.fillStyle = barGrad;
// ── Progress indicator dot ──
const dotX = progress * displayW;
const dotIdx = Math.floor(progress * (NUM_POINTS - 1));
const clampedIdx = Math.min(dotIdx, NUM_POINTS - 1);
const dotY = points[clampedIdx]?.y ?? midY;
// Draw upper bar (rounded top)
ctx.beginPath();
ctx.moveTo(x, midY);
ctx.lineTo(x, midY - barH + radius);
ctx.arcTo(x, midY - barH, x + barW, midY - barH, radius);
ctx.lineTo(x + barW, midY);
ctx.fill();
// Outer glow
const dotGlow = ctx.createRadialGradient(dotX, dotY, 0, dotX, dotY, 10);
dotGlow.addColorStop(0, 'rgba(255, 255, 255, 0.9)');
dotGlow.addColorStop(0.3, 'rgba(255, 255, 255, 0.5)');
dotGlow.addColorStop(1, 'rgba(255, 255, 255, 0)');
ctx.fillStyle = dotGlow;
ctx.beginPath();
ctx.arc(dotX, dotY, 10, 0, Math.PI * 2);
ctx.fill();
// Draw lower bar (mirrored, rounded bottom)
ctx.beginPath();
ctx.moveTo(x, midY);
ctx.lineTo(x, midY + barH - radius);
ctx.arcTo(x, midY + barH, x + barW, midY + barH, radius);
ctx.lineTo(x + barW, midY);
ctx.fill();
// Bright top highlight on peak bars
if (h > 50) {
ctx.fillStyle = `rgba(255, 255, 255, ${Math.min(0.5, (h - 50) / 100)})`;
ctx.beginPath();
ctx.arc(x + barW / 2, midY - barH + radius, radius * 0.6, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(x + barW / 2, midY + barH - radius, radius * 0.6, 0, Math.PI * 2);
ctx.fill();
}
// Center line — thin subtle separator
if (i === 0) {
ctx.strokeStyle = 'rgba(255, 255, 255, 0.04)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, midY);
ctx.lineTo(displayW, midY);
ctx.stroke();
}
}
// Core dot
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(dotX, dotY, 4, 0, Math.PI * 2);
ctx.fill();
rafRef.current = requestAnimationFrame(animate);
};
rafRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(rafRef.current);
}, [width, height, barCount]);
}, [height]);
return (
<canvas
ref={canvasRef}
className="w-full"
className="block mx-auto"
aria-hidden="true"
/>
);
}
/** Draw a smooth waveform using quadratic bezier curves */
function drawSmoothPath(
ctx: CanvasRenderingContext2D,
points: { x: number; y: number }[],
stroke: string | CanvasGradient,
lineWidth: number,
_displayW: number,
midY: number,
) {
if (points.length < 2) return;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
// First segment: control point is halfway between first two points at midY level
const firstCpX = (points[0].x + points[1].x) / 2;
const firstCpY = midY + (midY - points[0].y); // reflect first point around midY
ctx.quadraticCurveTo(firstCpX, firstCpY, points[1].x, points[1].y);
// Subsequent segments: reflect previous control point for smoothness (SVG T command equivalent)
let prevCpX = firstCpX;
let prevCpY = firstCpY;
let prevX = points[1].x;
let prevY = points[1].y;
for (let i = 2; i < points.length; i++) {
const reflectedCpX = 2 * prevX - prevCpX;
const reflectedCpY = 2 * prevY - prevCpY;
ctx.quadraticCurveTo(reflectedCpX, reflectedCpY, points[i].x, points[i].y);
prevCpX = reflectedCpX;
prevCpY = reflectedCpY;
prevX = points[i].x;
prevY = points[i].y;
}
ctx.strokeStyle = stroke;
ctx.lineWidth = lineWidth;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.stroke();
}