Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,20 @@ npm run fix # fix lint + format issues together
- **Frame rate**: `SimModel.frameRateProperty` (default 30 fps) drives `frameDurationProperty`. The user can change frame rate via `PlaybackControlsNode`; frame stepping and time display use this value.
- **SceneryStack layout**: use `HBox` / `VBox` for rows and columns. Prefer `align: 'center'` and explicit `spacing` values. Do not set absolute pixel positions unless absolutely necessary.

## Constants and colors

- **Colors**: All UI colors live in `TrackLabColors.ts` as `ProfileColorProperty` instances for automatic dark/light theme switching. When adding a new color, create it there — never hardcode `rgb(…)` or hex strings in view files.
- **Layout constants**: Shared numeric constants (panel margins, drag speeds, touch dilation) live in `TrackLabConstants.ts`. File-local constants that are used only within a single view file can remain local, but any constant duplicated across two or more files should be hoisted to `TrackLabConstants.ts`.
- **Overlay constants**: Draggable overlays (measuring tape, angle tool, calibration tool, coordinate system) share `OVERLAY_DRAG_SPEED`, `OVERLAY_SHIFT_DRAG_SPEED`, `OVERLAY_TOUCH_DILATION`, and label panel styling constants (`LABEL_PANEL_*`) from `TrackLabConstants.ts`.
- **Control panel constants**: `CONTROL_ICON_SIZE`, `CONTROL_PANEL_ROWS_SPACING`, `CONTROL_PANEL_X_MARGIN`, `CONTROL_PANEL_Y_MARGIN` are shared across `ControlPanel.ts`, `MeasurementToolsPanel.ts`, and `InfoDialogNode.ts`.

## Accessibility

- **Localized a11y strings**: All accessibility text lives in `StringManager.getA11y()` backed by the `a11y` section in `strings_en.json` / `strings_fr.json`. Never hardcode English strings for `accessibleName` or `aria-label`.
- **Interactive elements**: Every interactive SceneryStack node (button, checkbox, draggable handle) must have an `accessibleName` sourced from the a11y string properties.
- **Canvas overlays**: Non-interactive overlay containers that wrap a Canvas or DOM element should have `tagName: "div"` and an `accessibleName` so screen readers can identify them.
- **HTML tables**: Use `<caption>` (visually hidden) and `aria-label` on `<th>` elements for data tables.

## Testing

There is currently **no test suite**, and none should be added at this stage. The codebase is evolving rapidly — APIs, model structure, and UI components change frequently enough that maintaining tests would cost more than they save right now. Do not install a test framework or create test files.
8 changes: 0 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions scripts/bouncingBallToSVG.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* bouncingBallToSVG.ts
*
* Generates an SVG of a bouncing ball for use as an icon.
*
*
* The bouncing ball is a simple parabolic motion with a restitution coefficient.
* The ball is drawn as a series of circles, one for each snapshot.
* The floor is drawn as a line.
Expand Down Expand Up @@ -108,7 +108,7 @@ function buildSvg(snapshots: Point[]): string {
(p) =>
` <circle cx="${p.x}" cy="${p.y}" r="${BALL_RADIUS}" ` +
`fill="${BALL_FILL}" stroke="${BALL_STROKE}" stroke-width="${BALL_STROKE_WIDTH}" ` +
`opacity="${BALL_OPACITY}"/>`
`opacity="${BALL_OPACITY}"/>`,
)
.join("\n");

Expand Down
25 changes: 25 additions & 0 deletions src/TrackLabColors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,31 @@ const TrackLabColors = {
new Color(0, 150, 180),
),

// Measuring tape overlay
measuringTapeColorProperty: profileColor("measuringTapeColor", new Color(240, 185, 55), new Color(220, 170, 40)),
measuringTapeShadowProperty: profileColor("measuringTapeShadow", new Color(0, 0, 0, 0.45), new Color(0, 0, 0, 0.45)),

// Angle tool overlay
angleToolColorProperty: profileColor("angleToolColor", new Color(170, 100, 255), new Color(150, 80, 230)),
angleToolShadowProperty: profileColor("angleToolShadow", new Color(0, 0, 0, 0.45), new Color(0, 0, 0, 0.45)),

// Shared overlay handle outline (used by measuring tape and angle tool endpoints)
overlayHandleOutlineProperty: profileColor(
"overlayHandleOutline",
new Color(0, 0, 0, 0.65),
new Color(0, 0, 0, 0.65),
),

// Shared overlay icon shadow (used by measurement tool panel icons)
iconShadowProperty: profileColor("iconShadow", new Color(0, 0, 0, 0.5), new Color(0, 0, 0, 0.5)),

// Calibration tool warning (endpoints too close)
calibrationWarningColorProperty: profileColor(
"calibrationWarningColor",
new Color(255, 60, 60),
new Color(255, 60, 60),
),

// Track list panel
trashIconProperty: profileColor("trashIcon", new Color(255, 102, 102), new Color(220, 80, 80)),
trashButtonBaseProperty: profileColor("trashButtonBase", new Color(60, 20, 20, 0.5), new Color(80, 30, 30, 0.6)),
Expand Down
23 changes: 23 additions & 0 deletions src/TrackLabConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,26 @@ export const WEBCAM_PREVIEW_HEIGHT = 324; // height of the preview and review vi
// Opacity applied to coordinate system and calibration tool overlays while
// the user is actively digitizing, signalling that those tools are locked out.
export const DIGITIZING_DIM_OPACITY = 0.35;

// ── Shared overlay drag speeds ────────────────────────────────────────────────
// Keyboard drag speeds in pixels/second used by all draggable overlays
// (measuring tape, angle tool, calibration tool, coordinate system).
export const OVERLAY_DRAG_SPEED = 200; // normal keyboard drag
export const OVERLAY_SHIFT_DRAG_SPEED = 40; // shift-key fine adjustment

// ── Shared overlay label panel styling ────────────────────────────────────────
// Label panels that float near the measuring tape midpoint and angle tool vertex.
export const LABEL_PANEL_CORNER_RADIUS = 4;
export const LABEL_PANEL_X_MARGIN = 6;
export const LABEL_PANEL_Y_MARGIN = 3;
export const LABEL_PANEL_SCALE = 0.8;

// ── Shared overlay endpoint touch dilation ────────────────────────────────────
export const OVERLAY_TOUCH_DILATION = 12;

// ── Control panel icon and layout ─────────────────────────────────────────────
// Shared by ControlPanel, MeasurementToolsPanel, and InfoDialogNode.
export const CONTROL_ICON_SIZE = 20;
export const CONTROL_PANEL_ROWS_SPACING = 12;
export const CONTROL_PANEL_X_MARGIN = 12;
export const CONTROL_PANEL_Y_MARGIN = 12;
40 changes: 40 additions & 0 deletions src/i18n/StringManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,26 @@ export class StringManager {
removeTrackStringProperty: ReadOnlyProperty<string>;
dataTableStringProperty: ReadOnlyProperty<string>;
exportCSVStringProperty: ReadOnlyProperty<string>;
measuringTapeBaseStringProperty: ReadOnlyProperty<string>;
measuringTapeTipStringProperty: ReadOnlyProperty<string>;
angleToolVertexStringProperty: ReadOnlyProperty<string>;
angleArm1StringProperty: ReadOnlyProperty<string>;
angleArm2StringProperty: ReadOnlyProperty<string>;
toggleAxesStringProperty: ReadOnlyProperty<string>;
toggleCalibrationStringProperty: ReadOnlyProperty<string>;
toggleMagnifierStringProperty: ReadOnlyProperty<string>;
toggleAutoTrackingStringProperty: ReadOnlyProperty<string>;
toggleMeasuringTapeStringProperty: ReadOnlyProperty<string>;
toggleAngleToolStringProperty: ReadOnlyProperty<string>;
graphRescaleStringProperty: ReadOnlyProperty<string>;
graphZoomInStringProperty: ReadOnlyProperty<string>;
graphZoomOutStringProperty: ReadOnlyProperty<string>;
graphPanLeftStringProperty: ReadOnlyProperty<string>;
graphPanRightStringProperty: ReadOnlyProperty<string>;
graphPanUpStringProperty: ReadOnlyProperty<string>;
graphPanDownStringProperty: ReadOnlyProperty<string>;
kinematicsGraphStringProperty: ReadOnlyProperty<string>;
selectTrackForGraphStringProperty: ReadOnlyProperty<string>;
} {
return {
videoPlayerStringProperty: this.stringProperties.a11y.videoPlayerStringProperty,
Expand All @@ -423,6 +443,26 @@ export class StringManager {
removeTrackStringProperty: this.stringProperties.a11y.removeTrackStringProperty,
dataTableStringProperty: this.stringProperties.a11y.dataTableStringProperty,
exportCSVStringProperty: this.stringProperties.a11y.exportCSVStringProperty,
measuringTapeBaseStringProperty: this.stringProperties.a11y.measuringTapeBaseStringProperty,
measuringTapeTipStringProperty: this.stringProperties.a11y.measuringTapeTipStringProperty,
angleToolVertexStringProperty: this.stringProperties.a11y.angleToolVertexStringProperty,
angleArm1StringProperty: this.stringProperties.a11y.angleArm1StringProperty,
angleArm2StringProperty: this.stringProperties.a11y.angleArm2StringProperty,
toggleAxesStringProperty: this.stringProperties.a11y.toggleAxesStringProperty,
toggleCalibrationStringProperty: this.stringProperties.a11y.toggleCalibrationStringProperty,
toggleMagnifierStringProperty: this.stringProperties.a11y.toggleMagnifierStringProperty,
toggleAutoTrackingStringProperty: this.stringProperties.a11y.toggleAutoTrackingStringProperty,
toggleMeasuringTapeStringProperty: this.stringProperties.a11y.toggleMeasuringTapeStringProperty,
toggleAngleToolStringProperty: this.stringProperties.a11y.toggleAngleToolStringProperty,
graphRescaleStringProperty: this.stringProperties.a11y.graphRescaleStringProperty,
graphZoomInStringProperty: this.stringProperties.a11y.graphZoomInStringProperty,
graphZoomOutStringProperty: this.stringProperties.a11y.graphZoomOutStringProperty,
graphPanLeftStringProperty: this.stringProperties.a11y.graphPanLeftStringProperty,
graphPanRightStringProperty: this.stringProperties.a11y.graphPanRightStringProperty,
graphPanUpStringProperty: this.stringProperties.a11y.graphPanUpStringProperty,
graphPanDownStringProperty: this.stringProperties.a11y.graphPanDownStringProperty,
kinematicsGraphStringProperty: this.stringProperties.a11y.kinematicsGraphStringProperty,
selectTrackForGraphStringProperty: this.stringProperties.a11y.selectTrackForGraphStringProperty,
};
}

Expand Down
26 changes: 23 additions & 3 deletions src/i18n/strings_en.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,32 @@
},
"a11y": {
"videoPlayer": "Video player",
"videoScrubber": "Video timeline drag to seek",
"videoScrubber": "Video timeline \u2014 drag to seek",
"rewindToStart": "Rewind to start",
"digitizingArea": "Video digitizing area click to record the position of the active track",
"digitizingArea": "Video digitizing area \u2014 click to record the position of the active track",
"digitizeTrack": "Digitize track {{symbol}}",
"removeTrack": "Remove track {{symbol}}",
"dataTable": "Track data",
"exportCSV": "Export track data as CSV file"
"exportCSV": "Export track data as CSV file",
"measuringTapeBase": "Measuring tape base endpoint",
"measuringTapeTip": "Measuring tape tip endpoint",
"angleToolVertex": "Angle tool vertex",
"angleArm1": "Angle tool arm 1 endpoint",
"angleArm2": "Angle tool arm 2 endpoint",
"toggleAxes": "Toggle coordinate axes",
"toggleCalibration": "Toggle calibration tool",
"toggleMagnifier": "Toggle digitizing magnifier",
"toggleAutoTracking": "Toggle auto-tracking",
"toggleMeasuringTape": "Toggle measuring tape",
"toggleAngleTool": "Toggle angle tool",
"graphRescale": "Rescale graph to fit data",
"graphZoomIn": "Zoom in on graph",
"graphZoomOut": "Zoom out on graph",
"graphPanLeft": "Pan graph left",
"graphPanRight": "Pan graph right",
"graphPanUp": "Pan graph up",
"graphPanDown": "Pan graph down",
"kinematicsGraph": "Kinematics graph",
"selectTrackForGraph": "Select track to display on graph"
}
}
22 changes: 21 additions & 1 deletion src/i18n/strings_fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,26 @@
"digitizeTrack": "Num\u00e9riser la piste {{symbol}}",
"removeTrack": "Supprimer la piste {{symbol}}",
"dataTable": "Donn\u00e9es de piste",
"exportCSV": "Exporter les donn\u00e9es de piste en fichier CSV"
"exportCSV": "Exporter les donn\u00e9es de piste en fichier CSV",
"measuringTapeBase": "Extr\u00e9mit\u00e9 de base du ruban de mesure",
"measuringTapeTip": "Extr\u00e9mit\u00e9 du ruban de mesure",
"angleToolVertex": "Sommet du rapporteur",
"angleArm1": "Extr\u00e9mit\u00e9 du bras 1 du rapporteur",
"angleArm2": "Extr\u00e9mit\u00e9 du bras 2 du rapporteur",
"toggleAxes": "Basculer les axes de coordonn\u00e9es",
"toggleCalibration": "Basculer l'outil de calibration",
"toggleMagnifier": "Basculer la loupe de num\u00e9risation",
"toggleAutoTracking": "Basculer le suivi automatique",
"toggleMeasuringTape": "Basculer le ruban de mesure",
"toggleAngleTool": "Basculer le rapporteur",
"graphRescale": "Redimensionner le graphique pour ajuster les donn\u00e9es",
"graphZoomIn": "Zoom avant sur le graphique",
"graphZoomOut": "Zoom arri\u00e8re sur le graphique",
"graphPanLeft": "D\u00e9placer le graphique vers la gauche",
"graphPanRight": "D\u00e9placer le graphique vers la droite",
"graphPanUp": "D\u00e9placer le graphique vers le haut",
"graphPanDown": "D\u00e9placer le graphique vers le bas",
"kinematicsGraph": "Graphique cin\u00e9matique",
"selectTrackForGraph": "S\u00e9lectionner la piste \u00e0 afficher sur le graphique"
}
}
85 changes: 59 additions & 26 deletions src/screen-name/graph/ConfigurableGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { Shape } from "scenerystack/kite";
import { Orientation } from "scenerystack/phet-core";
import { FireListener, HBox, Node, Rectangle, Text } from "scenerystack/scenery";
import { PhetFont } from "scenerystack/scenery-phet";
import { StringManager } from "../../i18n/StringManager.js";
import TrackLabColors from "../../TrackLabColors.js";
import trackLab from "../../TrackLabNamespace.js";
import GraphControlsPanel from "./GraphControlsPanel.js";
Expand Down Expand Up @@ -326,8 +327,10 @@ export default class ConfigurableGraph extends Node {
const buttonPadding = BUTTON_PADDING;
const buttonSpacing = BUTTON_SPACING;

const a11yStrings = StringManager.getInstance().getA11y();

// Helper function to create a button
const createButton = (label: string, onClick: () => void): Node => {
const createButton = (label: string, onClick: () => void, accessibleName?: TReadOnlyProperty<string>): Node => {
const buttonText = new Text(label, {
font: BUTTON_FONT,
fill: TrackLabColors.controlPanelStrokeProperty,
Expand All @@ -341,6 +344,8 @@ export default class ConfigurableGraph extends Node {

const button = new Node({
children: [buttonBackground, buttonText],
tagName: "button",
...(accessibleName && { accessibleName }),
});

// Center the text in the button
Expand All @@ -367,37 +372,65 @@ export default class ConfigurableGraph extends Node {
};

// Create rescale button
const rescaleButton = createButton("↻", () => {
// Reset manual zoom flag and rescale to fit data
this.dataManager.setManuallyZoomed(false);
this.dataManager.updateAxisRanges();
});
const rescaleButton = createButton(
"↻",
() => {
// Reset manual zoom flag and rescale to fit data
this.dataManager.setManuallyZoomed(false);
this.dataManager.updateAxisRanges();
},
a11yStrings.graphRescaleStringProperty,
);

// Create zoom buttons (will be wired up after interactionHandler is created)
const zoomInButton = createButton("+", () => {
this.interactionHandler.zoomIn();
});
// Create zoom buttons
const zoomInButton = createButton(
"+",
() => {
this.interactionHandler.zoomIn();
},
a11yStrings.graphZoomInStringProperty,
);

const zoomOutButton = createButton("−", () => {
this.interactionHandler.zoomOut();
});
const zoomOutButton = createButton(
"−",
() => {
this.interactionHandler.zoomOut();
},
a11yStrings.graphZoomOutStringProperty,
);

// Create pan buttons (will be wired up after interactionHandler is created)
const panLeftButton = createButton("←", () => {
this.interactionHandler.pan("left");
});
// Create pan buttons
const panLeftButton = createButton(
"←",
() => {
this.interactionHandler.pan("left");
},
a11yStrings.graphPanLeftStringProperty,
);

const panRightButton = createButton("→", () => {
this.interactionHandler.pan("right");
});
const panRightButton = createButton(
"→",
() => {
this.interactionHandler.pan("right");
},
a11yStrings.graphPanRightStringProperty,
);

const panUpButton = createButton("↑", () => {
this.interactionHandler.pan("up");
});
const panUpButton = createButton(
"↑",
() => {
this.interactionHandler.pan("up");
},
a11yStrings.graphPanUpStringProperty,
);

const panDownButton = createButton("↓", () => {
this.interactionHandler.pan("down");
});
const panDownButton = createButton(
"↓",
() => {
this.interactionHandler.pan("down");
},
a11yStrings.graphPanDownStringProperty,
);

// Create HBox to hold all buttons
const controlButtonsPanel = new HBox({
Expand Down
Loading