- Notifications
You must be signed in to change notification settings - Fork 165
React Bridge for PaperJS Drawing Library #115
Description
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 :)