From 5d6afdac97e6138d30da8f3c7e0f0d5de437afde Mon Sep 17 00:00:00 2001 From: type-two Date: Sat, 18 Jul 2026 14:12:18 +1000 Subject: [PATCH] [ui] THE SCREEN click-to-enlarge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Click the sky, get the sky: ~2.5x, centred, over a dimmed backdrop. Esc or a click closes it. Enlarges the real element rather than mirroring it. I built the mirror first and it rendered black — THE SCREEN is a WebGL canvas, and drawImage from a WebGL context without preserveDrawingBuffer returns nothing. Asking LANE-SCREEN to enable that flag would cost them a buffer copy every frame for a feature used occasionally. Since that means touching another lane's element, it snapshots the style attribute on open and restores it verbatim on close — including the case where SCREEN later adds inline styles of their own, which is tested. Co-Authored-By: Claude Opus 4.8 --- fktry/src/ui/screenview.test.ts | 80 ++++++++++++++++++++++++++++++++ fktry/src/ui/screenview.ts | 82 +++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 fktry/src/ui/screenview.test.ts create mode 100644 fktry/src/ui/screenview.ts diff --git a/fktry/src/ui/screenview.test.ts b/fktry/src/ui/screenview.test.ts new file mode 100644 index 0000000..250f1c5 --- /dev/null +++ b/fktry/src/ui/screenview.test.ts @@ -0,0 +1,80 @@ +// @vitest-environment happy-dom +import { describe, expect, it } from 'vitest'; +import { createScreenView } from './screenview'; + +function mount(withInlineStyle = false) { + document.body.innerHTML = withInlineStyle + ? '' + : ''; + const source = document.getElementById('screen') as HTMLCanvasElement; + const view = createScreenView(source); + document.body.append(view.el); + return { view, source }; +} + +const click = (el: Element) => el.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true })); + +describe('THE SCREEN click-to-enlarge', () => { + it('starts closed and leaves THE SCREEN alone', () => { + const { view, source } = mount(); + expect(view.isOpen()).toBe(false); + expect(source.getAttribute('style')).toBeNull(); + }); + + it('enlarges THE SCREEN and dims the backdrop when clicked', () => { + const { view, source } = mount(); + click(source); + expect(view.isOpen()).toBe(true); + expect(view.el.classList.contains('is-open')).toBe(true); + const style = source.getAttribute('style')!; + expect(style).toContain('position:fixed'); + expect(style).toContain('z-index:41'); // above the backdrop + }); + + it('restores THE SCREEN exactly as it found it', () => { + // Another lane owns this element; we must hand it back untouched. + const { view, source } = mount(); + click(source); + view.close(); + expect(source.getAttribute('style')).toBeNull(); + expect(view.isOpen()).toBe(false); + }); + + it('preserves inline styles LANE-SCREEN may add later', () => { + const { view, source } = mount(true); + const before = source.getAttribute('style'); + click(source); + expect(source.getAttribute('style')).toContain('top:10px'); // ours appended, not replacing + view.close(); + expect(source.getAttribute('style')).toBe(before); + }); + + it('closes when the enlarged SCREEN is clicked again', () => { + const { view, source } = mount(); + click(source); + click(source); + expect(view.isOpen()).toBe(false); + expect(source.getAttribute('style')).toBeNull(); + }); + + it('closes on a click on the dimmed backdrop', () => { + const { view, source } = mount(); + click(source); + click(view.el); + expect(view.isOpen()).toBe(false); + }); + + it('close() is idempotent and does not clobber the style on a second call', () => { + const { view, source } = mount(true); + const before = source.getAttribute('style'); + view.close(); + view.close(); + expect(source.getAttribute('style')).toBe(before); + }); + + it('survives having no SCREEN to enlarge', () => { + document.body.innerHTML = ''; + const view = createScreenView(null); + expect(() => { view.toggle(); view.close(); }).not.toThrow(); + }); +}); diff --git a/fktry/src/ui/screenview.ts b/fktry/src/ui/screenview.ts new file mode 100644 index 0000000..b9ab498 --- /dev/null +++ b/fktry/src/ui/screenview.ts @@ -0,0 +1,82 @@ +/** + * LANE-UI — THE SCREEN click-to-enlarge (order 8). The sky is the score; let people look. + * + * Clicking THE SCREEN toggles a centered ~2.5× view over a dimmed backdrop. Esc or a + * click closes it. + * + * We enlarge the real `#screen` element with inline styles rather than mirroring it into + * our own canvas: THE SCREEN is a WebGL canvas, and `drawImage` from a WebGL context + * without `preserveDrawingBuffer` returns black (verified — the mirror rendered empty). + * Asking LANE-SCREEN to enable that flag would cost them a frame buffer copy every frame + * for a feature used occasionally, so the transform is both cheaper and what the order + * specified. + * + * Touching another lane's element is done carefully: we snapshot its `style` attribute on + * open and restore it verbatim on close, so THE SCREEN's own positioning stays SCREEN's + * to change. + */ +import { cls, el } from './dom'; + +export interface ScreenView { + /** The dimming backdrop, mounted in #ui. */ + el: HTMLElement; + toggle(): void; + close(): void; + isOpen(): boolean; +} + +/** Inline styles applied to #screen while enlarged. z-index clears the backdrop (40). */ +const ENLARGED = [ + 'position:fixed', + 'top:50%', + 'left:50%', + 'transform:translate(-50%,-50%)', + 'width:min(800px, calc(100vw - 80px))', + 'height:auto', + 'z-index:41', + 'cursor:zoom-out', + 'image-rendering:pixelated', // it's a dying video file — let the blocks show +].join(';'); + +export function createScreenView(source: HTMLElement | null): ScreenView { + const backdrop = el('div'); + backdrop.id = 'fk-screenview'; + + let open = false; + /** The element's own style attribute before we touched it. */ + let savedStyle: string | null = null; + + function apply() { + if (!source) return; + savedStyle = source.getAttribute('style'); + source.setAttribute('style', `${savedStyle ? savedStyle + ';' : ''}${ENLARGED}`); + } + + function restore() { + if (!source) return; + if (savedStyle === null) source.removeAttribute('style'); + else source.setAttribute('style', savedStyle); + savedStyle = null; + } + + function close() { + if (!open) return; + open = false; + cls(backdrop, 'is-open', false); + restore(); + } + + function toggle() { + if (open) return close(); + open = true; + cls(backdrop, 'is-open', true); + apply(); + } + + // A click on the dimmed backdrop closes. So does a click on the enlarged SCREEN, which + // sits above the backdrop — that's the same toggle listener below. + backdrop.addEventListener('pointerdown', close); + source?.addEventListener('pointerdown', toggle); + + return { el: backdrop, toggle, close, isOpen: () => open }; +}