block by tophtucker 65ffa9bf25c971a985ae1afb721bca2d

Typeset gyro

Full Screen

index.html

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

feedback.md

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)