diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..a27a84a --- /dev/null +++ b/.jshintrc @@ -0,0 +1,5 @@ +{ + "node":true, + "browser": true, + "expr": true +} \ No newline at end of file diff --git a/examples/custom-draw/app.js b/examples/custom-draw/app.js new file mode 100644 index 0000000..935df15 --- /dev/null +++ b/examples/custom-draw/app.js @@ -0,0 +1,92 @@ +/** @jsx React.DOM */ + +'use strict'; + +var React = require('react'); +var ReactCanvas = require('react-canvas'); + +var Surface = ReactCanvas.Surface; +var Group = ReactCanvas.Group; + + +ReactCanvas.registerLayerType('circle', function (ctx, layer) { + var x = layer.frame.x; + var y = layer.frame.y; + var width = layer.frame.width; + var height = layer.frame.height; + var centerX = x + width / 2; + var centerY = y + height / 2; + + var fillColor = layer.backgroundColor || '#FFF'; + var strokeColor = layer.borderColor || '#FFF'; + var strokeWidth = layer.borderWidth || 0; + + var shadowColor = layer.shadowColor || 0; + var shadowOffsetX = layer.shadowOffsetX || 0; + var shadowOffsetY = layer.shadowOffsetY || 0; + var shadowBlur = layer.shadowBlur || 0; + + var radius = Math.min(width / 2, height / 2) - Math.ceil(strokeWidth / 2); + + + + ctx.beginPath(); + ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI, false); + if (shadowOffsetX || shadowOffsetY) { + ctx.shadowColor = shadowColor; + ctx.shadowBlur = shadowBlur; + ctx.shadowOffsetX = shadowOffsetX; + ctx.shadowOffsetY = shadowOffsetY; + } + + ctx.fillStyle = fillColor; + ctx.fill(); + if (strokeWidth > 0) { + ctx.lineWidth = strokeWidth; + ctx.strokeStyle = strokeColor; + ctx.stroke(); + } +}); + +var Circle = ReactCanvas.createCanvasComponent({ + displayName: 'Circle', + layerType: 'circle', + + applyCustomProps: function (prevProps, props) { + var style = props.style || {}; + var layer = this.node; + layer.shadowColor = style.shadowColor || 0; + layer.shadowOffsetX = style.shadowOffsetX || 0; + layer.shadowOffsetY = style.shadowOffsetY || 0; + layer.shadowBlur = style.shadowBlur || 0; + } +}); + + + +var App = React.createClass({ + + render: function () { + return ( + + + + ); + }, + +}); + +React.render(, document.getElementById('main')); diff --git a/examples/custom-draw/index.html b/examples/custom-draw/index.html new file mode 100644 index 0000000..71b6817 --- /dev/null +++ b/examples/custom-draw/index.html @@ -0,0 +1,17 @@ + + + + + + ReactCanvas: ListView + + + + + +
+ + + diff --git a/lib/DrawingUtils.js b/lib/DrawingUtils.js index e2db275..d6e84fb 100644 --- a/lib/DrawingUtils.js +++ b/lib/DrawingUtils.js @@ -7,6 +7,40 @@ var FrameUtils = require('./FrameUtils'); var CanvasUtils = require('./CanvasUtils'); var Canvas = require('./Canvas'); +// Global map of layer type to drawFunction +var _layerTypesToDrawFunction = { + 'image': drawImageRenderLayer, + 'text': drawTextRenderLayer, + 'gradient': drawGradientRenderLayer +}; + +/** + * Retrieve a draw function for the given layer type + * return the default `drawBaseRenderLayer` function if this type is not registered + * + * @param {String} type the layer type + * @return {Function} the function responsible for drawing the layer + */ +function getDrawFunction(type) { + return _layerTypesToDrawFunction.hasOwnProperty(type) ? + _layerTypesToDrawFunction[type] : + drawBaseRenderLayer; +} + +/** + * Register a new layer type and the associated draw function + * + * @param {String} type the layer type + * @param {Function} drawFunction the function responsible for drawing the layer + */ +function registerLayerType(type, drawFunction) { + if (_layerTypesToDrawFunction.hasOwnProperty(type)) { + throw new Error('type `' + type + '` is already registered.'); + } + _layerTypesToDrawFunction[type] = drawFunction; +} + + // Global backing store cache var _backingStores = []; @@ -147,26 +181,13 @@ function handleFontLoad (fontFace) { * @param {RenderLayer} layer */ function drawRenderLayer (ctx, layer) { - var customDrawFunc; + var drawFunction = getDrawFunction(layer.type); // 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 @@ -191,14 +212,11 @@ function drawRenderLayer (ctx, layer) { // 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); + drawCacheableRenderLayer(ctx, layer, drawFunction); } else { - // Draw default properties, such as background color. + // Draw layer ctx.save(); - drawBaseRenderLayer(ctx, layer); - - // Draw custom properties if needed. - customDrawFunc && customDrawFunc(ctx, layer); + drawFunction && drawFunction(ctx, layer); ctx.restore(); // Draw child layers, sorted by their z-index. @@ -277,7 +295,7 @@ function drawBaseRenderLayer (ctx, layer) { * @param {Function} customDrawFunc * @private */ -function drawCacheableRenderLayer (ctx, layer, customDrawFunc) { +function drawCacheableRenderLayer (ctx, layer, drawFunction) { // See if there is a pre-drawn canvas in the pool. var backingStore = getBackingStore(layer.backingStoreId); var backingStoreScale = layer.scale || window.devicePixelRatio; @@ -310,12 +328,9 @@ function drawCacheableRenderLayer (ctx, layer, customDrawFunc) { backingContext = backingStore.getContext('2d'); layer.translate(-frameOffsetX, -frameOffsetY); - // Draw default properties, such as background color. + // Draw layer backingContext.save(); - drawBaseRenderLayer(backingContext, layer); - - // Custom drawing operations - customDrawFunc && customDrawFunc(backingContext, layer); + drawFunction && drawFunction(backingContext, layer); backingContext.restore(); // Draw child layers, sorted by their z-index. @@ -362,6 +377,8 @@ function sortByZIndexAscending (layerA, layerB) { * @private */ function drawImageRenderLayer (ctx, layer) { + drawBaseRenderLayer(ctx, layer); + if (!layer.imageUrl) { return; } @@ -379,6 +396,8 @@ function drawImageRenderLayer (ctx, layer) { * @private */ function drawTextRenderLayer (ctx, layer) { + drawBaseRenderLayer(ctx, layer); + // Fallback to standard font. var fontFace = layer.fontFace || FontFace.Default(); @@ -399,6 +418,8 @@ function drawTextRenderLayer (ctx, layer) { * @private */ function drawGradientRenderLayer (ctx, layer) { + drawBaseRenderLayer(ctx, layer); + // Default to linear gradient from top to bottom. var x1 = layer.x1 || layer.frame.x; var y1 = layer.y1 || layer.frame.y; @@ -409,10 +430,13 @@ function drawGradientRenderLayer (ctx, layer) { module.exports = { drawRenderLayer: drawRenderLayer, + drawBaseRenderLayer: drawBaseRenderLayer, + drawGradientRenderLayer: drawGradientRenderLayer, invalidateBackingStore: invalidateBackingStore, invalidateAllBackingStores: invalidateAllBackingStores, handleImageLoad: handleImageLoad, handleFontLoad: handleFontLoad, layerContainsImage: layerContainsImage, - layerContainsFontFace: layerContainsFontFace + layerContainsFontFace: layerContainsFontFace, + registerLayerType: registerLayerType }; diff --git a/lib/ReactCanvas.js b/lib/ReactCanvas.js index ba4dd79..5912224 100644 --- a/lib/ReactCanvas.js +++ b/lib/ReactCanvas.js @@ -10,7 +10,9 @@ var ReactCanvas = { ListView: require('./ListView'), FontFace: require('./FontFace'), - measureText: require('./measureText') + measureText: require('./measureText'), + createCanvasComponent: require('./createCanvasComponent'), + registerLayerType: require('./DrawingUtils').registerLayerType }; module.exports = ReactCanvas; diff --git a/lib/createCanvasComponent.js b/lib/createCanvasComponent.js new file mode 100644 index 0000000..ee7bc75 --- /dev/null +++ b/lib/createCanvasComponent.js @@ -0,0 +1,43 @@ +'use strict'; + +var createComponent = require('./createComponent'); +var LayerMixin = require('./LayerMixin'); + + +/** + * Create a new component + * + * @param {{layerType: String, applyCustomProps: ?Function}} specs component specs + * @return {Function} Generated ReactCanvas component class + */ +function createCanvasComponent(specs) { + if (!specs.layerType) { + throw new Error('createCanvasComponent(...): specification should contains an unique `layerType` property'); + } + + return createComponent(specs.displayName || 'CanvasComponent', LayerMixin, { + applyCustomProps: specs.applyCustomProps, + + mountComponent: function (rootID, transaction, context) { + var props = this._currentElement.props; + var layer = this.node; + layer.type = specs.layerType; + var emptyProps = {}; + this.applyLayerProps(emptyProps, props); + this.applyCustomProps && this.applyCustomProps(emptyProps, props); + return layer; + }, + + receiveComponent: function (nextComponent, transaction, context) { + var prevProps = this._currentElement.props; + var props = nextComponent.props; + this.applyLayerProps(prevProps, props); + this.applyCustomProps && this.applyCustomProps(prevProps, props); + this._currentElement = nextComponent; + this.node.invalidateLayout(); + } + }); +} + + +module.exports = createCanvasComponent; diff --git a/webpack.config.js b/webpack.config.js index b82ceef..0f8f0d1 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -6,7 +6,8 @@ module.exports = { entry: { 'listview': ['./examples/listview/app.js'], 'timeline': ['./examples/timeline/app.js'], - 'css-layout': ['./examples/css-layout/app.js'] + 'css-layout': ['./examples/css-layout/app.js'], + 'custom-draw': ['./examples/custom-draw/app.js'] }, output: {