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 (