UNPKG

@nafr/echo-ui

Version:

A UI library born for WAA

144 lines (143 loc) 5.84 kB
import { jsx as _jsx } from "react/jsx-runtime"; import * as d3 from 'd3'; import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react'; import { useResizeObserver } from '../../../lib/hooks/useResizeObserver'; import { cn, validValue } from '../../../lib/utils'; import { useStyle } from './styles'; import { TYPE, DELAY, WIDTH, HEIGHT, LINE_WIDTH, LINE_COLOR, AMPLITUDE, FREQUENCY, } from './constants'; export const LFO = forwardRef((props, ref) => { const { type = TYPE, frequency = FREQUENCY, amplitude: _anplitude = AMPLITUDE, delay: _delay = DELAY, lineWidth = LINE_WIDTH, lineColor = LINE_COLOR, ...restProps } = props; const amplitude = validValue(_anplitude, 0, 1); const delay = validValue(_delay, 0, 1000); const LFORef = useRef(null); const xScale = useRef(null); const yScale = useRef(null); useImperativeHandle(ref, () => LFORef.current); useEffect(() => { generateScales(); generateWave(); }, [frequency, amplitude, delay, type, lineWidth, lineColor]); const dimensions = useResizeObserver(LFORef, WIDTH, HEIGHT, () => { generateScales(); generateWave(); }, true, { frequency, amplitude, delay, }); const generateScales = () => { const { width, height } = dimensions.current; xScale.current = d3.scaleLinear().domain([0, 1]).range([0, width]); yScale.current = d3.scaleLinear().domain([0, 1]).range([height, 0]); }; const generateWave = () => { if (type !== 'sine' && type !== 'square' && type !== 'triangle') return; const { width, height } = dimensions.current; const svg = d3.select(LFORef.current).select('svg').attr('width', width).attr('height', height); svg.selectAll('*').remove(); // Draw the center line if the amplitude is 0 if (frequency === 0) { svg .append('line') .attr('x1', 0) .attr('y1', height / 2) .attr('x2', width) .attr('y2', height / 2) .attr('stroke', lineColor) .attr('stroke-width', lineWidth); } // Draw the delay line const delayInSeconds = delay / 1000; if (delay > 0) { svg .append('circle') .attr('cx', delayInSeconds) .attr('cy', yScale.current(0.5)) .attr('r', lineWidth / 2) .attr('fill', lineColor); svg .append('line') .attr('x1', 0) .attr('y1', yScale.current(0.5)) .attr('x2', xScale.current(delayInSeconds) + 1) .attr('y2', yScale.current(0.5)) .attr('stroke', lineColor) .attr('stroke-width', lineWidth); } let data = []; switch (type) { case 'sine': data = generateSineWaveData; break; case 'square': data = generateSquareWaveData; break; case 'triangle': data = generateTriangleWaveData; break; default: break; } const line = d3 .line() .x((d) => xScale.current(d.x)) .y((d) => yScale.current(d.y)); svg .append('path') .datum(data) .attr('fill', 'none') .attr('stroke', lineColor) .attr('stroke-width', lineWidth) .attr('d', line) .attr('transform', `translate(${delayInSeconds * width}, 0)`); }; const generateSineWaveData = useMemo(() => { if (type !== 'sine') return []; return d3.range(0, 1, 0.001).map((x) => { const adjustedX = x * 2 * Math.PI * frequency; const y = Math.sin(adjustedX) * amplitude * 0.5 + 0.5; return { x, y }; }); }, [type, frequency, amplitude]); const generateSquareWaveData = useMemo(() => { if (type !== 'square') return []; const centerY = 0.5; const halfHeight = centerY * amplitude; return [{ x: 0, y: centerY }].concat(d3.range(0, 1, 0.001).map((x) => { const adjustedX = x * 2 * Math.PI * frequency; const y = Math.floor(adjustedX / Math.PI) % 2 === 0 ? centerY + halfHeight : centerY - halfHeight; return { x, y }; })); }, [type, frequency, amplitude]); const generateTriangleWaveData = useMemo(() => { if (type !== 'triangle') return []; const centerY = 0.5; return d3.range(0, 1, 0.001).map((x) => { // Adjust the phase so that the waveform starts in the middle of the rising segment // and ensure that the starting point is at the center of the Y-axis // By mapping the x value to the corresponding position in a cycle, // the waveform is in the middle of the rising segment when it starts const adjustedX = x * 2 * Math.PI * frequency; const phase = ((adjustedX + Math.PI / 2) % (2 * Math.PI)) / Math.PI; let y; if (phase < 1) y = phase * amplitude; else y = (2 - phase) * amplitude; y = y - amplitude / 2 + centerY; return { x, y }; }); }, [type, frequency, amplitude]); const { base, svg } = useStyle(); return (_jsx("div", { ref: LFORef, className: cn(base(), restProps.className), style: { ...restProps.style, padding: 0, overflow: 'hidden', pointerEvents: 'none', userSelect: 'none', }, children: _jsx("svg", { className: cn(svg()) }) })); });