502 lines
13 KiB
HTML
502 lines
13 KiB
HTML
<!doctype html>
|
|
<html manifest="choose.appcache">
|
|
<head>
|
|
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
|
<link rel="shortcut icon" href="favicon.ico" />
|
|
<title>Choose app</title>
|
|
<style type="text/css">
|
|
body {
|
|
background: black;
|
|
color: white;
|
|
}
|
|
#canvas {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 6px;
|
|
width: 100%
|
|
height: calc(100%-6px);
|
|
z-index: 1;
|
|
border-bottom: 6px solid #1a8000;
|
|
}
|
|
#ruler {
|
|
position: absolute;
|
|
z-index: 0;
|
|
width: 1cm;
|
|
height: 1cm;
|
|
}
|
|
.intro {
|
|
position: absolute;
|
|
z-index: 0;
|
|
top: 50%;
|
|
transform: translate(0, -50%);
|
|
text-align: center;
|
|
font-size: 10vw;
|
|
text-transform: uppercase;
|
|
font-family: sans-serif;
|
|
color: white;
|
|
}
|
|
#about {
|
|
position: absolute;
|
|
z-index: 2;
|
|
bottom: 0;
|
|
right: 0;
|
|
font-size: 3vw;
|
|
text-decoration: none;
|
|
display: none;
|
|
}
|
|
.circle {
|
|
position: absolute;
|
|
border-radius: 50%;
|
|
z-index: 1;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<span id = "intro" class = "intro">All players place a finger on the screen</span>
|
|
<a id = "about" target = "_blank" href = "https://github.com/dperelman/choose">About Choose</a>
|
|
<canvas id = "canvas"></canvas>
|
|
<div id = "ruler"></div>
|
|
<script >
|
|
var touchInfos = {};
|
|
var complete = false;
|
|
|
|
var TOUCH_WAIT_TIME = 2000;
|
|
var TEXT_FADE_TIME = 750;
|
|
|
|
// Based on http://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/
|
|
var lastHue = Math.random();
|
|
var GOLDEN_RATIO_CONJUGATE = 0.618033988749895;
|
|
function nextColor() {
|
|
var h = lastHue;
|
|
h += GOLDEN_RATIO_CONJUGATE;
|
|
h %= 1;
|
|
lastHue = h;
|
|
return toHexColor(hsvToRgb(h, 0.5, 0.95));
|
|
}
|
|
|
|
// From http://snipplr.com/view.php?codeview&id=14590
|
|
/**
|
|
* HSV to RGB color conversion
|
|
*
|
|
* H runs from 0 to 360 degrees
|
|
* S and V run from 0 to 1
|
|
*
|
|
* Ported from the excellent java algorithm by Eugene Vishnevsky at:
|
|
* http://www.cs.rit.edu/~ncs/color/t_convert.html
|
|
*/
|
|
function hsvToRgb(h, s, v) {
|
|
var r, g, b;
|
|
var i;
|
|
var f, p, q, t;
|
|
|
|
// Make sure our arguments stay in-range
|
|
h = Math.max(0, Math.min(1, h));
|
|
s = Math.max(0, Math.min(1, s));
|
|
v = Math.max(0, Math.min(1, v));
|
|
|
|
if(s == 0) {
|
|
// Achromatic (grey)
|
|
r = g = b = v;
|
|
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
|
}
|
|
|
|
h *= 6; // sector 0 to 5
|
|
i = Math.floor(h);
|
|
f = h - i; // factorial part of h
|
|
p = v * (1 - s);
|
|
q = v * (1 - s * f);
|
|
t = v * (1 - s * (1 - f));
|
|
|
|
switch(i) {
|
|
case 0:
|
|
r = v;
|
|
g = t;
|
|
b = p;
|
|
break;
|
|
|
|
case 1:
|
|
r = q;
|
|
g = v;
|
|
b = p;
|
|
break;
|
|
|
|
case 2:
|
|
r = p;
|
|
g = v;
|
|
b = t;
|
|
break;
|
|
|
|
case 3:
|
|
r = p;
|
|
g = q;
|
|
b = v;
|
|
break;
|
|
|
|
case 4:
|
|
r = t;
|
|
g = p;
|
|
b = v;
|
|
break;
|
|
|
|
default: // case 5:
|
|
r = v;
|
|
g = p;
|
|
b = q;
|
|
}
|
|
|
|
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
|
}
|
|
function toHexColor(rgb) {
|
|
var res = '#';
|
|
for (var i = 0; i < 3; i++) {
|
|
res += ("0" + Math.round(rgb[i]).toString(16)).slice(-2);
|
|
}
|
|
return res;
|
|
}
|
|
function distance(x1, y1, x2, y2) {
|
|
var x = x1-x2;
|
|
var y = y1-y2;
|
|
return Math.sqrt(x*x + y*y);
|
|
}
|
|
|
|
// From http://stackoverflow.com/a/2450976
|
|
function shuffle(array) {
|
|
var currentIndex = array.length, temporaryValue, randomIndex ;
|
|
|
|
// While there remain elements to shuffle...
|
|
while (0 !== currentIndex) {
|
|
|
|
// Pick a remaining element...
|
|
randomIndex = Math.floor(Math.random() * currentIndex);
|
|
currentIndex -= 1;
|
|
|
|
// And swap it with the current element.
|
|
temporaryValue = array[currentIndex];
|
|
array[currentIndex] = array[randomIndex];
|
|
array[randomIndex] = temporaryValue;
|
|
}
|
|
|
|
return array;
|
|
}
|
|
|
|
function clearExpiredTouchInfos() {
|
|
var now = Date.now();
|
|
for(var tid in touchInfos) {
|
|
var touch = touchInfos[tid];
|
|
if(!touch.active && touch.expirationTime < now) {
|
|
document.body.removeChild(touchInfos[tid].div);
|
|
delete touchInfos[tid];
|
|
}
|
|
}
|
|
}
|
|
|
|
function areAllTouchInfosActive() {
|
|
for(var tid in touchInfos) {
|
|
var touch = touchInfos[tid];
|
|
if(!touch.active) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function computeTouchInfoCharge(touch) {
|
|
var now = Date.now();
|
|
if(!touch.active) {
|
|
return Math.min(TOUCH_WAIT_TIME, Math.max(0, touch.expirationTime - now));
|
|
} else {
|
|
return Math.min(TOUCH_WAIT_TIME, now - touch.touchStartTime);
|
|
}
|
|
}
|
|
function touchInfoMaxCharge() {
|
|
var max = 0;
|
|
for(var tid in touchInfos) {
|
|
var touch = touchInfos[tid];
|
|
max = Math.max(max, computeTouchInfoCharge(touch));
|
|
}
|
|
return max;
|
|
}
|
|
function touchInfoMaxPairCharge() {
|
|
if(Object.keys(touchInfos).length < 2) {
|
|
return 0;
|
|
}
|
|
var charges = [];
|
|
for(var tid in touchInfos) {
|
|
var touch = touchInfos[tid];
|
|
var charge = computeTouchInfoCharge(touch);
|
|
charges.push(charge);
|
|
}
|
|
charges.sort(function(a,b) { return a-b; });
|
|
return charges[charges.length-2];
|
|
}
|
|
function touchInfoMinActiveTime() {
|
|
var max = 0;
|
|
for(var tid in touchInfos) {
|
|
var touch = touchInfos[tid];
|
|
if(!touch.active) {
|
|
return 0;
|
|
} else {
|
|
max = Math.max(max, touch.touchStartTime);
|
|
}
|
|
}
|
|
return Date.now() - max;
|
|
}
|
|
|
|
function checkComplete() {
|
|
now = Date.now();
|
|
if(!complete) {
|
|
clearExpiredTouchInfos();
|
|
if(Object.keys(touchInfos).length > 1
|
|
&& areAllTouchInfosActive()
|
|
&& touchInfoMinActiveTime() > TOUCH_WAIT_TIME) {
|
|
complete = true;
|
|
var players = [];
|
|
for (var id in touchInfos) {
|
|
players.push(id);
|
|
}
|
|
shuffle(players);
|
|
for (var i = 0; i < players.length; i++) {
|
|
touchInfos[players[i]].turnOrder = i + 1;
|
|
}
|
|
if(navigator.vibrate) {
|
|
navigator.vibrate(200);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
document.body.addEventListener('touchmove', function(event) {
|
|
// prevent scrolling (from http://www.html5rocks.com/en/mobile/touch/ )
|
|
event.preventDefault();
|
|
if(!complete) {
|
|
for (var i = 0; i < event.changedTouches.length; i++) {
|
|
var touch = event.changedTouches[i];
|
|
var id = touch.identifier;
|
|
touchInfos[id].x = touch.pageX;
|
|
touchInfos[id].y = touch.pageY;
|
|
}
|
|
}
|
|
}, false);
|
|
|
|
document.body.addEventListener('touchstart', function(event) {
|
|
if(!complete) {
|
|
event.preventDefault();
|
|
clearExpiredTouchInfos();
|
|
|
|
var now = Date.now();
|
|
|
|
for (var i = 0; i < event.changedTouches.length; i++) {
|
|
var touch = event.changedTouches[i];
|
|
var id = touch.identifier;
|
|
var x = touch.pageX;
|
|
var y = touch.pageY;
|
|
|
|
var newTouch = undefined;
|
|
var foundNearTouch = false;
|
|
for (var tid in touchInfos) {
|
|
var ti = touchInfos[tid];
|
|
if(!foundNearTouch
|
|
&& !ti.active
|
|
&& distance(x, y, ti.x, ti.y) < MAX_TOUCH_RECOVERY_DISTANCE) {
|
|
foundNearTouch = true;
|
|
var newTouch = ti;
|
|
var charge = computeTouchInfoCharge(ti);
|
|
newTouch.touchStartTime = now - charge;
|
|
delete touchInfos[tid];
|
|
break;
|
|
}
|
|
}
|
|
if(!foundNearTouch) {
|
|
var newTouch = {};
|
|
newTouch.color = nextColor();
|
|
newTouch.touchStartTime = now;
|
|
newTouch.div = null;
|
|
}
|
|
newTouch.x = x;
|
|
newTouch.y = y;
|
|
newTouch.active = true;
|
|
newTouch.expirationTime = undefined;
|
|
touchInfos[id] = newTouch;
|
|
}
|
|
}
|
|
}, false);
|
|
|
|
document.body.addEventListener('touchend', function(event) {
|
|
if(!complete) {
|
|
event.preventDefault();
|
|
var now = Date.now();
|
|
|
|
for (var i = 0; i < event.changedTouches.length; i++) {
|
|
var id = event.changedTouches[i].identifier;
|
|
var touch = touchInfos[id];
|
|
var chargeTime = computeTouchInfoCharge(touch);
|
|
touch.active = false;
|
|
touch.expirationTime = now + chargeTime;
|
|
delete touchInfos[id];
|
|
touchInfos[id + '-expired-' + now] = touch;
|
|
}
|
|
}
|
|
}, false);
|
|
|
|
// From http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
|
|
// shim layer with setTimeout fallback
|
|
window.requestAnimFrame = (function(){
|
|
return window.requestAnimationFrame ||
|
|
window.webkitRequestAnimationFrame ||
|
|
window.mozRequestAnimationFrame ||
|
|
function( callback ){
|
|
window.setTimeout(callback, 1000 / 60);
|
|
};
|
|
})();
|
|
|
|
var canvas = document.getElementById('canvas');
|
|
// From http://stackoverflow.com/a/8876069
|
|
canvas.width = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
|
|
canvas.height = Math.max(document.documentElement.clientHeight, window.innerHeight || 0) - 6;
|
|
var context = canvas.getContext('2d');
|
|
|
|
var RADIUS = 0;
|
|
var ruler = document.getElementById('ruler');
|
|
var MAX_TOUCH_RECOVERY_DISTANCE = 0;
|
|
|
|
document.body.onload = function() {
|
|
var CM_IN_PX = document.defaultView.getComputedStyle(ruler).width.slice(0, -2);
|
|
ruler.style.display = 'none';
|
|
|
|
RADIUS = 1*CM_IN_PX;
|
|
MAX_TOUCH_RECOVERY_DISTANCE = 2*RADIUS;
|
|
}
|
|
|
|
function drawCircle(centerX, centerY, radius, lineWidth, color, fill) {
|
|
// From http://www.html5canvastutorials.com/tutorials/html5-canvas-circles/
|
|
context.beginPath();
|
|
context.arc(centerX, centerY, radius, 0, 2 * Math.PI, false);
|
|
if(fill) {
|
|
context.fillStyle = color;
|
|
context.fill();
|
|
}
|
|
context.lineWidth = 5;
|
|
context.strokeStyle = color;
|
|
context.stroke();
|
|
context.closePath();
|
|
}
|
|
|
|
var randomRotations = [];
|
|
for(var i = 0; i < 10; i++) {
|
|
randomRotations[i] = Math.PI * 2 * Math.random();
|
|
}
|
|
// From http://www.arungudelli.com/html5/html5-canvas-polygon/
|
|
function drawRegularPolygon(ctx, x, y, radius, sides) {
|
|
if (sides < 3) return;
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
var a = ((Math.PI * 2)/sides);
|
|
var off = randomRotations[sides];
|
|
ctx.translate(x,y);
|
|
ctx.rotate(off);
|
|
ctx.moveTo(radius,0);
|
|
for (var i = 1; i < sides; i++) {
|
|
var theta = a*i;
|
|
ctx.lineTo(radius*Math.cos(theta),radius*Math.sin(theta));
|
|
}
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
|
|
function render() {
|
|
checkComplete();
|
|
|
|
var currentTime = Date.now();
|
|
|
|
if(complete) {
|
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
document.getElementById('about').style.display = 'inherit';
|
|
document.body.style.background = 'white';
|
|
canvas.style.border = 'white';
|
|
for (var id in touchInfos) {
|
|
var touch = touchInfos[id];
|
|
document.body.removeChild(touch.div);
|
|
var turn = touch.turnOrder;
|
|
var color = touch.color;
|
|
if(turn == 1) {
|
|
drawCircle(touch.x, touch.y, 1.5*RADIUS, '1vw', color, true);
|
|
} else if(turn == 2) {
|
|
drawCircle(touch.x, touch.y, 0.6*RADIUS, '1vw', color, true);
|
|
drawCircle(touch.x, touch.y, RADIUS, '1vw', color, false);
|
|
} else {
|
|
context.fillStyle = color;
|
|
context.strokeStyle = color;
|
|
drawRegularPolygon(context, touch.x, touch.y, RADIUS, turn);
|
|
}
|
|
if(turn == 1) {
|
|
context.font = 2*RADIUS + "px sans-serif";
|
|
} else {
|
|
context.font = RADIUS + "px sans-serif";
|
|
}
|
|
context.fillStyle = 'black';
|
|
context.textAlign = 'center';
|
|
context.textBaseline = 'middle';
|
|
context.fillText(turn, touch.x, touch.y);
|
|
}
|
|
} else {
|
|
var intro = document.getElementById('intro');
|
|
var pairMaxCharge = touchInfoMaxPairCharge();
|
|
if(pairMaxCharge == 0) {
|
|
intro.style.display = '';
|
|
intro.style.color = 'white';
|
|
document.body.style.background = 'black';
|
|
} else {
|
|
if(pairMaxCharge < TEXT_FADE_TIME) {
|
|
intro.style.display = '';
|
|
var brightness = 255 - Math.round(1.0 * pairMaxCharge / TEXT_FADE_TIME * 255.0);
|
|
intro.style.color = toHexColor([brightness, brightness, brightness]);
|
|
} else {
|
|
intro.style.display = 'none';
|
|
intro.style.color = 'black';
|
|
}
|
|
var progress = Math.max(0.0, 1.0 * (pairMaxCharge - TEXT_FADE_TIME)/TOUCH_WAIT_TIME);
|
|
var progressColor = toHexColor(hsvToRgb(0.3, 1, Math.pow(progress, 1.5)/2));
|
|
document.body.style.background = progressColor;
|
|
}
|
|
|
|
for (var id in touchInfos) {
|
|
var touch = touchInfos[id];
|
|
var charge = computeTouchInfoCharge(touch);
|
|
var radius = charge/TOUCH_WAIT_TIME * RADIUS;
|
|
var color = touch.color;
|
|
if(touch.div == null) {
|
|
var d = document.createElement('div');
|
|
d.className = 'circle';
|
|
d.style.background = color;
|
|
d.style.position = 'absolute';
|
|
document.body.appendChild(d);
|
|
touch.div = d;
|
|
}
|
|
touch.div.style.width = 2*radius + "px";
|
|
touch.div.style.height = 2*radius + "px";
|
|
touch.div.style.top = touch.y-radius + "px";
|
|
touch.div.style.left = touch.x-radius + "px";
|
|
}
|
|
}
|
|
return !complete;
|
|
}
|
|
(function animloop(){
|
|
if(render()) {
|
|
requestAnimFrame(animloop);
|
|
}
|
|
})();
|
|
|
|
if(navigator.vibrate) {
|
|
// Do a vibrate on page load to request vibration permissions in browsers
|
|
// that show a confirmation dialog for it.
|
|
navigator.vibrate(1);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|