|
<!doctype html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="utf-8"> |
|
<title>Split Flap Display version 1</title> |
|
<style> |
|
.container { |
|
clear : left; |
|
margin : 5px auto; |
|
padding : 5px; |
|
background: #444; |
|
max-width : 946px; |
|
} |
|
.container:after { |
|
content: ""; |
|
display: table; |
|
clear : both; |
|
} |
|
.flapset { |
|
position : relative; |
|
float : left; |
|
perspective: 300px; |
|
height : 1em; |
|
width : 1em; |
|
margin : 0 2px; |
|
font-family: monospace; |
|
font-size : 18px; |
|
line-height: 1em; |
|
text-align : center; |
|
color : #EAEAEA; |
|
} |
|
.controls { |
|
margin-left: 300px; |
|
} |
|
|
|
|
|
.flap { |
|
position : absolute; |
|
width : 1em; |
|
height : 0.5em; |
|
overflow : hidden; |
|
background: #333; |
|
z-index : 0; |
|
|
|
animation-duration : .05s; |
|
animation-timing-function: linear; |
|
animation-fill-mode : forwards; |
|
transform-style : preserve-3d; |
|
backface-visibility : hidden; |
|
} |
|
|
|
.flap span { |
|
display: inline-block; |
|
} |
|
|
|
.flap-bottom span { |
|
transform: translateY(-0.5em); /* show bottom only */ |
|
} |
|
|
|
.flap-top { |
|
transform-origin: left bottom; |
|
} |
|
|
|
.flap-bottom { |
|
transform-origin: left top; |
|
transform : translateY(0.5em) rotate3d(1,0,0,180deg); |
|
} |
|
|
|
.enter { |
|
z-index: 2; |
|
} |
|
|
|
.flap-top.enter { |
|
animation-name: topEnter; |
|
} |
|
.flap-bottom.enter { |
|
animation-name: bottomEnter; |
|
} |
|
.flap-top.exit { |
|
animation-name: topExit; |
|
} |
|
.flap-bottom.exit { |
|
animation-name: bottomExit; |
|
} |
|
|
|
/* |
|
To live in the gap |
|
between the moment that is expiring |
|
and the one that is arising ... |
|
And when you close your eyes |
|
what do you see? |
|
nothing |
|
Now open them. |
|
|
|
-- Laurie Anderson, Heart of a Dog |
|
*/ |
|
|
|
@keyframes topEnter { |
|
0% { |
|
z-index: 1; |
|
} |
|
100% { |
|
z-index: 2; |
|
} |
|
} |
|
|
|
@keyframes topExit { |
|
0% { |
|
transform: rotate3d(1,0,0,0deg); |
|
z-index: 2; |
|
} |
|
50% { |
|
transform: rotate3d(1,0,0,-90deg); |
|
z-index: 2; |
|
} |
|
100% { |
|
transform: rotate3d(1,0,0,-180deg); |
|
z-index: 0; |
|
} |
|
} |
|
|
|
@keyframes bottomEnter { |
|
0% { |
|
transform: translateY(0.5em) rotate3d(1,0,0,180deg); |
|
z-index: 0; |
|
} |
|
50% { |
|
transform: translateY(0.5em) rotate3d(1,0,0,90deg); |
|
z-index: 1; |
|
} |
|
100% { |
|
transform: translateY(0.5em) rotate3d(1,0,0,0deg); |
|
z-index: 2; |
|
} |
|
} |
|
|
|
@keyframes bottomExit { |
|
0% { |
|
transform: translateY(0.5em) rotate3d(1,0,0,0deg); |
|
z-index: 2; |
|
} |
|
75% { |
|
transform: translateY(0.5em) rotate3d(1,0,0,0deg); |
|
z-index: 1; |
|
} |
|
100% { |
|
opacity: 1e-6; |
|
transform: translateY(0.5em) rotate3d(1,0,0,180deg); |
|
z-index: 0; |
|
} |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="controls"> |
|
<button id="flip">flip</button> |
|
<button id="reset">reset</button> |
|
</div> |
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.js"></script> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/queue-async/1.0.7/queue.min.js"></script> |
|
<script> |
|
// let's get off to a zany start |
|
String.prototype.padRight = function(length, character) { |
|
return this + Array(length - this.length + 1).join(character || " "); |
|
} |
|
|
|
// ASCII characters to use |
|
var charGroups = [ |
|
{offset: 32, length: 1}, // begin with space |
|
{offset: 48, length: 10}, |
|
{offset: 65, length: 26} |
|
]; |
|
|
|
var chars = genChars(charGroups); |
|
var columns = 43; // tab stops? |
|
var rows = 1; |
|
var delayFactor = 50; |
|
var defaultDuration = 100; |
|
var duration, containers; |
|
|
|
d3.select("#flip").on("click", update); |
|
d3.select("#reset").on("click", reset); |
|
|
|
init(); |
|
|
|
|
|
/** |
|
* Build the DOM structure |
|
* |
|
* @return void |
|
*/ |
|
function init() { |
|
containers = d3.select("body").selectAll(".container") |
|
.data(d3.range(rows)).enter() |
|
.append("div") |
|
.attr("class", "container"); |
|
|
|
containers.selectAll(".flapset") |
|
.data(d3.range(columns)).enter() |
|
.append("div") |
|
.attr("class", "flapset") |
|
.attr("data-d", function(d) { return d; }) |
|
.selectAll(".flap") |
|
.data(chars).enter() |
|
.call(addFlaps); |
|
|
|
// initialise first flap in each set with enter class |
|
var starters = reset(); |
|
// computes the animation speed using CSS style rule of first element |
|
duration = getAnimationDuration(starters[0][0]) || defaultDuration; |
|
} |
|
|
|
|
|
/** |
|
* Reset lines |
|
* |
|
* @return d3.selection the first flaps in each widget |
|
*/ |
|
function reset() { |
|
d3.selectAll(".flap").classed("enter", false); |
|
return d3.selectAll(".flap[data-next='1'").classed("enter", true); |
|
} |
|
|
|
|
|
/** |
|
* update messages |
|
* |
|
* @return void |
|
*/ |
|
function update() { |
|
// temporary |
|
var msg = characters("Hello World", columns); |
|
var line = Math.floor(Math.random() * rows); |
|
|
|
|
|
dispatch(line, msg); |
|
} |
|
|
|
|
|
/** |
|
* dispatch a msg to a given line |
|
* |
|
* @param int line zero-indexed line no. |
|
* @param Array chars msg to display,prepped as individual characters |
|
* @return void |
|
*/ |
|
function dispatch(line, chars) { |
|
containers.each(function(d, i) { |
|
// there's got to be a better way of targeting a specific element in a selection |
|
if (i === line) { |
|
d3.select(this).selectAll(".flapset") |
|
.each(function(d, i) { |
|
window.setTimeout(function() { |
|
flip(this, chars[i]); |
|
}.bind(d3.select(this)), i * delayFactor); |
|
}); |
|
} |
|
}); |
|
} |
|
|
|
|
|
/** |
|
* Starts a split-flap widget flipping |
|
* |
|
* @return void |
|
*/ |
|
function flip() { |
|
var _this = arguments[0]; |
|
var letter = arguments[1]; |
|
|
|
// space char -- used for padding |
|
if (letter == chars[0]) return; |
|
|
|
var interval = window.setInterval(function() { |
|
|
|
var enter = _this.selectAll(".enter"); |
|
var d = enter[0][0].dataset.d; |
|
var next = chars[ enter[0][0].dataset.next ]; |
|
|
|
_this.selectAll(".exit").classed("exit", false); |
|
enter.classed("exit", true).classed("enter", false); |
|
_this.selectAll(".flap[data-d='" + next + "']").classed("enter", true); |
|
|
|
if (next == letter) { |
|
clearInterval(interval); |
|
} |
|
|
|
}, duration); |
|
} |
|
|
|
|
|
/** |
|
* Adds top & bottom flaps for each character. Not super elegant. |
|
* Note that all.flap-tops will be added before the .flap-bottoms. |
|
* |
|
* @param d3.selection selection |
|
* @return void |
|
*/ |
|
function addFlaps(selection) { |
|
selection.append("div") |
|
.attr("class", "flap flap-top") |
|
.attr("data-d", function(d) { return d; }) |
|
.attr("data-next", getNextIndex) |
|
.append("span") |
|
.text(function(d) { return d;}); |
|
selection.append("div") |
|
.attr("class", "flap flap-bottom") |
|
.attr("data-d", function(d) { return d; }) |
|
.attr("data-next", getNextIndex) |
|
.append("span") |
|
.text(function(d) { return d;}); |
|
} |
|
|
|
|
|
/** |
|
* Prepares the characters in a msg |
|
* |
|
* @param String msg the message |
|
* @param Int cols number of columns |
|
* @return Array the chars,to uppercase and right-padded with spaces |
|
*/ |
|
function characters(msg, cols) { |
|
return msg.slice(0, cols).padRight(cols).toUpperCase().split(""); |
|
} |
|
|
|
|
|
|
|
/** |
|
* Generates an array of characters. |
|
* |
|
* @param Array groups Contains objects for each set of consecutive |
|
* ASCII chars desired. Object should have the following properties: |
|
* "offset": the decimal code for the first character in the group |
|
* "length": the length of the group |
|
* @return Array |
|
*/ |
|
function genChars(groups) { |
|
var a = []; |
|
|
|
if (typeof groups === undefined || !(groups instanceof Array)) { |
|
groups = []; |
|
} |
|
|
|
groups.forEach(function(d) { |
|
if (d && d.length && d.offset) { |
|
for (var i = 0; i < d.length; i++) { |
|
a.push(String.fromCharCode(d.offset + i)); |
|
} |
|
} |
|
}); |
|
return a; |
|
} |
|
|
|
|
|
/** |
|
* Get the index of the following char for a given char, or 0. |
|
* Each div.flap stores this value in its data to make it easier |
|
* to specify which should next have the "enter" class added. |
|
* |
|
* @param String d some character |
|
* @return Integer the next index |
|
*/ |
|
function getNextIndex(d) { |
|
var i = chars.indexOf(d) + 1; |
|
if (i === chars.length ) { |
|
i = 0; |
|
} |
|
return i; |
|
} |
|
|
|
/** |
|
* Gets the duration for an element's animation as set in |
|
* the CSS rule. |
|
* |
|
* @param element |
|
* @return Float milliseconds |
|
*/ |
|
function getAnimationDuration(element) { |
|
var properties = [ |
|
'animation-duration', |
|
'WebkitAnimationDuration', |
|
'msAnimationDuration', |
|
'MozAnimationDuration', |
|
'OAnimationDuration' |
|
]; |
|
var p; |
|
var style = window.getComputedStyle(element); |
|
|
|
while (p = properties.shift()) { |
|
if (typeof style[p] !== undefined) { |
|
return parseFloat(style[p]) * 1000; |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
</script> |
|
|
|
</body> |
|
</html> |