Files
upstream-jitsi-meet/react/features/time-timer/components/web/Disk.tsx
upstream-mirror 1d2bfcac0c
Some checks failed
Prosody Plugin Tests / test (push) Failing after 10m11s
Close stale issues and PRs / stale (push) Failing after 1s
snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
2026-09-15 15:56:32 +05:00

182 lines
5.6 KiB
TypeScript

import React from 'react';
import { makeStyles } from 'tss-react/mui';
import BaseTheme from '../../../base/ui/components/BaseTheme.web';
/**
* SVG disk renderer used by {@code TimeTimerPill}. Draws a ring with an
* optional filled wedge. During overrun lap 2+ (once the disk has filled
* once), an additional CSS conic-gradient overlay conveys the ongoing
* sweep — a continuous gradient from the base red at the 12 o'clock origin
* to a darker red at the current leading edge.
*/
const CX = 50;
const CY = 50;
const RADIUS = 46;
/**
* Converts a polar coordinate (clock angle in degrees, 0 = 12 o'clock) to
* cartesian coordinates on the disk.
*
* @param {number} angleDeg - Angle in degrees, clockwise from the top.
* @param {number} r - Radius.
* @returns {{ x: number, y: number }}
*/
function polarToCartesian(angleDeg: number, r: number) {
const rad = ((angleDeg - 90) * Math.PI) / 180;
return {
x: CX + (r * Math.cos(rad)),
y: CY + (r * Math.sin(rad))
};
}
/**
* Builds an SVG path for a filled pie wedge covering the given fraction of
* the disk, drawn clockwise from the top.
*
* @param {number} fraction - Fraction of the disk to fill (0..1).
* @returns {string}
*/
function wedgePath(fraction: number): string {
const clamped = Math.max(0, Math.min(1, fraction));
if (clamped <= 0) {
return '';
}
if (clamped >= 1) {
const top = polarToCartesian(0, RADIUS);
const bottom = polarToCartesian(180, RADIUS);
return [
`M ${top.x} ${top.y}`,
`A ${RADIUS} ${RADIUS} 0 0 1 ${bottom.x} ${bottom.y}`,
`A ${RADIUS} ${RADIUS} 0 0 1 ${top.x} ${top.y}`,
'Z'
].join(' ');
}
const endAngle = clamped * 360;
const start = polarToCartesian(0, RADIUS);
const end = polarToCartesian(endAngle, RADIUS);
const largeArc = endAngle > 180 ? 1 : 0;
return [
`M ${CX} ${CY}`,
`L ${start.x} ${start.y}`,
`A ${RADIUS} ${RADIUS} 0 ${largeArc} 1 ${end.x} ${end.y}`,
'Z'
].join(' ');
}
interface IProps {
color: string;
fraction: number;
/**
* Angular position (degrees, clockwise from the top / 12 o'clock) of
* the leading edge of the overrun sweep during lap 2 and onward. When
* provided, the disk renders a conic-gradient overlay from the base
* red at the origin to a darker red at this angle, over the (fully
* filled) base disk.
*/
overrunArcEndDeg?: number;
size: number;
}
const useStyles = makeStyles<{ size: number; }>()((_theme, { size }) => {
return {
root: {
flex: '0 0 auto',
height: size,
position: 'relative',
width: size
},
svg: {
display: 'block',
height: size,
width: size
},
// Circular element overlaid exactly on top of the solid SVG disk. Its
// conic-gradient background is applied inline (see below) rather than
// here: the leading-edge angle changes every second during overrun, so
// baking it into a tss-react style would generate a fresh CSS class on
// every tick. The static positioning stays here.
overrunOverlay: {
borderRadius: '50%',
inset: 0,
position: 'absolute'
}
};
});
/**
* Builds the lap-2+ overrun sweep — a conic gradient running clockwise from
* the 12 o'clock origin: base red → darker red at the current leading edge,
* then back to base red for the remainder (already filled from lap 1).
*
* @param {number} deg - Angular position of the leading edge, in degrees.
* @returns {string}
*/
function overrunGradient(deg: number): string {
const base = BaseTheme.palette.timeTimerExpiredDisk;
const edge = BaseTheme.palette.timeTimerExpiredOverrunEdge;
return `conic-gradient(from 0deg, ${base} 0deg, ${edge} ${deg}deg, ${base} ${deg}deg, ${base} 360deg)`;
}
/**
* Renders the time-timer disk as a ring with an optional filled wedge, plus
* a conic-gradient overrun sweep during lap 2+.
*
* @param {IProps} props - Component props.
* @returns {ReactElement}
*/
const Disk = ({ color, fraction, overrunArcEndDeg, size }: IProps) => {
const { classes } = useStyles({ size });
const showOverrun = typeof overrunArcEndDeg === 'number';
return (
<div className = { classes.root }>
<svg
className = { classes.svg }
viewBox = '0 0 100 100'>
{/*
* Per Figma: a faint 5% white fill behind the wedge so the
* unfilled portion of the disk reads as a subtle lighter
* shape against the navy chip background. Drawn FIRST so the
* wedge paints over it.
*/}
<circle
cx = { CX }
cy = { CY }
fill = '#FFFFFF'
fillOpacity = '0.05'
r = { RADIUS } />
<circle
cx = { CX }
cy = { CY }
fill = 'none'
r = { RADIUS }
stroke = { color }
strokeWidth = '6' />
{fraction > 0 && (
<path
d = { wedgePath(fraction) }
fill = { color } />
)}
</svg>
{showOverrun && (
<div
className = { classes.overrunOverlay }
style = {{ background: overrunGradient(overrunArcEndDeg ?? 0) }} />
)}
</div>
);
};
export default Disk;