feat: page layout and overlay canvas sizing
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
96221a217d
commit
dd409f56c0
5 changed files with 108 additions and 1 deletions
14
index.html
14
index.html
|
|
@ -4,8 +4,22 @@
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>G-Code Overlay</title>
|
<title>G-Code Overlay</title>
|
||||||
|
<link rel="stylesheet" href="/src/styles.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div id="stage">
|
||||||
|
<img id="stream" alt="CNC camera stream" />
|
||||||
|
<canvas id="overlay"></canvas>
|
||||||
|
</div>
|
||||||
|
<aside id="panel">
|
||||||
|
<h1>G-Code Overlay</h1>
|
||||||
|
<section>
|
||||||
|
<label class="filebtn">Open G-code<input id="gcode-file" type="file" accept=".nc,.gcode,.tap,.txt,.ngc" hidden /></label>
|
||||||
|
<p id="status">No file loaded.</p>
|
||||||
|
</section>
|
||||||
|
<section id="align-panel"></section>
|
||||||
|
<section id="calib-panel"></section>
|
||||||
|
</aside>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
18
src/app/layout.test.ts
Normal file
18
src/app/layout.test.ts
Normal file
|
|
@ -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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
40
src/app/layout.ts
Normal file
40
src/app/layout.ts
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
23
src/main.ts
23
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');
|
||||||
|
|
|
||||||
14
src/styles.css
Normal file
14
src/styles.css
Normal file
|
|
@ -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; }
|
||||||
Loading…
Add table
Reference in a new issue