Skip to content

Commit

Permalink
[update] Replace SVG rendering with Canvas
Browse files Browse the repository at this point in the history
This is not an arbitrary change, however further study into using Canvas
over SVG has made me discover a few things:

- This is a performance critical component. Using Canvas bypasses the
  React rendering pipeline. This means that <Ink /> will not choke up
  the React update process.
- The memory usage is staggeringly slower than SVG
- React doesn't have fantastic support for SVG filters. With canvas we
  can begin to explore composite operations and other fun effects.
  • Loading branch information
nhunzaker committed Feb 28, 2015
1 parent e162454 commit c80955a
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 54 deletions.
2 changes: 1 addition & 1 deletion dist/ink.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-ink",
"version": "3.3.1",
"version": "4.0.0-rc1",
"description": "A React component for adding material design style ink.",
"main": "dist/ink.js",
"scripts": {
Expand Down
110 changes: 79 additions & 31 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,27 @@
*/

let HAS_TOUCH = require('./util/hasTouch')
let MOUSE_LEFT = 0
let pixelRatio = require('./util/pixelRatio')
let React = require('react')
let STYLE = require('./style')
let Store = require('./util/store')
let MOUSE_LEFT = 0
let Types = React.PropTypes
let TAU = Math.PI * 2
let Equations = require('./util/equations')

let Ink = React.createClass({

shouldComponentUpdate(props, state) {
return !!state.frame
for (let p in props) {
if (this.props[p] !== props[p]) return true
}

for (let s in state) {
if (this.state[s] !== state[s]) return true
}

return false
},

propTypes: {
Expand All @@ -30,7 +41,6 @@ let Ink = React.createClass({
return {
background : true,
duration : 1500,
fill : 'currentColor',
opacity : 0.2,
radius : 150,
recenter : true
Expand Down Expand Up @@ -61,54 +71,92 @@ let Ink = React.createClass({
}
},

tick(frame) {
this.setState({ frame })
tick() {
let { ctx, color, height, store, width } = this.state

let density = pixelRatio(ctx)

ctx.save()

ctx.scale(density, density)

ctx.clearRect(0, 0, width, height)
ctx.fillStyle = color
ctx.globalAlpha = store.getTotalOpacity()
ctx.fillRect(0, 0, width, height)

store.map(this.makeBlot, this)

ctx.restore()
},

makeBlot(blot) {
let { ctx, height, width } = this.state
let { x, y, radius } = blot

ctx.save()
ctx.globalAlpha = Equations.getBlotOpacity(blot)
ctx.beginPath()

if (this.props.recenter) {
let size = Math.max(height, width)

ctx.translate(Equations.getBlotShiftX(blot, size, width),
Equations.getBlotShiftY(blot, size, height))
}

ctx.arc(x, y, radius * Equations.getBlotScale(blot), 0, TAU)

ctx.closePath()
ctx.fill()
ctx.restore()
},

componentWillUnmount() {
this.state.store.stop()
},

pushBlot(timeStamp, clientX, clientY) {
let { top, bottom, left, right } = this.getDOMNode().getBoundingClientRect()
let el = this.getDOMNode()

let { top, bottom, left, right } = el.getBoundingClientRect()

let height = bottom - top
let width = right - left
let size = Math.max(height, width)

this.state.store.add({
duration : this.props.duration,
maxOpacity : this.props.opacity,
mouseDown : timeStamp,
mouseUp : 0,
radius : Math.min(size, this.props.radius),
recenter : this.props.recenter,
x : clientX - left,
y : clientY - top,
size : size,
height : height,
width : width

this.setState({
color : el.style.color,
ctx : this.state.ctx || this.refs.canvas.getDOMNode().getContext('2d'),
height : height,
width : width
}, () => {
this.state.store.add({
duration : this.props.duration,
opacity : this.props.opacity,
mouseDown : timeStamp,
mouseUp : 0,
radius : Math.min(Math.max(width, height), this.props.radius),
x : clientX - left,
y : clientY - top
})
})
},

popBlot(time) {
this.state.store.release(time)
},

makeBlot({ radius:r, opacity:fillOpacity, transform }, key) {
return React.createElement('circle', { key, r, fillOpacity, transform })
},

render() {
let { background, fill, style } = this.props
let { store, touchEvents } = this.state
let css = { ...STYLE, ...style }
let { height, width, touchEvents } = this.state

return (
<svg className="ink" style={ css } fill={ fill } { ...touchEvents } onDragOver={ this._onRelease }>
{ store.map(this.makeBlot) }
<rect width="100%" height="100%" fillOpacity={ background ? this.state.store.getTotalOpacity() : 0 } />
</svg>
<canvas ref="canvas"
className="ink"
style={{ ...STYLE, ...this.props.style }}
height={ height * 2 }
width={ width * 2 }
onDragOver={ this._onRelease }
{ ...touchEvents } />
)
},

Expand Down
4 changes: 2 additions & 2 deletions src/style.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
module.exports = {
borderRadius: "inherit",
color: "inherit",
height: "100%",
left: 0,
position: "absolute",
top: 0,
width: "100%",
willChange: 'transform, opacity'
width: "100%"
}
27 changes: 13 additions & 14 deletions src/util/equations.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,25 @@ function getRadius(blot) {
module.exports = {

getBlotOpacity(blot) {
return easing(getRelease(blot), blot.maxOpacity, -blot.maxOpacity, blot.duration)
return easing(getRelease(blot), blot.opacity, -blot.opacity, blot.duration)
},

getBlotOuterOpacity(blot) {
return min(blot.opacity, easing(getPress(blot), 0, 0.3, blot.duration * 3))
return min(this.getBlotOpacity(blot),
easing(getPress(blot), 0, 0.3, blot.duration * 3))
},

getBlotTransform(blot) {
let { recenter, x, y, size, width, height } = blot

let radius = getRadius(blot)

if (recenter) {
let shift = min(1, radius / size * 2 / SQRT_2)
x += shift * (width / 2 - x)
y += shift * (height / 2 -y)
}
getBlotShiftX(blot, size, width) {
return min(1,
getRadius(blot) / size * 2 / SQRT_2) * (width / 2 - blot.x)
},

let scale = (radius / blot.radius)
getBlotShiftY(blot, size, height) {
return min(1,
getRadius(blot) / size * 2 / SQRT_2) * (height / 2 - blot.y)
},

return `translate(${ x },${ y }) scale(${ scale }, ${ scale }) `
getBlotScale(blot) {
return getRadius(blot) / blot.radius
}
}
13 changes: 13 additions & 0 deletions src/util/pixelRatio.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Good stuff here:
// http://www.html5rocks.com/en/tutorials/canvas/hidpi/

export default context => {
let devicePixelRatio = window.devicePixelRatio || 1
let backingStoreRatio = context.webkitBackingStorePixelRatio ||
context.mozBackingStorePixelRatio ||
context.msBackingStorePixelRatio ||
context.oBackingStorePixelRatio ||
context.backingStorePixelRatio || 1

return devicePixelRatio / backingStoreRatio
}
6 changes: 1 addition & 5 deletions src/util/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ var Equations = require('./equations')
module.exports = function(publicize) {
let _data = []
let _playing = false
let _totalOpacity = 0
let _frame

let Store = {
Expand Down Expand Up @@ -49,10 +48,7 @@ module.exports = function(publicize) {
},

shouldPrune(blot) {
blot.opacity = Equations.getBlotOpacity(blot)
blot.transform = Equations.getBlotTransform(blot)

return blot.opacity >= 0.01
return Equations.getBlotOpacity(blot) > 0.01
},

prune() {
Expand Down
1 change: 1 addition & 0 deletions webpack.example.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ var WebPack = require('webpack')

module.exports = {
entry: './example/index.js',
devtool: 'source-map',

output: {
path: './example',
Expand Down

0 comments on commit c80955a

Please sign in to comment.