can you portal elements into a Canvas in react-three-fibre? - reactjs

Is it possible to portal some react-three-fibre elements into a canvas from outside of one?
I.e. in the following example, which was my first attempt to portal a mesh into a Canvas which errored with "target element is not a DOM element" (which makes sense, given these aren't DOM elements)
import * as React from 'react'
import { Canvas, createPortal } from '#react-three/fiber'
export const Element = ({ parent }) => {
return createPortal(
<mesh>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="red" />
</mesh>
, parent);
};
export const MyPage = () => {
const ref = React.useRef(null);
return (
<div>
<Canvas>
<group ref={ref} />
</Canvas>
<Element parent={ref.current} />
</div>
);
};
So I found the createPortal method (which has incredibly limited documentation) from react-three-fiber, but with that it throws errors, presumably here because the createPortal method only works when already inside a Canvas context and is for portaling around inside a Canvas
import * as ReactDOM from 'react-dom'
import * as React from 'react'
import { Canvas } from '#react-three/fiber'
export const Element = ({ parent }) => {
return ReactDOM.createPortal(
<mesh>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="red" />
</mesh>
, parent);
};
export const MyPage = () => {
const ref = React.useRef(null);
return (
<div>
<Canvas>
<group ref={ref} />
</Canvas>
<Element parent={ref.current} />
</div>
);
};
the above throws this error:
react-dom.development.js:22839 Uncaught R3F hooks can only be used within the Canvas component!
Is there a way of doing this?
My hope is to be able to portal elements from elsewhere in the project into a single document-sized Canvas element that sits at the root.
update:
I have managed to get this to work by wrapping the call to createPortal in a second <Canvas />
export const Element = ({ parent }) => {
return (
<Canvas>
{createPortal(
<mesh>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="blue" />
</mesh>,
portalTo
)}
</Canvas>
);
};
</code>
however this is unworkable as three.js lmits the number of canvases in a project (and having unused actual canvases everywhere in the DOM is really gross anyway)
is there a way of giving createPortal access to that context without needing to render it inside a Canvas?

Related

How to move the camera using react-three-fiber and #react-three/drei?

I'm drawing a 3d model with the following configuration.
react-three-fiber
#react-three/drei
The following is a CodeSandBox with a model drawn.
CodeSandBox-A
The drawn model is close to the top of the canvas, and I want to center it.
Could you please tell me how to center the model on the canvas?
This is the first time I've touched Three.js, so I'm sure I may have made some elementary mistakes.
The code is as follows.
I referred to the following document for the props of the Canvas component.
React Three Fiber Documentation
import { useMemo, Suspense } from "react";
import "./style.css";
import { Canvas } from "#react-three/fiber";
const { useGLTF, OrbitControls, Stage } = require("#react-three/drei");
const CanvasContainer = () => {
const { scene } = useGLTF("/scene.glb");
return (
<Canvas camera={{ fov: 70, position: [0, 1000, 1000] }}>
// No matter what I put in this "position" value, I could not see any change in the canvas.
<OrbitControls enablePan={true} enableZoom={true} enableRotate={true} />
<Stage>
<group dispose={null}>
<primitive scale={[1, 1, 1]} object={scene} />
</group>
</Stage>
</Canvas>
);
};
const App = () => {
const isWindow = typeof window === "undefined";
const memoCanvas = useMemo(() => <CanvasContainer />, []);
return (
<>
<h1>Canvas</h1>
<div className="canvas_container">
{!isWindow && <Suspense fallback={<div />}>{memoCanvas}</Suspense>}
</div>
</>
);
};
export default App;
setDefaultCamera removed in v6.x. · Issue #1147 · pmndrs/react-three-fiber
I also tried the code to use useThree as described in the above Issue.
The following is its CodeSandBox.
CodeSandBox-B
As you can see from the CodeSandBox, I was unable to move the camera.
give orbitcontrols the "makeDefault" prop, then it should in the center. otherwise check out dreis Bounds component. see: https://twitter.com/0xca0a/status/1453807293074661385
the memocanvas thing btw makes no sense, don't put jsx into usememo.

Cannot update a component while rendering a different component - Environment / React- Three-Fiber

My App.jsx
import "./App.css";
import { Canvas } from "#react-three/fiber";
import React, { Suspense } from "react";
import { Html, useProgress } from "#react-three/drei";
const Scene = React.lazy(() => import('./components/Scene.jsx'));
function Loader() {
const { progress } = useProgress();
return <Html center>{progress} % loaded</Html>
}
function App() {
return (
<div id="canvas">
<Canvas>
<Suspense fallback={<Loader />}>
<Scene />
</Suspense>
</Canvas>
</div>
);
}
export default App;
My Scene.jsx
function Scene() {
<>
<ambientLight intensity={0.2} />
<directionalLight />
<GizmoHelper
alignment="bottom-right"
margin={[80, 80]}
>
<GizmoViewport
axisColors={["red", "green", "blue"]}
labelColor="black"
/>
</GizmoHelper>
<Text
position={[-3, 0, 0]}
anchorX="center"
anchorY="middle"
fontSize="1.5"
outLineBlur="5"
outlineColor="red"
curveRadius="2.9"
font="db.onlinewebfonts.com/t/02654c5f87934978cd2129477ad40869.ttf">
Hello world
<meshStandardMaterial
color="#ec2d2d"
opacity="1"
roughness="0"
metalness="0.3"
normalMap={normalMap}
/>
</Text>
<OrbitControls />
<Environment preset="city" background="true" />
</>
);
}
My problem:
Every time I start the app the environment doesn't load and I get the following error:
Warning: Cannot update a component (`Loader`) while rendering a different component (`Environment`)
If I change the preset while the live server is running the environment updates and shows instantly.
I have already tried to write a function and render the environment only when the window is loaded but I still got the same error.
I have read somewhere I could use a setTimeout but not sure whether this the right approach.
If anyone could point me in the right direction.
Update:
I did not include the fact that I was using the GizmoHelper component.
Removing the GizmoHelper from the scene solved the problem.
I have added the GizmoHelper in above code in case someone else has the same problem.
Further Update:
I noticed the error is still logged in the console but now it does not affect the initial rendering of the environment anymore.
Might still cause an issue further down the road though.

How to move camera with mouse move instead of mouse click

I was using react-three-fiber package and wanted to implement camera movement feature with mouse move instead of mouse click. For example, if you visit roundme.com then you will see a picture of a tour and when you move mouse camera, kinda, moves towards the position of pointer. So, to achieve it I decided to get current position of mouse and put that position to OrbitControls like this:
import React, { Suspense, useRef } from "react";
import { Canvas } from "react-three-fiber";
import { Html, OrbitControls } from "drei";
import "../../styles/index.css";
import { ReactReduxContext, Provider } from "react-redux";
import Portals from "../Portal/portal.component";
const Panorama = () => {
const mouse = useRef({ x: 0, y: 0 });
function getMousePos(e) {
console.log(e.clientX, e.clientY);
return { x: e.clientX, y: e.clientY };
}
return (
<>
<ReactReduxContext.Consumer>
{({ store }) => (
<Canvas
onMouseMove={(e) => (mouse.current = getMousePos(e))}
invalidateFrameloop
concurrent
camera={{ position: [50, 0, 0.1] }}
>
<OrbitControls
enableZoom={true}
enablePan={true}
dampingFactor={1}
minDistance={10}
maxDistance={500}
autoRotate
zoomSpeed={5}
autoRotateSpeed={0.5}
rotateSpeed={-1.4}
/>
<Suspense
fallback={
<Html center style={{ color: "white" }}>
loading...
</Html>
}
>
<Provider store={store}>
<Portals />
</Provider>
</Suspense>
</Canvas>
)}
</ReactReduxContext.Consumer>
</>
);
};
export default Panorama;
But camera does not move when I move mouse.
I'm not sure if you can use this same pattern with drei's OrbitControls, but here is how you can achieve it with the camera-controls package:
import CameraControls from 'camera-controls'
CameraControls.install({ THREE })
extend({ CameraControls })
function Controls() {
const ref = useRef()
const camera = useThree((state) => state.camera)
const gl = useThree((state) => state.gl)
useFrame((state, delta) => {
// update camera angles according to mouse position
ref.current.azimuthAngle = -state.mouse.x
ref.current.polarAngle = Math.PI / 2 + state.mouse.y
ref.current.update(delta)
})
return <cameraControls ref={ref} args={[camera, gl.domElement]} />
}
And then simply put <Controls /> inside of your <Canvas />.

ReactJS KonvasJS How to create 2 layers that cannot overlaps

i'm trying to create 2 layers (Images) that cannot overlap,
So they can still dragged freely, but won't showed over each other.
I've tried using zIndex or a blank Rect, but can't figure out how to make 2 draggable layers that can't overlap (Very similar to overflow: hidden behavior)
a GIF is attached to show the problem, each layer shouldn't be visible behind the divider line.
return (
<Stage width={size.width} height={size.height}>
{stateImages.map((imageConfig) => {
index++
return <Layer><Image
x={size.width/2 * index}
y={0}
image={imageConfig.image}
draggable
/>
</Layer>
})}
{stateImages.length > 1 &&
<Layer>
<Rect
x={size.width / 2}
y={0}
width={4}
height={size.height}
fill="white"
shadowBlur={10}
zIndex={2}
/>
</Layer>
}
</Stage>
)
You can use clip https://konvajs.org/docs/clipping/Clipping_Regions.html#page-title for that use case:
import React, { Component } from "react";
import { render } from "react-dom";
import { Stage, Layer, Image, Group } from "react-konva";
import useImage from "use-image";
// the first very simple and recommended way:
const MyImage = () => {
const [image] = useImage("https://i.imgur.com/ktWThtZ.png");
return <Image image={image} draggable />;
};
class App extends Component {
render() {
return (
<Stage width={window.innerWidth} height={window.innerHeight}>
<Layer>
<Group
clipX={0}
clipY={0}
clipWidth={window.innerWidth / 2}
clipHeight={window.innerHeight}
>
<MyImage />
</Group>
<Group
x={window.innerWidth / 2}
clipX={0}
clipY={0}
clipWidth={window.innerWidth / 2}
clipHeight={window.innerHeight}
>
<MyImage />
</Group>
</Layer>
</Stage>
);
}
}
render(<App />, document.getElementById("root"));
https://codesandbox.io/s/react-konva-clip-images-demo-onp3w?file=/src/index.js

How to implement Three.js inside Gatsby?

I've installed react-three-fiber and three packages. I'm following this tutorial, but I have doubts of where to put this line:
const rootElement = document.getElementById("root");
Also, I'm starting to receive this error:
Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
My index.js:
import React, { Children, useRef, useState } from "react"
import { Link } from "gatsby"
import { Canvas} from 'react-three-fiber'
import Layout from "../components/layout"
const IndexPage = () => {
return(
<Layout>
<div>
<h1>Hi</h1>
</div>
<canvas>
<Children></Children>
</canvas>
</Layout>
)
}
const rootElement = document.getElementById("root");
export default IndexPage
Any ideas?
Make sure you have installed both three and react-three-fiber.
npm install three react-three-fiber
Then in your gatsby page component just import Canvas from react-three-fiber before using it in your JSX.
import React from "react"
import { Canvas} from 'react-three-fiber'
import Layout from "../components/layout"
const IndexPage = () => (
<Layout>
<Canvas />
</Layout>
)
export default IndexPage
Regarding const rootElement = document.getElementById("root"); :
Although Gatsby is built with React, it doesn't require you to select a root element in order to render your app. If this sounds confusing, you should spend a little bit of time to take a read of the Gatsby docs.
If you were to build the example from the react-three-fiber docs in Gatsby, it would look something like this.
import React, { useRef, useState } from "react"
import { Canvas, useFrame } from "react-three-fiber"
const Box = props => {
// This reference will give us direct access to the mesh so we can animate it
const mesh = useRef()
// Set up state for the hovered and active state
const [hovered, setHover] = useState(false)
const [active, setActive] = useState(false)
// Rotate mesh every frame, this is outside of React without overhead
useFrame(() => (mesh.current.rotation.x = mesh.current.rotation.y += 0.01))
return (
<mesh
{...props}
ref={mesh}
scale={active ? [1.5, 1.5, 1.5] : [1, 1, 1]}
onClick={e => setActive(!active)}
onPointerOver={e => setHover(true)}
onPointerOut={e => setHover(false)}
>
<boxBufferGeometry attach="geometry" args={[1, 1, 1]} />
<meshStandardMaterial
attach="material"
color={hovered ? "hotpink" : "orange"}
/>
</mesh>
)
}
const IndexPage = () => (
<Canvas>
<ambientLight />
<pointLight position={[10, 10, 10]} />
<Box position={[-1.2, 0, 0]} />
<Box position={[1.2, 0, 0]} />
</Canvas>
)
export default IndexPage

Resources