diff --git a/src/ui/VoiceStage.svelte b/src/ui/VoiceStage.svelte index 500c801..a515202 100644 --- a/src/ui/VoiceStage.svelte +++ b/src/ui/VoiceStage.svelte @@ -411,8 +411,9 @@ .tile:hover { background: var(--bg-hover); } .tile.talking { border-color: var(--speaking); box-shadow: 0 0 0 1px var(--speaking), 0 0 18px -4px var(--speaking); } .tile.focused { border-color: var(--accent); } - /* A camera fills its tile; your own is mirrored, the way a mirror shows you */ - .tile video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; background: #000; } + /* A camera keeps its own shape inside the tile, whatever size the tile is: nothing is cut + off, a phone held upright gets bars at the sides. Your own is mirrored, like a mirror. */ + .tile video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; background: #000; } .tile video.mirror { transform: scaleX(-1); } .icon-btn.live, .icon-btn.live:hover { color: var(--speaking); } .camgrid { position: relative; display: flex; flex-direction: column; flex: none; } diff --git a/test/e2e/share.e2e.ts b/test/e2e/share.e2e.ts index afce016..6be9c46 100644 --- a/test/e2e/share.e2e.ts +++ b/test/e2e/share.e2e.ts @@ -294,6 +294,18 @@ try { await camsShown(bob.page, 2); await camsShown(alice.page, 2); assert.equal(await bob.page.locator('.stage .tile.self video.mirror').count(), 1, 'own camera is mirrored'); + // The picture keeps its shape in the tile at any tile size: whole, not cropped + const shape = () => bob.page.evaluate(() => [...document.querySelectorAll('.stage .tile video')].map(v => { + const el = v as HTMLVideoElement, r = el.getBoundingClientRect(); + const scale = Math.min(r.width / el.videoWidth, r.height / el.videoHeight); + return { fit: getComputedStyle(el).objectFit, inside: el.videoWidth * scale <= r.width + 1 && el.videoHeight * scale <= r.height + 1, box: r.width > 50 && r.height > 50 }; + })); + for (const size of [{ width: 1280, height: 800 }, { width: 760, height: 620 }]) { + await bob.page.setViewportSize(size); + await camsShown(bob.page, 2); + assert.ok((await shape()).every(s => s.fit === 'contain' && s.inside && s.box), `cameras whole at ${size.width} wide`); + } + await bob.page.setViewportSize({ width: 1280, height: 800 }); assert.equal(await bob.page.locator('.stage .tile:not(.self) video.mirror').count(), 0); await alice.page.locator('.stage .status', { hasText: '1 watching' }).waitFor(); await bob.page.locator('.stage .status', { hasText: '1 watching' }).waitFor();