reactjs / react-art

React Bridge to the ART Drawing Library

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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