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/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..10f4cbd --- /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: Zdog.Anchor + 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: FrameRequestCallback[] = [] +export function addEffect(callback: FrameRequestCallback): void { + 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: MutableRefObject = useRef({ + scene, + illu: undefined, + size: null, + 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: number + let active = true + const render: FrameRequestCallback = t => { + const { size, subscribers } = state.current + if (size && 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 91% rename from rollup.config.js rename to rollup.config.ts index 5f50164..de2704d 100644 --- a/rollup.config.js +++ b/rollup.config.ts @@ -1,14 +1,14 @@ import path from 'path' import babel from 'rollup-plugin-babel' -import resolve from 'rollup-plugin-node-resolve' 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) const extensions = ['.js', '.jsx', '.ts', '.tsx'] -const getBabelOptions = ({ useESModules }, targets) => ({ +const getBabelOptions = ({ useESModules }, targets = '') => ({ babelrc: false, extensions, //exclude: '**/node_modules/**', @@ -50,9 +50,10 @@ function createConfig(entry, out) { commonjs({ include: 'node_modules/**', }), + sizeSnapshot(), ], }, ] } -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..58dec09 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "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"