From 318683b6c431d5ee3de39976d8edfeddd72e8173 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Wed, 29 Apr 2026 10:48:40 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20remplacement=20=C3=A9galiseur=20=C3=A0?= =?UTF-8?q?=20barres=20par=20waveform=20continue=20style=20Stitch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- frontend/src/components/AudioPlayer.tsx | 2 +- frontend/src/components/AudioVisualizer.tsx | 215 +++++++++++--------- 2 files changed, 121 insertions(+), 96 deletions(-) diff --git a/frontend/src/components/AudioPlayer.tsx b/frontend/src/components/AudioPlayer.tsx index 63a7fa4..0d581e2 100644 --- a/frontend/src/components/AudioPlayer.tsx +++ b/frontend/src/components/AudioPlayer.tsx @@ -231,7 +231,7 @@ export default function AudioPlayer({ episode, onClose, userId, mode = 'floating {/* Audio Visualizer */} -
+
diff --git a/frontend/src/components/AudioVisualizer.tsx b/frontend/src/components/AudioVisualizer.tsx index 7f0218c..5074406 100644 --- a/frontend/src/components/AudioVisualizer.tsx +++ b/frontend/src/components/AudioVisualizer.tsx @@ -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(null); const rafRef = useRef(0); - const heightsRef = useRef(Array(barCount).fill(3)); + const heightsRef = useRef(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 (