The Future of Web Animation

A presentation at VueConf US in March 2019 in Tampa, FL, USA by Sarah Drasner

Slide 1

Slide 1

THE Future of WEB Animation

Slide 2

Slide 2

@sarah_edo

Slide 3

Slide 3

Communicating @sarah_edo

Slide 4

Slide 4

? @sarah_edo

Slide 5

Slide 5

Happiness sadness Fear curiosity @sarah_edo

Slide 6

Slide 6

Happiness sadness Fear curiosity @sarah_edo

Slide 7

Slide 7

@sarah_edo

Slide 8

Slide 8

@sarah_edo

Slide 9

Slide 9

Anything can happen

Slide 10

Slide 10

SARAH DRASNER @SARAH_EDO

Slide 11

Slide 11

Hooks Composition Components css-tricks.com/what-hooks-mean-for-vue/

Slide 12

Slide 12

@sarah_edo

Slide 13

Slide 13

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

Slide 14

Slide 14

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

Slide 15

Slide 15

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

Slide 16

Slide 16

src/components/BaseAppDetails.vue import { preventscroll } from “./../hooks/preventscroll.js”; import { lettering } from “./../hooks/lettering.js”; export default { … hooks() { preventscroll(); lettering(); } } @sarah_edo

Slide 17

Slide 17

• • 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

Slide 18

Slide 18

@sarah_edo

Slide 19

Slide 19

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

Slide 20

Slide 20

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

Slide 21

Slide 21

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

Slide 22

Slide 22

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

Slide 23

Slide 23

page transitions

Slide 24

Slide 24

@sarah_edo

Slide 25

Slide 25

Cognitive Load Kathy Sierra: Badass, Making Users Awesome @sarah_edo

Slide 26

Slide 26

@sarah_edo

Slide 27

Slide 27

@sarah_edo

Slide 28

Slide 28

@sarah_edo

Slide 29

Slide 29

Templates in the pages directory <nuxt-link to=”/product”>Product</nuxt-link> @sarah_edo

Slide 30

Slide 30

@sarah_edo

Slide 31

Slide 31

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

Slide 32

Slide 32

@sarah_edo

Slide 33

Slide 33

@sarah_edo

Slide 34

Slide 34

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

Slide 35

Slide 35

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

Slide 36

Slide 36

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

Slide 37

Slide 37

<transition-group /> Flip under the hood Rosario’s article @sarah_edo

Slide 38

Slide 38

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

Slide 39

Slide 39

//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

Slide 40

Slide 40

@sarah_edo

Slide 41

Slide 41

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

Slide 42

Slide 42

@sarah_edo

Slide 43

Slide 43

@sarah_edo

Slide 44

Slide 44

@sarah_edo

Slide 45

Slide 45

@joshcarpenter @sarah_edo

Slide 46

Slide 46

@joshcarpenter @sarah_edo

Slide 47

Slide 47

@joshcarpenter @sarah_edo

Slide 48

Slide 48

@joshcarpenter @sarah_edo

Slide 49

Slide 49

Index page design inspired from this dribbble shot @sarah_edo

Slide 50

Slide 50

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

Slide 51

Slide 51

<div v-if=”page ===$$ ‘vrview’” :style=”{ perspectiveOrigin: `${x}% ${y}%`, transform: `rotateY(${x * 4}deg)` }”> <app-nav /> </div> <app-nav v-else/> <nuxt /> <div class=”movearound” v-if=”page ===$$ ‘indview’ || page ===$$ ‘vrview’” :style=”{ perspectiveOrigin: `${x/2}% ${y/2}%`, transform: `rotateY(${x*4}deg) translate3d(0, 0, 0)` }”> <app-bottommain :x=”x” :y=”y” /> </div> @sarah_edo

Slide 52

Slide 52

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

Slide 53

Slide 53

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

Slide 54

Slide 54

div { width: 500px; height: 500px; } border-radius: 50%; } @media vr { div { material: grass; } } @sarah_edo

Slide 55

Slide 55

sphere { material: envmap(0.9) reflectivity(0.5); } threejs.org/examples @sarah_edo

Slide 56

Slide 56

@joshcarpenter @sarah_edo

Slide 57

Slide 57

@sarah_edo

Slide 58

Slide 58

@sarah_edo

Slide 59

Slide 59

ARKit 2.0 @OsFalmer @sarah_edo

Slide 60

Slide 60

@sarah_edo

Slide 61

Slide 61

Daniel @danielkuntz0 @sarah_edo

Slide 62

Slide 62

AR Expedition @sarah_edo

Slide 63

Slide 63

static 2d is an abstraction

Slide 64

Slide 64

Minh Pham on Dribbble @sarah_edo

Slide 65

Slide 65

@sarah_edo

Slide 66

Slide 66

@sarah_edo

Slide 67

Slide 67

Voice controlled Experiences on the web

Slide 68

Slide 68

@sarah_edo

Slide 69

Slide 69

Made in collaboration with Brian Holt @holtbt @sarah_edo

Slide 70

Slide 70

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

Slide 71

Slide 71

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

Slide 72

Slide 72

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

Slide 73

Slide 73

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

Slide 74

Slide 74

@sarah_edo

Slide 75

Slide 75

@sarah_edo

Slide 76

Slide 76

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

Slide 77

Slide 77

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

Slide 78

Slide 78

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

Slide 79

Slide 79

In Base.vue (Components) watch: { shapeZoom() { this.createShapes() cancelAnimationFrame(this.storeRAF) this.animate() } }, Repo: github.com/sdras/three-vue-pattern @sarah_edo

Slide 80

Slide 80

@sarah_edo

Slide 81

Slide 81

“We are in a rut. A rut shaped like this” -Jen Simmons @sarah_edo

Slide 82

Slide 82

Slide 83

Slide 83

Take up space

Slide 84

Slide 84

Think bigger

Slide 85

Slide 85

Thank you! @sarah_edo