import type { FC } from '../../lib/teact/teact'; import React, { memo, useEffect, useRef } from '../../lib/teact/teact'; import { requestMutation } from '../../lib/fasterdom/fasterdom'; import { animate, timingFunctions } from '../../util/animation'; import buildClassName from '../../util/buildClassName'; import useDynamicColorListener from '../../hooks/stickers/useDynamicColorListener'; import { useStateRef } from '../../hooks/useStateRef'; import useDevicePixelRatio from '../../hooks/window/useDevicePixelRatio'; import Icon from '../common/Icon'; import './ProgressSpinner.scss'; const SIZES = { s: 42, m: 48, l: 54, xl: 52, }; const STROKE_WIDTH = 2; const STROKE_WIDTH_XL = 3; const PADDING = 2; const MIN_PROGRESS = 0.05; const MAX_PROGRESS = 1; const GROW_DURATION = 600; // 0.6 s const ROTATE_DURATION = 2000; // 2 s const ProgressSpinner: FC<{ progress?: number; size?: 's' | 'm' | 'l' | 'xl'; square?: boolean; transparent?: boolean; noCross?: boolean; rotationOffset?: number; withColor?: boolean; onClick?: (e: React.MouseEvent) => void; }> = ({ progress = 0, size = 'l', square, transparent, noCross, rotationOffset, withColor, onClick, }) => { // eslint-disable-next-line no-null/no-null const canvasRef = useRef(null); const width = SIZES[size]; const progressRef = useStateRef(progress); const dpr = useDevicePixelRatio(); const color = useDynamicColorListener(canvasRef, !withColor); useEffect(() => { let isFirst = true; let growFrom = MIN_PROGRESS; let growStartedAt: number | undefined; let prevProgress: number | undefined; animate(() => { if (!canvasRef.current) { return false; } if (progressRef.current !== prevProgress) { growFrom = Math.min(Math.max(MIN_PROGRESS, prevProgress || 0), MAX_PROGRESS); growStartedAt = Date.now(); prevProgress = progressRef.current; } const targetProgress = Math.min(Math.max(MIN_PROGRESS, progressRef.current), MAX_PROGRESS); const t = Math.min(1, (Date.now() - growStartedAt!) / GROW_DURATION); const animationFactor = timingFunctions.easeOutQuad(t); const currentProgress = growFrom + (targetProgress - growFrom) * animationFactor; drawSpinnerArc( canvasRef.current, width * dpr, (size === 'xl' ? STROKE_WIDTH_XL : STROKE_WIDTH) * dpr, color ?? 'white', currentProgress, dpr, isFirst, rotationOffset, ); isFirst = false; return currentProgress < 1; }, requestMutation); }, [progressRef, size, width, dpr, rotationOffset, color]); const className = buildClassName( `ProgressSpinner size-${size}`, transparent && 'transparent', square && 'square', noCross && 'no-cross', ); return (
{!noCross && }
); }; function drawSpinnerArc( canvas: HTMLCanvasElement, size: number, strokeWidth: number, color: string, progress: number, dpr: number, shouldInit = false, rotationOffset?: number, ) { const centerCoordinate = size / 2; const radius = (size - strokeWidth) / 2 - PADDING * dpr; const offset = rotationOffset ?? (Date.now() % ROTATE_DURATION) / ROTATE_DURATION; const startAngle = (2 * Math.PI) * offset; const endAngle = startAngle + (2 * Math.PI) * progress; const ctx = canvas.getContext('2d')!; if (shouldInit) { canvas.width = size; canvas.height = size; ctx.lineCap = 'round'; ctx.strokeStyle = color; ctx.lineWidth = strokeWidth; } ctx.clearRect(0, 0, size, size); ctx.beginPath(); ctx.arc(centerCoordinate, centerCoordinate, radius, startAngle, endAngle); ctx.stroke(); } export default memo(ProgressSpinner);