Skip to content

Instantly share code, notes, and snippets.

@tophtucker
Last active June 6, 2019 07:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save tophtucker/65ffa9bf25c971a985ae1afb721bca2d to your computer and use it in GitHub Desktop.
Save tophtucker/65ffa9bf25c971a985ae1afb721bca2d to your computer and use it in GitHub Desktop.
Typeset gyro

USER TESTING: John

  • Too much going on when page loads!!!
  • He loaded it lying down on his back; either do gyro relative to initial position or communicate the neutral position better
  • Doesn't get the 'record' metaphor; feels more like a play/stop situation
  • Text wrapping confuses the linear tape recording metaphor
  • Can't tell if toggles are on or off
  • Wants toggles to be hooked up 'live' to visualize the relevant gyro axis
  • Confusing to have three gyro and one accelerometer button
  • Text should scroll with cursor
  • Wants to highlight a letter/word and manipulate it
  • He's not sure toggles are needed at all
  • Kinda imagines letters coming more slowly through the frame; while they're in-frame they're tied to gyro; as they pass out of frame they're locked in place
  • He wants to be able to type his own text, so he cares about the message (and then gather stats on how people manipulate which words)
<!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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment