UNPKG

@playcanvas/blocks

Version:

High level abstract 3D primitives for React

143 lines 6.71 kB
"use client"; import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; import { useEffect, useRef, useState } from "react"; import { Entity } from "@playcanvas/react"; import { Camera, Script } from "@playcanvas/react/components"; import { useTimeline, useAssetViewer } from "./splat-viewer-context.js"; import { Vec3 } from "playcanvas"; import { AnimCamera, createRotationTrack } from "./utils/animation.js"; // assumed import { computeStartingPose, Pose } from "./utils/pose.js"; import { useApp, useParent } from "@playcanvas/react/hooks"; import { StaticPostEffects } from "./utils/effects.js"; import { paris, neutral, noir } from "./utils/style.js"; // @ts-expect-error There is no type definition for the camera-controls script import { CameraControls } from "playcanvas/scripts/esm/camera-controls.mjs"; import { useRenderOnCameraChange } from "./hooks/use-render-on-camera-change.js"; const variants = new Map([ ['paris', paris], ['neutral', neutral], ['noir', noir] ]); const length = (a, b) => Math.sqrt(Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2) + Math.pow(a[2] - b[2], 2)); function CameraController({ focus = [0, 0, 0], distance = 0, animate = false, ...props }) { const entity = useParent(); useEffect(() => { /** * FIXME: * `cameraControls` name will be mangled in many bundlers. This needs to be updated when * PlayCanvas engine > v2.7.5 is released. See https://github.com/playcanvas/engine/pull/7593 */ // @ts-expect-error CameraControls is not defined in the script const controls = (entity.script?.cameraControls || entity.script?._CameraControls); if (controls) { controls.focus(new Vec3().fromArray(focus), false); } }, [focus, distance, animate]); return (_jsx(_Fragment, { children: _jsx(Script, { script: CameraControls, rotateSpeed: 0.5, rotateDamping: 0.985, ...props }) })); } export function SmartCamera({ fov = 30, animationTrack, variant = "neutral" }) { const entityRef = useRef(null); const { subscribe, isPlaying } = useTimeline(); const { mode, subscribeCameraReset } = useAssetViewer(); const timeoutRef = useRef(null); const [shouldUseRenderOnCameraChange, setShouldUseRenderOnCameraChange] = useState(false); const app = useApp(); const initialPoseRef = useRef(null); useEffect(() => { timeoutRef.current = setTimeout(() => { setShouldUseRenderOnCameraChange(true); app.renderNextFrame = true; }, 200); return () => clearTimeout(timeoutRef.current); }); useRenderOnCameraChange(shouldUseRenderOnCameraChange ? entityRef.current : null); const [pose, setPose] = useState({ position: [2, 1, 2], target: [0, 0, 0] }); const [animation, setAnimation] = useState(animationTrack ? AnimCamera.fromTrack(animationTrack) : null); useEffect(() => { const initialPose = computeStartingPose(app, fov); initialPoseRef.current = initialPose; setPose(initialPose); if (!animation) { const actualPose = new Pose().fromLookAt(new Vec3().fromArray(initialPose.position), new Vec3().fromArray(initialPose.target)); const track = createRotationTrack(actualPose); setAnimation(track); } }, [app]); // Expose reset functionality through callback useEffect(() => { if (!subscribeCameraReset) return; const unsubscribe = subscribeCameraReset(() => { if (!initialPoseRef.current || !entityRef.current) return; setPose(computeStartingPose(app, fov)); // Force a render app.renderNextFrame = true; }); return unsubscribe; }, [subscribeCameraReset]); // Listen to timeline changes useEffect(() => { if (!animation || !isPlaying) return; const pose = new Pose(); // const rot = new Quat(); const unsub = subscribe((t) => { animation.cursor.value = t * animation.frameRate; animation.update(); animation.getPose(pose); // if (mode === "animation") { entityRef.current?.setPosition(animation.position); entityRef.current?.setRotation(pose.rotation); // } else if (mode === "transition") { // // blend from previous to this // const { pos: fromPos, rot: fromRot } = transitionStartRef.current!; // const currentPos = new Vec3().lerp(fromPos, pose, 0.1); // const currentRot = new Quat().slerp(fromRot, rot, 0.1); // entityRef.current?.setPosition(currentPos); // entityRef.current?.setRotation(currentRot); // if ( // currentPos.distance(pose) < 0.01 && // Quat.dot(currentRot, rot) > 0.999 // ) { // setMode("animation"); // } // } }); return unsub; }, [mode, animation, isPlaying]); // When timeline starts playing, begin transition // useEffect(() => { // if (isPlaying && mode === "interactive" && animation) { // transitionStartRef.current = { // pos: entityRef.current?.getPosition().clone(), // rot: entityRef.current?.getRotation().clone(), // }; // setMode("transition"); // } // }, [isPlaying]); // if (!animationTrack || mode === "interactive") { // return ( // <Entity name="camera" ref={entityRef} position={pose.position}> // <Camera fov={fov} clearColor="#f3e8ff" /> // <CameraController // focus={pose.target} // enablePan={type === "fly"} // enableFly={type === "fly"} // enableOrbit={type === "orbit"} // /> // <StaticPostEffects /> // </Entity> // ); // } // If the variant is a string, use the default variant, otherwise use the variant object passed in const style = typeof variant === 'string' ? variants.get(variant) ?? neutral : variant; const distance = length(pose.position, pose.target) * 0.5; return (_jsxs(Entity, { name: "camera", ref: entityRef, position: pose.position, children: [_jsx(Camera, { fov: fov, clearColor: "#f3e8ff" }), _jsx(CameraController, { focus: pose.target, distance: distance, enablePan: mode === "fly", enableFly: true, enableOrbit: mode === "orbit", enabled: !isPlaying }), variant !== 'none' && _jsx(StaticPostEffects, { ...style })] })); } //# sourceMappingURL=smart-camera.js.map