diff --git a/src/components/voice/diagram.tsx b/src/components/voice/diagram.tsx
index fc83a19..a193cd0 100644
--- a/src/components/voice/diagram.tsx
+++ b/src/components/voice/diagram.tsx
@@ -1,9 +1,9 @@
import { type Accessor, createSignal, createUniqueId, For, onCleanup, onMount, Show } from "solid-js";
-import { FLIGHT, FRAME, fill, has, head, type Loss, type Packet, type Segment, type Word } from "./sim";
+import { FLIGHT, FRAME, fill, has, head, type Loss, type Packet, reach, type Segment, type Word } from "./sim";
// Every drawn element can be swapped for a hand-drawn image (PNG/SVG under /public).
// Sizes are in viewBox units, where the diagram is 600 wide; draw at 4x for crisp output.
-// An array of images cycles at FPS, like hand-drawn animation. Frames follow the clock, so scrubbing works.
+// An array of images cycles at FPS, like hand-drawn animation. Frames follow the clock.
export type Sprite = string | string[];
export interface Art {
@@ -48,7 +48,8 @@ export interface Props {
}
const HOLD = 2.5; // seconds to linger on the final frame before looping
-const SPEEDS = [0.5, 1, 0.25];
+const SPEED = 0.5; // half speed, so the packets are easy to follow
+const STEP = 0.1; // seconds per arrow key
const W = 600;
const GUTTER = 44;
@@ -72,7 +73,6 @@ const COLOR = {
export function Diagram(props: Props) {
const [t, setT] = createSignal(0);
const [playing, setPlaying] = createSignal(false);
- const [speed, setSpeed] = createSignal(SPEEDS[0]);
const now = () => Math.min(t(), props.end);
const id = createUniqueId();
@@ -90,7 +90,7 @@ export function Diagram(props: Props) {
const dt = (time - last) / 1000;
last = time;
if (playing()) {
- const next = t() + dt * speed();
+ const next = t() + dt * SPEED;
setT(next > props.end + HOLD ? 0 : next);
}
frame = requestAnimationFrame(tick);
@@ -109,46 +109,77 @@ export function Diagram(props: Props) {
const x = (ts: number) => GUTTER + (ts / props.axis) * TRACK_W;
+ // The keyboard path for seeking: arrows step, Home/End jump. Clicking a track is the mouse path.
+ const key = (e: KeyboardEvent) => {
+ const target = {
+ ArrowLeft: now() - STEP,
+ ArrowDown: now() - STEP,
+ ArrowRight: now() + STEP,
+ ArrowUp: now() + STEP,
+ Home: 0,
+ End: props.end,
+ }[e.key];
+ if (target === undefined) return;
+ e.preventDefault();
+ setPlaying(false);
+ setT(Math.max(0, Math.min(props.end, target)));
+ };
+
return (
-
+
+
-
+
- {
- setPlaying(false);
- setT(e.currentTarget.valueAsNumber);
- }}
- aria-label="Time"
- />
-
- {now().toFixed(1)}s
-
-
-
{props.legend.done}
{props.legend.have}
waiting for retransmit
@@ -214,6 +220,7 @@ function Swatch(props: { fill?: string; stroke?: string; children: string }) {
interface ViewProps {
t: Accessor;
x: (ts: number) => number;
+ seek: (frac: number, segments: Segment[]) => void; // jump to when a track reaches `frac` of the way along
art: Art;
hatch: string;
wobble?: string;
@@ -271,7 +278,9 @@ function PanelView(props: ViewProps & { panel: Panel }) {
{p.result.text}
- {(href) => }
+
+ {(href) => }
+
>
);
}
@@ -314,6 +323,20 @@ function TrackView(props: ViewProps & { track: Track; y: number; notes: "above"
stroke-width="2"
/>
+ {/* Click a track to jump to when it reaches that point. */}
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: the mouse path; the