├── .npmignore
├── example
├── src
│ ├── main.jsx
│ ├── components
│ │ ├── Test.jsx
│ │ └── RenderOnDemand.jsx
│ └── index.css
├── .gitignore
├── index.html
├── README.md
├── vite.config.js
├── .eslintrc.cjs
├── package.json
└── public
│ └── vite.svg
├── .gitignore
├── index.html
├── vite.config.js
├── .eslintrc.cjs
├── src
├── index.js
├── utils.js
├── hooks.jsx
└── Illustration.jsx
├── LICENSE
├── package.json
└── readme.md
/.npmignore:
--------------------------------------------------------------------------------
1 | examples/
--------------------------------------------------------------------------------
/example/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import App from "./components/RenderOnDemand";
4 | // import App from "./components/Test";
5 | import "./index.css";
6 |
7 | ReactDOM.createRoot(document.getElementById("root")).render(
8 |
9 |
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # React + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
--------------------------------------------------------------------------------
/example/vite.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | import { defineConfig } from "vite";
3 | import react from "@vitejs/plugin-react";
4 | import path from "path";
5 |
6 | // https://vitejs.dev/config/
7 | export default defineConfig(({ mode }) => {
8 | let alias = {};
9 |
10 | if (mode === "development") {
11 | alias = {
12 | "react-zdog": path.resolve(__dirname, "../src/index.js"),
13 | };
14 | }
15 |
16 | return {
17 | resolve: {
18 | alias: alias,
19 | },
20 | plugins: [react()],
21 | };
22 | });
23 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react";
3 | import path from "node:path";
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig({
7 | plugins: [react()],
8 | build: {
9 | lib: {
10 | entry: path.resolve(__dirname, "src/index.js"),
11 | name: "react-zdog",
12 | formats: ["es"],
13 | fileName: (format) => `react-zdog.${format}.js`,
14 | },
15 | rollupOptions: {
16 | external: ["react", "react-dom", "zdog"],
17 | },
18 | },
19 | });
20 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:react/recommended',
7 | 'plugin:react/jsx-runtime',
8 | 'plugin:react-hooks/recommended',
9 | ],
10 | ignorePatterns: ['dist', '.eslintrc.cjs'],
11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
12 | settings: { react: { version: '18.2' } },
13 | plugins: ['react-refresh'],
14 | rules: {
15 | 'react-refresh/only-export-components': [
16 | 'warn',
17 | { allowConstantExport: true },
18 | ],
19 | },
20 | }
21 |
--------------------------------------------------------------------------------
/example/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:react/recommended',
7 | 'plugin:react/jsx-runtime',
8 | 'plugin:react-hooks/recommended',
9 | ],
10 | ignorePatterns: ['dist', '.eslintrc.cjs'],
11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
12 | settings: { react: { version: '18.2' } },
13 | plugins: ['react-refresh'],
14 | rules: {
15 | 'react-refresh/only-export-components': [
16 | 'warn',
17 | { allowConstantExport: true },
18 | ],
19 | },
20 | }
21 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "react": "^18.2.0",
14 | "react-dom": "^18.2.0",
15 | "react-zdog": "^1.2.1",
16 | "zdog": "^1.1.3"
17 | },
18 | "devDependencies": {
19 | "@types/react": "^18.2.15",
20 | "@types/react-dom": "^18.2.7",
21 | "@vitejs/plugin-react": "^4.0.3",
22 | "eslint": "^8.45.0",
23 | "eslint-plugin-react": "^7.32.2",
24 | "eslint-plugin-react-hooks": "^4.6.0",
25 | "eslint-plugin-react-refresh": "^0.4.3",
26 | "vite": "^4.4.5"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Zdog from 'zdog'
2 | import { useRender, useInvalidate, useZdog } from './hooks'
3 | import { createZdog } from './utils'
4 | import { Illustration } from './Illustration'
5 |
6 | const Anchor = createZdog(Zdog.Anchor)
7 | const Shape = createZdog(Zdog.Shape)
8 | const Group = createZdog(Zdog.Group)
9 | const Rect = createZdog(Zdog.Rect)
10 | const RoundedRect = createZdog(Zdog.RoundedRect)
11 | const Ellipse = createZdog(Zdog.Ellipse)
12 | const Polygon = createZdog(Zdog.Polygon)
13 | const Hemisphere = createZdog(Zdog.Hemisphere)
14 | const Cylinder = createZdog(Zdog.Cylinder)
15 | const Cone = createZdog(Zdog.Cone)
16 | const Box = createZdog(Zdog.Box)
17 |
18 | export {
19 | Illustration,
20 | useRender,
21 | useZdog,
22 | useInvalidate,
23 | Anchor,
24 | Shape,
25 | Group,
26 | Rect,
27 | RoundedRect,
28 | Ellipse,
29 | Polygon,
30 | Hemisphere,
31 | Cylinder,
32 | Cone,
33 | Box,
34 | }
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Paul Henschel
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/example/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-zdog",
3 | "version": "1.2.2",
4 | "description": "React-fiber renderer for zdog",
5 | "type": "module",
6 | "repository": {
7 | "type": "git",
8 | "url": "git+https://github.com/drcmda/react-zdog.git"
9 | },
10 | "keywords": [
11 | "react",
12 | "renderer",
13 | "fiber",
14 | "zdog"
15 | ],
16 | "author": "Paul Henschel",
17 | "license": "MIT",
18 | "bugs": {
19 | "url": "https://github.com/drcmda/react-zdog/issues"
20 | },
21 | "homepage": "https://github.com/drcmda/react-zdog#readme",
22 | "scripts": {
23 | "dev": "vite",
24 | "build": "vite build",
25 | "publish:patch": "npm version patch && npm run build && npm publish",
26 | "publish:minor": "npm version minor && npm run build && npm publish",
27 | "publish:major": "npm version major && npm run build && npm publish",
28 | "preview": "vite preview"
29 | },
30 | "dependencies": {
31 | "react": "^18.2.0",
32 | "react-dom": "^18.2.0",
33 | "resize-observer-polyfill": "^1.5.1"
34 | },
35 | "devDependencies": {
36 | "@types/react": "^18.2.15",
37 | "@types/react-dom": "^18.2.7",
38 | "@vitejs/plugin-react": "^4.0.3",
39 | "eslint": "^8.45.0",
40 | "eslint-plugin-react": "^7.32.2",
41 | "eslint-plugin-react-hooks": "^4.6.0",
42 | "eslint-plugin-react-refresh": "^0.4.3",
43 | "vite": "^4.4.5"
44 | },
45 | "files": [
46 | "dist"
47 | ],
48 | "module": "./dist/react-zdog.es.js",
49 | "exports": {
50 | ".": {
51 | "import": "./dist/react-zdog.es.js"
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/example/src/components/Test.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import { TAU } from "zdog";
3 | import { useRef } from "react";
4 | import { Illustration, Anchor, Shape } from "react-zdog";
5 |
6 | const side = [
7 | [-1, -1, 1],
8 | [-1, 0, 1],
9 | [-1, 1, 1],
10 | [0, -1, 1],
11 | [0, 1, 1],
12 | [1, -1, 1],
13 | [1, 0, 1],
14 | [1, 1, 1],
15 | ];
16 | const middle = [
17 | [1, 1, 0],
18 | [1, -1, 0],
19 | [-1, 1, 0],
20 | [-1, -1, 0],
21 | ];
22 |
23 | function Dots({ stroke = 2.5, color = "lightblue", coords, ...props }) {
24 | return (
25 |
26 | {coords.map(([x, y, z], index) => (
27 | console.log(index, e, obj, ">>>>>>>>>>>>>>>>")}
30 | onPointerMove={() => console.log(index, "MOVE")}
31 | onPointerEnter={() => console.log(index, "Enter")}
32 | onPointerLeave={() => console.log(index, "Leave")}
33 | stroke={stroke}
34 | color={color}
35 | translate={{ x, y, z }}
36 | />
37 | ))}
38 |
39 | );
40 | }
41 |
42 | function Box() {
43 | let ref = useRef(undefined);
44 |
45 | return (
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
54 | export default function App() {
55 | return (
56 |
63 |
64 |
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | import Zdog from 'zdog'
2 | import React from 'react'
3 | import { useZdogPrimitive } from './hooks'
4 |
5 | export function applyProps(instance, newProps) {
6 | Zdog.extend(instance, newProps)
7 | }
8 |
9 | export const createZdog = primitive =>
10 | React.forwardRef(({ children, ...rest }, ref) => useZdogPrimitive(primitive, children, rest, ref)[0])
11 |
12 | export function generateRandomHexColor() {
13 | const randomInt = Math.floor(Math.random() * 16777216)
14 | const hexColor = randomInt.toString(16).toUpperCase()
15 | const color = '#' + hexColor.padStart(6, '0')
16 | if (color === '#000000') {
17 | return generateRandomHexColor()
18 | } else {
19 | return '#' + hexColor.padStart(6, '0')
20 | }
21 | }
22 |
23 | const componentToHex = c => {
24 | let hex = c.toString(16)
25 | return hex.length == 1 ? '0' + hex : hex
26 | }
27 |
28 | export const rgbToHex = (r, g, b) => {
29 | return '#' + componentToHex(r) + componentToHex(g) + componentToHex(b)
30 | }
31 |
32 | export function createProxy(target, handleChange, parentProp) {
33 | return new Proxy(target, {
34 | set(obj, prop, value) {
35 | if (typeof value === 'object' && value !== null) {
36 | value = createProxy(value, handleChange)
37 | }
38 | handleChange(obj, prop, value, parentProp)
39 | obj[prop] = value
40 | return true
41 | },
42 | get(obj, prop) {
43 | if (typeof obj[prop] === 'object' && obj[prop] !== null) {
44 | return createProxy(obj[prop], handleChange, prop)
45 | }
46 | return obj[prop]
47 | },
48 | })
49 | }
50 |
51 | export const getMousePos = (canvas, evt, canvas_ghost) => {
52 | const rect = canvas.getBoundingClientRect()
53 | return {
54 | x: ((evt.clientX - rect.left) / (rect.right - rect.left)) * canvas_ghost.width,
55 | y: ((evt.clientY - rect.top) / (rect.bottom - rect.top)) * canvas_ghost.height,
56 | }
57 | }
58 |
59 | export const getPixel = ({ x, y, canvasContext }) => {
60 | let imageData = canvasContext.getImageData(x, y, 1, 1)
61 | let data = imageData.data
62 | return rgbToHex(data[0], data[1], data[2])
63 | }
64 |
--------------------------------------------------------------------------------
/example/src/components/RenderOnDemand.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import { TAU } from "zdog";
3 | import { useEffect, useRef, useState } from "react";
4 | import {
5 | Illustration,
6 | Anchor,
7 | Shape,
8 | useInvalidate,
9 | useZdog,
10 | } from "react-zdog";
11 |
12 | const side = [
13 | [-1, -1, 1],
14 | [-1, 0, 1],
15 | [-1, 1, 1],
16 | [0, -1, 1],
17 | [0, 1, 1],
18 | [1, -1, 1],
19 | [1, 0, 1],
20 | [1, 1, 1],
21 | ];
22 | const middle = [
23 | [1, 1, 0],
24 | [1, -1, 0],
25 | [-1, 1, 0],
26 | [-1, -1, 0],
27 | ];
28 |
29 | function Dots({ stroke = 2.5, color = "lightblue", coords, ...props }) {
30 | return (
31 |
32 | {coords.map(([x, y, z], index) => (
33 |
39 | ))}
40 |
41 | );
42 | }
43 |
44 | function Box() {
45 | let ref = useRef(undefined);
46 |
47 | const [color, setColor] = useState("lightblue");
48 |
49 | useEffect(() => {
50 | const t = setInterval(() => {
51 | setColor((c) => (c === "lightblue" ? "red" : "lightblue"));
52 | }, 2000);
53 |
54 | return () => clearTimeout(t);
55 | }, []);
56 |
57 | return (
58 |
59 |
65 |
66 |
67 |
68 | );
69 | }
70 |
71 | const DragControl = () => {
72 | const invalidate = useInvalidate();
73 |
74 | const state = useZdog();
75 |
76 | useEffect(() => {
77 | if (state.illu) {
78 | state.illu.onDragMove = function () {
79 | invalidate();
80 | };
81 | }
82 | }, [state, invalidate]);
83 |
84 | return <>>;
85 | };
86 |
87 | export default function App() {
88 | return (
89 |
95 |
96 |
97 |
98 | );
99 | }
100 |
--------------------------------------------------------------------------------
/example/src/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | html,
6 | body,
7 | #root {
8 | width: 100%;
9 | height: 100%;
10 | margin: 0;
11 | padding: 0;
12 | background-color: #272727;
13 | -webkit-touch-callout: none;
14 | -webkit-user-select: none;
15 | -khtml-user-select: none;
16 | -moz-user-select: none;
17 | -ms-user-select: none;
18 | user-select: none;
19 | overflow: hidden;
20 | }
21 |
22 | #root {
23 | overflow: auto;
24 | }
25 |
26 | body {
27 | position: fixed;
28 | overflow: hidden;
29 | overscroll-behavior-y: none;
30 | font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir,
31 | helvetica neue, helvetica, ubuntu, roboto, noto, segoe ui, arial, sans-serif;
32 | color: black;
33 | -webkit-font-smoothing: antialiased;
34 | }
35 |
36 | .main {
37 | position: relative;
38 | width: 100%;
39 | height: 100%;
40 | color: #fff8de;
41 | overflow: hidden;
42 | }
43 |
44 | span.header {
45 | font-family: "Josefin Sans", sans-serif;
46 | font-weight: 700;
47 | position: absolute;
48 | display: inline-block;
49 | width: 500px;
50 | transform: translate3d(0, -50%, 0);
51 | font-size: 9em;
52 | line-height: 0.9em;
53 | pointer-events: none;
54 | top: 350px;
55 | left: 50px;
56 | }
57 |
58 | span.header-left {
59 | font-family: "Josefin Sans", sans-serif;
60 | font-weight: 700;
61 | position: absolute;
62 | display: inline-block;
63 | transform: translate3d(0, -50%, 0);
64 | line-height: 1em;
65 | top: 200px;
66 | left: 60px;
67 | font-size: 4em;
68 | width: 200px;
69 | }
70 |
71 | div.header-major {
72 | font-family: "Josefin Sans", sans-serif;
73 | font-weight: 700;
74 | position: absolute;
75 | top: 0;
76 | left: 0;
77 | width: 100%;
78 | height: 100%;
79 | display: flex;
80 | align-items: center;
81 | justify-content: center;
82 | }
83 |
84 | div.header-major > span {
85 | font-size: 15em;
86 | }
87 |
88 | @media only screen and (max-width: 600px) {
89 | span.header {
90 | top: 200px;
91 | left: 60px;
92 | font-size: 4em;
93 | width: 200px;
94 | }
95 | .bottom-left {
96 | display: none;
97 | }
98 | }
99 |
100 | a,
101 | .main > span {
102 | font-family: "Josefin Sans", sans-serif;
103 | font-weight: 400;
104 | font-size: 18px;
105 | color: inherit;
106 | position: absolute;
107 | display: inline;
108 | text-decoration: none;
109 | z-index: 1;
110 | }
111 |
112 | .main > span {
113 | z-index: 0;
114 | }
115 |
116 | .main > span > a {
117 | position: unset;
118 | text-transform: capitalize;
119 | }
120 |
121 | .top-left {
122 | top: 60px;
123 | left: 60px;
124 | }
125 |
126 | .top-right {
127 | top: 60px;
128 | right: 60px;
129 | }
130 |
131 | .bottom-left {
132 | bottom: 60px;
133 | left: 60px;
134 | }
135 |
136 | .bottom-right {
137 | bottom: 60px;
138 | right: 60px;
139 | }
140 |
141 | canvas {
142 | width: 100%;
143 | height: 100%;
144 | position: absolute;
145 | top: 0;
146 | overflow: hidden;
147 | }
148 |
149 | .grid {
150 | display: flex;
151 | flex-wrap: wrap;
152 | }
153 |
154 | .grid .item {
155 | position: relative;
156 | width: 50vw;
157 | height: 50vw;
158 | background: #eee;
159 | }
160 |
161 | @media only screen and (max-width: 480px) {
162 | .grid .item {
163 | width: 100vw;
164 | height: 100vw;
165 | }
166 | }
167 |
168 | .scroll-container {
169 | position: absolute;
170 | overflow: auto;
171 | top: 0px;
172 | width: 100%;
173 | height: 100vh;
174 | font-size: 20em;
175 | font-weight: 800;
176 | line-height: 0.9em;
177 | }
178 |
179 | span.middle {
180 | font-family: "Josefin Sans", sans-serif;
181 | font-weight: 700;
182 | position: absolute;
183 | display: inline-block;
184 | font-size: 60vh;
185 | line-height: 0.9em;
186 | pointer-events: none;
187 | top: 50%;
188 | left: 50%;
189 | transform: translate3d(-50%, -37%, 0);
190 | text-transform: uppercase;
191 | letter-spacing: -40px;
192 | }
193 |
--------------------------------------------------------------------------------
/src/hooks.jsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | useContext,
3 | useRef,
4 | useEffect,
5 | useLayoutEffect,
6 | useState,
7 | useImperativeHandle,
8 | useCallback,
9 | useMemo,
10 | } from 'react'
11 | import ResizeObserver from 'resize-observer-polyfill'
12 | import { applyProps, createProxy, generateRandomHexColor } from './utils'
13 |
14 | export const stateContext = React.createContext()
15 | export const parentContext = React.createContext()
16 | const ghostParentContext = React.createContext()
17 |
18 | export function useMeasure() {
19 | const ref = useRef()
20 | const [bounds, set] = useState({ left: 0, top: 0, width: 0, height: 0 })
21 | const [ro] = useState(() => new ResizeObserver(([entry]) => set(entry.contentRect)))
22 | useEffect(() => {
23 | if (ref.current) ro.observe(ref.current)
24 | return () => ro.disconnect()
25 | }, [ref.current])
26 | return [{ ref }, bounds]
27 | }
28 |
29 | export function useRender(fn, deps = []) {
30 | const state = useContext(stateContext)
31 | useEffect(() => {
32 | // Subscribe to the render-loop
33 | const unsubscribe = state.current.subscribe(fn)
34 | // Call subscription off on unmount
35 | return () => unsubscribe()
36 | }, deps)
37 | }
38 |
39 | export function useZdog() {
40 | const state = useContext(stateContext)
41 | return state.current
42 | }
43 |
44 | export function useZdogPrimitive(primitive, children, props, ref) {
45 | const state = useContext(stateContext)
46 | const parent = useContext(parentContext)
47 |
48 | const ghostParent = useContext(ghostParentContext)
49 |
50 | const colorId = useMemo(() => generateRandomHexColor(), [])
51 |
52 | const hiddenNodeProps = useMemo(() => {
53 | return {
54 | stroke: false,
55 | ...props,
56 | color: colorId,
57 | leftFace: colorId,
58 | rightFace: colorId,
59 | topFace: colorId,
60 | bottomFace: colorId,
61 | }
62 | }, [colorId, props])
63 |
64 | const [node] = useState(() => new primitive(props))
65 | const [ghost_node] = useState(() => new primitive(hiddenNodeProps))
66 |
67 | const syncGhostNode = (obj, prop, value, parentProp) => {
68 | if (parentProp) {
69 | ghost_node[parentProp][prop] = value
70 | } else {
71 | ghost_node[prop] = value
72 | }
73 |
74 | state.current.illu.updateRenderGraph()
75 | }
76 |
77 | const [proxyNode] = useState(() => createProxy(node, syncGhostNode))
78 |
79 | useImperativeHandle(ref, () => proxyNode)
80 |
81 | useLayoutEffect(() => {
82 | applyProps(node, props)
83 | if (parent) {
84 | state.current.illu.updateRenderGraph()
85 | }
86 | }, [props])
87 |
88 | useLayoutEffect(() => {
89 | applyProps(ghost_node, hiddenNodeProps)
90 | }, [hiddenNodeProps])
91 |
92 | useLayoutEffect(() => {
93 | if (!parent) return
94 |
95 | parent.addChild(node)
96 | state.current.illu.updateGraph()
97 |
98 | return () => {
99 | parent.removeChild(node)
100 | parent.updateFlatGraph()
101 | state.current.illu.updateGraph()
102 | }
103 | }, [parent])
104 |
105 | useEffect(() => {
106 | if (!parent) return
107 |
108 | state.current.itemMap[colorId] = node
109 | if (props.onClick) {
110 | state.current.clickEventMap[colorId] = props.onClick
111 | }
112 | if (props.onPointerMove) {
113 | state.current.pointerMoveEventMap[colorId] = props.onPointerMove
114 | }
115 | if (props.onPointerEnter) {
116 | state.current.pointerEnterEventMap[colorId] = props.onPointerEnter
117 | }
118 | if (props.onPointerLeave) {
119 | state.current.pointerLeaveEventMap[colorId] = props.onPointerLeave
120 | }
121 |
122 | return () => {
123 | delete state.current.itemMap[colorId]
124 | delete state.current.clickEventMap[colorId]
125 | delete state.current.pointerMoveEventMap[colorId]
126 | delete state.current.pointerEnterEventMap[colorId]
127 | delete state.current.pointerLeaveEventMap[colorId]
128 | }
129 | }, [props])
130 |
131 | useLayoutEffect(() => {
132 | if (!ghostParent) return
133 |
134 | ghostParent.addChild(ghost_node)
135 | state.current.illu_ghost.updateGraph()
136 |
137 | return () => {
138 | ghostParent.removeChild(ghost_node)
139 | ghostParent.updateFlatGraph()
140 | state.current.illu_ghost.updateGraph()
141 | }
142 | }, [ghostParent])
143 |
144 | return [
145 |
146 | {children}
147 | ,
148 | node,
149 | ghost_node,
150 | ]
151 | }
152 |
153 | export function useInvalidate() {
154 | const state = useZdog()
155 |
156 | const invalidate = useCallback(() => state.illu.updateRenderGraph(), [state])
157 |
158 | return invalidate
159 | }
160 |
--------------------------------------------------------------------------------
/src/Illustration.jsx:
--------------------------------------------------------------------------------
1 | import Zdog from 'zdog'
2 | import React, { useState, useRef, useEffect, useLayoutEffect } from 'react'
3 | import { useMeasure, useZdogPrimitive, stateContext } from './hooks'
4 | import { applyProps, getMousePos, getPixel } from './utils'
5 |
6 | export const Illustration = React.memo(
7 | ({
8 | children,
9 | style,
10 | resize,
11 | element: Element = 'svg',
12 | frameloop = 'always',
13 | dragRotate,
14 | onDragMove = () => {},
15 | onDragStart = () => {},
16 | onDragEnd = () => {},
17 | pointerEvents = false,
18 | ...rest
19 | }) => {
20 | const canvas = useRef()
21 |
22 | //ref to secondary canvas and 2d context
23 | const canvas_ghost = useRef()
24 |
25 | const [ghostCanvasContext, setGhostCanvasContext] = useState(null)
26 |
27 | useEffect(() => {
28 | setGhostCanvasContext(canvas_ghost.current.getContext('2d', { willReadFrequently: true }))
29 | }, [])
30 |
31 | const [bind, size] = useMeasure()
32 | const [result, scene, ghostScene] = useZdogPrimitive(Zdog.Anchor, children)
33 |
34 | const state = useRef({
35 | scene,
36 | illu: undefined,
37 | size: {},
38 | subscribers: [],
39 | subscribe: fn => {
40 | state.current.subscribers.push(fn)
41 | return () => (state.current.subscribers = state.current.subscribers.filter(s => s !== fn))
42 | },
43 | illu_ghost: undefined,
44 | itemMap: {},
45 | clickEventMap: {},
46 | pointerMoveEventMap: {},
47 | pointerEnterEventMap: {},
48 | pointerLeaveEventMap: {},
49 | pointerEvents,
50 | })
51 |
52 | useEffect(() => {
53 | state.current.size = size
54 | if (state.current.illu) {
55 | state.current.illu.setSize(size.width, size.height)
56 | state.current.illu_ghost.setSize(size.width, size.height)
57 | if (frameloop === 'demand') {
58 | state.current.illu.updateRenderGraph()
59 | state.current.illu_ghost.updateRenderGraph()
60 | }
61 | }
62 | }, [size])
63 |
64 | useEffect(() => {
65 | state.current.illu = new Zdog.Illustration({
66 | element: canvas.current,
67 | dragRotate,
68 | onDragMove: () => {
69 | state.current.illu_ghost.rotate = {
70 | x: state.current.illu.rotate.x,
71 | y: state.current.illu.rotate.y,
72 | z: state.current.illu.rotate.z,
73 | }
74 | onDragMove()
75 | },
76 | onDragStart: onDragStart,
77 | onDragEnd: onDragEnd,
78 | ...rest,
79 | })
80 | state.current.illu.addChild(scene)
81 | state.current.illu.updateGraph()
82 |
83 | state.current.illu_ghost = new Zdog.Illustration({
84 | element: canvas_ghost.current,
85 | ...rest,
86 | })
87 | state.current.illu_ghost.addChild(ghostScene)
88 | state.current.illu_ghost.updateGraph()
89 |
90 | let frame
91 | let active = true
92 | function render(t) {
93 | const { size, subscribers } = state.current
94 | if (size.width && size.height) {
95 | // Run local effects
96 | subscribers.forEach(fn => fn(t))
97 | // Render scene
98 | if (frameloop !== 'demand') {
99 | state.current.illu.updateRenderGraph()
100 | }
101 | }
102 | if (active && frameloop !== 'demand') frame = requestAnimationFrame(render)
103 | }
104 |
105 | // Start render loop
106 | render()
107 |
108 | return () => {
109 | // Take no chances, the loop has got to stop if the component unmounts
110 | active = false
111 | cancelAnimationFrame(frame)
112 | }
113 | }, [frameloop])
114 |
115 | // Takes care of updating the main illustration
116 | useLayoutEffect(() => {
117 | state.current.illu && applyProps(state.current.illu, rest)
118 | state.current.illu_ghost && applyProps(state.current.illu_ghost, rest)
119 | }, [rest])
120 |
121 | const click = e => {
122 | if (!pointerEvents) return
123 |
124 | state.current.illu_ghost && state.current.illu_ghost.updateRenderGraph()
125 | const coords = getMousePos(canvas.current, e, canvas_ghost.current)
126 | const pixel = getPixel({ ...coords, canvasContext: ghostCanvasContext })
127 | const colorId = pixel.toUpperCase()
128 | const clickEvent = state.current.clickEventMap[colorId]
129 | clickEvent && clickEvent(e, state.current.itemMap[colorId])
130 | }
131 |
132 | const prevColorId = useRef(null)
133 | const pointerOnObj = useRef(null)
134 |
135 | const setPointerOnObj = newState => {
136 | pointerOnObj.current = newState
137 | }
138 |
139 | const pointerMove = e => {
140 | if (!pointerEvents) return
141 |
142 | state.current.illu_ghost && state.current.illu_ghost.updateRenderGraph()
143 | const coords = getMousePos(canvas.current, e, canvas_ghost.current)
144 | const pixel = getPixel({ ...coords, canvasContext: ghostCanvasContext })
145 | const colorId = pixel.toUpperCase()
146 |
147 | if (colorId !== '#000000' && prevColorId.current !== colorId && pointerOnObj.current !== colorId) {
148 | const pointerEnterEvent = state.current.pointerEnterEventMap[colorId]
149 | pointerEnterEvent && pointerEnterEvent(e, state.current.itemMap[colorId])
150 | setPointerOnObj(prevColorId.current)
151 | }
152 |
153 | if (
154 | prevColorId.current &&
155 | prevColorId.current !== '#000000' &&
156 | prevColorId.current !== colorId &&
157 | pointerOnObj.current
158 | ) {
159 | const pointerLeaveEvent = state.current.pointerLeaveEventMap[prevColorId.current]
160 | pointerLeaveEvent && pointerLeaveEvent(e, state.current.itemMap[prevColorId.current])
161 | }
162 |
163 | const pointerMoveEvent = state.current.pointerMoveEventMap[colorId]
164 | pointerMoveEvent && pointerMoveEvent(e, state.current.itemMap[colorId])
165 |
166 | prevColorId.current = colorId
167 | }
168 |
169 | return (
170 | <>
171 |
182 |
190 | {state.current.illu && }
191 |
192 |
206 | >
207 | )
208 | }
209 | )
210 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | npm install zdog react-zdog
6 | # or
7 | yarn add zdog react-zdog
8 |
9 |  
10 |
11 | react-zdog is a declarative abstraction of [zdog](https://zzz.dog/), 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](https://codesandbox.io/s/nervous-feather-vk9uh).
12 |
13 | # How it looks like
14 |
15 | ```jsx
16 | import ReactDOM from "react-dom";
17 | import React from "react";
18 | import { Illustration, Shape } from "react-zdog";
19 |
20 | ReactDOM.render(
21 |
22 |
23 | ,
24 | document.getElementById("root")
25 | );
26 | ```
27 |
28 | # Illustration
29 |
30 | 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.
31 |
32 | ```jsx
33 | // Can be either 'svg' or 'canvas'
34 | ```
35 |
36 | - `element`: Sets the graphics rendering DOM Element. Can be either 'svg' or 'canvas'. Default is "svg"
37 | - `frameloop`: Determins the render loop behavior, Can be either 'always' or 'demand'. default is 'always'.
38 | - `pointerEvents`: enables pointer events on zdog elements if set to true. Default is False.
39 | - `style`: styles for main renderer dom elemeent container.
40 | - `onDragStart`: callback on illustration's on drag start event listener
41 | - `onDragMove`: callback on illustration's on drag move event listener
42 | - `onDragEnd`: callback on illustration's on drag end event listener
43 |
44 | 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.
45 |
46 | # Hooks
47 |
48 | All hooks can only be used _inside_ the Illustration element because they rely on context updates!
49 |
50 | #### useRender(callback, dependencies=[])
51 |
52 | If you're running effects that need to get updated every frame, useRender gives you access to the render-loop.
53 |
54 | ```jsx
55 | import { useRender } from "react-zdog";
56 |
57 | function Spin({ children }) {
58 | const ref = useRef(undefined);
59 | useRender((t) => (ref.current.rotate.y += 0.01));
60 | return {children} ;
61 | }
62 | ```
63 |
64 | #### useZdog()
65 |
66 | Gives you access to the underlying state-model.
67 |
68 | ```jsx
69 | import { useZdog } from 'react-zdog'
70 |
71 | function MyComponent() {
72 | const {
73 | illu, // The parent Zdog.Illustration object
74 | scene, // The Zdog.Anchor object that's being used as the default scene
75 | size, // Current canvas size
76 | } = useZdog()
77 | ```
78 |
79 | ### useInvalidate()
80 |
81 | 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`
82 |
83 | ```jsx
84 | function MyComponent() {
85 | const invalidate = useInvalidate()
86 | const boxRef = useRef()
87 | const rotate = () => {
88 | boxRef.current.rotate.x += 0.03;
89 | boxRef.current.rotate.y += 0.03; //this will update underlying javascript object
90 | invalidate() //But you need to call invalidate to render the changes on screen
91 | }
92 |
93 | return (
94 |
98 | )}
99 | ```
100 |
101 | # Pointer Events
102 |
103 | React-zdog supports the Click, Pointer Move, Pointer Enter and Pointer Leave events on Zdog elemets.
104 | To use pointer events just enable the pointer events by setting `pointerEvents` prop to `true` on `Illustration` component.
105 |
106 | ```jsx
107 |
108 | ```
109 |
110 | and use onClick, onPointerMove, onPointerEnter and OnPointerLeave on any zdog element.
111 |
112 | ```jsx
113 | const onClick = (e, ele) => {
114 | //runs when user clicks on box
115 | };
116 |
117 | const onPointerMove = (e, ele) => {
118 | //runs when user moves pointer over box
119 | };
120 |
121 | const onPointerEnter = (e, ele) => {
122 | //runs when user's pointer enters the box
123 | };
124 |
125 | const onPointerLeave = (e, ele) => {
126 | //runs when user's pointer leaves the box
127 | };
128 |
129 | return (
130 |
136 | );
137 | ```
138 |
139 |
140 | 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.
141 |
142 |
143 | # Examples
144 |
145 |
146 | Basic Example
147 |
148 | ```jsx
149 | import React, { useRef, useEffect } from 'react';
150 | import { Illustration, useRender, useInvalidate, Box } from 'react-zdog';
151 |
152 | // RotatingCube Component
153 | const RotatingCube = () => {
154 | const boxRef = useRef();
155 |
156 | // Use the useRender hook to continuously update the rotation
157 | useRender(() => {
158 | if (boxRef.current) {
159 | boxRef.current.rotate.x += 0.03;
160 | boxRef.current.rotate.y += 0.03;
161 | }
162 | });
163 |
164 | return (
165 |
176 | );
177 |
178 | };
179 |
180 | // App Component
181 | const App = () => {
182 | return (
183 |
184 |
185 |
186 | );
187 | };
188 |
189 | export default App;
190 |
191 | ````
192 |
193 |
194 |
195 | Pointer Events Example
196 |
197 | ```jsx
198 | import React, { useRef, useState } from 'react';
199 | import { Illustration, useRender, Box } from 'react-zdog';
200 |
201 | // InteractiveCube Component
202 | const InteractiveCube = () => {
203 | const [isClicked, setIsClicked] = useState(false);
204 |
205 | const colorsBeforeClick = {
206 | main: "#E44",
207 | left: "#4E4",
208 | right: "#44E",
209 | top: "#EE4",
210 | bottom: "#4EE"
211 | };
212 |
213 | const colorsAfterClick = {
214 | main: "#FF5733",
215 | left: "#33FF57",
216 | right: "#3357FF",
217 | top: "#FF33A1",
218 | bottom: "#A133FF"
219 | };
220 |
221 | const currentColors = isClicked ? colorsAfterClick : colorsBeforeClick;
222 |
223 | const handleBoxClick = () => {
224 | setIsClicked(!isClicked);
225 | };
226 |
227 |
228 | return (
229 |
240 | );
241 | };
242 |
243 | // App Component
244 | const App = () => {
245 | return (
246 |
247 |
248 |
249 | );
250 | };
251 |
252 | export default App;
253 |
254 | ````
255 |
256 |
257 |
258 |
259 | On Demand rendering Example
260 |
261 | ```jsx
262 | import React, { useRef, useEffect } from "react";
263 | import { Illustration, useInvalidate, Box } from "react-zdog";
264 |
265 | // RotatingCube Component
266 | const RotatingCube = () => {
267 | const boxRef = useRef();
268 | const invalidate = useInvalidate();
269 |
270 | useEffect(() => {
271 | const animate = () => {
272 | if (boxRef.current) {
273 | boxRef.current.rotate.x += 0.03;
274 | boxRef.current.rotate.y += 0.03;
275 | invalidate(); // Manually trigger a render
276 | }
277 | };
278 |
279 | const intervalId = setInterval(animate, 1000); // only renders the scene graph one a second instead of 60 times per second
280 |
281 | return () => intervalId && clearInterval(intervalId);
282 | }, [invalidate]);
283 |
284 | return (
285 |
296 | );
297 | };
298 |
299 | // App Component
300 | const App = () => {
301 | return (
302 |
303 |
304 |
305 | );
306 | };
307 |
308 | export default App;
309 | ```
310 |
311 |
312 |
313 | # Roadmap
314 |
315 | - Create more Examples
316 | - add More events support
317 |
318 | # Contributing
319 |
320 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
321 |
--------------------------------------------------------------------------------