From 260c26f28032d7178f91ce9b115149c86c0762da Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Mon, 15 Jun 2020 12:29:41 +0200 Subject: [PATCH 1/4] Add typescript declarations --- index.js | 164 -------------------- index.tsx | 216 +++++++++++++++++++++++++++ package.json | 10 +- rollup.config.js => rollup.config.ts | 7 +- tsconfig.json | 21 +++ yarn.lock | 10 ++ 6 files changed, 257 insertions(+), 171 deletions(-) delete mode 100644 index.js create mode 100644 index.tsx rename rollup.config.js => rollup.config.ts (90%) create mode 100644 tsconfig.json diff --git a/index.js b/index.js deleted file mode 100644 index 11ce3a4..0000000 --- a/index.js +++ /dev/null @@ -1,164 +0,0 @@ -import Zdog from 'zdog' -import React, { useContext, useRef, useEffect, useLayoutEffect, useState, useImperativeHandle } from 'react' -import ResizeObserver from 'resize-observer-polyfill' - -const stateContext = React.createContext() -const parentContext = React.createContext() - -let globalEffects = [] -export function addEffect(callback) { - globalEffects.push(callback) -} - -export function invalidate() { - // TODO: render loop has to be able to render frames on demand -} - -export function applyProps(instance, newProps) { - Zdog.extend(instance, newProps) - invalidate() -} - -function useMeasure() { - const ref = useRef() - const [bounds, set] = useState({ left: 0, top: 0, width: 0, height: 0 }) - const [ro] = useState(() => new ResizeObserver(([entry]) => set(entry.contentRect))) - useEffect(() => { - if (ref.current) ro.observe(ref.current) - return () => ro.disconnect() - }, [ref.current]) - return [{ ref }, bounds] -} - -function useRender(fn, deps = []) { - const state = useContext(stateContext) - useEffect(() => { - // Subscribe to the render-loop - const unsubscribe = state.current.subscribe(fn) - // Call subscription off on unmount - return () => unsubscribe() - }, deps) -} - -function useZdog() { - const state = useContext(stateContext) - return state.current -} - -function useZdogPrimitive(primitive, children, props, ref) { - const state = useContext(stateContext) - const parent = useContext(parentContext) - const [node] = useState(() => new primitive(props)) - - useImperativeHandle(ref, () => node) - useLayoutEffect(() => void applyProps(node, props), [props]) - useLayoutEffect(() => { - if (parent) { - parent.addChild(node) - state.current.illu.updateGraph() - return () => { - parent.removeChild(node) - parent.updateFlatGraph() - state.current.illu.updateGraph() - } - } - }, [parent]) - return [, node] -} - -const Illustration = React.memo(({ children, style, resize, element: Element = 'svg', dragRotate, ...rest }) => { - const canvas = useRef() - const [bind, size] = useMeasure() - const [result, scene] = useZdogPrimitive(Zdog.Anchor, children) - - const state = useRef({ - scene, - illu: undefined, - size: {}, - subscribers: [], - subscribe: fn => { - state.current.subscribers.push(fn) - return () => (state.current.subscribers = state.current.subscribers.filter(s => s !== fn)) - }, - }) - - useEffect(() => { - state.current.size = size - if (state.current.illu) state.current.illu.setSize(size.width, size.height) - }, [size]) - - useEffect(() => { - state.current.illu = new Zdog.Illustration({ element: canvas.current, dragRotate, ...rest }) - state.current.illu.addChild(scene) - state.current.illu.updateGraph() - - let frame - let active = true - function render(t) { - const { size, subscribers } = state.current - if (size.width && size.height) { - // Run global effects - globalEffects.forEach(fn => fn(t)) - // Run local effects - subscribers.forEach(fn => fn(t)) - // Render scene - state.current.illu.updateRenderGraph() - } - if (active) frame = requestAnimationFrame(render) - } - - // Start render loop - render() - - return () => { - // Take no chances, the loop has got to stop if the component unmounts - active = false - cancelAnimationFrame(frame) - } - }, []) - - // Takes care of updating the main illustration - useLayoutEffect(() => void (state.current.illu && applyProps(state.current.illu, rest)), [rest]) - - return ( -
- - {state.current.illu && } -
- ) -}) - -const createZdog = primitive => - React.forwardRef(({ children, ...rest }, ref) => useZdogPrimitive(primitive, children, rest, ref)[0]) - -const Anchor = createZdog(Zdog.Anchor) -const Shape = createZdog(Zdog.Shape) -const Group = createZdog(Zdog.Group) -const Rect = createZdog(Zdog.Rect) -const RoundedRect = createZdog(Zdog.RoundedRect) -const Ellipse = createZdog(Zdog.Ellipse) -const Polygon = createZdog(Zdog.Polygon) -const Hemisphere = createZdog(Zdog.Hemisphere) -const Cylinder = createZdog(Zdog.Cylinder) -const Cone = createZdog(Zdog.Cone) -const Box = createZdog(Zdog.Box) - -export { - Illustration, - useRender, - useZdog, - Anchor, - Shape, - Group, - Rect, - RoundedRect, - Ellipse, - Polygon, - Hemisphere, - Cylinder, - Cone, - Box, -} diff --git a/index.tsx b/index.tsx new file mode 100644 index 0000000..896c96f --- /dev/null +++ b/index.tsx @@ -0,0 +1,216 @@ +import React, { + CSSProperties, + ForwardRefExoticComponent, + MutableRefObject, + PropsWithChildren, + ReactNode, + Ref, + RefAttributes, + useContext, + useEffect, + useImperativeHandle, + useLayoutEffect, + useRef, + useState, +} from 'react' +import ResizeObserver from 'resize-observer-polyfill' +import * as Zdog from 'zdog' + +interface Bounds { + left: number + top: number + width: number + height: number +} + +type UnsubscribeFn = () => void +interface ZdogState { + scene: Primitive + illu: Zdog.Illustration + size: Bounds | {} + subscribers: FrameRequestCallback[] + subscribe: (fn: FrameRequestCallback) => UnsubscribeFn +} + +type PrimitiveProps = PropsWithChildren[0]> + +const stateContext = React.createContext>(null) +const parentContext = React.createContext(null) + +let globalEffects: Function[] = [] +export function addEffect(callback) { + globalEffects.push(callback) +} + +export function invalidate(): void { + // TODO: render loop has to be able to render frames on demand +} + +export function applyProps(instance: Zdog.AnchorOptions, newProps: Zdog.AnchorOptions): void { + Zdog.extend(instance, newProps) + invalidate() +} + +function useMeasure(): [{ ref: MutableRefObject }, Bounds] { + const ref = useRef() + const [bounds, set] = useState({ left: 0, top: 0, width: 0, height: 0 }) + const [ro] = useState(() => new ResizeObserver(([entry]) => set(entry.contentRect))) + useEffect(() => { + if (ref.current) ro.observe(ref.current) + return () => ro.disconnect() + }, [ref.current]) + + return [{ ref }, bounds] +} + +function useRender(fn: FrameRequestCallback, deps: any[] = []) { + const state = useContext(stateContext) + useEffect(() => { + // Subscribe to the render-loop + const unsubscribe = state.current.subscribe(fn) + // Call subscription off on unmount + return () => unsubscribe() + }, deps) +} + +function useZdog() { + const state = useContext(stateContext) + return state.current +} + +function useZdogPrimitive( + primitive: Primitive, + children: ReactNode, + props?: PrimitiveProps, + ref?: Ref> +): [JSX.Element, InstanceType] { + const state = useContext(stateContext) + const parent = useContext(parentContext) + const [node] = useState(() => new primitive(props) as InstanceType) + + useImperativeHandle(ref, () => node) + useLayoutEffect(() => void applyProps(node, props), [props]) + useLayoutEffect(() => { + if (parent) { + parent.addChild(node) + state.current.illu.updateGraph() + + return () => { + parent.removeChild(node) + //@ts-ignore updateFlatGraph missing in zdog types + parent.updateFlatGraph() + state.current.illu.updateGraph() + } + } + }, [parent]) + return [, node] +} + +export type IllustrationProps = Omit & + PropsWithChildren<{ + style?: CSSProperties + element?: 'svg' | 'canvas' + }> + +const Illustration = React.memo( + ({ children, style, resize, element: Element = 'svg', dragRotate, ...rest }) => { + const canvas = useRef() + const [bind, size] = useMeasure() + const [result, scene] = useZdogPrimitive(Zdog.Anchor, children) + + const state = useRef({ + scene, + illu: undefined, + size: {}, + subscribers: [], + subscribe: fn => { + state.current.subscribers.push(fn) + return () => (state.current.subscribers = state.current.subscribers.filter(s => s !== fn)) + }, + }) + + useEffect(() => { + state.current.size = size + if (state.current.illu) state.current.illu.setSize(size.width, size.height) + }, [size]) + + useEffect(() => { + state.current.illu = new Zdog.Illustration({ element: canvas.current, dragRotate, ...rest }) + state.current.illu.addChild(scene) + state.current.illu.updateGraph() + + let frame + let active = true + const render: FrameRequestCallback = t => { + const { size, subscribers } = state.current + if (size.width && size.height) { + // Run global effects + globalEffects.forEach(fn => fn(t)) + // Run local effects + subscribers.forEach(fn => fn(t)) + // Render scene + state.current.illu.updateRenderGraph() + } + if (active) frame = requestAnimationFrame(render) + } + + // Start render loop + render(0) + + return () => { + // Take no chances, the loop has got to stop if the component unmounts + active = false + cancelAnimationFrame(frame) + } + }, []) + + // Takes care of updating the main illustration + useLayoutEffect(() => void (state.current.illu && applyProps(state.current.illu, rest)), [rest]) + + return ( +
+ + {state.current.illu && } +
+ ) + } +) +type ZdogComponent = ForwardRefExoticComponent< + Omit, 'addTo'> & RefAttributes> +> +const createZdog = (primitive: Primitive): ZdogComponent => + React.forwardRef, PrimitiveProps>( + ({ children, ...rest }, ref) => useZdogPrimitive(primitive, children, rest, ref)[0] + ) + +const Anchor = createZdog(Zdog.Anchor) +const Shape = createZdog(Zdog.Shape) +const Group = createZdog(Zdog.Group) +const Rect = createZdog(Zdog.Rect) +const RoundedRect = createZdog(Zdog.RoundedRect) +const Ellipse = createZdog(Zdog.Ellipse) +const Polygon = createZdog(Zdog.Polygon) +const Hemisphere = createZdog(Zdog.Hemisphere) +const Cylinder = createZdog(Zdog.Cylinder) +const Cone = createZdog(Zdog.Cone) +const Box = createZdog(Zdog.Box) + +export { + Illustration, + useRender, + useZdog, + Anchor, + Shape, + Group, + Rect, + RoundedRect, + Ellipse, + Polygon, + Hemisphere, + Cylinder, + Cone, + Box, +} diff --git a/package.json b/package.json index ed0155c..4004022 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,12 @@ "description": "React-fiber renderer for zdog", "main": "dist/index.cjs.js", "module": "dist/index.js", + "types": "dist/index.d.ts", + "typings": "dist/index.d.ts", "sideEffects": false, "scripts": { "prebuild": "rimraf dist", - "build": "rollup -c", + "build": "rollup -c rollup.config.ts && tsc", "prepare": "npm run build", "test": "echo \"Error: no test specified\" && exit 1" }, @@ -54,7 +56,6 @@ "zdog": ">=1.1" }, "devDependencies": { - "zdog": "^1.1.0", "@babel/core": "7.4.4", "@babel/plugin-proposal-class-properties": "7.4.4", "@babel/plugin-proposal-do-expressions": "7.2.0", @@ -68,6 +69,7 @@ "@babel/preset-typescript": "^7.3.3", "@types/lodash-es": "^4.17.3", "@types/react": "^16.8.15", + "@types/zdog": "^1.1.1", "babel-eslint": "^10.0.1", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "husky": "^2.1.0", @@ -79,6 +81,8 @@ "rollup-plugin-babel": "^4.3.2", "rollup-plugin-commonjs": "^9.3.4", "rollup-plugin-node-resolve": "^4.2.3", - "rollup-plugin-size-snapshot": "^0.8.0" + "rollup-plugin-size-snapshot": "^0.8.0", + "typescript": "^3.9.5", + "zdog": "^1.1.0" } } diff --git a/rollup.config.js b/rollup.config.ts similarity index 90% rename from rollup.config.js rename to rollup.config.ts index 5f50164..1e80556 100644 --- a/rollup.config.js +++ b/rollup.config.ts @@ -1,14 +1,13 @@ import path from 'path' import babel from 'rollup-plugin-babel' -import resolve from 'rollup-plugin-node-resolve' import commonjs from 'rollup-plugin-commonjs' -import { sizeSnapshot } from 'rollup-plugin-size-snapshot' +import resolve from 'rollup-plugin-node-resolve' const root = process.platform === 'win32' ? path.resolve('/') : '/' const external = id => !id.startsWith('.') && !id.startsWith(root) const extensions = ['.js', '.jsx', '.ts', '.tsx'] -const getBabelOptions = ({ useESModules }, targets) => ({ +const getBabelOptions = ({ useESModules }, targets = '') => ({ babelrc: false, extensions, //exclude: '**/node_modules/**', @@ -55,4 +54,4 @@ function createConfig(entry, out) { ] } -export default [...createConfig('index', 'index')] +export default [...createConfig('index.tsx', 'index')] diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..467fc7d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "dist" + }, + "exclude": ["node_modules"], + "include": ["index.tsx"] +} diff --git a/yarn.lock b/yarn.lock index 56d61c3..b7ca954 100644 --- a/yarn.lock +++ b/yarn.lock @@ -798,6 +798,11 @@ dependencies: "@types/node" "*" +"@types/zdog@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/zdog/-/zdog-1.1.1.tgz#62daf2340fa417caef3b40adb0c028c777206a0e" + integrity sha512-pEtwfB3RqQQVFf5Vk92mnVW9XRXUEx2HMItcfg+dSHgMPG4W7jmzDvPbPJYNK8qyZNZNnNWLAqbQpluvOy2Mng== + "@webassemblyjs/ast@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359" @@ -4069,6 +4074,11 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typescript@^3.9.5: + version "3.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.5.tgz#586f0dba300cde8be52dd1ac4f7e1009c1b13f36" + integrity sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ== + unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" From 28a4c0e45a0d9894f629ff43e83dc5723eea1895 Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Mon, 15 Jun 2020 12:38:31 +0200 Subject: [PATCH 2/4] Simplify tsconfig --- index.tsx | 14 +++++++------- tsconfig.json | 7 ------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/index.tsx b/index.tsx index 896c96f..0a00b75 100644 --- a/index.tsx +++ b/index.tsx @@ -27,7 +27,7 @@ type UnsubscribeFn = () => void interface ZdogState { scene: Primitive illu: Zdog.Illustration - size: Bounds | {} + size: Bounds subscribers: FrameRequestCallback[] subscribe: (fn: FrameRequestCallback) => UnsubscribeFn } @@ -38,7 +38,7 @@ const stateContext = React.createContext>(null) const parentContext = React.createContext(null) let globalEffects: Function[] = [] -export function addEffect(callback) { +export function addEffect(callback: Function): void { globalEffects.push(callback) } @@ -114,14 +114,14 @@ export type IllustrationProps = Omit( ({ children, style, resize, element: Element = 'svg', dragRotate, ...rest }) => { - const canvas = useRef() + const canvas = useRef() const [bind, size] = useMeasure() const [result, scene] = useZdogPrimitive(Zdog.Anchor, children) - const state = useRef({ + const state: MutableRefObject = useRef({ scene, illu: undefined, - size: {}, + size: null, subscribers: [], subscribe: fn => { state.current.subscribers.push(fn) @@ -139,11 +139,11 @@ const Illustration = React.memo( state.current.illu.addChild(scene) state.current.illu.updateGraph() - let frame + let frame: number let active = true const render: FrameRequestCallback = t => { const { size, subscribers } = state.current - if (size.width && size.height) { + if (size && size.width && size.height) { // Run global effects globalEffects.forEach(fn => fn(t)) // Run local effects diff --git a/tsconfig.json b/tsconfig.json index 467fc7d..58dec09 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,9 @@ { "compilerOptions": { - "target": "esnext", "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": false, - "forceConsistentCasingInFileNames": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, "jsx": "preserve", "declaration": true, "emitDeclarationOnly": true, From 773faf177b6513d1bf23c03278a10cf02452acd8 Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Mon, 15 Jun 2020 12:46:24 +0200 Subject: [PATCH 3/4] Remove unused generic --- index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/index.tsx b/index.tsx index 0a00b75..10f4cbd 100644 --- a/index.tsx +++ b/index.tsx @@ -24,8 +24,8 @@ interface Bounds { } type UnsubscribeFn = () => void -interface ZdogState { - scene: Primitive +interface ZdogState { + scene: Zdog.Anchor illu: Zdog.Illustration size: Bounds subscribers: FrameRequestCallback[] @@ -37,8 +37,8 @@ type PrimitiveProps = PropsWithChildren>(null) const parentContext = React.createContext(null) -let globalEffects: Function[] = [] -export function addEffect(callback: Function): void { +let globalEffects: FrameRequestCallback[] = [] +export function addEffect(callback: FrameRequestCallback): void { globalEffects.push(callback) } From 58a9e9e5c2025657e75d5a42c4c07d4fc023ec25 Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Thu, 18 Jun 2020 11:48:27 +0200 Subject: [PATCH 4/4] Update size snapshot --- .size-snapshot.json | 6 +++--- rollup.config.ts | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.size-snapshot.json b/.size-snapshot.json index aee764c..72beee1 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -14,8 +14,8 @@ } }, "dist/index.cjs.js": { - "bundled": 6218, - "minified": 3799, - "gzipped": 1371 + "bundled": 7065, + "minified": 4289, + "gzipped": 1465 } } diff --git a/rollup.config.ts b/rollup.config.ts index 1e80556..de2704d 100644 --- a/rollup.config.ts +++ b/rollup.config.ts @@ -2,6 +2,7 @@ import path from 'path' import babel from 'rollup-plugin-babel' import commonjs from 'rollup-plugin-commonjs' import resolve from 'rollup-plugin-node-resolve' +import { sizeSnapshot } from 'rollup-plugin-size-snapshot' const root = process.platform === 'win32' ? path.resolve('/') : '/' const external = id => !id.startsWith('.') && !id.startsWith(root) @@ -49,6 +50,7 @@ function createConfig(entry, out) { commonjs({ include: 'node_modules/**', }), + sizeSnapshot(), ], }, ]