Skip to content

Instantly share code, notes, and snippets.

@slashinfty
Created April 18, 2021 12:14
Show Gist options
  • Save slashinfty/f95727dfe4b5b8a91e3a161349263059 to your computer and use it in GitHub Desktop.
Save slashinfty/f95727dfe4b5b8a91e3a161349263059 to your computer and use it in GitHub Desktop.
Fake LiveSplit w/ max 10 splits (requires Skeleton CSS Boilerplate from http://getskeleton.com/)
body {
width: 600px;
background-color: #F1F1D4;
}
#timer {
font-size: 100px;
text-align: right;
margin-top: -35px;
}
.c {
text-align: center;
}
.r {
text-align: right;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Split Stopwatch</title>
<!--<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Inconsolata:wght@300&display=swap" rel="stylesheet"> -->
<link rel="stylesheet" href="index.css">
<link rel="stylesheet" href="normalize.css">
<link rel="stylesheet" href="skeleton.css">
</head>
<body>
<div class="container">
<div class="row c" style="margin-top: 5%;">
<p><button onclick="openFile();">Import</button><input type="file" onchange="upload();" id="upload" accept=".json" hidden>&nbsp;<button onclick="stopwatch.saveBest();">Save Best</button>&nbsp;<button onclick="stopwatch.savePB();">Save PB</button>&nbsp;<button onclick="stopwatch.export();">Export</button></p>
<p><button onclick="stopwatch.start();">Start</button>&nbsp;<button onclick="stopwatch.split();">Split</button>&nbsp;<button onclick="stopwatch.reset();">Reset</button>&nbsp;<button onclick="stopwatch.stop();">Stop</button></p>
</div>
<div class="row c">
<div id="game" class="eleven columns"><h3>Game Name</h3></div>
</div>
<div class="row">
<div id="timer" class="eleven columns">0.0</div>
</div>
<div class="row" id="headers">
<div class="five columns" id="category">Category Name</div>
<div class="two columns r">Delta</div>
<div class="two columns r">Split</div>
<div class="two columns r">Save</div>
</div>
<div class="row"><span class="eleven columns" style="margin-top: -18px; margin-bottom: -28px;"><hr></span></div>
<div class="row" id="segment1">
<div class="five columns" id="title1"></div>
<div class="two columns r" id="delta1"></div>
<div class="two columns r" id="split1"></div>
<div class="two columns r" id="save1"></div>
</div>
<div class="row" id="segment2">
<div class="five columns" id="title2"></div>
<div class="two columns r" id="delta2"></div>
<div class="two columns r" id="split2"></div>
<div class="two columns r" id="save2"></div>
</div>
<div class="row" id="segment3">
<div class="five columns" id="title3"></div>
<div class="two columns r" id="delta3"></div>
<div class="two columns r" id="split3"></div>
<div class="two columns r" id="save3"></div>
</div>
<div class="row" id="segment4">
<div class="five columns" id="title4"></div>
<div class="two columns r" id="delta4"></div>
<div class="two columns r" id="split4"></div>
<div class="two columns r" id="save4"></div>
</div>
<div class="row" id="segment5">
<div class="five columns" id="title5"></div>
<div class="two columns r" id="delta5"></div>
<div class="two columns r" id="split5"></div>
<div class="two columns r" id="save5"></div>
</div>
<div class="row" id="segment6">
<div class="five columns" id="title6"></div>
<div class="two columns r" id="delta6"></div>
<div class="two columns r" id="split6"></div>
<div class="two columns r" id="save6"></div>
</div>
<div class="row" id="segment7">
<div class="five columns" id="title7"></div>
<div class="two columns r" id="delta7"></div>
<div class="two columns r" id="split7"></div>
<div class="two columns r" id="save7"></div>
</div>
<div class="row" id="segment8">
<div class="five columns" id="title8"></div>
<div class="two columns r" id="delta8"></div>
<div class="two columns r" id="split8"></div>
<div class="two columns r" id="save8"></div>
</div>
<div class="row" id="segment9">
<div class="five columns" id="title9"></div>
<div class="two columns r" id="delta9"></div>
<div class="two columns r" id="split9"></div>
<div class="two columns r" id="save9"></div>
</div>
<div class="row" id="segment10">
<div class="five columns" id="title10"></div>
<div class="two columns r" id="delta10"></div>
<div class="two columns r" id="split10"></div>
<div class="two columns r" id="save10"></div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js" integrity="sha512-Qlv6VSKh1gDKGoJbnyA5RMXYcvnpIqhO++MhIM2fStMcGT9i2T//tSwYFlcyoRRDcDZ+TYHpH8azBBCyhpSeqw==" crossorigin="anonymous"></script>
<script src="index.js"></script>
</body>
</html>
var stopwatch;
const delay = ms => new Promise(res => setTimeout(res, ms));
class Stopwatch {
constructor(display, file) {
this.running = false;
this.display = display;
this.splits = file;
this.delay = this.splits.delay;
this.currentSplits = [];
this.currentSegments = [];
this.reset();
this.load();
this.segment = 0;
}
reset() {
if (this.running) !this.running;
this.times = [ 0, 0, 0, 0 ];
this.segment = 0;
this.print(this.times);
this.load(this.splits);
}
async start() {
if (this.times.every(t => t === 0)) await delay(this.delay);
if (!this.time) this.time = performance.now();
if (!this.running) {
this.segment++;
this.running = true;
requestAnimationFrame(this.step.bind(this));
}
}
split() {
let times = this.times;
// Set split time
document.getElementById('split' + this.segment).innerText = this.format(times);
// Get ms and store it
const ms = this.arrayToMS(times);
this.currentSplits.push(ms);
// Get delta
const pb = this.splits.splits[this.segment - 1].pb;
const delta = ms <= pb ? pb - ms : ms - pb;
document.getElementById('delta' + this.segment).innerText = ms <= pb ? '-' + this.format(this.msToArray(delta)) : '+' + this.format(this.msToArray(delta));
// Get time save
const pbSeg = this.splits.splits[this.segment - 1].seg;
const lastTime = this.segment === 1 ? 0 : this.currentSplits[this.segment - 2];
const seg = ms - lastTime;
this.currentSegments.push(seg);
const save = seg <= pbSeg ? pbSeg - seg : seg - pbSeg;
document.getElementById('save' + this.segment).innerText = seg <= pbSeg ? '-' + this.format(this.msToArray(save)) : '+' + this.format(this.msToArray(save));
if (this.segment === this.splits.splits.length) this.stop();
else this.segment++;
}
stop() {
this.running = false;
this.time = null;
}
saveBest() {
this.splits.splits.forEach((s, i) => s.best = this.currentSegments[i] < s.best ? this.currentSegments[i] : s.best);
}
savePB() {
if (this.segment !== this.splits.splits.length) return;
console.log(this.currentSegments);
this.splits.splits.forEach((s, i) => {
s.pb = this.currentSplits[i];
s.seg = this.currentSegments[i];
if (s.seg < s.best) s.best = s.seg;
});
}
step(timestamp) {
if (!this.running) return;
this.calculate(timestamp);
this.time = timestamp;
this.print();
requestAnimationFrame(this.step.bind(this));
}
calculate(timestamp) {
var diff = timestamp - this.time;
// Hundredths of a second are 100 ms
this.times[3] += diff / 100;
// Seconds are 100 hundredths of a second
if (this.times[3] >= 10) {
this.times[2] += 1;
this.times[3] -= 10;
}
// Minutes are 60 seconds
if (this.times[2] >= 60) {
this.times[1] += 1;
this.times[2] -= 60;
}
// Hours are 60 minutes
if (this.times[1] >= 60) {
this.times[0] += 1;
this.times[1] -= 60;
}
}
load() {
[...document.querySelectorAll('div[id^="segment"] > div')].forEach(e => e.innerHTML = '&nbsp;');
document.getElementById('game').innerHTML = `<h4>` + this.splits.game + `</h4>`;
document.getElementById('category').innerText = this.splits.category;
this.splits.splits.forEach((split, index) => {
let count = index + 1;
document.getElementById('title' + count).innerText = split.name;
document.getElementById('split' + count).innerText = this.format(this.msToArray(split.pb));
document.getElementById('save' + count).innerText = this.format(this.msToArray(split.seg - split.best));
});
}
export() {
if (this.running) return;
const blob = new Blob([JSON.stringify(this.splits)], {type: 'application/json'});
const saveAs = window.saveAs;
saveAs(blob, 'splits.json');
}
msToArray(perf) {
const arr = [ 0, 0, 0, 0 ];
const parts = (perf / 1000).toString().split('.');
if (parts.length > 1) arr[3] = Math.round(parseInt((parts[1] + '0000').substr(0, 3))) / 100;
arr[2] = parseInt(parts[0]);
if (arr[2] >= 60) {
arr[1] = Math.floor(arr[2] / 60);
arr[2] = arr[2] % 60;
}
if (arr[1] >= 60) {
arr[0] = Math.floor(arr[1] / 60);
arr[1] = arr[1] % 60;
}
return arr;
}
arrayToMS(arr) {
return Math.round((arr[3] * 100 + arr[2] * 1000 + arr[1] * 60000 + arr[0] * 3600000) * 1000) / 1000;
}
print() {
this.display.innerText = this.format(this.times);
}
format(times) {
if (times[0] > 0) {
return `${times[0]}:${this.pad0(times[1], 2)}:${this.pad0(times[2], 2)}.${Math.floor(times[3])}`
} else if (times[1] > 0) {
return `${times[1]}:${this.pad0(times[2], 2)}.${Math.floor(times[3])}`
} else if (times[2] > 0) {
return `${times[2]}.${Math.floor(times[3])}`;
} else {
return `0.${Math.floor(times[3])}`;
}
}
pad0 = (value, count) => {
var result = value.toString();
for (; result.length < count; --count)
result = '0' + result;
return result;
}
}
const openFile = () => document.getElementById('upload').click();
const upload = () => {
const file = document.getElementById('upload').files[0];
const reader = new FileReader();
reader.onloadend = () => stopwatch = new Stopwatch(
document.getElementById('timer'),
JSON.parse(reader.result)
);
reader.readAsText(file);
}
{
"game": "Super Mario World",
"category": "No Starworld",
"delay": 2200,
"splits": [
{
"name": "Iggy",
"pb": 193932,
"seg": 193932,
"best": 193265
},
{
"name": "Morton",
"pb": 518597,
"seg": 324665,
"best": 308781
},
{
"name": "Ludwig",
"pb": 1014955,
"seg": 496358,
"best": 493981
},
{
"name": "Roy",
"pb": 1290261,
"seg": 275306,
"best": 257712
},
{
"name": "Wendy",
"pb": 1698370,
"seg": 408109,
"best": 408109
},
{
"name": "Bowser",
"pb": 2305276,
"seg": 606906,
"best": 568239
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment