Skip to content

Instantly share code, notes, and snippets.

@queckezz
Last active January 6, 2016 15:12
Show Gist options
  • Save queckezz/91abb31cd43e4887ecf4 to your computer and use it in GitHub Desktop.
Save queckezz/91abb31cd43e4887ecf4 to your computer and use it in GitHub Desktop.
vdux#11 discussion

Continuation of vdux#11 discussion

So I basically implemented the animation imperatively without vdux. Also the it works only with one ripple effect at a time. Meaning it will reset if the animation is running and you click again.

Animation Ripple Effect

Note on self:

  • Use progressInTime in state. On each animation frame, update progressInTime and render the circles based of off it.
  • Find some way to inject state into requestAnimationFrame() loop
  • progressInTime = 1 -> dispatch(removeRipple()). store.getState() maybe the only solution?
  • Think in frames. Have a function which transitions one frame further into the animation.
  • Handle the "Can the animation proceed to the next frame?" kinda question seperately
import easing from 'eases/quad-in'
const pyth = (x, y) => Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))
const drawCircle = (ctx, x, y, r, transparency = 0.1) => {
ctx.beginPath()
ctx.arc(x, y, r, 0, 2 * Math.PI, false)
ctx.fillStyle = `rgba(255, 255, 255, ${transparency})`;
ctx.fill()
}
const tween = (start, end, easing, progress) => start + easing(progress) * (end - start)
const animate = (
{ ctx, width, height },
x,
y,
scaleRatio
) => {
let progressInTime = 0
const duration = 750
let prevTime
const loop = () => {
var now = new Date().getTime()
var dt = now - (prevTime || now)
prevTime = now
progressInTime += dt
const progress = progressInTime / duration
const changeInRadii = tween(0, scaleRatio, easing, progress)
const changeInOpacity = tween(0.25, 0, easing, progress)
ctx.clearRect(0, 0, width, height)
drawCircle(ctx, x, y, changeInRadii, changeInOpacity)
if (changeInRadii <= scaleRatio ) requestAnimationFrame(loop)
}
loop()
}
const main = (canvasEl, buttonEl) => {
// sizing
canvasEl.style.width ='100%'
canvasEl.style.height='100%'
const width = canvasEl.width = canvasEl.offsetWidth
const height = canvasEl.height = canvasEl.offsetHeight
const canvas = {
ctx: canvasEl.getContext('2d'),
width,
height
}
buttonEl.addEventListener('click', e => {
const x = e.offsetX
const y = e.offsetY
const w = e.target.offsetWidth
const h = e.target.offsetHeight
const offsetX = Math.abs( (w / 2) - x )
const offsetY = Math.abs( (h / 2) - y )
const deltaX = (w / 2) + offsetX
const deltaY = (h / 2) + offsetY
const scaleRatio = pyth(deltaX, deltaY)
animate(canvas, x, y, scaleRatio)
})
}
main(
document.getElementById('canvas'),
document.getElementById('button')
)
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
button {
background: none;
border: none;
background-color: #3E3F44;
padding: 30px 90px;
color: #fff;
font-size: 12px;
font-family: 'Raleway';
font-weight: bold;
letter-spacing: 2;
text-transform: uppercase;
border-radius: 5px;
position: relative;
}
canvas {
position: absolute;
top: 0;
left: 0;
}
</style>
<button type='button' id='button' name='button'>
Click To Ripple
<canvas id='canvas'></canvas>
</button>
<script src='index.js'></script>
const step = (
dt,
state,
duration = 750
) => {
const progressInTime = state.progressInTime + dt
const progress = progressInTime / duration
const changeInRadii = tween(0, state.scaleRatio, easing, progress)
const changeInOpacity = tween(0.25, 0, easing, progress)
return {
changeInOpacity,
changeInRadii,
progressInTime,
isDone: progress >== 1
}
}
const render = (
ctx,
changes
) => {
// side effect
drawCircle(
ctx,
changes.x,
changes.y,
changes.changeInRadii,
changes.changeInOpacity
)
}
const updates = (
dt,
state,
dispatch,
{ ctx, width, height } // canvas
) => {
ctx.clearRect(ctx, 0, 0, width, height)
return map(
(ripple, i) => {
const changes = step(dt, ripple)
changes.isDone
? return removeRipple(i)
: return updateRipple(i, changes)
},
ripples
)
}
const rafMiddleware = ({ dispatch, getState }) => next => {
let frame = null
return action => {
if (action.type !== RAF) return next(action)
const {
update,select, renderContext } = action
const loop = dt => {
const selectedState = select(getState())
if (isEmpty(selectedState)) {
frame ? cancelAnimationFrame(frame) : return
}
render(
renderContext,
selectedState
)
dispatch(update(
dt,
selectedState,
renderContext
))
frame = requestAnimationFrame(loop)
}
loop()
}
}
const afterMount = () => raf({
update: updates,
select: state => state.ripples,
renderContext: { canvas, width: canvas.width, height: canvas.height }
})
const render = () => {
return (
<button
onClick={e => addRipple({
x,
y,
scaleRatio
}))
}>
</button>
)
}
export {
afterMount,
render
}
import map from '@f/map-array'
const step = (
dt,
ripple,
duration = 750
) => {
const progressInTime = ripple.progressInTime + dt
const progress = progressInTime / duration
const changeInRadii = tween(0, ripple.scaleRatio, easing, progress)
const changeInOpacity = tween(0.25, 0, easing, progress)
return {
progressInTime,
changeInOpacity,
changeInRadii,
isDone: progress >== 1
}
}
const render = canvas => ripples => map(
(ripple, i) => {
drawCircle(
ctx,
ripple.x,
ripple.y,
ripple.radius
ripple.opacity
)
},
ripples
)
const animate = local => (
dt,
ripples,
) => map(
(ripple, i) => {
const changes = step(dt, ripple)
changes.isDone
? return local(removeRipple(i))
: return local(updateRipple(i, changes))
},
ripples
)
beforeUpdate = (prev, next) => {
const { ripples, canvas } = next.state
if (ripples.length == 0) {
rafCancel(next.state.rafId)
} else {
return [
bind(
rafUpdate(animate(next.local), ripples),
() => rafRender(render(ctx)),
next.local(storeRafId)(id)
)
]
}
}

Now as you can see on each click I trigger the animation but just one circle

drawCircle(ctx, x, y, changeInRadii, changeInOpacity)

My goal is to implement this with vdux and add multiple circles. The best way (I thought) is to save all the "ripple circles" in the redux local state. So onClick I need to add a ripple effect.

const addRipple = createAction('ADD_RIPPLE')
const removeRipple = createAction('REMOVE_RIPPLE')

const initialState = () => ({
  ripples: []
})

const reducer = combineActions({
  ripples: handleAction({
    [addRipple]: (state, action) => ([ ...state, action.payload ])
    [removeRipple]: (state, action) => ...
  })
})

After the animation finishes I also need to remove the ripple effect again from the array. So I need a way to dispatch an action.

// in the animate function like above
if (changeInRadii <= scaleRatio ) requestAnimationFrame(loop)

// change to
if (changeInRadii <= scaleRatio ) {
  requestAnimationFrame(loop)
} else {
  // dispatch to remove?
  dispatch(local(removeRipple()))
}

EDIT2: This kind of depends on what you need to actually do in your event handlers. I see that you're saving the value of canvas inside your render closure. What do you intend to do with it there?

I actually get the 2d context and setting the width dynamically. The context needs to be set in the animation loop.

const width = canvas.width  = canvas.offsetWidth
const height = canvas.height = canvas.offsetHeight

const ctx = canvas.getContext('2d')

There a couple of things that I wanted to achieve and solve with vdux

  • The requestAnimationFrame should stop if no circles are currently animating
  • There should always ever be one requestAnimationFrame running per component
  • Elegant way to manage the state of the current animations

So thats where I got stuck. If theres <0 ripples added, the requestanimationframe should be running. Then I need to keep pushing in ripple effects and remove then if they are done (750ms passed). How do you solve this with virtex-local? Again, there are different puzzles which don't fit together to me with this model. I hope this is enough to get the gist of what im trying to achieve and what's the problem here.

@ashaffer
Copy link

ashaffer commented Jan 1, 2016

When you say per component do you mean per instance or per abstract component?

@queckezz
Copy link
Author

queckezz commented Jan 1, 2016

Per instance, per drawing context.

@ashaffer
Copy link

ashaffer commented Jan 1, 2016

I think one way to do it is using beforeUpdate. You can do something like this:

import {raf, cancelAnimationFrame} from 'redux-effects-timeout'
import {bind} from 'redux-effects'

function beforeUpdate (prev, next) {
  if (next.state.ripples !== prev.state.ripples) {
    return [
      cancelAnimationFrame(next.state.rafId),
      bind(
        raf(animate(next.state.ripples)),
        next.local(storeRafHandle)
      )
    ]      
  }
}

function render () {
  let canvas
  return (
    <button onClick={local(e => addRipple(canvas, x, y)}>
      <canvas hook={node => canvas = node} />
    </button>
  )
}

const reducer = combineActions({
  rafId: handleActions({
    [storeRafId]: (state, id) => id
  }),
  ripples: handleActions({
    [addRipple]: (state, ripple) => ([ ...state, ripple])
    [removeRipple]: (state, action) => ...
  })
})

note: I switched the reducer creator to @f/handle-actions style, where just the payload is passed.

I think something along those lines will work? Your animate function will be able to return new actions (e.g. remove a ripple, cancel the animation loop, start the next frame). Let me know if i'm misunderstanding some aspect of the problem, I didn't actually make this work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment