Created
May 16, 2019 14:10
-
-
Save thomaswilburn/9790d8d8ea6c77ba6f1171c34236a48a to your computer and use it in GitHub Desktop.
Code excerpts from White Lies
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var directors = { | |
"image": require("./image-scene"), | |
"audio": require("./audio-scene"), | |
"map": require("./map-scene") | |
}; | |
// state of the last block and director | |
var lastBlock = null; | |
var director = null; | |
// check blocks in reverse order | |
var sceneElements = $(".block").reverse(); | |
var threshold = .9; | |
// called to check scroll position | |
var onScroll = function() { | |
var entered = null; | |
for (var i = 0; i < sceneElements.length; i++) { | |
var block = sceneElements[i]; | |
// where is this block on screen? | |
var bounds = block.getBoundingClientRect(); | |
if ((bounds.top < window.innerHeight * threshold) && bounds.bottom > 0) { | |
// we found the active block | |
// skip updates if it's the same as last time | |
if (block == lastBlock) return; | |
var on = block.dataset.type; | |
// if we changed block type, let the previous director leave | |
if (director && director != directors[on]) { | |
director.exit(); | |
} | |
// trigger the new director | |
director = directors[on]; | |
entered = director.enter(block); | |
lastBlock = block; | |
// leave the loop | |
break; | |
} | |
} | |
// wipe all active scenes | |
$(".scene.active").forEach(el => el.classList.remove("active")); | |
// if a director entered, set its scene element to active | |
if (entered) { | |
entered.classList.add("active"); | |
} else { | |
// if we hit a non-directed block or left the scroll... | |
lastBlock = null; | |
if (director) { | |
director.exit(); | |
director = null; | |
} | |
} | |
}; | |
window.addEventListener("scroll", debounce(onScroll, 50)); | |
onScroll(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var $ = require("./lib/qsa"); | |
var flags = require("./flags") | |
var mapElement = $.one(".map.scene"); | |
var svg = $.one("svg", mapElement); | |
var audioDirector = require("./audio-scene"); | |
var fullMap = svg.getAttribute("viewBox").split(" ").map(Number); | |
// find the midpoint between two points | |
var between = (a, b) => a.map((v, i) => (v + b[i]) / 2); | |
// create a rectangular zone around a point | |
var around = ([x, y], buffer) => [x - buffer, y - buffer, buffer * 2, buffer * 2]; | |
// create a bounding box containing multiple layers | |
var combine = function(layers, padding = 0) { | |
var bounds = { | |
x: Infinity, | |
y: Infinity, | |
x2: -Infinity, | |
y2: -Infinity | |
}; | |
layers.forEach(function(l) { | |
var box = l.getBBox(); | |
var bottom = box.y + box.height; | |
var right = box.x + box.width; | |
if (box.x < bounds.x) bounds.x = box.x; | |
if (box.y < bounds.y) bounds.y = box.y; | |
if (right > bounds.x2) bounds.x2 = right; | |
if (bottom > bounds.y2) bounds.y2 = bottom; | |
}); | |
bounds.width = bounds.x2 - bounds.x; | |
bounds.height = bounds.y2 - bounds.y; | |
bounds.x -= padding; | |
bounds.y -= padding; | |
bounds.width += padding * 2; | |
bounds.height += padding * 2; | |
return [ bounds.x, bounds.y, bounds.width, bounds.height ]; | |
}; | |
var zooms = { | |
exitWalkers: combine($(".starting, .walkers, .silver-moon", svg), 200), | |
walking: combine($(".starting, .walkers, .silver-moon", svg), 200), | |
passingSilverMoon: combine($(".walking, .silver-moon", svg), 200), | |
attack: combine($(".attack"), 200) | |
}; | |
// animate viewbox transitions | |
var tween = null; | |
var tweenViewbox = function(destination, duration = 6000) { | |
if (tween) window.cancelAnimationFrame(tween); | |
// if prefers-reduced-motion, just jump right to the end | |
if (flags.prefersReducedMotion) { | |
return svg.setAttribute("viewBox", destination.join(" ")); | |
} | |
// find the delta for the animation | |
var start = svg.getAttribute("viewBox").split(" ").map(Number); | |
var change = start.map((v, i) => destination[i] - v); | |
var began = Date.now(); | |
// animation update function | |
var tick = function() { | |
var now = Date.now(); | |
var elapsed = now - began; | |
var ratio = elapsed / duration; | |
if (ratio >= 1) return; | |
// easing stolen from jQuery | |
var eased = .5 - Math.cos(ratio * Math.PI) / 2; | |
var delta = change.map((v, i) => v * eased + start[i]); | |
svg.setAttribute("viewBox", delta.join(" ")); | |
tween = requestAnimationFrame(tick); | |
}; | |
tick(); | |
}; | |
var enter = function(block) { | |
audioDirector.exit(); | |
if (block.classList.contains("has-audio")) audioDirector.enter(block); | |
var location = block.dataset.location; | |
if (zooms[location]) { | |
tweenViewbox(zooms[location]); | |
} else { | |
tweenViewbox(fullMap); | |
} | |
// trigger visibility for optional layers via CSS | |
svg.setAttribute("data-location", location); | |
return mapElement; | |
}; | |
var exit = function() { | |
if (tween) { | |
window.cancelAnimationFrame(tween); | |
svg.setAttribute("viewBox", fullMap.join(" ")); | |
} | |
audioDirector.exit(); | |
}; | |
module.exports = { enter, exit }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var $ = require("./lib/qsa"); | |
var trackEvent = require("./lib/tracking"); | |
var element = $.one(".audio-player"); | |
var isSafari = navigator.userAgent.match(/i(phone|pad)/i); | |
// our Player prototype | |
// call the constructor with the element containing its UI | |
var Player = function(element) { | |
this.element = element; | |
this.autoplay = $.one(".autoplay", element); | |
this.playButton = $.one(".play-pause", element); | |
this.progress = $.one(".ring", element); | |
this.title = $.one(".label .content", element); | |
this.bug = $.one(".scroll-bug", element); | |
this.proxy = $.one("audio", element); | |
// bind event listeners to the instance | |
// these are "wrapped" to re-dispatch events to the source track in Safari | |
"onTime onPlay onPause onEnd" | |
.split(" ") | |
.forEach(m => this[m] = this.wrapProxyEvents(this[m]).bind(this)); | |
// bind UI event listeners | |
"onAutoplay onClickedPlay" | |
.split(" ") | |
.forEach(m => this[m] = this[m].bind(this)); | |
this.autoplay.checked = false; | |
this.autoplay.addEventListener("change", this.onAutoplay); | |
this.playButton.addEventListener("click", this.onClickedPlay); | |
if (isSafari) { | |
console.log("Safari detected - using proxy audio track for playback"); | |
this.bindTrack(this.proxy); | |
} | |
document.addEventListener("keyup", e => { | |
if (e.code == "KeyP") this.playButton.click(); | |
if (e.code == "KeyA") this.autoplay.click(); | |
}); | |
}; | |
Player.prototype = { | |
track: null, | |
element: null, | |
autoplay: null, | |
playButton: null, | |
progress: null, | |
title: null, | |
bug: null, | |
proxy: null, | |
// simple getter for play/pause state | |
get paused() { | |
if (!this.track) return true; | |
return isSafari ? this.proxy.paused : this.track.paused; | |
}, | |
play(track) { | |
this.cue(track); | |
this.bug.classList.remove("show"); | |
if (track.paused) { | |
this.startAudio(track); | |
} | |
this.playButton.setAttribute("aria-pressed", "true"); | |
}, | |
startAudio(track) { | |
track.dataset.played = true; | |
if (isSafari) { | |
this.proxy.src = track.src; | |
track = this.proxy; | |
} | |
track.currentTime = 0; | |
var promise = track.play(); | |
if (promise) { | |
promise.catch(err => console.log(err)); | |
} | |
}, | |
// cue() assigns the track and gets it ready to play | |
cue(track) { | |
if (track != this.track) { | |
this.stop(); | |
// listen for timeupdate and play/pause events | |
// in safari, we just keep listening to the proxy | |
if (!isSafari) { | |
if (this.track) this.unbindTrack(this.track); | |
this.bindTrack(track); | |
} | |
} | |
this.track = track; | |
track.volume = 1; | |
// set the metadata | |
this.title.innerHTML = track.dataset.title; | |
}, | |
stop() { | |
if (!this.track) return; | |
this.element.classList.remove("playing"); | |
this.track.pause(); | |
this.proxy.pause(); | |
this.title.innerHTML = ""; | |
this.playButton.setAttribute("aria-pressed", "false"); | |
}, | |
show() { | |
this.bug.classList.remove("show"); | |
this.element.classList.remove("hidden"); | |
}, | |
hide() { | |
this.element.classList.add("hidden"); | |
this.bug.classList.remove("show"); | |
}, | |
bindTrack(track) { | |
track.addEventListener("timeupdate", this.onTime); | |
track.addEventListener("play", this.onPlay); | |
track.addEventListener("pause", this.onPause); | |
track.addEventListener("ended", this.onEnd); | |
}, | |
unbindTrack(track) { | |
track.removeEventListener("timeupdate", this.onTime); | |
track.removeEventListener("play", this.onPlay); | |
track.removeEventListener("pause", this.onPause); | |
track.removeEventListener("ended", this.onEnd); | |
}, | |
// the play button is the only place that we can "pause" | |
// so its logic is slightly different from play() | |
onClickedPlay() { | |
if (!this.track) return; | |
var track = isSafari ? this.proxy : this.track; | |
trackEvent("play-button", track.paused ? "play" : "pause"); | |
if (!track.paused) { | |
track.pause(); | |
this.playButton.setAttribute("aria-pressed", "false"); | |
// autoplay.checked = false; | |
} else { | |
track.volume = 1; | |
track.play(); | |
this.playButton.setAttribute("aria-pressed", "true"); | |
} | |
}, | |
onAutoplay() { | |
var checked = this.autoplay.checked; | |
document.body.classList.toggle("autoplay-on", checked); | |
trackEvent("autoplay-toggled", checked); | |
if (!checked) { | |
this.stop(); | |
} else if (this.track && this.track.paused) { | |
this.play(this.track); | |
} | |
}, | |
// update the progress ring | |
onTime() { | |
var track = isSafari ? this.proxy : this.track; | |
var ratio = track.currentTime / track.duration; | |
this.progress.style.strokeDashoffset = ratio * Math.PI * 30; | |
}, | |
onPlay() { | |
this.element.classList.add("playing"); | |
this.show(); | |
}, | |
onPause() { | |
}, | |
onEnd() { | |
this.element.classList.remove("playing"); | |
this.bug.classList.add("show"); | |
}, | |
// handle events in Safari | |
// we still want events fired on the block audio tracks for transcript animations | |
// however, they're not actually doing the playback in Safari | |
// this function wraps our listeners up so that they re-dispatch fake events | |
// when attached to the proxy, they create matching events on the "playing" audio element | |
wrapProxyEvents(f) { | |
if (!isSafari) return f; | |
return function(event) { | |
f.call(this, event); | |
var e = new CustomEvent(event.type); | |
this.track.currentTime = this.proxy.currentTime; | |
this.track.dispatchEvent(e); | |
} | |
} | |
}; | |
module.exports = new Player(element); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment