gave the pulse box better animation pacing so the colors don't change in the middle of a pulse

This commit is contained in:
Carter 2026-03-16 14:41:18 -06:00
parent 62763dd291
commit 575c84246c
5 changed files with 50 additions and 91 deletions

View file

@ -1,56 +1,76 @@
import { Box, BoxProps, Theme } from "@mui/material"; import { Box, BoxProps, Theme } from "@mui/material";
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import classNames from "classnames"; import classNames from "classnames";
import { forwardRef, ReactNode } from "react"; import { forwardRef, ReactNode, useCallback, useState } from "react";
const useStyles = makeStyles((_theme: Theme) => ({ const useStyles = makeStyles((_theme: Theme) => ({
pulseWrapper: { pulseWrapper: {
position: 'relative', position: 'relative',
display: 'block', // Changed to block to respect Box margins display: 'block',
width: 'fit-content', // Shrinks to child width width: 'fit-content',
'&::after': { },
content: '""', pulseRing: {
position: 'absolute', position: 'absolute',
top: 0, top: 0,
left: 0, left: 0,
width: '100%', width: '100%',
height: '100%', height: '100%',
borderRadius: 'inherit', borderRadius: 'inherit',
animation: '$pulse 1.75s infinite ease-in-out',
pointerEvents: 'none', pointerEvents: 'none',
boxSizing: 'border-box', boxSizing: 'border-box',
transition: "border-color 0.35s ease-in-out", animation: '$pulse 4.4s infinite ease-in-out',
},
}, },
'@keyframes pulse': { '@keyframes pulse': {
from: { /* pulse, pause, pulse, pause - CONSISTENT: both pauses equal, both pulses equal */
boxShadow: '0 0 0 2px var(--pulse-color)', /* Pulse 1 (0-42%) */
opacity: 1, '0%': { boxShadow: '0 0 0 2px var(--pulse-color)', opacity: 0 },
}, '2%': { boxShadow: '0 0 0 2px var(--pulse-color)', opacity: 1 },
to: { '40%': { boxShadow: '0 0 0 14px var(--pulse-color)', opacity: 0 },
boxShadow: '0 0 0 14px var(--pulse-color)', '42%': { boxShadow: '0 0 0 2px var(--pulse-color)', opacity: 0 },
opacity: 0, /* Pause 1 (42-50%) */
}, '50%': { boxShadow: '0 0 0 2px var(--pulse-color)', opacity: 0 },
/* Pulse 2 (50-92%) */
'52%': { boxShadow: '0 0 0 2px var(--pulse-color)', opacity: 1 },
'90%': { boxShadow: '0 0 0 14px var(--pulse-color)', opacity: 0 },
'92%': { boxShadow: '0 0 0 2px var(--pulse-color)', opacity: 0 },
/* Pause 2 (92-100%) - same length as Pause 1, color change at iteration */
'100%': { boxShadow: '0 0 0 2px var(--pulse-color)', opacity: 0 },
}, },
})); }));
interface Props extends BoxProps { interface Props extends BoxProps {
color: string; color: string;
colors?: string[];
children: ReactNode; children: ReactNode;
className?: string; className?: string;
} }
const PulseBox = forwardRef<HTMLDivElement, Props>((props, ref) => { const PulseBox = forwardRef<HTMLDivElement, Props>((props, ref) => {
const { color, children, className, ...boxProps } = props; const { color, colors: colorsProp, children, className, ...boxProps } = props;
const classes = useStyles() const classes = useStyles()
const colors = colorsProp && colorsProp.length > 0 ? colorsProp : [color];
const [colorIndex, setColorIndex] = useState(0);
const displayColor = colors[colorIndex % colors.length] ?? color;
const onAnimationIteration = useCallback(() => {
if (colors.length > 1) {
setColorIndex((i) => (i + 1) % colors.length);
}
}, [colors.length]);
return ( return (
<Box <Box
ref={ref} ref={ref}
{...boxProps} {...boxProps}
className={classNames([classes.pulseWrapper, className])} className={classNames([classes.pulseWrapper, className])}
style={{ '--pulse-color': color } as React.CSSProperties} style={{ '--pulse-color': displayColor } as React.CSSProperties}
> >
<span
className={classes.pulseRing}
aria-hidden
onAnimationIteration={onAnimationIteration}
/>
{children} {children}
</Box> </Box>
); );

View file

@ -57,21 +57,6 @@ export default function StatusDust(props: Props) {
const colors = or(device.status.sen5x?.overlays.map(overlay => overlay.color), []); const colors = or(device.status.sen5x?.overlays.map(overlay => overlay.color), []);
const messages = or(device.status.sen5x?.overlays.map(overlay => overlay.message), []); const messages = or(device.status.sen5x?.overlays.map(overlay => overlay.message), []);
const [color, setColor] = useState(colors.length > 0 ? colors[0] : "");
const [, setColorIndex] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setColorIndex(prevIndex => {
const newIndex = prevIndex >= colors.length - 1 ? 0 : prevIndex + 1;
setColor(colors[newIndex]); // Use the new index here
return newIndex;
});
}, 7500);
return () => clearInterval(interval);
}, []);
// const [currentIndex, setCurrentIndex] = useState(0); // const [currentIndex, setCurrentIndex] = useState(0);
// Auto-cycle through measurements every 3 seconds // Auto-cycle through measurements every 3 seconds
@ -142,7 +127,7 @@ export default function StatusDust(props: Props) {
return ( return (
<Tooltip title={tooltip()}> <Tooltip title={tooltip()}>
<PulseBox color={color} className={classes.box} > <PulseBox color={colors[0] ?? ""} colors={colors} className={classes.box} >
<Grid2 container direction="column" > <Grid2 container direction="column" >
<Grid2 container direction="column" <Grid2 container direction="column"
className={`${classes.carouselItem} ${classes.active }`} className={`${classes.carouselItem} ${classes.active }`}

View file

@ -85,23 +85,8 @@ export default function StatusGas(props: Props) {
messages = or(device.status[gasType]?.overlays?.map(overlay => overlay.message), []); messages = or(device.status[gasType]?.overlays?.map(overlay => overlay.message), []);
} }
const [color, setColor] = useState(colors.length > 0 ? colors[0] : "");
const [, setColorIndex] = useState(0)
const gasTypes: ("o2" | "no2" | "co2" | "co" | "lel" | "h2s")[] = ["o2", "no2", "co2", "co", "lel", "h2s"] const gasTypes: ("o2" | "no2" | "co2" | "co" | "lel" | "h2s")[] = ["o2", "no2", "co2", "co", "lel", "h2s"]
useEffect(() => {
const interval = setInterval(() => {
setColorIndex(prevIndex => {
const newIndex = prevIndex >= colors.length - 1 ? 0 : prevIndex + 1;
setColor(colors[newIndex]); // Use the new index here
return newIndex;
});
}, 7500);
return () => clearInterval(interval);
}, []);
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
// Auto-cycle through measurements every 5 seconds // Auto-cycle through measurements every 5 seconds
@ -183,7 +168,7 @@ export default function StatusGas(props: Props) {
} }
return ( return (
<Tooltip title={tooltip()}> <Tooltip title={tooltip()}>
<PulseBox color={color} className={gasType === "all" ? classes.boxTall : classes.box}> <PulseBox color={colors[0] ?? ""} colors={colors} className={gasType === "all" ? classes.boxTall : classes.box}>
<Grid2 container direction="column" > <Grid2 container direction="column" >
{ gasType === "all" && { gasType === "all" &&
<Typography variant="body2"> <Typography variant="body2">

View file

@ -5,7 +5,6 @@ import { Device } from "models";
import PulseBox from "./PulseBox"; import PulseBox from "./PulseBox";
import RelativeTimestamp from "./RelativeTimestamp"; import RelativeTimestamp from "./RelativeTimestamp";
import { or } from "utils"; import { or } from "utils";
import { useEffect, useState } from "react";
interface Props { interface Props {
device: Device; device: Device;
@ -33,21 +32,6 @@ export default function StatusPlenum(props: Props) {
const colors = or(device.status.plenum?.overlays.map(overlay => overlay.color), []); const colors = or(device.status.plenum?.overlays.map(overlay => overlay.color), []);
const messages = or(device.status.plenum?.overlays.map(overlay => overlay.message), []); const messages = or(device.status.plenum?.overlays.map(overlay => overlay.message), []);
const [color, setColor] = useState(colors.length > 0 ? colors[0] : "");
const [, setColorIndex] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setColorIndex(prevIndex => {
const newIndex = prevIndex >= colors.length - 1 ? 0 : prevIndex + 1;
setColor(colors[newIndex]); // Use the new index here
return newIndex;
});
}, 5000);
return () => clearInterval(interval);
}, []);
const tooltip = () => { const tooltip = () => {
if (messages.length === 0) return null if (messages.length === 0) return null
if (messages.length < 2) return ( if (messages.length < 2) return (
@ -107,7 +91,7 @@ export default function StatusPlenum(props: Props) {
return ( return (
<Tooltip title={tooltip()}> <Tooltip title={tooltip()}>
<PulseBox color={color} className={classes.box} > <PulseBox color={colors[0] ?? ""} colors={colors} className={classes.box} >
<Grid2 container direction="column" > <Grid2 container direction="column" >
<Grid2> <Grid2>
<Typography variant="body2" style={{ color: orange[700]}}> <Typography variant="body2" style={{ color: orange[700]}}>

View file

@ -58,21 +58,6 @@ export default function StatusSen5x(props: Props) {
const colors = or(device.status.sen5x?.overlays.map(overlay => overlay.color), []); const colors = or(device.status.sen5x?.overlays.map(overlay => overlay.color), []);
const messages = or(device.status.sen5x?.overlays.map(overlay => overlay.message), []); const messages = or(device.status.sen5x?.overlays.map(overlay => overlay.message), []);
const [color, setColor] = useState(colors.length > 0 ? colors[0] : "");
const [, setColorIndex] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setColorIndex(prevIndex => {
const newIndex = prevIndex >= colors.length - 1 ? 0 : prevIndex + 1;
setColor(colors[newIndex]); // Use the new index here
return newIndex;
});
}, 7500);
return () => clearInterval(interval);
}, []);
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
// Auto-cycle through measurements every 5 seconds // Auto-cycle through measurements every 5 seconds
@ -147,7 +132,7 @@ export default function StatusSen5x(props: Props) {
return ( return (
<Tooltip title={tooltip()}> <Tooltip title={tooltip()}>
<PulseBox color={color} className={classes.box} > <PulseBox color={colors[0] ?? ""} colors={colors} className={classes.box} >
<Grid2 container direction="column" > <Grid2 container direction="column" >
<Grid2 container direction="column" <Grid2 container direction="column"
className={`${classes.carouselItem} ${ className={`${classes.carouselItem} ${