509 lines
17 KiB
HTML
509 lines
17 KiB
HTML
|
<!DOCTYPE html>
|
||
|
<html>
|
||
|
<head>
|
||
|
<meta charset="UTF-8"/>
|
||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no"/>
|
||
|
|
||
|
<title>zzz.cat Speedtest</title>
|
||
|
|
||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Oswald:300,400|Source+Sans+Pro:400,600"
|
||
|
type="text/css" media="all"/>
|
||
|
|
||
|
<style type="text/css">
|
||
|
:root {
|
||
|
--dark-gray: #32363b;
|
||
|
--grass-green: #02b159;
|
||
|
}
|
||
|
|
||
|
html, body {
|
||
|
background-color: white;
|
||
|
border: none;
|
||
|
box-sizing: border-box;
|
||
|
color: var(--dark-gray);
|
||
|
padding: 0;
|
||
|
margin: 0;
|
||
|
}
|
||
|
|
||
|
html {
|
||
|
line-height: 1.15;
|
||
|
-ms-text-size-adjust: 100%;
|
||
|
-webkit-text-size-adjust: 100%;
|
||
|
}
|
||
|
|
||
|
body {
|
||
|
font-size: 18px;
|
||
|
font-size: 1.125rem;
|
||
|
line-height: 1.33333;
|
||
|
font-family: "Source Sans Pro", sans-serif;
|
||
|
font-weight: 400;
|
||
|
letter-spacing: 0;
|
||
|
margin: 1rem 1rem 2rem;
|
||
|
text-align: center;
|
||
|
-moz-osx-font-smoothing: grayscale;
|
||
|
-webkit-font-smoothing: antialiased;
|
||
|
}
|
||
|
|
||
|
*, *:before, *:after {
|
||
|
box-sizing: inherit;
|
||
|
}
|
||
|
|
||
|
#startStopBtn {
|
||
|
display: inline-block;
|
||
|
margin: 0 auto;
|
||
|
padding-left: 2rem;
|
||
|
padding-right: 2rem;
|
||
|
color: white;
|
||
|
background-color: var(--grass-green);
|
||
|
border: 0.15em solid var(--grass-green);
|
||
|
border-radius: 0.3em;
|
||
|
transition: all 0.3s;
|
||
|
box-sizing: border-box;
|
||
|
height: 3em;
|
||
|
line-height: 2.7em;
|
||
|
cursor: pointer;
|
||
|
/*box-shadow:0 0 0 rgba(0,0,0,0.1), inset 0 0 0 rgba(0,0,0,0.1);*/
|
||
|
font-weight: 600;
|
||
|
}
|
||
|
|
||
|
#startStopBtn:hover {
|
||
|
/*box-shadow: 0 0 2em rgba(0,0,0,0.1), inset 0 0 1em rgba(0,0,0,0.1);*/
|
||
|
}
|
||
|
|
||
|
#startStopBtn.running {
|
||
|
background-color: var(--dark-gray);
|
||
|
border-color: var(--dark-gray);
|
||
|
color: white;
|
||
|
}
|
||
|
|
||
|
#startStopBtn:before {
|
||
|
content: "Start Speed Test";
|
||
|
}
|
||
|
|
||
|
#startStopBtn.running:before {
|
||
|
content: "Abort";
|
||
|
}
|
||
|
|
||
|
#test {
|
||
|
margin-top: 2rem;
|
||
|
margin-bottom: 2rem;
|
||
|
}
|
||
|
|
||
|
div.testArea {
|
||
|
display: inline-block;
|
||
|
width: calc(100% - 2rem - 2rem); /* subtract margins */
|
||
|
max-width: 16em;
|
||
|
margin: 2rem;
|
||
|
position: relative;
|
||
|
box-sizing: border-box;
|
||
|
}
|
||
|
|
||
|
div.testArea::before {
|
||
|
content: '';
|
||
|
display: block;
|
||
|
padding-top: 100%;
|
||
|
}
|
||
|
|
||
|
div.testName {
|
||
|
bottom: 0;
|
||
|
font-size: 1rem;
|
||
|
font-weight: 600;
|
||
|
left: 0;
|
||
|
position: absolute;
|
||
|
width: 100%;
|
||
|
z-index: 9;
|
||
|
}
|
||
|
|
||
|
div.meterText {
|
||
|
bottom: 1.8em;
|
||
|
font-family: "Oswald", sans-serif;
|
||
|
font-size: 4em;
|
||
|
font-weight: 300;
|
||
|
left: 0;
|
||
|
line-height: 1;
|
||
|
position: absolute;
|
||
|
width: 100%;
|
||
|
z-index: 9;
|
||
|
}
|
||
|
|
||
|
div.meterText:empty:before {
|
||
|
content: "0.00";
|
||
|
}
|
||
|
|
||
|
div.unit {
|
||
|
bottom: 3.5em;
|
||
|
font-family: "Oswald", sans-serif;
|
||
|
font-size: 1.5em;
|
||
|
font-weight: 400;
|
||
|
letter-spacing: 0.5px;
|
||
|
left: 0;
|
||
|
line-height: 1;
|
||
|
position: absolute;
|
||
|
text-transform: uppercase;
|
||
|
width: 100%;
|
||
|
z-index: 9;
|
||
|
}
|
||
|
|
||
|
div.testArea canvas {
|
||
|
position: absolute;
|
||
|
top: 0;
|
||
|
left: 0;
|
||
|
width: 100%;
|
||
|
height: 100%;
|
||
|
z-index: 1;
|
||
|
}
|
||
|
|
||
|
#ipArea {
|
||
|
margin-top: 2rem;
|
||
|
}
|
||
|
|
||
|
#shareArea {
|
||
|
width: 95%;
|
||
|
max-width: 40em;
|
||
|
margin: 0 auto;
|
||
|
margin-top: 2em;
|
||
|
}
|
||
|
|
||
|
#shareArea > * {
|
||
|
display: block;
|
||
|
width: 100%;
|
||
|
height: auto;
|
||
|
margin: 0.25em 0;
|
||
|
}
|
||
|
|
||
|
#privacyPolicy {
|
||
|
position: fixed;
|
||
|
top: 10em;
|
||
|
bottom: 10em;
|
||
|
left: 15em;
|
||
|
right: 15em;
|
||
|
overflow-y: auto;
|
||
|
width: auto;
|
||
|
height: auto;
|
||
|
box-shadow: 0 0 3em 1em #000000;
|
||
|
z-index: 999999;
|
||
|
text-align: left;
|
||
|
background-color: #FFFFFF;
|
||
|
padding: 1em;
|
||
|
}
|
||
|
|
||
|
#privacyCloseBtn {
|
||
|
display: inline-block;
|
||
|
margin: 0 auto;
|
||
|
padding-left: 2rem;
|
||
|
padding-right: 2rem;
|
||
|
color: white;
|
||
|
background-color: var(--grass-green);
|
||
|
border: 0.15em solid var(--grass-green);
|
||
|
border-radius: 0.3em;
|
||
|
transition: all 0.3s;
|
||
|
box-sizing: border-box;
|
||
|
height: 3em;
|
||
|
line-height: 2.7em;
|
||
|
cursor: pointer;
|
||
|
/*box-shadow:0 0 0 rgba(0,0,0,0.1), inset 0 0 0 rgba(0,0,0,0.1);*/
|
||
|
font-weight: 600;
|
||
|
}
|
||
|
|
||
|
a.privacy {
|
||
|
margin-top: 2em;
|
||
|
text-align: center;
|
||
|
font-size: 0.8em;
|
||
|
color: #808080;
|
||
|
display: block;
|
||
|
}
|
||
|
|
||
|
@media all and (max-width: 65em) {
|
||
|
/* 1040 px */
|
||
|
div.testGroup {
|
||
|
margin-left: -1rem;
|
||
|
margin-right: -1rem;
|
||
|
}
|
||
|
|
||
|
div.testArea {
|
||
|
font-size: 1.5384615vw; /* 16 px */
|
||
|
margin: 1rem;
|
||
|
width: calc(100% - 1rem - 1rem); /* subtract margins */
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@media all and (max-width: 40em) {
|
||
|
/* 640 px */
|
||
|
div.testArea {
|
||
|
font-size: 0.8em; /* 12.8 px */
|
||
|
}
|
||
|
}
|
||
|
|
||
|
</style>
|
||
|
<script type="text/javascript">
|
||
|
function I(id) {
|
||
|
return document.getElementById(id);
|
||
|
}
|
||
|
|
||
|
var meterBk = "#ebecf0"; // ui--input-border
|
||
|
var dlColor = "#02b159", // grass-green
|
||
|
ulColor = "#02b159", // grass-green
|
||
|
pingColor = "#02b159", // grass-green
|
||
|
jitColor = "#02b159"; // grass-green
|
||
|
var progColor = "#0ac9f7"; // sky-blue
|
||
|
|
||
|
function setPageTitle() {
|
||
|
var pageTitleDiv = document.getElementById('pageTitle');
|
||
|
var titlePrefixNode = document.createTextNode('Speed Test:');
|
||
|
var lineBreakElement = document.createElement('br');
|
||
|
var locationNode = document.createTextNode('Unknown');
|
||
|
switch (window.location.hostname) {
|
||
|
case 'us1.zzz.cat':
|
||
|
locationNode.textContent = 'Unites States 1 (Fremont, Linode)';
|
||
|
break;
|
||
|
case 'us2.zzz.cat':
|
||
|
locationNode.textContent = 'Unites States 2 (LAX (CN2), Bandwagon)';
|
||
|
break;
|
||
|
case 'us3.zzz.cat':
|
||
|
locationNode.textContent = 'Unites States 3 (LAX (CN2 GIA-E), Bandwagon)';
|
||
|
break;
|
||
|
case 'jp1.zzz.cat':
|
||
|
locationNode.textContent = 'Japan 1 (Tokyo 2, Linode)';
|
||
|
break;
|
||
|
case 'jp2.zzz.cat':
|
||
|
locationNode.textContent = 'Japan 2 (Tokyo 2, Linode)';
|
||
|
break;
|
||
|
case 'sg1.zzz.cat':
|
||
|
locationNode.textContent = 'Singapore (Singapore, Linode)';
|
||
|
break;
|
||
|
case 'ca1.zzz.cat':
|
||
|
locationNode.textContent = 'Canada (Toronto, Linode)';
|
||
|
break;
|
||
|
default:
|
||
|
locationNode.textContent = window.location.hostname;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
pageTitleDiv.appendChild(titlePrefixNode);
|
||
|
pageTitleDiv.appendChild(lineBreakElement);
|
||
|
pageTitleDiv.appendChild(locationNode);
|
||
|
}
|
||
|
|
||
|
//CODE FOR GAUGES
|
||
|
function drawMeter(c, amount, bk, fg, progress, prog) {
|
||
|
var ctx = c.getContext("2d");
|
||
|
var dp = window.devicePixelRatio || 1;
|
||
|
var cw = c.clientWidth * dp, ch = c.clientHeight * dp;
|
||
|
var sizScale = ch * 0.0055;
|
||
|
if (c.width == cw && c.height == ch) {
|
||
|
ctx.clearRect(0, 0, cw, ch);
|
||
|
} else {
|
||
|
c.width = cw;
|
||
|
c.height = ch;
|
||
|
}
|
||
|
ctx.beginPath();
|
||
|
ctx.strokeStyle = bk;
|
||
|
ctx.lineCap = "round";
|
||
|
ctx.lineWidth = 4 * sizScale;
|
||
|
ctx.arc(
|
||
|
c.width / 2,
|
||
|
c.height / 2,// c.height/2-58*sizScale,
|
||
|
c.height / 2 - ctx.lineWidth,//c.height/1.8-ctx.lineWidth,
|
||
|
-Math.PI * 1.2,
|
||
|
Math.PI * 0.2
|
||
|
);
|
||
|
ctx.stroke();
|
||
|
ctx.beginPath();
|
||
|
ctx.strokeStyle = fg;
|
||
|
ctx.lineCap = (amount > 0 ? "round" : "butt");
|
||
|
ctx.lineWidth = 4 * sizScale;
|
||
|
ctx.arc(
|
||
|
c.width / 2,
|
||
|
c.height / 2,// c.height/2-58*sizScale,
|
||
|
c.height / 2 - ctx.lineWidth,//c.height/1.8-ctx.lineWidth,
|
||
|
-Math.PI * 1.2,
|
||
|
amount * Math.PI * 1.2 - Math.PI * 1.2
|
||
|
);
|
||
|
ctx.stroke();
|
||
|
if (typeof progress !== "undefined") {
|
||
|
ctx.fillStyle = prog;
|
||
|
ctx.fillRect(
|
||
|
c.width * 0.3,
|
||
|
c.height - 28 * sizScale,
|
||
|
c.width * 0.4 * progress,
|
||
|
4 * sizScale
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function mbpsToAmount(s) {
|
||
|
return 1 - (1 / (Math.pow(1.3, Math.sqrt(s))));
|
||
|
}
|
||
|
|
||
|
function msToAmount(s) {
|
||
|
return 1 - (1 / (Math.pow(1.08, Math.sqrt(s))));
|
||
|
}
|
||
|
|
||
|
//SPEEDTEST AND UI CODE
|
||
|
var w = null; //speedtest worker
|
||
|
var data = null; //data from worker
|
||
|
function startStop() {
|
||
|
if (w != null) {
|
||
|
//speedtest is running, abort
|
||
|
w.postMessage('abort');
|
||
|
w = null;
|
||
|
data = null;
|
||
|
I("startStopBtn").className = "";
|
||
|
initUI();
|
||
|
} else {
|
||
|
//test is not running, begin
|
||
|
w = new Worker('speedtest_worker.js');
|
||
|
w.postMessage('start'); //Add optional parameters as a JSON object to this command
|
||
|
I("startStopBtn").className = "running";
|
||
|
w.onmessage = function (e) {
|
||
|
data = JSON.parse(e.data);
|
||
|
var status = data.testState;
|
||
|
if (status >= 4) {
|
||
|
//test completed
|
||
|
I("startStopBtn").className = "";
|
||
|
w = null;
|
||
|
updateUI(true);
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//this function reads the data sent back by the worker and updates the UI
|
||
|
function updateUI(forced) {
|
||
|
if (!forced && (!data || !w)) return;
|
||
|
console.log(data);
|
||
|
var status = data.testState;
|
||
|
I("ip").textContent = data.clientIp;
|
||
|
I("dlText").textContent = (status == 1 && data.dlStatus == 0) ? "..." : data.dlStatus;
|
||
|
drawMeter(I("dlMeter"), mbpsToAmount(Number(data.dlStatus * (status == 1 ? oscillate() : 1))), meterBk, dlColor, Number(data.dlProgress), progColor);
|
||
|
I("ulText").textContent = (status == 3 && data.ulStatus == 0) ? "..." : data.ulStatus;
|
||
|
drawMeter(I("ulMeter"), mbpsToAmount(Number(data.ulStatus * (status == 3 ? oscillate() : 1))), meterBk, ulColor, Number(data.ulProgress), progColor);
|
||
|
I("pingText").textContent = data.pingStatus;
|
||
|
drawMeter(I("pingMeter"), msToAmount(Number(data.pingStatus * (status == 2 ? oscillate() : 1))), meterBk, pingColor, Number(data.pingProgress), progColor);
|
||
|
I("jitText").textContent = data.jitterStatus;
|
||
|
drawMeter(I("jitMeter"), msToAmount(Number(data.jitterStatus * (status == 2 ? oscillate() : 1))), meterBk, jitColor, Number(data.pingProgress), progColor);
|
||
|
}
|
||
|
|
||
|
function oscillate() {
|
||
|
return 1 + 0.02 * Math.sin(Date.now() / 100);
|
||
|
}
|
||
|
|
||
|
//poll the status from the worker (this will call updateUI)
|
||
|
setInterval(function () {
|
||
|
if (w) w.postMessage('status');
|
||
|
}, 200);
|
||
|
//update the UI every frame
|
||
|
window.requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame || (function (callback, element) {
|
||
|
setTimeout(callback, 1000 / 60);
|
||
|
});
|
||
|
|
||
|
function frame() {
|
||
|
requestAnimationFrame(frame);
|
||
|
updateUI();
|
||
|
}
|
||
|
|
||
|
frame(); //start frame loop
|
||
|
//function to (re)initialize UI
|
||
|
function initUI() {
|
||
|
drawMeter(I("dlMeter"), 0, meterBk, dlColor, 0);
|
||
|
drawMeter(I("ulMeter"), 0, meterBk, ulColor, 0);
|
||
|
drawMeter(I("pingMeter"), 0, meterBk, pingColor, 0);
|
||
|
drawMeter(I("jitMeter"), 0, meterBk, jitColor, 0);
|
||
|
I("dlText").textContent = "";
|
||
|
I("ulText").textContent = "";
|
||
|
I("pingText").textContent = "";
|
||
|
I("jitText").textContent = "";
|
||
|
I("ip").textContent = "";
|
||
|
}
|
||
|
</script>
|
||
|
</head>
|
||
|
<body>
|
||
|
<div>
|
||
|
<h1 id="pageTitle"></h1>
|
||
|
<div id="test">
|
||
|
<div class="testGroup">
|
||
|
<div class="testArea">
|
||
|
<div class="testName">Download</div>
|
||
|
<canvas id="dlMeter" class="meter"></canvas>
|
||
|
<div id="dlText" class="meterText"></div>
|
||
|
<div class="unit">Mbit/s</div>
|
||
|
</div>
|
||
|
<div class="testArea">
|
||
|
<div class="testName">Upload</div>
|
||
|
<canvas id="ulMeter" class="meter"></canvas>
|
||
|
<div id="ulText" class="meterText"></div>
|
||
|
<div class="unit">Mbit/s</div>
|
||
|
</div>
|
||
|
<div class="testArea">
|
||
|
<div class="testName">Ping</div>
|
||
|
<canvas id="pingMeter" class="meter"></canvas>
|
||
|
<div id="pingText" class="meterText"></div>
|
||
|
<div class="unit">ms</div>
|
||
|
</div>
|
||
|
|
||
|
<div class="testArea">
|
||
|
<div class="testName">Jitter</div>
|
||
|
<canvas id="jitMeter" class="meter"></canvas>
|
||
|
<div id="jitText" class="meterText"></div>
|
||
|
<div class="unit">ms</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
<div id="ipArea">
|
||
|
IP Address: <span id="ip"></span>
|
||
|
</div>
|
||
|
<div id="shareArea" style="display:none">
|
||
|
<h3>Share results</h3>
|
||
|
<p>Test ID: <span id="testId"></span></p>
|
||
|
<input type="text" value="" id="resultsURL" readonly="readonly"
|
||
|
onclick="this.select();this.focus();this.select();document.execCommand('copy');alert('Link copied')"/>
|
||
|
<img src="" id="resultsImg"/>
|
||
|
</div>
|
||
|
</div>
|
||
|
<div id="startStopBtn" onclick="startStop()"></div>
|
||
|
<div id="privacyPolicy" style="display:none;">
|
||
|
<h2>Privacy Policy</h2>
|
||
|
<p>This HTML5 Speedtest server is configured with telemetry enabled.</p>
|
||
|
<h4>What data we collect</h4>
|
||
|
<p>
|
||
|
At the end of the test, the following data is collected and stored:
|
||
|
<ul>
|
||
|
<li>Test ID</li>
|
||
|
<li>Time of testing</li>
|
||
|
<li>Test results (download and upload speed, ping and jitter)</li>
|
||
|
<li>IP address</li>
|
||
|
<li>ISP information</li>
|
||
|
<li>Approximate location (inferred from IP address, not GPS)</li>
|
||
|
<li>User agent and browser locale</li>
|
||
|
<li>Test log (contains no personal information)</li>
|
||
|
</ul>
|
||
|
</p>
|
||
|
<h4>How we use the data</h4>
|
||
|
<p>
|
||
|
Data collected through this service is used to:
|
||
|
<ul>
|
||
|
<li>Allow sharing of test results (sharable image for forums, etc.)</li>
|
||
|
<li>To improve the service offered to you (for instance, to detect problems on our side)</li>
|
||
|
</ul>
|
||
|
No personal information is disclosed to third parties.
|
||
|
</p>
|
||
|
<h4>Your consent</h4>
|
||
|
<p>
|
||
|
By starting the test, you consent to the terms of this privacy policy.
|
||
|
</p>
|
||
|
<h4>Data removal</h4>
|
||
|
<p>
|
||
|
If you want to have your information deleted, you need to provide either the ID of the test or your IP
|
||
|
address. This is the only way to identify your data, without this information we won't be able to comply
|
||
|
with your request.<br/><br/>
|
||
|
Contact this email address for all deletion requests: maddie at zzz dot cat</a>.
|
||
|
</p>
|
||
|
<br/><br/>
|
||
|
<a id="privacyCloseBtn" class="privacy" href="#"
|
||
|
onclick="I('privacyPolicy').style.display='none'">Close</a><br/>
|
||
|
</div>
|
||
|
<div><a class="privacy" href="#" onclick="I('privacyPolicy').style.display=''">Privacy</a></div>
|
||
|
</div>
|
||
|
<script type="text/javascript">setPageTitle();</script>
|
||
|
<script type="text/javascript">setTimeout(initUI, 100);</script>
|
||
|
</body>
|
||
|
</html>
|