diff --git a/index.html b/index.html index fc7d181..d8e7e68 100644 --- a/index.html +++ b/index.html @@ -4,8 +4,22 @@ G-Code Overlay + +
+ CNC camera stream + +
+ diff --git a/src/app/layout.test.ts b/src/app/layout.test.ts new file mode 100644 index 0000000..2c61c7f --- /dev/null +++ b/src/app/layout.test.ts @@ -0,0 +1,18 @@ +import { describe, it, expect } from 'vitest'; +import { computeOverlayRect } from './layout'; + +describe('computeOverlayRect', () => { + it('matches the displayed video box (letterboxed: wide video in tall box)', () => { + // video is 16:9 (1600×900 native) shown in a 800×800 box → fits to width, 450 tall, centred + const r = computeOverlayRect(1600, 900, 800, 800); + expect(r.width).toBeCloseTo(800, 3); + expect(r.height).toBeCloseTo(450, 3); + expect(r.left).toBeCloseTo(0, 3); + expect(r.top).toBeCloseTo(175, 3); + }); + + it('falls back to the container when the video has no intrinsic size yet', () => { + const r = computeOverlayRect(0, 0, 640, 480); + expect(r).toEqual({ left: 0, top: 0, width: 640, height: 480 }); + }); +}); diff --git a/src/app/layout.ts b/src/app/layout.ts new file mode 100644 index 0000000..c0c6b1b --- /dev/null +++ b/src/app/layout.ts @@ -0,0 +1,40 @@ +export interface Rect { + left: number; + top: number; + width: number; + height: number; +} + +/** + * Compute the on-screen rectangle of a media element shown with object-fit: contain + * inside a container. Returns container bounds if the media size is unknown. + */ +export function computeOverlayRect( + mediaW: number, + mediaH: number, + containerW: number, + containerH: number, +): Rect { + if (mediaW <= 0 || mediaH <= 0) { + return { left: 0, top: 0, width: containerW, height: containerH }; + } + const scale = Math.min(containerW / mediaW, containerH / mediaH); + const width = mediaW * scale; + const height = mediaH * scale; + return { + left: (containerW - width) / 2, + top: (containerH - height) / 2, + width, + height, + }; +} + +/** Position and size a canvas to overlay a media element inside a container. */ +export function syncOverlay(canvas: HTMLCanvasElement, rect: Rect): void { + canvas.style.left = `${rect.left}px`; + canvas.style.top = `${rect.top}px`; + canvas.style.width = `${rect.width}px`; + canvas.style.height = `${rect.height}px`; + canvas.width = Math.round(rect.width); + canvas.height = Math.round(rect.height); +} diff --git a/src/main.ts b/src/main.ts index 492599b..428548a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1 +1,22 @@ -console.log('gcode-overlay booting'); +import './styles.css'; +import { computeOverlayRect, syncOverlay } from './app/layout'; + +const stage = document.getElementById('stage') as HTMLDivElement; +const stream = document.getElementById('stream') as HTMLImageElement; +const overlay = document.getElementById('overlay') as HTMLCanvasElement; + +function resize(): void { + const rect = computeOverlayRect( + stream.naturalWidth, + stream.naturalHeight, + stage.clientWidth, + stage.clientHeight, + ); + syncOverlay(overlay, rect); +} + +window.addEventListener('resize', resize); +stream.addEventListener('load', resize); +resize(); + +console.log('layout ready'); diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..672d4e6 --- /dev/null +++ b/src/styles.css @@ -0,0 +1,14 @@ +* { box-sizing: border-box; } +body { margin: 0; display: flex; height: 100vh; font-family: system-ui, sans-serif; background: #111; color: #eee; } +#stage { position: relative; flex: 1; overflow: hidden; background: #000; } +#stream { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; } +#overlay { position: absolute; pointer-events: none; } +#overlay.interactive { pointer-events: auto; cursor: crosshair; } +#panel { width: 320px; padding: 16px; overflow-y: auto; background: #1b1b1b; } +#panel h1 { font-size: 16px; } +.filebtn { display: inline-block; padding: 8px 12px; background: #2962ff; border-radius: 4px; cursor: pointer; } +button { padding: 6px 10px; margin: 2px 0; background: #333; color: #eee; border: 1px solid #555; border-radius: 4px; cursor: pointer; } +input[type="number"] { width: 80px; background: #222; color: #eee; border: 1px solid #555; border-radius: 4px; padding: 4px; } +section { margin-top: 16px; border-top: 1px solid #333; padding-top: 12px; } +.row { display: flex; align-items: center; gap: 6px; margin: 4px 0; } +small { color: #999; }