Skip to content
This repository was archived by the owner on Jul 19, 2019. It is now read-only.
This repository was archived by the owner on Jul 19, 2019. It is now read-only.

React Bridge for PaperJS Drawing Library  #115

@HriBB

Description

@HriBB

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 :)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions