mlncn-presentations/future-here/stack.v0.js
2015-07-30 15:24:39 -04:00

219 lines
6.3 KiB
JavaScript

var stack = (function() {
var stack = {},
event = d3.dispatch("activate", "deactivate"),
section = d3.selectAll("section"),
self = d3.select(window),
body = document.body,
root = document.documentElement,
timeout,
duration = 250,
ease = "cubic-in-out",
screenY,
size,
yActual,
yFloor,
yTarget,
yActive = -1,
yMax,
yOffset,
n = section[0].length;
// Invert the z-index so the earliest slides are on top.
section.classed("stack", true).style("z-index", function(d, i) { return n - i; });
// Detect the slide height (by showing an active slide).
section.classed("active", true);
size = section.node().getBoundingClientRect().height;
section.classed("active", false);
// Sets the stack position.
stack.position = function(y1) {
var y0 = body.scrollTop / size;
if (arguments.length < 1) return y0;
// clamp and round
if (y1 >= n) y1 = n - 1;
else if (y1 < 0) y1 = Math.max(0, n + y1);
y1 = Math.floor(y1);
if (y0 - y1) {
self.on("scroll.stack", null);
leap(y1);
d3.select(body).transition()
.duration(duration)
.ease(ease)
.tween("scrollTop", tween(yTarget = y1))
.each("end", function() { yTarget = null; self.on("scroll.stack", scroll); });
}
location.replace("#" + y1);
return stack;
};
// Don't do anything fancy for iOS.
if (section.style("display") == "block") return;
self
.on("keydown.stack", keydown)
.on("resize.stack", resize)
.on("scroll.stack", scroll)
.on("mousemove.stack", snap)
.on("hashchange.stack", hashchange);
resize();
hashchange();
scroll();
// if scrolling up, jump to edge of previous slide
function leap(yNew) {
if ((yActual < n - 1) && (yActual == yFloor) && (yNew < yActual)) {
yActual -= .5 - yOffset / size / 2;
scrollTo(0, yActual * size);
reactivate();
return true;
}
}
function reactivate() {
var yNewActive = Math.floor(yActual) + (yActual % 1 ? .5 : 0);
if (yNewActive !== yActive) {
var yNewActives = {};
yNewActives[Math.floor(yNewActive)] = 1;
yNewActives[Math.ceil(yNewActive)] = 1;
if (yActive >= 0) {
var yOldActives = {};
yOldActives[Math.floor(yActive)] = 1;
yOldActives[Math.ceil(yActive)] = 1;
for (var i in yOldActives) {
if (i in yNewActives) delete yNewActives[i];
else event.deactivate.call(section[0][+i], +i);
}
}
for (var i in yNewActives) {
event.activate.call(section[0][+i], +i);
}
yActive = yNewActive;
}
}
function resize() {
yOffset = (window.innerHeight - size) / 2;
yMax = 1 + yOffset / size;
d3.select(body)
.style("margin-top", yOffset + "px")
.style("margin-bottom", yOffset + "px")
.style("height", (n - .5) * size + yOffset + "px");
}
function hashchange() {
var hash = +location.hash.slice(1);
if (!isNaN(hash) && hash !== yFloor) stack.position(hash);
}
function keydown() {
var delta;
switch (d3.event.keyCode) {
case 39: // right arrow
if (d3.event.metaKey) return;
case 40: // down arrow
case 34: // page down
delta = d3.event.metaKey ? Infinity : 1; break;
case 37: // left arrow
if (d3.event.metaKey) return;
case 38: // up arrow
case 33: // page up
delta = d3.event.metaKey ? -Infinity : -1; break;
case 32: // space
delta = d3.event.shiftKey ? -1 : 1;
break;
default: return;
}
if (timeout) timeout = clearTimeout(timeout);
if (yTarget == null) yTarget = (delta > 0 ? Math.floor : Math.ceil)(yActual == yFloor ? yFloor : yActual + (.5 - yOffset / size / 2));
stack.position(yTarget = Math.max(0, Math.min(n - 1, yTarget + delta)));
d3.event.preventDefault();
}
function scroll() {
// Detect whether to scroll with documentElement or body.
if (body !== root && root.scrollTop) body = root;
var yNew = Math.max(0, body.scrollTop / size);
if (yNew >= n - 1.51 + yOffset / size) yNew = n - 1;
// if scrolling up, jump to edge of previous slide
if (leap(yNew)) return;
var yNewFloor = Math.max(0, Math.floor(yActual = yNew)),
yError = Math.min(yMax, (yActual % 1) * 2);
if (yFloor != yNewFloor) {
location.replace("#" + yNewFloor);
yFloor = yNewFloor;
}
section
.classed("active", false);
d3.select(section[0][yFloor])
.style("-webkit-transform", yError ? "translate3d(0," + (-yError * size) + "px,0)" : null)
.style("-o-transform", yError ? "translate(0," + (-yError * size) + "px)" : null)
.style("-moz-transform", yError ? "translate(0," + (-yError * size) + "px)" : null)
.style("transform", yError ? "translate(0," + (-yError * size) + "px)" : null)
.classed("active", yError != yMax);
d3.select(section[0][yFloor + 1])
.style("-webkit-transform", yError ? "translate3d(0,0,0)" : null)
.style("-o-transform", yError ? "translate(0,0)" : null)
.style("-moz-transform", yError ? "translate(0,0)" : null)
.style("transform", yError ? "translate(0,0)" : null)
.classed("active", yError > 0);
reactivate();
}
function snap() {
var y = d3.event.clientY;
if (y === screenY) return; // ignore move on scroll
screenY = y;
if (yTarget != null) return; // don't snap if already snapping
var y0 = stack.position(),
y1 = Math.max(0, Math.round(y0 + .25));
// if we're before the first slide, or after the last slide, do nothing
if (y0 <= 0 || y0 >= n - 1.51 + yOffset / size) return;
// if the previous slide is not visible, immediate jump
if (y1 > y0 && y1 - y0 < .5 - yOffset / size) scrollTo(0, y1 * size);
// else transition
else if (y1 !== y0) stack.position(y1);
}
function tween(y) {
return function() {
var i = d3.interpolateNumber(this.scrollTop, y * size);
return function(t) { scrollTo(0, i(t)); scroll(); };
};
}
stack.duration = function(_) {
if (!arguments.length) return duration;
duration = _;
return stack;
};
stack.ease = function(_) {
if (!arguments.length) return ease;
ease = _;
return stack;
};
d3.rebind(stack, event, "on");
return stack;
})();