Open Loosetooth opened 1 month ago
PaperJS uses "global" scope for the currently active canvas, so if you have multiple scopes, I think thats a problem, not Vite. I have a new, unpublished version locally, that works around this problem, but its hacky.
Thanks for the hint!
It indeed seems to be the case that some paper.js variables are not accessible somehow. (They're in another scope?)
Feel free to share your "hacky" solution if you can, I will try to look into it further as well.
I investigated a bit more, and indeed, there are two "javascript scopes" for some reason. Whatever that might mean exactly.
Taking that into account, I can work around the issue in the following way:
PaperScope
objectscope.setup(canvasRef)
function inside of the correct scope before handing it over to the Canvas
component.The following code gives an example, where I have a scope that I call .setup()
on BEFORE I give it to the Canvas
component:
import { useEffect, useRef, useState } from 'react';
import { Canvas, View, Layer, Circle } from 'react-paper-bindings';
import { Grid } from './Grid';
type Panel2DProps = {
scope: paper.PaperScope
}
export const Panel2D = (props: Panel2DProps) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [scope, setScope] = useState<paper.PaperScope | undefined>(undefined);
useEffect(() => {
if (!canvasRef.current) return;
console.log("Calling setup on the canvas");
props.scope.setup(canvasRef.current);
setScope(props.scope);
}, [props.scope, canvasRef.current]);
return (
<Canvas width={500} height={500} scope={scope} ref={canvasRef}>
<View>
{/* background layer */}
<Layer>
<Grid />
</Layer>
{/* path layer */}
<Layer>
<Circle center={[50, 50]} radius={10} fillColor="red" />
</Layer>
</View>
</Canvas>
);
}
And this renders the canvas with its content just fine.
Observations:
scope.setup(ref)
be called inside of the react-paper-bindings
package always results in the wrong scope being set up.setup
has been called), calling scope.setup()
again from the correct javascript scope does not help.Some ideas to move forwards with this:
scope.setup()
manually from the correct scope. (We could still default to the current scope.setup()
being called in the Canvas
component.)Canvas
component uses the same scope as the rest of the project.I guess 1. would be the easiest for me to implement at the moment, although I understand that this might be less ideal if it results in more complexity for the user. Currently I have no idea how or why there are multiple scopes in the sample Vite project.
Check out the v3 branch ;)
I think the insertItems: false
is the key.
https://github.com/react-paper/react-paper-bindings/blob/v3/src/Canvas.tsx#L55
But there are some "side effects", and I am not sure ATM what they are and how I solved it. Would need to dig through the code again ...
Here's my local v3 version. You can do a diff, to see if there are any changes.
I can publish v3 to npm, but to be honest, I think paper.js is not actively maintained anymore ... Lehni did publish v0.12.18 two months ago, but before that, 2 years nothing ...
Maybe something like https://pixijs.com/ or https://konvajs.org/ is a more future-proof solution.
Anyway, here's the code
Canvas.tsx
import React, {
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
} from 'react'
import { ConcurrentRoot } from 'react-reconciler/constants'
import { FiberRoot } from 'react-reconciler'
import paper from 'paper/dist/paper-core'
import { Renderer } from './Renderer'
export type PaperScopeSettings = {
insertItems?: boolean
applyMatrix?: boolean
handleSize?: number
hitTolerance?: number
}
export type CanvasProps = React.ComponentProps<'canvas'> & {
width: number
height: number
settings?: PaperScopeSettings
scope?: paper.PaperScope
onScopeReady?: (scope: paper.PaperScope) => void
}
export type CanvasRef = HTMLCanvasElement | null
export type ScopeRef = paper.PaperScope | null | undefined
export type FiberRef = FiberRoot | null
export const Canvas = forwardRef<CanvasRef, CanvasProps>(function Canvas(
{ children, width, height, settings, scope, onScopeReady, ...props },
forwardedRef
) {
const [canvas, setCanvas] = useState<CanvasRef>(null)
const canvasRef = useRef<CanvasRef>(null)
const scopeRef = useRef<ScopeRef>(scope)
const fiberRef = useRef<FiberRef>(null)
useImperativeHandle<CanvasRef, CanvasRef>(
forwardedRef,
() => canvasRef.current
)
// create
useEffect(() => {
if (canvas instanceof HTMLCanvasElement) {
if (!scopeRef.current) {
scopeRef.current = new paper.PaperScope()
}
Object.assign(scopeRef.current.settings, {
...settings,
insertItems: false,
})
scopeRef.current.setup(canvas)
fiberRef.current = Renderer.createContainer(
scopeRef.current,
ConcurrentRoot,
null,
false,
null,
'',
console.error,
null
)
Renderer.updateContainer(null, fiberRef.current, null, () => null)
if (typeof onScopeReady === 'function') {
onScopeReady(scopeRef.current)
}
} else if (canvasRef.current) {
setCanvas(canvasRef.current)
}
// destroy
return () => {
if (canvas) {
if (fiberRef.current) {
Renderer.updateContainer(null, fiberRef.current, null, () => null)
}
scopeRef.current = null
canvasRef.current = null
fiberRef.current = null
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [canvas])
// update
useEffect(() => {
if (canvas && fiberRef.current) {
Renderer.updateContainer(children, fiberRef.current, null, () => null)
}
}, [canvas, children])
// resize
useEffect(() => {
if (canvas && scopeRef.current && scopeRef.current.view) {
scopeRef.current.view.viewSize = new scopeRef.current.Size(width, height)
}
}, [canvas, width, height])
return <canvas {...props} ref={canvasRef} />
})
Renderer.injectIntoDevTools({
// @ts-ignore
findHostInstanceByFiber: () => null,
bundleType: process.env.NODE_ENV !== 'production' ? 1 : 0,
version: React.version,
rendererPackageName: 'react-konva',
})
Renderer.tsx
import Reconciler from 'react-reconciler'
import { DefaultEventPriority } from 'react-reconciler/constants'
import paper from 'paper/dist/paper-core'
import * as Item from './Items'
type Container = paper.PaperScope
type Instance =
| paper.View
| paper.Tool
| paper.Layer
| paper.Group
| paper.Path
| paper.CompoundPath
| paper.SymbolItem
| paper.PointText
| paper.Raster
type Type = keyof typeof Item
type Index<T> = { [key: string]: T }
type Props = Index<any>
type ApplyProp = (instance: Instance, props: Props, prev: Props) => void
const applyProp: Index<ApplyProp> = {
active: (instance, props) => {
if (
props.active &&
(instance instanceof paper.Tool || instance instanceof paper.Layer)
) {
instance.activate()
}
},
point: (instance, props, prev) => {
if (instance instanceof paper.Item) {
instance.translate([
props.point[0] - prev.point[0],
props.point[1] - prev.point[1],
])
}
},
center: (instance, props, prev) => {
if (instance instanceof paper.Item) {
instance.translate([
props.center[0] - prev.center[0],
props.center[1] - prev.center[1],
])
}
},
radius: (instance, props, prev) => {
if (instance instanceof paper.Item) {
instance.scale(props.radius / prev.radius)
}
},
rotation: (instance, props, prev) => {
if (instance instanceof paper.Item) {
if (props.rotation && prev.rotation) {
instance.rotate(props.rotation - prev.rotation)
} else {
instance.rotation = props.rotation
}
}
},
size: (instance, props, prev) => {
if (instance instanceof paper.Item) {
instance.scale(props.size[0] / prev.size[0], props.size[1] / prev.size[1])
}
},
visible: (instance, props) => {
if (instance instanceof paper.Item && props.visible !== instance.visible) {
instance.visible = props.visible
}
},
}
const applyProps = (
instance: Instance,
props: Props,
prevProps: Props = {}
) => {
const keys = Object.keys(props)
const len = keys.length
let i = 0
// https://stackoverflow.com/a/7252102
while (i < len) {
const prop = keys[i]
if (
prop !== 'id' &&
prop !== 'children' &&
props[prop] !== prevProps[prop]
) {
if (applyProp[prop]) {
applyProp[prop](instance, props, prevProps)
} else {
instance[prop as keyof typeof instance] = props[prop]
}
}
i++
}
}
const getSymbolDefinition = (scope: Container, { id, name, svg }: Props) => {
const key = id || name
if (!key) throw new Error('Missing id or name prop on SymbolItem')
if (!svg) throw new Error('Missing svg prop on SymbolItem')
// return cached definition
if (scope.symbols && scope.symbols[key]) {
return scope.symbols[key]
}
// create symbols cache
if (!scope.symbols) {
scope.symbols = {}
}
// create definition
const definition = new paper.SymbolDefinition(scope.project.importSVG(svg))
scope.symbols[key] = definition
// return created definition
return definition
}
export const Renderer = Reconciler({
createInstance: (type: Type, instanceProps: Props, scope: Container) => {
const { children, ...other } = instanceProps
const props: Props = { ...other, _project: scope.project }
let instance: Instance
// temporary paper fix for https://github.com/paperjs/paper.js/issues/2012
if (scope.project !== paper.project) {
scope.activate()
}
switch (type) {
case Item.View:
instance = scope.view
instance.project = scope.project
break
case Item.Tool:
instance = new scope.Tool()
break
case Item.Layer:
instance = new scope.Layer(props)
break
case Item.Group:
instance = new scope.Group(props)
break
case Item.Path:
instance = new scope.Path(props)
break
case Item.CompoundPath:
instance = new scope.CompoundPath(props)
break
case Item.Arc:
instance = new scope.Path.Arc(props)
break
case Item.Circle:
instance = new scope.Path.Circle(props)
break
case Item.Ellipse:
instance = new scope.Path.Ellipse(props)
break
case Item.Line:
instance = new scope.Path.Line(props)
break
case Item.Rectangle:
instance = new scope.Path.Rectangle(props)
break
case Item.RegularPolygon:
instance = new scope.Path.RegularPolygon(props)
break
case Item.PointText:
instance = new scope.PointText(props)
break
case Item.SymbolItem: {
const definition = getSymbolDefinition(scope, props)
instance = new scope.SymbolItem(definition, props.center)
break
}
case Item.Raster: {
instance = new scope.Raster(props)
break
}
default:
throw new Error(`PaperRenderer does not support the type "${type}"`)
}
instance.props = other
instance.type = type
return instance
},
createTextInstance: () => {
throw new Error('PaperRenderer does not support text children')
},
getPublicInstance: (instance: Instance) => instance,
prepareForCommit: () => null,
prepareUpdate: () => true,
resetAfterCommit: () => {},
resetTextContent: () => {},
getRootHostContext: () => null,
getChildHostContext: () => null,
shouldSetTextContent: () => false,
getCurrentEventPriority: () => DefaultEventPriority,
getInstanceFromNode: () => undefined,
getInstanceFromScope: () => undefined,
preparePortalMount: () => {},
prepareScopeUpdate: () => {},
beforeActiveInstanceBlur: () => {},
afterActiveInstanceBlur: () => {},
detachDeletedInstance: () => {},
clearContainer: () => {},
scheduleTimeout: setTimeout,
cancelTimeout: clearTimeout,
noTimeout: -1,
isPrimaryRenderer: false,
warnsIfNotActing: false,
supportsMutation: true,
supportsHydration: false,
supportsPersistence: false,
appendInitialChild: (parent: Instance, child: Instance) => {
if (parent instanceof paper.Group && child instanceof paper.Item) {
child.addTo(parent)
} else if (
parent instanceof paper.CompoundPath &&
child instanceof paper.Item
) {
child.addTo(parent)
} else if (parent instanceof paper.View && child instanceof paper.Item) {
child.addTo(parent.project)
} else {
throw new Error(`[react-paper-bindings] missing appendInitialChild logic`)
}
},
finalizeInitialChildren: (instance: Instance, _type: Type, props: Props) => {
if (
instance instanceof paper.View ||
instance instanceof paper.Tool ||
instance instanceof paper.Layer
) {
applyProps(instance, props)
}
return false
},
appendChild: (parent: Instance, child: Instance) => {
if (parent instanceof paper.Group && child instanceof paper.Item) {
child.addTo(parent)
} else if (parent instanceof paper.View && child instanceof paper.Item) {
child.addTo(parent.project)
} else {
throw new Error(`[react-paper-bindings] missing appendChild logic`)
}
},
appendChildToContainer: (_container: Container, child: Instance) => {
if (!(child instanceof paper.View || child instanceof paper.Tool)) {
throw new Error(
'[react-paper-bindings] Canvas can only hold View and Tool nodes'
)
}
},
insertBefore: (parent: Instance, child: Instance, before: Instance) => {
if (
parent instanceof paper.Group &&
child instanceof paper.Item &&
before instanceof paper.Item
) {
child.insertAbove(before)
} else if (
parent instanceof paper.View &&
child instanceof paper.Layer &&
before instanceof paper.Layer
) {
child.insertAbove(before)
} else {
throw new Error(`[react-paper-bindings] missing insertBefore logic`)
}
},
insertInContainerBefore: (
_container: Container,
child: Instance,
before: Instance
) => {
if (
!(child instanceof paper.View || child instanceof paper.Tool) ||
!(before instanceof paper.View || before instanceof paper.Tool)
) {
throw new Error(
`[react-paper-bindings] Canvas can only hold View and Tool nodes`
)
}
},
removeChild: (_parent: Instance, child: Instance) => {
if (typeof child.remove === 'function') {
child.remove()
} else {
throw new Error(`[react-paper-bindings] missing removeChild logic`)
}
},
removeChildFromContainer: (_container: Container, child: Instance) => {
if (typeof child.remove === 'function') {
child.remove()
} else {
throw new Error(
`[react-paper-bindings] missing removeChildFromContainer logic`
)
}
},
commitTextUpdate: () => {},
commitMount: () => {},
commitUpdate: (
instance: Instance,
_payload: unknown,
_type: Type,
oldProps: Props,
newProps: Props
) => {
applyProps(instance, newProps, oldProps)
},
})
Looking at your code above, I think the problem is, that at first render, the scope is undefined so paper.js uses its own internal scope or maybe even creates a new one ... Who knows :)
PaperJS was created before the "bundler era" and does not play well with webpack or vite. I asked Lehni about it, but he said that it would be too much work to "fix" this. Basically it would be easier to rewrite from scratch
I'm having some trouble getting
react-paper-bindings
to work with Vite.I made a simple test repo to reproduce the error.
It contains two canvases, one uses react-paper-bindings:
react-paper-binding Canvas:
This one does not render anything and throws the following error in the console:
I went digging a bit in the paper.js code, and it looks like it fails on this line. So
paper.project
seems to be null.Regular paper.js javascript Canvas
This one works fine.
Any ideas what is going wrong? Help is much appreciated.