Fixes in views
This commit is contained in:
@@ -26,6 +26,13 @@
|
||||
--cam-w: 549px;
|
||||
--cam-h: 309px;
|
||||
--cam-pad: 10px;
|
||||
/* Screen-capture frame transform — auto-synced from the Screen Capture
|
||||
item the same way. Defaults match the current scene's bounds. */
|
||||
--scr-x: 10px;
|
||||
--scr-y: 14px;
|
||||
--scr-w: 1976px;
|
||||
--scr-h: 1209px;
|
||||
--scr-pad: 0px;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
@@ -92,17 +99,38 @@
|
||||
0 0 28px rgba(79, 210, 255, 0.20),
|
||||
inset 0 0 60px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
.camera-frame .tick {
|
||||
|
||||
/* ───────── Screen-capture frame ─────────
|
||||
Same pattern as the camera frame — corner ticks + label + tracked size —
|
||||
but transparent (the desktop renders behind, must show through) and a
|
||||
thinner perimeter line, since at full-canvas scale a bright cyan rectangle
|
||||
all around the desktop reads as too loud. Bright ticks carry the "this is
|
||||
a framed feed" signal; the line itself is just a hint. */
|
||||
.screen-frame {
|
||||
position: absolute;
|
||||
top: calc(var(--scr-y) - var(--scr-pad));
|
||||
left: calc(var(--scr-x) - var(--scr-pad));
|
||||
width: calc(var(--scr-w) + var(--scr-pad) * 2);
|
||||
height: calc(var(--scr-h) + var(--scr-pad) * 2);
|
||||
pointer-events: none;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(79, 210, 255, 0.30);
|
||||
box-shadow: 0 0 18px rgba(79, 210, 255, 0.08);
|
||||
}
|
||||
|
||||
.camera-frame .tick,
|
||||
.screen-frame .tick {
|
||||
position: absolute;
|
||||
width: 24px; height: 24px;
|
||||
border: 3px solid var(--hud-edge);
|
||||
box-shadow: 0 0 8px var(--hud-glow);
|
||||
}
|
||||
.camera-frame .tick.tl { top: -2px; left: -2px; border-right: none; border-bottom: none; }
|
||||
.camera-frame .tick.tr { top: -2px; right: -2px; border-left: none; border-bottom: none; }
|
||||
.camera-frame .tick.bl { bottom: -2px; left: -2px; border-right: none; border-top: none; }
|
||||
.camera-frame .tick.br { bottom: -2px; right: -2px; border-left: none; border-top: none; }
|
||||
.camera-frame .label {
|
||||
.camera-frame .tick.tl, .screen-frame .tick.tl { top: -2px; left: -2px; border-right: none; border-bottom: none; }
|
||||
.camera-frame .tick.tr, .screen-frame .tick.tr { top: -2px; right: -2px; border-left: none; border-bottom: none; }
|
||||
.camera-frame .tick.bl, .screen-frame .tick.bl { bottom: -2px; left: -2px; border-right: none; border-top: none; }
|
||||
.camera-frame .tick.br, .screen-frame .tick.br { bottom: -2px; right: -2px; border-left: none; border-top: none; }
|
||||
.camera-frame .label,
|
||||
.screen-frame .label {
|
||||
position: absolute;
|
||||
top: 10px; left: 14px;
|
||||
font-family: var(--mono);
|
||||
@@ -133,7 +161,8 @@
|
||||
letter-spacing: 0.20em;
|
||||
color: var(--hud-dim);
|
||||
}
|
||||
.camera-frame.off { display: none; }
|
||||
.camera-frame.off,
|
||||
.screen-frame.off { display: none; }
|
||||
|
||||
/* ───────── Bottom status bar ───────── */
|
||||
.status-bar {
|
||||
@@ -203,6 +232,14 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="screen-frame" id="scr">
|
||||
<span class="tick tl"></span>
|
||||
<span class="tick tr"></span>
|
||||
<span class="tick bl"></span>
|
||||
<span class="tick br"></span>
|
||||
<span class="label">SCR 01</span>
|
||||
</div>
|
||||
|
||||
<div class="camera-frame" id="cam">
|
||||
<span class="tick tl"></span>
|
||||
<span class="tick tr"></span>
|
||||
@@ -273,53 +310,68 @@
|
||||
}
|
||||
tickElapsed(); setInterval(tickElapsed, 1000);
|
||||
|
||||
// ── OBS WS: live camera frame sync ──
|
||||
// URL params let you rename the camera source / scene without editing JS:
|
||||
// ?camera=Webcam&scene=Desktop
|
||||
// ── OBS WS: live frame sync for camera + screen capture ──
|
||||
// URL params let you rename the sources / scene without editing JS:
|
||||
// ?camera=Webcam&screen=Display&scene=Desktop
|
||||
const params = new URLSearchParams(location.search);
|
||||
const CAM_SOURCE = params.get('camera') || 'Camera';
|
||||
const CAM_SCENE = params.get('scene') || 'Desktop';
|
||||
const SCR_SOURCE = params.get('screen') || 'Screen Capture (PipeWire)';
|
||||
const SCENE = params.get('scene') || 'Desktop';
|
||||
|
||||
const camEl = document.getElementById('cam');
|
||||
const camEl = document.getElementById('cam');
|
||||
const scrEl = document.getElementById('scr');
|
||||
const rootStyle = document.documentElement.style;
|
||||
function setCameraVisible(visible) { camEl.classList.toggle('off', !visible); }
|
||||
// Map an OBS sceneItemTransform into the four CSS vars driving the frame.
|
||||
// Assumes alignment 5 (top-left) — OBS's default for newly added items.
|
||||
function setCameraTransform(t) {
|
||||
if (!t) return;
|
||||
const w = (t.sourceWidth ?? 0) * (t.scaleX ?? 1);
|
||||
const h = (t.sourceHeight ?? 0) * (t.scaleY ?? 1);
|
||||
if (!(w > 0 && h > 0)) return;
|
||||
rootStyle.setProperty('--cam-x', `${t.positionX}px`);
|
||||
rootStyle.setProperty('--cam-y', `${t.positionY}px`);
|
||||
rootStyle.setProperty('--cam-w', `${w}px`);
|
||||
rootStyle.setProperty('--cam-h', `${h}px`);
|
||||
|
||||
// Compute rendered (w, h) on canvas from an OBS sceneItemTransform.
|
||||
// Camera item has no bounds → scale × source. Screen capture uses
|
||||
// bounds_type=stretch (1) → boundsWidth/boundsHeight are authoritative.
|
||||
function renderedSize(t) {
|
||||
const useBounds = t.boundsType
|
||||
&& t.boundsType !== 'OBS_BOUNDS_NONE'
|
||||
&& (t.boundsWidth ?? 0) > 0;
|
||||
if (useBounds) return { w: t.boundsWidth, h: t.boundsHeight };
|
||||
return {
|
||||
w: (t.sourceWidth ?? 0) * (t.scaleX ?? 1),
|
||||
h: (t.sourceHeight ?? 0) * (t.scaleY ?? 1),
|
||||
};
|
||||
}
|
||||
|
||||
async function syncCamera(obs) {
|
||||
let camItemId = null;
|
||||
// Bind a frame element to a scene item: live-sync size/position into the
|
||||
// --{prefix}-x/y/w/h CSS vars and toggle .off on enable changes.
|
||||
// Assumes alignment 5 (top-left) — OBS's default for newly added items.
|
||||
async function syncSourceFrame(obs, { sceneName, sourceName, frameEl, varPrefix }) {
|
||||
const setVisible = (v) => frameEl.classList.toggle('off', !v);
|
||||
const setTransform = (t) => {
|
||||
if (!t) return;
|
||||
const { w, h } = renderedSize(t);
|
||||
if (!(w > 0 && h > 0)) return;
|
||||
rootStyle.setProperty(`--${varPrefix}-x`, `${t.positionX}px`);
|
||||
rootStyle.setProperty(`--${varPrefix}-y`, `${t.positionY}px`);
|
||||
rootStyle.setProperty(`--${varPrefix}-w`, `${w}px`);
|
||||
rootStyle.setProperty(`--${varPrefix}-h`, `${h}px`);
|
||||
};
|
||||
let itemId = null;
|
||||
try {
|
||||
const r = await obs.call('GetSceneItemId',
|
||||
{ sceneName: CAM_SCENE, sourceName: CAM_SOURCE });
|
||||
camItemId = r.sceneItemId;
|
||||
const r = await obs.call('GetSceneItemId', { sceneName, sourceName });
|
||||
itemId = r.sceneItemId;
|
||||
const e = await obs.call('GetSceneItemEnabled',
|
||||
{ sceneName: CAM_SCENE, sceneItemId: camItemId });
|
||||
setCameraVisible(e.sceneItemEnabled);
|
||||
{ sceneName, sceneItemId: itemId });
|
||||
setVisible(e.sceneItemEnabled);
|
||||
const t = await obs.call('GetSceneItemTransform',
|
||||
{ sceneName: CAM_SCENE, sceneItemId: camItemId });
|
||||
setCameraTransform(t.sceneItemTransform);
|
||||
{ sceneName, sceneItemId: itemId });
|
||||
setTransform(t.sceneItemTransform);
|
||||
} catch (err) {
|
||||
console.warn(`[desktop] camera sync init failed (${CAM_SCENE}/${CAM_SOURCE}):`, err.message);
|
||||
console.warn(`[desktop] frame sync failed (${sceneName}/${sourceName}):`, err.message);
|
||||
return;
|
||||
}
|
||||
obs.addEventListener('event', (ev) => {
|
||||
const d = ev.detail || {};
|
||||
if (d.eventData?.sceneName !== CAM_SCENE
|
||||
|| d.eventData?.sceneItemId !== camItemId) return;
|
||||
if (d.eventData?.sceneName !== sceneName
|
||||
|| d.eventData?.sceneItemId !== itemId) return;
|
||||
if (d.eventType === 'SceneItemEnableStateChanged') {
|
||||
setCameraVisible(d.eventData.sceneItemEnabled);
|
||||
setVisible(d.eventData.sceneItemEnabled);
|
||||
} else if (d.eventType === 'SceneItemTransformChanged') {
|
||||
setCameraTransform(d.eventData.sceneItemTransform);
|
||||
setTransform(d.eventData.sceneItemTransform);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -332,7 +384,11 @@
|
||||
obs.addEventListener('close', () => {
|
||||
setTimeout(connectOBS, 2500);
|
||||
});
|
||||
await syncCamera(obs);
|
||||
// Run both syncs in parallel — independent items, independent failures.
|
||||
await Promise.all([
|
||||
syncSourceFrame(obs, { sceneName: SCENE, sourceName: CAM_SOURCE, frameEl: camEl, varPrefix: 'cam' }),
|
||||
syncSourceFrame(obs, { sceneName: SCENE, sourceName: SCR_SOURCE, frameEl: scrEl, varPrefix: 'scr' }),
|
||||
]);
|
||||
} catch (e) {
|
||||
// OBS WS optional — silently retry; defaults stand meanwhile.
|
||||
setTimeout(connectOBS, 2500);
|
||||
|
||||
Reference in New Issue
Block a user