vuetamin
v0.0.3
Published
Central animation loop and state management
Downloads
11
Maintainers
Readme
vuetamin - Central animation loop and state management
THIS IS A PROOF OF CONCEPT
Although I am using this in one of my Vue projects, this Vue plugin is nothing more than an experiment, it's not tested thoroughly and might change any time. I do plan however to get this in a state where it's safe to use and well tested.
Features
- Run methods from multiple components in a single requestAnimationFrame loop
- Central state management
- For every rAF loop all methods will receive the exact same state
- Prevent unnecessary redraws
Concepts
Basic
Instead of making assumptions about your state, Vuetamin does not do anything by itself. It offers mutations, which similiar to vuex provide a structure to change data. It does not observe changes. It's your task to queue up what needs to be run.
Manual
To do something with the Vuetamin data in your components, you need to specify a method and a thread in your component. A thread is kind of like an event. It could be named "position_change". When mutating the data in the store, you have to trigger a thread. Vuetamin then queues this thread and all its methods for the next loop.
Performance
A method can subscribe to multiple threads. Vuetamin will make sure that for every loop a method is only called once.
Why not...?
There are several ways to share data across multiple components. And for most cases they are more than enough and perform very well. Using vuex would work, or just plain Vue and passing data to components via props.
Sharing data via events also seems like a good idea. Components would listen for certain events and then redraw or rerender.
The problem with all these options is that you would still need an animation loop in every component and then "buffer" the incoming data via the components own reactive data.
- Multiple animation loops can be out of sync
- It can be tricky and complex to prevent unnecessary redraws
- The structure can get quite messy since it's not clear where data is coming from
- To mitigate performance issues you might consider debouncing events, which introduces a lag
Usage
npm
Install Vuetamin from npm.
npm install --save vuetamin
Install the plugin in your Vue app.
import Vue from 'vue'
import App from './App.vue'
import Vuetamin from 'vuetamin'
import { store } from './store'
Vue.use(Vuetamin, { store })
new Vue({
render: h => h(App)
}).$mount('#app')
CDN
<head>
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
<script src="https://unpkg.com/vuetamin/dist/vuetamin.min.js"></script>
</head>
<body>
<div id="app"></div>
<script>
new Vue({ el: '#app' })
</script>
</body>
The store object
The store.js file should look something like this.
/**
* Optional. Put mutation and action names as an object to share them
* across multiple files.
*/
export const threads = {
POSITION: 'position',
COLOR: 'color'
}
/**
* Export your store as an object.
*/
export const store = {
/**
* This is where all data is stored. It must be a function returning
* a single object. You can set the initial values directly or run
* additional code beforehand. This will only be called when Vuetamin
* is instanciating its store.
*/
data: function () {
const viewport = {
width: window.innerWidth,
height: window.innerHeight
}
return {
position: {
x: viewport.width / 2,
y: viewport.height /2
},
color: '#ff4302',
viewport: viewport,
timeout: null
}
},
/**
* The state function is called before every animation loop. Its return
* value is passed to all methods in the queue.
* The state function receives the store data as an argument. Though it's
* possible to mutate values in data, it's a bad idea.
*
* You can do additional calculations here based on the data. Keep in mind
* that this function will be called for every loop, so it's a good idea to
* do expensive calculations in a mutation and store the result in data.
*
* The function can return anything from a single value, to an object or
* an array. Technically even a function..?
*/
state: function (data) {
return {
color: data.color,
position: data.position,
isLeft: data.position.x < (data.viewport.width / 2)
}
},
/**
* Mutations change a single value. In the first parameter you have access
* to the context, consisting of { data, trigger, mutate, action }.
*
* It's possible to call another mutation or action from here, but for
* that it's probably a better idea to write an action that calls one
* or multiple mutations and insted call this action from somewhere.
*
* The second argument is whatever has been passed to the mutation.
*
* The trigger function takes the name of a thread as its argument
* and puts all handlers from this thread to the current animation queue.
*/
mutations: {
updatePosition: function ({ data, trigger }, newPosition) {
data.position = newPosition
trigger(threads.POSITION)
},
updateColor: function ({ data, trigger }, newColor) {
data.color = color
trigger(threads.COLOR)
},
updateViewport: function ({ data, action }, newViewport) {
data.viewport = newViewport
action('redrawAll')
}
},
/**
* Actions don't differ from mutations much. They also get the same context
* and it's possible to pass a value/payload to an action.
*
* They are meant to be used when multiple mutations need to be called or
* when the function otherwise behaves a bit differently, like doing things
* outside of the store.
*
* In this example, the redrawAll action is being used to trigger all threads
* once, after some time has passed. Here this is used to not immediately redraw
* every canvas in the app when the viewport changes.
*/
actions: {
redrawAll: function ({ data, trigger }, { noTimeout } = {}) {
const timeout = noTimeout ? 0 : 1000
window.clearTimeout(data.timeout)
data.timeout = window.setTimeout(() => {
threads.forEach(thread => trigger(thread))
}, timeout)
}
}
}
Now you're ready to use Vuetamin in any component you like. Here is an example of a component which displays the current {x,y} position on a canvas. It also stores a variable from the Vuetamin store in the local data of the component, which is then used in a computed property.
<template>
<canvas ref="canvas" :style="backgroundColor"></canvas>
</template>
import { threads } from '@/store'
export default {
/**
* Using the vuetamin property you can attach methods that will
* be called for when the specified thread is triggered.
*
* It has to be an object, with its properties being the name of
* the method and the value an array of thread names. You can also pass a
* string if you only need a single thread.
*/
vuetamin: {
drawCursor: [threads.POSITION],
drawBackground: threads.COLOR
},
/**
* Define your methods as usual. They get passed the state as the only
* parameter.
*/
methods: {
/**
* This method will draw the position on the canvas.
*/
drawCursor (state) {
const ctx = this.$refs.canvas.getContext('2d')
ctx.clearRect(0, 0, state.viewport.width, state.viewport.height)
ctx.beginPath()
ctx.fillStyle = state.color
ctx.arc(state.position.x, state.position.y, 8, 0, Math.PI * 2, true)
ctx.fill()
},
/**
* This method will pass a value from the Vuetamin state to the local data
* of the Vue component.
*/
drawBackground (state) {
this.background = state.color
}
}
/**
* You can locally store values from Vuetamin in your component, to use them
* in templates, computed properties, etc.
*/
data () {
return {
background: '#FFFFFF'
}
},
computed: {
canvasStyle () {
return {
backgroundColor: this.background
}
}
}
}
API
To interact with Vuetamin in your app, you can use the global this.$vuetamin
.
mutation
this.$vuetamin.store.mutate('updateColor', color)
Get data and state
You also have access to the store data and state:
this.$vuetamin.store.getData()
this.$vuetamin.store.getState()
Trigger a thread
Just like in a mutation, you can also trigger a thread from within your app:
this.$vuetamin.trigger('color_changed')
Add or remove component
It's possible to manually add or remove a component. Just call the following functions and pass the component:
this.$vuetamin.addComponent(this)
this.$vuetamin.removeComponent(this)