http-server/hyper-build/projects/fishbowl.html

402 lines
29 KiB
HTML
Raw Normal View History

2024-04-28 17:11:48 -05:00
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"/><meta content="width=device-width, initial-scale=1" name="viewport"/><title> &#60loganGatlin/&#62 </title><link href="resources/favicon.svg" type="image/x-icon" rel="icon"/><style>
/*!
Pure v3.0.0
Copyright 2013 Yahoo!
Licensed under the BSD License.
https://github.com/pure-css/pure/blob/master/LICENSE
*/
/*!
normalize.css v | MIT License | https://necolas.github.io/normalize.css/
Copyright (c) Nicolas Gallagher and Jonathan Neal
*/
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}html{font-family:sans-serif}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{display:flex;flex-flow:row wrap;align-content:flex-start}.pure-u{display:inline-block;vertical-align:top}.pure-u-1,.pure-u-1-1,.pure-u-1-12,.pure-u-1-2,.pure-u-1-24,.pure-u-1-3,.pure-u-1-4,.pure-u-1-5,.pure-u-1-6,.pure-u-1-8,.pure-u-10-24,.pure-u-11-12,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-2-24,.pure-u-2-3,.pure-u-2-5,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24,.pure-u-3-24,.pure-u-3-4,.pure-u-3-5,.pure-u-3-8,.pure-u-4-24,.pure-u-4-5,.pure-u-5-12,.pure-u-5-24,.pure-u-5-5,.pure-u-5-6,.pure-u-5-8,.pure-u-6-24,.pure-u-7-12,.pure-u-7-24,.pure-u-7-8,.pure-u-8-24,.pure-u-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%}.pure-u-1-12,.pure-u-2-24{width:8.3333%}.pure-u-1-8,.pure-u-3-24{width:12.5%}.pure-u-1-6,.pure-u-4-24{width:16.6667%}.pure-u-1-5{width:20%}.pure-u-5-24{width:20.8333%}.pure-u-1-4,.pure-u-6-24{width:25%}.pure-u-7-24{width:29.1667%}.pure-u-1-3,.pure-u-8-24{width:33.3333%}.pure-u-3-8,.pure-u-9-24{width:37.5%}.pure-u-2-5{width:40%}.pure-u-10-24,.pure-u-5-12{width:41.6667%}.pure-u-11-24{width:45.8333%}.pure-u-1-2,.pure-u-12-24{width:50%}.pure-u-13-24{width:54.1667%}.pure-u-14-24,.pure-u-7-12{width:58.3333%}.pure-u-3-5{width:60%}.pure-u-15-24,.pure-u-5-8{width:62.5%}.pure-u-16-24,.pure-u-2-3{width:66.6667%}.pure-u-17-24{width:70.8333%}.pure-u-18-24,.pure-u-3-4{width:75%}.pure-u-19-24{width:79.1667%}.pure-u-4-5{width:80%}.pure-u-20-24,.pure-u-5-6{width:83.3333%}.pure-u-21-24,.pure-u-7-8{width:87.5%}.pure-u-11-12,.pure-u-22-24{width:91.6667%}.pure-u-23-24{width:95.8333%}.pure-u-1,.pure-u-1-1,.pure-u-24-24,.pure-u-5-5{width:100%}.pure-button{display:inline-block;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;user-select:none;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-group{letter-spacing:-.31em;text-rendering:optimizespeed}.opera-only :-o-
</style><style>
@font-face {
font-family: Cascadia;
src: url("/resources/CascadiaMonoPL.woff2");
}
body {
color: #444;
font-size: 1.2em;
}
.link {
text-decoration: none;
color: #1e90ff;
}
.link :visited {
color: #1e90ff;
}
table {
display: block;
margin-top: 1em;
margin-bottom: 1em;
margin-left: auto;
margin-right: auto;
width: 60%;
text-align: left;
border-collapse: collapse;
}
th {
padding: 0.25em 0.25em 0.25em 0.25em;
border: 2px solid;
border-collapse: collapse;
}
td {
padding: 0.25em 0.25em 0.25em 0.25em;
border: 2px solid;
border-collapse: collapse;
}
/* https://css-loaders.com/spinner/ */
/* HTML: <div class="loader"></div> */
.loading {
width: 512px;
padding: 8px;
aspect-ratio: 1;
border-radius: 50%;
background: #25b09b;
--_m:
conic-gradient(#0000 10%,#000),
linear-gradient(#000 0 0) content-box;
-webkit-mask: var(--_m);
mask: var(--_m);
-webkit-mask-composite: source-out;
mask-composite: subtract;
animation: l3 1s infinite linear;
}
@keyframes l3 {to{transform: rotate(1turn)}}
.centered {
display: block;
margin-top: 1em;
margin-bottom: 1em;
margin-left: auto;
margin-right: auto;
width: 50%;
}
.pure-img-responsive { max-width: 100%; height: auto;
}
.cascadia {
font-family: Cascadia;
}
.home-menu {
padding: 0.5em;
text-align: center;
box-shadow: 0 1px 1px rgba(0,0,0, 0.10);
}
.home-menu {
background: #333;
}
.home-menu a {
color: #ccc;
}
.home-menu li a:hover,
.home-menu li a:focus {
animation: menu-hover-anim 0.25s ease-out;
animation-fill-mode: forwards;
background: none;
border: none;
}
.link {
color: #00f;
text-decoration: none;
}
.video {
text-align: center;
}
.video iframe {
width: 32em;
height: 18em;
frameborder: "0";
}
/*
Add transition to containers so they can push in and out.
*/
#layout,
#menu,
.menu-link {
-webkit-transition: all 0.2s ease-out;
-moz-transition: all 0.2s ease-out;
-ms-transition: all 0.2s ease-out;
-o-transition: all 0.2s ease-out;
transition: all 0.2s ease-out;
}
/*
This is the parent `<div>` that contains the menu and the content area.
*/
#layout {
position: relative;
left: 0;
padding-left: 0;
}
#layout.active #menu {
left: 150px;
width: 150px;
}
#layout.active .menu-link {
left: 150px;
}
/*
The content `<div>` is where all your content goes.
*/
.content {
margin: 0 auto;
padding: 0 2em;
max-width: 800px;
margin-bottom: 50px;
line-height: 1.6em;
text-align: justify;
}
.header {
margin: 0;
color: #444;
text-align: center;
padding: 2.5em 4em 0;
border-bottom: 1px solid #eee;
}
.header h1 {
margin: 0.2em 0;
font-size: 2.5em;
font-weight: bold;
}
.header h2 {
font-weight: 300;
color: #888;
padding: 0;
margin-top: 0;
}
.distinct {
background-color: #444;
color: #fff;
padding: 0.5em 0.5em 0.5em 0.5em;
}
@keyframes card-hover-anim {
to {background-color: #ccc;}
}
@keyframes card-text-hover-anim {
to {color: #222;}
}
.spaced {
padding: 0.5em 0.5em 0.5em 0.5em;
margin: 1em 0em 1em 0em;
}
h1 a svg {
vertical-align: middle;
}
.card {
background-color: #eee;
text-decoration: none;
text-align: left;
}
.card h1 {
color: #444;
}
.card h2 {
color: #777;
}
.card h3 {
color: #999;
}
.card a {
text-decoration: none;
display: block;
}
.card:hover {
animation: card-hover-anim 0.25s ease-out;
animation-fill-mode: forwards;
}
/* .card:hover h3 { */
/* animation: card-text-hover-anim 0.25s ease-out; */
/* animation-fill-mode: forwards; */
/* } */
.card img {
vertical-align: bottom;
float: right;
padding: 0em 0.1em 0em 0.1em;
width: 60px;
}
.card svg {
vertical-align: bottom;
float: right;
padding: 0em 0.1em 0em 0.1em;
width: 3em;
height: auto;
}
.content-subhead {
margin: 50px 0 20px 0;
font-weight: 300;
color: #444;
}
code {
font-family: Cascadia;
}
.code-block {
/* background-color: #fdf6e3; */
background-color: #eee;
color: #002b36;
font-family: Cascadia;
font-size: 0.85em;
padding: 0em 0.5em 0em 0.5em;
margin: 1em 0em 1em 0em;
page-break-inside: avoid;
display: block;
overflow: auto;
word-wrap: break-word;
max-width: 100%;
}
.code-block pre {
display: block;
margin: 0 0 0 0;
padding: 0 0 0 0;
}
.code-block pre code {
font-family: Cascadia;
display: block;
white-space: pre-wrap;
}
.code-block span {
/* background-color: #fdf6e3; */
background-color: #eee;
}
.keyword {
color: #6c71c4;
}
.constant {
color: #cb4b16;
}
.function {
color: #268bd2;
}
.operator {
color: #6c71c4;
}
.punctuation {
color: #586e75;
}
.string {
color: #859900;
}
.type {
color: #2aa198;
}
.property {
color: #586e75;
}
</style></head><body><div class="pure-g"><div class="pure-u-1-1"><div class="header"><h1> Fishbowl <a target="_blank" href="https://github.com/Xterminate1818/fishbowl"><svg viewBox="0 0 128 128" style="width: 1em; padding-bottom: 0.2em;"><title> View source on GitHub </title><g fill="#181616"><path clip-rule="evenodd" d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z" fill-rule="evenodd"></path><path d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"></path></g></svg></a></h1><h2> Vector Math - Standard Library </h2></div><div class="content"><h2 class="distinct"> Gallery </h2><div class="pure-g"><div class="pure-u-1-3"><img title="Mona Lisa - Leonardo da Vinci" src="/resources/mona_lisa.gif" class="pure-img"/></div><div class="pure-u-1-3"><img class="pure-img" src="/resources/wanderer.gif" title="Wanderer above the Sea of Fog - Caspar David Friederich"/></div><div class="pure-u-1-3"><img src="/resources/starry_night.gif" title="The Starry Night - Vincent van Gogh" class="pure-img"/></div></div><h2 class="distinct"> Motivation </h2>
Physics solvers tend to be very complex software. In the
past, I toyed around with physics solvers for different kinds of geometries, but
struggled to implement these algorithms in a way that was both convincing
and performant. The idea for Fishbowl came from a video by the creator
Pezzza. They describe the high-level implementation details of a Verlet
Integration solver. This approach caught my interest.
<div class="video"><iframe title="YouTube video player" src="https://www.youtube-nocookie.com/embed/lS_qeBy3aQI?si=gLrHCWO-XUSP0T0j" frameborder="0" allowfullscreen allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe></div>
Pezza's work is very impressive, particularly the demonstration at the end
which proves the program is deterministic. However, the focus of Pezzza's
project was to create a performant real-time application. I was instead
interested in pre-rendered images and animations. I decided to expand the
one-off demonstration at the end of Pezzza's video into a fully fledged
app.
<h2 class="distinct"> Approach </h2>
At the center of any physics solver is the physics 'step', a fixed interval of
time where objects move and collide. The physics step of Fishbowl consists of five
distinct phases.
<div class="code-block">
<pre><code>
<span class="comment">&#x2f;&#x2f; Keep all objects inside the simulation bounds</span>
<span class="variable builtin">self</span><span class="punctuation delimiter">.</span><span class="function">constrain_rect</span><span class="punctuation bracket">(</span><span class="punctuation bracket">)</span><span class="punctuation delimiter">;</span>
<span class="comment">&#x2f;&#x2f; Sort objects from left-most to right-most.</span>
<span class="variable builtin">self</span><span class="punctuation delimiter">.</span><span class="function">sort</span><span class="punctuation bracket">(</span><span class="punctuation bracket">)</span><span class="punctuation delimiter">;</span>
<span class="comment">&#x2f;&#x2f; Apply collision impulses</span>
<span class="variable builtin">self</span><span class="punctuation delimiter">.</span><span class="function">collide</span><span class="punctuation bracket">(</span><span class="punctuation bracket">)</span><span class="punctuation delimiter">;</span>
<span class="comment">&#x2f;&#x2f; Calculate new velocities and accelerations</span>
<span class="variable builtin">self</span><span class="punctuation delimiter">.</span><span class="function">integrate</span><span class="punctuation bracket">(</span><span class="punctuation bracket">)</span><span class="punctuation delimiter">;</span>
<span class="comment">&#x2f;&#x2f; Finally, advance the simulation clock</span>
<span class="variable builtin">self</span><span class="punctuation delimiter">.</span><span class="variable other member">clock</span> <span class="operator">+=</span> <span class="constant numeric integer">1</span><span class="punctuation delimiter">;</span>
</code></pre>
</div>
Because the program doesn't run in real time, simulating over one second
may take more or less than a second. Calculating a larger number of shorter
time steps will make the simulation more accurate, but will take longer to
process. Fishbowl uses an number of optimizations to reduce processing time.
By keeping the array of objects sorted from left to right, the number of collision
checks is significantly reduced. The rendering system is decoupled from the simulation,
and the two are pipelined together and run in parallel.
<h2 class="distinct"> Rendering </h2>
The method I used for drawing circles to a frame went through numerous iterations.
Rendering performance is a major bottleneck, and so it was important to do so in
parallel. After writing and benchmarking several software renderers, I landed on
WebGPU. While quite bloated, WebGPU provides a fast, low level, and cross platform
rendering API designed for async programs. This allows Fishbowl to easily integrate
with both my server, which is entirely async, and the simulation, which is mostly
synchronous. While I would have liked to use a bespoke solution, GPU accelerated
rendering is far too quick to realistically consider any other option.
<h2 class="distinct"> Try It Out </h2>
You can try out Fishbowl below, or download the CLI tool from my GitHub.
It works best on images that are roughly square and don't contain any text.
Keep in mind the web version below will be slower and generate lower quality
results than the native application, as it is being run on my home server with
limited resources.
<br/><br/><img class="pure-img centered" width="512" height="512" id="result_image" hidden/><form class="pure-form-stacked" style="justify-content: center;" enctype="multipart/form-data" method="post" id="fishForm"><fieldset><label for="file"> Select an image </label><input accept="image/bmp,image/gif,image/vnd.microsoft.icon,image/jpeg,image/png,image/tiff,image/webp" id="file" type="file" name="image" required/><br/><input type="submit" class="pure-button pure-button-primary" value="Run Fishbowl"/></fieldset></form><!----><script>
const form = document.getElementById("fishForm");
const image = document.getElementById("result_image");
form.onsubmit = async (ev) => {
ev.preventDefault();
image.src = "";
image.hidden = false;
image.classList.add("loading");
const form = ev.currentTarget;
const action = form.action;
try {
const form_data = new FormData(form);
const response = await fetch(action, {
method: 'POST',
body: form_data,
});
const blob = await response.blob();
image.src = URL.createObjectURL(blob);
image.classList.remove("loading");
} catch (error) {
console.log(error);
}
}
</script></div></div></div></body></html>