diff --git a/src/TrackLabConstants.ts b/src/TrackLabConstants.ts index 9c689b7..bcdd15e 100644 --- a/src/TrackLabConstants.ts +++ b/src/TrackLabConstants.ts @@ -13,7 +13,6 @@ export const PANEL_CORNER_RADIUS = 8; // ── Screen layout bounds ─────────────────────────────────────────────────────── // SceneryStack's ScreenView.DEFAULT_LAYOUT_BOUNDS = Bounds2(0, 0, 1024, 618). const LAYOUT_WIDTH = 1024; -const LAYOUT_HEIGHT = 618; // ── Video display dimensions ─────────────────────────────────────────────────── // The video element is always rendered at this fixed pixel size. @@ -22,10 +21,12 @@ export const VIDEO_WIDTH = 640; export const VIDEO_HEIGHT = 360; // ── Video position in screen (layout) coordinates ──────────────────────────── -// The video element is centered at layoutBounds.center + (0, VIDEO_PLAYER_Y_OFFSET). -export const VIDEO_PLAYER_Y_OFFSET = -20; // video center offset below layout center +// The video player VBox is top-anchored at y=10. The source-control row +// (~40 px) plus MAIN_CONTENT_SPACING (10 px) places the video top at ~60 px, +// so the video centre sits at approximately y=240. +export const VIDEO_PLAYER_Y_OFFSET = -20; // kept for reference; no longer used for positioning export const VIDEO_CENTER_X = LAYOUT_WIDTH / 2; // 512 -export const VIDEO_CENTER_Y = LAYOUT_HEIGHT / 2 + VIDEO_PLAYER_Y_OFFSET; // 289 +export const VIDEO_CENTER_Y = 240; // approximate video centre with top-anchored layout // ── Initial calibration tool geometry ───────────────────────────────────────── // Half-length of the default calibration segment (pixels from centre to each endpoint). diff --git a/src/screen-name/view/PlaybackControlsNode.ts b/src/screen-name/view/PlaybackControlsNode.ts index 0244309..3fdff57 100644 --- a/src/screen-name/view/PlaybackControlsNode.ts +++ b/src/screen-name/view/PlaybackControlsNode.ts @@ -108,11 +108,9 @@ export class PlaybackControlsNode extends HBox { // ── Time and frame info display ──────────────────────────────────────── const formatDuration = (seconds: number): string => { if (!Number.isFinite(seconds) || seconds <= 0) { - return "0:00"; + return "0.00 s"; } - const mins = Math.floor(seconds / 60); - const secs = Math.floor(seconds % 60); - return `${mins}:${String(secs).padStart(2, "0")}`; + return `${seconds.toFixed(2)} s`; }; const totalTimeTextProperty = new DerivedProperty([model.durationProperty], (duration: number) => @@ -167,7 +165,7 @@ export class PlaybackControlsNode extends HBox { tandem: Tandem.OPT_OUT, }); - this.children = [infoDisplay, timeControlNode, scrubber, rewindButton]; + this.children = [timeControlNode, scrubber, rewindButton, infoDisplay]; this.disposePlaybackControlsNode = () => { timeSpeedProperty.unlink(onSpeedChange); diff --git a/src/screen-name/view/SimScreenView.ts b/src/screen-name/view/SimScreenView.ts index 4144b97..432f100 100644 --- a/src/screen-name/view/SimScreenView.ts +++ b/src/screen-name/view/SimScreenView.ts @@ -6,7 +6,6 @@ import { CONTROL_PANEL_LEFT_MARGIN, DATA_TABLE_TOP_SPACING, RESET_BUTTON_MARGIN, - VIDEO_PLAYER_Y_OFFSET, } from "../../TrackLabConstants.js"; import type { SimModel } from "../model/SimModel.js"; import { CalibrationToolNode } from "./CalibrationToolNode.js"; @@ -66,7 +65,7 @@ export class SimScreenView extends ScreenView { // SimModel from the tool state properties above). this.videoPlayerNode = new VideoPlayerNode(model, this); this.videoPlayerNode.left = controlPanel.right + 20; - this.videoPlayerNode.centerY = this.layoutBounds.centerY + VIDEO_PLAYER_Y_OFFSET; + this.videoPlayerNode.top = this.layoutBounds.top + 10; this.addChild(this.videoPlayerNode); // ── Coordinate system overlay (above video, below camera modal) ───────── @@ -96,6 +95,16 @@ export class SimScreenView extends ScreenView { }); this.addChild(resetAllButton); + // ── Playback controls bar (bottom of screen, same height as reset button) ─ + const playbackControlsNode = this.videoPlayerNode.playbackControlsNode; + this.addChild(playbackControlsNode); + playbackControlsNode.centerX = this.videoPlayerNode.centerX; + playbackControlsNode.centerY = resetAllButton.centerY; + playbackControlsNode.boundsProperty.lazyLink(() => { + playbackControlsNode.centerX = this.videoPlayerNode.centerX; + playbackControlsNode.centerY = resetAllButton.centerY; + }); + // ── Kinematics graph (bottom right, above reset all) ───────────────── const kinematicsGraph = new KinematicsGraphNode(model, this, trackLabPreferences); this.addChild(kinematicsGraph); diff --git a/src/screen-name/view/VideoPlayerNode.ts b/src/screen-name/view/VideoPlayerNode.ts index 78ab5f5..2c39bb5 100644 --- a/src/screen-name/view/VideoPlayerNode.ts +++ b/src/screen-name/view/VideoPlayerNode.ts @@ -23,6 +23,8 @@ export class VideoPlayerNode extends Node { private readonly videoElement: HTMLVideoElement; /** Webcam recording panel; positioned by SimScreenView for correct z-ordering. */ public readonly webcamPanel: WebcamPanel; + /** Playback controls bar; positioned by SimScreenView at the bottom of the screen. */ + public readonly playbackControlsNode: PlaybackControlsNode; private readonly model: SimModel; private readonly disposeVideoPlayer: () => void; /** Tracks the current blob URL so it can be revoked when a new one is loaded. */ @@ -103,15 +105,15 @@ export class VideoPlayerNode extends Node { }; model.playbackRateProperty.link(playbackRateListener); - // ── Playback controls ───────────────────────────────────────────────── - const playbackControlsNode = new PlaybackControlsNode( + // ── Playback controls (positioned by SimScreenView at screen bottom) ── + this.playbackControlsNode = new PlaybackControlsNode( model, this.videoElement, () => this.seekByFrames(-1), () => this.seekByFrames(1), ); // Pin to the video width so internal text changes never shift the row. - playbackControlsNode.preferredWidth = VIDEO_WIDTH; + this.playbackControlsNode.preferredWidth = VIDEO_WIDTH; // ── Fit video element to its intrinsic aspect ratio ─────────────────── // When a new clip is loaded, scale it to fill as much of VIDEO_WIDTH × @@ -131,14 +133,14 @@ export class VideoPlayerNode extends Node { this.videoElement.width = displayW; this.videoElement.height = displayH; model.videoDimensionsProperty.value = new Dimension2(displayW, displayH); - playbackControlsNode.preferredWidth = displayW; + this.playbackControlsNode.preferredWidth = displayW; model.tracker.resize(displayW, displayH); }; this.videoElement.addEventListener("loadedmetadata", onDimensionsLoaded); // Sync model time from video during playback (event-driven, not polled) const onTimeUpdate = () => { - if (!playbackControlsNode.scrubbing) { + if (!this.playbackControlsNode.scrubbing) { model.currentTimeProperty.value = this.videoElement.currentTime; } }; @@ -173,7 +175,7 @@ export class VideoPlayerNode extends Node { // ── Layout ───────────────────────────────────────────────────────────── const mainContent = new VBox({ - children: [videoSourceControlNode, videoLayer, playbackControlsNode], + children: [videoSourceControlNode, videoLayer], spacing: MAIN_CONTENT_SPACING, align: "center", }); @@ -208,7 +210,7 @@ export class VideoPlayerNode extends Node { this.currentBlobUrl = null; } autoTrackerNode.dispose(); - playbackControlsNode.dispose(); + this.playbackControlsNode.dispose(); videoSourceControlNode.dispose(); autoTrackingShownProperty.dispose(); };