npm install zdog react-zdog
# or
yarn add zdog react-zdog

react-zdog is a declarative abstraction of zdog, a cute pseudo 3d-engine. Doing zdog in React allows you to break up your scene graph into declarative, re-usable components with clean, reactive semantics. Try a live demo here.
How it looks like
import ReactDOM from "react-dom";
import React from "react";
import { Illustration, Shape } from "react-zdog";
ReactDOM.render(
<Illustration zoom={8}>
<Shape stroke={20} color="lightblue" rotate={{ x: Math.PI }} />
</Illustration>,
document.getElementById("root")
);
Illustration
The Illustration
object is your portal into zdog. It forwards unreserved properties to the internal Zdog.Illustration instance. The component auto adjusts to re-size changes and fills out the wrapping relative/absolute parent.
<Illustration element="svg" />
element
: Sets the graphics rendering DOM Element. Can be either 'svg' or 'canvas'. Default is "svg"frameloop
: Determins the render loop behavior, Can be either 'always' or 'demand'. default is 'always'.pointerEvents
: enables pointer events on zdog elements if set to true. Default is False.style
: styles for main renderer dom elemeent container.onDragStart
: callback on illustration's on drag start event listeneronDragMove
: callback on illustration's on drag move event listeneronDragEnd
: callback on illustration's on drag end event listener
And all the other props you will pass will be attached to illustration object. So any other properties or methods that you wanna set on illustration can be passed as prop as it is.
Hooks
All hooks can only be used inside the Illustration element because they rely on context updates!
useRender(callback, dependencies=[])
If you're running effects that need to get updated every frame, useRender gives you access to the render-loop.
import { useRender } from "react-zdog";
function Spin({ children }) {
const ref = useRef(undefined);
useRender((t) => (ref.current.rotate.y += 0.01));
return <Anchor ref={ref}>{children}</Anchor>;
}
useZdog()
Gives you access to the underlying state-model.
import { useZdog } from 'react-zdog'
function MyComponent() {
const {
illu,
scene,
size,
} = useZdog()
useInvalidate()
Gives you access to function that updates the one scene frame on each call. It is useful only if you're setting frameloop
props on Illustration component as demand
function MyComponent() {
const invalidate = useInvalidate()
const boxRef = useRef()
const rotate = () => {
boxRef.current.rotate.x += 0.03;
boxRef.current.rotate.y += 0.03;
invalidate()
}
return (
<Box
ref={boxRef}
{/* ...other props */}
/>
)}
Pointer Events
React-zdog supports the Click, Pointer Move, Pointer Enter and Pointer Leave events on Zdog elemets.
To use pointer events just enable the pointer events by setting pointerEvents
prop to true
on Illustration
component.
<Illustration pointerEvents={true} />
and use onClick, onPointerMove, onPointerEnter and OnPointerLeave on any zdog element.
const onClick = (e, ele) => {
};
const onPointerMove = (e, ele) => {
};
const onPointerEnter = (e, ele) => {
};
const onPointerLeave = (e, ele) => {
};
return (
<Box
onClick={onClick}
onPointerMove={onPointerMove}
onPointerEnter={onPointerEnter}
onPointerLeave={onPointerLeave}
/>
);
Note: zdog dosen't support pointer events out of the box, it is react-zdog specific feature which is added recently and was tested, but if you find some issue with events (and with any other thing) please open a issue and let us know.
Examples
Basic Example
import React, { useRef, useEffect } from 'react';
import { Illustration, useRender, useInvalidate, Box } from 'react-zdog';
const RotatingCube = () => {
const boxRef = useRef();
useRender(() => {
if (boxRef.current) {
boxRef.current.rotate.x += 0.03;
boxRef.current.rotate.y += 0.03;
}
});
return (
<Box
ref={boxRef}
width={50}
height={50}
depth={50}
color="#E44"
leftFace="#4E4"
rightFace="#44E"
topFace="#EE4"
bottomFace="#4EE"
/>
);
};
const App = () => {
return (
<Illustration zoom={4}>
<RotatingCube />
</Illustration>
);
};
export default App;
Pointer Events Example
import React, { useRef, useState } from 'react';
import { Illustration, useRender, Box } from 'react-zdog';
const InteractiveCube = () => {
const [isClicked, setIsClicked] = useState(false);
const colorsBeforeClick = {
main: "#E44",
left: "#4E4",
right: "#44E",
top: "#EE4",
bottom: "#4EE"
};
const colorsAfterClick = {
main: "#FF5733",
left: "#33FF57",
right: "#3357FF",
top: "#FF33A1",
bottom: "#A133FF"
};
const currentColors = isClicked ? colorsAfterClick : colorsBeforeClick;
const handleBoxClick = () => {
setIsClicked(!isClicked);
};
return (
<Box
width={50}
height={50}
depth={50}
color={currentColors.main}
leftFace={currentColors.left}
rightFace={currentColors.right}
topFace={currentColors.top}
bottomFace={currentColors.bottom}
onClick={handleBoxClick}
/>
);
};
const App = () => {
return (
<Illustration pointerEvents={true} zoom={4}>
<InteractiveCube />
</Illustration>
);
};
export default App;
On Demand rendering Example
import React, { useRef, useEffect } from "react";
import { Illustration, useInvalidate, Box } from "react-zdog";
const RotatingCube = () => {
const boxRef = useRef();
const invalidate = useInvalidate();
useEffect(() => {
const animate = () => {
if (boxRef.current) {
boxRef.current.rotate.x += 0.03;
boxRef.current.rotate.y += 0.03;
invalidate();
}
};
const intervalId = setInterval(animate, 1000);
return () => intervalId && clearInterval(intervalId);
}, [invalidate]);
return (
<Box
ref={boxRef}
width={50}
height={50}
depth={50}
color="#E44"
leftFace="#4E4"
rightFace="#44E"
topFace="#EE4"
bottomFace="#4EE"
/>
);
};
const App = () => {
return (
<Illustration zoom={4} frameloop="demand">
<RotatingCube />
</Illustration>
);
};
export default App;
Roadmap
- Create more Examples
- add More events support
Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.