diff --git a/docs/user-manual/react/examples/model-viewer.mdx b/docs/user-manual/react/examples/model-viewer.mdx
new file mode 100644
index 00000000000..a68a9931e45
--- /dev/null
+++ b/docs/user-manual/react/examples/model-viewer.mdx
@@ -0,0 +1,20 @@
+---
+title: Model Viewer
+description: Live PlayCanvas React example — a glb model viewer with environment lighting, shadows and orbit controls.
+---
+
+import { CodeExample } from '@site/src/components/playcanvas-react/CodeExample';
+import ModelViewerExample from '@site/src/components/playcanvas-react/examples/ModelViewerExample';
+import ModelViewerExampleSource from '!!raw-loader!@site/src/components/playcanvas-react/examples/ModelViewerExample.jsx';
+
+A simple glb model viewer with environment lighting, a shadow catcher and auto-rotating orbit controls. Drag to orbit the camera around the model.
+
+
+
+
diff --git a/docs/user-manual/react/examples/motion.mdx b/docs/user-manual/react/examples/motion.mdx
new file mode 100644
index 00000000000..453f5c10c0f
--- /dev/null
+++ b/docs/user-manual/react/examples/motion.mdx
@@ -0,0 +1,20 @@
+---
+title: Motion
+description: Live PlayCanvas React example — animating entities and lights with springs from the Motion animation library.
+---
+
+import { CodeExample } from '@site/src/components/playcanvas-react/CodeExample';
+import MotionExample from '@site/src/components/playcanvas-react/examples/MotionExample';
+import MotionExampleSource from '!!raw-loader!@site/src/components/playcanvas-react/examples/MotionExample.jsx';
+
+Hover over the capsule to see entities and lights animate with springs from the [Motion](https://motion.dev/) animation library. Motion values drive each entity's position, rotation and scale, while a script applies animated intensities to the lights.
+
+
+
+
diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/user-manual/react/examples/model-viewer.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/user-manual/react/examples/model-viewer.mdx
new file mode 100644
index 00000000000..3e5147dc231
--- /dev/null
+++ b/i18n/ja/docusaurus-plugin-content-docs/current/user-manual/react/examples/model-viewer.mdx
@@ -0,0 +1,20 @@
+---
+title: モデルビューア
+description: PlayCanvas Reactのライブサンプル — 環境ライティング、影、オービットコントロールを備えたglbモデルビューア。
+---
+
+import { CodeExample } from '@site/src/components/playcanvas-react/CodeExample';
+import ModelViewerExample from '@site/src/components/playcanvas-react/examples/ModelViewerExample';
+import ModelViewerExampleSource from '!!raw-loader!@site/src/components/playcanvas-react/examples/ModelViewerExample.jsx';
+
+環境ライティング、シャドウキャッチャー、自動回転するオービットコントロールを備えたシンプルなglbモデルビューアです。ドラッグしてモデルの周りでカメラを回転させてみましょう。
+
+
+
+
diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/user-manual/react/examples/motion.mdx b/i18n/ja/docusaurus-plugin-content-docs/current/user-manual/react/examples/motion.mdx
new file mode 100644
index 00000000000..1f741d45821
--- /dev/null
+++ b/i18n/ja/docusaurus-plugin-content-docs/current/user-manual/react/examples/motion.mdx
@@ -0,0 +1,20 @@
+---
+title: モーション
+description: PlayCanvas Reactのライブサンプル — Motionアニメーションライブラリのスプリングでエンティティとライトをアニメーション。
+---
+
+import { CodeExample } from '@site/src/components/playcanvas-react/CodeExample';
+import MotionExample from '@site/src/components/playcanvas-react/examples/MotionExample';
+import MotionExampleSource from '!!raw-loader!@site/src/components/playcanvas-react/examples/MotionExample.jsx';
+
+カプセルにホバーすると、[Motion](https://motion.dev/)アニメーションライブラリのスプリングでエンティティとライトがアニメーションします。モーション値が各エンティティの位置、回転、スケールを駆動し、スクリプトがアニメーションされた強度をライトに適用します。
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
index e01122c578f..2e74e8dd739 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -25,6 +25,7 @@
"docusaurus-plugin-sass": "^0.2.6",
"js-yaml": "^4.2.0",
"leva": "^0.10.1",
+ "motion": "^12.40.0",
"playcanvas": "^2.19.6",
"prism-react-renderer": "^2.4.1",
"raw-loader": "^4.0.2",
@@ -12178,6 +12179,33 @@
"url": "https://github.com/sponsors/rawify"
}
},
+ "node_modules/framer-motion": {
+ "version": "12.40.0",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.40.0.tgz",
+ "integrity": "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^12.40.0",
+ "motion-utils": "^12.39.0",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
@@ -17087,6 +17115,47 @@
"ufo": "^1.6.3"
}
},
+ "node_modules/motion": {
+ "version": "12.40.0",
+ "resolved": "https://registry.npmjs.org/motion/-/motion-12.40.0.tgz",
+ "integrity": "sha512-yjrHUrBFW6kQvjJwRsoiPSAhC5tRwRqNGJWmiJ4CrGnbKp0V88AdzkhBmDoqIsIPfarOe0Uddd37Xq43/gIocA==",
+ "license": "MIT",
+ "dependencies": {
+ "framer-motion": "^12.40.0",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/motion-dom": {
+ "version": "12.40.0",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.40.0.tgz",
+ "integrity": "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-utils": "^12.39.0"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "12.39.0",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz",
+ "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==",
+ "license": "MIT"
+ },
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
diff --git a/package.json b/package.json
index 7f1183a8da7..39cbc867412 100644
--- a/package.json
+++ b/package.json
@@ -53,6 +53,7 @@
"docusaurus-plugin-sass": "^0.2.6",
"js-yaml": "^4.2.0",
"leva": "^0.10.1",
+ "motion": "^12.40.0",
"playcanvas": "^2.19.6",
"prism-react-renderer": "^2.4.1",
"raw-loader": "^4.0.2",
diff --git a/sidebars.js b/sidebars.js
index 30aa984836f..25566ec27e9 100644
--- a/sidebars.js
+++ b/sidebars.js
@@ -479,6 +479,8 @@ const sidebars = {
id: 'user-manual/react/examples/index',
},
items: [
+ 'user-manual/react/examples/model-viewer',
+ 'user-manual/react/examples/motion',
'user-manual/react/examples/physics',
]
},
diff --git a/src/components/playcanvas-react/examples/ModelViewerExample.jsx b/src/components/playcanvas-react/examples/ModelViewerExample.jsx
new file mode 100644
index 00000000000..bd0a222fed8
--- /dev/null
+++ b/src/components/playcanvas-react/examples/ModelViewerExample.jsx
@@ -0,0 +1,66 @@
+import { Application, Entity } from '@playcanvas/react';
+import { Camera, Environment, Render, Script } from '@playcanvas/react/components';
+import { useEnvAtlas, useModel } from '@playcanvas/react/hooks';
+import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
+import { Vec2 } from 'playcanvas';
+import AutoRotate from '../AutoRotate';
+import Grid from '../Grid';
+import StaticPostEffects from '../PostEffects';
+import ShadowCatcher from '../ShadowCatcher';
+
+const OrbitControls = ({ zoomRange = [1, 10], pitchRange = [-90, -5], ...props }) => (
+
+);
+
+// ↑ imports hidden
+
+const ModelViewerScene = () => {
+ /**
+ * A simple glb viewer with environment lighting, a shadow catcher
+ * and auto-rotating orbit controls.
+ */
+ const { asset: envAtlas } = useEnvAtlas('/assets/environment.png');
+ const { asset: model } = useModel('/assets/lambo.glb');
+
+ if (!model || !envAtlas) {
+ return null;
+ }
+
+ return (
+
+ {/* Create some environment lighting */}
+
+
+ {/* Render the background grid */}
+
+
+ {/* Add a shadow catcher to catch the shadows from the model */}
+
+
+ {/* Create a camera entity */}
+
+
+
+
+
+
+
+ {/* Render the model */}
+
+
+ );
+};
+
+const ModelViewerExample = () => (
+
+
+
+);
+
+export default ModelViewerExample;
diff --git a/src/components/playcanvas-react/examples/MotionExample.jsx b/src/components/playcanvas-react/examples/MotionExample.jsx
new file mode 100644
index 00000000000..1131c66a308
--- /dev/null
+++ b/src/components/playcanvas-react/examples/MotionExample.jsx
@@ -0,0 +1,267 @@
+import { useEffect, useRef, useState } from 'react';
+import { Application, Entity } from '@playcanvas/react';
+import { Camera, Environment, Light, Render, Script } from '@playcanvas/react/components';
+import { useApp, useEnvAtlas } from '@playcanvas/react/hooks';
+import { animate, useMotionValue, useMotionValueEvent, useSpring, useTransform } from 'motion/react';
+import { EVENT_MOUSEMOVE, Script as PcScript, Vec2 } from 'playcanvas';
+import StaticPostEffects from '../PostEffects';
+
+// ↑ imports hidden
+
+/**
+ * Three spring values that animate together as an [x, y, z] array.
+ */
+const useMotionVec3 = (initial, defaultValue = 0) => {
+ const x = useSpring(initial?.[0] ?? defaultValue);
+ const y = useSpring(initial?.[1] ?? defaultValue);
+ const z = useSpring(initial?.[2] ?? defaultValue);
+
+ const array = useTransform([x, y, z], ([xVal, yVal, zVal]) => [xVal, yVal, zVal]);
+
+ const animateArray = (target) => {
+ if (!target) return;
+ x.set(target[0] ?? x.get());
+ y.set(target[1] ?? y.get());
+ z.set(target[2] ?? z.get());
+ };
+
+ return { array, animateArray };
+};
+
+/**
+ * An Entity whose position, rotation and scale spring toward the values
+ * passed in the `animate` prop, driven by Motion spring values.
+ */
+const MotionEntity = ({ children, animate: animateProps, ...props }) => {
+ const position = useMotionVec3(props.position, 0);
+ const rotation = useMotionVec3(props.rotation, 0);
+ const scale = useMotionVec3(props.scale, 1);
+
+ const entityRef = useRef(null);
+
+ useEffect(() => {
+ if (animateProps) {
+ position.animateArray(animateProps.position);
+ rotation.animateArray(animateProps.rotation);
+ scale.animateArray(animateProps.scale);
+ }
+ }, [animateProps]);
+
+ useMotionValueEvent(position.array, 'change', ([x, y, z]) => {
+ entityRef.current?.setLocalPosition(x, y, z);
+ });
+
+ useMotionValueEvent(rotation.array, 'change', ([x, y, z]) => {
+ entityRef.current?.setLocalEulerAngles(x, y, z);
+ });
+
+ useMotionValueEvent(scale.array, 'change', ([x, y, z]) => {
+ entityRef.current?.setLocalScale(x, y, z);
+ });
+
+ return (
+
+ {children}
+
+ );
+};
+
+/**
+ * Applies an animated motion value to the light's intensity every frame.
+ */
+class LightIntensityScript extends PcScript {
+ static scriptName = 'lightIntensity';
+
+ intensityMV = null;
+
+ update() {
+ if (this.entity.light && this.intensityMV) {
+ this.entity.light.intensity = this.intensityMV.get();
+ }
+ }
+}
+
+/**
+ * A light whose intensity animates toward the `intensity` prop. A motion
+ * value tweens the intensity and a script applies it to the light every frame.
+ */
+const MotionLight = ({ intensity = 1, type = 'directional', transition = { duration: 0.2 }, ...props }) => {
+ const intensityMV = useMotionValue(intensity);
+
+ useEffect(() => {
+ animate(intensityMV, intensity, transition);
+ }, [intensity]);
+
+ return (
+ <>
+
+
+ >
+ );
+};
+
+/**
+ * Rotates the entity toward the pointer position.
+ */
+class MouseRotatesEntity extends PcScript {
+ static scriptName = 'mouseRotatesEntity';
+
+ initialize() {
+ this.target = new Vec2();
+ this.current = new Vec2();
+ this.handleMouseMove = (e) => {
+ // Normalize the pointer position to [-1, 1] and map it to degrees
+ const canvas = this.app.graphicsDevice.canvas;
+ this.target.set(
+ (e.x / canvas.clientWidth) * 2 - 1,
+ (e.y / canvas.clientHeight) * 2 - 1
+ ).mulScalar(15);
+ };
+ this.app.mouse?.on(EVENT_MOUSEMOVE, this.handleMouseMove);
+ }
+
+ destroy() {
+ this.app.mouse?.off(EVENT_MOUSEMOVE, this.handleMouseMove);
+ }
+
+ update(dt) {
+ this.current.lerp(this.current, this.target, 0.4 * dt);
+ this.entity.setEulerAngles(this.current.y, this.current.x, 0);
+ }
+}
+
+const MotionScene = () => {
+ const app = useApp();
+ const { asset: envAtlas } = useEnvAtlas('/assets/environment.png');
+
+ const [hovered, setHovered] = useState(false);
+
+ const setCursor = (cursor) => {
+ app.graphicsDevice.canvas.style.cursor = cursor;
+ };
+
+ const onPointerOver = () => {
+ setCursor('pointer');
+ setHovered(true);
+ };
+
+ const onPointerOut = () => {
+ setCursor('auto');
+ setHovered(false);
+ };
+
+ if (!envAtlas) {
+ return null;
+ }
+
+ const rotation = [0, 0, 90];
+ const scale = hovered ? [1.2, 1.2, 1.2] : [1, 1, 1];
+
+ return (
+
+
+
+
+
+
+
+ {/* Create some environment lighting */}
+
+
+ {/* Create some additional lighting */}
+
+
+
+
+
+
+
+ {/* Create a capsule button that animates when hovered */}
+
+
+
+ {/* Create a decoration that animates when hovered */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* A soft gradient glow that fades in on hover */}
+
+
+ {/* The headline overlay */}
+
+
+ Hover
+
+
+
+ );
+};
+
+const MotionExample = () => (
+
+
+
+);
+
+export default MotionExample;