@oiij/use
Version:
Som Composable Functions for Vue 3
297 lines (295 loc) • 11.9 kB
JavaScript
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 };