From b26e4f0b0c64706cae4cf8918770a0424c831605 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Feb 2026 02:06:08 +0000 Subject: [PATCH 1/2] Hoist shared constants to TrackLabConstants and add a11y strings - Extract overlay constants (drag speed, touch dilation, label panel styling) and control panel constants to TrackLabConstants.ts - Add new shared colors (overlay label background/stroke, graph colors) to TrackLabColors.ts - Add localized accessibility strings for all interactive elements across overlays, control panel, measurement tools, info dialog, and graph - Update AngleToolNode, CalibrationToolNode, MeasuringTapeNode, ControlPanel, MeasurementToolsPanel, InfoDialogNode, ConfigurableGraph, and KinematicsGraphNode to use shared constants and a11y strings - Update CLAUDE.md with documentation for new constants and a11y conventions https://claude.ai/code/session_01QY96Kv94K89iT4fHoUbSLi --- CLAUDE.md | 14 +++ src/TrackLabColors.ts | 25 ++++ src/TrackLabConstants.ts | 23 ++++ src/i18n/StringManager.ts | 40 ++++++ src/i18n/strings_en.json | 26 +++- src/i18n/strings_fr.json | 22 +++- src/screen-name/graph/ConfigurableGraph.ts | 85 +++++++++---- src/screen-name/view/AngleToolNode.ts | 71 ++++++----- src/screen-name/view/CalibrationToolNode.ts | 29 +++-- src/screen-name/view/ControlPanel.ts | 116 +++++++++++------ src/screen-name/view/InfoDialogNode.ts | 119 ++++++++++++------ src/screen-name/view/KinematicsGraphNode.ts | 17 ++- src/screen-name/view/MeasurementToolsPanel.ts | 109 ++++++++++------ src/screen-name/view/MeasuringTapeNode.ts | 61 +++++---- 14 files changed, 535 insertions(+), 222 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0f98404..d9d6ba3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 `` (visually hidden) and `aria-label` on `` 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. diff --git a/src/TrackLabColors.ts b/src/TrackLabColors.ts index ffb5c65..a508d83 100644 --- a/src/TrackLabColors.ts +++ b/src/TrackLabColors.ts @@ -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)), diff --git a/src/TrackLabConstants.ts b/src/TrackLabConstants.ts index 8cce2b7..316fba3 100644 --- a/src/TrackLabConstants.ts +++ b/src/TrackLabConstants.ts @@ -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; diff --git a/src/i18n/StringManager.ts b/src/i18n/StringManager.ts index 7249383..c80a2be 100644 --- a/src/i18n/StringManager.ts +++ b/src/i18n/StringManager.ts @@ -413,6 +413,26 @@ export class StringManager { removeTrackStringProperty: ReadOnlyProperty; dataTableStringProperty: ReadOnlyProperty; exportCSVStringProperty: ReadOnlyProperty; + measuringTapeBaseStringProperty: ReadOnlyProperty; + measuringTapeTipStringProperty: ReadOnlyProperty; + angleToolVertexStringProperty: ReadOnlyProperty; + angleArm1StringProperty: ReadOnlyProperty; + angleArm2StringProperty: ReadOnlyProperty; + toggleAxesStringProperty: ReadOnlyProperty; + toggleCalibrationStringProperty: ReadOnlyProperty; + toggleMagnifierStringProperty: ReadOnlyProperty; + toggleAutoTrackingStringProperty: ReadOnlyProperty; + toggleMeasuringTapeStringProperty: ReadOnlyProperty; + toggleAngleToolStringProperty: ReadOnlyProperty; + graphRescaleStringProperty: ReadOnlyProperty; + graphZoomInStringProperty: ReadOnlyProperty; + graphZoomOutStringProperty: ReadOnlyProperty; + graphPanLeftStringProperty: ReadOnlyProperty; + graphPanRightStringProperty: ReadOnlyProperty; + graphPanUpStringProperty: ReadOnlyProperty; + graphPanDownStringProperty: ReadOnlyProperty; + kinematicsGraphStringProperty: ReadOnlyProperty; + selectTrackForGraphStringProperty: ReadOnlyProperty; } { return { videoPlayerStringProperty: this.stringProperties.a11y.videoPlayerStringProperty, @@ -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, }; } diff --git a/src/i18n/strings_en.json b/src/i18n/strings_en.json index d431a97..49d85d0 100644 --- a/src/i18n/strings_en.json +++ b/src/i18n/strings_en.json @@ -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" } } diff --git a/src/i18n/strings_fr.json b/src/i18n/strings_fr.json index dd97b59..2396b83 100644 --- a/src/i18n/strings_fr.json +++ b/src/i18n/strings_fr.json @@ -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" } } diff --git a/src/screen-name/graph/ConfigurableGraph.ts b/src/screen-name/graph/ConfigurableGraph.ts index d5ecd4f..3252382 100644 --- a/src/screen-name/graph/ConfigurableGraph.ts +++ b/src/screen-name/graph/ConfigurableGraph.ts @@ -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"; @@ -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): Node => { const buttonText = new Text(label, { font: BUTTON_FONT, fill: TrackLabColors.controlPanelStrokeProperty, @@ -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 @@ -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({ diff --git a/src/screen-name/view/AngleToolNode.ts b/src/screen-name/view/AngleToolNode.ts index cdb8d93..01d0318 100644 --- a/src/screen-name/view/AngleToolNode.ts +++ b/src/screen-name/view/AngleToolNode.ts @@ -16,12 +16,20 @@ import { Circle, Line, Node, Path, RichDragListener, Text } from "scenerystack/s import { 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 { + LABEL_PANEL_CORNER_RADIUS, + LABEL_PANEL_SCALE, + LABEL_PANEL_X_MARGIN, + LABEL_PANEL_Y_MARGIN, + OVERLAY_DRAG_SPEED, + OVERLAY_SHIFT_DRAG_SPEED, + OVERLAY_TOUCH_DILATION, +} from "../../TrackLabConstants.js"; import type { SimModel } from "../model/SimModel.js"; // ── Visual constants ────────────────────────────────────────────────────────── -const ARM_COLOR = "rgb(170, 100, 255)"; -const ARM_SHADOW_COLOR = "rgba(0, 0, 0, 0.45)"; const ARM_LINE_WIDTH = 2.5; const ARM_SHADOW_WIDTH = 5; const ARM_DASH: number[] = [8, 4]; @@ -31,17 +39,16 @@ const ARC_LINE_WIDTH = 2; const VERTEX_RADIUS = 7; const ENDPOINT_RADIUS = 5; -const TOUCH_DILATION = 12; +const VERTEX_OUTLINE_WIDTH = 1.5; +const ARM_ENDPOINT_LINE_WIDTH = 2; const FONT = new PhetFont({ size: 12, weight: "bold" }); const LABEL_OFFSET = ARC_RADIUS + 20; // distance from vertex to label centre -const DRAG_SPEED = 200; -const SHIFT_DRAG_SPEED = 40; - // Minimum arm length below which the arc and label are hidden to avoid // degenerate geometry (zero-length arm → atan2 is undefined). const MIN_ARM_LENGTH = 5; +const ANGLE_DECIMAL_PLACES = 1; /** * Three-handle angle tool. Drag the vertex to reposition the whole tool's @@ -53,13 +60,15 @@ export class AngleToolNode extends Node { public constructor(visibleProperty: TReadOnlyProperty, model: SimModel) { super(); + const a11yStrings = StringManager.getInstance().getA11y(); + // ── Arm shadow lines ────────────────────────────────────────────────── const shadow1 = new Line(0, 0, 0, 0, { - stroke: ARM_SHADOW_COLOR, + stroke: TrackLabColors.angleToolShadowProperty, lineWidth: ARM_SHADOW_WIDTH, }); const shadow2 = new Line(0, 0, 0, 0, { - stroke: ARM_SHADOW_COLOR, + stroke: TrackLabColors.angleToolShadowProperty, lineWidth: ARM_SHADOW_WIDTH, }); this.addChild(shadow1); @@ -67,12 +76,12 @@ export class AngleToolNode extends Node { // ── Arm lines ───────────────────────────────────────────────────────── const arm1Line = new Line(0, 0, 0, 0, { - stroke: ARM_COLOR, + stroke: TrackLabColors.angleToolColorProperty, lineWidth: ARM_LINE_WIDTH, lineDash: ARM_DASH, }); const arm2Line = new Line(0, 0, 0, 0, { - stroke: ARM_COLOR, + stroke: TrackLabColors.angleToolColorProperty, lineWidth: ARM_LINE_WIDTH, lineDash: ARM_DASH, }); @@ -81,60 +90,60 @@ export class AngleToolNode extends Node { // ── Arc at vertex ───────────────────────────────────────────────────── const arcPath = new Path(null, { - stroke: ARM_COLOR, + stroke: TrackLabColors.angleToolColorProperty, lineWidth: ARC_LINE_WIDTH, }); this.addChild(arcPath); // ── Vertex handle ───────────────────────────────────────────────────── const vertexNode = new Circle(VERTEX_RADIUS, { - fill: ARM_COLOR, - stroke: "rgba(0, 0, 0, 0.65)", - lineWidth: 1.5, + fill: TrackLabColors.angleToolColorProperty, + stroke: TrackLabColors.overlayHandleOutlineProperty, + lineWidth: VERTEX_OUTLINE_WIDTH, cursor: "grab", tagName: "div", focusable: true, - accessibleName: "Angle tool vertex", + accessibleName: a11yStrings.angleToolVertexStringProperty, }); - const vertexTouchArea = Shape.circle(0, 0, VERTEX_RADIUS + TOUCH_DILATION); + const vertexTouchArea = Shape.circle(0, 0, VERTEX_RADIUS + OVERLAY_TOUCH_DILATION); vertexNode.mouseArea = vertexTouchArea; vertexNode.touchArea = vertexTouchArea; this.addChild(vertexNode); // ── Arm endpoint handles ────────────────────────────────────────────── - const makeArmEndpoint = (accessibleName: string) => { + const makeArmEndpoint = (accessibleName: TReadOnlyProperty) => { const node = new Circle(ENDPOINT_RADIUS, { fill: "transparent", - stroke: ARM_COLOR, - lineWidth: 2, + stroke: TrackLabColors.angleToolColorProperty, + lineWidth: ARM_ENDPOINT_LINE_WIDTH, cursor: "crosshair", tagName: "div", focusable: true, accessibleName, }); - const touchArea = Shape.circle(0, 0, ENDPOINT_RADIUS + TOUCH_DILATION); + const touchArea = Shape.circle(0, 0, ENDPOINT_RADIUS + OVERLAY_TOUCH_DILATION); node.mouseArea = touchArea; node.touchArea = touchArea; return node; }; - const arm1Node = makeArmEndpoint("Angle arm 1"); - const arm2Node = makeArmEndpoint("Angle arm 2"); + const arm1Node = makeArmEndpoint(a11yStrings.angleArm1StringProperty); + const arm2Node = makeArmEndpoint(a11yStrings.angleArm2StringProperty); this.addChild(arm1Node); this.addChild(arm2Node); // ── Angle label ─────────────────────────────────────────────────────── - const angleText = new Text("---", { + const angleText = new Text("", { font: FONT, fill: TrackLabColors.textOnDarkProperty, }); const labelPanel = new Panel(angleText, { fill: TrackLabColors.panelFillProperty, stroke: TrackLabColors.panelStrokeProperty, - cornerRadius: 4, - xMargin: 6, - yMargin: 3, + cornerRadius: LABEL_PANEL_CORNER_RADIUS, + xMargin: LABEL_PANEL_X_MARGIN, + yMargin: LABEL_PANEL_Y_MARGIN, }); - labelPanel.setScaleMagnitude(0.8); + labelPanel.setScaleMagnitude(LABEL_PANEL_SCALE); this.addChild(labelPanel); // ── Geometry multilink ──────────────────────────────────────────────── @@ -183,7 +192,7 @@ export class AngleToolNode extends Node { // Angle value const angleDeg = (Math.abs(diff) * 180) / Math.PI; - angleText.string = `${angleDeg.toFixed(1)}\u00b0`; + angleText.string = `${angleDeg.toFixed(ANGLE_DECIMAL_PLACES)}\u00b0`; // Label along the bisector, outside the arc const bisectorAngle = a1 + diff / 2; @@ -196,21 +205,21 @@ export class AngleToolNode extends Node { vertexNode.addInputListener( new RichDragListener({ positionProperty: model.angleVertexProperty, - keyboardDragListenerOptions: { dragSpeed: DRAG_SPEED, shiftDragSpeed: SHIFT_DRAG_SPEED }, + keyboardDragListenerOptions: { dragSpeed: OVERLAY_DRAG_SPEED, shiftDragSpeed: OVERLAY_SHIFT_DRAG_SPEED }, tandem: Tandem.OPT_OUT, }), ); arm1Node.addInputListener( new RichDragListener({ positionProperty: model.angleArm1Property, - keyboardDragListenerOptions: { dragSpeed: DRAG_SPEED, shiftDragSpeed: SHIFT_DRAG_SPEED }, + keyboardDragListenerOptions: { dragSpeed: OVERLAY_DRAG_SPEED, shiftDragSpeed: OVERLAY_SHIFT_DRAG_SPEED }, tandem: Tandem.OPT_OUT, }), ); arm2Node.addInputListener( new RichDragListener({ positionProperty: model.angleArm2Property, - keyboardDragListenerOptions: { dragSpeed: DRAG_SPEED, shiftDragSpeed: SHIFT_DRAG_SPEED }, + keyboardDragListenerOptions: { dragSpeed: OVERLAY_DRAG_SPEED, shiftDragSpeed: OVERLAY_SHIFT_DRAG_SPEED }, tandem: Tandem.OPT_OUT, }), ); diff --git a/src/screen-name/view/CalibrationToolNode.ts b/src/screen-name/view/CalibrationToolNode.ts index a4d7a32..1941cf7 100644 --- a/src/screen-name/view/CalibrationToolNode.ts +++ b/src/screen-name/view/CalibrationToolNode.ts @@ -5,7 +5,6 @@ * and specify its value in desired units to establish the model-to-pixel scale. */ -import { Color } from "scenerystack"; import type { TReadOnlyProperty } from "scenerystack/axon"; import { DerivedProperty, Multilink } from "scenerystack/axon"; import { Shape } from "scenerystack/kite"; @@ -16,7 +15,13 @@ import { ButtonNode, ComboBox, type ComboBoxItem, Panel, TextPushButton } from " import { Tandem } from "scenerystack/tandem"; import { StringManager } from "../../i18n/StringManager.js"; import TrackLabColors from "../../TrackLabColors.js"; -import { BUTTON_X_MARGIN, BUTTON_Y_MARGIN, DIGITIZING_DIM_OPACITY } from "../../TrackLabConstants.js"; +import { + BUTTON_X_MARGIN, + BUTTON_Y_MARGIN, + DIGITIZING_DIM_OPACITY, + OVERLAY_DRAG_SPEED, + OVERLAY_SHIFT_DRAG_SPEED, +} from "../../TrackLabConstants.js"; import type { SimModel } from "../model/SimModel.js"; import { CALIBRATION_UNITS } from "../model/SimModel.js"; @@ -37,11 +42,9 @@ const MIDPOINT_PANEL_X_MARGIN = 8; const MIDPOINT_PANEL_Y_MARGIN = 6; const MIDPOINT_PANEL_SPACING = 8; const MIDPOINT_Y_OFFSET = 12; // pixels above midpoint where the panel sits -const ENDPOINT_DRAG_SPEED = 200; // pixels/s for normal keyboard drag -const ENDPOINT_SHIFT_DRAG_SPEED = 40; // pixels/s for shift-key keyboard drag // Pixel distance below which endpoints are considered overlapping and a warning is shown. const OVERLAP_WARNING_DISTANCE = 10; -const ENDPOINT_WARNING_COLOR = new Color(255, 60, 60); +const CALIBRATION_DECIMAL_PLACES = 2; /** * Two-endpoint calibration ruler overlay for setting the real-world scale. @@ -136,7 +139,7 @@ export class CalibrationToolNode extends Node { // Button showing current value + unit; clicking it opens the keypad. const buttonLabelProperty = new DerivedProperty( [model.calibDistanceProperty, model.calibUnitProperty], - (dist, unit) => `${dist.toFixed(2)} ${unit}`, + (dist, unit) => `${dist.toFixed(CALIBRATION_DECIMAL_PLACES)} ${unit}`, ); const distanceButton = new TextPushButton(buttonLabelProperty, { @@ -199,7 +202,7 @@ export class CalibrationToolNode extends Node { // Shown when endpoints are too close together to produce a valid calibration. const overlapWarning = new Text(calibrationStrings.pointsTooCloseStringProperty, { font: WARNING_FONT, - fill: ENDPOINT_WARNING_COLOR, + fill: TrackLabColors.calibrationWarningColorProperty, visible: false, }); this.addChild(overlapWarning); @@ -227,7 +230,9 @@ export class CalibrationToolNode extends Node { // Show warning and highlight endpoints when too close to be useful. const tooClose = p1.distance(p2) < OVERLAP_WARNING_DISTANCE; - const endpointFill = tooClose ? ENDPOINT_WARNING_COLOR : TrackLabColors.calibrationFillProperty.value; + const endpointFill = tooClose + ? TrackLabColors.calibrationWarningColorProperty.value + : TrackLabColors.calibrationFillProperty.value; endpoint1.fill = endpointFill; endpoint2.fill = endpointFill; overlapWarning.visible = tooClose; @@ -246,8 +251,8 @@ export class CalibrationToolNode extends Node { new RichDragListener({ positionProperty: model.calibPoint1Property, keyboardDragListenerOptions: { - dragSpeed: ENDPOINT_DRAG_SPEED, - shiftDragSpeed: ENDPOINT_SHIFT_DRAG_SPEED, + dragSpeed: OVERLAY_DRAG_SPEED, + shiftDragSpeed: OVERLAY_SHIFT_DRAG_SPEED, }, tandem: Tandem.OPT_OUT, }), @@ -256,8 +261,8 @@ export class CalibrationToolNode extends Node { new RichDragListener({ positionProperty: model.calibPoint2Property, keyboardDragListenerOptions: { - dragSpeed: ENDPOINT_DRAG_SPEED, - shiftDragSpeed: ENDPOINT_SHIFT_DRAG_SPEED, + dragSpeed: OVERLAY_DRAG_SPEED, + shiftDragSpeed: OVERLAY_SHIFT_DRAG_SPEED, }, tandem: Tandem.OPT_OUT, }), diff --git a/src/screen-name/view/ControlPanel.ts b/src/screen-name/view/ControlPanel.ts index e4c5501..e3298ed 100644 --- a/src/screen-name/view/ControlPanel.ts +++ b/src/screen-name/view/ControlPanel.ts @@ -8,12 +8,18 @@ import { Circle, Line, Node, VBox } from "scenerystack/scenery"; import { ArrowNode } from "scenerystack/scenery-phet"; import { Checkbox, Panel } from "scenerystack/sun"; +import { StringManager } from "../../i18n/StringManager.js"; import type { TrackLabPreferencesModel } from "../../preferences/TrackLabPreferencesModel.js"; import TrackLabColors from "../../TrackLabColors.js"; -import { PANEL_CORNER_RADIUS } from "../../TrackLabConstants.js"; +import { + CONTROL_ICON_SIZE, + CONTROL_PANEL_ROWS_SPACING, + CONTROL_PANEL_X_MARGIN, + CONTROL_PANEL_Y_MARGIN, + PANEL_CORNER_RADIUS, +} from "../../TrackLabConstants.js"; import type { SimModel } from "../model/SimModel.js"; -const ICON_SIZE = 20; // bounding box each icon targets const ICON_ARROW_HEAD_SIZE = 5; // headWidth and headHeight for icon arrows const ICON_ARROW_TAIL_WIDTH = 1.5; const ICON_LINE_WIDTH_THICK = 1.5; @@ -22,36 +28,55 @@ const ICON_LINE_WIDTH_MAGNIFIER = 2; // handle line in magnifier icon const ICON_DOT_RADIUS = 3; // calibration endpoint dots const ICON_CENTER_DOT_RADIUS = 2; // centre dot in tracking icon const ICON_LINE_DASH: number[] = [3, 2]; -const PANEL_ROWS_SPACING = 12; -const PANEL_X_MARGIN = 12; -const PANEL_Y_MARGIN = 12; + +// ── Icon layout fractions ───────────────────────────────────────────────────── +const ICON_ORIGIN_FRACTION = 0.7; // where axes originate (fraction of CONTROL_ICON_SIZE) +const ICON_X_ARROW_END_FRACTION = 0.85; +const ICON_Y_ARROW_END_FRACTION = 0.05; +const ICON_HALF_FRACTION = 0.4; +const ICON_CENTER_FRACTION = 0.5; +const ICON_MAGNIFIER_RADIUS_FRACTION = 0.32; +const ICON_TRACKING_RADIUS_FRACTION = 0.35; +const ICON_TRACKING_GAP_FRACTION = 0.15; // ── Icons ───────────────────────────────────────────────────────────────── /** Two small XY arrows. */ function axesIcon(): Node { - const xArrow = new ArrowNode(0, ICON_SIZE * 0.7, ICON_SIZE * 0.85, ICON_SIZE * 0.7, { - fill: TrackLabColors.axisXColorProperty, - stroke: null, - headWidth: ICON_ARROW_HEAD_SIZE, - headHeight: ICON_ARROW_HEAD_SIZE, - tailWidth: ICON_ARROW_TAIL_WIDTH, - }); - const yArrow = new ArrowNode(0, ICON_SIZE * 0.7, 0, ICON_SIZE * 0.05, { - fill: TrackLabColors.axisYColorProperty, - stroke: null, - headWidth: ICON_ARROW_HEAD_SIZE, - headHeight: ICON_ARROW_HEAD_SIZE, - tailWidth: ICON_ARROW_TAIL_WIDTH, - }); + const xArrow = new ArrowNode( + 0, + CONTROL_ICON_SIZE * ICON_ORIGIN_FRACTION, + CONTROL_ICON_SIZE * ICON_X_ARROW_END_FRACTION, + CONTROL_ICON_SIZE * ICON_ORIGIN_FRACTION, + { + fill: TrackLabColors.axisXColorProperty, + stroke: null, + headWidth: ICON_ARROW_HEAD_SIZE, + headHeight: ICON_ARROW_HEAD_SIZE, + tailWidth: ICON_ARROW_TAIL_WIDTH, + }, + ); + const yArrow = new ArrowNode( + 0, + CONTROL_ICON_SIZE * ICON_ORIGIN_FRACTION, + 0, + CONTROL_ICON_SIZE * ICON_Y_ARROW_END_FRACTION, + { + fill: TrackLabColors.axisYColorProperty, + stroke: null, + headWidth: ICON_ARROW_HEAD_SIZE, + headHeight: ICON_ARROW_HEAD_SIZE, + tailWidth: ICON_ARROW_TAIL_WIDTH, + }, + ); return new Node({ children: [xArrow, yArrow] }); } /** Two endpoint dots joined by a dashed line. */ function calibrationIcon(): Node { - const cx = ICON_SIZE * 0.5; - const cy = ICON_SIZE * 0.5; - const half = ICON_SIZE * 0.4; + const cx = CONTROL_ICON_SIZE * ICON_CENTER_FRACTION; + const cy = CONTROL_ICON_SIZE * ICON_CENTER_FRACTION; + const half = CONTROL_ICON_SIZE * ICON_HALF_FRACTION; const calColor = TrackLabColors.calibrationFillProperty; return new Node({ children: [ @@ -68,7 +93,7 @@ function calibrationIcon(): Node { /** Circle with a diagonal handle — magnifying glass silhouette. */ function magnifyIcon(): Node { - const r = ICON_SIZE * 0.32; + const r = CONTROL_ICON_SIZE * ICON_MAGNIFIER_RADIUS_FRACTION; const cx = r + 1; const cy = r + 1; const gray = TrackLabColors.iconGrayProperty; @@ -81,20 +106,26 @@ function magnifyIcon(): Node { x: cx, y: cy, }), - new Line(cx + r * 0.7, cy + r * 0.7, ICON_SIZE - 1, ICON_SIZE - 1, { - stroke: gray, - lineWidth: ICON_LINE_WIDTH_MAGNIFIER, - }), + new Line( + cx + r * ICON_ORIGIN_FRACTION, + cy + r * ICON_ORIGIN_FRACTION, + CONTROL_ICON_SIZE - 1, + CONTROL_ICON_SIZE - 1, + { + stroke: gray, + lineWidth: ICON_LINE_WIDTH_MAGNIFIER, + }, + ), ], }); } /** Crosshair with a small centre dot — tracking target. */ function trackingIcon(): Node { - const cx = ICON_SIZE * 0.5; - const cy = ICON_SIZE * 0.5; - const r = ICON_SIZE * 0.35; - const gap = ICON_SIZE * 0.15; + const cx = CONTROL_ICON_SIZE * ICON_CENTER_FRACTION; + const cy = CONTROL_ICON_SIZE * ICON_CENTER_FRACTION; + const r = CONTROL_ICON_SIZE * ICON_TRACKING_RADIUS_FRACTION; + const gap = CONTROL_ICON_SIZE * ICON_TRACKING_GAP_FRACTION; const gray = TrackLabColors.iconGrayProperty; return new Node({ children: [ @@ -128,10 +159,11 @@ function trackingIcon(): Node { // ── Helper ──────────────────────────────────────────────────────────────── -function makeRow(icon: Node, property: SimModel["axesVisibleProperty"]): Checkbox { +function makeRow(icon: Node, property: SimModel["axesVisibleProperty"], accessibleName: string): Checkbox { return new Checkbox(property, icon, { checkboxColor: TrackLabColors.checkboxColorProperty, checkboxColorBackground: TrackLabColors.checkboxColorBackgroundProperty, + accessibleName, }); } @@ -148,19 +180,25 @@ export class ControlPanel extends Panel { * @param trackLabPreferences - Determines whether the auto-tracking checkbox is shown. */ public constructor(model: SimModel, trackLabPreferences: TrackLabPreferencesModel) { - const autoTrackingCheckbox = makeRow(trackingIcon(), model.autoTrackingProperty); + const a11yStrings = StringManager.getInstance().getA11y(); + + const autoTrackingCheckbox = makeRow( + trackingIcon(), + model.autoTrackingProperty, + a11yStrings.toggleAutoTrackingStringProperty.value, + ); // The auto-tracking checkbox is only visible if the preference allows it. // When the preference is disabled, the checkbox is completely hidden from the panel. autoTrackingCheckbox.visibleProperty = trackLabPreferences.enableAutoTrackingProperty; const rows = new VBox({ children: [ - makeRow(axesIcon(), model.axesVisibleProperty), - makeRow(calibrationIcon(), model.calibrationVisibleProperty), - makeRow(magnifyIcon(), model.magnifyVideoProperty), + makeRow(axesIcon(), model.axesVisibleProperty, a11yStrings.toggleAxesStringProperty.value), + makeRow(calibrationIcon(), model.calibrationVisibleProperty, a11yStrings.toggleCalibrationStringProperty.value), + makeRow(magnifyIcon(), model.magnifyVideoProperty, a11yStrings.toggleMagnifierStringProperty.value), autoTrackingCheckbox, ], - spacing: PANEL_ROWS_SPACING, + spacing: CONTROL_PANEL_ROWS_SPACING, align: "left", }); @@ -168,8 +206,8 @@ export class ControlPanel extends Panel { fill: TrackLabColors.panelFillProperty, stroke: TrackLabColors.panelStrokeProperty, cornerRadius: PANEL_CORNER_RADIUS, - xMargin: PANEL_X_MARGIN, - yMargin: PANEL_Y_MARGIN, + xMargin: CONTROL_PANEL_X_MARGIN, + yMargin: CONTROL_PANEL_Y_MARGIN, }); } } diff --git a/src/screen-name/view/InfoDialogNode.ts b/src/screen-name/view/InfoDialogNode.ts index dd2da70..c05d8ed 100644 --- a/src/screen-name/view/InfoDialogNode.ts +++ b/src/screen-name/view/InfoDialogNode.ts @@ -13,7 +13,7 @@ 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 { CONTROL_ICON_SIZE, PANEL_CORNER_RADIUS } from "../../TrackLabConstants.js"; import trackLab from "../../TrackLabNamespace.js"; // ── Layout constants ────────────────────────────────────────────────────────── @@ -26,8 +26,27 @@ 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; -const ICON_SIZE = 20; // bounding box for step icons const ICON_SPACING = 8; // gap between icon and text +const TITLE_MAX_WIDTH_SPACING = 8; // gap between title text and close button + +// ── Icon shared constants ──────────────────────────────────────────────────── +const ICON_ARROW_HEAD_SIZE = 5; +const ICON_ARROW_TAIL_WIDTH = 1.5; +const ICON_LINE_WIDTH_THICK = 1.5; +const ICON_LINE_WIDTH_MAGNIFIER = 2; +const ICON_DOT_RADIUS = 3; +const ICON_CENTER_DOT_RADIUS = 2; +const ICON_LINE_DASH: number[] = [3, 2]; + +// Icon layout fractions +const ICON_ORIGIN_FRACTION = 0.7; +const ICON_X_ARROW_END_FRACTION = 0.85; +const ICON_Y_ARROW_END_FRACTION = 0.05; +const ICON_HALF_FRACTION = 0.4; +const ICON_CENTER_FRACTION = 0.5; +const ICON_MAGNIFIER_RADIUS_FRACTION = 0.32; +const ICON_TRACKING_RADIUS_FRACTION = 0.35; +const ICON_TRACKING_GAP_FRACTION = 0.15; // ── Icon helpers ────────────────────────────────────────────────────────────── @@ -42,45 +61,57 @@ function videoIcon(): Node { .lineTo(18, 16) .lineTo(2, 16) .close(); - const folder = new Path(folderShape, { stroke: gray, lineWidth: 1.5, fill: null }); + const folder = new Path(folderShape, { stroke: gray, lineWidth: ICON_LINE_WIDTH_THICK, fill: null }); const play = new Path(new Shape().moveTo(8, 7).lineTo(8, 13).lineTo(13, 10).close(), { fill: gray }); return new Node({ children: [folder, play] }); } /** Two small XY arrows for coordinate system. */ function axesIcon(): Node { - const xArrow = new ArrowNode(0, ICON_SIZE * 0.7, ICON_SIZE * 0.85, ICON_SIZE * 0.7, { - fill: TrackLabColors.axisXColorProperty, - stroke: null, - headWidth: 5, - headHeight: 5, - tailWidth: 1.5, - }); - const yArrow = new ArrowNode(0, ICON_SIZE * 0.7, 0, ICON_SIZE * 0.05, { - fill: TrackLabColors.axisYColorProperty, - stroke: null, - headWidth: 5, - headHeight: 5, - tailWidth: 1.5, - }); + const xArrow = new ArrowNode( + 0, + CONTROL_ICON_SIZE * ICON_ORIGIN_FRACTION, + CONTROL_ICON_SIZE * ICON_X_ARROW_END_FRACTION, + CONTROL_ICON_SIZE * ICON_ORIGIN_FRACTION, + { + fill: TrackLabColors.axisXColorProperty, + stroke: null, + headWidth: ICON_ARROW_HEAD_SIZE, + headHeight: ICON_ARROW_HEAD_SIZE, + tailWidth: ICON_ARROW_TAIL_WIDTH, + }, + ); + const yArrow = new ArrowNode( + 0, + CONTROL_ICON_SIZE * ICON_ORIGIN_FRACTION, + 0, + CONTROL_ICON_SIZE * ICON_Y_ARROW_END_FRACTION, + { + fill: TrackLabColors.axisYColorProperty, + stroke: null, + headWidth: ICON_ARROW_HEAD_SIZE, + headHeight: ICON_ARROW_HEAD_SIZE, + tailWidth: ICON_ARROW_TAIL_WIDTH, + }, + ); return new Node({ children: [xArrow, yArrow] }); } /** Two endpoint dots joined by a dashed line for calibration. */ function calibrationIcon(): Node { - const cx = ICON_SIZE * 0.5; - const cy = ICON_SIZE * 0.5; - const half = ICON_SIZE * 0.4; + const cx = CONTROL_ICON_SIZE * ICON_CENTER_FRACTION; + const cy = CONTROL_ICON_SIZE * ICON_CENTER_FRACTION; + const half = CONTROL_ICON_SIZE * ICON_HALF_FRACTION; const calColor = TrackLabColors.calibrationFillProperty; return new Node({ children: [ new Line(cx - half, cy, cx + half, cy, { stroke: calColor, - lineWidth: 1.5, - lineDash: [3, 2], + lineWidth: ICON_LINE_WIDTH_THICK, + lineDash: ICON_LINE_DASH, }), - new Circle(3, { fill: calColor, x: cx - half, y: cy }), - new Circle(3, { fill: calColor, x: cx + half, y: cy }), + new Circle(ICON_DOT_RADIUS, { fill: calColor, x: cx - half, y: cy }), + new Circle(ICON_DOT_RADIUS, { fill: calColor, x: cx + half, y: cy }), ], }); } @@ -88,8 +119,8 @@ function calibrationIcon(): Node { /** Plus sign for adding tracks. */ function addTrackIcon(): Node { const gray = TrackLabColors.iconGrayProperty; - const center = ICON_SIZE * 0.5; - const size = ICON_SIZE * 0.6; + const center = CONTROL_ICON_SIZE * ICON_CENTER_FRACTION; + const size = CONTROL_ICON_SIZE * 0.6; return new Node({ children: [ new Rectangle(center - 1, center - size / 2, 2, size, { fill: gray }), @@ -100,7 +131,7 @@ function addTrackIcon(): Node { /** Magnifying glass for digitizing. */ function magnifyIcon(): Node { - const r = ICON_SIZE * 0.32; + const r = CONTROL_ICON_SIZE * ICON_MAGNIFIER_RADIUS_FRACTION; const cx = r + 1; const cy = r + 1; const gray = TrackLabColors.iconGrayProperty; @@ -108,36 +139,42 @@ function magnifyIcon(): Node { children: [ new Circle(r, { stroke: gray, - lineWidth: 1.5, + lineWidth: ICON_LINE_WIDTH_THICK, fill: null, x: cx, y: cy, }), - new Line(cx + r * 0.7, cy + r * 0.7, ICON_SIZE - 1, ICON_SIZE - 1, { - stroke: gray, - lineWidth: 2, - }), + new Line( + cx + r * ICON_ORIGIN_FRACTION, + cy + r * ICON_ORIGIN_FRACTION, + CONTROL_ICON_SIZE - 1, + CONTROL_ICON_SIZE - 1, + { + stroke: gray, + lineWidth: ICON_LINE_WIDTH_MAGNIFIER, + }, + ), ], }); } /** Crosshair with centre dot for auto-tracking. */ function trackingIcon(): Node { - const cx = ICON_SIZE * 0.5; - const cy = ICON_SIZE * 0.5; - const r = ICON_SIZE * 0.35; - const gap = ICON_SIZE * 0.15; + const cx = CONTROL_ICON_SIZE * ICON_CENTER_FRACTION; + const cy = CONTROL_ICON_SIZE * ICON_CENTER_FRACTION; + const r = CONTROL_ICON_SIZE * ICON_TRACKING_RADIUS_FRACTION; + const gap = CONTROL_ICON_SIZE * ICON_TRACKING_GAP_FRACTION; const gray = TrackLabColors.iconGrayProperty; return new Node({ children: [ new Circle(r, { stroke: gray, - lineWidth: 1.5, + lineWidth: ICON_LINE_WIDTH_THICK, fill: null, x: cx, y: cy, }), - new Circle(2, { fill: gray, x: cx, y: cy }), + new Circle(ICON_CENTER_DOT_RADIUS, { fill: gray, x: cx, y: cy }), new Line(cx, cy - r - gap, cx, cy - gap, { stroke: gray, lineWidth: 1 }), new Line(cx, cy + gap, cx, cy + r + gap, { stroke: gray, lineWidth: 1 }), new Line(cx - r - gap, cy, cx - gap, cy, { stroke: gray, lineWidth: 1 }), @@ -153,13 +190,13 @@ function makeStep(icon: Node, titleProp: ReadOnlyProperty, bodyProp: Rea const titleText = new Text(titleProp, { font: STEP_TITLE_FONT, fill: TrackLabColors.textOnDarkProperty, - maxWidth: CONTENT_WIDTH - ICON_SIZE - ICON_SPACING, + maxWidth: CONTENT_WIDTH - CONTROL_ICON_SIZE - ICON_SPACING, }); const bodyText = new RichText(bodyProp, { font: STEP_BODY_FONT, fill: TrackLabColors.textMutedProperty, - lineWrap: CONTENT_WIDTH - ICON_SIZE - ICON_SPACING, + lineWrap: CONTENT_WIDTH - CONTROL_ICON_SIZE - ICON_SPACING, }); const textBox = new VBox({ @@ -208,7 +245,7 @@ export class InfoDialogNode extends Node { const headerNode = new Node({ children: [titleText, closeButton] }); closeButton.right = CONTENT_WIDTH; closeButton.centerY = titleText.centerY; - titleText.maxWidth = CONTENT_WIDTH - closeButton.width - 8; + titleText.maxWidth = CONTENT_WIDTH - closeButton.width - TITLE_MAX_WIDTH_SPACING; // ── Steps ──────────────────────────────────────────────────────────────── const content = new VBox({ diff --git a/src/screen-name/view/KinematicsGraphNode.ts b/src/screen-name/view/KinematicsGraphNode.ts index 3557102..5b9bfda 100644 --- a/src/screen-name/view/KinematicsGraphNode.ts +++ b/src/screen-name/view/KinematicsGraphNode.ts @@ -10,6 +10,7 @@ import { Property } from "scenerystack/axon"; import { Node, Text, VBox } from "scenerystack/scenery"; import { PhetFont } from "scenerystack/scenery-phet"; import { Checkbox } from "scenerystack/sun"; +import { StringManager } from "../../i18n/StringManager.js"; import type { TrackLabPreferencesModel } from "../../preferences/TrackLabPreferencesModel.js"; import TrackLabColors from "../../TrackLabColors.js"; import ConfigurableGraph from "../graph/ConfigurableGraph.js"; @@ -36,7 +37,12 @@ export class KinematicsGraphNode extends Node { private readonly disposeKinematicsGraph: () => void; public constructor(model: SimModel, listParent: Node, preferencesModel: TrackLabPreferencesModel) { - super({ visible: false }); + const a11yStrings = StringManager.getInstance().getA11y(); + super({ + visible: false, + tagName: "div", + accessibleName: a11yStrings.kinematicsGraphStringProperty, + }); this.model = model; this.selectedTracksProperty = new Property>(new Set()); @@ -227,11 +233,13 @@ export class KinematicsGraphNode extends Node { fill: track.color, }); - // Create checkbox + // Create checkbox with accessibility label + const kinematicsGraphStrings = StringManager.getInstance().getKinematicsGraph(); const checkbox = new Checkbox(checkboxProperty, label, { checkboxColor: TrackLabColors.checkboxColorProperty, checkboxColorBackground: TrackLabColors.checkboxColorBackgroundProperty, spacing: 4, + accessibleName: kinematicsGraphStrings.trackItemStringProperty.value.replace("{{symbol}}", track.symbol), }); // Store for later disposal @@ -263,8 +271,9 @@ export class KinematicsGraphNode extends Node { const checkboxContainer = this.trackCheckboxPanel.children[0]; if (checkboxContainer) { // Position relative to graph's current dimensions - checkboxContainer.right = this.graph.getGraphWidth() - 8; - checkboxContainer.top = 8; + const CHECKBOX_INSET = 8; + checkboxContainer.right = this.graph.getGraphWidth() - CHECKBOX_INSET; + checkboxContainer.top = CHECKBOX_INSET; } } diff --git a/src/screen-name/view/MeasurementToolsPanel.ts b/src/screen-name/view/MeasurementToolsPanel.ts index 406b267..f1e18ef 100644 --- a/src/screen-name/view/MeasurementToolsPanel.ts +++ b/src/screen-name/view/MeasurementToolsPanel.ts @@ -13,51 +13,77 @@ import { Shape } from "scenerystack/kite"; import { Circle, Line, Node, Path, VBox } from "scenerystack/scenery"; import { Checkbox, Panel } from "scenerystack/sun"; +import { StringManager } from "../../i18n/StringManager.js"; import TrackLabColors from "../../TrackLabColors.js"; -import { PANEL_CORNER_RADIUS } from "../../TrackLabConstants.js"; +import { + CONTROL_ICON_SIZE, + CONTROL_PANEL_ROWS_SPACING, + CONTROL_PANEL_X_MARGIN, + CONTROL_PANEL_Y_MARGIN, + PANEL_CORNER_RADIUS, +} from "../../TrackLabConstants.js"; import type { SimModel } from "../model/SimModel.js"; -const ICON_SIZE = 20; -const PANEL_ROWS_SPACING = 12; -const PANEL_X_MARGIN = 12; -const PANEL_Y_MARGIN = 12; +// ── Icon geometry constants ────────────────────────────────────────────────── +const ICON_EDGE_INSET = 1; +const ICON_TAPE_BODY_LINE_WIDTH = 2.5; +const ICON_REEL_RADIUS = 4; +const ICON_REEL_OUTLINE_WIDTH = 1; +const ICON_REEL_DOT_RADIUS = 1.5; +const ICON_TIP_RADIUS = 2.5; +const ICON_TICK_LENGTH = 4; +const ICON_TICK_HALF_LENGTH = 2.5; +const ICON_TICK_LINE_WIDTH = 1.5; +const ICON_ARM_LINE_WIDTH = 1.5; +const ICON_ARC_RADIUS = 7; // ── Icons ───────────────────────────────────────────────────────────────────── /** Measuring tape: a horizontal line with a reel circle at one end and a tick * mark at each end plus the midpoint to evoke a tape measure. */ function measuringTapeIcon(): Node { - const cy = ICON_SIZE / 2; - const color = "rgb(240, 185, 55)"; - const x0 = 1; - const x1 = ICON_SIZE - 1; - const xMid = ICON_SIZE / 2; + const cy = CONTROL_ICON_SIZE / 2; + const x0 = ICON_EDGE_INSET; + const x1 = CONTROL_ICON_SIZE - ICON_EDGE_INSET; + const xMid = CONTROL_ICON_SIZE / 2; return new Node({ children: [ // Tape body line - new Line(x0, cy, x1, cy, { stroke: color, lineWidth: 2.5 }), + new Line(x0, cy, x1, cy, { + stroke: TrackLabColors.measuringTapeColorProperty, + lineWidth: ICON_TAPE_BODY_LINE_WIDTH, + }), // Reel circle (left end) - new Circle(4, { - fill: color, - stroke: "rgba(0,0,0,0.5)", - lineWidth: 1, + new Circle(ICON_REEL_RADIUS, { + fill: TrackLabColors.measuringTapeColorProperty, + stroke: TrackLabColors.iconShadowProperty, + lineWidth: ICON_REEL_OUTLINE_WIDTH, x: x0, y: cy, }), // Inner dot of reel - new Circle(1.5, { fill: "rgba(0,0,0,0.5)", x: x0, y: cy }), + new Circle(ICON_REEL_DOT_RADIUS, { fill: TrackLabColors.iconShadowProperty, x: x0, y: cy }), // Right tip circle - new Circle(2.5, { - fill: color, - stroke: "rgba(0,0,0,0.5)", - lineWidth: 1, + new Circle(ICON_TIP_RADIUS, { + fill: TrackLabColors.measuringTapeColorProperty, + stroke: TrackLabColors.iconShadowProperty, + lineWidth: ICON_REEL_OUTLINE_WIDTH, x: x1, y: cy, }), // Tick marks at left, mid, right - new Line(x0, cy - 4, x0, cy + 4, { stroke: color, lineWidth: 1.5 }), - new Line(xMid, cy - 2.5, xMid, cy + 2.5, { stroke: color, lineWidth: 1.5 }), - new Line(x1, cy - 4, x1, cy + 4, { stroke: color, lineWidth: 1.5 }), + new Line(x0, cy - ICON_TICK_LENGTH, x0, cy + ICON_TICK_LENGTH, { + stroke: TrackLabColors.measuringTapeColorProperty, + lineWidth: ICON_TICK_LINE_WIDTH, + }), + new Line(xMid, cy - ICON_TICK_HALF_LENGTH, xMid, cy + ICON_TICK_HALF_LENGTH, { + stroke: TrackLabColors.measuringTapeColorProperty, + lineWidth: ICON_TICK_LINE_WIDTH, + }), + new Line(x1, cy - ICON_TICK_LENGTH, x1, cy + ICON_TICK_LENGTH, { + stroke: TrackLabColors.measuringTapeColorProperty, + lineWidth: ICON_TICK_LINE_WIDTH, + }), ], }); } @@ -65,33 +91,32 @@ function measuringTapeIcon(): Node { /** Angle tool: two lines meeting at a bottom-left vertex with a small arc * showing the angle between them. */ function angleToolIcon(): Node { - const color = "rgb(170, 100, 255)"; // Vertex at bottom-left, arm1 goes up-right, arm2 goes right const vx = 2; - const vy = ICON_SIZE - 2; - const a1x = ICON_SIZE - 2; + const vy = CONTROL_ICON_SIZE - 2; + const a1x = CONTROL_ICON_SIZE - 2; const a1y = 2; - const a2x = ICON_SIZE - 2; - const a2y = ICON_SIZE - 2; - const arcRadius = 7; + const a2x = CONTROL_ICON_SIZE - 2; + const a2y = CONTROL_ICON_SIZE - 2; const angle1 = Math.atan2(a1y - vy, a1x - vx); const angle2 = Math.atan2(a2y - vy, a2x - vx); - const arcShape = new Shape().arc(vx, vy, arcRadius, angle1, angle2, false); + const arcShape = new Shape().arc(vx, vy, ICON_ARC_RADIUS, angle1, angle2, false); return new Node({ children: [ - new Line(vx, vy, a1x, a1y, { stroke: color, lineWidth: 1.5 }), - new Line(vx, vy, a2x, a2y, { stroke: color, lineWidth: 1.5 }), - new Path(arcShape, { stroke: color, lineWidth: 1.5 }), + new Line(vx, vy, a1x, a1y, { stroke: TrackLabColors.angleToolColorProperty, lineWidth: ICON_ARM_LINE_WIDTH }), + new Line(vx, vy, a2x, a2y, { stroke: TrackLabColors.angleToolColorProperty, lineWidth: ICON_ARM_LINE_WIDTH }), + new Path(arcShape, { stroke: TrackLabColors.angleToolColorProperty, lineWidth: ICON_ARM_LINE_WIDTH }), ], }); } // ── Helper ───────────────────────────────────────────────────────────────────── -function makeRow(icon: Node, property: SimModel["measuringTapeVisibleProperty"]): Checkbox { +function makeRow(icon: Node, property: SimModel["measuringTapeVisibleProperty"], accessibleName: string): Checkbox { return new Checkbox(property, icon, { checkboxColor: TrackLabColors.checkboxColorProperty, checkboxColorBackground: TrackLabColors.checkboxColorBackgroundProperty, + accessibleName, }); } @@ -103,12 +128,18 @@ function makeRow(icon: Node, property: SimModel["measuringTapeVisibleProperty"]) */ export class MeasurementToolsPanel extends Panel { public constructor(model: SimModel) { + const a11yStrings = StringManager.getInstance().getA11y(); + const rows = new VBox({ children: [ - makeRow(measuringTapeIcon(), model.measuringTapeVisibleProperty), - makeRow(angleToolIcon(), model.angleToolVisibleProperty), + makeRow( + measuringTapeIcon(), + model.measuringTapeVisibleProperty, + a11yStrings.toggleMeasuringTapeStringProperty.value, + ), + makeRow(angleToolIcon(), model.angleToolVisibleProperty, a11yStrings.toggleAngleToolStringProperty.value), ], - spacing: PANEL_ROWS_SPACING, + spacing: CONTROL_PANEL_ROWS_SPACING, align: "left", }); @@ -116,8 +147,8 @@ export class MeasurementToolsPanel extends Panel { fill: TrackLabColors.panelFillProperty, stroke: TrackLabColors.panelStrokeProperty, cornerRadius: PANEL_CORNER_RADIUS, - xMargin: PANEL_X_MARGIN, - yMargin: PANEL_Y_MARGIN, + xMargin: CONTROL_PANEL_X_MARGIN, + yMargin: CONTROL_PANEL_Y_MARGIN, }); } } diff --git a/src/screen-name/view/MeasuringTapeNode.ts b/src/screen-name/view/MeasuringTapeNode.ts index 837b3b6..e02a054 100644 --- a/src/screen-name/view/MeasuringTapeNode.ts +++ b/src/screen-name/view/MeasuringTapeNode.ts @@ -15,24 +15,29 @@ import { Circle, Line, Node, RichDragListener, Text } from "scenerystack/scenery import { 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 { + LABEL_PANEL_CORNER_RADIUS, + LABEL_PANEL_SCALE, + LABEL_PANEL_X_MARGIN, + LABEL_PANEL_Y_MARGIN, + OVERLAY_DRAG_SPEED, + OVERLAY_SHIFT_DRAG_SPEED, + OVERLAY_TOUCH_DILATION, +} from "../../TrackLabConstants.js"; import type { SimModel } from "../model/SimModel.js"; // ── Visual constants ────────────────────────────────────────────────────────── -const TAPE_COLOR = "rgb(240, 185, 55)"; -const TAPE_SHADOW_COLOR = "rgba(0, 0, 0, 0.45)"; const TAPE_LINE_WIDTH = 4; const TAPE_SHADOW_WIDTH = 7; const TAPE_DASH: number[] = [10, 5]; - const ENDPOINT_RADIUS = 7; -const TOUCH_DILATION = 12; - +const ENDPOINT_DOT_RADIUS = 2.5; +const ENDPOINT_OUTLINE_WIDTH = 1.5; const FONT = new PhetFont({ size: 12, weight: "bold" }); const LABEL_Y_OFFSET = 16; // pixels above the midpoint - -const DRAG_SPEED = 200; -const SHIFT_DRAG_SPEED = 40; +const DISTANCE_DECIMAL_PLACES = 2; /** * Two-endpoint measuring tape overlay. Both endpoints are independently draggable. @@ -44,28 +49,32 @@ export class MeasuringTapeNode extends Node { public constructor(visibleProperty: TReadOnlyProperty, model: SimModel) { super(); + const a11yStrings = StringManager.getInstance().getA11y(); + // ── Shadow line (contrast on any background) ────────────────────────── const shadowLine = new Line(0, 0, 0, 0, { - stroke: TAPE_SHADOW_COLOR, + stroke: TrackLabColors.measuringTapeShadowProperty, lineWidth: TAPE_SHADOW_WIDTH, }); this.addChild(shadowLine); // ── Main tape line ──────────────────────────────────────────────────── const tapeLine = new Line(0, 0, 0, 0, { - stroke: TAPE_COLOR, + stroke: TrackLabColors.measuringTapeColorProperty, lineWidth: TAPE_LINE_WIDTH, lineDash: TAPE_DASH, }); this.addChild(tapeLine); - const makeEndpoint = (accessibleName: string) => { + const makeEndpoint = (accessibleName: TReadOnlyProperty) => { const endpointCircle = new Circle(ENDPOINT_RADIUS, { - fill: TAPE_COLOR, - stroke: "rgba(0, 0, 0, 0.65)", - lineWidth: 1.5, + fill: TrackLabColors.measuringTapeColorProperty, + stroke: TrackLabColors.overlayHandleOutlineProperty, + lineWidth: ENDPOINT_OUTLINE_WIDTH, + }); + const endpointDot = new Circle(ENDPOINT_DOT_RADIUS, { + fill: TrackLabColors.overlayHandleOutlineProperty, }); - const endpointDot = new Circle(2.5, { fill: "rgba(0, 0, 0, 0.65)" }); const endpointNode = new Node({ children: [endpointCircle, endpointDot], cursor: "grab", @@ -73,31 +82,31 @@ export class MeasuringTapeNode extends Node { focusable: true, accessibleName, }); - const endpointTouchArea = Shape.circle(0, 0, ENDPOINT_RADIUS + TOUCH_DILATION); + const endpointTouchArea = Shape.circle(0, 0, ENDPOINT_RADIUS + OVERLAY_TOUCH_DILATION); endpointNode.mouseArea = endpointTouchArea; endpointNode.touchArea = endpointTouchArea; return endpointNode; }; // ── Symmetric endpoints ──────────────────────────────────────────────── - const endpoint1Node = makeEndpoint("Measuring tape base"); - const endpoint2Node = makeEndpoint("Measuring tape tip"); + const endpoint1Node = makeEndpoint(a11yStrings.measuringTapeBaseStringProperty); + const endpoint2Node = makeEndpoint(a11yStrings.measuringTapeTipStringProperty); this.addChild(endpoint1Node); this.addChild(endpoint2Node); // ── Distance label ──────────────────────────────────────────────────── - const distanceText = new Text("---", { + const distanceText = new Text("", { font: FONT, fill: TrackLabColors.textOnDarkProperty, }); const labelPanel = new Panel(distanceText, { fill: TrackLabColors.panelFillProperty, stroke: TrackLabColors.panelStrokeProperty, - cornerRadius: 4, - xMargin: 6, - yMargin: 3, + cornerRadius: LABEL_PANEL_CORNER_RADIUS, + xMargin: LABEL_PANEL_X_MARGIN, + yMargin: LABEL_PANEL_Y_MARGIN, }); - labelPanel.setScaleMagnitude(0.8); + labelPanel.setScaleMagnitude(LABEL_PANEL_SCALE); this.addChild(labelPanel); // ── Geometry + label multilinks ─────────────────────────────────────── @@ -116,7 +125,7 @@ export class MeasuringTapeNode extends Node { (p1, p2, mvt, unit) => { const m1 = mvt.inversePosition2(p1); const m2 = mvt.inversePosition2(p2); - distanceText.string = `${m1.distance(m2).toFixed(2)} ${unit}`; + distanceText.string = `${m1.distance(m2).toFixed(DISTANCE_DECIMAL_PLACES)} ${unit}`; }, ); @@ -124,14 +133,14 @@ export class MeasuringTapeNode extends Node { endpoint1Node.addInputListener( new RichDragListener({ positionProperty: model.tapPoint1Property, - keyboardDragListenerOptions: { dragSpeed: DRAG_SPEED, shiftDragSpeed: SHIFT_DRAG_SPEED }, + keyboardDragListenerOptions: { dragSpeed: OVERLAY_DRAG_SPEED, shiftDragSpeed: OVERLAY_SHIFT_DRAG_SPEED }, tandem: Tandem.OPT_OUT, }), ); endpoint2Node.addInputListener( new RichDragListener({ positionProperty: model.tapPoint2Property, - keyboardDragListenerOptions: { dragSpeed: DRAG_SPEED, shiftDragSpeed: SHIFT_DRAG_SPEED }, + keyboardDragListenerOptions: { dragSpeed: OVERLAY_DRAG_SPEED, shiftDragSpeed: OVERLAY_SHIFT_DRAG_SPEED }, tandem: Tandem.OPT_OUT, }), ); From f80384e2f3e9043d5568194dcd8180b96cf2826b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Feb 2026 02:07:18 +0000 Subject: [PATCH 2/2] Chore: minor lockfile and formatting cleanup - Remove stale peer flags in package-lock.json - Fix trailing whitespace and comma in bouncingBallToSVG.ts https://claude.ai/code/session_01QY96Kv94K89iT4fHoUbSLi --- package-lock.json | 8 -------- scripts/bouncingBallToSVG.ts | 4 ++-- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6f25fd6..00387fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,7 +73,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -3247,7 +3246,6 @@ "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -3285,7 +3283,6 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3510,7 +3507,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6153,7 +6149,6 @@ "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -6209,7 +6204,6 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -6470,7 +6464,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -6848,7 +6841,6 @@ "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "rollup": "dist/bin/rollup" }, diff --git a/scripts/bouncingBallToSVG.ts b/scripts/bouncingBallToSVG.ts index 7ca29b0..7615f51 100644 --- a/scripts/bouncingBallToSVG.ts +++ b/scripts/bouncingBallToSVG.ts @@ -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. @@ -108,7 +108,7 @@ function buildSvg(snapshots: Point[]): string { (p) => ` ` + `opacity="${BALL_OPACITY}"/>`, ) .join("\n");