Repository: Flipboard/react-canvas Branch: master Commit: 0b71180b4061 Files: 47 Total size: 137.9 KB Directory structure: gitextract_70rohpyf/ ├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── examples/ │ ├── common/ │ │ ├── data.js │ │ ├── examples.css │ │ └── touch-emulator.js │ ├── css-layout/ │ │ ├── app.js │ │ └── index.html │ ├── gradient/ │ │ ├── app.js │ │ └── index.html │ ├── listview/ │ │ ├── app.js │ │ ├── components/ │ │ │ └── Item.js │ │ └── index.html │ └── timeline/ │ ├── app.js │ ├── components/ │ │ └── Page.js │ └── index.html ├── gulpfile.js ├── lib/ │ ├── Canvas.js │ ├── CanvasUtils.js │ ├── ContainerMixin.js │ ├── DrawingUtils.js │ ├── Easing.js │ ├── EventTypes.js │ ├── FontFace.js │ ├── FontUtils.js │ ├── FrameUtils.js │ ├── Gradient.js │ ├── Group.js │ ├── Image.js │ ├── ImageCache.js │ ├── Layer.js │ ├── LayerMixin.js │ ├── Layout.js │ ├── ListView.js │ ├── ReactCanvas.js │ ├── RenderLayer.js │ ├── Surface.js │ ├── Text.js │ ├── __tests__/ │ │ └── clamp-test.js │ ├── clamp.js │ ├── createComponent.js │ ├── hitTest.js │ ├── layoutNode.js │ └── measureText.js ├── package.json └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": ["react"] } ================================================ FILE: .gitignore ================================================ build node_modules yarn.lock npm-debug.log ================================================ FILE: LICENSE ================================================ Copyright (c) 2015, Flipboard All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Flipboard nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ # react-canvas [Introductory blog post](http://engineering.flipboard.com/2015/02/mobile-web) React Canvas adds the ability for React components to render to `` rather than DOM. This project is a work-in-progress. Though much of the code is in production on flipboard.com, the React canvas bindings are relatively new and the API is subject to change. ## Motivation Having a long history of building interfaces geared toward mobile devices, we found that the reason mobile web apps feel slow when compared to native apps is the DOM. CSS animations and transitions are the fastest path to smooth animations on the web, but they have several limitations. React Canvas leverages the fact that most modern mobile browsers now have hardware accelerated canvas. While there have been other attempts to bind canvas drawing APIs to React, they are more focused on visualizations and games. Where React Canvas differs is in the focus on building application user interfaces. The fact that it renders to canvas is an implementation detail. React Canvas brings some of the APIs web developers are familiar with and blends them with a high performance drawing engine. ## Installation React Canvas is available through npm: ```npm install react-canvas``` ## React Canvas Components React Canvas provides a set of standard React components that abstract the underlying rendering implementation. ### <Surface> **Surface** is the top-level component. Think of it as a drawing canvas in which you can place other components. ### <Layer> **Layer** is the the base component by which other components build upon. Common styles and properties such as top, width, left, height, backgroundColor and zIndex are expressed at this level. ### <Group> **Group** is a container component. Because React enforces that all components return a single component in `render()`, Groups can be useful for parenting a set of child components. The Group is also an important component for optimizing scrolling performance, as it allows the rendering engine to cache expensive drawing operations. ### <Text> **Text** is a flexible component that supports multi-line truncation, something which has historically been difficult and very expensive to do in DOM. ### <Image> **Image** is exactly what you think it is. However, it adds the ability to hide an image until it is fully loaded and optionally fade it in on load. ### <Gradient> **Gradient** can be used to set the background of a group or surface. ```javascript render() { ... return ( ); } getGradientColors(){ return [ { color: "transparent", position: 0 }, { color: "#000", position: 1 } ] } ``` ### <ListView> **ListView** is a touch scrolling container that renders a list of elements in a column. Think of it like UITableView for the web. It leverages many of the same optimizations that make table views on iOS and list views on Android fast. ## Events React Canvas components support the same event model as normal React components. However, not all event types are currently supported. For a full list of supported events see [EventTypes](lib/EventTypes.js). ## Building Components Here is a very simple component that renders text below an image: ```javascript var React = require('react'); var ReactCanvas = require('react-canvas'); var Surface = ReactCanvas.Surface; var Image = ReactCanvas.Image; var Text = ReactCanvas.Text; var MyComponent = React.createClass({ render: function () { var surfaceWidth = window.innerWidth; var surfaceHeight = window.innerHeight; var imageStyle = this.getImageStyle(); var textStyle = this.getTextStyle(); return ( Here is some text below an image. ); }, getImageHeight: function () { return Math.round(window.innerHeight / 2); }, getImageStyle: function () { return { top: 0, left: 0, width: window.innerWidth, height: this.getImageHeight() }; }, getTextStyle: function () { return { top: this.getImageHeight() + 10, left: 0, width: window.innerWidth, height: 20, lineHeight: 20, fontSize: 12 }; } }); ``` ## ListView Many mobile interfaces involve an infinitely long scrolling list of items. React Canvas provides the ListView component to do just that. Because ListView virtualizes elements outside of the viewport, passing children to it is different than a normal React component where children are declared in render(). The `numberOfItemsGetter`, `itemHeightGetter` and `itemGetter` props are all required. ```javascript var ListView = ReactCanvas.ListView; var MyScrollingListView = React.createClass({ render: function () { return ( ); }, getNumberOfItems: function () { // Return the total number of items in the list }, getItemHeight: function () { // Return the height of a single item }, renderItem: function (index) { // Render the item at the given index, usually a }, }); ``` See the [timeline example](examples/timeline/app.js) for a more complete example. Currently, ListView requires that each item is of the same height. Future versions will support variable height items. ## Text sizing React Canvas provides the `measureText` function for computing text metrics. The [Page component](examples/timeline/components/Page.js) in the timeline example contains an example of using measureText to achieve precise multi-line ellipsized text. Custom fonts are not currently supported but will be added in a future version. ## css-layout There is experimental support for using [css-layout](https://github.com/facebook/css-layout) to style React Canvas components. This is a more expressive way of defining styles for a component using standard CSS styles and flexbox. Future versions may not support css-layout out of the box. The performance implications need to be investigated before baking this in as a core layout principle. See the [css-layout example](examples/css-layout). ## Accessibility This area needs further exploration. Using fallback content (the canvas DOM sub-tree) should allow screen readers such as VoiceOver to interact with the content. We've seen mixed results with the iOS devices we've tested. Additionally there is a standard for [focus management](http://www.w3.org/TR/2010/WD-2dcontext-20100304/#dom-context-2d-drawfocusring) that is not supported by browsers yet. One approach that was raised by [Bespin](http://vimeo.com/3195079) in 2009 is to keep a [parallel DOM](http://robertnyman.com/2009/04/03/mozilla-labs-online-code-editor-bespin/#comment-560310) in sync with the elements rendered in canvas. ## Running the examples ``` npm install npm start ``` This will start a live reloading server on port 8080. To override the default server and live reload ports, run `npm start` with PORT and/or RELOAD_PORT environment variables. **A note on NODE_ENV and React**: running the examples with `NODE_ENV=production` will noticeably improve scrolling performance. This is because React skips propType validation in production mode. ## Using with webpack The [brfs](https://github.com/substack/brfs) transform is required in order to use the project with webpack. ```bash npm install -g brfs npm install --save-dev transform-loader brfs ``` Then add the [brfs](https://github.com/substack/brfs) transform to your webpack config ```javascript module: { postLoaders: [ { loader: "transform?brfs" } ] } ``` ## Contributing We welcome pull requests for bug fixes, new features, and improvements to React Canvas. Contributors to the main repository must accept Flipboard's Apache-style [Individual Contributor License Agreement (CLA)](https://docs.google.com/forms/d/1gh9y6_i8xFn6pA15PqFeye19VqasuI9-bGp_e0owy74/viewform) before any changes can be merged. ================================================ FILE: examples/common/data.js ================================================ module.exports = [ { title: '10 Unbelievable Secrets That Will Make Your Airline Pilot Nervous', excerpt: 'With these words the Witch fell down in a brown, melted, shapeless mass and began to spread over the clean boards of the kitchen floor. Seeing that she had really melted away to nothing, Dorothy drew another bucket of water and threw it over the mess. She then swept it all out the door. After picking out the silver shoe, which was all that was left of the old woman, she cleaned and dried it with a cloth, and put it on her foot again. Then, being at last free to do as she chose, she ran out to the courtyard to tell the Lion that the Wicked Witch of the West had come to an end, and that they were no longer prisoners in a strange land.', imageUrl: 'https://placekitten.com/360/420' }, { title: 'Will Batman Save Leaf Blowing?', excerpt: 'The splendid fellow sprang to his feet, and grasping me by the shoulder raised his sword on high, exclaiming: "And had the choice been left to me I could not have chosen a more fitting mate for the first princess of Barsoom. Here is my hand upon your shoulder, John Carter, and my word that Sab Than shall go out at the point of my sword for the sake of my love for Helium, for Dejah Thoris, and for you. This very night I shall try to reach his quarters in the palace." "How?" I asked. "You are strongly guarded and a quadruple force patrols the sky." He bent his head in thought a moment, then raised it with an air of confidence.', imageUrl: 'https://placekitten.com/361/421' }, { title: '8 Scary Things Your Professor Is Using Against You', excerpt: 'For a minute he scarcely realised what this meant, and, although the heat was excessive, he clambered down into the pit close to the bulk to see the Thing more clearly. He fancied even then that the cooling of the body might account for this, but what disturbed that idea was the fact that the ash was falling only from the end of the cylinder. And then he perceived that, very slowly, the circular top of the cylinder was rotating on its body. It was such a gradual movement that he discovered it only through noticing that a black mark that had been near him five minutes ago was now at the other side of the circumference.', imageUrl: 'https://placekitten.com/362/422' }, { title: 'Kanye West\'s Top 10 Scandalous Microsoft Excel Secrets', excerpt: 'My wife was curiously silent throughout the drive, and seemed oppressed with forebodings of evil. I talked to her reassuringly, pointing out that the Martians were tied to the Pit by sheer heaviness, and at the utmost could but crawl a little out of it; but she answered only in monosyllables. Had it not been for my promise to the innkeeper, she would, I think, have urged me to stay in Leatherhead that night. Would that I had! Her face, I remember, was very white as we parted. For my own part, I had been feverishly excited all day.', imageUrl: 'https://placekitten.com/363/423' }, { title: 'The Embarassing Secrets Of Julia Roberts', excerpt: 'Passepartout heard the street door shut once; it was his new master going out. He heard it shut again; it was his predecessor, James Forster, departing in his turn. Passepartout remained alone in the house in Saville Row. "Faith," muttered Passepartout, somewhat flurried, "I\'ve seen people at Madame Tussaud\'s as lively as my new master!" Madame Tussaud\'s "people," let it be said, are of wax, and are much visited in London; speech is all that is wanting to make them human. During his brief interview with Mr. Fogg, Passepartout had been carefully observing him.', imageUrl: 'https://placekitten.com/364/424' }, { title: '20 Unbelievable Things Girlfriends Won\'t Tell Their Friends', excerpt: 'On March 3, 1866, Powell and I packed his provisions on two of our burros, and bidding me good-bye he mounted his horse, and started down the mountainside toward the valley, across which led the first stage of his journey. The morning of Powell\'s departure was, like nearly all Arizona mornings, clear and beautiful; I could see him and his little pack animals picking their way down the mountainside toward the valley, and all during the morning I would catch occasional glimpses of them as they topped a hog back or came out upon a level plateau.', imageUrl: 'https://placekitten.com/365/425' }, { title: 'Can Vladimir Putin Save Beard Care?', excerpt: 'So powerfully did the whole grim aspect of Ahab affect me, and the livid brand which streaked it, that for the first few moments I hardly noted that not a little of this overbearing grimness was owing to the barbaric white leg upon which he partly stood. It had previously come to me that this ivory leg had at sea been fashioned from the polished bone of the sperm whale\'s jaw. "Aye, he was dismasted off Japan," said the old Gay-Head Indian once; "but like his dismasted craft, he shipped another mast without coming home for it.', imageUrl: 'https://placekitten.com/366/426' }, { title: '15 Truths That Will Make Your Psychiatrist Feel Ashamed', excerpt: 'Again was I suddenly recalled to my immediate surroundings by a repetition of the weird moan from the depths of the cave. Naked and unarmed as I was, I had no desire to face the unseen thing which menaced me. My revolvers were strapped to my lifeless body which, for some unfathomable reason, I could not bring myself to touch. My carbine was in its boot, strapped to my saddle, and as my horse had wandered off I was left without means of defense. My only alternative seemed to lie in flight and my decision was crystallized by a recurrence of the rustling sound.', imageUrl: 'https://placekitten.com/367/427' }, { title: '6 Terrible Facts That Make Boyfriends Stronger', excerpt: 'First they came to a great hall in which were many ladies and gentlemen of the court, all dressed in rich costumes. These people had nothing to do but talk to each other, but they always came to wait outside the Throne Room every morning, although they were never permitted to see Oz. As Dorothy entered they looked at her curiously, and one of them whispered: "Are you really going to look upon the face of Oz the Terrible?" "Of course," answered the girl, "if he will see me." "Oh, he will see you," said the soldier who had taken her message to the Wizard.', imageUrl: 'https://placekitten.com/368/428' }, { title: '5 Surprising Dental Care Tips From Robert De Niro', excerpt: 'At once, with a quick mental leap, he linked the Thing with the flash upon Mars. The thought of the confined creature was so dreadful to him that he forgot the heat and went forward to the cylinder to help turn. But luckily the dull radiation arrested him before he could burn his hands on the still-glowing metal. At that he stood irresolute for a moment, then turned, scrambled out of the pit, and set off running wildly into Woking. The time then must have been somewhere about six o\'clock. He met a waggoner and tried to make him understand, but the tale he told and his appearance were so wild--his hat had fallen off in the pit--that the man simply drove on.', imageUrl: 'https://placekitten.com/369/429' }, ]; ================================================ FILE: examples/common/examples.css ================================================ html, body { margin: 0; padding: 0; font: 16px Helvetica, sans-serif; height: 100%; overflow: hidden; background: #ddd; } #main { background: #fff; position: relative; height: 100%; max-width: 420px; max-height: 700px; } ================================================ FILE: examples/common/touch-emulator.js ================================================ // https://github.com/hammerjs/touchemulator (function(window, document, exportName, undefined) { "use strict"; var isMultiTouch = false; var multiTouchStartPos; var eventTarget; var touchElements = {}; // polyfills if(!document.createTouch) { document.createTouch = function(view, target, identifier, pageX, pageY, screenX, screenY, clientX, clientY) { // auto set if(clientX == undefined || clientY == undefined) { clientX = pageX - window.pageXOffset; clientY = pageY - window.pageYOffset; } return new Touch(target, identifier, { pageX: pageX, pageY: pageY, screenX: screenX, screenY: screenY, clientX: clientX, clientY: clientY }); }; } if(!document.createTouchList) { document.createTouchList = function() { var touchList = new TouchList(); for (var i = 0; i < arguments.length; i++) { touchList[i] = arguments[i]; } touchList.length = arguments.length; return touchList; }; } /** * create an touch point * @constructor * @param target * @param identifier * @param pos * @param deltaX * @param deltaY * @returns {Object} touchPoint */ function Touch(target, identifier, pos, deltaX, deltaY) { deltaX = deltaX || 0; deltaY = deltaY || 0; this.identifier = identifier; this.target = target; this.clientX = pos.clientX + deltaX; this.clientY = pos.clientY + deltaY; this.screenX = pos.screenX + deltaX; this.screenY = pos.screenY + deltaY; this.pageX = pos.pageX + deltaX; this.pageY = pos.pageY + deltaY; } /** * create empty touchlist with the methods * @constructor * @returns touchList */ function TouchList() { var touchList = []; touchList.item = function(index) { return this[index] || null; }; // specified by Mozilla touchList.identifiedTouch = function(id) { return this[id + 1] || null; }; return touchList; } /** * Simple trick to fake touch event support * this is enough for most libraries like Modernizr and Hammer */ function fakeTouchSupport() { var objs = [window, document.documentElement]; var props = ['ontouchstart', 'ontouchmove', 'ontouchcancel', 'ontouchend']; for(var o=0; o 2; // pointer events } /** * disable mouseevents on the page * @param ev */ function preventMouseEvents(ev) { ev.preventDefault(); ev.stopPropagation(); } /** * only trigger touches when the left mousebutton has been pressed * @param touchType * @returns {Function} */ function onMouse(touchType) { return function(ev) { // prevent mouse events preventMouseEvents(ev); if (ev.which !== 1) { return; } // The EventTarget on which the touch point started when it was first placed on the surface, // even if the touch point has since moved outside the interactive area of that element. // also, when the target doesnt exist anymore, we update it if (ev.type == 'mousedown' || !eventTarget || (eventTarget && !eventTarget.dispatchEvent)) { eventTarget = ev.target; } // shiftKey has been lost, so trigger a touchend if (isMultiTouch && !ev.shiftKey) { triggerTouch('touchend', ev); isMultiTouch = false; } triggerTouch(touchType, ev); // we're entering the multi-touch mode! if (!isMultiTouch && ev.shiftKey) { isMultiTouch = true; multiTouchStartPos = { pageX: ev.pageX, pageY: ev.pageY, clientX: ev.clientX, clientY: ev.clientY, screenX: ev.screenX, screenY: ev.screenY }; triggerTouch('touchstart', ev); } // reset if (ev.type == 'mouseup') { multiTouchStartPos = null; isMultiTouch = false; eventTarget = null; } } } /** * trigger a touch event * @param eventName * @param mouseEv */ function triggerTouch(eventName, mouseEv) { var touchEvent = document.createEvent('Event'); touchEvent.initEvent(eventName, true, true); touchEvent.altKey = mouseEv.altKey; touchEvent.ctrlKey = mouseEv.ctrlKey; touchEvent.metaKey = mouseEv.metaKey; touchEvent.shiftKey = mouseEv.shiftKey; touchEvent.touches = getActiveTouches(mouseEv, eventName); touchEvent.targetTouches = getActiveTouches(mouseEv, eventName); touchEvent.changedTouches = getChangedTouches(mouseEv, eventName); eventTarget.dispatchEvent(touchEvent); } /** * create a touchList based on the mouse event * @param mouseEv * @returns {TouchList} */ function createTouchList(mouseEv) { var touchList = new TouchList(); if (isMultiTouch) { var f = TouchEmulator.multiTouchOffset; var deltaX = multiTouchStartPos.pageX - mouseEv.pageX; var deltaY = multiTouchStartPos.pageY - mouseEv.pageY; touchList.push(new Touch(eventTarget, 1, multiTouchStartPos, (deltaX*-1) - f, (deltaY*-1) + f)); touchList.push(new Touch(eventTarget, 2, multiTouchStartPos, deltaX+f, deltaY-f)); } else { touchList.push(new Touch(eventTarget, 1, mouseEv, 0, 0)); } return touchList; } /** * receive all active touches * @param mouseEv * @returns {TouchList} */ function getActiveTouches(mouseEv, eventName) { // empty list if (mouseEv.type == 'mouseup') { return new TouchList(); } var touchList = createTouchList(mouseEv); if(isMultiTouch && mouseEv.type != 'mouseup' && eventName == 'touchend') { touchList.splice(1, 1); } return touchList; } /** * receive a filtered set of touches with only the changed pointers * @param mouseEv * @param eventName * @returns {TouchList} */ function getChangedTouches(mouseEv, eventName) { var touchList = createTouchList(mouseEv); // we only want to return the added/removed item on multitouch // which is the second pointer, so remove the first pointer from the touchList // // but when the mouseEv.type is mouseup, we want to send all touches because then // no new input will be possible if(isMultiTouch && mouseEv.type != 'mouseup' && (eventName == 'touchstart' || eventName == 'touchend')) { touchList.splice(0, 1); } return touchList; } /** * show the touchpoints on the screen */ function showTouches(ev) { var touch, i, el, styles; // first all visible touches for(i = 0; i < ev.touches.length; i++) { touch = ev.touches[i]; el = touchElements[touch.identifier]; if(!el) { el = touchElements[touch.identifier] = document.createElement("div"); document.body.appendChild(el); } styles = TouchEmulator.template(touch); for(var prop in styles) { el.style[prop] = styles[prop]; } } // remove all ended touches if(ev.type == 'touchend' || ev.type == 'touchcancel') { for(i = 0; i < ev.changedTouches.length; i++) { touch = ev.changedTouches[i]; el = touchElements[touch.identifier]; if(el) { el.parentNode.removeChild(el); delete touchElements[touch.identifier]; } } } } /** * TouchEmulator initializer */ function TouchEmulator() { if (hasTouchSupport()) { return; } fakeTouchSupport(); window.addEventListener("mousedown", onMouse('touchstart'), true); window.addEventListener("mousemove", onMouse('touchmove'), true); window.addEventListener("mouseup", onMouse('touchend'), true); window.addEventListener("mouseenter", preventMouseEvents, true); window.addEventListener("mouseleave", preventMouseEvents, true); window.addEventListener("mouseout", preventMouseEvents, true); window.addEventListener("mouseover", preventMouseEvents, true); // it uses itself! window.addEventListener("touchstart", showTouches, false); window.addEventListener("touchmove", showTouches, false); window.addEventListener("touchend", showTouches, false); window.addEventListener("touchcancel", showTouches, false); } // start distance when entering the multitouch mode TouchEmulator.multiTouchOffset = 75; /** * css template for the touch rendering * @param touch * @returns object */ TouchEmulator.template = function(touch) { var size = 30; var transform = 'translate('+ (touch.clientX-(size/2)) +'px, '+ (touch.clientY-(size/2)) +'px)'; return { position: 'fixed', left: 0, top: 0, background: '#fff', border: 'solid 1px #999', opacity: .6, borderRadius: '100%', height: size + 'px', width: size + 'px', padding: 0, margin: 0, display: 'block', overflow: 'hidden', pointerEvents: 'none', webkitUserSelect: 'none', mozUserSelect: 'none', userSelect: 'none', webkitTransform: transform, mozTransform: transform, transform: transform } }; // export if (typeof define == "function" && define.amd) { define(function() { return TouchEmulator; }); } else if (typeof module != "undefined" && module.exports) { module.exports = TouchEmulator; } else { window[exportName] = TouchEmulator; } })(window, document, "TouchEmulator"); ================================================ FILE: examples/css-layout/app.js ================================================ var React = require('react'); var ReactDOM = require('react-dom'); var ReactCanvas = require('react-canvas'); var Surface = ReactCanvas.Surface; var Group = ReactCanvas.Group; var Image = ReactCanvas.Image; var Text = ReactCanvas.Text; var FontFace = ReactCanvas.FontFace; var App = React.createClass({ componentDidMount: function () { window.addEventListener('resize', this.handleResize, true); }, render: function () { var size = this.getSize(); return ( Professor PuddinPop With these words the Witch fell down in a brown, melted, shapeless mass and began to spread over the clean boards of the kitchen floor. Seeing that she had really melted away to nothing, Dorothy drew another bucket of water and threw it over the mess. She then swept it all out the door. After picking out the silver shoe, which was all that was left of the old woman, she cleaned and dried it with a cloth, and put it on her foot again. Then, being at last free to do as she chose, she ran out to the courtyard to tell the Lion that the Wicked Witch of the West had come to an end, and that they were no longer prisoners in a strange land. ); }, // Styles // ====== getSize: function () { return document.getElementById('main').getBoundingClientRect(); }, getPageStyle: function () { var size = this.getSize(); return { position: 'relative', padding: 14, width: size.width, height: size.height, backgroundColor: '#f7f7f7', flexDirection: 'column' }; }, getImageGroupStyle: function () { return { position: 'relative', flex: 1, backgroundColor: '#eee' }; }, getImageStyle: function () { return { position: 'absolute', left: 0, top: 0, right: 0, bottom: 0 }; }, getTitleStyle: function () { return { fontFace: FontFace('Georgia'), fontSize: 22, lineHeight: 28, height: 28, marginBottom: 10, color: '#333', textAlign: 'center' }; }, getExcerptStyle: function () { return { fontFace: FontFace('Georgia'), fontSize: 17, lineHeight: 25, marginTop: 15, flex: 1, color: '#333' }; }, // Events // ====== handleResize: function () { this.forceUpdate(); } }); ReactDOM.render(, document.getElementById('main')); ================================================ FILE: examples/css-layout/index.html ================================================ ReactCanvas: css-layout
================================================ FILE: examples/gradient/app.js ================================================ 'use strict'; var React = require('react'); var ReactDOM = require('react-dom'); var ReactCanvas = require('react-canvas'); var Gradient = ReactCanvas.Gradient; var Surface = ReactCanvas.Surface; var App = React.createClass({ render: function () { var size = this.getSize(); return ( ); }, getGradientStyle: function(){ var size = this.getSize(); return { top: 0, left: 0, width: size.width, height: size.height }; }, getGradientColors: function(){ return [ { color: "transparent", position: 0 }, { color: "#000", position: 1 } ]; }, getSize: function () { return document.getElementById('main').getBoundingClientRect(); } }); ReactDOM.render(, document.getElementById('main')); ================================================ FILE: examples/gradient/index.html ================================================ ReactCanvas: ListView
================================================ FILE: examples/listview/app.js ================================================ 'use strict'; var React = require('react'); var ReactDOM = require('react-dom'); var ReactCanvas = require('react-canvas'); var Item = require('./components/Item'); var articles = require('../common/data'); var Surface = ReactCanvas.Surface; var ListView = ReactCanvas.ListView; var App = React.createClass({ render: function () { var size = this.getSize(); return ( ); }, renderItem: function (itemIndex, scrollTop) { var article = articles[itemIndex % articles.length]; return ( ); }, getSize: function () { return document.getElementById('main').getBoundingClientRect(); }, // ListView // ======== getListViewStyle: function () { return { top: 0, left: 0, width: window.innerWidth, height: window.innerHeight }; }, getNumberOfItems: function () { return 1000; }, }); ReactDOM.render(, document.getElementById('main')); ================================================ FILE: examples/listview/components/Item.js ================================================ 'use strict'; var React = require('react'); var ReactCanvas = require('react-canvas'); var Group = ReactCanvas.Group; var Image = ReactCanvas.Image; var Text = ReactCanvas.Text; var Item = React.createClass({ propTypes: { width: React.PropTypes.number.isRequired, height: React.PropTypes.number.isRequired, imageUrl: React.PropTypes.string.isRequired, title: React.PropTypes.string.isRequired, itemIndex: React.PropTypes.number.isRequired, }, statics: { getItemHeight: function () { return 80; } }, render: function () { return ( {this.props.title} ); }, getStyle: function () { return { width: this.props.width, height: Item.getItemHeight(), backgroundColor: (this.props.itemIndex % 2) ? '#eee' : '#a5d2ee' }; }, getImageStyle: function () { return { top: 10, left: 10, width: 60, height: 60, backgroundColor: '#ddd', borderColor: '#999', borderWidth: 1 }; }, getTitleStyle: function () { return { top: 32, left: 80, width: this.props.width - 90, height: 18, fontSize: 14, lineHeight: 18 }; } }); module.exports = Item; ================================================ FILE: examples/listview/index.html ================================================ ReactCanvas: ListView
================================================ FILE: examples/timeline/app.js ================================================ 'use strict'; var React = require('react'); var ReactDOM = require('react-dom'); var ReactCanvas = require('react-canvas'); var Page = require('./components/Page'); var articles = require('../common/data'); var Surface = ReactCanvas.Surface; var ListView = ReactCanvas.ListView; var App = React.createClass({ render: function () { var size = this.getSize(); return ( ); }, renderPage: function (pageIndex, scrollTop) { var size = this.getSize(); var article = articles[pageIndex % articles.length]; var pageScrollTop = pageIndex * this.getPageHeight() - scrollTop; return ( ); }, getSize: function () { return document.getElementById('main').getBoundingClientRect(); }, // ListView // ======== getListViewStyle: function () { var size = this.getSize(); return { top: 0, left: 0, width: size.width, height: size.height }; }, getNumberOfPages: function () { return 1000; }, getPageHeight: function () { return this.getSize().height; } }); ReactDOM.render(, document.getElementById('main')); ================================================ FILE: examples/timeline/components/Page.js ================================================ 'use strict'; var React = require('react'); var ReactCanvas = require('react-canvas'); var Group = ReactCanvas.Group; var Image = ReactCanvas.Image; var Text = ReactCanvas.Text; var FontFace = ReactCanvas.FontFace; var measureText = ReactCanvas.measureText; var CONTENT_INSET = 14; var TEXT_SCROLL_SPEED_MULTIPLIER = 0.6; var TEXT_ALPHA_SPEED_OUT_MULTIPLIER = 1.25; var TEXT_ALPHA_SPEED_IN_MULTIPLIER = 2.6; var IMAGE_LAYER_INDEX = 2; var TEXT_LAYER_INDEX = 1; var Page = React.createClass({ propTypes: { width: React.PropTypes.number.isRequired, height: React.PropTypes.number.isRequired, article: React.PropTypes.object.isRequired, scrollTop: React.PropTypes.number.isRequired }, componentWillMount: function () { // Pre-compute headline/excerpt text dimensions. var article = this.props.article; var maxWidth = this.props.width - 2 * CONTENT_INSET; var titleStyle = this.getTitleStyle(); var excerptStyle = this.getExcerptStyle(); this.titleMetrics = measureText(article.title, maxWidth, titleStyle.fontFace, titleStyle.fontSize, titleStyle.lineHeight); this.excerptMetrics = measureText(article.excerpt, maxWidth, excerptStyle.fontFace, excerptStyle.fontSize, excerptStyle.lineHeight); }, render: function () { var groupStyle = this.getGroupStyle(); var imageStyle = this.getImageStyle(); var titleStyle = this.getTitleStyle(); var excerptStyle = this.getExcerptStyle(); // Layout title and excerpt below image. titleStyle.height = this.titleMetrics.height; excerptStyle.top = titleStyle.top + titleStyle.height + CONTENT_INSET; excerptStyle.height = this.props.height - excerptStyle.top - CONTENT_INSET; return ( {this.props.article.title} {this.props.article.excerpt} ); }, // Styles // ====== getGroupStyle: function () { return { top: 0, left: 0, width: this.props.width, height: this.props.height, }; }, getImageHeight: function () { return Math.round(this.props.height * 0.5); }, getImageStyle: function () { return { top: 0, left: 0, width: this.props.width, height: this.getImageHeight(), backgroundColor: '#eee', zIndex: IMAGE_LAYER_INDEX }; }, getTitleStyle: function () { return { top: this.getImageHeight() + CONTENT_INSET, left: CONTENT_INSET, width: this.props.width - 2 * CONTENT_INSET, fontSize: 22, lineHeight: 30, fontFace: FontFace('Avenir Next Condensed, Helvetica, sans-serif', null, {weight: 500}) }; }, getExcerptStyle: function () { return { left: CONTENT_INSET, width: this.props.width - 2 * CONTENT_INSET, fontFace: FontFace('Georgia, serif'), fontSize: 15, lineHeight: 23 }; }, getTextGroupStyle: function () { var imageHeight = this.getImageHeight(); var translateY = 0; var alphaMultiplier = (this.props.scrollTop <= 0) ? -TEXT_ALPHA_SPEED_OUT_MULTIPLIER : TEXT_ALPHA_SPEED_IN_MULTIPLIER; var alpha = 1 - (this.props.scrollTop / this.props.height) * alphaMultiplier; alpha = Math.min(Math.max(alpha, 0), 1); translateY = -this.props.scrollTop * TEXT_SCROLL_SPEED_MULTIPLIER; return { width: this.props.width, height: this.props.height - imageHeight, top: imageHeight, left: 0, alpha: alpha, translateY: translateY, zIndex: TEXT_LAYER_INDEX }; } }); module.exports = Page; ================================================ FILE: examples/timeline/index.html ================================================ ReactCanvas: Timeline
================================================ FILE: gulpfile.js ================================================ var gulp = require('gulp'); var del = require('del'); var connect = require('gulp-connect'); var webpack = require('webpack-stream'); var webpackConfig = require('./webpack.config.js'); var port = process.env.PORT || 8080; var reloadPort = process.env.RELOAD_PORT || 35729; gulp.task('clean', function () { del(['build']); }); gulp.task('build', function () { return gulp.src(webpackConfig.entry.timeline[0]) .pipe(webpack(webpackConfig)) .pipe(gulp.dest('build/')); }); gulp.task('serve', function () { connect.server({ port: port, livereload: { port: reloadPort } }); }); gulp.task('reload-js', function () { return gulp.src('./build/*.js') .pipe(connect.reload()); }); gulp.task('watch', function () { gulp.watch(['./build/*.js'], ['reload-js']); }); gulp.task('default', ['clean', 'build', 'serve', 'watch']); ================================================ FILE: lib/Canvas.js ================================================ 'use strict'; // Note that this class intentionally does not use PooledClass. // DrawingUtils manages pooling for more fine-grained control. function Canvas (width, height, scale) { // Re-purposing an existing canvas element. if (!this._canvas) { this._canvas = document.createElement('canvas'); } this.width = width; this.height = height; this.scale = scale || window.devicePixelRatio; this._canvas.width = this.width * this.scale; this._canvas.height = this.height * this.scale; this._canvas.getContext('2d').scale(this.scale, this.scale); } Object.assign(Canvas.prototype, { getRawCanvas: function () { return this._canvas; }, getContext: function () { return this._canvas.getContext('2d'); } }); // PooledClass: // Be fairly conserative - we are potentially drawing a large number of medium // to large size images. Canvas.poolSize = 30; module.exports = Canvas; ================================================ FILE: lib/CanvasUtils.js ================================================ 'use strict'; var FontFace = require('./FontFace'); var clamp = require('./clamp'); var measureText = require('./measureText'); /** * Draw an image into a . This operation requires that the image * already be loaded. * * @param {CanvasContext} ctx * @param {Image} image The source image (from ImageCache.get()) * @param {Number} x The x-coordinate to begin drawing * @param {Number} y The y-coordinate to begin drawing * @param {Number} width The desired width * @param {Number} height The desired height * @param {Object} options Available options are: * {Number} originalWidth * {Number} originalHeight * {Object} focusPoint {x,y} * {String} backgroundColor */ function drawImage (ctx, image, x, y, width, height, options) { options = options || {}; if (options.backgroundColor) { ctx.save(); ctx.fillStyle = options.backgroundColor; ctx.fillRect(x, y, width, height); ctx.restore(); } var dx = 0; var dy = 0; var dw = 0; var dh = 0; var sx = 0; var sy = 0; var sw = 0; var sh = 0; var scale; var scaledSize; var actualSize; var focusPoint = options.focusPoint; actualSize = { width: image.getWidth(), height: image.getHeight() }; scale = Math.max( width / actualSize.width, height / actualSize.height ) || 1; scale = parseFloat(scale.toFixed(4), 10); scaledSize = { width: actualSize.width * scale, height: actualSize.height * scale }; if (focusPoint) { // Since image hints are relative to image "original" dimensions (original != actual), // use the original size for focal point cropping. if (options.originalHeight) { focusPoint.x *= (actualSize.height / options.originalHeight); focusPoint.y *= (actualSize.height / options.originalHeight); } } else { // Default focal point to [0.5, 0.5] focusPoint = { x: actualSize.width * 0.5, y: actualSize.height * 0.5 }; } // Clip the image to rectangle (sx, sy, sw, sh). sx = Math.round(clamp(width * 0.5 - focusPoint.x * scale, width - scaledSize.width, 0)) * (-1 / scale); sy = Math.round(clamp(height * 0.5 - focusPoint.y * scale, height - scaledSize.height, 0)) * (-1 / scale); sw = Math.round(actualSize.width - (sx * 2)); sh = Math.round(actualSize.height - (sy * 2)); // Scale the image to dimensions (dw, dh). dw = Math.round(width); dh = Math.round(height); // Draw the image on the canvas at coordinates (dx, dy). dx = Math.round(x); dy = Math.round(y); ctx.drawImage(image.getRawImage(), sx, sy, sw, sh, dx, dy, dw, dh); } /** * @param {CanvasContext} ctx * @param {String} text The text string to render * @param {Number} x The x-coordinate to begin drawing * @param {Number} y The y-coordinate to begin drawing * @param {Number} width The maximum allowed width * @param {Number} height The maximum allowed height * @param {FontFace} fontFace The FontFace to to use * @param {Object} options Available options are: * {Number} fontSize * {Number} lineHeight * {String} textAlign * {String} color * {String} backgroundColor */ function drawText (ctx, text, x, y, width, height, fontFace, options) { var textMetrics; var currX = x; var currY = y; var currText; var options = options || {}; options.fontSize = options.fontSize || 16; options.lineHeight = options.lineHeight || 18; options.textAlign = options.textAlign || 'left'; options.backgroundColor = options.backgroundColor || 'transparent'; options.color = options.color || '#000'; textMetrics = measureText( text, width, fontFace, options.fontSize, options.lineHeight ); ctx.save(); // Draw the background if (options.backgroundColor !== 'transparent') { ctx.fillStyle = options.backgroundColor; ctx.fillRect(0, 0, width, height); } ctx.fillStyle = options.color; ctx.font = fontFace.attributes.style + ' ' + fontFace.attributes.weight + ' ' + options.fontSize + 'px ' + fontFace.family; textMetrics.lines.forEach(function (line, index) { currText = line.text; currY = (index === 0) ? y + options.fontSize : (y + options.fontSize + options.lineHeight * index); // Account for text-align: left|right|center switch (options.textAlign) { case 'center': currX = x + (width / 2) - (line.width / 2); break; case 'right': currX = x + width - line.width; break; default: currX = x; } if ((index < textMetrics.lines.length - 1) && ((options.fontSize + options.lineHeight * (index + 1)) > height)) { currText = currText.replace(/\,?\s?\w+$/, '…'); } if (currY <= (height + y)) { ctx.fillText(currText, currX, currY); } }); ctx.restore(); } /** * Draw a linear gradient * * @param {CanvasContext} ctx * @param {Number} x1 gradient start-x coordinate * @param {Number} y1 gradient start-y coordinate * @param {Number} x2 gradient end-x coordinate * @param {Number} y2 gradient end-y coordinate * @param {Array} colorStops Array of {(String)color, (Number)position} values * @param {Number} x x-coordinate to begin fill * @param {Number} y y-coordinate to begin fill * @param {Number} width how wide to fill * @param {Number} height how tall to fill */ function drawGradient(ctx, x1, y1, x2, y2, colorStops, x, y, width, height) { var grad; ctx.save(); grad = ctx.createLinearGradient(x1, y1, x2, y2); colorStops.forEach(function (colorStop) { grad.addColorStop(colorStop.position, colorStop.color); }); ctx.fillStyle = grad; ctx.fillRect(x, y, width, height); ctx.restore(); } module.exports = { drawImage: drawImage, drawText: drawText, drawGradient: drawGradient, }; ================================================ FILE: lib/ContainerMixin.js ================================================ 'use strict'; // Adapted from ReactART: // https://github.com/reactjs/react-art var React = require('react'); var ReactMultiChild = require('react-dom/lib/ReactMultiChild'); var emptyObject = require('fbjs/lib/emptyObject'); var ContainerMixin = Object.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, afterNode, toIndex, lastIndex) { 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, afterNode, 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.remove(); child._mountImage = null; this.node.invalidateLayout(); }, 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++; } } }, getHostNode: function () { return this.node }, getNativeNode: function () { return this.node }, }); module.exports = ContainerMixin; ================================================ FILE: lib/DrawingUtils.js ================================================ 'use strict'; var ImageCache = require('./ImageCache'); var FontUtils = require('./FontUtils'); var FontFace = require('./FontFace'); var FrameUtils = require('./FrameUtils'); var CanvasUtils = require('./CanvasUtils'); var Canvas = require('./Canvas'); // Global backing store cache var _backingStores = []; /** * Maintain a cache of backing for RenderLayer's which are accessible * through the RenderLayer's `backingStoreId` property. * * @param {String} id The unique `backingStoreId` for a RenderLayer * @return {HTMLCanvasElement} */ function getBackingStore (id) { for (var i=0, len=_backingStores.length; i < len; i++) { if (_backingStores[i].id === id) { return _backingStores[i].canvas; } } return null; } /** * Purge a layer's backing store from the cache. * * @param {String} id The layer's backingStoreId */ function invalidateBackingStore (id) { for (var i=0, len=_backingStores.length; i < len; i++) { if (_backingStores[i].id === id) { _backingStores.splice(i, 1); break; } } } /** * Purge the entire backing store cache. */ function invalidateAllBackingStores () { _backingStores = []; } /** * Find the nearest backing store ancestor for a given layer. * * @param {RenderLayer} layer */ function getBackingStoreAncestor (layer) { while (layer) { if (layer.backingStoreId) { return layer; } layer = layer.parentLayer; } return null; } /** * Check if a layer is using a given image URL. * * @param {RenderLayer} layer * @param {String} imageUrl * @return {Boolean} */ function layerContainsImage (layer, imageUrl) { // Check the layer itself. if (layer.type === 'image' && layer.imageUrl === imageUrl) { return layer; } // Check the layer's children. if (layer.children) { for (var i=0, len=layer.children.length; i < len; i++) { if (layerContainsImage(layer.children[i], imageUrl)) { return layer.children[i]; } } } return false; } /** * Check if a layer is using a given FontFace. * * @param {RenderLayer} layer * @param {FontFace} fontFace * @return {Boolean} */ function layerContainsFontFace (layer, fontFace) { // Check the layer itself. if (layer.type === 'text' && layer.fontFace && layer.fontFace.id === fontFace.id) { return layer; } // Check the layer's children. if (layer.children) { for (var i=0, len=layer.children.length; i < len; i++) { if (layerContainsFontFace(layer.children[i], fontFace)) { return layer.children[i]; } } } return false; } /** * Invalidates the backing stores for layers which contain an image layer * associated with the given imageUrl. * * @param {String} imageUrl */ function handleImageLoad (imageUrl) { _backingStores.forEach(function (backingStore) { if (layerContainsImage(backingStore.layer, imageUrl)) { invalidateBackingStore(backingStore.id); } }); } /** * Invalidates the backing stores for layers which contain a text layer * associated with the given font face. * * @param {FontFace} fontFace */ function handleFontLoad (fontFace) { _backingStores.forEach(function (backingStore) { if (layerContainsFontFace(backingStore.layer, fontFace)) { invalidateBackingStore(backingStore.id); } }); } /** * Draw a RenderLayer instance to a context. * * @param {CanvasRenderingContext2d} ctx * @param {RenderLayer} layer */ function drawRenderLayer (ctx, layer) { var customDrawFunc; // Performance: avoid drawing hidden layers. if (typeof layer.alpha === 'number' && layer.alpha <= 0) { return; } switch (layer.type) { case 'image': customDrawFunc = drawImageRenderLayer; break; case 'text': customDrawFunc = drawTextRenderLayer; break; case 'gradient': customDrawFunc = drawGradientRenderLayer; break; } // Establish drawing context for certain properties: // - alpha // - translate var saveContext = (layer.alpha !== null && layer.alpha < 1) || (layer.translateX || layer.translateY); if (saveContext) { ctx.save(); // Alpha: if (layer.alpha !== null && layer.alpha < 1) { ctx.globalAlpha = layer.alpha; } // Translation: if (layer.translateX || layer.translateY) { ctx.translate(layer.translateX || 0, layer.translateY || 0); } } // If the layer is bitmap-cacheable, draw in a pooled off-screen canvas. // We disable backing stores on pad since we flip there. if (layer.backingStoreId) { drawCacheableRenderLayer(ctx, layer, customDrawFunc); } else { // Draw default properties, such as background color. ctx.save(); drawBaseRenderLayer(ctx, layer); // Draw custom properties if needed. customDrawFunc && customDrawFunc(ctx, layer); ctx.restore(); // Draw child layers, sorted by their z-index. if (layer.children) { layer.children.slice().sort(sortByZIndexAscending).forEach(function (childLayer) { drawRenderLayer(ctx, childLayer); }); } } // Pop the context state if we established a new drawing context. if (saveContext) { ctx.restore(); } } /** * Draw base layer properties into a rendering context. * NOTE: The caller is responsible for calling save() and restore() as needed. * * @param {CanvasRenderingContext2d} ctx * @param {RenderLayer} layer */ function drawBaseRenderLayer (ctx, layer) { var frame = layer.frame; // Border radius: if (layer.borderRadius) { ctx.beginPath(); ctx.moveTo(frame.x + layer.borderRadius, frame.y); ctx.arcTo(frame.x + frame.width, frame.y, frame.x + frame.width, frame.y + frame.height, layer.borderRadius); ctx.arcTo(frame.x + frame.width, frame.y + frame.height, frame.x, frame.y + frame.height, layer.borderRadius); ctx.arcTo(frame.x, frame.y + frame.height, frame.x, frame.y, layer.borderRadius); ctx.arcTo(frame.x, frame.y, frame.x + frame.width, frame.y, layer.borderRadius); ctx.closePath(); // Create a clipping path when drawing an image or using border radius. if (layer.type === 'image') { ctx.clip(); } // Border with border radius: if (layer.borderColor) { ctx.lineWidth = layer.borderWidth || 1; ctx.strokeStyle = layer.borderColor; ctx.stroke(); } } // Border color (no border radius): if (layer.borderColor && !layer.borderRadius) { ctx.lineWidth = layer.borderWidth || 1; ctx.strokeStyle = layer.borderColor; ctx.strokeRect(frame.x, frame.y, frame.width, frame.height); } // Shadow: ctx.shadowBlur = layer.shadowBlur; ctx.shadowColor = layer.shadowColor; ctx.shadowOffsetX = layer.shadowOffsetX; ctx.shadowOffsetY = layer.shadowOffsetY; // Background color: if (layer.backgroundColor) { ctx.fillStyle = layer.backgroundColor; if (layer.borderRadius) { // Fill the current path when there is a borderRadius set. ctx.fill(); } else { ctx.fillRect(frame.x, frame.y, frame.width, frame.height); } } } /** * Draw a bitmap-cacheable layer into a pooled . The result will be * drawn into the given context. This will populate the layer backing store * cache with the result. * * @param {CanvasRenderingContext2d} ctx * @param {RenderLayer} layer * @param {Function} customDrawFunc * @private */ function drawCacheableRenderLayer (ctx, layer, customDrawFunc) { // See if there is a pre-drawn canvas in the pool. var backingStore = getBackingStore(layer.backingStoreId); var backingStoreScale = layer.scale || window.devicePixelRatio; var frameOffsetY = layer.frame.y; var frameOffsetX = layer.frame.x; var backingContext; if (!backingStore) { if (_backingStores.length >= Canvas.poolSize) { // Re-use the oldest backing store once we reach the pooling limit. backingStore = _backingStores[0].canvas; Canvas.call(backingStore, layer.frame.width, layer.frame.height, backingStoreScale); // Move the re-use canvas to the front of the queue. _backingStores[0].id = layer.backingStoreId; _backingStores[0].canvas = backingStore; _backingStores.push(_backingStores.shift()); } else { // Create a new backing store, we haven't yet reached the pooling limit backingStore = new Canvas(layer.frame.width, layer.frame.height, backingStoreScale); _backingStores.push({ id: layer.backingStoreId, layer: layer, canvas: backingStore }); } // Draw into the backing at (0, 0) - we will later use the // to draw the layer as an image at the proper coordinates. backingContext = backingStore.getContext('2d'); layer.translate(-frameOffsetX, -frameOffsetY); // Draw default properties, such as background color. backingContext.save(); drawBaseRenderLayer(backingContext, layer); // Custom drawing operations customDrawFunc && customDrawFunc(backingContext, layer); backingContext.restore(); // Draw child layers, sorted by their z-index. if (layer.children) { layer.children.slice().sort(sortByZIndexAscending).forEach(function (childLayer) { drawRenderLayer(backingContext, childLayer); }); } // Restore layer's original frame. layer.translate(frameOffsetX, frameOffsetY); } // We have the pre-rendered canvas ready, draw it into the destination canvas. if (layer.clipRect) { // Fill the clipping rect in the destination canvas. var sx = (layer.clipRect.x - layer.frame.x) * backingStoreScale; var sy = (layer.clipRect.y - layer.frame.y) * backingStoreScale; var sw = layer.clipRect.width * backingStoreScale; var sh = layer.clipRect.height * backingStoreScale; var dx = layer.clipRect.x; var dy = layer.clipRect.y; var dw = layer.clipRect.width; var dh = layer.clipRect.height; // No-op for zero size rects. iOS / Safari will throw an exception. if (sw > 0 && sh > 0) { ctx.drawImage(backingStore.getRawCanvas(), sx, sy, sw, sh, dx, dy, dw, dh); } } else { // Fill the entire canvas ctx.drawImage(backingStore.getRawCanvas(), layer.frame.x, layer.frame.y, layer.frame.width, layer.frame.height); } } /** * @private */ function sortByZIndexAscending (layerA, layerB) { return (layerA.zIndex || 0) - (layerB.zIndex || 0); } /** * @private */ function drawImageRenderLayer (ctx, layer) { if (!layer.imageUrl) { return; } // Don't draw until loaded var image = ImageCache.get(layer.imageUrl); if (!image.isLoaded()) { return; } CanvasUtils.drawImage(ctx, image, layer.frame.x, layer.frame.y, layer.frame.width, layer.frame.height); } /** * @private */ function drawTextRenderLayer (ctx, layer) { // Fallback to standard font. var fontFace = layer.fontFace || FontFace.Default(); // Don't draw text until loaded if (!FontUtils.isFontLoaded(fontFace)) { return; } CanvasUtils.drawText(ctx, layer.text, layer.frame.x, layer.frame.y, layer.frame.width, layer.frame.height, fontFace, { fontSize: layer.fontSize, lineHeight: layer.lineHeight, textAlign: layer.textAlign, color: layer.color }); } /** * @private */ function drawGradientRenderLayer (ctx, layer) { // Default to linear gradient from top to bottom. var x1 = layer.x1 || layer.frame.x; var y1 = layer.y1 || layer.frame.y; var x2 = layer.x2 || layer.frame.x; var y2 = layer.y2 || layer.frame.y + layer.frame.height; CanvasUtils.drawGradient(ctx, x1, y1, x2, y2, layer.colorStops, layer.frame.x, layer.frame.y, layer.frame.width, layer.frame.height); } module.exports = { drawRenderLayer: drawRenderLayer, invalidateBackingStore: invalidateBackingStore, invalidateAllBackingStores: invalidateAllBackingStores, handleImageLoad: handleImageLoad, handleFontLoad: handleFontLoad, layerContainsImage: layerContainsImage, layerContainsFontFace: layerContainsFontFace }; ================================================ FILE: lib/Easing.js ================================================ // Penner easing equations // https://gist.github.com/gre/1650294 var Easing = { linear: function (t) { return t; }, easeInQuad: function (t) { return Math.pow(t, 2); }, easeOutQuad: function (t) { return t * (2-t); }, easeInOutQuad: function (t) { return t < .5 ? 2 * t * t : -1 + (4 - 2 * t) * t; }, easeInCubic: function (t) { return t * t * t; }, easeOutCubic: function (t) { return (--t) * t * t + 1; }, easeInOutCubic: function (t) { return t < .5 ? 4 * t * t * t : (t-1) * (2*t - 2) * (2*t - 2) + 1; } }; module.exports = Easing; ================================================ FILE: lib/EventTypes.js ================================================ 'use strict'; // Supported events that RenderLayer's can subscribe to. module.exports = { onTouchStart: 'touchstart', onTouchMove: 'touchmove', onTouchEnd: 'touchend', onTouchCancel: 'touchcancel', onClick: 'click', onContextMenu: 'contextmenu', onDoubleClick: 'dblclick' }; ================================================ FILE: lib/FontFace.js ================================================ 'use strict'; var _fontFaces = {}; /** * @param {String} family The CSS font-family value * @param {String} url The remote URL for the font file * @param {Object} attributes Font attributes supported: style, weight * @return {Object} */ function FontFace (family, url, attributes) { var fontFace; var fontId; attributes = attributes || {}; attributes.style = attributes.style || 'normal'; attributes.weight = attributes.weight || 400; fontId = getCacheKey(family, url, attributes); fontFace = _fontFaces[fontId]; if (!fontFace) { fontFace = {}; fontFace.id = fontId; fontFace.family = family; fontFace.url = url; fontFace.attributes = attributes; _fontFaces[fontId] = fontFace; } return fontFace; } /** * Helper for retrieving the default family by weight. * * @param {Number} fontWeight * @return {FontFace} */ FontFace.Default = function (fontWeight) { return FontFace('sans-serif', null, {weight: fontWeight}); }; /** * @internal */ function getCacheKey (family, url, attributes) { return family + url + Object.keys(attributes).sort().map(function (key) { return attributes[key]; }); } module.exports = FontFace; ================================================ FILE: lib/FontUtils.js ================================================ 'use strict'; var FontFace = require('./FontFace'); var _useNativeImpl = (typeof window.FontFace !== 'undefined'); var _pendingFonts = {}; var _loadedFonts = {}; var _failedFonts = {}; var kFontLoadTimeout = 3000; /** * Check if a font face has loaded * @param {FontFace} fontFace * @return {Boolean} */ function isFontLoaded (fontFace) { // For remote URLs, check the cache. System fonts (sans url) assume loaded. return _loadedFonts[fontFace.id] !== undefined || !fontFace.url; } /** * Load a remote font and execute a callback. * @param {FontFace} fontFace The font to Load * @param {Function} callback Function executed upon font Load */ function loadFont (fontFace, callback) { var defaultNode; var testNode; var checkFont; // See if we've previously loaded it. if (_loadedFonts[fontFace.id]) { return callback(null); } // See if we've previously failed to load it. if (_failedFonts[fontFace.id]) { return callback(_failedFonts[fontFace.id]); } // System font: assume already loaded. if (!fontFace.url) { return callback(null); } // Font load is already in progress: if (_pendingFonts[fontFace.id]) { _pendingFonts[fontFace.id].callbacks.push(callback); return; } // Create the test 's for measuring. defaultNode = createTestNode('Helvetica', fontFace.attributes); testNode = createTestNode(fontFace.family, fontFace.attributes); document.body.appendChild(testNode); document.body.appendChild(defaultNode); _pendingFonts[fontFace.id] = { startTime: Date.now(), defaultNode: defaultNode, testNode: testNode, callbacks: [callback] }; // Font watcher checkFont = function () { var currWidth = testNode.getBoundingClientRect().width; var defaultWidth = defaultNode.getBoundingClientRect().width; var loaded = currWidth !== defaultWidth; if (loaded) { handleFontLoad(fontFace, null); } else { // Timeout? if (Date.now() - _pendingFonts[fontFace.id].startTime >= kFontLoadTimeout) { handleFontLoad(fontFace, true); } else { requestAnimationFrame(checkFont); } } }; // Start watching checkFont(); } // Internal // ======== /** * Native FontFace loader implementation * @internal */ function loadFontNative (fontFace, callback) { var theFontFace; // See if we've previously loaded it. if (_loadedFonts[fontFace.id]) { return callback(null); } // See if we've previously failed to load it. if (_failedFonts[fontFace.id]) { return callback(_failedFonts[fontFace.id]); } // System font: assume it's installed. if (!fontFace.url) { return callback(null); } // Font load is already in progress: if (_pendingFonts[fontFace.id]) { _pendingFonts[fontFace.id].callbacks.push(callback); return; } _pendingFonts[fontFace.id] = { startTime: Date.now(), callbacks: [callback] }; // Use font loader API theFontFace = new window.FontFace(fontFace.family, 'url(' + fontFace.url + ')', fontFace.attributes); theFontFace.load().then(function () { _loadedFonts[fontFace.id] = true; callback(null); }, function (err) { _failedFonts[fontFace.id] = err; callback(err); }); } /** * Helper method for created a hidden with a given font. * Uses TypeKit's default test string, which is said to result * in highly varied measured widths when compared to the default font. * @internal */ function createTestNode (family, attributes) { var span = document.createElement('span'); span.setAttribute('data-fontfamily', family); span.style.cssText = 'position:absolute; left:-5000px; top:-5000px; visibility:hidden;' + 'font-size:100px; font-family:"' + family + '", Helvetica;font-weight: ' + attributes.weight + ';' + 'font-style:' + attributes.style + ';'; span.innerHTML = 'BESs'; return span; } /** * @internal */ function handleFontLoad (fontFace, timeout) { var error = timeout ? 'Exceeded load timeout of ' + kFontLoadTimeout + 'ms' : null; if (!error) { _loadedFonts[fontFace.id] = true; } else { _failedFonts[fontFace.id] = error; } // Execute pending callbacks. _pendingFonts[fontFace.id].callbacks.forEach(function (callback) { callback(error); }); // Clean up DOM if (_pendingFonts[fontFace.id].defaultNode) { document.body.removeChild(_pendingFonts[fontFace.id].defaultNode); } if (_pendingFonts[fontFace.id].testNode) { document.body.removeChild(_pendingFonts[fontFace.id].testNode); } // Clean up waiting queue delete _pendingFonts[fontFace.id]; } module.exports = { isFontLoaded: isFontLoaded, loadFont: _useNativeImpl ? loadFontNative : loadFont }; ================================================ FILE: lib/FrameUtils.js ================================================ 'use strict'; function Frame (x, y, width, height) { this.x = x; this.y = y; this.width = width; this.height = height; } /** * Get a frame object * * @param {Number} x * @param {Number} y * @param {Number} width * @param {Number} height * @return {Frame} */ function make (x, y, width, height) { return new Frame(x, y, width, height); } /** * Return a zero size anchored at (0, 0). * * @return {Frame} */ function zero () { return make(0, 0, 0, 0); } /** * Return a cloned frame * * @param {Frame} frame * @return {Frame} */ function clone (frame) { return make(frame.x, frame.y, frame.width, frame.height); } /** * Creates a new frame by a applying edge insets. This method accepts CSS * shorthand notation e.g. inset(myFrame, 10, 0); * * @param {Frame} frame * @param {Number} top * @param {Number} right * @param {?Number} bottom * @param {?Number} left * @return {Frame} */ function inset (frame, top, right, bottom, left) { var frameCopy = clone(frame); // inset(myFrame, 10, 0) => inset(myFrame, 10, 0, 10, 0) if (typeof bottom === 'undefined') { bottom = top; left = right; } // inset(myFrame, 10) => inset(myFrame, 10, 10, 10, 10) if (typeof right === 'undefined') { right = bottom = left = top; } frameCopy.x += left; frameCopy.y += top; frameCopy.height -= (top + bottom); frameCopy.width -= (left + right); return frameCopy; } /** * Compute the intersection region between 2 frames. * * @param {Frame} frame * @param {Frame} otherFrame * @return {Frame} */ function intersection (frame, otherFrame) { var x = Math.max(frame.x, otherFrame.x); var width = Math.min(frame.x + frame.width, otherFrame.x + otherFrame.width); var y = Math.max(frame.y, otherFrame.y); var height = Math.min(frame.y + frame.height, otherFrame.y + otherFrame.height); if (width >= x && height >= y) { return make(x, y, width - x, height - y); } return null; } /** * Compute the union of two frames * * @param {Frame} frame * @param {Frame} otherFrame * @return {Frame} */ function union (frame, otherFrame) { var x1 = Math.min(frame.x, otherFrame.x); var x2 = Math.max(frame.x + frame.width, otherFrame.x + otherFrame.width); var y1 = Math.min(frame.y, otherFrame.y); var y2 = Math.max(frame.y + frame.height, otherFrame.y + otherFrame.height); return make(x1, y1, x2 - x1, y2 - y1); } /** * Determine if 2 frames intersect each other * * @param {Frame} frame * @param {Frame} otherFrame * @return {Boolean} */ function intersects (frame, otherFrame) { return !(otherFrame.x > frame.x + frame.width || otherFrame.x + otherFrame.width < frame.x || otherFrame.y > frame.y + frame.height || otherFrame.y + otherFrame.height < frame.y); } module.exports = { make: make, zero: zero, clone: clone, inset: inset, intersection: intersection, intersects: intersects, union: union }; ================================================ FILE: lib/Gradient.js ================================================ 'use strict'; var React = require('react'); var createComponent = require('./createComponent'); var LayerMixin = require('./LayerMixin'); var Gradient = createComponent('Gradient', LayerMixin, { applyGradientProps: function (prevProps, props) { var layer = this.node; layer.type = 'gradient'; layer.colorStops = props.colorStops || []; this.applyLayerProps(prevProps, props); }, mountComponent: function ( transaction, nativeParent, nativeContainerInfo, context ) { var props = this._currentElement.props; var layer = this.node; this.applyGradientProps({}, props); return layer; }, receiveComponent: function (nextComponent, transaction, context) { var prevProps = this._currentElement.props; var props = nextComponent.props; this.applyGradientProps({}, props); this._currentElement = nextComponent; this.node.invalidateLayout(); }, }); module.exports = Gradient; ================================================ FILE: lib/Group.js ================================================ 'use strict'; var createComponent = require('./createComponent'); var ContainerMixin = require('./ContainerMixin'); var LayerMixin = require('./LayerMixin'); var RenderLayer = require('./RenderLayer'); var Group = createComponent('Group', LayerMixin, ContainerMixin, { mountComponent: function ( transaction, nativeParent, nativeContainerInfo, context ) { var props = this._currentElement.props; var layer = this.node; this.applyLayerProps({}, props); this.mountAndInjectChildren(props.children, transaction, context); return layer; }, receiveComponent: function (nextComponent, transaction, context) { var props = nextComponent.props; var prevProps = this._currentElement.props; this.applyLayerProps(prevProps, props); this.updateChildren(props.children, transaction, context); this._currentElement = nextComponent; this.node.invalidateLayout(); }, unmountComponent: function () { LayerMixin.unmountComponent.call(this); this.unmountChildren(); } }); module.exports = Group; ================================================ FILE: lib/Image.js ================================================ 'use strict'; var React = require('react'); var createComponent = require('./createComponent'); var LayerMixin = require('./LayerMixin'); var Layer = require('./Layer'); var Group = require('./Group'); var ImageCache = require('./ImageCache'); var Easing = require('./Easing'); var clamp = require('./clamp'); var FADE_DURATION = 200; var RawImage = createComponent('Image', LayerMixin, { applyImageProps: function (prevProps, props) { var layer = this.node; layer.type = 'image'; layer.imageUrl = props.src; }, mountComponent: function ( transaction, nativeParent, nativeContainerInfo, context ) { var props = this._currentElement.props; var layer = this.node; this.applyLayerProps({}, props); this.applyImageProps({}, props); return layer; }, receiveComponent: function (nextComponent, transaction, context) { var prevProps = this._currentElement.props; var props = nextComponent.props; this.applyLayerProps(prevProps, props); this.applyImageProps(prevProps, props); this._currentElement = nextComponent; this.node.invalidateLayout(); }, }); var Image = React.createClass({ propTypes: { src: React.PropTypes.string.isRequired, style: React.PropTypes.object, useBackingStore: React.PropTypes.bool, fadeIn: React.PropTypes.bool, fadeInDuration: React.PropTypes.number }, getInitialState: function () { var loaded = ImageCache.get(this.props.src).isLoaded(); return { loaded: loaded, imageAlpha: loaded ? 1 : 0 }; }, componentDidMount: function () { ImageCache.get(this.props.src).on('load', this.handleImageLoad); }, componentWillUpdate: function(nextProps, nextState) { if(nextProps.src !== this.props.src) { ImageCache.get(this.props.src).removeListener('load', this.handleImageLoad); ImageCache.get(nextProps.src).on('load', this.handleImageLoad); var loaded = ImageCache.get(nextProps.src).isLoaded(); this.setState({loaded: loaded}); } }, componentWillUnmount: function () { if (this._pendingAnimationFrame) { cancelAnimationFrame(this._pendingAnimationFrame); } ImageCache.get(this.props.src).removeListener('load', this.handleImageLoad); }, componentDidUpdate: function (prevProps, prevState) { if (this.refs.image) { this.refs.image.invalidateLayout(); } }, render: function () { var rawImage; var imageStyle = Object.assign({}, this.props.style); var style = Object.assign({}, this.props.style); var backgroundStyle = Object.assign({}, this.props.style); var useBackingStore = this.state.loaded ? this.props.useBackingStore : false; // Hide the image until loaded. imageStyle.alpha = this.state.imageAlpha; // Hide opaque background if image loaded so that images with transparent // do not render on top of solid color. style.backgroundColor = imageStyle.backgroundColor = null; backgroundStyle.alpha = clamp(1 - this.state.imageAlpha, 0, 1); return ( React.createElement(Group, {ref: 'main', style: style}, React.createElement(Layer, {ref: 'background', style: backgroundStyle}), React.createElement(RawImage, {ref: 'image', src: this.props.src, style: imageStyle, useBackingStore: useBackingStore}) ) ); }, handleImageLoad: function () { var imageAlpha = 1; if (this.props.fadeIn) { imageAlpha = 0; this._animationStartTime = Date.now(); this._pendingAnimationFrame = requestAnimationFrame(this.stepThroughAnimation); } this.setState({ loaded: true, imageAlpha: imageAlpha }); }, stepThroughAnimation: function () { var fadeInDuration = this.props.fadeInDuration || FADE_DURATION; var alpha = Easing.easeInCubic((Date.now() - this._animationStartTime) / fadeInDuration); alpha = clamp(alpha, 0, 1); this.setState({ imageAlpha: alpha }); if (alpha < 1) { this._pendingAnimationFrame = requestAnimationFrame(this.stepThroughAnimation); } } }); module.exports = Image; ================================================ FILE: lib/ImageCache.js ================================================ 'use strict'; var EventEmitter = require('events'); var NOOP = function () {}; function Img (src) { this._originalSrc = src; this._img = new Image(); this._img.onload = this.emit.bind(this, 'load'); this._img.onerror = this.emit.bind(this, 'error'); this._img.crossOrigin = true; this._img.src = src; // The default impl of events emitter will throw on any 'error' event unless // there is at least 1 handler. Logging anything in this case is unnecessary // since the browser console will log it too. this.on('error', NOOP); // Default is just 10. this.setMaxListeners(100); } Object.assign(Img.prototype, EventEmitter.prototype, { /** * Pooling owner looks for this */ destructor: function () { // Make sure we aren't leaking callbacks. this.removeAllListeners(); }, /** * Retrieve the original image URL before browser normalization * * @return {String} */ getOriginalSrc: function () { return this._originalSrc; }, /** * Retrieve a reference to the underyling node. * * @return {HTMLImageElement} */ getRawImage: function () { return this._img; }, /** * Retrieve the loaded image width * * @return {Number} */ getWidth: function () { return this._img.naturalWidth; }, /** * Retrieve the loaded image height * * @return {Number} */ getHeight: function () { return this._img.naturalHeight; }, /** * @return {Bool} */ isLoaded: function () { return this._img.naturalHeight > 0; } }); var kInstancePoolLength = 300; var _instancePool = { length: 0, // Keep all the nodes in memory. elements: { }, // Push with 0 frequency push: function (hash, data) { this.length++; this.elements[hash] = { hash: hash, // Helps identifying freq: 0, data: data }; }, get: function (path) { var element = this.elements[path]; if( element ){ element.freq++; return element.data; } return null; }, // used to explicitely remove the path removeElement: function (path) { // Now almighty GC can claim this soul var element = this.elements[path]; delete this.elements[path]; this.length--; return element; }, _reduceLeastUsed: function (least, currentHash) { var current = _instancePool.elements[currentHash]; if( least.freq > current.freq ){ return current; } return least; }, popLeastUsed: function () { var reducer = _instancePool._reduceLeastUsed; var minUsed = Object.keys(this.elements).reduce(reducer, { freq: Infinity }); if( minUsed.hash ){ return this.removeElement(minUsed.hash); } return null; } }; var ImageCache = { /** * Retrieve an image from the cache * * @return {Img} */ get: function (src) { var image = _instancePool.get(src); if (!image) { // Awesome LRU image = new Img(src); if (_instancePool.length >= kInstancePoolLength) { _instancePool.popLeastUsed().destructor(); } _instancePool.push(image.getOriginalSrc(), image); } return image; } }; module.exports = ImageCache; ================================================ FILE: lib/Layer.js ================================================ 'use strict'; var createComponent = require('./createComponent'); var LayerMixin = require('./LayerMixin'); var Layer = createComponent('Layer', LayerMixin, { mountComponent: function ( transaction, nativeParent, nativeContainerInfo, context ) { var props = this._currentElement.props; var layer = this.node; this.applyLayerProps({}, props); return layer; }, receiveComponent: function (nextComponent, transaction, context) { var prevProps = this._currentElement.props; var props = nextComponent.props; this.applyLayerProps(prevProps, props); this._currentElement = nextComponent; this.node.invalidateLayout(); } }); module.exports = Layer; ================================================ FILE: lib/LayerMixin.js ================================================ 'use strict'; // Adapted from ReactART: // https://github.com/reactjs/react-art var FrameUtils = require('./FrameUtils'); var DrawingUtils = require('./DrawingUtils'); var EventTypes = require('./EventTypes'); var LAYER_GUID = 0; var LayerMixin = { construct: function(element) { this._currentElement = element; this._layerId = LAYER_GUID++; }, 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) { // TODO }, destroyEventListeners: function() { // TODO }, applyLayerProps: function (prevProps, props) { var layer = this.node; var style = (props && props.style) ? props.style : {}; layer._originalStyle = style; // Common layer properties layer.alpha = style.alpha; layer.backgroundColor = style.backgroundColor; layer.borderColor = style.borderColor; layer.borderWidth = style.borderWidth; layer.borderRadius = style.borderRadius; layer.clipRect = style.clipRect; layer.frame = FrameUtils.make(style.left || 0, style.top || 0, style.width || 0, style.height || 0); layer.scale = style.scale; layer.translateX = style.translateX; layer.translateY = style.translateY; layer.zIndex = style.zIndex; // Shadow layer.shadowColor = style.shadowColor; layer.shadowBlur = style.shadowBlur; layer.shadowOffsetX = style.shadowOffsetX; layer.shadowOffsetY = style.shadowOffsetY; // Generate backing store ID as needed. if (props.useBackingStore) { layer.backingStoreId = this._layerId; } // Register events for (var type in EventTypes) { this.putEventListener(EventTypes[type], props[type]); } }, mountComponentIntoNode: function(rootID, container) { throw new Error( 'You cannot render a Canvas component standalone. ' + 'You need to wrap it in a Surface.' ); }, unmountComponent: function() { this.destroyEventListeners(); }, getHostNode: function () { return this.node }, getNativeNode: function () { return this.node }, }; module.exports = LayerMixin; ================================================ FILE: lib/Layout.js ================================================ // https://github.com/facebook/css-layout /** * Copyright (c) 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. */ var computeLayout = (function() { function capitalizeFirst(str) { return str.charAt(0).toUpperCase() + str.slice(1); } function getSpacing(node, type, suffix, location) { var key = type + capitalizeFirst(location) + suffix; if (key in node.style) { return node.style[key]; } key = type + suffix; if (key in node.style) { return node.style[key]; } return 0; } function getPositiveSpacing(node, type, suffix, location) { var key = type + capitalizeFirst(location) + suffix; if (key in node.style && node.style[key] >= 0) { return node.style[key]; } key = type + suffix; if (key in node.style && node.style[key] >= 0) { return node.style[key]; } return 0; } function isUndefined(value) { return value === undefined; } function getMargin(node, location) { return getSpacing(node, 'margin', '', location); } function getPadding(node, location) { return getPositiveSpacing(node, 'padding', '', location); } function getBorder(node, location) { return getPositiveSpacing(node, 'border', 'Width', location); } function getPaddingAndBorder(node, location) { return getPadding(node, location) + getBorder(node, location); } function getMarginAxis(node, axis) { return getMargin(node, leading[axis]) + getMargin(node, trailing[axis]); } function getPaddingAndBorderAxis(node, axis) { return getPaddingAndBorder(node, leading[axis]) + getPaddingAndBorder(node, trailing[axis]); } function getJustifyContent(node) { if ('justifyContent' in node.style) { return node.style.justifyContent; } return 'flex-start'; } function getAlignItem(node, child) { if ('alignSelf' in child.style) { return child.style.alignSelf; } if ('alignItems' in node.style) { return node.style.alignItems; } return 'stretch'; } function getFlexDirection(node) { if ('flexDirection' in node.style) { return node.style.flexDirection; } return 'column'; } function getPositionType(node) { if ('position' in node.style) { return node.style.position; } return 'relative'; } function getFlex(node) { return node.style.flex; } function isFlex(node) { return ( getPositionType(node) === CSS_POSITION_RELATIVE && getFlex(node) > 0 ); } function isFlexWrap(node) { return node.style.flexWrap === 'wrap'; } function getDimWithMargin(node, axis) { return node.layout[dim[axis]] + getMarginAxis(node, axis); } function isDimDefined(node, axis) { return !isUndefined(node.style[dim[axis]]) && node.style[dim[axis]] >= 0; } function isPosDefined(node, pos) { return !isUndefined(node.style[pos]); } function isMeasureDefined(node) { return 'measure' in node.style; } function getPosition(node, pos) { if (pos in node.style) { return node.style[pos]; } return 0; } // When the user specifically sets a value for width or height function setDimensionFromStyle(node, axis) { // The parent already computed us a width or height. We just skip it if (!isUndefined(node.layout[dim[axis]])) { return; } // We only run if there's a width or height defined if (!isDimDefined(node, axis)) { return; } // The dimensions can never be smaller than the padding and border node.layout[dim[axis]] = fmaxf( node.style[dim[axis]], getPaddingAndBorderAxis(node, axis) ); } // If both left and right are defined, then use left. Otherwise return // +left or -right depending on which is defined. function getRelativePosition(node, axis) { if (leading[axis] in node.style) { return getPosition(node, leading[axis]); } return -getPosition(node, trailing[axis]); } var leading = { row: 'left', column: 'top' }; var trailing = { row: 'right', column: 'bottom' }; var pos = { row: 'left', column: 'top' }; var dim = { row: 'width', column: 'height' }; function fmaxf(a, b) { if (a > b) { return a; } return b; } var CSS_UNDEFINED = undefined; var CSS_FLEX_DIRECTION_ROW = 'row'; var CSS_FLEX_DIRECTION_COLUMN = 'column'; var CSS_JUSTIFY_FLEX_START = 'flex-start'; var CSS_JUSTIFY_CENTER = 'center'; var CSS_JUSTIFY_FLEX_END = 'flex-end'; var CSS_JUSTIFY_SPACE_BETWEEN = 'space-between'; var CSS_JUSTIFY_SPACE_AROUND = 'space-around'; var CSS_ALIGN_FLEX_START = 'flex-start'; var CSS_ALIGN_CENTER = 'center'; var CSS_ALIGN_FLEX_END = 'flex-end'; var CSS_ALIGN_STRETCH = 'stretch'; var CSS_POSITION_RELATIVE = 'relative'; var CSS_POSITION_ABSOLUTE = 'absolute'; return function layoutNode(node, parentMaxWidth) { var/*css_flex_direction_t*/ mainAxis = getFlexDirection(node); var/*css_flex_direction_t*/ crossAxis = mainAxis === CSS_FLEX_DIRECTION_ROW ? CSS_FLEX_DIRECTION_COLUMN : CSS_FLEX_DIRECTION_ROW; // Handle width and height style attributes setDimensionFromStyle(node, mainAxis); setDimensionFromStyle(node, crossAxis); // The position is set by the parent, but we need to complete it with a // delta composed of the margin and left/top/right/bottom node.layout[leading[mainAxis]] += getMargin(node, leading[mainAxis]) + getRelativePosition(node, mainAxis); node.layout[leading[crossAxis]] += getMargin(node, leading[crossAxis]) + getRelativePosition(node, crossAxis); if (isMeasureDefined(node)) { var/*float*/ width = CSS_UNDEFINED; if (isDimDefined(node, CSS_FLEX_DIRECTION_ROW)) { width = node.style.width; } else if (!isUndefined(node.layout[dim[CSS_FLEX_DIRECTION_ROW]])) { width = node.layout[dim[CSS_FLEX_DIRECTION_ROW]]; } else { width = parentMaxWidth - getMarginAxis(node, CSS_FLEX_DIRECTION_ROW); } width -= getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); // We only need to give a dimension for the text if we haven't got any // for it computed yet. It can either be from the style attribute or because // the element is flexible. var/*bool*/ isRowUndefined = !isDimDefined(node, CSS_FLEX_DIRECTION_ROW) && isUndefined(node.layout[dim[CSS_FLEX_DIRECTION_ROW]]); var/*bool*/ isColumnUndefined = !isDimDefined(node, CSS_FLEX_DIRECTION_COLUMN) && isUndefined(node.layout[dim[CSS_FLEX_DIRECTION_COLUMN]]); // Let's not measure the text if we already know both dimensions if (isRowUndefined || isColumnUndefined) { var/*css_dim_t*/ measure_dim = node.style.measure( /*(c)!node->context,*/ width ); if (isRowUndefined) { node.layout.width = measure_dim.width + getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); } if (isColumnUndefined) { node.layout.height = measure_dim.height + getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_COLUMN); } } return; } // Pre-fill some dimensions straight from the parent for (var/*int*/ i = 0; i < node.children.length; ++i) { var/*css_node_t**/ child = node.children[i]; // Pre-fill cross axis dimensions when the child is using stretch before // we call the recursive layout pass if (getAlignItem(node, child) === CSS_ALIGN_STRETCH && getPositionType(child) === CSS_POSITION_RELATIVE && !isUndefined(node.layout[dim[crossAxis]]) && !isDimDefined(child, crossAxis)) { child.layout[dim[crossAxis]] = fmaxf( node.layout[dim[crossAxis]] - getPaddingAndBorderAxis(node, crossAxis) - getMarginAxis(child, crossAxis), // You never want to go smaller than padding getPaddingAndBorderAxis(child, crossAxis) ); } else if (getPositionType(child) == CSS_POSITION_ABSOLUTE) { // Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both // left and right or top and bottom). for (var/*int*/ ii = 0; ii < 2; ii++) { var/*css_flex_direction_t*/ axis = (ii != 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; if (!isUndefined(node.layout[dim[axis]]) && !isDimDefined(child, axis) && isPosDefined(child, leading[axis]) && isPosDefined(child, trailing[axis])) { child.layout[dim[axis]] = fmaxf( node.layout[dim[axis]] - getPaddingAndBorderAxis(node, axis) - getMarginAxis(child, axis) - getPosition(child, leading[axis]) - getPosition(child, trailing[axis]), // You never want to go smaller than padding getPaddingAndBorderAxis(child, axis) ); } } } } var/*float*/ definedMainDim = CSS_UNDEFINED; if (!isUndefined(node.layout[dim[mainAxis]])) { definedMainDim = node.layout[dim[mainAxis]] - getPaddingAndBorderAxis(node, mainAxis); } // We want to execute the next two loops one per line with flex-wrap var/*int*/ startLine = 0; var/*int*/ endLine = 0; var/*int*/ nextOffset = 0; var/*int*/ alreadyComputedNextLayout = 0; // We aggregate the total dimensions of the container in those two variables var/*float*/ linesCrossDim = 0; var/*float*/ linesMainDim = 0; while (endLine < node.children.length) { // Layout non flexible children and count children by type // mainContentDim is accumulation of the dimensions and margin of all the // non flexible children. This will be used in order to either set the // dimensions of the node if none already exist, or to compute the // remaining space left for the flexible children. var/*float*/ mainContentDim = 0; // There are three kind of children, non flexible, flexible and absolute. // We need to know how many there are in order to distribute the space. var/*int*/ flexibleChildrenCount = 0; var/*float*/ totalFlexible = 0; var/*int*/ nonFlexibleChildrenCount = 0; for (var/*int*/ i = startLine; i < node.children.length; ++i) { var/*css_node_t**/ child = node.children[i]; var/*float*/ nextContentDim = 0; // It only makes sense to consider a child flexible if we have a computed // dimension for the node. if (!isUndefined(node.layout[dim[mainAxis]]) && isFlex(child)) { flexibleChildrenCount++; totalFlexible += getFlex(child); // Even if we don't know its exact size yet, we already know the padding, // border and margin. We'll use this partial information to compute the // remaining space. nextContentDim = getPaddingAndBorderAxis(child, mainAxis) + getMarginAxis(child, mainAxis); } else { var/*float*/ maxWidth = CSS_UNDEFINED; if (mainAxis === CSS_FLEX_DIRECTION_ROW) { // do nothing } else if (isDimDefined(node, CSS_FLEX_DIRECTION_ROW)) { maxWidth = node.layout[dim[CSS_FLEX_DIRECTION_ROW]] - getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); } else { maxWidth = parentMaxWidth - getMarginAxis(node, CSS_FLEX_DIRECTION_ROW) - getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); } // This is the main recursive call. We layout non flexible children. if (alreadyComputedNextLayout === 0) { layoutNode(child, maxWidth); } // Absolute positioned elements do not take part of the layout, so we // don't use them to compute mainContentDim if (getPositionType(child) === CSS_POSITION_RELATIVE) { nonFlexibleChildrenCount++; // At this point we know the final size and margin of the element. nextContentDim = getDimWithMargin(child, mainAxis); } } // The element we are about to add would make us go to the next line if (isFlexWrap(node) && !isUndefined(node.layout[dim[mainAxis]]) && mainContentDim + nextContentDim > definedMainDim && // If there's only one element, then it's bigger than the content // and needs its own line i !== startLine) { alreadyComputedNextLayout = 1; break; } alreadyComputedNextLayout = 0; mainContentDim += nextContentDim; endLine = i + 1; } // Layout flexible children and allocate empty space // In order to position the elements in the main axis, we have two // controls. The space between the beginning and the first element // and the space between each two elements. var/*float*/ leadingMainDim = 0; var/*float*/ betweenMainDim = 0; // The remaining available space that needs to be allocated var/*float*/ remainingMainDim = 0; if (!isUndefined(node.layout[dim[mainAxis]])) { remainingMainDim = definedMainDim - mainContentDim; } else { remainingMainDim = fmaxf(mainContentDim, 0) - mainContentDim; } // If there are flexible children in the mix, they are going to fill the // remaining space if (flexibleChildrenCount !== 0) { var/*float*/ flexibleMainDim = remainingMainDim / totalFlexible; // The non flexible children can overflow the container, in this case // we should just assume that there is no space available. if (flexibleMainDim < 0) { flexibleMainDim = 0; } // We iterate over the full array and only apply the action on flexible // children. This is faster than actually allocating a new array that // contains only flexible children. for (var/*int*/ i = startLine; i < endLine; ++i) { var/*css_node_t**/ child = node.children[i]; if (isFlex(child)) { // At this point we know the final size of the element in the main // dimension child.layout[dim[mainAxis]] = flexibleMainDim * getFlex(child) + getPaddingAndBorderAxis(child, mainAxis); var/*float*/ maxWidth = CSS_UNDEFINED; if (mainAxis === CSS_FLEX_DIRECTION_ROW) { // do nothing } else if (isDimDefined(node, CSS_FLEX_DIRECTION_ROW)) { maxWidth = node.layout[dim[CSS_FLEX_DIRECTION_ROW]] - getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); } else { maxWidth = parentMaxWidth - getMarginAxis(node, CSS_FLEX_DIRECTION_ROW) - getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); } // And we recursively call the layout algorithm for this child layoutNode(child, maxWidth); } } // We use justifyContent to figure out how to allocate the remaining // space available } else { var/*css_justify_t*/ justifyContent = getJustifyContent(node); if (justifyContent === CSS_JUSTIFY_FLEX_START) { // Do nothing } else if (justifyContent === CSS_JUSTIFY_CENTER) { leadingMainDim = remainingMainDim / 2; } else if (justifyContent === CSS_JUSTIFY_FLEX_END) { leadingMainDim = remainingMainDim; } else if (justifyContent === CSS_JUSTIFY_SPACE_BETWEEN) { remainingMainDim = fmaxf(remainingMainDim, 0); if (flexibleChildrenCount + nonFlexibleChildrenCount - 1 !== 0) { betweenMainDim = remainingMainDim / (flexibleChildrenCount + nonFlexibleChildrenCount - 1); } else { betweenMainDim = 0; } } else if (justifyContent === CSS_JUSTIFY_SPACE_AROUND) { // Space on the edges is half of the space between elements betweenMainDim = remainingMainDim / (flexibleChildrenCount + nonFlexibleChildrenCount); leadingMainDim = betweenMainDim / 2; } } // Position elements in the main axis and compute dimensions // At this point, all the children have their dimensions set. We need to // find their position. In order to do that, we accumulate data in // variables that are also useful to compute the total dimensions of the // container! var/*float*/ crossDim = 0; var/*float*/ mainDim = leadingMainDim + getPaddingAndBorder(node, leading[mainAxis]); for (var/*int*/ i = startLine; i < endLine; ++i) { var/*css_node_t**/ child = node.children[i]; if (getPositionType(child) === CSS_POSITION_ABSOLUTE && isPosDefined(child, leading[mainAxis])) { // In case the child is position absolute and has left/top being // defined, we override the position to whatever the user said // (and margin/border). child.layout[pos[mainAxis]] = getPosition(child, leading[mainAxis]) + getBorder(node, leading[mainAxis]) + getMargin(child, leading[mainAxis]); } else { // If the child is position absolute (without top/left) or relative, // we put it at the current accumulated offset. child.layout[pos[mainAxis]] += mainDim; } // Now that we placed the element, we need to update the variables // We only need to do that for relative elements. Absolute elements // do not take part in that phase. if (getPositionType(child) === CSS_POSITION_RELATIVE) { // The main dimension is the sum of all the elements dimension plus // the spacing. mainDim += betweenMainDim + getDimWithMargin(child, mainAxis); // The cross dimension is the max of the elements dimension since there // can only be one element in that cross dimension. crossDim = fmaxf(crossDim, getDimWithMargin(child, crossAxis)); } } var/*float*/ containerMainAxis = node.layout[dim[mainAxis]]; // If the user didn't specify a width or height, and it has not been set // by the container, then we set it via the children. if (isUndefined(node.layout[dim[mainAxis]])) { containerMainAxis = fmaxf( // We're missing the last padding at this point to get the final // dimension mainDim + getPaddingAndBorder(node, trailing[mainAxis]), // We can never assign a width smaller than the padding and borders getPaddingAndBorderAxis(node, mainAxis) ); } var/*float*/ containerCrossAxis = node.layout[dim[crossAxis]]; if (isUndefined(node.layout[dim[crossAxis]])) { containerCrossAxis = fmaxf( // For the cross dim, we add both sides at the end because the value // is aggregate via a max function. Intermediate negative values // can mess this computation otherwise crossDim + getPaddingAndBorderAxis(node, crossAxis), getPaddingAndBorderAxis(node, crossAxis) ); } // Position elements in the cross axis for (var/*int*/ i = startLine; i < endLine; ++i) { var/*css_node_t**/ child = node.children[i]; if (getPositionType(child) === CSS_POSITION_ABSOLUTE && isPosDefined(child, leading[crossAxis])) { // In case the child is absolutely positionned and has a // top/left/bottom/right being set, we override all the previously // computed positions to set it correctly. child.layout[pos[crossAxis]] = getPosition(child, leading[crossAxis]) + getBorder(node, leading[crossAxis]) + getMargin(child, leading[crossAxis]); } else { var/*float*/ leadingCrossDim = getPaddingAndBorder(node, leading[crossAxis]); // For a relative children, we're either using alignItems (parent) or // alignSelf (child) in order to determine the position in the cross axis if (getPositionType(child) === CSS_POSITION_RELATIVE) { var/*css_align_t*/ alignItem = getAlignItem(node, child); if (alignItem === CSS_ALIGN_FLEX_START) { // Do nothing } else if (alignItem === CSS_ALIGN_STRETCH) { // You can only stretch if the dimension has not already been set // previously. if (!isDimDefined(child, crossAxis)) { child.layout[dim[crossAxis]] = fmaxf( containerCrossAxis - getPaddingAndBorderAxis(node, crossAxis) - getMarginAxis(child, crossAxis), // You never want to go smaller than padding getPaddingAndBorderAxis(child, crossAxis) ); } } else { // The remaining space between the parent dimensions+padding and child // dimensions+margin. var/*float*/ remainingCrossDim = containerCrossAxis - getPaddingAndBorderAxis(node, crossAxis) - getDimWithMargin(child, crossAxis); if (alignItem === CSS_ALIGN_CENTER) { leadingCrossDim += remainingCrossDim / 2; } else { // CSS_ALIGN_FLEX_END leadingCrossDim += remainingCrossDim; } } } // And we apply the position child.layout[pos[crossAxis]] += linesCrossDim + leadingCrossDim; } } linesCrossDim += crossDim; linesMainDim = fmaxf(linesMainDim, mainDim); startLine = endLine; } // If the user didn't specify a width or height, and it has not been set // by the container, then we set it via the children. if (isUndefined(node.layout[dim[mainAxis]])) { node.layout[dim[mainAxis]] = fmaxf( // We're missing the last padding at this point to get the final // dimension linesMainDim + getPaddingAndBorder(node, trailing[mainAxis]), // We can never assign a width smaller than the padding and borders getPaddingAndBorderAxis(node, mainAxis) ); } if (isUndefined(node.layout[dim[crossAxis]])) { node.layout[dim[crossAxis]] = fmaxf( // For the cross dim, we add both sides at the end because the value // is aggregate via a max function. Intermediate negative values // can mess this computation otherwise linesCrossDim + getPaddingAndBorderAxis(node, crossAxis), getPaddingAndBorderAxis(node, crossAxis) ); } // Calculate dimensions for absolutely positioned elements for (var/*int*/ i = 0; i < node.children.length; ++i) { var/*css_node_t**/ child = node.children[i]; if (getPositionType(child) == CSS_POSITION_ABSOLUTE) { // Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both // left and right or top and bottom). for (var/*int*/ ii = 0; ii < 2; ii++) { var/*css_flex_direction_t*/ axis = (ii !== 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; if (!isUndefined(node.layout[dim[axis]]) && !isDimDefined(child, axis) && isPosDefined(child, leading[axis]) && isPosDefined(child, trailing[axis])) { child.layout[dim[axis]] = fmaxf( node.layout[dim[axis]] - getPaddingAndBorderAxis(node, axis) - getMarginAxis(child, axis) - getPosition(child, leading[axis]) - getPosition(child, trailing[axis]), // You never want to go smaller than padding getPaddingAndBorderAxis(child, axis) ); } } for (var/*int*/ ii = 0; ii < 2; ii++) { var/*css_flex_direction_t*/ axis = (ii !== 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; if (isPosDefined(child, trailing[axis]) && !isPosDefined(child, leading[axis])) { child.layout[leading[axis]] = node.layout[dim[axis]] - child.layout[dim[axis]] - getPosition(child, trailing[axis]); } } } } }; })(); if (typeof module === 'object') { module.exports = computeLayout; } ================================================ FILE: lib/ListView.js ================================================ 'use strict'; var React = require('react'); var Scroller = require('scroller'); var Group = require('./Group'); var clamp = require('./clamp'); var ListView = React.createClass({ propTypes: { style: React.PropTypes.object, numberOfItemsGetter: React.PropTypes.func.isRequired, itemHeightGetter: React.PropTypes.func.isRequired, itemGetter: React.PropTypes.func.isRequired, snapping: React.PropTypes.bool, scrollingDeceleration: React.PropTypes.number, scrollingPenetrationAcceleration: React.PropTypes.number, onScroll: React.PropTypes.func }, getDefaultProps: function () { return { style: { left: 0, top: 0, width: 0, height: 0 }, snapping: false, scrollingDeceleration: 0.95, scrollingPenetrationAcceleration: 0.08 }; }, getInitialState: function () { return { scrollTop: 0 }; }, componentDidMount: function () { this.createScroller(); this.updateScrollingDimensions(); }, render: function () { var items = this.getVisibleItemIndexes().map(this.renderItem); return ( React.createElement(Group, { style: this.props.style, onTouchStart: this.handleTouchStart, onTouchMove: this.handleTouchMove, onTouchEnd: this.handleTouchEnd, onTouchCancel: this.handleTouchEnd}, items ) ); }, renderItem: function (itemIndex) { var item = this.props.itemGetter(itemIndex, this.state.scrollTop); var itemHeight = this.props.itemHeightGetter(); var style = { top: 0, left: 0, width: this.props.style.width, height: itemHeight, translateY: (itemIndex * itemHeight) - this.state.scrollTop, zIndex: itemIndex }; return ( React.createElement(Group, {style: style, key: itemIndex}, item ) ); }, // Events // ====== handleTouchStart: function (e) { if (this.scroller) { this.scroller.doTouchStart(e.touches, e.timeStamp); } }, handleTouchMove: function (e) { if (this.scroller) { e.preventDefault(); this.scroller.doTouchMove(e.touches, e.timeStamp, e.scale); } }, handleTouchEnd: function (e) { if (this.scroller) { this.scroller.doTouchEnd(e.timeStamp); if (this.props.snapping) { this.updateScrollingDeceleration(); } } }, handleScroll: function (left, top) { this.setState({ scrollTop: top }); if (this.props.onScroll) { this.props.onScroll(top); } }, // Scrolling // ========= createScroller: function () { var options = { scrollingX: false, scrollingY: true, decelerationRate: this.props.scrollingDeceleration, penetrationAcceleration: this.props.scrollingPenetrationAcceleration, }; this.scroller = new Scroller(this.handleScroll, options); }, updateScrollingDimensions: function () { var width = this.props.style.width; var height = this.props.style.height; var scrollWidth = width; var scrollHeight = this.props.numberOfItemsGetter() * this.props.itemHeightGetter(); this.scroller.setDimensions(width, height, scrollWidth, scrollHeight); }, getVisibleItemIndexes: function () { var itemIndexes = []; var itemHeight = this.props.itemHeightGetter(); var itemCount = this.props.numberOfItemsGetter(); var scrollTop = this.state.scrollTop; var itemScrollTop = 0; for (var index=0; index < itemCount; index++) { itemScrollTop = (index * itemHeight) - scrollTop; // Item is completely off-screen bottom if (itemScrollTop >= this.props.style.height) { continue; } // Item is completely off-screen top if (itemScrollTop <= -this.props.style.height) { continue; } // Part of item is on-screen. itemIndexes.push(index); } return itemIndexes; }, updateScrollingDeceleration: function () { var currVelocity = this.scroller.__decelerationVelocityY; var currScrollTop = this.state.scrollTop; var targetScrollTop = 0; var estimatedEndScrollTop = currScrollTop; while (Math.abs(currVelocity).toFixed(6) > 0) { estimatedEndScrollTop += currVelocity; currVelocity *= this.props.scrollingDeceleration; } // Find the page whose estimated end scrollTop is closest to 0. var closestZeroDelta = Infinity; var pageHeight = this.props.itemHeightGetter(); var pageCount = this.props.numberOfItemsGetter(); var pageScrollTop; for (var pageIndex=0, len=pageCount; pageIndex < len; pageIndex++) { pageScrollTop = (pageHeight * pageIndex) - estimatedEndScrollTop; if (Math.abs(pageScrollTop) < closestZeroDelta) { closestZeroDelta = Math.abs(pageScrollTop); targetScrollTop = pageHeight * pageIndex; } } this.scroller.__minDecelerationScrollTop = targetScrollTop; this.scroller.__maxDecelerationScrollTop = targetScrollTop; } }); module.exports = ListView; ================================================ FILE: lib/ReactCanvas.js ================================================ 'use strict'; var ReactCanvas = { Surface: require('./Surface'), Layer: require('./Layer'), Group: require('./Group'), Image: require('./Image'), Text: require('./Text'), ListView: require('./ListView'), Gradient: require('./Gradient'), FontFace: require('./FontFace'), measureText: require('./measureText') }; module.exports = ReactCanvas; ================================================ FILE: lib/RenderLayer.js ================================================ 'use strict'; var FrameUtils = require('./FrameUtils'); var DrawingUtils = require('./DrawingUtils'); var EventTypes = require('./EventTypes'); function RenderLayer () { this.children = []; this.frame = FrameUtils.zero(); } RenderLayer.prototype = { /** * Retrieve the root injection layer * * @return {RenderLayer} */ getRootLayer: function () { var root = this; while (root.parentLayer) { root = root.parentLayer; } return root; }, /** * RenderLayers are injected into a root owner layer whenever a Surface is * mounted. This is the integration point with React internals. * * @param {RenderLayer} parentLayer */ inject: function (parentLayer) { if (this.parentLayer && this.parentLayer !== parentLayer) { this.remove(); } if (!this.parentLayer) { parentLayer.addChild(this); } }, /** * Inject a layer before a reference layer * * @param {RenderLayer} parentLayer * @param {RenderLayer} referenceLayer */ injectBefore: function (parentLayer, referenceLayer) { // FIXME this.inject(parentLayer); }, /** * Add a child to the render layer * * @param {RenderLayer} child */ addChild: function (child) { child.parentLayer = this; this.children.push(child); }, /** * Remove a layer from it's parent layer */ remove: function () { if (this.parentLayer) { this.parentLayer.children.splice(this.parentLayer.children.indexOf(this), 1); } }, /** * Attach an event listener to a layer. Supported events are defined in * lib/EventTypes.js * * @param {String} type * @param {Function} callback * @param {?Object} callbackScope * @return {Function} invoke to unsubscribe the listener */ subscribe: function (type, callback, callbackScope) { // This is the integration point with React, called from LayerMixin.putEventListener(). // Enforce that only a single callbcak can be assigned per event type. for (var eventType in EventTypes) { if (EventTypes[eventType] === type) { this[eventType] = callback; } } // Return a function that can be called to unsubscribe from the event. return this.removeEventListener.bind(this, type, callback, callbackScope); }, /** * @param {String} type * @param {Function} callback * @param {?Object} callbackScope */ addEventListener: function (type, callback, callbackScope) { for (var eventType in EventTypes) { if (EventTypes[eventType] === type) { delete this[eventType]; } } }, /** * @param {String} type * @param {Function} callback * @param {?Object} callbackScope */ removeEventListener: function (type, callback, callbackScope) { var listeners = this.eventListeners[type]; var listener; if (listeners) { for (var index=0, len=listeners.length; index < len; index++) { listener = listeners[index]; if (listener.callback === callback && listener.callbackScope === callbackScope) { listeners.splice(index, 1); break; } } } }, /** * Translate a layer's frame * * @param {Number} x * @param {Number} y */ translate: function (x, y) { if (this.frame) { this.frame.x += x; this.frame.y += y; } if (this.clipRect) { this.clipRect.x += x; this.clipRect.y += y; } if (this.children) { this.children.forEach(function (child) { child.translate(x, y); }); } }, /** * Layers should call this method when they need to be redrawn. Note the * difference here between `invalidateBackingStore`: updates that don't * trigger layout should prefer `invalidateLayout`. For instance, an image * component that is animating alpha level after the image loads would * call `invalidateBackingStore` once after the image loads, and at each * step in the animation would then call `invalidateRect`. * * @param {?Frame} frame Optional, if not passed the entire layer's frame * will be invalidated. */ invalidateLayout: function () { // Bubble all the way to the root layer. this.getRootLayer().draw(); }, /** * Layers should call this method when their backing needs to be * redrawn. For instance, an image component would call this once after the * image loads. */ invalidateBackingStore: function () { if (this.backingStoreId) { DrawingUtils.invalidateBackingStore(this.backingStoreId); } this.invalidateLayout(); }, /** * Only the root owning layer should implement this function. */ draw: function () { // Placeholer } }; module.exports = RenderLayer; ================================================ FILE: lib/Surface.js ================================================ 'use strict'; var React = require('react'); var ReactUpdates = require('react-dom/lib/ReactUpdates'); var invariant = require('fbjs/lib/invariant'); var ContainerMixin = require('./ContainerMixin'); var RenderLayer = require('./RenderLayer'); var FrameUtils = require('./FrameUtils'); var DrawingUtils = require('./DrawingUtils'); var hitTest = require('./hitTest'); var layoutNode = require('./layoutNode'); /** * Surface is a standard React component and acts as the main drawing canvas. * ReactCanvas components cannot be rendered outside a Surface. */ var Surface = React.createClass({ mixins: [ContainerMixin], propTypes: { className: React.PropTypes.string, id: React.PropTypes.string, top: React.PropTypes.number.isRequired, left: React.PropTypes.number.isRequired, width: React.PropTypes.number.isRequired, height: React.PropTypes.number.isRequired, scale: React.PropTypes.number.isRequired, enableCSSLayout: React.PropTypes.bool }, getDefaultProps: function () { return { scale: window.devicePixelRatio || 1 }; }, componentDidMount: function () { // Prepare the for drawing. this.scale(); // ContainerMixin expects `this.node` to be set prior to mounting children. // `this.node` is injected into child components and represents the current // render tree. this.node = new RenderLayer(); this.node.frame = FrameUtils.make(this.props.left, this.props.top, this.props.width, this.props.height); this.node.draw = this.batchedTick; // This is the integration point between custom canvas components and React var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(); transaction.perform( this.mountAndInjectChildrenAtRoot, this, this.props.children, transaction ); ReactUpdates.ReactReconcileTransaction.release(transaction); // Execute initial draw on mount. this.node.draw(); }, componentWillUnmount: function () { // Implemented in ReactMultiChild.Mixin this.unmountChildren(); }, componentDidUpdate: function (prevProps, prevState) { // We have to manually apply child reconciliation since child are not // declared in render(). var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(); transaction.perform( this.updateChildrenAtRoot, this, this.props.children, transaction ); ReactUpdates.ReactReconcileTransaction.release(transaction); // Re-scale the when changing size. if (prevProps.width !== this.props.width || prevProps.height !== this.props.height) { this.scale(); } // Redraw updated render tree to . if (this.node) { this.node.draw(); } }, render: function () { // Scale the drawing area to match DPI. var width = this.props.width * this.props.scale; var height = this.props.height * this.props.scale; var style = { width: this.props.width, height: this.props.height }; return ( React.createElement('canvas', { ref: 'canvas', className: this.props.className, id: this.props.id, width: width, height: height, style: style, onTouchStart: this.handleTouchStart, onTouchMove: this.handleTouchMove, onTouchEnd: this.handleTouchEnd, onTouchCancel: this.handleTouchEnd, onClick: this.handleClick, onContextMenu: this.handleContextMenu, onDoubleClick: this.handleDoubleClick}) ); }, // Drawing // ======= getContext: function () { ('production' !== process.env.NODE_ENV ? invariant( this.isMounted(), 'Tried to access drawing context on an unmounted Surface.' ) : invariant(this.isMounted())); return this.refs.canvas.getContext('2d'); }, scale: function () { this.getContext().scale(this.props.scale, this.props.scale); }, batchedTick: function () { if (this._frameReady === false) { this._pendingTick = true; return; } this.tick(); }, tick: function () { // Block updates until next animation frame. this._frameReady = false; this.clear(); this.draw(); requestAnimationFrame(this.afterTick); }, afterTick: function () { // Execute pending draw that may have been scheduled during previous frame this._frameReady = true; if (this._pendingTick) { this._pendingTick = false; this.batchedTick(); } }, clear: function () { this.getContext().clearRect(0, 0, this.props.width, this.props.height); }, draw: function () { var layout; if (this.node) { if (this.props.enableCSSLayout) { layout = layoutNode(this.node); } DrawingUtils.drawRenderLayer(this.getContext(), this.node); } }, // Events // ====== hitTest: function (e) { var hitTarget = hitTest(e, this.node, this.refs.canvas); if (hitTarget) { hitTarget[hitTest.getHitHandle(e.type)](e); } }, handleTouchStart: function (e) { var hitTarget = hitTest(e, this.node, this.refs.canvas); var touch; if (hitTarget) { // On touchstart: capture the current hit target for the given touch. this._touches = this._touches || {}; for (var i=0, len=e.touches.length; i < len; i++) { touch = e.touches[i]; this._touches[touch.identifier] = hitTarget; } hitTarget[hitTest.getHitHandle(e.type)](e); } }, handleTouchMove: function (e) { this.hitTest(e); }, handleTouchEnd: function (e) { // touchend events do not generate a pageX/pageY so we rely // on the currently captured touch targets. if (!this._touches) { return; } var hitTarget; var hitHandle = hitTest.getHitHandle(e.type); for (var i=0, len=e.changedTouches.length; i < len; i++) { hitTarget = this._touches[e.changedTouches[i].identifier]; if (hitTarget && hitTarget[hitHandle]) { hitTarget[hitHandle](e); } delete this._touches[e.changedTouches[i].identifier]; } }, handleClick: function (e) { this.hitTest(e); }, handleContextMenu: function (e) { this.hitTest(e); }, handleDoubleClick: function (e) { this.hitTest(e); }, }); module.exports = Surface; ================================================ FILE: lib/Text.js ================================================ 'use strict'; var createComponent = require('./createComponent'); var LayerMixin = require('./LayerMixin'); var Text = createComponent('Text', LayerMixin, { applyTextProps: function (prevProps, props) { var style = (props && props.style) ? props.style : {}; var layer = this.node; layer.type = 'text'; layer.text = childrenAsString(props.children); layer.color = style.color; layer.fontFace = style.fontFace; layer.fontSize = style.fontSize; layer.lineHeight = style.lineHeight; layer.textAlign = style.textAlign; }, mountComponent: function ( transaction, nativeParent, nativeContainerInfo, context ) { var props = this._currentElement.props; var layer = this.node; this.applyLayerProps({}, props); this.applyTextProps({}, props); return layer; }, receiveComponent: function (nextComponent, transaction, context) { var props = nextComponent.props; var prevProps = this._currentElement.props; this.applyLayerProps(prevProps, props); this.applyTextProps(prevProps, props); this._currentElement = nextComponent; this.node.invalidateLayout(); } }); function childrenAsString(children) { if (!children) { return ''; } if (typeof children === 'string') { return children; } if (children.length) { return children.join('\n'); } return ''; } module.exports = Text; ================================================ FILE: lib/__tests__/clamp-test.js ================================================ jest.dontMock('../clamp.js'); var clamp = require('../clamp'); describe('clamp', function() { it('returns the min if n is less than min', function() { expect(clamp(-1, 0, 1)).toBe(0); }); it('returns the max if n is greater than max', function() { expect(clamp(2, 0, 1)).toBe(1); }); it('returns n if n is between min and max', function() { expect(clamp(0.5, 0, 1)).toBe(0.5); }); }); ================================================ FILE: lib/clamp.js ================================================ 'use strict'; /** * Clamp a number between a minimum and maximum value. * @param {Number} number * @param {Number} min * @param {Number} max * @return {Number} */ module.exports = function (number, min, max) { return Math.min(Math.max(number, min), max); }; ================================================ FILE: lib/createComponent.js ================================================ 'use strict'; // Adapted from ReactART: // https://github.com/reactjs/react-art var RenderLayer = require('./RenderLayer'); function createComponent (name) { var ReactCanvasComponent = function (element) { this.node = null; this.subscriptions = null; this.listeners = null; this.node = new RenderLayer(); this._mountImage = null; this._currentElement = element; this._renderedChildren = null; this._mostRecentlyPlacedChild = null; }; ReactCanvasComponent.displayName = name; for (var i = 1, l = arguments.length; i < l; i++) { Object.assign(ReactCanvasComponent.prototype, arguments[i]); } return ReactCanvasComponent; } module.exports = createComponent; ================================================ FILE: lib/hitTest.js ================================================ 'use strict'; var FrameUtils = require('./FrameUtils'); var EventTypes = require('./EventTypes'); /** * RenderLayer hit testing * * @param {Event} e * @param {RenderLayer} rootLayer * @param {?HTMLElement} rootNode * @return {RenderLayer} */ function hitTest (e, rootLayer, rootNode) { var touch = e.touches ? e.touches[0] : e; var touchX = touch.pageX; var touchY = touch.pageY; var rootNodeBox; if (rootNode) { rootNodeBox = rootNode.getBoundingClientRect(); touchX -= rootNodeBox.left; touchY -= rootNodeBox.top; } touchY = touchY - window.pageYOffset; touchX = touchX - window.pageXOffset; return getLayerAtPoint( rootLayer, e.type, FrameUtils.make(touchX, touchY, 1, 1), rootLayer.translateX || 0, rootLayer.translateY || 0 ); } /** * @private */ function sortByZIndexDescending (layer, otherLayer) { return (otherLayer.zIndex || 0) - (layer.zIndex || 0); } /** * @private */ function getHitHandle (type) { var hitHandle; for (var tryHandle in EventTypes) { if (EventTypes[tryHandle] === type) { hitHandle = tryHandle; break; } } return hitHandle; } /** * @private */ function getLayerAtPoint (root, type, point, tx, ty) { var layer = null; var hitHandle = getHitHandle(type); var sortedChildren; var hitFrame = FrameUtils.clone(root.frame); // Early bail for non-visible layers if (typeof root.alpha === 'number' && root.alpha < 0.01) { return null; } // Child-first search if (root.children) { sortedChildren = root.children.slice().reverse().sort(sortByZIndexDescending); for (var i=0, len=sortedChildren.length; i < len; i++) { layer = getLayerAtPoint( sortedChildren[i], type, point, tx + (root.translateX || 0), ty + (root.translateY || 0) ); if (layer) { break; } } } // Check for hit outsets if (root.hitOutsets) { hitFrame = FrameUtils.inset(FrameUtils.clone(hitFrame), -root.hitOutsets[0], -root.hitOutsets[1], -root.hitOutsets[2], -root.hitOutsets[3] ); } // Check for x/y translation if (tx) { hitFrame.x += tx; } if (ty) { hitFrame.y += ty; } // No child layer at the given point. Try the parent layer. if (!layer && root[hitHandle] && FrameUtils.intersects(hitFrame, point)) { layer = root; } return layer; } module.exports = hitTest; module.exports.getHitHandle = getHitHandle; ================================================ FILE: lib/layoutNode.js ================================================ 'use strict'; var computeLayout = require('./Layout'); /** * This computes the CSS layout for a RenderLayer tree and mutates the frame * objects at each node. * * @param {Renderlayer} root * @return {Object} */ function layoutNode (root) { var rootNode = createNode(root); computeLayout(rootNode); walkNode(rootNode); return rootNode; } function createNode (layer) { return { layer: layer, layout: { width: undefined, // computeLayout will mutate height: undefined, // computeLayout will mutate top: 0, left: 0, }, style: layer._originalStyle || {}, children: (layer.children || []).map(createNode) }; } function walkNode (node, parentLeft, parentTop) { node.layer.frame.x = node.layout.left + (parentLeft || 0); node.layer.frame.y = node.layout.top + (parentTop || 0); node.layer.frame.width = node.layout.width; node.layer.frame.height = node.layout.height; if (node.children && node.children.length > 0) { node.children.forEach(function (child) { walkNode(child, node.layer.frame.x, node.layer.frame.y); }); } } module.exports = layoutNode; ================================================ FILE: lib/measureText.js ================================================ 'use strict'; var FontFace = require('./FontFace'); var FontUtils = require('./FontUtils'); var LineBreaker = require('linebreak'); var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); var _cache = {}; var _zeroMetrics = { width: 0, height: 0, lines: [] }; function getCacheKey (text, width, fontFace, fontSize, lineHeight) { return text + width + fontFace.id + fontSize + lineHeight; } /** * Given a string of text, available width, and font return the measured width * and height. * @param {String} text The input string * @param {Number} width The available width * @param {FontFace} fontFace The FontFace to use * @param {Number} fontSize The font size in CSS pixels * @param {Number} lineHeight The line height in CSS pixels * @return {Object} Measured text size with `width` and `height` members. */ module.exports = function measureText (text, width, fontFace, fontSize, lineHeight) { var cacheKey = getCacheKey(text, width, fontFace, fontSize, lineHeight); var cached = _cache[cacheKey]; if (cached) { return cached; } // Bail and return zero unless we're sure the font is ready. if (!FontUtils.isFontLoaded(fontFace)) { return _zeroMetrics; } var measuredSize = {}; var textMetrics; var lastMeasuredWidth; var words; var tryLine; var currentLine; var breaker; var bk; var lastBreak; ctx.font = fontFace.attributes.style + ' ' + fontFace.attributes.weight + ' ' + fontSize + 'px ' + fontFace.family; textMetrics = ctx.measureText(text); measuredSize.width = textMetrics.width; measuredSize.height = lineHeight; measuredSize.lines = []; if (measuredSize.width <= width) { // The entire text string fits. measuredSize.lines.push({width: measuredSize.width, text: text}); } else { // Break into multiple lines. measuredSize.width = width; currentLine = ''; breaker = new LineBreaker(text); while (bk = breaker.nextBreak()) { var word = text.slice(lastBreak ? lastBreak.position : 0, bk.position); tryLine = currentLine + word; textMetrics = ctx.measureText(tryLine); if (textMetrics.width > width || (lastBreak && lastBreak.required)) { measuredSize.height += lineHeight; measuredSize.lines.push({width: lastMeasuredWidth, text: currentLine.trim()}); currentLine = word; lastMeasuredWidth = ctx.measureText(currentLine.trim()).width; } else { currentLine = tryLine; lastMeasuredWidth = textMetrics.width; } lastBreak = bk; } currentLine = currentLine.trim(); if (currentLine.length > 0) { textMetrics = ctx.measureText(currentLine); measuredSize.lines.push({width: textMetrics, text: currentLine}); } } _cache[cacheKey] = measuredSize; return measuredSize; }; ================================================ FILE: package.json ================================================ { "name": "react-canvas", "version": "1.3.0", "description": "High performance rendering for React components", "main": "lib/ReactCanvas.js", "repository": { "type": "git", "url": "https://github.com/Flipboard/react-canvas.git" }, "scripts": { "start": "./node_modules/.bin/gulp", "test": "./node_modules/.bin/jest" }, "keywords": [ "react", "canvas" ], "author": "Michael Johnston ", "license": "BSD-3-Clause", "homepage": "https://github.com/Flipboard/react-canvas", "bugs": { "url": "https://github.com/Flipboard/react-canvas/issues" }, "devDependencies": { "babel-core": "^6.22.1", "babel-loader": "^6.2.10", "babel-preset-react": "^6.22.0", "brfs": "^1.4.3", "del": "^2.2.2", "envify": "^4.0.0", "gulp": "^3.9.1", "gulp-connect": "^5.0.0", "jest": "^18.1.0", "react": "^15.0.0", "react-dom": "^15.0.0", "transform-loader": "^0.2.3", "webpack": "^1.14.0", "webpack-stream": "^3.2.0" }, "peerDependencies": { "react": "^15.0.0" }, "dependencies": { "fbjs": "^0.8.8", "linebreak": "^0.3.0", "scroller": "git://github.com/mjohnston/scroller" } } ================================================ FILE: webpack.config.js ================================================ module.exports = { cache: true, watch: true, entry: { 'listview': ['./examples/listview/app.js'], 'timeline': ['./examples/timeline/app.js'], 'gradient': ['./examples/gradient/app.js'], 'css-layout': ['./examples/css-layout/app.js'] }, output: { filename: '[name].js' }, module: { loaders: [ { test: /\.js$/, loader: 'babel-loader!transform/cacheable?envify' }, ], postLoaders: [ { loader: "transform?brfs" } ] }, devtool: ['source-map'], resolve: { root: __dirname, alias: { 'react-canvas': 'lib/ReactCanvas.js' } } };