React Bridge for PaperJS Drawing Library
HriBB opened this issue · comments
Hi :)
I am trying to create a PaperJS
bridge similar to ReactARTFiber
. I have a few questions about the integration, and I thought this was the best place to ask them, since I basically copy-pasted the code for ReactARTFiber.js.
Note that I use a custom version of React 16.0.0-alpha.12, where I export ReactFiberReconciler
through react-dom
package.
I have a few problems specific to how PaperJS works. Also check out this issue
This is my PaperRenderer
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import paper from 'paper'
import invariant from 'fbjs/lib/invariant'
import emptyObject from 'fbjs/lib/emptyObject'
import { ReactFiberReconciler } from 'react-dom'
const TYPES = {
LAYER: 'Layer',
GROUP: 'Group',
PATH: 'Path',
CIRCLE: 'Circle',
TOOL: 'Tool',
}
const Layer = TYPES.LAYER
const Group = TYPES.GROUP
const Path = TYPES.PATH
const Circle = TYPES.CIRCLE
const Tool = TYPES.TOOL
class Paper extends Component {
static propTypes = {
activeTool: PropTypes.string,
height: PropTypes.number,
onWheel: PropTypes.func,
width: PropTypes.number,
zoom: PropTypes.number,
}
componentDidMount() {
const { activeTool, children, height, width, zoom } = this.props
this._paper = new paper.PaperScope()
this._paper.setup(this._canvas)
this._paper.view.viewSize = new paper.Size(width, height)
this._paper.view.zoom = zoom
this._mountNode = PaperRenderer.createContainer(this._paper)
PaperRenderer.updateContainer(
children,
this._mountNode,
this,
)
this._paper.view.draw()
if (activeTool) {
this._paper.tools.forEach(tool => {
if (tool.name === activeTool) {
tool.activate()
}
})
}
}
componentDidUpdate(prevProps, prevState) {
const { activeTool, children, height, width, zoom } = this.props
if (width !== prevProps.width || height !== prevProps.height) {
this._paper.view.viewSize = new paper.Size(width, height)
}
if (zoom !== prevProps.zoom) {
this._paper.view.zoom = zoom
}
PaperRenderer.updateContainer(
children,
this._mountNode,
this,
)
this._paper.view.draw()
if (activeTool !== prevProps.activeTool) {
this._paper.tools.forEach(tool => {
if (tool.name === activeTool) {
tool.activate()
}
})
}
}
componentWillUnmount() {
PaperRenderer.updateContainer(
null,
this._mountNode,
this,
)
}
render() {
const { height, onWheel, width } = this.props
const canvasProps = {
ref: ref => this._canvas = ref,
height,
onWheel,
width,
}
return (
<canvas {...canvasProps} />
)
}
}
function applyLayerProps(instance, props, prevProps = {}) {
// TODO
}
function applyToolProps(tool, props, prevProps = {}) {
// TODO
}
function applyGroupProps(tool, props, prevProps = {}) {
// TODO
}
function applyCircleProps(instance, props, prevProps = {}) {
if (props.center !== prevProps.center) {
instance.center = new paper.Point(props.center)
}
if (props.strokeColor !== prevProps.strokeColor) {
instance.strokeColor = props.strokeColor
}
if (props.strokeWidth !== prevProps.strokeWidth) {
instance.strokeWidth = props.strokeWidth
}
if (props.fillColor !== prevProps.fillColor) {
instance.fillColor = props.fillColor
}
}
function applyPathProps(instance, props, prevProps = {}) {
if (props.strokeColor !== prevProps.strokeColor) {
instance.strokeColor = props.strokeColor
}
if (props.strokeWidth !== prevProps.strokeWidth) {
instance.strokeWidth = props.strokeWidth
}
}
const PaperRenderer = ReactFiberReconciler({
appendChild(parentInstance, child) {
if (child.parentNode === parentInstance) {
child.remove()
}
if (
child instanceof paper.Path &&
(
parentInstance instanceof paper.Layer ||
parentInstance instanceof paper.Group
)
) {
child.addTo(parentInstance)
}
},
appendInitialChild(parentInstance, child) {
if (typeof child === 'string') {
// Noop for string children of Text (eg <Text>{'foo'}{'bar'}</Text>)
invariant(false, 'Text children should already be flattened.')
return
}
if (
child instanceof paper.Path &&
(
parentInstance instanceof paper.Layer ||
parentInstance instanceof paper.Group
)
) {
child.addTo(parentInstance)
}
},
commitTextUpdate(textInstance, oldText, newText) {
// Noop
},
commitMount(instance, type, newProps) {
// Noop
},
commitUpdate(instance, updatePayload, type, oldProps, newProps, internalInstanceHandle) {
instance._applyProps(instance, newProps, oldProps)
},
createInstance(type, props, internalInstanceHandle) {
const { children, ...paperProps } = props
let instance
switch (type) {
case TYPES.TOOL:
instance = new paper.Tool(paperProps)
instance._applyProps = applyToolProps
break
case TYPES.LAYER:
instance = new paper.Layer(paperProps)
instance._applyProps = applyLayerProps
break
case TYPES.GROUP:
instance = new paper.Group(paperProps)
instance._applyProps = applyGroupProps
break
case TYPES.PATH:
instance = new paper.Path(paperProps)
instance._applyProps = applyPathProps
break
case TYPES.CIRCLE:
instance = new paper.Path.Circle(paperProps)
instance._applyProps = applyCircleProps
break
}
invariant(instance, 'PaperReact does not support the type "%s"', type)
instance._applyProps(instance, props)
return instance
},
createTextInstance(text, rootContainerInstance, internalInstanceHandle) {
return text
},
finalizeInitialChildren(domElement, type, props) {
return false
},
insertBefore(parentInstance, child, beforeChild) {
invariant(
child !== beforeChild,
'PaperReact: Can not insert node before itself'
)
child.insertAbove(beforeChild)
},
prepareForCommit() {
// Noop
},
prepareUpdate(domElement, type, oldProps, newProps) {
return true
},
removeChild(parentInstance, child) {
//destroyEventListeners(child)
child.remove()
},
resetAfterCommit() {
// Noop
},
resetTextContent(domElement) {
// Noop
},
getRootHostContext() {
return emptyObject
},
getChildHostContext() {
return emptyObject
},
scheduleAnimationCallback: window.requestAnimationFrame,
scheduleDeferredCallback: window.requestIdleCallback,
shouldSetTextContent(props) {
return (
typeof props.children === 'string' ||
typeof props.children === 'number'
)
},
useSyncScheduling: true,
})
export {
Paper,
Layer,
Path,
Circle,
Group,
Tool,
}
This is my JSX structure
<Paper {...paperProps}>
<Layer>
<Path
segments={SEGMENTS}
strokeColor={strokeColor}
strokeScaling={false}
/>
<Group>
<Circle
center={[333,333]}
radius={20}
strokeColor={'black'}
fillColor={'green'}
strokeScaling={false}
/>
</Group>
</Layer>
<Layer>
<Path
dashArray={[6,4]}
segments={SEGMENTS2}
strokeColor={strokeColor}
strokeScaling={false}
/>
<Group>
<Circle
center={[464,444]}
radius={20}
strokeColor={'black'}
fillColor={'orange'}
strokeScaling={false}
/>
</Group>
</Layer>
<Layer>
{circles.map(circle =>
<Circle key={circle.id} {...circle} />
)}
</Layer>
<Tool
name={'move'}
onMouseDown={this.onMoveMouseDown}
onMouseDrag={this.onMoveMouseDrag}
onMouseUp={this.onMoveMouseUp}
/>
<Tool
name={'pen'}
onMouseDown={this.onPenMouseDown}
onMouseDrag={this.onPenMouseDrag}
onMouseUp={this.onPenMouseUp}
/>
</Paper>
Questions:
This might be a stupid question, but is there a way to reverse the process of calling createInstance? I would like to create parent instance before its children.
For example, this is the order in which PaperJS instances are created:
createInstance Path
createInstance Circle
createInstance Group
createInstance Layer
createInstance Path
createInstance Circle
createInstance Group
createInstance Layer
createInstance Circle (3x)
createInstance Layer
createInstance Tool (2x)
For example: even though Path
is a child of Layer
, its instance is created before Layer
. Problem is, when I create a new paper.Path
, if there is no paper.Layer
yet, PaperJS automatically creates one for me. So I end up with 4 paper.Layers
instead of 3.
Next problem I have is, when I change zoom
for example. My entire Paper tree is re-rendered, executing unnecessary calls to commitUpdate
, when all I need to do is set this._paper.view.zoom = zoom
in Paper
componentDidUpdate
. How can I optimize this? What is the right/best way? Basically I could completely skip this piece of code:
PaperRenderer.updateContainer(
children,
this._mountNode,
this,
)
I am also trying to figure out how to write tests. PaperJS supports node-canvas and offers import/export to SVG and JSON. Maybe I can use that.
Any other advice? I was looking at the react source code, but it's big, not really sure yet where to start :)