|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1"> |
|
<meta name="apple-mobile-web-app-capable" content="yes"> |
|
|
|
<style> |
|
|
|
* { |
|
box-sizing: border-box; |
|
} |
|
|
|
html, body { |
|
margin: 0; |
|
width: 100%; |
|
height: 100%; |
|
position: relative; |
|
font-size: 30px; |
|
font-family: sans-serif; |
|
} |
|
|
|
/* TEXT DISPLAY */ |
|
|
|
.container { |
|
position: absolute; |
|
top: 3em; |
|
left: 0; |
|
width: 100%; |
|
height: calc(100% - 5em); |
|
overflow-y: scroll; |
|
-webkit-overflow-scrolling: touch; |
|
padding: .5em; |
|
} |
|
|
|
p { |
|
font-size: 1em; |
|
margin: 0; |
|
} |
|
|
|
p span { |
|
position: relative; |
|
display: inline-block; |
|
white-space: pre; |
|
} |
|
|
|
p span.active { |
|
background: rgba(255,0,255,.2); |
|
} |
|
|
|
/* TIMELINE */ |
|
|
|
.timeline-row { |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 3em; |
|
border-bottom: 1px solid black; |
|
} |
|
|
|
.record { |
|
display: block; |
|
float: left; |
|
width: 20%; |
|
border-bottom: none; |
|
top: 0; |
|
height: 100%; |
|
} |
|
|
|
.timeline { |
|
display: block; |
|
float: right; |
|
width: 80%; |
|
height: 100%; |
|
overflow: hidden; |
|
position: relative; |
|
} |
|
|
|
.timeline .timeline-inner { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
height: 100%; |
|
} |
|
|
|
.timeline .timeline-inner span { |
|
position: absolute; |
|
height: 100%; |
|
line-height: 3rem; |
|
top: 0; |
|
width: 2em; |
|
text-align: center; |
|
font-size: 2em; |
|
} |
|
|
|
.timeline div.frame { |
|
position: absolute; |
|
top: 0; |
|
left: 50%; |
|
transform: translate(-50%, 0); |
|
width: 2em; |
|
height: 100%; |
|
border: 2px solid rgba(255,0,255,1); |
|
} |
|
|
|
/* CONTROLS */ |
|
|
|
.controls { |
|
position: fixed; |
|
bottom: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 2rem; |
|
border-top: 1px solid black; |
|
background: white; |
|
} |
|
|
|
.controls .row { |
|
height: 2em; |
|
} |
|
|
|
button { |
|
height: 100%; |
|
padding: .5em; |
|
background: white; |
|
border: none; |
|
border-right: 1px solid black; |
|
border-bottom: 1px solid black; |
|
font-family: inherit; |
|
font-size: inherit; |
|
cursor: pointer; |
|
} |
|
|
|
.controls .row.one-col button { |
|
width: 100%; |
|
} |
|
|
|
.controls .row.four-col button { |
|
width: 25%; |
|
} |
|
|
|
.controls button.record { |
|
color: red; |
|
} |
|
|
|
.toggles button.active { |
|
background: black; |
|
color: white; |
|
} |
|
|
|
.toggles button span { |
|
display: inline-block; |
|
} |
|
|
|
/* |
|
|
|
.toggles button.alpha span { |
|
animation: 3s linear 0s infinite normal alpha; |
|
} |
|
|
|
.toggles button.beta span { |
|
animation: 3s linear 0s infinite normal beta; |
|
} |
|
|
|
.toggles button.gamma span { |
|
animation: 3s linear 0s infinite normal gamma; |
|
} |
|
|
|
.toggles button.scale span { |
|
animation: 3s ease-in-out 0s infinite alternate scale; |
|
} |
|
|
|
@keyframes alpha { |
|
from { transform: rotateZ(0deg); } |
|
to { transform: rotateZ(360deg); } |
|
} |
|
|
|
@keyframes beta { |
|
from { transform: rotateX(0deg); } |
|
to { transform: rotateX(360deg); } |
|
} |
|
|
|
@keyframes gamma { |
|
from { transform: rotateY(0deg); } |
|
to { transform: rotateY(360deg); } |
|
} |
|
|
|
@keyframes scale { |
|
from { transform: scale(2); } |
|
to { transform: scale(.5); } |
|
} |
|
|
|
*/ |
|
|
|
</style> |
|
|
|
<body> |
|
|
|
<div class="row timeline-row"> |
|
<button class="record">▶️</button><div class="timeline"><div class="frame"></div></div> |
|
</div> |
|
|
|
<!-- No man is an island, entire of itself; every man is a piece of the continent, a part of the main. If a clod be washed away by the sea, Europe is the less, as well as if a promontory were, as well as if a manor of thy friend’s or of thine own were: any man’s death diminishes me, because I am involved in mankind, and therefore never send to know for whom the bell tolls; it tolls for thee. --> |
|
|
|
<div class="container"> |
|
<p>No man is an island, entire of itself; every man is a piece of the continent, a part of the main. Any man’s death diminishes me, because I am involved in mankind, and therefore never send to know for whom the bell tolls; it tolls for thee.</p> |
|
</div> |
|
|
|
<div class="controls"> |
|
<div class="row four-col toggles"> |
|
<button class="alpha"><span>A</span> |
|
</button><button class="beta"><span>A</span> |
|
</button><button class="gamma"><span>A</span> |
|
</button><button class="scale"><span>A</span></button> |
|
</div> |
|
</div> |
|
|
|
</body> |
|
|
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
<script> |
|
|
|
var toggles = { |
|
alpha: true, |
|
beta: true, |
|
gamma: false, |
|
scale: true |
|
} |
|
|
|
setupToggles(toggles, d3.select('.toggles')); |
|
|
|
// store current rotation in euler angles |
|
var rotation = { |
|
alpha: 0, beta: 0, gamma: 0 |
|
}; |
|
|
|
// store whole history of acceleration and implied velocity and position, |
|
// starting from these initial conditions |
|
var z = [ |
|
{ |
|
position: 0, |
|
velocity: 0, |
|
acceleration: 0, |
|
time: undefined |
|
} |
|
]; |
|
|
|
var duration = 30 * 1000; |
|
|
|
var currentIndex = 0, |
|
indexOffset = 0; |
|
|
|
window.addEventListener('devicemotion', handleMotion); |
|
window.addEventListener('deviceorientation', handleOrientation); |
|
window.addEventListener('mousemove', handleMousemove); |
|
|
|
var sel = d3.select('.container p'); |
|
var text = sel.text().split(''); |
|
|
|
var letter = sel |
|
.text('') |
|
.selectAll('span') |
|
.data(text) |
|
.enter() |
|
.append('span') |
|
.text(function(d) { return d; }); |
|
|
|
var letterTl = d3.select('.timeline') |
|
.append('div') |
|
.classed('timeline-inner', true) |
|
.selectAll('span') |
|
.data(text) |
|
.enter() |
|
.append('span') |
|
.text(function(d) { return d; }) |
|
.style('left', function(d,i) { |
|
return i * 40 - this.offsetWidth/2 + 'px'; |
|
}); |
|
|
|
var timeScale = d3.scaleLinear() |
|
.domain([0, duration]) |
|
.range([0, letter.size()]) |
|
.clamp(true); |
|
|
|
var scaleScale = d3.scaleLinear() |
|
.domain([-5,5]) |
|
.range([-1,1]) |
|
.clamp(true); |
|
|
|
var timeline = d3.select('.timeline'); |
|
|
|
var timelineScale = d3.scaleLinear() |
|
.domain([timeline.node().offsetWidth / 2, timeline.node().offsetWidth / 2 - letter.size() * 40]) |
|
.range([0, letter.size()]) |
|
.clamp(true); |
|
|
|
var timelineDrag = d3.drag() |
|
.container(function() { return this; }) |
|
.on('start', function() { |
|
pauseTimer(); |
|
}) |
|
.on('drag', function() { |
|
d3.select('.timeline .timeline-inner') |
|
.style('left', function() { return d3.event.dx + parseInt(d3.select(this).style('left')) +'px'; }) |
|
}) |
|
.on('end', function() { |
|
d3.select('.timeline .timeline-inner') |
|
.style('left', function() { |
|
currentIndex = Math.round(timelineScale(parseInt(d3.select(this).style('left')))); |
|
return timelineScale.invert(currentIndex) + 'px'; |
|
}); |
|
}); |
|
|
|
timeline.call(timelineDrag); |
|
|
|
var renderTimer = d3.timer(render); |
|
var indexTimer; |
|
pauseTimer(); |
|
setIndex(0); |
|
|
|
function startTimer() { |
|
indexOffset = currentIndex; |
|
d3.select('button.record') |
|
.text('⏸') |
|
.on('click', pauseTimer); |
|
if(indexTimer) indexTimer.stop(); |
|
indexTimer = d3.timer(setIndex); |
|
} |
|
|
|
function pauseTimer() { |
|
d3.select('button.record') |
|
.text('▶') |
|
.on('click', startTimer); |
|
if(indexTimer) indexTimer.stop(); |
|
} |
|
|
|
function endTimer() { |
|
letter |
|
.classed('active', false); |
|
d3.select('button.record') |
|
.text('↩️️️') |
|
.on('click', restartTimer); |
|
if(indexTimer) indexTimer.stop(); |
|
} |
|
|
|
function restartTimer() { |
|
currentIndex = 0; |
|
indexOffset = 0; |
|
setIndex(0); |
|
pauseTimer(); |
|
} |
|
|
|
function setIndex(t) { |
|
currentIndex = Math.max(0,Math.floor(timeScale(t))) + indexOffset; |
|
if(currentIndex >= letter.size()) endTimer(); |
|
|
|
d3.select('.timeline .timeline-inner') |
|
.style('left', function() { return timelineScale.invert(currentIndex) + 'px'; }); |
|
|
|
// letterTl |
|
// .filter(function(d,i) { |
|
// return Math.abs(currentIndex - i) >= 4; |
|
// }).style('display', 'none'); |
|
// letterTl |
|
// .select(function(d,i) { |
|
// return (Math.abs(currentIndex - i) < 4) ? this : null; |
|
// }).style('display', 'block'); |
|
} |
|
|
|
function render() { |
|
|
|
d3.select('.controls .alpha span').style('transform', 'rotateZ('+ -rotation.alpha +'deg)'); |
|
d3.select('.controls .beta span').style('transform', 'rotateX('+ rotation.beta +'deg)'); |
|
d3.select('.controls .gamma span').style('transform', 'rotateY('+ rotation.gamma +'deg)'); |
|
d3.select('.controls .scale span').style('transform', 'scale('+ (Math.pow(2,scaleScale(z[0].acceleration))) +')'); |
|
|
|
var transformString = ''; |
|
if(toggles.alpha) transformString += 'rotateZ('+ -rotation.alpha +'deg) '; |
|
if(toggles.beta) transformString += 'rotateX('+ rotation.beta +'deg) '; |
|
if(toggles.gamma) transformString += 'rotateY('+ rotation.gamma +'deg) '; |
|
if(toggles.scale) transformString += 'scale('+ (Math.pow(2,scaleScale(z[0].acceleration))) +')'; |
|
|
|
letter |
|
.classed('active', false) |
|
.filter(function(d,i) { return i === currentIndex; }) |
|
.classed('active', true) |
|
.style('transform', transformString) |
|
.each(function() { |
|
|
|
var isTooHighToSee = this.offsetTop < this.offsetParent.scrollTop; |
|
var isTooLowToSee = this.offsetTop + this.offsetHeight > this.offsetParent.offsetHeight + this.offsetParent.scrollTop; |
|
|
|
if(isTooHighToSee) { |
|
this.offsetParent.scrollTop = this.offsetTop; |
|
} |
|
|
|
if(isTooLowToSee) { |
|
this.offsetParent.scrollTop = this.offsetTop + this.offsetHeight - this.offsetParent.offsetHeight; |
|
} |
|
}); |
|
|
|
letterTl |
|
.filter(function(d,i) { return i === currentIndex; }) |
|
.style('transform', transformString); |
|
|
|
} |
|
|
|
function handleOrientation(e) { |
|
if(e.gamma === null || e.beta === null || e.alpha === null) return; |
|
rotation = { |
|
gamma: e.gamma || 0, |
|
beta: e.beta || 0, |
|
alpha: e.alpha || 0 |
|
} |
|
} |
|
|
|
// accelerate according to z-axis device motion |
|
function handleMotion(e) { |
|
if(e.acceleration.x === null || e.acceleration.y === null || e.acceleration.z === null) return; |
|
accelerate(e.acceleration.z, e.timeStamp); |
|
} |
|
|
|
// for testing on desktop, basically: map horizontal mouse position to acceleration |
|
function handleMousemove(e) { |
|
var mouseAccelerator = d3.scaleLinear() |
|
.domain([0,innerWidth]) |
|
.range([-1,1]); |
|
accelerate(mouseAccelerator(e.pageX), e.timeStamp); |
|
} |
|
|
|
// step forward with new acceleration, applying some very crude filtering & friction |
|
function accelerate(a, t) { |
|
|
|
var newZ = Object.assign({}, z[0]); |
|
|
|
newZ.acceleration = Math.abs(a) > .1 ? a : 0; // noise filter |
|
newZ.time = t; |
|
newZ = eulerStep(z[0], newZ); |
|
|
|
newZ.velocity *= .9; // friction |
|
newZ.velocity = Math.abs(newZ.velocity) < .01 ? 0 : newZ.velocity; // noise filter |
|
newZ.position *= .999; // tend back to zero |
|
|
|
z.unshift(newZ); |
|
} |
|
|
|
// euler double integration |
|
function eulerStep(state0, state1) { |
|
var interval = (state1.time - state0.time) / 1000; // convert ms to s |
|
if(interval) { |
|
state1.position = state0.position + state0.velocity * interval; |
|
state1.velocity = state0.velocity + state0.acceleration * interval; |
|
} |
|
return Object.assign({}, state1); |
|
} |
|
|
|
function setupToggles(toggles, sel) { |
|
Object.keys(toggles).forEach(function(key) { |
|
sel.select('.'+key) |
|
.on('click', function() { |
|
if(d3.select(this).classed('active')) { |
|
// disable |
|
d3.select(this).classed('active', false); |
|
toggles[key] = false; |
|
} else { |
|
// enable |
|
d3.select(this).classed('active', true); |
|
toggles[key] = true; |
|
} |
|
}); |
|
|
|
if(toggles[key]) sel.select('.'+key).node().click(); |
|
}); |
|
} |
|
|
|
</script> |