Skip to content

Instantly share code, notes, and snippets.

@elundmark
Created May 24, 2022 05:53
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save elundmark/158a652ab1dedf98efc87c5f33ce6b91 to your computer and use it in GitHub Desktop.
Save elundmark/158a652ab1dedf98efc87c5f33ce6b91 to your computer and use it in GitHub Desktop.
Test Monitor FPS in Browser
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Test Monitor FPS in Browser</title>
<style>
html {
background-color: #000;
}
html, body, .output { padding: 0; }
html, body, .output, .canvas, .instructions { margin: 0; }
body {
opacity: 1;
color: #000;
}
body.error {
background-color: #F00;
color: #FFF;
font-size: 200%;
font-weight: bold;
padding: 1em;
text-align: center;
}
[data-resizing] { opacity: 0.5; }
.canvas, .output { transform: translateZ(0); }
.canvas {
position: absolute;
top: 0;
left: 0;
border: 0;
background-repeat: no-repeat;
background-position: 0 0;
}
.cl { clear: both; }
/* p, li { text-align: justify; } */
.ctr { text-align: center; }
.hud {
position: absolute;
bottom: 0;
right: 0;
}
.btn, .output {
-webkit-touch-callout: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.hud::after, .instructions::after {
content: "";
display: table;
clear: both;
}
.btn {
float: right;
margin: 0 0.5vmax 0.5vmax 0;
}
.btn, .close-instructions { cursor: pointer; }
.clr, .speed { display: block; }
.btn, .clr, .speed, .links { line-height: 0; }
img, .clr, .speed, .links {
width: 4vmax;
height: 4vmax;
}
.clr {
background-size: 25% 25%;
background-repeat: no-repeat;
background-image: url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20height='100'%20width='100'%3e%3cpath%20fill='%238A0000'%20d='M0%200v100L100%200z'/%3e%3c/svg%3e");
}
.speed {
font: 3vmax/4vmax Ubuntu, 'PT Sans', Arial, FreeSans, sans-serif;
text-align: center;
}
.links { margin-top: 0; }
.links.url img {
width: 2.5vmax;
height: 2.5vmax;
position: relative;
top: 0.75vmax;
left: 0.75vmax;
}
.links.reset_btn img {
width: 3vmax;
height: 3vmax;
position: relative;
top: 0.5vmax;
left: 0.5vmax;
}
.hl {
border: solid 0.1vmax #000;
box-shadow: -0.1vmax -0.1vmax 0 #FFF, 0.1vmax 0.1vmax 0 #FFF, -0.1vmax 0.1vmax 0 #FFF, 0.1vmax -0.1vmax 0 #FFF;
}
[data-active].speed {
font-weight: bold;
color: #FFF;
background-color: #000;
border-color: #FFF;
}
.instructions {
height: 50vh;
overflow: auto;
margin: 0;
opacity: 0;
transition: opacity 0.7s ease-in;
position: relative;
top: -0.65vmax;
right: 0.5vmax;
box-sizing: border-box;
font-family: 'PT Serif', 'Book Antiqua', 'Palatino Linotype', 'Times New Roman', serif;
font-size: 1.2em;
padding: 1em 1.5em 1em 1.5em;
text-align: left;
line-height: 1.3334;
float: right;
width: 30em;
background: #FFF;
}
.instructions h1 {
margin-top: 0;
text-transform: capitalize;
font-size: 2em;
}
.instructions h1 span {
border-bottom: 0.104167em solid #8A0000;
}
.instructions h1, .instructions h2, .instructions h3 { text-shadow: 0 0.1em 0.05em #FFF; }
[data-loaded] .instructions { opacity: 1; }
[data-started] .instructions { display: none; }
@media all and ( max-width: 600px ) { .instructions { height: 80vh; width: 90%; margin: 0 auto; } }
@media all and ( max-height: 600px ) { .instructions { height: 80vh; } }
.close-instructions {
position: absolute;
right: 0.3em;
top: 0.3em;
}
.close-instructions img {
width: 1.75em;
height: auto;
}
[data-autohide][data-started] .hud { display: none; }
[data-autohide][data-started][data-paused] .hud { display: block; }
pre, kbd, samp, code {
text-decoration: none;
font-weight: normal;
background-color: #9AEDFE;
}
pre, code { background-color: #E6E6E6; }
pre, code, samp, code, .output mark {
font-family: 'Ubuntu Mono - Bront', 'Ubuntu Mono', Menlo, Consolas, monospace;
}
pre { padding: 0.5em; }
ul {
padding-left: 1.25em;
list-style: square;
}
.output {
cursor: auto;
position: absolute;
top: 0;
right: 0;
color: #000;
}
.output mark {
font-style: normal;
font-weight: normal;
font-size: 2.2vw;
line-height: 1;
background-color: #8A0000;
color: #FFF;
display: inline-block;
min-width: 13vw;
padding: 0.25vw 0.5vw 0.3vw 0.6vw;
border-radius: 0 0 0 0.2vmax;
}
.btn, .clr, .speed, .links { background-color: #FFF; }
html, body, .clr, .speed { overflow: hidden; }
img { border: 0; }
.status .pause-icon {
display: none;
}
.toggle-border { display: none; }
.toggle-border.border-no { display: inline-block; }
[data-has-borders] .toggle-border.border-no { display: none; }
[data-has-borders] .toggle-border.border-yes { display: inline-block; }
[data-started] .status .pause-icon { display: inline-block; }
[data-started] .status .play-icon { display: none; }
[data-started][data-paused] .status .play-icon, [data-paused] .status .play-icon { display: inline-block; }
[data-started][data-paused] .status .pause-icon { display: none; }
.show-menu { display: none; }
[data-started] .show-menu { display: inline-block; }
</style>
</head>
<body data-clickable="true">
<canvas id="canvas" class="canvas" width="1" height="1" data-clickable="true"></canvas>
<p class="output ctr"><mark id="fps">Stopped</mark></p>
<div class="hud" data-clickable="true">
<article class="instructions hl">
<h1 class="ctr"><span>Test Monitor Response Time and Frame Rate in the Browser</span></h1>
<p class="ctr">
Close this message to start the test.
I recommend Chrome or Chromium in Fullscreen for optimal results.
This is <strong>not</strong> a professional tool, you need
<a href="https://www.vsynctester.com/" title="vsynctester.com" target="_blank">something</a>
<a href="https://www.testufo.com/frameskipping" title="testufo.com" target="_blank">better</a>
for that.
</p>
<hr/>
<h2 class="ctr">All keyboard shortcuts</h2>
<ul>
<li><kbd>Space</kbd> to Pause&#8725;Un-pause.</li>
<li><kbd>1</kbd> <kbd>2</kbd> <kbd>3</kbd> to change colors.</li>
<li><kbd>+</kbd> <kbd>-</kbd> <kbd>0</kbd> to set the grid size.</li>
<li><kbd>Escape</kbd> to stop and show the startup message.</li>
<li><kbd>Period</kbd> Advance one frame when <em>paused</em> (debugging).</li>
<li><kbd>Comma</kbd> One frame backwards when <em>paused</em> (debugging).</li>
</ul>
<hr/>
<h2 class="ctr">Advanced usage</h2>
<p class="ctr">You can specify all options stated below, in the url:</p>
<pre>?b=0&amp;g=2&amp;autostart=true</pre>
<h3>All options</h3>
<ul>
<li><code>autostart=true</code> Skip this screen and start.</li>
<li><code>autohide=true</code> Hide all controls when running.</li>
<li><code>fs=true</code> Switch to fullscreen automatically.</li>
<li><code>c1=ffffff</code> Override the color of the block cursor.</li>
<li><code>c2=000</code> Override the color of the grid.</li>
<li><code>c2=FF00FF</code> Override the color of the background.</li>
<li><code>g=4</code> Set number of cells per row.</li>
<li><code>b=8</code> Set border size.</li>
<li><code>sk=1</code> Set speed limiter (skips every N cycles).</li>
<li><code>bc=32</code> Override the highest number of cells.</li>
<li><code>dec=3</code> Set decimals of the fps.</li>
<li><code>u=500</code> Update fps every N milliseconds.</li>
<li>
<code>avg=true</code> Calculates by using 5 sec average.
This is turned off by default, it's mostly a hack fix for Firefox to calculate
a better average of the
<a href="https://bugzilla.mozilla.org/buglist.cgi?quicksearch=requestanimationframe" target="_blank">
execution time</a> which will jump around quite alot, unlike on Chrome or Chromium
which will produce a stable result (on a 60Hz monitor) between
<samp>60.046</samp> and <samp>60.049</samp>. These numbers match
the actual refresh rate of my refrence monitor on my laptop.
</li>
</ul>
<hr/>
<h2 class="ctr">Credits</h2>
<ul>
<li>
This work is licensed under a <a href="http://creativecommons.org/licenses/by-sa/4.0/">
Creative Commons Attribution-ShareAlike 4.0 International License</a>.
</li>
<li>
SVG images created in <a href="https://inkscape.org/">Inkscape</a> and
compressed with <a href="https://www.npmjs.com/package/svgo" target="_blank">svgo</a>
and <a href="https://www.npmjs.com/package/mini-svg-data-uri" target="_blank">mini-svg-data-uri</a>.
</li>
<li>
No libraries used, just plain javascript ES6 and inline data-uri svg.
So if you'd like to download and use this for yourself, go right ahead,
it works offline and you can even open it as a <code>file://</code>
</li>
</ul>
<div class="close-instructions" data-clickable="true">
<img src="data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20height='100'%20width='100'%3e%3crect%20transform='rotate(45%2050%2050)'%20ry='0'%20y='15'%20x='46'%20height='70'%20width='8'%20fill='gray'/%3e%3crect%20transform='rotate(45%2050%2050)'%20ry='0'%20y='46'%20x='15'%20height='8'%20width='70'%20fill='gray'/%3e%3c/svg%3e" alt="" data-clickable="true"/>
</div>
</article>
<p class="btn links url hl cl" title="Link that carries all your settings">
<a href="#" id="curr-link">
<img src="data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20512%20512'%3e%3cpath%20fill='gray'%20d='M326.612%20185.391c59.747%2059.809%2058.927%20155.698.36%20214.59-.11.12-.24.25-.36.37l-67.2%2067.2c-59.27%2059.27-155.699%2059.262-214.96%200-59.27-59.26-59.27-155.7%200-214.96l37.106-37.106c9.84-9.84%2026.786-3.3%2027.294%2010.606.648%2017.722%203.826%2035.527%209.69%2052.721%201.986%205.822.567%2012.262-3.783%2016.612l-13.087%2013.087c-28.026%2028.026-28.905%2073.66-1.155%20101.96%2028.024%2028.579%2074.086%2028.749%20102.325.51l67.2-67.19c28.191-28.191%2028.073-73.757%200-101.83-3.701-3.694-7.429-6.564-10.341-8.569a16.037%2016.037%200%2001-6.947-12.606c-.396-10.567%203.348-21.456%2011.698-29.806l21.054-21.055c5.521-5.521%2014.182-6.199%2020.584-1.731a152.482%20152.482%200%200120.522%2017.197zM467.547%2044.449c-59.261-59.262-155.69-59.27-214.96%200l-67.2%2067.2c-.12.12-.25.25-.36.37-58.566%2058.892-59.387%20154.781.36%20214.59a152.454%20152.454%200%200020.521%2017.196c6.402%204.468%2015.064%203.789%2020.584-1.731l21.054-21.055c8.35-8.35%2012.094-19.239%2011.698-29.806a16.037%2016.037%200%2000-6.947-12.606c-2.912-2.005-6.64-4.875-10.341-8.569-28.073-28.073-28.191-73.639%200-101.83l67.2-67.19c28.239-28.239%2074.3-28.069%20102.325.51%2027.75%2028.3%2026.872%2073.934-1.155%20101.96l-13.087%2013.087c-4.35%204.35-5.769%2010.79-3.783%2016.612%205.864%2017.194%209.042%2034.999%209.69%2052.721.509%2013.906%2017.454%2020.446%2027.294%2010.606l37.106-37.106c59.271-59.259%2059.271-155.699.001-214.959z'/%3e%3c/svg%3e" alt=""/>
</a>
</p>
<p class="btn links reset_btn hl" title="Reset All &mdash; reset and reload page">
<img id="reset-all" src="data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20512%20512'%3e%3cpath%20d='M460.9%2077.2c14.3%200%2026.4%204.9%2036.3%2014.8%209.9%209.9%2014.8%2022%2014.8%2036.3v255.5c0%2013.6-4.9%2025.5-14.8%2035.8-9.9%2010.2-22%2015.3-36.3%2015.3H216.7c-12.9%200-24.9-4.8-35.8-14.3L7.2%20269.3c-9.5-8.9-9.5-18.1%200-27.6L180.9%2090.4c10.2-8.9%2022.1-13.3%2035.8-13.3h244.2m-71.5%20281.1l36.8-37.8-65.4-64.4%2065.4-65.4-36.8-36.8-65.4%2064.4-65.4-64.4-36.8%2036.8%2065.4%2065.4-65.4%2064.4%2036.8%2037.8%2065.4-65.4%2065.4%2065.4'/%3e%3c/svg%3e" alt=""/>
</p>
<p class="btn hl">
<span class="clr" id="choose-clr-bg"></span>
</p>
<p class="btn hl">
<span class="clr" id="choose-clr-border"></span>
</p>
<p class="btn hl">
<span class="clr" id="choose-clr-pixel"></span>
</p>
<p class="btn hl">
<img class="toggle-border border-no" src="data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20height='100'%20width='100'%3e%3crect%20transform='rotate(45%2050%2050)'%20ry='0'%20x='49'%20height='100'%20width='2'/%3e%3c/svg%3e" alt=""/>
<img class="toggle-border border-yes" src="data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='100'%20height='100'%3e%3crect%20ry='0'%20x='31'%20width='4'%20height='100'/%3e%3crect%20ry='0'%20x='64'%20width='4'%20height='100'/%3e%3crect%20ry='0'%20y='31'%20width='100'%20height='4'/%3e%3crect%20ry='0'%20y='64'%20width='100'%20height='4'/%3e%3c/svg%3e" alt=""/>
</p>
<p class="btn hl">
<span class="speed" id="skip-frame-5">&frac16;</span>
</p>
<p class="btn hl">
<span class="speed" id="skip-frame-4">&frac15;</span>
</p>
<p class="btn hl">
<span class="speed" id="skip-frame-3">&frac14;</span>
</p>
<p class="btn hl">
<span class="speed" id="skip-frame-2">&frac13;</span>
</p>
<p class="btn hl">
<span class="speed" id="skip-frame-1">&frac12;</span>
</p>
<p class="btn hl">
<img id="smaller-grid" src="data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20height='100'%20width='100'%3e%3cpath%20fill='white'%20d='M0%200h100v100H0z'/%3e%3crect%20ry='0'%20y='45'%20x='15'%20height='10'%20width='70'/%3e%3c/svg%3e" alt=""/>
</p>
<p class="btn hl">
<img id="bigger-grid" src="data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20height='100'%20width='100'%3e%3cpath%20fill='white'%20d='M0%200h100v100H0z'/%3e%3crect%20ry='0'%20y='15'%20x='45'%20height='70'%20width='10'/%3e%3crect%20ry='0'%20y='45'%20x='15'%20height='10'%20width='70'/%3e%3c/svg%3e" alt=""/>
</p>
<p class="btn status hl">
<img class="pause-icon" src="data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20height='100'%20width='100'%3e%3crect%20ry='0'%20y='15'%20x='25'%20height='70'%20width='16'/%3e%3crect%20ry='0'%20y='15'%20x='58'%20height='70'%20width='16'/%3e%3c/svg%3e" alt=""/>
<img class="play-icon" src="data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='512'%20height='512'%3e%3cpath%20d='M96%20448l320-192L96%2064v384z'/%3e%3c/svg%3e" alt=""/>
</p>
<p class="btn links show-menu hl" title="Stop &mdash; and show the startup message">
<img id="escape-to-menu" src="data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2096%2096'%3e%3cpath%20fill='gray'%20d='M27%2065l21-21%2021%2021%206.5-6.5L48%2031%2020.5%2058.5z'/%3e%3c/svg%3e" alt=""/>
</p>
</div>
<script>
/* jshint esversion: 6 */
(function(){ "use strict";
// startup
if (typeof(window.performance && window.performance.now) !== "function"
|| typeof(document.querySelector("#canvas").getContext) !== "function"
|| typeof(window.SVGElement) !== "function"
|| typeof(window.requestAnimationFrame) !== "function"
|| typeof(window.cancelAnimationFrame) !== "function") {
document.body.className = "error";
document.body.innerHTML = `This browser is not supported.<br/>
Try the latest version of Chrome or Chromium`;
return;
}
const fullPath = document.location.href.replace(/\?[a-zA-Z0-9#=&%_+\-]*$/, "");
function Q(s, elem=document){
return elem.querySelector(s);
}
function QAll(s, elem=document){
return elem.querySelectorAll(s);
}
function reduce(a){
let sum = 0;
for (let i = 0, len = a.length; i < len; i += 1) {
sum += a[i];
}
return sum;
}
window.addEventListener("load", () => {
let $body = document.body,
$speedLimits = QAll("[id^='skip-frame-']"),
$canvas = Q("#canvas"),
canvasCtx = $canvas.getContext("2d", { alpha: false }),
$clrPixel = Q("#choose-clr-pixel"),
$clrBorder = Q("#choose-clr-border"),
$clrBg = Q("#choose-clr-bg"),
$clrCtrlElements = [ $clrPixel, $clrBorder, $clrBg ],
$fps = Q("#fps"),
$gridBtns = QAll("[id$='-grid']"),
$toggleBorder = QAll(".toggle-border"),
$clrBtns = QAll("[id^='choose-clr-']"),
$smallerGrid = Q("#smaller-grid"),
$biggerGrid = Q("#bigger-grid"),
$currLink = Q("#curr-link"),
$escToMenu = Q("#escape-to-menu"),
$resetAll = Q("#reset-all"),
$pauseToggles = QAll(".status img"),
$instructions = Q(".instructions"),
cachedCanvasImages = { img: [], pos: 0 },
isBusy = false,
colorMap = [
/* 0 1 2 3 4 5 6 7 */
"#FFFFFF", "#FDFDFD", "#BFBFBF", "#808080", "#666666", "#4D4D4D", "#282A36", "#000000",
/* 8 9 10 11 12 */
"#0000FF", "#00FF00", "#7FFFD4", "#00FFFF", "#3D5ADC",
/* 13 14 15 16 17 */
"#8A0000", "#FF0000", "#FF5555", "#FF6E67", "#FF7F50",
/* 18 19 20 */
"#F7347A", "#FF69B4", "#BD93F9",
/* 21 22 */
"#FFFF00", "#F1FA8C",
/* 23 24 */
"#65411F", "#9b622d",
],
currColor = [ 14, 7, 2 ],
defaults = {
c1: colorMap[currColor[0]],
c2: colorMap[currColor[1]],
c3: colorMap[currColor[2]],
g: 10,
sk: 0,
b: 4,
bc: 15,
dec: 2,
u: 500,
autostart: false,
autohide: false,
avg: false,
fs: false
},
optNamesArr = [],
opts = ( () => {
let o = {};
for (let key in defaults) {
optNamesArr.push(key);
if (defaults.hasOwnProperty(key)) o[key] = defaults[key];
}
return o;
})(),
started = false,
forcePause = false,
animationId,
cms = 0,
cstart = 0,
prevFps,
fpsAvgPoints = 10,
fpsTimes = [...Array(fpsAvgPoints)].map( () => 0),
fpsTimesFirstRun = true,
fpsTimesPlacer = 0,
refreshHudInt = 0,
fpsWaitMs = 0,
startMs = 0,
moves = 0,
skipped = 0,
gridSize = 0,
xMax = $body.offsetWidth,
yMax = window.innerHeight,
cursorSizeX = 0,
cursorSizeY = 0,
wpx = cursorSizeX * opts.g,
wpy = cursorSizeY * opts.g,
off
;
function debounce(func, wait, immediate){
/*
* http://davidwalsh.name/essential-javascript-functions
* Returns a function, that, as long as it continues to be invoked, will not
* be triggered. The function will be called after it stops being called for
* N milliseconds. If `immediate` is passed, trigger the function on the
* leading edge, instead of the trailing.
*/
let timeout;
return function (...args) {
let context = this,
callNow,
later
;
later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
if (args[0] && args[0].type === "submit") {
// Inhibit form submission early since this delays everything
args[0].preventDefault();
}
callNow = immediate && !timeout;
window.clearTimeout(timeout);
timeout = window.setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
function selectColor(i){
let hexPatt = /^#([0-9a-f]{3}|[0-9a-f]{6})$/;
if (opts[`c${i+1}`] && hexPatt.test(opts[`c${i+1}`])) {
return opts[`c${i+1}`];
}
return colorMap[currColor[i]];
}
function calcLayout(){
// measure browser viewport
xMax = Math.floor(window.innerWidth + opts.b);
yMax = Math.floor(window.innerHeight + opts.b);
$body.style.width = `${xMax}px`;
$body.style.height = `${yMax}px`;
cursorSizeX = Math.floor(xMax / opts.g);
cursorSizeY = Math.floor((yMax / xMax) * cursorSizeX);
wpx = cursorSizeX * opts.g;
wpy = cursorSizeY * opts.g;
}
function makeOffScreenCanvases(){
let oc = document.createElement("canvas")
// ,gc = document.createElement("canvas")
;
oc.width = `${xMax}`;
oc.height = `${yMax}`;
// gc.width = `${xMax}`;
// gc.height = `${yMax}`;
return {
screen: oc.getContext("2d", { alpha: false })
// ,grid: gc.getContext("2d", { alpha: false })
};
}
function setCanvasGrid(){
let start_microsec = performance.now();
let w = cursorSizeX + 0,
h = cursorSizeY + 0,
gridImg,
s = opts.g + 0,
len = s - 1,
c_cur = selectColor(0),
c_bor = selectColor(1),
c_bg = selectColor(2),
cxw = w - opts.b,
cyh = h - opts.b,
prevCursor
;
isBusy = true;
forcePause = true;
$body.setAttribute("data-resizing", "true");
// create cache canvas(es)
off = null;
off = makeOffScreenCanvases();
// reset cached images array
cachedCanvasImages.img = [];
cachedCanvasImages.pos = 0;
// set grid bg color
off.screen.fillStyle = c_bg;
// fill in gris bg color
off.screen.fillRect(0, 0, xMax, yMax);
// set screen border color
off.screen.fillStyle = c_bor;
for (let y = 0, xp, xw, yp, yh; y < len; y += 1) {
// Horizonal lines
xp = 0;
xw = xMax + 0;
yp = (h * (y + 1)) - opts.b;
yh = opts.b + 0;
off.screen.fillRect(xp, yp, xw, yh);
}
for (let x = 0, xp, xw, yp, yh; x < len; x += 1) {
// Vertical lines
xp = (w * (x + 1)) - opts.b;
xw = opts.b + 0;
yp = 0;
yh = yMax + 0;
off.screen.fillRect(xp, yp, xw, yh);
}
// save as image
// gridImg = off.grid.getImageData(0, 0, xMax, yMax);
off.screen.fillStyle = c_cur;
for (let i = 0, xPos = 0, yPos = 0, xComp = 0, yComp = 0; i < gridSize; i += 1){
xComp = xPos + (cxw * 2) > xMax ? xMax - (xPos + cxw) : 0;
yComp = yPos + (cyh * 2) > yMax ? yMax - (yPos + cyh) : 0;
if (prevCursor !== undefined) {
// paint over the previous cursor
off.screen.fillStyle = c_bg;
off.screen.fillRect(prevCursor.x, prevCursor.y, prevCursor.w, prevCursor.h);
off.screen.fillStyle = c_cur;
}
// off.screen.clearRect(0, 0, xMax, yMax);
// off.screen.putImageData(gridImg, 0, 0);
off.screen.fillRect(xPos, yPos, cxw + xComp, cyh + yComp);
cachedCanvasImages.img.push(off.screen.getImageData(0, 0, xMax, yMax));
// calculate the next cursor position - save the current as "prevCursor" first
prevCursor = { x: xPos, y: yPos, w: cxw + xComp, h: cyh + yComp };
if (opts.g === 1) {
if (xPos > 0) {
xPos = 0;
} else {
xPos = w;
}
} else {
xPos += w;
if (xPos >= wpx) {
// forward
// count down one row
xPos = 0;
yPos += h;
if (yPos >= wpy) {
// reset count to row 0
yPos = 0;
}
}
}
if (gridSize === 1) { // done once because its done now
off.screen.fillStyle = c_bg;
off.screen.fillRect(0, 0, cxw, cyh);
cachedCanvasImages.img.push(off.screen.getImageData(0, 0, xMax, yMax));
}
}
gridSize = cachedCanvasImages.img.length;
$canvas.width = `${xMax}`;
$canvas.height = `${yMax}`;
canvasCtx.clearRect(0, 0, xMax, yMax);
canvasCtx.putImageData(cachedCanvasImages.img[0], 0, 0);
forcePause = false;
console.log(performance.now()-start_microsec);
window.setTimeout( () => {
$body.removeAttribute("data-resizing");
isBusy = false;
}, 100);
}
function handleIfBusy(){
isBusy = true;
$body.setAttribute("data-resizing", "true");
}
function normalizeGridSize(){
opts.g = Math.max(1, opts.g);
opts.g = Math.min(opts.bc, opts.g);
gridSize = opts.g * opts.g;
}
function normalizeBorderSize(){
opts.b = Math.max(0, opts.b);
opts.b = Math.min(100, opts.b);
}
function parseUserOpts(o){
let s = window.decodeURIComponent(document.location.search.replace(/^\?+|&+$/g, "")).toLowerCase().split(/&+/),
colorPatt = /^(c[123])=([0-9a-f]{3}$|[0-9a-f]{6}$)/,
numPatt = /^([a-z]+)=([0-9]+)$/,
boolPatt = /^([a-z]+)=(true|false)$/
;
s = s.filter( (v) => !!v);
try {
for (let i = 0, val, len = s.length, m; i < len; i += 1) {
val = null;
if ((m=s[i].match(colorPatt))) {
val = `#${m[2]}`;
} else if ((m=s[i].match(numPatt))) {
val = +m[2];
} else if ((m=s[i].match(boolPatt))) {
val = !!JSON.parse(m[2]);
}
if (val !== null && o.hasOwnProperty(m[1]) && typeof(defaults[m[1]]) === typeof(o[m[1]])) {
o[m[1]] = val;
}
}
} catch(e){
console.error(e);
}
normalizeGridSize();
normalizeBorderSize();
opts.sk = Math.max(0, opts.sk);
opts.sk = Math.min([ ...$speedLimits ].length, opts.sk);
opts.u = Math.max(opts.u, 50);
[ ...$speedLimits ].forEach( (elem) => elem.removeAttribute("data-active"));
if (opts.sk > 0) Q(`#skip-frame-${opts.sk}`).setAttribute("data-active", "true");
return o;
}
function makeFullOptionsPath(){
let path = document.location.pathname, search = "";
if (!/\/index\.html$/.test(path)) path = path.replace(/\/+$/, "") + "/";
for (let key in opts) {
if (opts.hasOwnProperty(key) && opts[key] !== undefined) {
// skip default values
if (optNamesArr.indexOf(key) >= 0 && opts[key] === defaults[key]) continue;
search += `${key}=${opts[key].toString().replace(/^#+/, "")}&`;
}
}
search = search.replace(/&$/, "");
return path + (search ? `?${search}` : "");
}
function resetFps(){
if (started && forcePause && prevFps !== "Paused") {
$fps.textContent = prevFps = "Paused";
} else if (!forcePause && started && prevFps !== " — fps") {
$fps.textContent = prevFps = " — fps";
}
}
function updateLocationUrl(fresh=false){
let url = makeFullOptionsPath();
$currLink.setAttribute("href", url);
if (fresh && (document.location.search||"").length <= 1) return;
window.history.replaceState(fullPath, document.title || Q("title").textContent, url);
}
function incrementColorState(i, reverse){
currColor[i] = reverse ? currColor[i] - 1 : currColor[i] + 1;
if (currColor[i] >= colorMap.length) {
currColor[i] = 0;
} else if (currColor[i] < 0) {
currColor[i] = colorMap.length-1;
}
opts[`c${i+1}`] = colorMap[currColor[i]].toLowerCase();
updateLocationUrl();
}
function changeGridColors(n, rev=false){
forcePause = true;
if (isBusy) return;
handleIfBusy();
incrementColorState(n, rev);
setCanvasGrid();
$clrCtrlElements[n].style.backgroundColor = selectColor(n);
$body.style.backgroundColor = selectColor(2);
}
function setViewport(){
calcLayout();
setCanvasGrid();
for (let i = 0; i < $clrCtrlElements.length; i += 1) {
$clrCtrlElements[i].style.backgroundColor = selectColor(i);
$clrCtrlElements[i].style.backgroundColor = selectColor(i);
$clrCtrlElements[i].style.backgroundColor = selectColor(i);
}
}
function handleGridSizes(e={}){
let id = e.target && e.target.id;
if (typeof(e.preventDefault) === "function") e.preventDefault();
if (typeof(e.stopPropagation) === "function") e.stopPropagation();
if (!id || isBusy) return;
handleIfBusy();
if (id === "bigger-grid") {
opts.g -= 1;
} else if (id === "smaller-grid") {
opts.g += 1;
}
normalizeGridSize();
setViewport();
updateLocationUrl();
forcePause = true;
}
function movePixel(dir){
// forward movement
cachedCanvasImages.pos += dir;
if (cachedCanvasImages.pos >= gridSize || dir === 0) cachedCanvasImages.pos = 0;
// backwards movement
if (cachedCanvasImages.pos < 0) cachedCanvasImages.pos = gridSize - 1;
canvasCtx.putImageData(cachedCanvasImages.img[cachedCanvasImages.pos], 0, 0);
// set internal position in array
}
function animate(ms){
// Speed! All thse operations should be fast conditional ops only
skipped += 1;
if (skipped > opts.sk) skipped = 0;
if (ms === 0 || skipped < opts.sk) {
animationId = window.requestAnimationFrame(animate);
return;
}
if (startMs === 0) startMs = ms + 0;
if (!forcePause) {
movePixel(1);
// increment moves for fps counter here
moves += 1;
}
cms = ms + 0;
cstart = startMs + 0;
animationId = window.requestAnimationFrame(animate);
}
function refreshHud(){
/* Update Hud */
let doPaint = true,
m = moves + 0, // cache so get an accurate result
ms = cms + 0, // cache ...
startMs = cstart + 0, // cache ...
f
;
if (!started) return;
// paused?
if (forcePause) {
if (opts.avg) {
fpsTimes = [...Array(fpsAvgPoints)].map( () => 0);
fpsTimesFirstRun = true;
fpsTimesPlacer = 0;
}
return resetFps();
}
// reduce by starting value (see requestAnimationFrame)
cms -= cstart;
ms -= startMs;
// calc this current fps
f = m/((ms-fpsWaitMs)/1000);
// save for later use
if (opts.avg) fpsTimes[fpsTimesPlacer] = f;
// what we show on screen
if (f === 0 || f >= 500 || ms === 0) {
f = " — fps";
} else {
// calculate average fps of the last N (fpsAvgPoints) seconds
f = !opts.avg || fpsTimesFirstRun ? f : reduce(fpsTimes) / fpsAvgPoints;
f = f.toFixed(opts.dec);
f = `${f} fps`;
if (f === prevFps) doPaint = false;
prevFps = f + "";
}
// reset moves to avoid using average times
moves = 0;
fpsWaitMs = ms + 0;
// increment placer
if (opts.avg) {
fpsTimesPlacer = fpsTimesPlacer >= (fpsAvgPoints-1) ? 0 : fpsTimesPlacer + 1;
if (fpsTimesFirstRun && fpsTimesPlacer === 0) fpsTimesFirstRun = false;
}
// paint fps to screen
if (doPaint) $fps.textContent = f;
}
function resetPage(){
if (!started) return;
started = false;
window.cancelAnimationFrame(animationId);
window.clearInterval(refreshHudInt);
$fps.textContent = "Stopped";
cstart = 0;
cms = 0;
if (opts.avg) {
fpsTimes = [...Array(fpsAvgPoints)].map( () => 0);
fpsTimesPlacer = 0;
fpsTimesFirstRun = true;
}
moves = 0;
startMs = 0;
forcePause = false;
movePixel(0);
$body.removeAttribute("data-started");
// scroll text into view
$instructions.scrollTop = 0;
}
function init(){
started = true;
refreshHudInt = window.setInterval(refreshHud, opts.u);
$body.setAttribute("data-started", "true");
$body.removeAttribute("data-paused");
resetFps();
animate(0);
}
function togglePause(e={}){
if (typeof(e.preventDefault) === "function") e.preventDefault();
if (typeof(e.stopPropagation) === "function") e.stopPropagation();
forcePause = !forcePause;
if (!started) {// play icon on pageload was used to start it
forcePause = false;
return init();
}
if (forcePause) {
$body.setAttribute("data-paused", "true");
} else {
$body.removeAttribute("data-paused");
}
resetFps();
}
function toggleBorders(e={}){
if (typeof(e.preventDefault) === "function") e.preventDefault();
if (typeof(e.stopPropagation) === "function") e.stopPropagation();
if (isBusy) return;
handleIfBusy();
if (opts.b === 0) {
// show borders
$body.setAttribute("data-has-borders", "true");
opts.b = defaults.b + 0;
} else if (opts.b !== 0) {
// hide borders - only if usr hasn't specified b=0 already
$body.removeAttribute("data-has-borders");
// save prev value in defaults
defaults.b = opts.b + 0;
// hide
opts.b = 0;
}
normalizeBorderSize();
normalizeGridSize();
setViewport();
}
// listen for events
document.addEventListener("click", (e) => {
if (!e || !e.target || !e.target.getAttribute("data-clickable")) return;
e.preventDefault();
e.stopPropagation();
if (started) {
togglePause();
} else {
if (opts.fs && !document.fullscreenElement) {
document.documentElement.requestFullscreen();
}
init();
}
});
[ ...$pauseToggles ].forEach( (elem) => {
elem.addEventListener("click", togglePause);
});
$escToMenu.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
if (isBusy) return;
resetPage();
});
$resetAll.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
window.history.replaceState(fullPath, document.title || Q("title").textContent, fullPath);
document.location.reload();
});
[ ...$gridBtns ].forEach( (elem) => {
elem.addEventListener("click", handleGridSizes);
});
[ ...$toggleBorder ].forEach( (elem) => {
elem.addEventListener("click", toggleBorders);
});
[ ...$clrBtns ].forEach( (elem) => {
elem.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
if (isBusy) return;
switch(elem.id.replace("choose-clr-", "")){
case "pixel":
changeGridColors(0); break;
case "border":
changeGridColors(1); break;
case "bg":
changeGridColors(2); break;
}
});
});
[ ...$speedLimits ].forEach( (limiter) => {
limiter.addEventListener("click", (e) => {
let i;
e.preventDefault();
e.stopPropagation();
i = +limiter.id.replace("skip-frame-", "");
if (opts.sk === i) {
// same as already set, toggle back to normal
opts.sk = 0;
e.target.removeAttribute("data-active");
} else {
opts.sk = i;
[ ...$speedLimits ].forEach( (elem) => {
elem.removeAttribute("data-active");
});
e.target.setAttribute("data-active", "true");
}
skipped = 0;
if (opts.avg) {
fpsTimes = [...Array(fpsAvgPoints)].map( () => 0);
fpsTimesFirstRun = true;
fpsTimesPlacer = 0;
}
updateLocationUrl();
});
});
document.addEventListener("keydown", (e) => {
if (e && (e.ctrlKey || e.altKey || e.metaKey)) return;
if (started && forcePause) {
if (e.keyCode === 190 || e.key === "." || e.code === "Period") {
movePixel(1);
} else if (e.keyCode === 188 || e.key === "," || e.code === "Comma") {
movePixel(-1);
}
}
});
document.addEventListener("keyup", (e) => {
if (e && (e.ctrlKey || e.altKey || e.metaKey)) return;
if (!started && e.keyCode === 32) { // 'space' - fresh start
if (opts.fs && !document.fullscreenElement) {
document.documentElement.requestFullscreen();
}
init();
} else if (
e.keyCode === 109 ||
e.keyCode === 173 ||
e.key === "-" ||
e.code === "NumpadSubtract") {
// '-'
handleGridSizes({ target: $smallerGrid });
} else if (
e.keyCode === 61 ||
e.keyCode === 187 ||
e.key === "+" ||
e.code === "NumpadAdd") {
// '+'
handleGridSizes({ target: $biggerGrid });
} else if (started && e.keyCode === 32) { // 'space' - toggle pause
togglePause();
} else if (e.keyCode === 27) { // 'Escape'
if (!isBusy) resetPage();
} else if (e.keyCode === 49 || e.keyCode === 35 || e.key === "1" || e.code === "Digit1" || e.code === "Numpad1") {
changeGridColors(0, e.shiftKey);
} else if (e.keyCode === 50 || e.key === "2" || e.code === "Digit2" || e.code === "Numpad2") {
changeGridColors(1, e.shiftKey);
} else if (e.keyCode === 51 || e.keyCode === 34 || e.key === "3" || e.code === "Digit3" || e.code === "Numpad3") {
changeGridColors(2, e.shiftKey);
// } else if (e.keyCode === 48 || e.keyCode === 45 || e.key === "0"
// || e.code === "Digit0" || e.code === "Numpad0") {
// //
}
});
$fps.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
});
window.addEventListener("resize", debounce(setViewport, 500));
window.addEventListener("fullscreenchange", debounce(setViewport, 600));
window.addEventListener("orientationchange", debounce(setViewport, 400));
window.addEventListener("resize", handleIfBusy);
window.addEventListener("fullscreenchange", handleIfBusy);
window.addEventListener("orientationchange", handleIfBusy);
document.addEventListener("visibilitychange", () => {
if (typeof(document.hidden) === "boolean" && started && document.hidden) resetPage();
});
// parse user locationbar options
opts = parseUserOpts(opts);
// reflect cleaned up and Full options in url
updateLocationUrl(true);
// this prevents any w hite flashes on resize
$body.style.backgroundColor = selectColor(2);
// set toggle-border img state
if (opts.autohide) $body.setAttribute("data-autohide", "true");
if (opts.b > 0) $body.setAttribute("data-has-borders", "true");
$body.setAttribute("data-resizing", "true");
window.setTimeout( () => {
// setup viewer - also resets canvas element
setViewport();
if (!started && opts.autostart === true) {
// autostart
$instructions.scrollTo(0, 0);
$body.setAttribute("data-loaded", "true");
init();
} else {
// scroll text in up to show user is can be scrolled
$instructions.scrollTo(0, Math.floor($instructions.offsetHeight / 2));
$body.setAttribute("data-loaded", "true");
$instructions.scrollTo({ top: 0, left: 0, behavior: "smooth" });
}
}, 100);
}); }());
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment