UNPKG

react-art

Version:

React ART is a JavaScript library for drawing vector graphics using React. It provides declarative and reactive bindings to the ART library. Using the same declarative API you can render the output to either Canvas, SVG or VML (IE8).

616 lines (516 loc) • 16.6 kB
/** * Copyright 2013-2014 Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule ReactART */ "use strict"; require('art/modes/fast'); // Flip this to DOM mode for debugging var Transform = require('art/core/transform'); var Mode = require('art/modes/current'); var React = require('react'); var ReactInstanceMap = require('react/lib/ReactInstanceMap'); var ReactMultiChild = require('react/lib/ReactMultiChild'); var ReactUpdates = require('react/lib/ReactUpdates'); var assign = require('react/lib/Object.assign'); var emptyObject = require('react/lib/emptyObject'); var pooledTransform = new Transform(); // Utilities function childrenAsString(children) { if (!children) { return ''; } if (typeof children === 'string') { return children; } if (children.length) { return children.join('\n'); } return ''; } function createComponent(name) { var ReactARTComponent = function(props) { this.node = null; this.subscriptions = null; this.listeners = null; this._mountImage = null; this._renderedChildren = null; this._mostRecentlyPlacedChild = null; }; ReactARTComponent.displayName = name; for (var i = 1, l = arguments.length; i < l; i++) { assign(ReactARTComponent.prototype, arguments[i]); } return ReactARTComponent; } // ContainerMixin for components that can hold ART nodes var ContainerMixin = assign({}, ReactMultiChild.Mixin, { /** * Moves a child component to the supplied index. * * @param {ReactComponent} child Component to move. * @param {number} toIndex Destination index of the element. * @protected */ moveChild: function(child, toIndex) { var childNode = child._mountImage; var mostRecentlyPlacedChild = this._mostRecentlyPlacedChild; if (mostRecentlyPlacedChild == null) { // I'm supposed to be first. if (childNode.previousSibling) { if (this.node.firstChild) { childNode.injectBefore(this.node.firstChild); } else { childNode.inject(this.node); } } } else { // I'm supposed to be after the previous one. if (mostRecentlyPlacedChild.nextSibling !== childNode) { if (mostRecentlyPlacedChild.nextSibling) { childNode.injectBefore(mostRecentlyPlacedChild.nextSibling); } else { childNode.inject(this.node); } } } this._mostRecentlyPlacedChild = childNode; }, /** * Creates a child component. * * @param {ReactComponent} child Component to create. * @param {object} childNode ART node to insert. * @protected */ createChild: function(child, childNode) { child._mountImage = childNode; var mostRecentlyPlacedChild = this._mostRecentlyPlacedChild; if (mostRecentlyPlacedChild == null) { // I'm supposed to be first. if (this.node.firstChild) { childNode.injectBefore(this.node.firstChild); } else { childNode.inject(this.node); } } else { // I'm supposed to be after the previous one. if (mostRecentlyPlacedChild.nextSibling) { childNode.injectBefore(mostRecentlyPlacedChild.nextSibling); } else { childNode.inject(this.node); } } this._mostRecentlyPlacedChild = childNode; }, /** * Removes a child component. * * @param {ReactComponent} child Child to remove. * @protected */ removeChild: function(child) { child._mountImage.eject(); child._mountImage = null; }, updateChildrenAtRoot: function(nextChildren, transaction) { this.updateChildren(nextChildren, transaction, emptyObject); }, mountAndInjectChildrenAtRoot: function(children, transaction) { this.mountAndInjectChildren(children, transaction, emptyObject); }, /** * Override to bypass batch updating because it is not necessary. * * @param {?object} nextChildren. * @param {ReactReconcileTransaction} transaction * @internal * @override {ReactMultiChild.Mixin.updateChildren} */ updateChildren: function(nextChildren, transaction, context) { this._mostRecentlyPlacedChild = null; this._updateChildren(nextChildren, transaction, context); }, // Shorthands mountAndInjectChildren: function(children, transaction, context) { var mountedImages = this.mountChildren( children, transaction, context ); // Each mount image corresponds to one of the flattened children var i = 0; for (var key in this._renderedChildren) { if (this._renderedChildren.hasOwnProperty(key)) { var child = this._renderedChildren[key]; child._mountImage = mountedImages[i]; mountedImages[i].inject(this.node); i++; } } } }); // Surface is a React DOM Component, not an ART component. It serves as the // entry point into the ART reconciler. var Surface = React.createClass({ displayName: 'Surface', mixins: [ContainerMixin], componentDidMount: function() { var domNode = this.getDOMNode(); this.node = Mode.Surface(+this.props.width, +this.props.height, domNode); var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(); transaction.perform( this.mountAndInjectChildren, this, this.props.children, transaction, ReactInstanceMap.get(this)._context ); ReactUpdates.ReactReconcileTransaction.release(transaction); }, componentDidUpdate: function(oldProps) { var node = this.node; if (this.props.width != oldProps.width || this.props.height != oldProps.height) { node.resize(+this.props.width, +this.props.height); } var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(); transaction.perform( this.updateChildren, this, this.props.children, transaction, ReactInstanceMap.get(this)._context ); ReactUpdates.ReactReconcileTransaction.release(transaction); if (node.render) { node.render(); } }, componentWillUnmount: function() { this.unmountChildren(); }, render: function() { // This is going to be a placeholder because we don't know what it will // actually resolve to because ART may render canvas, vml or svg tags here. // We only allow a subset of properties since others might conflict with // ART's properties. var props = this.props; // TODO: ART's Canvas Mode overrides surface title and cursor return ( React.createElement(Mode.Surface.tagName, { accesskey: props.accesskey, className: props.className, draggable: props.draggable, role: props.role, style: props.style, tabindex: props.tabindex, title: props.title} ) ); } }); // Various nodes that can go into a surface var EventTypes = { onMouseMove: 'mousemove', onMouseOver: 'mouseover', onMouseOut: 'mouseout', onMouseUp: 'mouseup', onMouseDown: 'mousedown', onClick: 'click' }; var NodeMixin = { construct: function(element) { this._currentElement = element; }, getPublicInstance: function() { return this.node; }, putEventListener: function(type, listener) { var subscriptions = this.subscriptions || (this.subscriptions = {}); var listeners = this.listeners || (this.listeners = {}); listeners[type] = listener; if (listener) { if (!subscriptions[type]) { subscriptions[type] = this.node.subscribe(type, listener, this); } } else { if (subscriptions[type]) { subscriptions[type](); delete subscriptions[type]; } } }, handleEvent: function(event) { var listener = this.listeners[event.type]; if (!listener) { return; } if (typeof listener === 'function') { listener.call(this, event); } else if (listener.handleEvent) { listener.handleEvent(event); } }, destroyEventListeners: function() { var subscriptions = this.subscriptions; if (subscriptions) { for (var type in subscriptions) { subscriptions[type](); } } this.subscriptions = null; this.listeners = null; }, applyNodeProps: function(oldProps, props) { var node = this.node; var scaleX = props.scaleX != null ? props.scaleX : props.scale != null ? props.scale : 1; var scaleY = props.scaleY != null ? props.scaleY : props.scale != null ? props.scale : 1; pooledTransform .transformTo(1, 0, 0, 1, 0, 0) .move(props.x || 0, props.y || 0) .rotate(props.rotation || 0, props.originX, props.originY) .scale(scaleX, scaleY, props.originX, props.originY); if (props.transform != null) { pooledTransform.transform(props.transform); } if (node.xx !== pooledTransform.xx || node.yx !== pooledTransform.yx || node.xy !== pooledTransform.xy || node.yy !== pooledTransform.yy || node.x !== pooledTransform.x || node.y !== pooledTransform.y) { node.transformTo(pooledTransform); } if (props.cursor !== oldProps.cursor || props.title !== oldProps.title) { node.indicate(props.cursor, props.title); } if (node.blend && props.opacity !== oldProps.opacity) { node.blend(props.opacity == null ? 1 : props.opacity); } if (props.visible !== oldProps.visible) { if (props.visible == null || props.visible) { node.show(); } else { node.hide(); } } for (var type in EventTypes) { this.putEventListener(EventTypes[type], props[type]); } }, mountComponentIntoNode: function(rootID, container) { throw new Error( 'You cannot render an ART component standalone. ' + 'You need to wrap it in a Surface.' ); } }; // Group var Group = createComponent('Group', NodeMixin, ContainerMixin, { mountComponent: function(rootID, transaction, context) { this.node = Mode.Group(); var props = this._currentElement.props; this.applyGroupProps(emptyObject, props); this.mountAndInjectChildren(props.children, transaction, context); return this.node; }, receiveComponent: function(nextComponent, transaction, context) { var props = nextComponent.props; var oldProps = this._currentElement.props; this.applyGroupProps(oldProps, props); this.updateChildren(props.children, transaction, context); this._currentElement = nextComponent; }, applyGroupProps: function(oldProps, props) { this.node.width = props.width; this.node.height = props.height; this.applyNodeProps(oldProps, props); }, unmountComponent: function() { this.destroyEventListeners(); this.unmountChildren(); } }); // ClippingRectangle var ClippingRectangle = createComponent( 'ClippingRectangle', NodeMixin, ContainerMixin, { mountComponent: function(rootID, transaction, context) { this.node = Mode.ClippingRectangle(); var props = this._currentElement.props; this.applyClippingProps(emptyObject, props); this.mountAndInjectChildren(props.children, transaction, context); return this.node; }, receiveComponent: function(nextComponent, transaction, context) { var props = nextComponent.props; var oldProps = this._currentElement.props; this.applyClippingProps(oldProps, props); this.updateChildren(props.children, transaction, context); this._currentElement = nextComponent; }, applyClippingProps: function(oldProps, props) { this.node.width = props.width; this.node.height = props.height; this.node.x = props.x; this.node.y = props.y; this.applyNodeProps(oldProps, props); }, unmountComponent: function() { this.destroyEventListeners(); this.unmountChildren(); } }); // Renderables var RenderableMixin = assign({}, NodeMixin, { applyRenderableProps: function(oldProps, props) { if (oldProps.fill !== props.fill) { if (props.fill && props.fill.applyFill) { props.fill.applyFill(this.node); } else { this.node.fill(props.fill); } } if ( oldProps.stroke !== props.stroke || oldProps.strokeWidth !== props.strokeWidth || oldProps.strokeCap !== props.strokeCap || oldProps.strokeJoin !== props.strokeJoin || // TODO: Consider a deep check of stokeDash. // This may benefit the VML version in IE. oldProps.strokeDash !== props.strokeDash ) { this.node.stroke( props.stroke, props.strokeWidth, props.strokeCap, props.strokeJoin, props.strokeDash ); } this.applyNodeProps(oldProps, props); }, unmountComponent: function() { this.destroyEventListeners(); } }); // Shape var Shape = createComponent('Shape', RenderableMixin, { construct: function(element) { this._currentElement = element; this._oldPath = null; }, mountComponent: function(rootID, transaction, context) { this.node = Mode.Shape(); var props = this._currentElement.props; this.applyShapeProps(emptyObject, props); return this.node; }, receiveComponent: function(nextComponent, transaction, context) { var props = nextComponent.props; var oldProps = this._currentElement.props; this.applyShapeProps(oldProps, props); this._currentElement = nextComponent; }, applyShapeProps: function(oldProps, props) { var oldPath = this._oldPath; var path = props.d || childrenAsString(props.children); if (path !== oldPath || oldProps.width !== props.width || oldProps.height !== props.height) { this.node.draw( path, props.width, props.height ); this._oldPath = path; } this.applyRenderableProps(oldProps, props); } }); // Text var Text = createComponent('Text', RenderableMixin, { construct: function(element) { this._currentElement = element; this._oldString = null; }, mountComponent: function(rootID, transaction, context) { var props = this._currentElement.props; var newString = childrenAsString(props.children); this.node = Mode.Text(newString, props.font, props.alignment, props.path); this._oldString = newString; this.applyRenderableProps(emptyObject, props); return this.node; }, isSameFont: function(oldFont, newFont) { if (oldFont === newFont) { return true; } if (typeof newFont === 'string' || typeof oldFont === 'string') { return false; } return ( newFont.fontSize === oldFont.fontSize && newFont.fontStyle === oldFont.fontStyle && newFont.fontVariant === oldFont.fontVariant && newFont.fontWeight === oldFont.fontWeight && newFont.fontFamily === oldFont.fontFamily ); }, receiveComponent: function(nextComponent, transaction, context) { var props = nextComponent.props; var oldProps = this._currentElement.props; var oldString = this._oldString; var newString = childrenAsString(props.children); if (oldString !== newString || !this.isSameFont(oldProps.font, props.font) || oldProps.alignment !== props.alignment || oldProps.path !== props.path) { this.node.draw( newString, props.font, props.alignment, props.path ); this._oldString = newString; } this.applyRenderableProps(oldProps, props); this._currentElement = nextComponent; } }); // Declarative fill type objects - API design not finalized var slice = Array.prototype.slice; function LinearGradient(stops, x1, y1, x2, y2) { this.args = slice.call(arguments); }; LinearGradient.prototype.applyFill = function(node) { node.fillLinear.apply(node, this.args); }; function RadialGradient(stops, fx, fy, rx, ry, cx, cy) { this.args = slice.call(arguments); }; RadialGradient.prototype.applyFill = function(node) { node.fillRadial.apply(node, this.args); }; function Pattern(url, width, height, left, top) { this.args = slice.call(arguments); }; Pattern.prototype.applyFill = function(node) { node.fillImage.apply(node, this.args); }; var ReactART = { LinearGradient: LinearGradient, RadialGradient: RadialGradient, Pattern: Pattern, Transform: Transform, Path: Mode.Path, Surface: Surface, Group: Group, ClippingRectangle: ClippingRectangle, Shape: Shape, Text: Text }; module.exports = ReactART;