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
12 changes: 2 additions & 10 deletions src/TrackLabColors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,16 +90,8 @@ const TrackLabColors = {
),

// Axes (X red, Y green)
axisXColorProperty: profileColor(
"axisX",
new Color(255, 68, 68, 0.85),
new Color(238, 51, 51, 0.85),
),
axisYColorProperty: profileColor(
"axisY",
new Color(68, 204, 68, 0.85),
new Color(51, 187, 51, 0.85),
),
axisXColorProperty: profileColor("axisX", new Color(255, 68, 68, 0.85), new Color(238, 51, 51, 0.85)),
axisYColorProperty: profileColor("axisY", new Color(68, 204, 68, 0.85), new Color(51, 187, 51, 0.85)),

// Calibration tool (bright colors with shadows for visibility on all backgrounds)
calibrationFillProperty: profileColor(
Expand Down
35 changes: 35 additions & 0 deletions src/i18n/StringManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,41 @@ export class StringManager {
};
}

/**
* Get info dialog string properties
*/
public getInfoDialog(): {
titleStringProperty: ReadOnlyProperty<string>;
loadVideoTitleStringProperty: ReadOnlyProperty<string>;
loadVideoBodyStringProperty: ReadOnlyProperty<string>;
coordinateSystemTitleStringProperty: ReadOnlyProperty<string>;
coordinateSystemBodyStringProperty: ReadOnlyProperty<string>;
calibrationTitleStringProperty: ReadOnlyProperty<string>;
calibrationBodyStringProperty: ReadOnlyProperty<string>;
addTrackTitleStringProperty: ReadOnlyProperty<string>;
addTrackBodyStringProperty: ReadOnlyProperty<string>;
digitizeTitleStringProperty: ReadOnlyProperty<string>;
digitizeBodyStringProperty: ReadOnlyProperty<string>;
autoTrackTitleStringProperty: ReadOnlyProperty<string>;
autoTrackBodyStringProperty: ReadOnlyProperty<string>;
} {
return {
titleStringProperty: this.stringProperties.infoDialog.titleStringProperty,
loadVideoTitleStringProperty: this.stringProperties.infoDialog.loadVideoTitleStringProperty,
loadVideoBodyStringProperty: this.stringProperties.infoDialog.loadVideoBodyStringProperty,
coordinateSystemTitleStringProperty: this.stringProperties.infoDialog.coordinateSystemTitleStringProperty,
coordinateSystemBodyStringProperty: this.stringProperties.infoDialog.coordinateSystemBodyStringProperty,
calibrationTitleStringProperty: this.stringProperties.infoDialog.calibrationTitleStringProperty,
calibrationBodyStringProperty: this.stringProperties.infoDialog.calibrationBodyStringProperty,
addTrackTitleStringProperty: this.stringProperties.infoDialog.addTrackTitleStringProperty,
addTrackBodyStringProperty: this.stringProperties.infoDialog.addTrackBodyStringProperty,
digitizeTitleStringProperty: this.stringProperties.infoDialog.digitizeTitleStringProperty,
digitizeBodyStringProperty: this.stringProperties.infoDialog.digitizeBodyStringProperty,
autoTrackTitleStringProperty: this.stringProperties.infoDialog.autoTrackTitleStringProperty,
autoTrackBodyStringProperty: this.stringProperties.infoDialog.autoTrackBodyStringProperty,
};
}

/**
* Get video file label string properties
*/
Expand Down
15 changes: 15 additions & 0 deletions src/i18n/strings_en.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,21 @@
"pucksCollide": "Pucks Collide",
"springWars": "Spring Wars"
},
"infoDialog": {
"title": "How to Digitize a Track",
"loadVideoTitle": "1. Load a Video",
"loadVideoBody": "Select a video from the dropdown at the top \u2014 choose a sample clip, a webcam recording, or an uploaded file.",
"coordinateSystemTitle": "2. Set Up the Coordinate System",
"coordinateSystemBody": "Enable Axes in the control panel. Drag the origin to set the (0, 0) point, then rotate the axes to align with your scene.",
"calibrationTitle": "3. Calibrate the Scale",
"calibrationBody": "Enable Calibration in the control panel. Drag the two endpoints onto a known reference distance in the video, then enter the real-world length.",
"addTrackTitle": "4. Add a Track",
"addTrackBody": "In the Tracks panel, click \u201c+ Add Track\u201d to create a new track labelled A, B, C, \u2026",
"digitizeTitle": "5. Digitize Frame by Frame",
"digitizeBody": "Select a track and click on the object\u2019s position in the video. The video advances one frame automatically after each click.",
"autoTrackTitle": "6. Auto-Track (optional)",
"autoTrackBody": "Enable Auto-Tracking in the control panel, then drag a selection box around the object. TrackLab will track it across the remaining frames."
},
"a11y": {
"videoPlayer": "Video player",
"videoScrubber": "Video timeline — drag to seek",
Expand Down
15 changes: 15 additions & 0 deletions src/i18n/strings_fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,21 @@
"pucksCollide": "Rondelles en collision",
"springWars": "Guerres de ressorts"
},
"infoDialog": {
"title": "Comment num\u00e9riser une piste",
"loadVideoTitle": "1. Charger une vid\u00e9o",
"loadVideoBody": "S\u00e9lectionnez une vid\u00e9o dans le menu d\u00e9roulant en haut \u2014 choisissez un exemple, un enregistrement webcam ou un fichier import\u00e9.",
"coordinateSystemTitle": "2. Configurer le syst\u00e8me de coordonn\u00e9es",
"coordinateSystemBody": "Activez les Axes dans le panneau de contr\u00f4le. Faites glisser l\u2019origine pour d\u00e9finir le point (0,\u00a00), puis faites pivoter les axes pour les aligner sur votre sc\u00e8ne.",
"calibrationTitle": "3. Calibrer l\u2019\u00e9chelle",
"calibrationBody": "Activez la Calibration dans le panneau de contr\u00f4le. Faites glisser les deux extr\u00e9mit\u00e9s sur une distance de r\u00e9f\u00e9rence connue dans la vid\u00e9o, puis entrez la longueur r\u00e9elle.",
"addTrackTitle": "4. Ajouter une piste",
"addTrackBody": "Dans le panneau Pistes, cliquez sur \u00ab\u00a0+ Ajouter une piste\u00a0\u00bb pour cr\u00e9er une nouvelle piste \u00e9tiquet\u00e9e A, B, C, \u2026",
"digitizeTitle": "5. Num\u00e9riser image par image",
"digitizeBody": "S\u00e9lectionnez une piste et cliquez sur la position de l\u2019objet dans la vid\u00e9o. La vid\u00e9o avance automatiquement d\u2019une image apr\u00e8s chaque clic.",
"autoTrackTitle": "6. Suivi automatique (optionnel)",
"autoTrackBody": "Activez le Suivi automatique dans le panneau de contr\u00f4le, puis faites glisser un cadre de s\u00e9lection autour de l\u2019objet. TrackLab le suivra sur les images restantes."
},
"a11y": {
"videoPlayer": "Lecteur vid\u00e9o",
"videoScrubber": "Ligne de temps vid\u00e9o \u2014 glisser pour naviguer",
Expand Down
3 changes: 2 additions & 1 deletion src/screen-name/view/AutoTrackerNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,8 @@ export class AutoTrackerNode extends Node {
// biome-ignore lint/suspicious/noConsole: error logging for tracker init failure
console.error("AutoTracker: failed to initialise OpenCV tracker:", err);
if (this.initVersion === capturedVersion) {
const message = err instanceof Error ? err.message : autoTrackerStrings.trackingInitFailedStringProperty.value;
const message =
err instanceof Error ? err.message : autoTrackerStrings.trackingInitFailedStringProperty.value;
this.errorText.string = message;
this.errorText.visible = true;
this.hintText.visible = true;
Expand Down
3 changes: 2 additions & 1 deletion src/screen-name/view/DataTableNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ function buildHtmlTable(
// ── Accessible caption (visually hidden but read by screen readers) ────────
const caption = document.createElement("caption");
caption.textContent = a11y.tableCaption;
caption.style.cssText = "position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap;";
caption.style.cssText =
"position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap;";
table.appendChild(caption);

// ── Header row ─────────────────────────────────────────────────────────────
Expand Down
114 changes: 114 additions & 0 deletions src/screen-name/view/InfoDialogNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* InfoDialogNode.ts
*
* Modal dialog explaining the main steps for digitizing a track in TrackLab.
* Toggled by the InfoButton in the lower-left corner of the screen.
*/

import type { ReadOnlyProperty } from "scenerystack/axon";
import { Node, RichText, Text, VBox } from "scenerystack/scenery";
import { CloseButton, PhetFont } from "scenerystack/scenery-phet";
import { Panel } from "scenerystack/sun";
import { Tandem } from "scenerystack/tandem";
import { StringManager } from "../../i18n/StringManager.js";
import TrackLabColors from "../../TrackLabColors.js";
import { PANEL_CORNER_RADIUS } from "../../TrackLabConstants.js";
import trackLab from "../../TrackLabNamespace.js";

// ── Layout constants ──────────────────────────────────────────────────────────
const CONTENT_WIDTH = 370; // inner width of the panel content area
const PANEL_X_MARGIN = 18;
const PANEL_Y_MARGIN = 16;
const TITLE_FONT = new PhetFont({ size: 15, weight: "bold" });
const STEP_TITLE_FONT = new PhetFont({ size: 13, weight: "bold" });
const STEP_BODY_FONT = new PhetFont(13);
const STEPS_SPACING = 12; // vertical gap between steps
const STEP_INNER_SPACING = 2; // gap between step title and body text
const CLOSE_BUTTON_ICON_LENGTH = 10;

// ── Helpers ───────────────────────────────────────────────────────────────────

/** One step: bold heading above a softer-colored description. */
function makeStep(titleProp: ReadOnlyProperty<string>, bodyProp: ReadOnlyProperty<string>): Node {
const titleText = new Text(titleProp, {
font: STEP_TITLE_FONT,
fill: TrackLabColors.textOnDarkProperty,
maxWidth: CONTENT_WIDTH,
});

const bodyText = new RichText(bodyProp, {
font: STEP_BODY_FONT,
fill: TrackLabColors.textMutedProperty,
lineWrap: CONTENT_WIDTH,
});

return new VBox({
children: [titleText, bodyText],
spacing: STEP_INNER_SPACING,
align: "left",
});
}

// ── InfoDialogNode ────────────────────────────────────────────────────────────

/**
* Floating panel explaining how to digitize a track.
*
* Hidden by default (`visible = false`). Show by setting `visible = true`;
* the internal close button hides it again.
*/
export class InfoDialogNode extends Node {
public constructor() {
super({ visible: false });

const strings = StringManager.getInstance().getInfoDialog();

// ── Header: title + close button ─────────────────────────────────────────
const titleText = new Text(strings.titleStringProperty, {
font: TITLE_FONT,
fill: TrackLabColors.textOnDarkProperty,
});

const closeButton = new CloseButton({
listener: () => {
this.visible = false;
},
baseColor: TrackLabColors.buttonBaseDarkProperty,
iconLength: CLOSE_BUTTON_ICON_LENGTH,
tandem: Tandem.OPT_OUT,
});

// Lay out title and close button side-by-side, close button flush right.
const headerNode = new Node({ children: [titleText, closeButton] });
closeButton.right = CONTENT_WIDTH;
closeButton.centerY = titleText.centerY;
titleText.maxWidth = CONTENT_WIDTH - closeButton.width - 8;

// ── Steps ────────────────────────────────────────────────────────────────
const content = new VBox({
children: [
headerNode,
makeStep(strings.loadVideoTitleStringProperty, strings.loadVideoBodyStringProperty),
makeStep(strings.coordinateSystemTitleStringProperty, strings.coordinateSystemBodyStringProperty),
makeStep(strings.calibrationTitleStringProperty, strings.calibrationBodyStringProperty),
makeStep(strings.addTrackTitleStringProperty, strings.addTrackBodyStringProperty),
makeStep(strings.digitizeTitleStringProperty, strings.digitizeBodyStringProperty),
makeStep(strings.autoTrackTitleStringProperty, strings.autoTrackBodyStringProperty),
],
spacing: STEPS_SPACING,
align: "left",
});

const panel = new Panel(content, {
fill: TrackLabColors.panelFillProperty,
stroke: TrackLabColors.panelStrokeProperty,
cornerRadius: PANEL_CORNER_RADIUS,
xMargin: PANEL_X_MARGIN,
yMargin: PANEL_Y_MARGIN,
});

this.addChild(panel);
}
}

trackLab.register("InfoDialogNode", InfoDialogNode);
2 changes: 2 additions & 0 deletions src/screen-name/view/PlaybackControlsNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import { ButtonNode, RectangularPushButton, Slider } from "scenerystack/sun";
import { Tandem } from "scenerystack/tandem";
import { StringManager } from "../../i18n/StringManager.js";
import TrackLabColors from "../../TrackLabColors.js";

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

import { BUTTON_X_MARGIN, BUTTON_Y_MARGIN } from "../../TrackLabConstants.js";
import type { SimModel } from "../model/SimModel.js";

Expand Down
25 changes: 24 additions & 1 deletion src/screen-name/view/SimScreenView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@
import { DerivedProperty } from "scenerystack/axon";
import { Vector2 } from "scenerystack/dot";
import { DragListener } from "scenerystack/scenery";
import { ResetAllButton } from "scenerystack/scenery-phet";
import { InfoButton, ResetAllButton } from "scenerystack/scenery-phet";
import { ScreenView, type ScreenViewOptions } from "scenerystack/sim";
import { Tandem } from "scenerystack/tandem";
import type { TrackLabPreferencesModel } from "../../preferences/TrackLabPreferencesModel.js";
import { CONTROL_PANEL_LEFT_MARGIN, DATA_TABLE_TOP_SPACING, RESET_BUTTON_MARGIN } from "../../TrackLabConstants.js";
import type { SimModel } from "../model/SimModel.js";
import { CalibrationToolNode } from "./CalibrationToolNode.js";
import { ControlPanel } from "./ControlPanel.js";
import { CoordinateSystemNode } from "./CoordinateSystemNode.js";
import { DataTableNode } from "./DataTableNode.js";
import { InfoDialogNode } from "./InfoDialogNode.js";
import { KinematicsGraphNode } from "./KinematicsGraphNode.js";
import { TrackListPanel } from "./TrackListPanel.js";
import { VideoPlayerNode } from "./VideoPlayerNode.js";
Expand Down Expand Up @@ -118,6 +120,19 @@ export class SimScreenView extends ScreenView {
const kinematicsGraph = new KinematicsGraphNode(model, this, trackLabPreferences);
this.addChild(kinematicsGraph);

// ── Info dialog (explains digitizing workflow) ────────────────────────────
const infoDialogNode = new InfoDialogNode();
this.addChild(infoDialogNode);

// ── Info button (lower-left corner, same vertical level as reset button) ─
const infoButton = new InfoButton({
listener: () => {
infoDialogNode.visible = !infoDialogNode.visible;
},
tandem: Tandem.OPT_OUT,
});
this.addChild(infoButton);

// ── Webcam panel (topmost when visible, above coord/calibration overlays) ─
const webcamPanel = this.videoPlayerNode.webcamPanel;
this.addChild(webcamPanel);
Expand All @@ -138,6 +153,14 @@ export class SimScreenView extends ScreenView {
resetAllButton.right = visibleBounds.maxX - RESET_BUTTON_MARGIN;
resetAllButton.bottom = visibleBounds.maxY - RESET_BUTTON_MARGIN;

// Info button: lower-left corner, mirroring the reset button margin.
infoButton.left = visibleBounds.minX + RESET_BUTTON_MARGIN;
infoButton.centerY = resetAllButton.centerY;

// Info dialog: centered horizontally, positioned just above the info button.
infoDialogNode.centerX = this.layoutBounds.centerX;
infoDialogNode.bottom = infoButton.top - RESET_BUTTON_MARGIN;

// Data table: shift left by extraWidth so it stays within the layout area
// and doesn't drift into the extra visible space claimed by the graph.
dataTableNode.left = this.videoPlayerNode.right + DATA_TABLE_LEFT_SPACING - extraWidth;
Expand Down
2 changes: 2 additions & 0 deletions src/screen-name/view/TrackListPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import { ButtonNode, Checkbox, Panel, RectangularPushButton } from "scenerystack
import { Tandem } from "scenerystack/tandem";
import { StringManager } from "../../i18n/StringManager.js";
import TrackLabColors from "../../TrackLabColors.js";

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

import { BUTTON_X_MARGIN, BUTTON_Y_MARGIN, PANEL_CORNER_RADIUS } from "../../TrackLabConstants.js";
import type { SimModel } from "../model/SimModel.js";
import type { Track } from "../model/Track.js";
Expand Down
10 changes: 8 additions & 2 deletions src/screen-name/view/WebcamPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ import {
import { Tandem } from "scenerystack/tandem";
import { StringManager } from "../../i18n/StringManager.js";
import TrackLabColors from "../../TrackLabColors.js";
import { BUTTON_X_MARGIN, BUTTON_Y_MARGIN, WEBCAM_PREVIEW_HEIGHT, WEBCAM_PREVIEW_WIDTH } from "../../TrackLabConstants.js";
import {
BUTTON_X_MARGIN,
BUTTON_Y_MARGIN,
WEBCAM_PREVIEW_HEIGHT,
WEBCAM_PREVIEW_WIDTH,
} from "../../TrackLabConstants.js";
import { estimateVideoFrameRate, type FPSEstimate, fixWebmDuration, WebcamRecorder } from "../../webcam.js";
import { FRAME_RATE_RANGE, type SimModel } from "../model/SimModel.js";

Expand Down Expand Up @@ -372,7 +377,8 @@ export class WebcamPanel extends Node {
cameras.forEach((cam, i) => {
const opt = document.createElement("option");
opt.value = cam.deviceId;
opt.textContent = cam.label || this.webcamStrings.cameraLabelStringProperty.value.replace("{{number}}", String(i + 1));
opt.textContent =
cam.label || this.webcamStrings.cameraLabelStringProperty.value.replace("{{number}}", String(i + 1));
this.cameraSelect.appendChild(opt);
});
}
Expand Down