From 34836d6126032248ae69a8ef3f4a98599e7d0463 Mon Sep 17 00:00:00 2001 From: Lucas Garron Date: Mon, 20 Jan 2025 13:13:44 -0800 Subject: [PATCH] [twisty] Use `IntersectionObserver` to avoid rendering players until they're on the screen. Since we set `contain: size` on the `.wrapper` inside the player, this should not affect page layout. Since the `TwistyProp` hierarchy can be initialized and updated independent of the DOM, this means that you can even call `.play()` on a player before it starts rendering, and the rendering will smoothly pick up fromq the middle of the animation as soon as the player enters the viewport. (Note that there is no way to enforce any of this, though, and it is entirely possible for future code to make bad assumptions that breaks this. A browser test would be very useful for this.) --- src/cubing/twisty/views/TwistyPlayer.ts | 30 ++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/cubing/twisty/views/TwistyPlayer.ts b/src/cubing/twisty/views/TwistyPlayer.ts index 0b0dee739..58347a416 100644 --- a/src/cubing/twisty/views/TwistyPlayer.ts +++ b/src/cubing/twisty/views/TwistyPlayer.ts @@ -16,14 +16,14 @@ import type { SetupToLocation } from "../model/props/puzzle/state/SetupAnchorPro import type { PuzzleID } from "../model/props/puzzle/structure/PuzzleIDRequestProp"; import type { BackgroundThemeWithAuto } from "../model/props/viewer/BackgroundProp"; import type { BackViewLayoutWithAuto } from "../model/props/viewer/BackViewProp"; -import { - type ControlPanelThemeWithAuto, - controlsLocations, -} from "../model/props/viewer/ControlPanelProp"; import type { ColorScheme, ColorSchemeWithAuto, } from "../model/props/viewer/ColorSchemeRequestProp"; +import { + type ControlPanelThemeWithAuto, + controlsLocations, +} from "../model/props/viewer/ControlPanelProp"; import type { ViewerLinkPageWithAuto } from "../model/props/viewer/ViewerLinkProp"; import type { VisualizationFormatWithAuto } from "../model/props/viewer/VisualizationProp"; import type { VisualizationStrategy } from "../model/props/viewer/VisualizationStrategyProp"; @@ -169,6 +169,22 @@ const propOnly: Record = { experimentalMovePressCancelOptions: true, }; +let cachedSharedIntersectionObserver: IntersectionObserver | undefined; +const intersectedCallback = Symbol("intersectedCallback"); +function waitForIntersection(player: TwistyPlayer) { + cachedSharedIntersectionObserver ??= new IntersectionObserver( + (entries, observer) => { + for (const entry of entries) { + if (entry.isIntersecting && entry.intersectionRect.height > 0) { + (entry.target as TwistyPlayer)[intersectedCallback](); + observer.unobserve(entry.target); + } + } + }, + ); + cachedSharedIntersectionObserver.observe(player); +} + /** * TwistyPlayer is the heart of `cubing.js`. It can be used to display a puzzle on a web page like this: * @@ -232,11 +248,15 @@ export class TwistyPlayer #errorElem = document.createElement("div"); // TODO: Better pattern. #alreadyConnected = false; // TODO: support resetting async connectedCallback(): Promise { + this.addCSS(twistyPlayerCSS); + waitForIntersection(this); + } + + async [intersectedCallback](): Promise { if (this.#alreadyConnected) { return; } this.#alreadyConnected = true; - this.addCSS(twistyPlayerCSS); this.addElement(this.#visualizationWrapperElem).classList.add( "visualization-wrapper",