A presentation at JSHeroes 2019 in in Cluj-Napoca, Romania by Sarah Drasner
THE Future of WEB Animation
@sarah_edo
Communicating @sarah_edo
? @sarah_edo
Happiness sadness Fear curiosity @sarah_edo
Happiness sadness Fear curiosity @sarah_edo
@sarah_edo
@sarah_edo
Anything can happen
SARAH DRASNER @SARAH_EDO
Hooks Composition Components css-tricks.com/what-hooks-mean-for-vue/
@sarah_edo
src/hooks/preventScroll.js import { useDestroyed, useMounted } from “vue-hooks”; export function preventscroll() { const preventDefault = (e) =>” { e = e || window.event; if (e.preventDefault) e.preventDefault(); e.returnValue = false; } // keycodes for left, up, right, down const keys = { 37: 1, 38: 1, 39: 1, 40: 1 }; const preventDefaultForScrollKeys = (e) =>” { if (keys[e.keyCode]) { preventDefault(e); return false; } } … } @sarah_edo
src/hooks/preventScroll.js useMounted(() =>” { if (window.addEventListener) // older FF window.addEventListener(‘DOMMouseScroll’, preventDefault, false); window.onwheel = preventDefault; // modern standard window.onmousewheel = document.onmousewheel = preventDefault; // older browsers, IE window.touchmove = preventDefault; // mobile window.touchstart = preventDefault; // mobile document.onkeydown = preventDefaultForScrollKeys; }); @sarah_edo
src/hooks/preventScroll.js useDestroyed(() =>” { if (window.removeEventListener) window.removeEventListener(‘DOMMouseScroll’, preventDefault, false); //firefox window.addEventListener(‘DOMMouseScroll’, (e) =>” { e.stopPropagation(); }, true); window.onmousewheel = document.onmousewheel = null; window.onwheel = null; window.touchmove = null; window.touchstart = null; document.onkeydown = null; }); @sarah_edo
src/components/BaseAppDetails.vue import { preventscroll } from “./../hooks/preventscroll.js”; import { lettering } from “./../hooks/lettering.js”; export default { … hooks() { preventscroll(); lettering(); } } @sarah_edo
• • They allows us to pass state from one to the other. They make it explicit where logic is coming from. css-tricks.com/what-hooks-mean-for-vue/ @sarah_edo
@sarah_edo
src/hooks/windowWidth.js import { useData, useMounted } from ‘vue-hooks’; export function windowwidth() { const data = useData({ width: 0 }) useMounted(() =>” { data.width = window.innerWidth }) // this is something we can consume with the other hook return { data } } @sarah_edo
src/hooks/logoLettering.js import { Elastic, TweenMax } from “gsap”; import { useMounted } from ‘vue-hooks’; //the data comes from the other hook export function logolettering(data) { useMounted(function () { if (data.data.width > 1200) { const logoname = this.$refs.logoname.childNodes; Splitting({ target: logoname, by: “chars” }); TweenMax.staggerFromTo( logoname, 5, { opacity: 0, transformOrigin: “50% 50% -30px”, cycle: { color: [“red”, “purple”, “teal”], rotationY(i) { return i * 50 } } }, … @sarah_edo
src/components/BaseAppNavigation.vue import AppLogo from “./AppLogo.vue”; import { logolettering } from “./../hooks/logolettering.js”; import { windowwidth } from “./../hooks/windowwidth.js”; export default { components: { AppLogo }, hooks() { logolettering(windowwidth()); } }; @sarah_edo
Article css-tricks.com/what-hooks-mean-for-vue/ Demo sdras.github.io/vue-hooks-foodapp/ Repo github.com/sdras/vue-hooks-foodapp @sarah_edo
page transitions
@sarah_edo
Cognitive Load Kathy Sierra: Badass, Making Users Awesome @sarah_edo
@sarah_edo
@sarah_edo
@sarah_edo
Templates in the pages directory <nuxt-link to=”/product”>Product</nuxt-link> @sarah_edo
@sarah_edo
Transition hook already available name=“page” .page-enter-active, .page-leave-active { transition: all .25s ease-out; } .page-enter, .page-leave-active { opacity: 0; transform: scale(0.95); transform-origin: 50% 50%; } .page-enter-active, .page-leave-active { transition: all .25s ease-out; } .page-enter, .page-leave-active { opacity: 0; transform: scale(0.95); transform-origin: 50% 50%; } @sarah_edo
@sarah_edo
@sarah_edo
In store/index.js import Vuex from ‘vuex’ const createStore = () =>” { return new Vuex.Store({ state: { page: ‘index’ }, mutations: { updatePage(state, pageName) { state.page = pageName } } }) } export default createStore @sarah_edo
In middleware/pages.js: export default function(context) { // go tell the store to update the page context.store.commit(‘updatePage’, context.route.name) } register the middleware in nuxt.config.js: module.exports = { … router: { middleware: ‘pages’ } } @sarah_edo
In layouts/default.vue: <template> <div> <app-navigation /> <nuxt/> </div> </template> In AppNavTransition.vue: <h2 key=”profile-name” class=”profile-name”> <span v-if=”page ===$$ ‘group’” class=”user-trip”>{{ selectedUser.trip }}</span> <span v-else>{{ selectedUser.name }}</span> </h2> @sarah_edo
<transition-group /> Flip under the hood Rosario’s article @sarah_edo
Transition Group wraps it… <transition-group name=”layout” tag=”g”> <rect class=”items rect” ref=”rect” key=”rect” width=”171” height=”171”/> … </transition-group> Styles: .items, .list-move { transition: all 0.4s ease; } .active { .rect { transform: translate3d(0, 30px, 0); } … } @sarah_edo
//animations .place { .follow { transform: translate3d(-215px, -80px, 0); } .profile-photo { transform: translate3d(-20px, -100px, 0) scale(0.75); } .profile-name { transform: translate3d(140px, -125px, 0) scale(0.75); color: white; } .side-icon { transform: translate3d(0, -40px, 0); background: rgba(255, 255, 255, 0.9); } .calendar { opacity: 1; } } @sarah_edo
@sarah_edo
Demo vue/nuxt page-transitions.com github.com/sdras/page-transitions-travelapp Article css-tricks.com/native-like-animations-for-pagetransitions-on-the-web/ @sarah_edo
@sarah_edo
@sarah_edo
@sarah_edo
@joshcarpenter @sarah_edo
@joshcarpenter @sarah_edo
@joshcarpenter @sarah_edo
@joshcarpenter @sarah_edo
Index page design inspired from this dribbble shot @sarah_edo
methods: { startDrag() { this.dragging = true this.x = this.y = 0 }, stopDrag() { this.dragging = false }, coords(e) { if (this.page ===$$ ‘vrview’ && this.dragging) { this.x = (e.clientX || e.touches[0].clientX) / 60 + 4 this.y = (e.clientY || e.touches[0].clientY) / 100 + 5 } }, … } @sarah_edo
this.scene = new THREE.Scene() var geometry = new THREE.SphereBufferGeometry(500, 60, 40) // invert the geometry on the x-axis so that all of the faces point inward geometry.scale(-1, 1, 1) var material = new THREE.MeshBasicMaterial({ map: new THREE.TextureLoader().load(imgsrc) }) mesh = new THREE.Mesh(geometry, material) this.scene.add(mesh) @sarah_edo
update() { lat = Math.max(-85, Math.min(85, lat)) phi = THREE.Math.degToRad(90 - lat) theta = THREE.Math.degToRad(lon) camera.target.x = 500 * Math.sin(phi) * Math.cos(theta) camera.target.y = 500 * Math.cos(phi) camera.target.z = 500 * Math.sin(phi) * Math.sin(theta) camera.lookAt(camera.target) renderer.render(scene, camera) } @sarah_edo
div { width: 500px; height: 500px; } border-radius: 50%; } @media vr { div { material: grass; } } @sarah_edo
sphere { material: envmap(0.9) reflectivity(0.5); } threejs.org/examples @sarah_edo
@joshcarpenter @sarah_edo
@sarah_edo
@sarah_edo
ARKit 2.0 @OsFalmer @sarah_edo
@sarah_edo
Daniel @danielkuntz0 @sarah_edo
AR Expedition @sarah_edo
static 2d is an abstraction
Minh Pham on Dribbble @sarah_edo
@sarah_edo
@sarah_edo
Voice controlled Experiences on the web
@sarah_edo
Made in collaboration with Brian Holt @holtbt @sarah_edo
In store.js (Vuex) export default new Vuex.Store({ state: { counter: 0, intent: ‘None’, intensity: ‘None’, score: 0, // idle - awaiting user input // listening - listening to user input // fetching - fetching user data from the API uiState: ‘idle’, zoom: 3 }, getters: { intentStr: state => { var str = state.intent str = str.replace(/\b(App.)\b/gi, ”) return str }, @sarah_edo
In store.js (Vuex) actions: { getSpeech({ dispatch, commit, state }) { commit(‘setUiState’, ‘listening’) //keep recording speech all the time or activate it//for the first screen no, press a button. second screen yes. state.intent === ‘None’ ? (recognition.continuous = true) : (recognition.continuous = false) recognition.start() … }, recognition.onresult = function(event) { const last = event.results.length - 1 const phrase = event.results[last][0].transcript dispatch(‘getUnderstanding’, phrase) } @sarah_edo
In store.js (Vuex) getUnderstanding({ commit }, utterance) { commit(‘setUiState’, ‘fetching’) const url = https://westus.api.cognitive.microsoft.com/luis/v2.0/apps /4aba2274-c5df-4b0d-8ff7-57658254d042
https: axios({ method: ‘get’, url, params: { verbose: true, timezoneOffset: 0, q: utterance }, headers: { ‘Content-Type’: ‘application/json’, ‘Ocp-Apim-Subscription-Key’: ‘XXXXXXXXXXXXXXXXXXX’ } })
@sarah_edo
In store.js (Vuex) .then(({ data }) => { console.log(‘axios result’, data) if (altMaps.hasOwnProperty(data.query)) { commit(‘newIntent’, { intent: altMaps[data.query], score: 1 }) } else { commit(‘newIntent’, data.topScoringIntent) } commit(‘setUiState’, ‘idle’) commit(‘setZoom’) }) .catch(err => { console.error(‘axios error’, err) }) @sarah_edo
@sarah_edo
@sarah_edo
In store.js (Vuex) mutations: { newIntent: (state, {intent, score}) => { if (intent.includes(‘Intensity’)) { state.intensity = intent if (intent.includes(‘More’)) { state.counter++ } else if (intent.includes(‘Less’)) { state.counter-} } else { state.intent = intent } state.score = score }, … } @sarah_edo
In Base.vue (Components) createShapes() { this.bufferCamera.position.z = this.shapeZoom if (this.torusKnot !== null) { this.torusKnot.material.dispose() this.torusKnot.geometry.dispose() this.bufferScene.remove(this.torusKnot) } var shape = new THREE.TorusKnotGeometry( this.tConfig.a, this.tConfig.b, this.tConfig.c, this.tConfig.d ), material … this.torusKnot = new THREE.Mesh(shape, material) this.torusKnot.material.needsUpdate = true this.bufferScene.add(this.torusKnot) }, @sarah_edo
In Base.vue (Components) animate() { this.storeRAF = requestAnimationFrame(this.animate) this.bufferScene.rotation.x += 0.01 this.bufferScene.rotation.y += 0.02 this.renderer.render( this.bufferScene, this.bufferCamera, this.bufferTexture ) this.renderer.render(this.scene, this.camera) }, @sarah_edo
In Base.vue (Components) watch: { shapeZoom() { this.createShapes() cancelAnimationFrame(this.storeRAF) this.animate() } }, Repo: github.com/sdras/three-vue-pattern @sarah_edo
@sarah_edo
“We are in a rut. A rut shaped like this” -Jen Simmons @sarah_edo
Take up space
Think bigger
Thank you! @sarah_edo
We talk a lot about how to animate on the web, and what’s possible in browsers today, but where is animation heading? In this talk, we’ll start with some bleeding edge techniques such as native-like page transitions with nuxt, but then we’ll push it further. The intersection of health and animation with biofeedback sensors and Vue, the future of 3d in the browser complete with interviews with people who are writing these specs… this talk will show that in terms of animation on the web, we’re just getting started.