UNPKG

@oiij/use

Version:

Som Composable Functions for Vue 3

297 lines (295 loc) 11.9 kB
import { useRafFn } from "@vueuse/core"; //#region src/composables/use-spectrum.ts function easeInOutCubic(t) { return t < .5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2; } function drawRoundedRect(ctx, x, y, width, height, radius) { const r = Math.min(radius, width / 2, height / 2); ctx.beginPath(); ctx.moveTo(x + r, y); ctx.lineTo(x + width - r, y); ctx.quadraticCurveTo(x + width, y, x + width, y + r); ctx.lineTo(x + width, y + height - r); ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height); ctx.lineTo(x + r, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - r); ctx.lineTo(x, y + r); ctx.quadraticCurveTo(x, y, x + r, y); ctx.closePath(); ctx.fill(); } function polarToCartesian(centerX, centerY, radius, angleInRadians) { return { x: centerX + radius * Math.cos(angleInRadians), y: centerY + radius * Math.sin(angleInRadians) }; } function setupShadow(ctx, shadow) { if (shadow) { ctx.shadowColor = "rgba(0, 0, 0, 0.3)"; ctx.shadowBlur = 10; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 5; } } function useSpectrum(canvasRef, frequencyDataGetter, options) { const { type = "bar", color: defaultColor = "#00FFAA", shadow: defaultShadow = true, barOptions, lineOptions, circleBarOptions, circleLineOptions, animationSpeed = .5, manual } = options ?? {}; const dpr = window.devicePixelRatio || 1; let smoothedData = []; function updateFrequencyData() { const frequencyData = frequencyDataGetter(); if (smoothedData.length !== frequencyData.length) smoothedData = Array.from({ length: frequencyData.length }).fill(0); const smoothingFactor = Math.max(0, Math.min(1, animationSpeed)); for (let i = 0; i < frequencyData.length; i++) { const currentValue = frequencyData[i]; smoothedData[i] = smoothedData[i] + smoothingFactor * (currentValue - smoothedData[i]); } } function draw() { const canvas = canvasRef.value; if (!canvas) throw new Error("canvasRef is not a valid canvas element"); canvas.width = canvasRef.value.clientWidth * dpr; canvas.height = canvasRef.value.clientHeight * dpr; const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("canvasRef is not a valid canvas element"); ctx.scale(dpr, dpr); ctx.clearRect(0, 0, canvas.width, canvas.height); updateFrequencyData(); switch (type) { case "bar": drawBarSpectrum(ctx); break; case "line": drawLineSpectrum(ctx); break; case "circle-bar": drawCircleBarSpectrum(ctx); break; case "circle-line": drawCircleLineSpectrum(ctx); break; } } function drawBarSpectrum(ctx) { const { width = 8, minHeight = 8, spacing = 2, radius = 4, color = defaultColor, shadow = defaultShadow } = barOptions ?? {}; const canvas = ctx.canvas; const canvasWidth = canvas.width; const canvasHeight = canvas.height; setupShadow(ctx, shadow); const availableWidth = canvasWidth; const barCount = Math.min(smoothedData.length, Math.floor(availableWidth / (width + spacing))); if (barCount === 0) return; let fillStyle; if (Array.isArray(color)) { fillStyle = ctx.createLinearGradient(0, canvasHeight, 0, 0); fillStyle.addColorStop(0, color[0]); fillStyle.addColorStop(1, color[1]); } else fillStyle = color; ctx.fillStyle = fillStyle; const sampleStep = smoothedData.length / barCount; for (let i = 0; i < barCount; i++) { const dataIndex = Math.floor(i * sampleStep); const easedValue = easeInOutCubic((smoothedData[dataIndex] || 0) / 255); const barHeight = Math.max(minHeight, easedValue * canvasHeight * .8); drawRoundedRect(ctx, i * (width + spacing), canvasHeight - barHeight, width, barHeight, radius); } ctx.shadowColor = "transparent"; ctx.shadowBlur = 0; } function drawLineSpectrum(ctx) { const { width = 1, spacing = 20, color = defaultColor, smoothness = .5, fill = true, shadow = defaultShadow } = lineOptions ?? {}; const canvas = ctx.canvas; const canvasWidth = canvas.width; const canvasHeight = canvas.height; setupShadow(ctx, shadow); const availableWidth = canvasWidth; const pointCount = Math.min(smoothedData.length, Math.floor(availableWidth / spacing)); if (pointCount === 0) return; let strokeStyle; let fillStyle = null; if (Array.isArray(color)) { strokeStyle = ctx.createLinearGradient(0, canvasHeight, 0, 0); strokeStyle.addColorStop(0, color[0]); strokeStyle.addColorStop(1, color[1]); if (fill) { fillStyle = ctx.createLinearGradient(0, canvasHeight, 0, 0); fillStyle.addColorStop(0, `${color[0]}80`); fillStyle.addColorStop(1, `${color[1]}20`); } } else { strokeStyle = color; if (fill) fillStyle = `${color}40`; } ctx.strokeStyle = strokeStyle; ctx.lineWidth = width; ctx.lineCap = "round"; ctx.lineJoin = "round"; const sampleStep = smoothedData.length / pointCount; const points = []; for (let i = 0; i < pointCount; i++) { const dataIndex = Math.floor(i * sampleStep); const easedValue = easeInOutCubic((smoothedData[dataIndex] || 0) / 255); points.push({ x: i * spacing, y: canvasHeight - easedValue * canvasHeight * .8 }); } ctx.beginPath(); ctx.moveTo(0, canvasHeight); for (let i = 1; i < points.length - 1; i++) { const curr = points[i]; const next = points[i + 1]; const cp1x = curr.x + (next.x - curr.x) * smoothness; const cp1y = curr.y; const cp2x = next.x - (next.x - curr.x) * smoothness; const cp2y = next.y; ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, next.x, next.y); } ctx.lineTo(canvasWidth, canvasHeight); if (fill && fillStyle) { ctx.fillStyle = fillStyle; ctx.fill(); } ctx.stroke(); ctx.shadowColor = "transparent"; ctx.shadowBlur = 0; } function drawCircleBarSpectrum(ctx) { const { radius = Math.min(canvasRef.value.width, canvasRef.value.height) * .3, width = 8, minHeight = 8, barRadius = 4, spacing = 2, color = defaultColor, shadow = defaultShadow, startAngle = 0, endAngle = Math.PI * 2 } = circleBarOptions ?? {}; const canvas = ctx.canvas; const centerX = canvas.width / 2; const centerY = canvas.height / 2; setupShadow(ctx, shadow); const circumference = 2 * Math.PI * radius; const barCount = Math.min(smoothedData.length, Math.floor(circumference / (width + spacing))); if (barCount === 0) return; let fillStyle; if (Array.isArray(color)) { fillStyle = ctx.createLinearGradient(centerX - radius, centerY, centerX + radius, centerY); fillStyle.addColorStop(0, color[0]); fillStyle.addColorStop(1, color[1]); } else fillStyle = color; ctx.fillStyle = fillStyle; const sampleStep = smoothedData.length / barCount; const angleStep = (endAngle - startAngle) / barCount; for (let i = 0; i < barCount; i++) { const dataIndex = Math.floor(i * sampleStep); const easedValue = easeInOutCubic((smoothedData[dataIndex] || 0) / 255); const barHeight = Math.max(minHeight, easedValue * radius * .8); const angle = startAngle + i * angleStep; const barStartX = centerX + radius * Math.cos(angle); const barStartY = centerY + radius * Math.sin(angle); ctx.save(); ctx.translate(barStartX, barStartY); ctx.rotate(angle + Math.PI / 2); drawRoundedRect(ctx, -width / 2, -barHeight, width, barHeight, barRadius); ctx.restore(); } ctx.shadowColor = "transparent"; ctx.shadowBlur = 0; } function drawCircleLineSpectrum(ctx) { const { radius = Math.min(canvasRef.value.width, canvasRef.value.height) * .3, width = 1, spacing = 10, color = defaultColor, shadow = defaultShadow, smoothness = .5, fill = true, startAngle = 0, endAngle = Math.PI * 2 } = circleLineOptions ?? {}; const canvas = ctx.canvas; const centerX = canvas.width / 2; const centerY = canvas.height / 2; setupShadow(ctx, shadow); const circumference = 2 * Math.PI * radius; const pointCount = Math.min(smoothedData.length, Math.floor(circumference / spacing)); if (pointCount === 0) return; let strokeStyle; let fillStyle = null; if (Array.isArray(color)) { strokeStyle = ctx.createLinearGradient(centerX - radius, centerY, centerX + radius, centerY); strokeStyle.addColorStop(0, color[0]); strokeStyle.addColorStop(1, color[1]); if (fill) { fillStyle = ctx.createLinearGradient(centerX - radius, centerY, centerX + radius, centerY); fillStyle.addColorStop(0, `${color[0]}80`); fillStyle.addColorStop(1, `${color[1]}20`); } } else { strokeStyle = color; if (fill) fillStyle = `${color}40`; } ctx.strokeStyle = strokeStyle; ctx.lineWidth = width; ctx.lineCap = "round"; ctx.lineJoin = "round"; const sampleStep = smoothedData.length / pointCount; const angleStep = (endAngle - startAngle) / pointCount; const points = []; for (let i = 0; i < pointCount; i++) { const dataIndex = Math.floor(i * sampleStep); const easedValue = easeInOutCubic((smoothedData[dataIndex] || 0) / 255); const angle = startAngle + i * angleStep; const pointRadius = i === 0 ? radius : radius + easedValue * radius * .8; points.push(polarToCartesian(centerX, centerY, pointRadius, angle)); } ctx.beginPath(); if (points.length > 0) ctx.moveTo(points[0].x, points[0].y); for (let i = 1; i < points.length - 1; i++) { const curr = points[i]; const next = points[i + 1]; const currAngle = startAngle + i * angleStep; const nextAngle = startAngle + (i + 1) * angleStep; const currRadius = Math.sqrt((curr.x - centerX) ** 2 + (curr.y - centerY) ** 2); const nextRadius = Math.sqrt((next.x - centerX) ** 2 + (next.y - centerY) ** 2); const cp1Angle = currAngle + (nextAngle - currAngle) * smoothness; const cp1 = polarToCartesian(centerX, centerY, currRadius + (nextRadius - currRadius) * smoothness, cp1Angle); const cp2Angle = nextAngle - (nextAngle - currAngle) * smoothness; const cp2 = polarToCartesian(centerX, centerY, nextRadius - (nextRadius - currRadius) * smoothness, cp2Angle); ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, next.x, next.y); } if (fill && fillStyle && points.length > 0) { const lastAngle = startAngle + (pointCount - 1) * angleStep; ctx.lineTo(centerX + radius * Math.cos(lastAngle), centerY + radius * Math.sin(lastAngle)); if (Math.abs(endAngle - startAngle) >= 2 * Math.PI - .001) ctx.arc(centerX, centerY, radius, lastAngle, startAngle, true); else { const startPointOnBase = polarToCartesian(centerX, centerY, radius, startAngle); ctx.lineTo(startPointOnBase.x, startPointOnBase.y); } ctx.closePath(); ctx.fillStyle = fillStyle; ctx.fill(); ctx.beginPath(); ctx.moveTo(points[0].x, points[0].y); for (let i = 1; i < points.length - 1; i++) { const curr = points[i]; const next = points[i + 1]; const currAngle = startAngle + i * angleStep; const nextAngle = startAngle + (i + 1) * angleStep; const currRadius = Math.sqrt((curr.x - centerX) ** 2 + (curr.y - centerY) ** 2); const nextRadius = Math.sqrt((next.x - centerX) ** 2 + (next.y - centerY) ** 2); const cp1Angle = currAngle + (nextAngle - currAngle) * smoothness; const cp1 = polarToCartesian(centerX, centerY, currRadius + (nextRadius - currRadius) * smoothness, cp1Angle); const cp2Angle = nextAngle - (nextAngle - currAngle) * smoothness; const cp2 = polarToCartesian(centerX, centerY, nextRadius - (nextRadius - currRadius) * smoothness, cp2Angle); ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, next.x, next.y); } } ctx.stroke(); ctx.save(); ctx.shadowColor = "transparent"; ctx.shadowBlur = 0; ctx.strokeStyle = Array.isArray(color) ? color[0] : `${color}AA`; ctx.lineWidth = width; ctx.beginPath(); ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); ctx.stroke(); ctx.restore(); ctx.shadowColor = "transparent"; ctx.shadowBlur = 0; } const { pause, resume, isActive } = useRafFn(() => { draw(); }, { immediate: !manual }); return { canvasRef, pause, resume, isActive }; } //#endregion export { useSpectrum };