fired transitions run animation once

This commit is contained in:
Joeri Exelmans 2025-10-21 14:23:25 +02:00
parent 0ba4fd4cae
commit 29808a683c
6 changed files with 34 additions and 14 deletions

View file

@ -6,9 +6,10 @@ import "./BottomPanel.css";
import head from "../head.svg" ; import head from "../head.svg" ;
import { usePersistentState } from "@/util/persistent_state"; import { usePersistentState } from "@/util/persistent_state";
import { PersistentDetails } from "./PersistentDetails"; import { PersistentDetails } from "./PersistentDetails";
import { DigitalWatch } from "@/Plant/DigitalWatch/DigitalWatch";
export function BottomPanel(props: {errors: TraceableError[]}) { export function BottomPanel(props: {errors: TraceableError[]}) {
const [greeting, setGreeting] = useState(<><b><img src={head} style={{transform: "scaleX(-1)"}}/>&emsp;"Welcome to StateBuddy, buddy!"</b></>); const [greeting, setGreeting] = useState(<><b><img src={head} style={{transform: "scaleX(-1)"}}/>&emsp;"Welcome to StateBuddy, buddy!"</b><br/></>);
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
@ -17,7 +18,8 @@ export function BottomPanel(props: {errors: TraceableError[]}) {
}, []); }, []);
return <div className="toolbar bottom"> return <div className="toolbar bottom">
<>{greeting}</> {greeting}
<DigitalWatch alarm={true} light={true} h={12} m={30} s={33}/>
{props.errors.length > 0 && {props.errors.length > 0 &&
<div className="errorStatus"> <div className="errorStatus">
<PersistentDetails initiallyOpen={false} localStorageKey="errorsExpanded"> <PersistentDetails initiallyOpen={false} localStorageKey="errorsExpanded">

View file

@ -1,9 +1,10 @@
import { Dispatch, ReactElement, SetStateAction, useState, KeyboardEvent } from "react"; import { Dispatch, ReactElement, SetStateAction, useState, KeyboardEvent, useEffect, useRef } from "react";
import { parse as parseLabel } from "../statecharts/label_parser"; import { parse as parseLabel } from "../statecharts/label_parser";
export function TextDialog(props: {setModal: Dispatch<SetStateAction<ReactElement|null>>, text: string, done: (newText: string|undefined) => void}) { export function TextDialog(props: {setModal: Dispatch<SetStateAction<ReactElement|null>>, text: string, done: (newText: string|undefined) => void}) {
const [text, setText] = useState(props.text); const [text, setText] = useState(props.text);
function onKeyDown(e: KeyboardEvent) { function onKeyDown(e: KeyboardEvent) {
if (e.key === "Enter") { if (e.key === "Enter") {
if (!e.shiftKey) { if (!e.shiftKey) {
@ -19,19 +20,19 @@ export function TextDialog(props: {setModal: Dispatch<SetStateAction<ReactElemen
e.stopPropagation(); e.stopPropagation();
} }
let error = ""; let parseError = "";
try { try {
const parsed = parseLabel(text); parseLabel(text);
} catch (e) { } catch (e) {
// @ts-ignore // @ts-ignore
error = e.message; parseError = e.message;
} }
return <div onKeyDown={onKeyDown} style={{padding: 4}}> return <div onKeyDown={onKeyDown} style={{padding: 4}}>
Text label:<br/> Text label:<br/>
<textarea autoFocus style={{fontFamily: 'Roboto', width:'calc(100%-10px)', height: 60}} onChange={e=>setText(e.target.value)} value={text}/> <textarea autoFocus style={{fontFamily: 'Roboto', width: 400, height: 60}} onChange={e=>setText(e.target.value)} value={text} onFocus={e => e.target.select()}/>
<br/> <br/>
<span style={{color: 'var(--error-color)'}}>{error}</span><br/> <span style={{color: 'var(--error-color)'}}>{parseError}</span><br/>
<p><kbd>Enter</kbd> to confirm. <kbd>Esc</kbd> to cancel. <p><kbd>Enter</kbd> to confirm. <kbd>Esc</kbd> to cancel.
</p> </p>
(Tip: <kbd>Shift</kbd>+<kbd>Enter</kbd> to insert newline.) (Tip: <kbd>Shift</kbd>+<kbd>Enter</kbd> to insert newline.)

View file

@ -35,7 +35,7 @@ export function DigitalWatch({light, h, m, s, alarm, callbacks}: DigitalWatchPro
src: url(${digitalFont}); src: url(${digitalFont});
} }
`}</style> `}</style>
<svg version="1.1" width="222" height="236"> <svg version="1.1" width="222" height="236" style={{userSelect: 'none'}}>
{light ? {light ?
<image width="222" height="236" xlinkHref={imgWatchLight}/> <image width="222" height="236" xlinkHref={imgWatchLight}/>
: <image width="222" height="236" xlinkHref={imgWatch}/> : <image width="222" height="236" xlinkHref={imgWatch}/>

View file

@ -161,17 +161,18 @@ text.helper:hover {
} }
.arrow.fired { .arrow.fired {
stroke: rgb(160, 0, 168); stroke: rgb(160, 0, 168);
stroke-width: 4px; stroke-width: 3px;
/* animation: blinkTransition 1s infinite; */ animation: blinkTransition 1s;
} }
@keyframes blinkTransition { @keyframes blinkTransition {
100%,
0% { 0% {
stroke: rgb(231, 111, 0); stroke: rgb(255, 128, 9);
stroke-width: 6px;
filter: drop-shadow(0 0 5px rgba(255, 128, 9, 1));
} }
100% { 100% {
stroke: rgb(160, 0, 168); stroke: rgb(160 0 168);
} }
} }

View file

@ -119,6 +119,21 @@ export function VisualEditor({state, setState, ast, setAST, rt, errors, setError
} }
}, []); }, []);
useEffect(() => {
// bit of a hacky way to force the animation on fired transitions to replay, if the new 'rt' contains the same fired transitions as the previous one
requestAnimationFrame(() => {
console.log('rt changed');
document.querySelectorAll(".arrow.fired").forEach(el => {
el.style.animation = 'none';
requestAnimationFrame(() => {
el.style.animation = '';
})
setTimeout(() => {
}, 10); // <- small timeout seems to be necessary or the animation won't restart
});
})
}, [rt]);
useEffect(() => { useEffect(() => {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
const serializedState = JSON.stringify(state); const serializedState = JSON.stringify(state);

View file

@ -33,6 +33,7 @@ TODO
- regions in AND-state - regions in AND-state
- usability stuff: - usability stuff:
- show internal events
- highlight selected shapes while making a selection - highlight selected shapes while making a selection
- comments sometimes snap to transitions even if they belong to a state - comments sometimes snap to transitions even if they belong to a state
- highlight fired transitions - highlight fired transitions