Progress Spinner: Replace SVG+CSS with Canvas
This commit is contained in:
parent
b6b2ed0379
commit
f46c563d6d
@ -50,38 +50,17 @@
|
|||||||
width: 3.25rem;
|
width: 3.25rem;
|
||||||
height: 3.25rem;
|
height: 3.25rem;
|
||||||
|
|
||||||
svg {
|
&_canvas {
|
||||||
width: 3rem;
|
|
||||||
height: 3rem;
|
|
||||||
margin: 0.125rem;
|
margin: 0.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
circle {
|
|
||||||
stroke-width: 3px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.transparent {
|
&.transparent {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
&_canvas {
|
||||||
display: block;
|
display: block;
|
||||||
transform: rotate(-90deg);
|
background-color: transparent !important;
|
||||||
transform-origin: 50% 50%;
|
|
||||||
animation: 4s linear 0s infinite ProgressSpinnerAnimation;
|
|
||||||
}
|
|
||||||
|
|
||||||
circle {
|
|
||||||
transition: stroke-dashoffset 0.5s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes ProgressSpinnerAnimation {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,24 @@
|
|||||||
import React, { memo } from '../../lib/teact/teact';
|
|
||||||
import type { FC } from '../../lib/teact/teact';
|
import type { FC } from '../../lib/teact/teact';
|
||||||
|
import React, { memo, useEffect, useRef } from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import { DPR } from '../../util/windowEnvironment';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
|
||||||
import './ProgressSpinner.scss';
|
import './ProgressSpinner.scss';
|
||||||
|
import { animate, timingFunctions } from '../../util/animation';
|
||||||
|
import { requestMutation } from '../../lib/fasterdom/fasterdom';
|
||||||
|
import { useStateRef } from '../../hooks/useStateRef';
|
||||||
|
|
||||||
const RADIUSES = {
|
const SIZES = {
|
||||||
s: 22, m: 25, l: 28, xl: 20,
|
s: 42, m: 48, l: 54, xl: 52,
|
||||||
};
|
};
|
||||||
const STROKE_WIDTH = 2;
|
const STROKE_WIDTH = 2 * DPR;
|
||||||
|
const STROKE_WIDTH_XL = 3 * DPR;
|
||||||
|
const PADDING = 2 * DPR;
|
||||||
const MIN_PROGRESS = 0.05;
|
const MIN_PROGRESS = 0.05;
|
||||||
const MAX_PROGRESS = 1;
|
const MAX_PROGRESS = 1;
|
||||||
|
const GROW_DURATION = 600; // 0.6 s
|
||||||
|
const ROTATE_DURATION = 2000; // 2 s
|
||||||
|
|
||||||
const ProgressSpinner: FC<{
|
const ProgressSpinner: FC<{
|
||||||
progress?: number;
|
progress?: number;
|
||||||
@ -27,11 +35,47 @@ const ProgressSpinner: FC<{
|
|||||||
noCross,
|
noCross,
|
||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
const radius = RADIUSES[size];
|
// eslint-disable-next-line no-null/no-null
|
||||||
const circleRadius = radius - STROKE_WIDTH * 2;
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const borderRadius = radius - 1;
|
const width = SIZES[size];
|
||||||
const circumference = circleRadius * 2 * Math.PI;
|
const progressRef = useStateRef(progress);
|
||||||
const strokeDashOffset = circumference - Math.min(Math.max(MIN_PROGRESS, progress), MAX_PROGRESS) * circumference;
|
|
||||||
|
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,
|
||||||
|
'white',
|
||||||
|
currentProgress,
|
||||||
|
isFirst,
|
||||||
|
);
|
||||||
|
|
||||||
|
isFirst = false;
|
||||||
|
|
||||||
|
return currentProgress < 1;
|
||||||
|
}, requestMutation);
|
||||||
|
}, [progressRef, size, width]);
|
||||||
|
|
||||||
const className = buildClassName(
|
const className = buildClassName(
|
||||||
`ProgressSpinner size-${size}`,
|
`ProgressSpinner size-${size}`,
|
||||||
@ -45,25 +89,39 @@ const ProgressSpinner: FC<{
|
|||||||
className={className}
|
className={className}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<svg
|
<canvas ref={canvasRef} className="ProgressSpinner_canvas" style={`width: ${width}; height: ${width}px;`} />
|
||||||
viewBox={`0 0 ${borderRadius * 2} ${borderRadius * 2}`}
|
|
||||||
height={borderRadius * 2}
|
|
||||||
width={borderRadius * 2}
|
|
||||||
>
|
|
||||||
<circle
|
|
||||||
stroke="white"
|
|
||||||
fill="transparent"
|
|
||||||
stroke-width={STROKE_WIDTH}
|
|
||||||
stroke-dasharray={`${circumference} ${circumference}`}
|
|
||||||
stroke-dashoffset={strokeDashOffset}
|
|
||||||
stroke-linecap="round"
|
|
||||||
r={circleRadius}
|
|
||||||
cx={borderRadius}
|
|
||||||
cy={borderRadius}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function drawSpinnerArc(
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
size: number,
|
||||||
|
strokeWidth: number,
|
||||||
|
color: string,
|
||||||
|
progress: number,
|
||||||
|
shouldInit = false,
|
||||||
|
) {
|
||||||
|
const centerCoordinate = size / 2;
|
||||||
|
const radius = (size - strokeWidth) / 2 - PADDING;
|
||||||
|
const rotationOffset = (Date.now() % ROTATE_DURATION) / ROTATE_DURATION;
|
||||||
|
const startAngle = (2 * Math.PI) * rotationOffset;
|
||||||
|
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);
|
export default memo(ProgressSpinner);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user