Created
May 24, 2022 05:53
-
-
Save elundmark/158a652ab1dedf98efc87c5f33ce6b91 to your computer and use it in GitHub Desktop.
Test Monitor FPS in Browser
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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∕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&g=2&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 — 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">⅙</span> | |
</p> | |
<p class="btn hl"> | |
<span class="speed" id="skip-frame-4">⅕</span> | |
</p> | |
<p class="btn hl"> | |
<span class="speed" id="skip-frame-3">¼</span> | |
</p> | |
<p class="btn hl"> | |
<span class="speed" id="skip-frame-2">⅓</span> | |
</p> | |
<p class="btn hl"> | |
<span class="speed" id="skip-frame-1">½</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 — 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