chore: slider to change width and height in image

This commit is contained in:
jialin
2024-12-02 08:20:30 +08:00
parent 412c6dadca
commit ec2cd29096
24 changed files with 500 additions and 283 deletions
@@ -6,5 +6,6 @@
canvas {
display: block;
image-rendering: crisp-edges;
}
}
+66 -28
View File
@@ -4,19 +4,40 @@ import './index.less';
interface AudioAnimationProps {
width: number;
height: number;
scaleFactor?: number;
analyserData: {
data: Uint8Array;
analyser: any;
};
}
const AudioAnimation: React.FC<AudioAnimationProps> = (props) => {
const { width, height, analyserData } = props;
const AudioAnimation: React.FC<AudioAnimationProps> = ({
width,
height,
scaleFactor = 1.2,
analyserData
}) => {
const canvasRef = React.useRef<HTMLCanvasElement>(null);
const animationId = React.useRef<number>(0);
const isScaled = React.useRef<boolean>(false);
const oscillationOffset = React.useRef(0);
const direction = React.useRef(1);
const maxBarCount = 128;
const calculateJitter = (
i: number,
timestamp: number,
baseHeight: number,
minJitter: number,
jitterAmplitude: number
) => {
//
const jitterFactor = Math.sin(timestamp / 200 + i) * 0.5 + 0.5;
const jitter =
minJitter +
jitterFactor * (jitterAmplitude - minJitter) * (baseHeight / maxBarCount);
return jitter;
};
const startAudioVisualization = () => {
if (!canvasRef.current || !analyserData.data?.length) return;
@@ -33,42 +54,51 @@ const AudioAnimation: React.FC<AudioAnimationProps> = (props) => {
isScaled.current = true;
}
const barWidth = 3;
const barSpacing = 2;
const centerX = HEIGHT / 2;
const barWidth = 4;
const barSpacing = 6;
const centerLine = Math.floor(HEIGHT / 2);
const jitterAmplitude = 60; // 最大抖动幅度
const minJitter = 15; // 最小抖动幅度
const frameInterval = 2;
let frameCount = 0;
const jitterAmplitude = 40;
const minJitter = 10;
canvasCtx.fillStyle = '#0073EF';
let lastFrameTime = 0;
const draw = () => {
frameCount++;
if (frameCount % frameInterval !== 0) {
const gradient = canvasCtx.createLinearGradient(0, 0, 0, HEIGHT);
gradient.addColorStop(0, '#007BFF');
gradient.addColorStop(1, '#0069DA');
canvasCtx.fillStyle = gradient;
const draw = (timestamp: number) => {
const elapsed = timestamp - lastFrameTime;
if (elapsed < 16) {
animationId.current = requestAnimationFrame(draw);
return;
}
lastFrameTime = timestamp;
analyserData.analyser?.current?.getByteFrequencyData(analyserData.data);
canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
const barCount = analyserData.data.length;
const barCount = Math.min(maxBarCount, analyserData.data.length);
const totalWidth = barCount * (barWidth + barSpacing) - barSpacing;
let x = centerX - totalWidth / 2 + oscillationOffset.current;
oscillationOffset.current += direction.current * 0.5;
let x = WIDTH / 2 - totalWidth / 2 + oscillationOffset.current;
if (oscillationOffset.current > 20 || oscillationOffset.current < -20) {
oscillationOffset.current += direction.current;
if (Math.abs(oscillationOffset.current) > 20) {
direction.current *= -1;
}
for (let i = 0; i < barCount; i++) {
const baseHeight = Math.floor(analyserData.data[i] / 2);
const jitter =
minJitter +
Math.round((Math.random() - 0.5) * (jitterAmplitude - minJitter));
const barHeight = baseHeight + jitter;
const baseHeight = Math.floor(analyserData.data[i] / 2) * scaleFactor;
const jitter = calculateJitter(
i,
timestamp,
baseHeight,
minJitter,
jitterAmplitude
);
const barHeight = baseHeight + Math.round(jitter);
const topY = Math.round(centerLine - barHeight / 2);
const bottomY = Math.round(centerLine + barHeight / 2);
@@ -87,21 +117,29 @@ const AudioAnimation: React.FC<AudioAnimationProps> = (props) => {
animationId.current = requestAnimationFrame(draw);
};
draw();
draw(performance.now());
};
useEffect(() => {
if (!analyserData.data?.length || !analyserData.analyser.current) {
canvasRef.current
?.getContext('2d')
?.clearRect(0, 0, width * 2, height * 2);
const clearCanvas = () => {
if (canvasRef.current) {
const ctx = canvasRef.current.getContext('2d');
if (ctx) ctx.clearRect(0, 0, width * 2, height * 2);
}
};
if (!analyserData.data?.length || !analyserData.analyser?.current) {
clearCanvas();
cancelAnimationFrame(animationId.current);
animationId.current = 0;
return;
}
startAudioVisualization();
return () => {
if (animationId.current) cancelAnimationFrame(animationId.current);
cancelAnimationFrame(animationId.current);
clearCanvas();
};
}, [analyserData, width, height]);
+6
View File
@@ -75,6 +75,12 @@
.file-name {
line-height: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.ant-slider-horizontal {
+13 -55
View File
@@ -53,7 +53,7 @@ const AudioPlayer: React.FC<AudioPlayerProps> = forwardRef((props, ref) => {
});
const [playOn, setPlayOn] = React.useState<boolean>(false);
const [speakerOn, setSpeakerOn] = React.useState<boolean>(false);
const [volume, setVolume] = React.useState<number>(0.5);
const [volume, setVolume] = React.useState<number>(1);
const [speed, setSpeed] = React.useState<number>(defaultSpeed);
const timer = React.useRef<any>(null);
@@ -98,12 +98,12 @@ const AudioPlayer: React.FC<AudioPlayerProps> = forwardRef((props, ref) => {
}, []);
const handlePlay = useCallback(() => {
setPlayOn(!playOn);
if (playOn) {
audioRef.current?.pause();
} else {
audioRef.current?.play();
}
setPlayOn(!playOn);
}, [playOn]);
const handleFormatVolume = (val?: number) => {
@@ -118,10 +118,12 @@ const AudioPlayer: React.FC<AudioPlayerProps> = forwardRef((props, ref) => {
setVolume(round(value, 2));
}, []);
const initPlayerConfig = useCallback(() => {
audioRef.current!.volume = volume;
audioRef.current!.playbackRate = speed;
}, []);
const initPlayerConfig = () => {
if (audioRef.current) {
audioRef.current!.volume = volume;
audioRef.current!.playbackRate = speed;
}
};
const handleLoadedMetadata = useCallback(
(data: any) => {
@@ -168,6 +170,10 @@ const AudioPlayer: React.FC<AudioPlayerProps> = forwardRef((props, ref) => {
});
};
const handleOnLoad = (e: any) => {
console.log('onload', e);
};
useEffect(() => {
if (audioRef.current) {
initPlayerConfig();
@@ -195,6 +201,7 @@ const AudioPlayer: React.FC<AudioPlayerProps> = forwardRef((props, ref) => {
<Slider
tooltip={{ open: false }}
min={0}
step={1}
max={audioState.duration}
value={audioState.currentTime}
onChange={handleCurrentChange}
@@ -203,23 +210,6 @@ const AudioPlayer: React.FC<AudioPlayerProps> = forwardRef((props, ref) => {
<span className="time">{formatTime(audioState.duration)}</span>
</div>
<div className="controls">
{/* <Tooltip
overlayInnerStyle={{
backgroundColor: 'var(--color-white-1)'
}}
arrow={false}
title={
<CheckButtons
options={speedOptions}
onChange={handleSeepdChange}
size="small"
></CheckButtons>
}
>
<span style={{ cursor: 'pointer' }}>
{speed ? `${speed}x` : '1x'}
</span>
</Tooltip> */}
<Tooltip
title={intl.formatMessage({
id: 'playground.audio.button.slow'
@@ -276,38 +266,6 @@ const AudioPlayer: React.FC<AudioPlayerProps> = forwardRef((props, ref) => {
</div>
</div>
</div>
{/* <span className="speaker">
<Button
size="middle"
type="text"
icon={
volume > 0 ? (
<IconFont
type="icon-SpeakerHigh"
style={{ fontSize: '22px' }}
></IconFont>
) : (
<IconFont
type="icon-speaker-slash"
style={{ fontSize: '22px' }}
></IconFont>
)
}
></Button>
{speakerOn && (
<Slider
tooltip={{ formatter: handleFormatVolume }}
style={{ height: '100px' }}
className="volume-slider"
min={0}
max={1}
step={0.01}
value={volume}
vertical
onChange={handleVolumeChange}
/>
)}
</span> */}
</div>
<audio
controls
+1 -1
View File
@@ -69,7 +69,7 @@
left: 0;
bottom: 0;
right: 0;
backdrop-filter: blur(100px);
filter: blur(100px);
backdrop-filter: blur(100px);
z-index: 5;
}
+7 -1
View File
@@ -109,7 +109,13 @@ const SingleImage: React.FC<SingleImageProps> = (props) => {
overflow: 'hidden'
}}
>
<Progress percent={progress} type="circle" />
<Progress
percent={progress}
type="dashboard"
steps={{ count: 50, gap: 2 }}
format={() => <span className="font-size-20">{progress}%</span>}
trailColor="var(--ant-color-fill-secondary)"
/>
</span>
) : (
<span
@@ -1,6 +1,7 @@
import { Checkbox, Slider } from 'antd';
import { Checkbox } from 'antd';
import SealInput from '../seal-input';
import SealSelect from '../seal-select';
import Slider from '../seal-slider';
const components: {
InputNumber: typeof SealInput.Number;
@@ -3,6 +3,7 @@ import { useIntl } from '@umijs/max';
import React, { useCallback, useMemo } from 'react';
import LabelInfo from './components/label-info';
import componentsMap from './config/components';
const FieldComponent: React.FC<ParamsSchema> = (props) => {
const intl = useIntl();
const { type, label, attrs, style, value, ...rest } = props;
+114
View File
@@ -0,0 +1,114 @@
import { INPUT_WIDTH } from '@/constants';
import { InfoCircleOutlined } from '@ant-design/icons';
import {
Form,
InputNumber,
Slider,
Tooltip,
type SliderSingleProps
} from 'antd';
import React from 'react';
import FieldWrapper from './field-wrapper';
import SliderStyles from './styles/slider.less';
interface SealSliderProps extends SliderSingleProps {
required?: boolean;
label?: React.ReactNode;
labelWidth?: number | string;
description?: string;
isInFormItems?: boolean;
inputnumber?: boolean;
checkStatus?: 'success' | 'error' | 'warning' | '';
}
const SealSlider: React.FC<SealSliderProps> = (props) => {
const {
label,
value,
required,
description,
isInFormItems = true,
max,
min,
step,
defaultValue,
checkStatus,
inputnumber = false,
labelWidth,
tooltip = { open: false },
...rest
} = props;
let status = '';
if (isInFormItems) {
const statusData = Form?.Item?.useStatus?.();
status = statusData?.status || '';
}
const handleChange = (value: number) => {
props.onChange?.(value);
};
const handleInput = (value: number | null) => {
const newValue = value || 0;
props.onChange?.(newValue);
};
const renderLabel = React.useMemo(() => {
return (
<span
className={SliderStyles['slider-label']}
style={{ width: labelWidth || INPUT_WIDTH.mini }}
>
<span className="text">
{description ? (
<Tooltip title={description}>
<span> {label}</span>
<span className="m-l-5">
<InfoCircleOutlined />
</span>
</Tooltip>
) : (
<span>{label}</span>
)}
</span>
{inputnumber ? (
<InputNumber
className="label-val"
variant="outlined"
size="small"
value={value}
controls={false}
onChange={handleInput}
></InputNumber>
) : (
<span className="val">{value}</span>
)}
</span>
);
}, [label, labelWidth, description, value, max, min, step, defaultValue]);
return (
<FieldWrapper
required={required}
status={checkStatus || status}
label={renderLabel}
style={{ padding: '20px 2px 0' }}
variant="borderless"
>
<Slider
{...rest}
defaultValue={defaultValue}
max={max}
min={min}
step={step}
style={{ marginBottom: 0, marginTop: 16, marginInline: 0 }}
tooltip={tooltip}
value={value}
onChange={handleChange}
></Slider>
</FieldWrapper>
);
};
export default SealSlider;
@@ -0,0 +1,24 @@
:local(.slider-label) {
display: flex;
justify-content: space-between;
align-items: flex-start;
width: 100%;
:global(.val) {
color: var(--ant-color-text);
}
:global(.label-val) {
position: absolute !important;
top: -14px;
right: -14px;
width: 80px;
border-radius: var(--border-radius-base);
text-align: center;
border: 1px solid var(--ant-color-border) !important;
:global(.ant-input-number-input) {
text-align: center !important;
}
}
}
+99 -21
View File
@@ -1,10 +1,11 @@
import React, {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useRef
} from 'react';
import useWavesurfer from './hooks/use-wavesurfer';
import WaveSurfer, { WaveSurferOptions } from 'wavesurfer.js';
interface AudioPlayerProps {
autoplay: boolean;
@@ -15,27 +16,104 @@ interface AudioPlayerProps {
width?: number;
onReady?: () => void;
onClick?: (value: number) => void;
onFinish?: () => void;
onAnalyse?: (analyseData: any, frequencyBinCount: any) => void;
}
const AudioPlayer: React.FC<AudioPlayerProps> = forwardRef((props, ref) => {
const AudioPlayer: React.FC<
AudioPlayerProps & Omit<WaveSurferOptions, 'container'>
> = forwardRef((props, ref) => {
const { autoplay, audioUrl, speed = 1, ...rest } = props;
const container = useRef<HTMLDivElement>(null);
const {
createWavesurfer,
play,
pause,
duration,
destroyWavesurfer,
wavesurfer
} = useWavesurfer({
container,
autoplay: autoplay,
url: audioUrl,
audioRate: speed,
onReady: props.onReady,
onClick: props.onClick,
...rest
});
const wavesurfer = useRef<WaveSurfer | null>(null);
const container = useRef<any>(null);
const audioContext = useRef<any>(null);
const analyser = useRef<any>(null);
const dataArray = useRef<any>(null);
const audioStream = useRef<any>(null);
const mediaElement = useRef<any>(null);
const initAudioContext = useCallback(() => {
audioContext.current = new (window.AudioContext ||
window.webkitAudioContext)();
analyser.current = audioContext.current.createAnalyser();
analyser.current.fftSize = 512;
dataArray.current = new Uint8Array(analyser.current.frequencyBinCount);
}, []);
const generateVisualData = useCallback(() => {
const source = audioContext.current.createMediaElementSource(
mediaElement.current
);
source.connect(analyser.current);
analyser.current.connect(audioContext.current.destination);
}, []);
const listenEvents = () => {
wavesurfer.current?.on('ready', () => {
props.onReady?.();
});
wavesurfer.current?.on('click', (value) => {
props.onClick?.(value);
});
wavesurfer.current?.on('finish', () => {
props.onFinish?.();
});
wavesurfer.current?.on('play', () => {
// analyser.current?.getByteFrequencyData(dataArray.current);
// props.onAnalyse?.(dataArray.current, analyser);
});
};
const createWavesurfer = () => {
wavesurfer.current = WaveSurfer.create({
container: container.current,
url: audioUrl,
autoplay: autoplay,
audioRate: speed,
waveColor: '#4096ff',
progressColor: 'rgb(100, 0, 100)',
height: 60,
barWidth: 2,
barGap: 1,
barRadius: 2,
interact: true,
cursorWidth: 0,
...rest
});
mediaElement.current = wavesurfer.current?.getMediaElement();
initAudioContext();
generateVisualData();
listenEvents();
};
const destroyWavesurfer = () => {
if (wavesurfer.current) {
wavesurfer.current.destroy();
}
};
const play = () => {
if (wavesurfer.current) {
wavesurfer.current.play();
}
};
const duration = () => {
if (wavesurfer.current) {
return wavesurfer.current.getDuration();
}
return 0;
};
const pause = () => {
if (wavesurfer.current) {
wavesurfer.current.pause();
}
};
useImperativeHandle(ref, () => {
return {
@@ -46,13 +124,13 @@ const AudioPlayer: React.FC<AudioPlayerProps> = forwardRef((props, ref) => {
});
useEffect(() => {
if (container.current) {
if (container.current && audioUrl) {
createWavesurfer();
}
return () => {
destroyWavesurfer();
};
}, [container.current]);
}, [audioUrl, container.current]);
return <div ref={container} className="audio-container"></div>;
});
@@ -1,86 +0,0 @@
import { useRef } from 'react';
import WaveSurfer from 'wavesurfer.js';
interface Options {
container: React.RefObject<HTMLDivElement>;
waveColor?: string;
progressColor?: string;
url: string;
barWidth?: number;
barGap?: number;
barRadius?: number;
autoplay?: boolean;
audioRate?: number;
onReady?: () => void;
onClick: (value: number) => void;
}
const useWavesurfer = (options: Options) => {
const wavesurfer = useRef<WaveSurfer | null>(null);
const { container, url, ...rest } = options;
const createWavesurfer = () => {
if (!container.current) {
return;
}
if (wavesurfer.current) {
wavesurfer.current.destroy();
}
wavesurfer.current = WaveSurfer.create({
container: container.current,
waveColor: '#4096ff',
progressColor: 'rgb(100, 0, 100)',
url: url,
height: 60,
barWidth: 2,
barGap: 1,
barRadius: 2,
interact: true,
cursorWidth: 0,
...rest
});
wavesurfer.current?.on('ready', () => {
options.onReady?.();
});
wavesurfer.current?.on('click', (value) => {
options.onClick?.(value);
});
};
const destroyWavesurfer = () => {
if (wavesurfer.current) {
wavesurfer.current.destroy();
}
};
const play = () => {
if (wavesurfer.current) {
wavesurfer.current.play();
}
};
const duration = () => {
if (wavesurfer.current) {
return wavesurfer.current.getDuration();
}
return 0;
};
const pause = () => {
if (wavesurfer.current) {
wavesurfer.current.pause();
}
};
return {
createWavesurfer,
play,
pause,
wavesurfer,
duration,
destroyWavesurfer
};
};
export default useWavesurfer;
+21 -3
View File
@@ -37,9 +37,13 @@ interface SpeechContentProps {
const SpeechItem: React.FC<SpeechContentProps> = (props) => {
const intl = useIntl();
const [collapsed, setCollapsed] = useState(false);
const [isPlay, setIsPlay] = useState(false);
const [isPlay, setIsPlay] = useState(props.autoplay);
const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState(0);
const [audioChunks, setAudioChunks] = useState<any>({
data: [],
analyser: null
});
const ref = useRef<any>(null);
const handlePlay = () => {
@@ -52,8 +56,13 @@ const SpeechItem: React.FC<SpeechContentProps> = (props) => {
setIsPlay(true);
};
const handleCollapse = () => {
setCollapsed(!collapsed);
const handleOnAnalyse = (data: any, analyser: any) => {
setAudioChunks((pre: any) => {
return {
data: data,
analyser: analyser
};
});
};
const handleReay = () => {
@@ -80,6 +89,13 @@ const SpeechItem: React.FC<SpeechContentProps> = (props) => {
return (
<div>
<div className="speech-item">
{/* {isPlay && (
<AudioAnimation
height={82}
width={500}
analyserData={audioChunks}
></AudioAnimation>
)} */}
<div className="voice">
<IconFont type="icon-user_voice" className="font-size-16" />
</div>
@@ -89,6 +105,8 @@ const SpeechItem: React.FC<SpeechContentProps> = (props) => {
audioUrl={props.audioUrl}
onReady={handleReay}
onClick={handleOnClick}
onFinish={() => setIsPlay(false)}
onAnalyse={handleOnAnalyse}
ref={ref}
></AudioPlayer>
</div>