UNPKG

@nafr/echo-ui

Version:

A UI library born for WAA

132 lines (131 loc) 6 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import * as d3 from 'd3'; import { forwardRef, useImperativeHandle, useRef, useEffect, useState, useMemo } from 'react'; import { useResizeObserver } from '../../../lib/hooks'; import { cn, fixTo, formatTime } from '../../../lib/utils'; import { useStyle } from './styles'; import { WIDTH, HEIGHT, WAVE_COLOR, MASK_COLOR, WAVE_HEIGHT, CURSOR_WIDTH, CURSOR_COLOR, ANIMATION_DURATION, } from './constants'; export const Waveform = forwardRef((props, ref) => { const { data: _data, audioDuration = 0, percentage: _percentage = 0, hideCursor = false, cursorWidth = CURSOR_WIDTH, cursorColor = CURSOR_COLOR, hideCursorLabel = false, waveHeight = WAVE_HEIGHT, waveColor = WAVE_COLOR, maskColor = MASK_COLOR, disableAnimation = false, animationDuration = ANIMATION_DURATION, onMouseMove, onMouseLeave, onClick, ...restProps } = props; useImperativeHandle(ref, () => waveformRef.current); const data = useRef(null); const waveformRef = useRef(null); const waveRef = useRef(null); const maskRef = useRef(null); const xScale = useRef(null); const yScale = useRef(null); const initialized = useRef(false); const [percentage, setPercentage] = useState(_percentage); const [cursorX, setCursorX] = useState(null); const [isHover, setIsHover] = useState(false); const [hoverTime, setHoverTime] = useState(0); const dimensions = useResizeObserver(waveformRef, WIDTH, HEIGHT, () => { generateScales(); generateWave(); generateMask(); }, initialized.current); useEffect(() => { if (_data.length === 2) data.current = _data; else data.current = [..._data, ..._data]; generateScales(); generateWave(); generateMask(); }, [_data]); useEffect(() => { setPercentage(_percentage); }, [_percentage]); const generateScales = () => { const { width, height } = dimensions.current; const length = data.current?.length ? data.current?.[0].length : 0; xScale.current = d3.scaleLinear().domain([0, length]).range([0, width]); yScale.current = d3 .scaleLinear() .domain([-1, 1]) .range([height - height * ((100 - waveHeight) / 200), height * ((100 - waveHeight) / 200)]); }; const generateWave = () => { generatePath(waveRef, waveColor); }; const generateMask = () => { generatePath(maskRef, maskColor); }; const generatePath = (svg, color) => { if (!svg.current || !data.current || data.current.length !== 2) return; const _svg = d3.select(svg.current); _svg.selectAll('*').remove(); const { width, height } = dimensions.current; _svg.attr('width', width).attr('height', height); const area = d3.area().x((_, i) => xScale.current(i)); if (initialized.current || disableAnimation) { area.y0((d) => yScale.current(-d)).y1((d) => yScale.current(d)); } else { area.y0(() => yScale.current(0)).y1(() => yScale.current(0)); } const [pathA, pathB] = [data.current[0], data.current[1]].map((d) => _svg .append('path') .datum(d) .attr('class', 'echo-path-area') .attr('d', area) .attr('fill', color)); if (!initialized.current) { // eslint-disable-next-line no-extra-semi ; [pathA, pathB].forEach((path) => { path .transition() .duration(animationDuration) .attr('d', area.y0((d) => yScale.current(-d)).y1((d) => yScale.current(d))) .on('end', () => { initialized.current = true; }); }); } }; // ============================== // Mouse Events // ============================== const handleMouseMove = (e) => { const { left, width } = waveformRef.current.getBoundingClientRect(); const x = Math.max(0, Math.min(width, e.clientX - left)); setIsHover(true); setCursorX(x); setHoverTime(fixTo((x / width) * audioDuration, 0)); onMouseMove?.({ time: (x / width) * audioDuration, percentage: (x / width) * 100, nativeEvent: e, }); }; const handleMouseLeave = (e) => { setIsHover(false); onMouseLeave?.(e); }; const handleMouseClick = (e) => { const { width } = waveformRef.current.getBoundingClientRect(); const newPercentage = fixTo((cursorX / width) * 100); setPercentage(newPercentage); onClick?.({ time: (newPercentage / 100) * audioDuration, percentage: newPercentage, nativeEvent: e, }); }; const cursorStyle = useMemo(() => { return { width: cursorWidth, left: cursorX ? cursorX - cursorWidth / 2 : 0, backgroundColor: isHover ? cursorColor : 'transparent', }; }, [isHover, cursorX, cursorWidth, cursorColor]); const { base, svg, cursor, label } = useStyle(); return (_jsxs("div", { ...restProps, ref: waveformRef, className: cn(base(), restProps.className), style: { ...restProps.style, userSelect: 'none', }, onClick: handleMouseClick, onMouseMove: handleMouseMove, onMouseLeave: handleMouseLeave, children: [_jsx("svg", { ref: waveRef, className: cn(svg()) }), _jsx("svg", { ref: maskRef, className: cn(svg()), style: { clipPath: `inset(0 ${100 - percentage}% 0 0)`, } }), !hideCursor && initialized.current && (_jsx("div", { className: cn(cursor()), style: cursorStyle, children: !hideCursorLabel && (_jsx("span", { className: cn(label()), style: { color: isHover ? cursorColor : 'transparent' }, children: formatTime(hoverTime) })) }))] })); });