+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/composables/useImageScene.js b/app/composables/useImageScene.js
new file mode 100644
index 0000000..e4a6187
--- /dev/null
+++ b/app/composables/useImageScene.js
@@ -0,0 +1,327 @@
+import { ref, shallowRef, watch, onBeforeUnmount } from 'vue'
+import { registerSharedScene, unregisterSharedScene, getTHREE } from './useSharedRenderer'
+
+// Kept as module-level constants — previously read off an OrbitControls
+// instance that was created solely to store these two numbers and never
+// actually enabled/updated. With a shared renderer, per-scene OrbitControls
+// don't make sense anyway (see note in buildScene), so these are just
+// plain constants now.
+const MIN_DISTANCE = 1.2
+const MAX_DISTANCE = 4
+
+/**
+ * useImageScene
+ *
+ * Encapsulates a tilted, slowly-breathing Three.js "image plane" scene so
+ * it can be reused across components without duplicating Three.js
+ * setup/teardown logic.
+ *
+ * Designed to be called once PER image — e.g. from inside a v-for over a
+ * list of screenshots. Each call gets its own Scene/camera/mesh/texture,
+ * but rendering itself is done by a single shared WebGLRenderer (see
+ * useSharedRenderer.js), per the Three.js manual's "multiple scenes, one
+ * renderer" pattern (https://threejs.org/manual/#en/multiple-scenes).
+ * That renderer draws onto one full-viewport canvas positioned over the
+ * page; each instance's `containerRef` element just defines the
+ * on-screen rectangle its scene gets rendered into (via scissor/viewport),
+ * so there's nothing shared between instances except that renderer, the
+ * ticker's rAF loop, and the browser's single WebGL context.
+ *
+ * Nuxt note: Three.js touches `window`/`document`/WebGL, none of which
+ * exist during SSR. By default this composable initializes itself lazily
+ * (see `lazy` option below) once its containerRef is mounted and near the
+ * viewport, so you generally do NOT need to call init() yourself. If you
+ * set `lazy: false`, call init() from a client-only lifecycle hook.
+ *
+ * @param {object} options
+ * @param {import('vue').Ref|string} options.src - image URL (can be a ref for reactivity)
+ * @param {number} [options.tiltX]
+ * @param {number} [options.tiltY]
+ * @param {number} [options.baseDistance]
+ * @param {number} [options.zoomSpeed]
+ * @param {boolean} [options.lazy=true] - don't register with the shared
+ * renderer until the container scrolls near the viewport. Still worth
+ * doing even with a shared renderer/context: it avoids doing texture
+ * loads and per-frame work for scenes nobody is looking at yet.
+ * @param {boolean} [options.disposeOnExit=false] - fully release this
+ * scene's GPU-side resources (mesh geometry/material, texture) and
+ * unregister from the shared render loop when the container scrolls far
+ * out of view, rebuilding on re-entry. Note this no longer tears down a
+ * WebGL *context* (there's only ever one, shared globally) — it frees
+ * this instance's textures/geometry, which is still worthwhile on pages
+ * with many large images. If false, the scene is just paused (resources
+ * stay alive, rendering stops) while off-screen.
+ * @param {string} [options.rootMargin='200px'] - IntersectionObserver
+ * rootMargin, controls how far ahead of scroll position scenes preload.
+ */
+export function useImageScene(options = {}) {
+ const {
+ src,
+ tiltX = 0.35,
+ tiltY = -0.22,
+ baseDistance = 3.2,
+ zoomSpeed = 0.4,
+ lazy = true,
+ disposeOnExit = false,
+ rootMargin = '200px',
+ } = options
+
+ const containerRef = ref(null)
+ // Vestigial: kept only so existing templates that render
+ // `