@scenography/react 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +7 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +7 -7
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -98,13 +98,12 @@ function LoadingScreen({ gallery, title, label = "Cargando…", enterLabel = "En
|
|
|
98
98
|
(0, react.useEffect)(() => () => {
|
|
99
99
|
if (timer.current !== null) window.clearTimeout(timer.current);
|
|
100
100
|
}, []);
|
|
101
|
-
const enter = (0, react.useCallback)(
|
|
101
|
+
const enter = (0, react.useCallback)(() => {
|
|
102
102
|
if (entered) return;
|
|
103
|
-
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
103
|
+
if (window.matchMedia("(pointer: coarse)").matches) try {
|
|
104
|
+
const fullscreen = document.documentElement.requestFullscreen?.();
|
|
105
|
+
const screenOrientation = screen.orientation;
|
|
106
|
+
if (fullscreen) fullscreen.then(() => screenOrientation?.lock?.("landscape")).catch(() => void 0);
|
|
108
107
|
} catch {}
|
|
109
108
|
onEnter?.();
|
|
110
109
|
setEntered(true);
|
|
@@ -146,7 +145,8 @@ function LoadingScreen({ gallery, title, label = "Cargando…", enterLabel = "En
|
|
|
146
145
|
children: title
|
|
147
146
|
}) : null, ready ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
148
147
|
type: "button",
|
|
149
|
-
onClick:
|
|
148
|
+
onClick: enter,
|
|
149
|
+
onPointerUp: enter,
|
|
150
150
|
style: {
|
|
151
151
|
padding: "0.8rem 2.4rem",
|
|
152
152
|
border: "1px solid rgba(255,255,255,0.6)",
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../src/Gallery.tsx","../src/LoadingScreen.tsx"],"sourcesContent":["import { useEffect, useRef, type CSSProperties } from \"react\";\nimport {\n createGallery,\n type Gallery as GalleryInstance,\n type GalleryOptions,\n} from \"@scenography/engine\";\nimport type { Scene } from \"@scenography/scene\";\n\nexport interface GalleryProps {\n /** The scene definition to render. Changing it hot-reconciles in place. */\n scene: Scene;\n className?: string;\n style?: CSSProperties;\n /** Engine options (antialias, ssao, …). Applied once on mount. */\n options?: Omit<GalleryOptions, \"canvas\">;\n /** Called once with the gallery instance after it mounts (escape hatch). */\n onReady?: (gallery: GalleryInstance) => void;\n}\n\nconst fillStyle: CSSProperties = { display: \"block\", width: \"100%\", height: \"100%\" };\n\n/**\n * Declarative React wrapper over the engine. Mounts a `createGallery` onto a\n * canvas, keeps it sized to its container, and feeds scene changes through\n * `setScene` so edits reconcile without remounting.\n */\nexport function Gallery({ scene, className, style, options, onReady }: GalleryProps): JSX.Element {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const galleryRef = useRef<GalleryInstance | null>(null);\n\n // Mount once: create the gallery, track container size, run the loop.\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const gallery = createGallery(scene, { canvas, ...options });\n galleryRef.current = gallery;\n gallery.start();\n onReady?.(gallery);\n\n const resize = (): void => {\n const parent = canvas.parentElement;\n if (parent) gallery.resize(parent.clientWidth, parent.clientHeight);\n };\n resize();\n const observer = new ResizeObserver(resize);\n if (canvas.parentElement) observer.observe(canvas.parentElement);\n\n return () => {\n observer.disconnect();\n gallery.dispose();\n galleryRef.current = null;\n };\n // Mount/unmount only — scene updates are handled below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Hot-reconcile whenever the scene definition changes.\n useEffect(() => {\n galleryRef.current?.setScene(scene);\n }, [scene]);\n\n // Apply render-feature toggles live (antialias/ssao/bloom/shadows).\n useEffect(() => {\n galleryRef.current?.setRender({\n antialias: options?.antialias,\n ssao: options?.ssao,\n bloom: options?.bloom,\n shadows: options?.shadows,\n });\n }, [options?.antialias, options?.ssao, options?.bloom, options?.shadows]);\n\n return <canvas ref={canvasRef} className={className} style={{ ...fillStyle, ...style }} />;\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { Gallery, LoadingState } from \"@scenography/engine\";\n\nexport interface LoadingScreenProps {\n /** The live gallery, or null until `onReady` fires. */\n gallery: Gallery | null;\n /** Room title shown above the progress. */\n title?: string;\n /** Text while the assets load. Defaults to `\"Cargando…\"`. */\n label?: string;\n /** Button label once everything is ready. Defaults to `\"Entrar\"`. */\n enterLabel?: string;\n /** Hint shown in portrait on touch devices. */\n rotateHint?: string;\n /** Called when the visitor enters, before the fade starts. */\n onEnter?: () => void;\n /** Fade duration in ms. Defaults to `600`. */\n fadeDuration?: number;\n}\n\nconst EMPTY: LoadingState = { loaded: 0, total: 0, done: false };\n\n/**\n * Full-screen black loading cover for a gallery. Shows the initial asset\n * progress from `gallery.loading`; once done, the bar becomes an \"Entrar\"\n * button. The click is the user gesture needed for fullscreen, the landscape\n * lock and audio autoplay, and gives time to rotate the phone.\n */\nexport function LoadingScreen({\n gallery,\n title,\n label = \"Cargando…\",\n enterLabel = \"Entrar\",\n rotateHint = \"Gira el móvil para ver la sala en horizontal\",\n onEnter,\n fadeDuration = 600,\n}: LoadingScreenProps) {\n const [state, setState] = useState<LoadingState>(EMPTY);\n const [entered, setEntered] = useState(false);\n const [gone, setGone] = useState(false);\n const [portrait, setPortrait] = useState(false);\n const timer = useRef<number | null>(null);\n\n useEffect(() => {\n if (!gallery) return;\n return gallery.loading.subscribe(setState);\n }, [gallery]);\n\n useEffect(() => {\n const orientation = window.matchMedia(\"(orientation: portrait)\");\n const touch = window.matchMedia(\"(pointer: coarse)\").matches;\n const update = (): void => setPortrait(touch && orientation.matches);\n update();\n orientation.addEventListener(\"change\", update);\n return () => orientation.removeEventListener(\"change\", update);\n }, []);\n\n useEffect(\n () => () => {\n if (timer.current !== null) window.clearTimeout(timer.current);\n },\n [],\n );\n\n const enter = useCallback(async (): Promise<void> => {\n if (entered) return;\n // Fullscreen and the landscape lock need this gesture. iOS ignores both;\n // the room still opens.\n try {\n if (window.matchMedia(\"(pointer: coarse)\").matches) {\n if (document.documentElement.requestFullscreen) {\n await document.documentElement.requestFullscreen();\n }\n const screenOrientation = (screen as Screen & { orientation?: ScreenOrientation })\n .orientation as (ScreenOrientation & { lock?: (value: string) => Promise<void> }) | undefined;\n await screenOrientation?.lock?.(\"landscape\");\n }\n } catch {\n // Not supported: continue into the room.\n }\n onEnter?.();\n setEntered(true);\n timer.current = window.setTimeout(() => setGone(true), fadeDuration);\n }, [entered, fadeDuration, onEnter]);\n\n if (gone) return null;\n\n const ready = gallery !== null && state.done;\n const progress = state.total > 0 ? state.loaded / state.total : 0;\n const percent = Math.round(progress * 100);\n\n return (\n <div\n style={{\n position: \"fixed\",\n inset: 0,\n zIndex: 10000,\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n gap: \"1.25rem\",\n padding: \"2rem\",\n background: \"#000\",\n color: \"#fff\",\n textAlign: \"center\",\n fontFamily: \"ui-sans-serif, system-ui, sans-serif\",\n opacity: entered ? 0 : 1,\n pointerEvents: entered ? \"none\" : \"auto\",\n transition: `opacity ${fadeDuration}ms ease`,\n }}\n >\n {title ? (\n <h1 style={{ margin: 0, fontSize: \"clamp(1.5rem, 4vw, 2.25rem)\", fontWeight: 600 }}>\n {title}\n </h1>\n ) : null}\n\n {ready ? (\n <>\n <button\n type=\"button\"\n onClick={() => void enter()}\n style={{\n padding: \"0.8rem 2.4rem\",\n border: \"1px solid rgba(255,255,255,0.6)\",\n borderRadius: 999,\n background: \"#fff\",\n color: \"#000\",\n font: \"inherit\",\n fontSize: \"1.05rem\",\n fontWeight: 600,\n cursor: \"pointer\",\n }}\n >\n {enterLabel}\n </button>\n {portrait ? (\n <p style={{ margin: 0, maxWidth: \"22rem\", opacity: 0.75, lineHeight: 1.5 }}>{rotateHint}</p>\n ) : null}\n </>\n ) : (\n <>\n <p style={{ margin: 0, opacity: 0.75 }}>{label}</p>\n <div\n role=\"progressbar\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={percent}\n style={{\n width: \"min(20rem, 72vw)\",\n height: \"0.35rem\",\n borderRadius: 999,\n background: \"rgba(255,255,255,0.18)\",\n overflow: \"hidden\",\n }}\n >\n <div\n style={{\n width: `${percent}%`,\n height: \"100%\",\n background: \"#fff\",\n transition: \"width 200ms ease\",\n }}\n />\n </div>\n <p style={{ margin: 0, fontSize: \"0.8rem\", opacity: 0.6 }}>{percent} %</p>\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../src/Gallery.tsx","../src/LoadingScreen.tsx"],"sourcesContent":["import { useEffect, useRef, type CSSProperties } from \"react\";\nimport {\n createGallery,\n type Gallery as GalleryInstance,\n type GalleryOptions,\n} from \"@scenography/engine\";\nimport type { Scene } from \"@scenography/scene\";\n\nexport interface GalleryProps {\n /** The scene definition to render. Changing it hot-reconciles in place. */\n scene: Scene;\n className?: string;\n style?: CSSProperties;\n /** Engine options (antialias, ssao, …). Applied once on mount. */\n options?: Omit<GalleryOptions, \"canvas\">;\n /** Called once with the gallery instance after it mounts (escape hatch). */\n onReady?: (gallery: GalleryInstance) => void;\n}\n\nconst fillStyle: CSSProperties = { display: \"block\", width: \"100%\", height: \"100%\" };\n\n/**\n * Declarative React wrapper over the engine. Mounts a `createGallery` onto a\n * canvas, keeps it sized to its container, and feeds scene changes through\n * `setScene` so edits reconcile without remounting.\n */\nexport function Gallery({ scene, className, style, options, onReady }: GalleryProps): JSX.Element {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const galleryRef = useRef<GalleryInstance | null>(null);\n\n // Mount once: create the gallery, track container size, run the loop.\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const gallery = createGallery(scene, { canvas, ...options });\n galleryRef.current = gallery;\n gallery.start();\n onReady?.(gallery);\n\n const resize = (): void => {\n const parent = canvas.parentElement;\n if (parent) gallery.resize(parent.clientWidth, parent.clientHeight);\n };\n resize();\n const observer = new ResizeObserver(resize);\n if (canvas.parentElement) observer.observe(canvas.parentElement);\n\n return () => {\n observer.disconnect();\n gallery.dispose();\n galleryRef.current = null;\n };\n // Mount/unmount only — scene updates are handled below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Hot-reconcile whenever the scene definition changes.\n useEffect(() => {\n galleryRef.current?.setScene(scene);\n }, [scene]);\n\n // Apply render-feature toggles live (antialias/ssao/bloom/shadows).\n useEffect(() => {\n galleryRef.current?.setRender({\n antialias: options?.antialias,\n ssao: options?.ssao,\n bloom: options?.bloom,\n shadows: options?.shadows,\n });\n }, [options?.antialias, options?.ssao, options?.bloom, options?.shadows]);\n\n return <canvas ref={canvasRef} className={className} style={{ ...fillStyle, ...style }} />;\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { Gallery, LoadingState } from \"@scenography/engine\";\n\nexport interface LoadingScreenProps {\n /** The live gallery, or null until `onReady` fires. */\n gallery: Gallery | null;\n /** Room title shown above the progress. */\n title?: string;\n /** Text while the assets load. Defaults to `\"Cargando…\"`. */\n label?: string;\n /** Button label once everything is ready. Defaults to `\"Entrar\"`. */\n enterLabel?: string;\n /** Hint shown in portrait on touch devices. */\n rotateHint?: string;\n /** Called when the visitor enters, before the fade starts. */\n onEnter?: () => void;\n /** Fade duration in ms. Defaults to `600`. */\n fadeDuration?: number;\n}\n\nconst EMPTY: LoadingState = { loaded: 0, total: 0, done: false };\n\n/**\n * Full-screen black loading cover for a gallery. Shows the initial asset\n * progress from `gallery.loading`; once done, the bar becomes an \"Entrar\"\n * button. The click is the user gesture needed for fullscreen, the landscape\n * lock and audio autoplay, and gives time to rotate the phone.\n */\nexport function LoadingScreen({\n gallery,\n title,\n label = \"Cargando…\",\n enterLabel = \"Entrar\",\n rotateHint = \"Gira el móvil para ver la sala en horizontal\",\n onEnter,\n fadeDuration = 600,\n}: LoadingScreenProps) {\n const [state, setState] = useState<LoadingState>(EMPTY);\n const [entered, setEntered] = useState(false);\n const [gone, setGone] = useState(false);\n const [portrait, setPortrait] = useState(false);\n const timer = useRef<number | null>(null);\n\n useEffect(() => {\n if (!gallery) return;\n return gallery.loading.subscribe(setState);\n }, [gallery]);\n\n useEffect(() => {\n const orientation = window.matchMedia(\"(orientation: portrait)\");\n const touch = window.matchMedia(\"(pointer: coarse)\").matches;\n const update = (): void => setPortrait(touch && orientation.matches);\n update();\n orientation.addEventListener(\"change\", update);\n return () => orientation.removeEventListener(\"change\", update);\n }, []);\n\n useEffect(\n () => () => {\n if (timer.current !== null) window.clearTimeout(timer.current);\n },\n [],\n );\n\n const enter = useCallback((): void => {\n if (entered) return;\n // Ask for fullscreen and the landscape lock, but never let them hold up the\n // room: some mobile browsers leave these promises pending or reject them,\n // and the visitor must enter anyway. The call still happens inside the\n // click gesture, which is what the browser requires.\n if (window.matchMedia(\"(pointer: coarse)\").matches) {\n try {\n const fullscreen = document.documentElement.requestFullscreen?.();\n const screenOrientation = (screen as Screen & { orientation?: ScreenOrientation })\n .orientation as (ScreenOrientation & { lock?: (value: string) => Promise<void> }) | undefined;\n if (fullscreen) {\n void fullscreen\n .then(() => screenOrientation?.lock?.(\"landscape\"))\n .catch(() => undefined);\n }\n } catch {\n // Not supported: continue into the room.\n }\n }\n onEnter?.();\n setEntered(true);\n timer.current = window.setTimeout(() => setGone(true), fadeDuration);\n }, [entered, fadeDuration, onEnter]);\n\n if (gone) return null;\n\n const ready = gallery !== null && state.done;\n const progress = state.total > 0 ? state.loaded / state.total : 0;\n const percent = Math.round(progress * 100);\n\n return (\n <div\n style={{\n position: \"fixed\",\n inset: 0,\n zIndex: 10000,\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n gap: \"1.25rem\",\n padding: \"2rem\",\n background: \"#000\",\n color: \"#fff\",\n textAlign: \"center\",\n fontFamily: \"ui-sans-serif, system-ui, sans-serif\",\n opacity: entered ? 0 : 1,\n pointerEvents: entered ? \"none\" : \"auto\",\n transition: `opacity ${fadeDuration}ms ease`,\n }}\n >\n {title ? (\n <h1 style={{ margin: 0, fontSize: \"clamp(1.5rem, 4vw, 2.25rem)\", fontWeight: 600 }}>\n {title}\n </h1>\n ) : null}\n\n {ready ? (\n <>\n <button\n type=\"button\"\n onClick={enter}\n onPointerUp={enter}\n style={{\n padding: \"0.8rem 2.4rem\",\n border: \"1px solid rgba(255,255,255,0.6)\",\n borderRadius: 999,\n background: \"#fff\",\n color: \"#000\",\n font: \"inherit\",\n fontSize: \"1.05rem\",\n fontWeight: 600,\n cursor: \"pointer\",\n }}\n >\n {enterLabel}\n </button>\n {portrait ? (\n <p style={{ margin: 0, maxWidth: \"22rem\", opacity: 0.75, lineHeight: 1.5 }}>{rotateHint}</p>\n ) : null}\n </>\n ) : (\n <>\n <p style={{ margin: 0, opacity: 0.75 }}>{label}</p>\n <div\n role=\"progressbar\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={percent}\n style={{\n width: \"min(20rem, 72vw)\",\n height: \"0.35rem\",\n borderRadius: 999,\n background: \"rgba(255,255,255,0.18)\",\n overflow: \"hidden\",\n }}\n >\n <div\n style={{\n width: `${percent}%`,\n height: \"100%\",\n background: \"#fff\",\n transition: \"width 200ms ease\",\n }}\n />\n </div>\n <p style={{ margin: 0, fontSize: \"0.8rem\", opacity: 0.6 }}>{percent} %</p>\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -97,13 +97,12 @@ function LoadingScreen({ gallery, title, label = "Cargando…", enterLabel = "En
|
|
|
97
97
|
useEffect(() => () => {
|
|
98
98
|
if (timer.current !== null) window.clearTimeout(timer.current);
|
|
99
99
|
}, []);
|
|
100
|
-
const enter = useCallback(
|
|
100
|
+
const enter = useCallback(() => {
|
|
101
101
|
if (entered) return;
|
|
102
|
-
try {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
102
|
+
if (window.matchMedia("(pointer: coarse)").matches) try {
|
|
103
|
+
const fullscreen = document.documentElement.requestFullscreen?.();
|
|
104
|
+
const screenOrientation = screen.orientation;
|
|
105
|
+
if (fullscreen) fullscreen.then(() => screenOrientation?.lock?.("landscape")).catch(() => void 0);
|
|
107
106
|
} catch {}
|
|
108
107
|
onEnter?.();
|
|
109
108
|
setEntered(true);
|
|
@@ -145,7 +144,8 @@ function LoadingScreen({ gallery, title, label = "Cargando…", enterLabel = "En
|
|
|
145
144
|
children: title
|
|
146
145
|
}) : null, ready ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
|
|
147
146
|
type: "button",
|
|
148
|
-
onClick:
|
|
147
|
+
onClick: enter,
|
|
148
|
+
onPointerUp: enter,
|
|
149
149
|
style: {
|
|
150
150
|
padding: "0.8rem 2.4rem",
|
|
151
151
|
border: "1px solid rgba(255,255,255,0.6)",
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/Gallery.tsx","../src/LoadingScreen.tsx"],"sourcesContent":["import { useEffect, useRef, type CSSProperties } from \"react\";\nimport {\n createGallery,\n type Gallery as GalleryInstance,\n type GalleryOptions,\n} from \"@scenography/engine\";\nimport type { Scene } from \"@scenography/scene\";\n\nexport interface GalleryProps {\n /** The scene definition to render. Changing it hot-reconciles in place. */\n scene: Scene;\n className?: string;\n style?: CSSProperties;\n /** Engine options (antialias, ssao, …). Applied once on mount. */\n options?: Omit<GalleryOptions, \"canvas\">;\n /** Called once with the gallery instance after it mounts (escape hatch). */\n onReady?: (gallery: GalleryInstance) => void;\n}\n\nconst fillStyle: CSSProperties = { display: \"block\", width: \"100%\", height: \"100%\" };\n\n/**\n * Declarative React wrapper over the engine. Mounts a `createGallery` onto a\n * canvas, keeps it sized to its container, and feeds scene changes through\n * `setScene` so edits reconcile without remounting.\n */\nexport function Gallery({ scene, className, style, options, onReady }: GalleryProps): JSX.Element {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const galleryRef = useRef<GalleryInstance | null>(null);\n\n // Mount once: create the gallery, track container size, run the loop.\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const gallery = createGallery(scene, { canvas, ...options });\n galleryRef.current = gallery;\n gallery.start();\n onReady?.(gallery);\n\n const resize = (): void => {\n const parent = canvas.parentElement;\n if (parent) gallery.resize(parent.clientWidth, parent.clientHeight);\n };\n resize();\n const observer = new ResizeObserver(resize);\n if (canvas.parentElement) observer.observe(canvas.parentElement);\n\n return () => {\n observer.disconnect();\n gallery.dispose();\n galleryRef.current = null;\n };\n // Mount/unmount only — scene updates are handled below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Hot-reconcile whenever the scene definition changes.\n useEffect(() => {\n galleryRef.current?.setScene(scene);\n }, [scene]);\n\n // Apply render-feature toggles live (antialias/ssao/bloom/shadows).\n useEffect(() => {\n galleryRef.current?.setRender({\n antialias: options?.antialias,\n ssao: options?.ssao,\n bloom: options?.bloom,\n shadows: options?.shadows,\n });\n }, [options?.antialias, options?.ssao, options?.bloom, options?.shadows]);\n\n return <canvas ref={canvasRef} className={className} style={{ ...fillStyle, ...style }} />;\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { Gallery, LoadingState } from \"@scenography/engine\";\n\nexport interface LoadingScreenProps {\n /** The live gallery, or null until `onReady` fires. */\n gallery: Gallery | null;\n /** Room title shown above the progress. */\n title?: string;\n /** Text while the assets load. Defaults to `\"Cargando…\"`. */\n label?: string;\n /** Button label once everything is ready. Defaults to `\"Entrar\"`. */\n enterLabel?: string;\n /** Hint shown in portrait on touch devices. */\n rotateHint?: string;\n /** Called when the visitor enters, before the fade starts. */\n onEnter?: () => void;\n /** Fade duration in ms. Defaults to `600`. */\n fadeDuration?: number;\n}\n\nconst EMPTY: LoadingState = { loaded: 0, total: 0, done: false };\n\n/**\n * Full-screen black loading cover for a gallery. Shows the initial asset\n * progress from `gallery.loading`; once done, the bar becomes an \"Entrar\"\n * button. The click is the user gesture needed for fullscreen, the landscape\n * lock and audio autoplay, and gives time to rotate the phone.\n */\nexport function LoadingScreen({\n gallery,\n title,\n label = \"Cargando…\",\n enterLabel = \"Entrar\",\n rotateHint = \"Gira el móvil para ver la sala en horizontal\",\n onEnter,\n fadeDuration = 600,\n}: LoadingScreenProps) {\n const [state, setState] = useState<LoadingState>(EMPTY);\n const [entered, setEntered] = useState(false);\n const [gone, setGone] = useState(false);\n const [portrait, setPortrait] = useState(false);\n const timer = useRef<number | null>(null);\n\n useEffect(() => {\n if (!gallery) return;\n return gallery.loading.subscribe(setState);\n }, [gallery]);\n\n useEffect(() => {\n const orientation = window.matchMedia(\"(orientation: portrait)\");\n const touch = window.matchMedia(\"(pointer: coarse)\").matches;\n const update = (): void => setPortrait(touch && orientation.matches);\n update();\n orientation.addEventListener(\"change\", update);\n return () => orientation.removeEventListener(\"change\", update);\n }, []);\n\n useEffect(\n () => () => {\n if (timer.current !== null) window.clearTimeout(timer.current);\n },\n [],\n );\n\n const enter = useCallback(async (): Promise<void> => {\n if (entered) return;\n // Fullscreen and the landscape lock need this gesture. iOS ignores both;\n // the room still opens.\n try {\n if (window.matchMedia(\"(pointer: coarse)\").matches) {\n if (document.documentElement.requestFullscreen) {\n await document.documentElement.requestFullscreen();\n }\n const screenOrientation = (screen as Screen & { orientation?: ScreenOrientation })\n .orientation as (ScreenOrientation & { lock?: (value: string) => Promise<void> }) | undefined;\n await screenOrientation?.lock?.(\"landscape\");\n }\n } catch {\n // Not supported: continue into the room.\n }\n onEnter?.();\n setEntered(true);\n timer.current = window.setTimeout(() => setGone(true), fadeDuration);\n }, [entered, fadeDuration, onEnter]);\n\n if (gone) return null;\n\n const ready = gallery !== null && state.done;\n const progress = state.total > 0 ? state.loaded / state.total : 0;\n const percent = Math.round(progress * 100);\n\n return (\n <div\n style={{\n position: \"fixed\",\n inset: 0,\n zIndex: 10000,\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n gap: \"1.25rem\",\n padding: \"2rem\",\n background: \"#000\",\n color: \"#fff\",\n textAlign: \"center\",\n fontFamily: \"ui-sans-serif, system-ui, sans-serif\",\n opacity: entered ? 0 : 1,\n pointerEvents: entered ? \"none\" : \"auto\",\n transition: `opacity ${fadeDuration}ms ease`,\n }}\n >\n {title ? (\n <h1 style={{ margin: 0, fontSize: \"clamp(1.5rem, 4vw, 2.25rem)\", fontWeight: 600 }}>\n {title}\n </h1>\n ) : null}\n\n {ready ? (\n <>\n <button\n type=\"button\"\n onClick={() => void enter()}\n style={{\n padding: \"0.8rem 2.4rem\",\n border: \"1px solid rgba(255,255,255,0.6)\",\n borderRadius: 999,\n background: \"#fff\",\n color: \"#000\",\n font: \"inherit\",\n fontSize: \"1.05rem\",\n fontWeight: 600,\n cursor: \"pointer\",\n }}\n >\n {enterLabel}\n </button>\n {portrait ? (\n <p style={{ margin: 0, maxWidth: \"22rem\", opacity: 0.75, lineHeight: 1.5 }}>{rotateHint}</p>\n ) : null}\n </>\n ) : (\n <>\n <p style={{ margin: 0, opacity: 0.75 }}>{label}</p>\n <div\n role=\"progressbar\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={percent}\n style={{\n width: \"min(20rem, 72vw)\",\n height: \"0.35rem\",\n borderRadius: 999,\n background: \"rgba(255,255,255,0.18)\",\n overflow: \"hidden\",\n }}\n >\n <div\n style={{\n width: `${percent}%`,\n height: \"100%\",\n background: \"#fff\",\n transition: \"width 200ms ease\",\n }}\n />\n </div>\n <p style={{ margin: 0, fontSize: \"0.8rem\", opacity: 0.6 }}>{percent} %</p>\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/Gallery.tsx","../src/LoadingScreen.tsx"],"sourcesContent":["import { useEffect, useRef, type CSSProperties } from \"react\";\nimport {\n createGallery,\n type Gallery as GalleryInstance,\n type GalleryOptions,\n} from \"@scenography/engine\";\nimport type { Scene } from \"@scenography/scene\";\n\nexport interface GalleryProps {\n /** The scene definition to render. Changing it hot-reconciles in place. */\n scene: Scene;\n className?: string;\n style?: CSSProperties;\n /** Engine options (antialias, ssao, …). Applied once on mount. */\n options?: Omit<GalleryOptions, \"canvas\">;\n /** Called once with the gallery instance after it mounts (escape hatch). */\n onReady?: (gallery: GalleryInstance) => void;\n}\n\nconst fillStyle: CSSProperties = { display: \"block\", width: \"100%\", height: \"100%\" };\n\n/**\n * Declarative React wrapper over the engine. Mounts a `createGallery` onto a\n * canvas, keeps it sized to its container, and feeds scene changes through\n * `setScene` so edits reconcile without remounting.\n */\nexport function Gallery({ scene, className, style, options, onReady }: GalleryProps): JSX.Element {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const galleryRef = useRef<GalleryInstance | null>(null);\n\n // Mount once: create the gallery, track container size, run the loop.\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const gallery = createGallery(scene, { canvas, ...options });\n galleryRef.current = gallery;\n gallery.start();\n onReady?.(gallery);\n\n const resize = (): void => {\n const parent = canvas.parentElement;\n if (parent) gallery.resize(parent.clientWidth, parent.clientHeight);\n };\n resize();\n const observer = new ResizeObserver(resize);\n if (canvas.parentElement) observer.observe(canvas.parentElement);\n\n return () => {\n observer.disconnect();\n gallery.dispose();\n galleryRef.current = null;\n };\n // Mount/unmount only — scene updates are handled below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Hot-reconcile whenever the scene definition changes.\n useEffect(() => {\n galleryRef.current?.setScene(scene);\n }, [scene]);\n\n // Apply render-feature toggles live (antialias/ssao/bloom/shadows).\n useEffect(() => {\n galleryRef.current?.setRender({\n antialias: options?.antialias,\n ssao: options?.ssao,\n bloom: options?.bloom,\n shadows: options?.shadows,\n });\n }, [options?.antialias, options?.ssao, options?.bloom, options?.shadows]);\n\n return <canvas ref={canvasRef} className={className} style={{ ...fillStyle, ...style }} />;\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { Gallery, LoadingState } from \"@scenography/engine\";\n\nexport interface LoadingScreenProps {\n /** The live gallery, or null until `onReady` fires. */\n gallery: Gallery | null;\n /** Room title shown above the progress. */\n title?: string;\n /** Text while the assets load. Defaults to `\"Cargando…\"`. */\n label?: string;\n /** Button label once everything is ready. Defaults to `\"Entrar\"`. */\n enterLabel?: string;\n /** Hint shown in portrait on touch devices. */\n rotateHint?: string;\n /** Called when the visitor enters, before the fade starts. */\n onEnter?: () => void;\n /** Fade duration in ms. Defaults to `600`. */\n fadeDuration?: number;\n}\n\nconst EMPTY: LoadingState = { loaded: 0, total: 0, done: false };\n\n/**\n * Full-screen black loading cover for a gallery. Shows the initial asset\n * progress from `gallery.loading`; once done, the bar becomes an \"Entrar\"\n * button. The click is the user gesture needed for fullscreen, the landscape\n * lock and audio autoplay, and gives time to rotate the phone.\n */\nexport function LoadingScreen({\n gallery,\n title,\n label = \"Cargando…\",\n enterLabel = \"Entrar\",\n rotateHint = \"Gira el móvil para ver la sala en horizontal\",\n onEnter,\n fadeDuration = 600,\n}: LoadingScreenProps) {\n const [state, setState] = useState<LoadingState>(EMPTY);\n const [entered, setEntered] = useState(false);\n const [gone, setGone] = useState(false);\n const [portrait, setPortrait] = useState(false);\n const timer = useRef<number | null>(null);\n\n useEffect(() => {\n if (!gallery) return;\n return gallery.loading.subscribe(setState);\n }, [gallery]);\n\n useEffect(() => {\n const orientation = window.matchMedia(\"(orientation: portrait)\");\n const touch = window.matchMedia(\"(pointer: coarse)\").matches;\n const update = (): void => setPortrait(touch && orientation.matches);\n update();\n orientation.addEventListener(\"change\", update);\n return () => orientation.removeEventListener(\"change\", update);\n }, []);\n\n useEffect(\n () => () => {\n if (timer.current !== null) window.clearTimeout(timer.current);\n },\n [],\n );\n\n const enter = useCallback((): void => {\n if (entered) return;\n // Ask for fullscreen and the landscape lock, but never let them hold up the\n // room: some mobile browsers leave these promises pending or reject them,\n // and the visitor must enter anyway. The call still happens inside the\n // click gesture, which is what the browser requires.\n if (window.matchMedia(\"(pointer: coarse)\").matches) {\n try {\n const fullscreen = document.documentElement.requestFullscreen?.();\n const screenOrientation = (screen as Screen & { orientation?: ScreenOrientation })\n .orientation as (ScreenOrientation & { lock?: (value: string) => Promise<void> }) | undefined;\n if (fullscreen) {\n void fullscreen\n .then(() => screenOrientation?.lock?.(\"landscape\"))\n .catch(() => undefined);\n }\n } catch {\n // Not supported: continue into the room.\n }\n }\n onEnter?.();\n setEntered(true);\n timer.current = window.setTimeout(() => setGone(true), fadeDuration);\n }, [entered, fadeDuration, onEnter]);\n\n if (gone) return null;\n\n const ready = gallery !== null && state.done;\n const progress = state.total > 0 ? state.loaded / state.total : 0;\n const percent = Math.round(progress * 100);\n\n return (\n <div\n style={{\n position: \"fixed\",\n inset: 0,\n zIndex: 10000,\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n gap: \"1.25rem\",\n padding: \"2rem\",\n background: \"#000\",\n color: \"#fff\",\n textAlign: \"center\",\n fontFamily: \"ui-sans-serif, system-ui, sans-serif\",\n opacity: entered ? 0 : 1,\n pointerEvents: entered ? \"none\" : \"auto\",\n transition: `opacity ${fadeDuration}ms ease`,\n }}\n >\n {title ? (\n <h1 style={{ margin: 0, fontSize: \"clamp(1.5rem, 4vw, 2.25rem)\", fontWeight: 600 }}>\n {title}\n </h1>\n ) : null}\n\n {ready ? (\n <>\n <button\n type=\"button\"\n onClick={enter}\n onPointerUp={enter}\n style={{\n padding: \"0.8rem 2.4rem\",\n border: \"1px solid rgba(255,255,255,0.6)\",\n borderRadius: 999,\n background: \"#fff\",\n color: \"#000\",\n font: \"inherit\",\n fontSize: \"1.05rem\",\n fontWeight: 600,\n cursor: \"pointer\",\n }}\n >\n {enterLabel}\n </button>\n {portrait ? (\n <p style={{ margin: 0, maxWidth: \"22rem\", opacity: 0.75, lineHeight: 1.5 }}>{rotateHint}</p>\n ) : null}\n </>\n ) : (\n <>\n <p style={{ margin: 0, opacity: 0.75 }}>{label}</p>\n <div\n role=\"progressbar\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={percent}\n style={{\n width: \"min(20rem, 72vw)\",\n height: \"0.35rem\",\n borderRadius: 999,\n background: \"rgba(255,255,255,0.18)\",\n overflow: \"hidden\",\n }}\n >\n <div\n style={{\n width: `${percent}%`,\n height: \"100%\",\n background: \"#fff\",\n transition: \"width 200ms ease\",\n }}\n />\n </div>\n <p style={{ margin: 0, fontSize: \"0.8rem\", opacity: 0.6 }}>{percent} %</p>\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scenography/react",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "React bindings for scenography: a declarative <Gallery> component over the engine, with hot scene updates.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -23,19 +23,19 @@
|
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@scenography/engine": "0.7.
|
|
26
|
+
"@scenography/engine": "0.7.1"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/react": "^18.3.0",
|
|
30
30
|
"@types/three": "^0.160.0",
|
|
31
31
|
"react": "^18.3.0",
|
|
32
32
|
"three": "^0.160.0",
|
|
33
|
-
"@scenography/scene": "0.7.
|
|
33
|
+
"@scenography/scene": "0.7.1"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"react": ">=18",
|
|
37
37
|
"three": ">=0.160.0",
|
|
38
|
-
"@scenography/scene": "0.7.
|
|
38
|
+
"@scenography/scene": "0.7.1"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
41
|
"build": "tsdown",
|