Initial Commit
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/target
|
||||||
|
lgatlin.dev.cert
|
||||||
|
lgatlin.dev.priv
|
||||||
|
/logs
|
56
404.html
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link type="image/x-icon" href="resources/favicon.svg" rel="icon" />
|
||||||
|
<link rel="stylesheet" href="/styles/main.css" />
|
||||||
|
<link href="/styles/extend.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="pure-g">
|
||||||
|
<div class="home-menu pure-menu pure-menu-horizontal">
|
||||||
|
<ul class="pure-menu-list">
|
||||||
|
<li class="pure-menu-item">
|
||||||
|
<a href="/home" class="pure-menu-link">
|
||||||
|
Home
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="pure-menu-item">
|
||||||
|
<a href="/projects" class="pure-menu-link">
|
||||||
|
Projects
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="pure-menu-item">
|
||||||
|
<a class="pure-menu-link" href="/blog">
|
||||||
|
Blog
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="pure-menu-item">
|
||||||
|
<a class="pure-menu-link" href="/about">
|
||||||
|
About
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1-1">
|
||||||
|
<div class="header">
|
||||||
|
<h1>
|
||||||
|
Page does not exist
|
||||||
|
</h1>
|
||||||
|
<h2>
|
||||||
|
Error 404
|
||||||
|
</h2>
|
||||||
|
<a class="link" href="/home">
|
||||||
|
<button class="pure-button pure-button-primary">
|
||||||
|
Return Home
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
2462
Cargo.lock
generated
Normal file
15
Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
name = "server"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tiny_http = {version="0.12", features=["ssl-openssl"]}
|
||||||
|
log4rs = "1.3"
|
||||||
|
log = "0.4"
|
||||||
|
tokio = {version = "1", features = ["full"]}
|
||||||
|
ascii = "1.1"
|
||||||
|
fishbowl = {path="../fishbowl"}
|
||||||
|
|
3
hyper-build/404.html
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<div class="header"><h1> Page does not exist </h1><h2> Error 404 </h2></div><a href="/home" class="link"><button class="pure-button pure-button-primary button-xlarge">
|
||||||
|
Return Home
|
||||||
|
</button></a>
|
358
hyper-build/home.html
Normal file
402
hyper-build/projects/fishbowl.html
Normal file
394
hyper-build/projects/forte.html
Normal file
415
hyper-build/projects/fractal-explorer.html
Normal file
432
hyper-build/projects/html-templating.html
Normal file
380
hyper-build/projects/http-server.html
Normal file
324
hyper-build/projects/math-interpreter.html
Normal file
390
hyper-build/projects/nd-range.html
Normal file
324
hyper-build/projects/pokedex.html
Normal file
324
hyper-build/projects/stocktrading.html
Normal file
1
hyper-src/404.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<NotFound/>
|
108
hyper-src/home.html
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
<Container>
|
||||||
|
<div class="header">
|
||||||
|
<h1 id="title" />
|
||||||
|
<h2>
|
||||||
|
<a class="link" href="/resume.pdf"> My Resume </a> -
|
||||||
|
<a class="link" href="http://github.com/Xterminate1818"> GitHub </a> -
|
||||||
|
<a class="link" href="mailto:logan@gatlintc.com"> Email Me </a>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
var i = 0;
|
||||||
|
var txt = 'Welcome to my lgatlin.dev';
|
||||||
|
var speed = 30;
|
||||||
|
|
||||||
|
function typeWriter() {
|
||||||
|
if (i < txt.length) {
|
||||||
|
document.getElementById("title").innerHTML += txt.charAt(i);
|
||||||
|
i++;
|
||||||
|
setTimeout(typeWriter, speed + Math.random() * 25.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
typeWriter()
|
||||||
|
</script>
|
||||||
|
<div class="content">
|
||||||
|
<h2 class="distinct"> Dev Posts </h2>
|
||||||
|
|
||||||
|
<Card href="/projects/http-server">
|
||||||
|
<RustIcon />
|
||||||
|
<CloudflareIcon />
|
||||||
|
<h1> Web Server </h1>
|
||||||
|
<h3> Back End - TCP - SSL </h3>
|
||||||
|
<h2>
|
||||||
|
Currently serving you this website!
|
||||||
|
</h2>
|
||||||
|
</Card>
|
||||||
|
<Card href="/projects/html-templating">
|
||||||
|
<RustIcon />
|
||||||
|
<HtmlIcon />
|
||||||
|
<h1> HTML Templating Engine </h1>
|
||||||
|
<h3> Front End - Parser Design </h3>
|
||||||
|
<h2>
|
||||||
|
Used to generate this page!
|
||||||
|
</h2>
|
||||||
|
</Card>
|
||||||
|
<Card href="/projects/forte">
|
||||||
|
<RustIcon />
|
||||||
|
<WasmIcon />
|
||||||
|
<h1> Forte Assembly Language </h1>
|
||||||
|
<h3> Programming Language - Hackathon </h3>
|
||||||
|
<h2>
|
||||||
|
Radically different machine code. A creative-coding endeavor
|
||||||
|
</h2>
|
||||||
|
</Card>
|
||||||
|
<Card href="/projects/fishbowl">
|
||||||
|
<RustIcon />
|
||||||
|
<WgpuIcon />
|
||||||
|
<h1> Fishbowl </h1>
|
||||||
|
<h3> Image Encoding - Hardware Rendering </h3>
|
||||||
|
<h2> Kinematic image processing with GPU acceleration </h2>
|
||||||
|
</Card>
|
||||||
|
<Card href="/projects/math-interpreter">
|
||||||
|
<RustIcon />
|
||||||
|
<WasmIcon />
|
||||||
|
<h1> Math Interpreter </h1>
|
||||||
|
<h3> Parser Design </h3>
|
||||||
|
<h2>
|
||||||
|
Interpret and evaluate plain-text math expressions
|
||||||
|
</h2>
|
||||||
|
</Card>
|
||||||
|
<Card href="/projects/nd-range">
|
||||||
|
<RustIcon />
|
||||||
|
<h1> nd-range </h1>
|
||||||
|
<h3> Vector Math - Standard Library </h3>
|
||||||
|
<h2>
|
||||||
|
An extension of Rust's 'Range' type
|
||||||
|
using the Cartesian Product Algorithm
|
||||||
|
</h2>
|
||||||
|
</Card>
|
||||||
|
<Card href="/projects/fractal-explorer">
|
||||||
|
<RustIcon />
|
||||||
|
<RaylibIcon />
|
||||||
|
<h1> Fractal Explorer </h1>
|
||||||
|
<h3> Parallel Algorithms - Optimization - Hackathon </h3>
|
||||||
|
<h2>
|
||||||
|
A Mandelbrot Fractal viewer using CPU parallelism
|
||||||
|
and the derivative bail algorithm
|
||||||
|
</h2>
|
||||||
|
</Card>
|
||||||
|
<Card href="/projects/pokedex">
|
||||||
|
<PythonIcon />
|
||||||
|
<h1> Pokédex </h1>
|
||||||
|
<h3> TKinter - Web APIs - Native UI </h3>
|
||||||
|
<h2>
|
||||||
|
A TKinter app for viewing the original Pokédex, with
|
||||||
|
stats scraped from online sources
|
||||||
|
</h2>
|
||||||
|
</Card>
|
||||||
|
<Card href="/projects/stock-trading">
|
||||||
|
<PythonIcon />
|
||||||
|
<h1> Stock Trading A.I. </h1>
|
||||||
|
<h3> Command Line App - Web APIs </h3>
|
||||||
|
<h2>
|
||||||
|
A simple heuristic trading algorithm
|
||||||
|
</h2>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</Container>
|
115
hyper-src/projects/fishbowl.html
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
<Container>
|
||||||
|
<div class="header">
|
||||||
|
<h1> Fishbowl <GithubIcon href="https://github.com/Xterminate1818/fishbowl"/> </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 class="pure-img" src="/resources/mona_lisa.gif" title="Mona Lisa - Leonardo da Vinci" />
|
||||||
|
</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 class="pure-img" src="/resources/starry_night.gif" title="The Starry Night - Vincent van Gogh" />
|
||||||
|
</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 src="https://www.youtube-nocookie.com/embed/lS_qeBy3aQI?si=gLrHCWO-XUSP0T0j" title="YouTube video player"
|
||||||
|
frameborder="0"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||||
|
referrerpolicy="strict-origin-when-cross-origin" allowfullscreen>
|
||||||
|
</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.
|
||||||
|
<@code lang="rust">
|
||||||
|
// Keep all objects inside the simulation bounds
|
||||||
|
self.constrain_rect();
|
||||||
|
// Sort objects from left-most to right-most.
|
||||||
|
self.sort();
|
||||||
|
// Apply collision impulses
|
||||||
|
self.collide();
|
||||||
|
// Calculate new velocities and accelerations
|
||||||
|
self.integrate();
|
||||||
|
// Finally, advance the simulation clock
|
||||||
|
self.clock += 1;
|
||||||
|
</@code>
|
||||||
|
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;" method="post" enctype="multipart/form-data" id="fishForm">
|
||||||
|
<fieldset>
|
||||||
|
<label for="file"> Select an image </label>
|
||||||
|
<input id="file" type="file" name="image" required
|
||||||
|
accept="image/bmp,image/gif,image/vnd.microsoft.icon,image/jpeg,image/png,image/tiff,image/webp"/>
|
||||||
|
<br/>
|
||||||
|
<input class="pure-button pure-button-primary" type="submit" value="Run Fishbowl"/>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
<!-- Handle displaying image -->
|
||||||
|
<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>
|
||||||
|
</Container>
|
203
hyper-src/projects/forte.html
Normal file
|
@ -0,0 +1,203 @@
|
||||||
|
<Container>
|
||||||
|
<div class="header">
|
||||||
|
<h1> Forte <GithubIcon href="https://github.com/Xterminate1818/forte"/> </h1>
|
||||||
|
<h2> Programming Language - Hackathon </h2>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h2 class="distinct"> Motivation </h2>
|
||||||
|
In February of 2024, I competed at RowdyHacks during the 9th
|
||||||
|
annual Hackathon. This was my second time attending, having
|
||||||
|
<a class="link" href="/projects/fractal-viewer">
|
||||||
|
placed second the previous year
|
||||||
|
</a>.
|
||||||
|
My understanding of programming and of Rust had grown significantly over
|
||||||
|
that time, and I wanted to one-up myself. I had just finished my Computer
|
||||||
|
Organization class, where we learned to program in x86 assembly. The class
|
||||||
|
made me realize that the design of an assembly language has a profound
|
||||||
|
impact on every higher level language built on-top of it. As a Rust
|
||||||
|
programmer who cares a great deal about safety and soundness guarantees, it
|
||||||
|
troubles me that I have to compile to an inherently unsafe assembly
|
||||||
|
language. I wanted to find out how feasible it would be to write a new
|
||||||
|
assembly language designed for easy static analysis and safety guarantees.
|
||||||
|
I am very satisfied with the result, which I named Forte.
|
||||||
|
<h2 class="distinct"> Specifications </h2>
|
||||||
|
<h3> Instructions </h3>
|
||||||
|
Forte is an assembly language and bytecode for a hypothetical 128-bit
|
||||||
|
processor. It contains 26 instructions, but no directly exposed registers.
|
||||||
|
<div class="pure-g">
|
||||||
|
<table class="striped pure-u-1-2">
|
||||||
|
<thead>
|
||||||
|
<th> Name </th>
|
||||||
|
<th> Mnemonic </th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td> Push </td>
|
||||||
|
<td> push </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> Pop </td> <td> pop </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> Duplicate </td>
|
||||||
|
<td> dup </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> Add </td>
|
||||||
|
<td> add </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> Difference </td>
|
||||||
|
<td> diff </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> Multiply </td>
|
||||||
|
<td> mul </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> Divide </td>
|
||||||
|
<td> div </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> Remainder </td>
|
||||||
|
<td> rem </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> Bitwise And </td>
|
||||||
|
<td> and </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> Bitwise Or </td>
|
||||||
|
<td> or </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> Bitwise Xor </td>
|
||||||
|
<td> xor </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> Shift Right </td>
|
||||||
|
<td> shr </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> Shift Left </td>
|
||||||
|
<td> shl </td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<table class="striped pure-u-1-2">
|
||||||
|
<thead>
|
||||||
|
<th> Name (cont.) </th>
|
||||||
|
<th> Mnemonic (cont.) </th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td> Branch Equal </td>
|
||||||
|
<td> beq </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> Branch Unequal </td>
|
||||||
|
<td> bne </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> Branch Greater </td>
|
||||||
|
<td> bgt </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> Branch Lesser </td>
|
||||||
|
<td> blt </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> Function Start </td>
|
||||||
|
<td> fun </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> Call </td>
|
||||||
|
<td> call </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> Return </td>
|
||||||
|
<td> ret </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> Loop </td>
|
||||||
|
<td> loop </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> Iterate Loop </td>
|
||||||
|
<td> iter </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> Begin Execution </td>
|
||||||
|
<td> exe </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> Store </td>
|
||||||
|
<td> sto </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> Load </td>
|
||||||
|
<td> lod </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> Stack Length </td>
|
||||||
|
<td> len </td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<h3> The Three Stacks </h3>
|
||||||
|
The stack is an essential part of any assembly language, however it is
|
||||||
|
often a point of vulnerability. Stack-smashing and stack-overflow
|
||||||
|
vulnerabilities can very easily allow attackers to overwright function
|
||||||
|
return addresses, and and execute arbitrary code. This is why Forte uses
|
||||||
|
three distinct stacks.
|
||||||
|
<h3> The Program Stack </h3>
|
||||||
|
The program stack, or p-stack for short, contains all of the state for the
|
||||||
|
user-space program. When you push or pop a value in Forte, you are
|
||||||
|
interacting with the p-stack. What's most interesting about the p-stack is
|
||||||
|
what it does not contain: pointers. There is no way to jump to an address
|
||||||
|
on the p-stack, as its contents are considered untrusted.
|
||||||
|
<h3> The Control Stack </h3>
|
||||||
|
In order to call functions, return addresses must be stored somewhere.
|
||||||
|
While most assembly languages place these on the stack alongside other
|
||||||
|
values, Forte segregates them into a separate control stack (c-stack for
|
||||||
|
short.) This is similar to the "shadow stack" option some compilers use,
|
||||||
|
except implemented at a hardware level. The only way to interact with the
|
||||||
|
c-stack is through call and return instructions.
|
||||||
|
<h3> The Function Stack </h3>
|
||||||
|
Most assembly languages have a "jump" instruction which moves the program
|
||||||
|
counter to an arbitrary point in memory. This is useful but not
|
||||||
|
particularly safe. Forte only allows jumping to valid functions. The
|
||||||
|
locations of these functions are kept in the function stack (f-stack.)
|
||||||
|
Functions are added to the f-stack after they are validated during the
|
||||||
|
warmup phase, which I will discuss next.
|
||||||
|
<h3> Warmup </h3>
|
||||||
|
When a Forte program begins, it does not immediately start executing
|
||||||
|
instructions. Instead, it validates the programs correctness and builds
|
||||||
|
the f-stack. Every <code>fun</code> (function start) instruction will
|
||||||
|
push the address of that instruction to the f-stack. Every instruction that
|
||||||
|
interacts with the stack will increment or decrement an internal register
|
||||||
|
accordingly. If this value value falls below zero or above the maximum
|
||||||
|
stack size, then the program is determined to be unsafe and execution
|
||||||
|
is cancelled before it begins. The warmup phase will take O(N) time,
|
||||||
|
where N is the number of instructions.
|
||||||
|
<h3> Recital </h3>
|
||||||
|
After the <code>exe</code> (begin execution) instruction is reached, the
|
||||||
|
Recital phase begins. The function pointer returns to the address at the
|
||||||
|
top of the f-stack, which was the last defined function. When the program
|
||||||
|
counter reaches the <code>exe</code> again, the program has terminated
|
||||||
|
successfully. This combination of design decisions means that the program
|
||||||
|
counter can never be lower than the first function address, or larger than
|
||||||
|
the execution instruction. This makes arbitrary code execution attacks more
|
||||||
|
difficult to pull off.
|
||||||
|
<h2 class="distinct"> Safety </h2>
|
||||||
|
Creating a memory safe assembly language presents much different challenges
|
||||||
|
from a compiled language like Rust. There are no compile-time checks or
|
||||||
|
guarantees, any string of bytes could be interpreted as a "program." The core
|
||||||
|
design principle of Forte is to make unsecure programs impossible (or at least
|
||||||
|
very difficult) to express, and to make static analysis simple. This is a delicate
|
||||||
|
balance to strike - limiting Forte's capabilities necessarily makes it more
|
||||||
|
cumbersome and less performant.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</Container>
|
97
hyper-src/projects/fractal-explorer.html
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
<Container>
|
||||||
|
<div class="header">
|
||||||
|
<h1> Fractal Explorer <GithubIcon href="https://github.com/Xterminate1818/rowdyhacks-2023"/> </h1>
|
||||||
|
<h2> Parallel Algorithms - Optimization - Hackathon Finalist </h2>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h2 class="distinct"> Motivation </h2>
|
||||||
|
In March of 2023 I attended RowdyHacks 8, my first hackathon. I was
|
||||||
|
in the middle of my Calculus II class, which I was enjoying a lot. With
|
||||||
|
Math at the front of my mind, I decided to write a program to visualize
|
||||||
|
the Mandelbrot Fractal. I had written a similar program in Snap, a visual
|
||||||
|
block-based programming language, so I was confident I could achieve results
|
||||||
|
within the 24 hour time limit. I wanted to specifically optimize the program
|
||||||
|
for speed without using GPU acceleration, because the laptop I was using did
|
||||||
|
not have one.
|
||||||
|
<h2 class="distinct"> Approach </h2>
|
||||||
|
The premise behind the Mandelbrot Fractal is to take a complex number, and
|
||||||
|
insert it into a recursive function. If the function does not diverge to
|
||||||
|
infinity, then that number is part of the Mandelbrot Set, and it gets plotted
|
||||||
|
on the screen.
|
||||||
|
Calculating the Mandelbrot Set is deceptively simple. The hard part is doing so
|
||||||
|
with a very large degree of precision, and repeating the calculations millions of
|
||||||
|
times (prefferably in parallel.)
|
||||||
|
<@code lang="rust">
|
||||||
|
// Snippet from my initial naive algorithm
|
||||||
|
//
|
||||||
|
// This will produce the value of one pixel on the screen
|
||||||
|
pub fn fractal(val: Complex, max_iterations: usize) -> usize {
|
||||||
|
let mut iterations = 0;
|
||||||
|
let mut last = Complex::new(0.0, 0.0);
|
||||||
|
let mut squared = Complex::new(0.0, 0.0);
|
||||||
|
// Values exceeding 4 usually diverge. If the number
|
||||||
|
// does not diverge after `max_iterations`, it probably
|
||||||
|
// never will.
|
||||||
|
while squared.re + squared.im <= 4.0 &&
|
||||||
|
iterations < max_iterations {
|
||||||
|
let im = 2.0 * last.re * last.im + val.im;
|
||||||
|
let re = squared.re - squared.im + val.re;
|
||||||
|
last = Complex { re, im };
|
||||||
|
squared.re = re.powi(2);
|
||||||
|
squared.im = im.powi(2);
|
||||||
|
iterations += 1;
|
||||||
|
}
|
||||||
|
iterations
|
||||||
|
}
|
||||||
|
</@code>
|
||||||
|
While researching the Mandelbrot set, I found a faster solution called the derivative
|
||||||
|
bail algorithm. This algorithm considers the first derivative of the function rather
|
||||||
|
than the functions value itself, which is more accurate and performant most of the time.
|
||||||
|
<@code lang="rust">
|
||||||
|
// Snippet from my derivative bail implementation
|
||||||
|
pub fn get_dbail(val: Complex, max_dvt: f64, max_iter: usize) -> usize {
|
||||||
|
let max_dvt = max_dvt.powi(2);
|
||||||
|
let mut it = 0;
|
||||||
|
let mut last = Complex::new(0.0, 0.0);
|
||||||
|
let mut squared = Complex::new(0.0, 0.0);
|
||||||
|
let mut deriv = Complex::new(0.0, 0.0);
|
||||||
|
while squared.re + squared.im <= 4.0
|
||||||
|
&& deriv.re.powi(2) + deriv.im.powi(2) <= max_dvt
|
||||||
|
&& it <= max_iter
|
||||||
|
{
|
||||||
|
deriv.re = 2.0 * (deriv.re * val.re - deriv.im * val.im);
|
||||||
|
deriv.im = 2.0 * (deriv.re * val.im + deriv.im * val.re);
|
||||||
|
let im = 2.0 * last.re * last.im + val.im;
|
||||||
|
let re = squared.re - squared.im + val.re;
|
||||||
|
last = Complex { re, im };
|
||||||
|
squared.re = re.powi(2);
|
||||||
|
squared.im = im.powi(2);
|
||||||
|
it += 1;
|
||||||
|
}
|
||||||
|
it
|
||||||
|
}
|
||||||
|
</@code>
|
||||||
|
Deciding whether a number does or does not fall into the set is more of a heuristic
|
||||||
|
than an analytic process. A good algorithm balances accuracy with efficiency. The
|
||||||
|
most glaring limitation of any calculation is the precision of the floating point
|
||||||
|
numbers used, especially when zooming in to the fractal. I deliberated the pros and
|
||||||
|
cons of using a floating point library with very high, or even arbitrary precision.
|
||||||
|
After extensive benchmarking, I was unable to get any of these to be efficient enough.
|
||||||
|
<h2 class="distinct"> Multi-Threading </h2>
|
||||||
|
In order to display the fractal, I needed an array of pixel data I could push to the
|
||||||
|
screen. The X and Y coordinates represent real and imaginary components respectively.
|
||||||
|
This means I need to run the function for every pixel on the screen. The Mandelbrot
|
||||||
|
Fractal is an obvious candidate for multi-threading, because the result of every
|
||||||
|
calculation is independent, and there are many calculations that need to be done.
|
||||||
|
I achieve this by breaking the pixel buffer into N partitions, and spawning a thread
|
||||||
|
to handle each (where N can be changed at runtime). This allowed me to rapidly iterate,
|
||||||
|
and find the optimal number of threads for the best performance.
|
||||||
|
<h2 class="distinct"> Results </h2>
|
||||||
|
I finished the project right before the deadline. This was my first time competing at
|
||||||
|
a hackathon, and the most code I had ever written in a 24 hour period. I was absolutely
|
||||||
|
exhausted, with no real idea of what to expect. I presented my project to the judges and
|
||||||
|
the other hackers at the event. The reception was incredibly positive, and I met many
|
||||||
|
brilliant people I stay in contact with to this day. My project won 2nd prize overall
|
||||||
|
at the event, for which I am extremely honored and greatful.
|
||||||
|
</div>
|
||||||
|
</Container>
|
111
hyper-src/projects/html-templating.html
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
<Container>
|
||||||
|
<div class="header">
|
||||||
|
<h1> HTML Templating Engine <GithubIcon href="https://github.com/Xterminate1818/html"/> </h1>
|
||||||
|
<h2> Front End - Parser Design </h2>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h2 class="distinct"> Motivation </h2>
|
||||||
|
HTML is a powerful tool for creating static web pages, but
|
||||||
|
is not easily modular or scalable. Front end frameworks like
|
||||||
|
React provide reusable components. I want to use components
|
||||||
|
without the overhead of a front-end framework. I want the final output to be statically generated rather than generated on the fly
|
||||||
|
by the server or the client, minified and with inlined CSS and
|
||||||
|
JavaScript.
|
||||||
|
<h2 class="distinct"> Approach </h2>
|
||||||
|
After looking at some of the HTML parsing libraries, I was
|
||||||
|
unsatisfied with the approaches most developers took. The DOM
|
||||||
|
is represented as a tree structure, where each node keeps a
|
||||||
|
pointer to its children.
|
||||||
|
<@code lang="rust">
|
||||||
|
// From the html_parser crate
|
||||||
|
pub enum Node {
|
||||||
|
Text(String),
|
||||||
|
Element(Element),
|
||||||
|
Comment(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Element {
|
||||||
|
pub name: String,
|
||||||
|
pub attributes: HashMap<String, Option<String>>,
|
||||||
|
pub children: Vec<Node>,
|
||||||
|
// other fields omitted
|
||||||
|
}
|
||||||
|
</@code>
|
||||||
|
This is bad for cache locality and effectively forces the use
|
||||||
|
of recursion in order to traverse the DOM. Recursion is undesirable
|
||||||
|
for performance and robustness reasons. My approach is to create a
|
||||||
|
flat array of elements. Parsing is done using a non-recursive parser
|
||||||
|
combinator model.
|
||||||
|
<@code lang="rust">
|
||||||
|
// My implementation (fields omitted)
|
||||||
|
pub enum HtmlElement {
|
||||||
|
DocType,
|
||||||
|
Comment(/* */),
|
||||||
|
OpenTag { /* */ },
|
||||||
|
CloseTag { /* */ },
|
||||||
|
Text( /* */ ),
|
||||||
|
Script { /* */ },
|
||||||
|
Style { /* */ },
|
||||||
|
Directive { /* */ },
|
||||||
|
}
|
||||||
|
</@code>
|
||||||
|
This approach lends itself to linear iterative algorithms. An
|
||||||
|
element's children can be represented as a slice of the DOM array,
|
||||||
|
from the opening tag to its close tag.
|
||||||
|
<h2 class="distinct"> Templates </h2>
|
||||||
|
I define a "template" as a user defined element which expands into
|
||||||
|
a larger piece of HTML. Templates can pass on children and attributes
|
||||||
|
to the final output. Some special templates can perform other functions,
|
||||||
|
like inlining a CSS file or syntax highlighting a code block.
|
||||||
|
<@code lang="html">
|
||||||
|
<!-- Here is a template definition -->
|
||||||
|
<Echo>
|
||||||
|
<h1 class=@class1>
|
||||||
|
<@children/>
|
||||||
|
</h1>
|
||||||
|
<h2 class=@class2>
|
||||||
|
<@children/>
|
||||||
|
</h2>
|
||||||
|
<h3 class=@class3>
|
||||||
|
<@children/>
|
||||||
|
</h3>
|
||||||
|
</Echo>
|
||||||
|
|
||||||
|
<!-- And this is how to use the template -->
|
||||||
|
<Echo class1="some_class1" class2="some_class2" class3="some_class3">
|
||||||
|
Echo!
|
||||||
|
</Echo>
|
||||||
|
|
||||||
|
<!-- Which expands to this -->
|
||||||
|
<h1 class="some_class1">
|
||||||
|
Echo!
|
||||||
|
</h1>
|
||||||
|
<h2 class="some_class2">
|
||||||
|
Echo!
|
||||||
|
</h2>
|
||||||
|
<h3 class="some_class3">
|
||||||
|
Echo!
|
||||||
|
</h3>
|
||||||
|
</@code>
|
||||||
|
The example above shows how templates can inherit attributes and child
|
||||||
|
elements from their invocation. Special templates begin with the '@'
|
||||||
|
symbol, and perform some sort of meta-function. In addition to
|
||||||
|
<@children/>, <@code/> performs syntax highlighting, and
|
||||||
|
<@style/> inlines a CSS file.
|
||||||
|
|
||||||
|
<h2 class="distinct"> Dependencies </h2>
|
||||||
|
The templating engine only relies on two crates for performing syntax
|
||||||
|
highlighting of code blocks. It uses inkjet, a thin wrapper over
|
||||||
|
treesitter. Treesitter is an efficient error-tolerant language parser
|
||||||
|
commonly used in IDEs. While it is fast, it is by far the slowest and
|
||||||
|
most cumbersome part of the program. Good looking formatted code is
|
||||||
|
important for my website, and implementing parsers for every language
|
||||||
|
I use is out of scope, so this is a necessary evil.
|
||||||
|
<@code lang="toml">
|
||||||
|
// Snippet from Cargo.toml
|
||||||
|
[dependencies]
|
||||||
|
inkjet = "0.10"
|
||||||
|
v_htmlescape = "0.15"
|
||||||
|
</@code>
|
||||||
|
</div>
|
||||||
|
</Container>
|
62
hyper-src/projects/http-server.html
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<Container>
|
||||||
|
<div class="header">
|
||||||
|
<h1> Rust HTTP Server </h1>
|
||||||
|
<h2> Keywords: Back End - TCP - SSL </h2>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h2 class="distinct"> Motivation </h2>
|
||||||
|
As a systems programmer, I want to understand the technologies
|
||||||
|
I use at a very low level. Modern web stacks abstract away the
|
||||||
|
process of handling TCP connections, serving SSL certificates,
|
||||||
|
and parsing HTTP requests. The best way to understand a technology
|
||||||
|
is to implement it from the ground up, I studied the HTTP 1.1
|
||||||
|
specification and wrote a server to host my website.
|
||||||
|
<h2 class="distinct"> Approach </h2>
|
||||||
|
At the most basic level, the server will translate the request URL
|
||||||
|
into a local file path, and respond with the file matching that path
|
||||||
|
if it exists. Request URLs are sanitized to prevent accessing files
|
||||||
|
outside of the server's directory. Two tables are used for rewriting
|
||||||
|
and routing URLs. This makes handling URLs predictable and robust.
|
||||||
|
<@code lang="rust">
|
||||||
|
// Snippet from the request handling code
|
||||||
|
pub async fn construct_response(&self, request_url: &str) -> Response {
|
||||||
|
let sanitized_url = rewrite_url(request_url);
|
||||||
|
if sanitized_url != request_url {
|
||||||
|
return Self::redirect(&sanitized_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
let routed_url = self.perform_routing(&sanitized_url).await;
|
||||||
|
if let Some(response) =
|
||||||
|
self.perform_redirecting(&routed_url).await {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
match self.get_resource(&routed_url).await {
|
||||||
|
Some(bytes) => Response::from_data(bytes),
|
||||||
|
None => Self::not_found(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</@code>
|
||||||
|
I use Cloudflare to manage my domain name, DNS records, and SSL certificates.
|
||||||
|
Cloudflare automatically caches resources, rewrites HTTP requests to HTTPS,
|
||||||
|
and limits request sizes. This allows my server to only listen on port 443
|
||||||
|
and neglect caching server-side.
|
||||||
|
<h2 class="distinct"> Dependencies </h2>
|
||||||
|
My goal with this project was to provide a functional server using as few
|
||||||
|
libraries as possible. The final project directly depends on four crates:
|
||||||
|
<@code lang=toml>
|
||||||
|
// Snippet from Cargo.toml
|
||||||
|
[dependencies]
|
||||||
|
log = "0.4"
|
||||||
|
log4rs = "1.3"
|
||||||
|
tiny_http = "0.12"
|
||||||
|
tokio = {version = "1", features = ["full"]}
|
||||||
|
</@code>
|
||||||
|
The first two crates, log and log4rs, provide logging functions which aren't critical to the
|
||||||
|
server's functionality. The tiny_http crate provides a simple wrapper for the standard
|
||||||
|
library TCP stream, and performs SSL encryption. It is used as the backbone
|
||||||
|
for many Rust web frameworks. While I could implement these features myself, I
|
||||||
|
decided against it for security and browser compatibility reasons. Finally there is
|
||||||
|
tokio, an async runtime. This is necessary because Rust does not provide a runtime
|
||||||
|
by default, and building one is out of scope.
|
||||||
|
</div>
|
||||||
|
</Container>
|
1
hyper-src/projects/math-interpreter.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<Wip/>
|
108
hyper-src/projects/nd-range.html
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
<Container>
|
||||||
|
<div class="header">
|
||||||
|
<h1> nd-range </h1>
|
||||||
|
<h2> Vector Math - Standard Library </h2>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h2 class="distinct"> Motivation </h2>
|
||||||
|
The Rust standard library provides several 'Range' types which
|
||||||
|
represent integers inside a given bounds (i.e. 1 ≤ n ≤ 100).
|
||||||
|
Ranges can be iterated over, and used for bounds testing of numbers.
|
||||||
|
In designing video games, it is often necessary to express 2 or 3
|
||||||
|
dimensional quantities or bounds. Problems like iterating over
|
||||||
|
voxels in a 3D world, or testing for an intersection between rectangles
|
||||||
|
lend themselves to the idea of Ranges.
|
||||||
|
<@code lang="rust">
|
||||||
|
// A 3D array representing a 128x128x128 voxel world
|
||||||
|
let world_data = [[[0; 128]; 128]; 128];
|
||||||
|
// The '0..128' here is a range from 0 to 127 inclusive
|
||||||
|
for x in 0..128 {
|
||||||
|
for y in 0..128 {
|
||||||
|
for z in 0..128 {
|
||||||
|
let voxel = &world[x][y][z];
|
||||||
|
// Do something with voxel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</@code>
|
||||||
|
However, in these contexts the Range type is not particularly helpful.
|
||||||
|
I am forced to use three different ranges and a triple nested 'for' loop.
|
||||||
|
The idea for nd-range is to expand the native Range type so that it
|
||||||
|
can be used over arbitrary dimensions. This allows us to iterate over
|
||||||
|
X, Y, and Z coordinates in a single loop.
|
||||||
|
<@code lang="rust">
|
||||||
|
let world_data = [[[0; 128]; 128]; 128];
|
||||||
|
for [x, y, z] in nrange!(0..128, 0..128, 0..128) {
|
||||||
|
let voxel = &world[x][y][z];
|
||||||
|
// Do something with voxel
|
||||||
|
}
|
||||||
|
</@code>
|
||||||
|
<h2 class="distinct"> Approach </h2>
|
||||||
|
An nd-range is similar in concept to an axis-aligned bounding box,
|
||||||
|
or AABB. These are common abstractions used in game development,
|
||||||
|
and there are well established algorithms that test for overlap.
|
||||||
|
Iterating over is slightly more complex, requiring some vector
|
||||||
|
algebra. If we interpret the range of values over each axis as a
|
||||||
|
vector, we can apply the cartesian product algorithm to find every
|
||||||
|
position within the bounded space. Below is a diagram from Wikipedia
|
||||||
|
(CC BY-SA 3.0).
|
||||||
|
<img src="https://upload.wikimedia.org/wikipedia/commons/4/4e/Cartesian_Product_qtl1.svg"
|
||||||
|
class="centered"
|
||||||
|
/>
|
||||||
|
Because a range is contiguous by definition, generating a cartesian
|
||||||
|
product is simple and performant. The iterator object has a space
|
||||||
|
complexity of O(N), where n is the number of dimensions. Rust's const generics
|
||||||
|
allow the entire struct to exist on the stack, which is a boon to performance.
|
||||||
|
<h2 class="distinct"> Performance </h2>
|
||||||
|
My points of comparison are the itertools and cartesian crates, which provide
|
||||||
|
comparable algorithms. Despite its popularity, I was shocked to discover how
|
||||||
|
slow the itertools implementation of the cartesian product is. The cartesian-rs
|
||||||
|
crate is lesser known, and uses a creative macro-based solution. I benchmarked
|
||||||
|
all three approaches, as well as the nested for-loop base case, over a 100x100x100
|
||||||
|
range.
|
||||||
|
<table class="striped">
|
||||||
|
<thead>
|
||||||
|
<th scope="col"> Implementation </th>
|
||||||
|
<th scope="col"> Average Time (ns) </th>
|
||||||
|
<th scope="col"> Error (+/- ns) </th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<a href="https://docs.rs/itertools/latest/itertools/trait.Itertools.html#method.cartesian_product">
|
||||||
|
itertools
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<td> 1,821,573 </td>
|
||||||
|
<td> 31,501 </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<a href="https://crates.io/crates/cartesian">
|
||||||
|
cartesian-rs
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<td> 989,242 </td>
|
||||||
|
<td> 50,835 </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"> nd-range </th>
|
||||||
|
<td> 968,853 </td>
|
||||||
|
<td> 15,792 </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"> nested loops </th>
|
||||||
|
<td> 911,853 </td>
|
||||||
|
<td> 46,066 </td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h2 class="distinct"> Pros and Cons</h2>
|
||||||
|
My implementation is competitive with, and possibly faster than
|
||||||
|
its competitors for my use case. While nd-range only works for
|
||||||
|
ranges of contiguous integers, itertools and cartesian-rs work
|
||||||
|
for any iterators. However, by restricting my use case I can
|
||||||
|
extract more performance gains and integrate better with the Rust
|
||||||
|
standard library.
|
||||||
|
</div>
|
||||||
|
</Container>
|
1
hyper-src/projects/pokedex.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<Wip/>
|
1
hyper-src/projects/stocktrading.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<Wip />
|
BIN
resources/CascadiaMonoPL.woff2
Normal file
4
resources/award.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-award" viewBox="0 0 16 16">
|
||||||
|
<path d="M9.669.864 8 0 6.331.864l-1.858.282-.842 1.68-1.337 1.32L2.6 6l-.306 1.854 1.337 1.32.842 1.68 1.858.282L8 12l1.669-.864 1.858-.282.842-1.68 1.337-1.32L13.4 6l.306-1.854-1.337-1.32-.842-1.68zm1.196 1.193.684 1.365 1.086 1.072L12.387 6l.248 1.506-1.086 1.072-.684 1.365-1.51.229L8 10.874l-1.355-.702-1.51-.229-.684-1.365-1.086-1.072L3.614 6l-.25-1.506 1.087-1.072.684-1.365 1.51-.229L8 1.126l1.356.702z"/>
|
||||||
|
<path d="M4 11.794V16l4-1 4 1v-4.206l-2.018.306L8 13.126 6.018 12.1z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 620 B |
1
resources/cloudflare-icon.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#FFF" d="m115.679 69.288-15.591-8.94-2.689-1.163-63.781.436v32.381h82.061z"/><path fill="#F38020" d="M87.295 89.022c.763-2.617.472-5.015-.8-6.796-1.163-1.635-3.125-2.58-5.488-2.689l-44.737-.581c-.291 0-.545-.145-.691-.363s-.182-.509-.109-.8c.145-.436.581-.763 1.054-.8l45.137-.581c5.342-.254 11.157-4.579 13.192-9.885l2.58-6.723c.109-.291.145-.581.073-.872-2.906-13.158-14.644-22.97-28.672-22.97-12.938 0-23.913 8.359-27.838 19.952a13.35 13.35 0 0 0-9.267-2.58c-6.215.618-11.193 5.597-11.811 11.811-.145 1.599-.036 3.162.327 4.615C10.104 70.051 2 78.337 2 88.549c0 .909.073 1.817.182 2.726a.895.895 0 0 0 .872.763h82.57c.472 0 .909-.327 1.054-.8l.617-2.216z"/><path fill="#FAAE40" d="M101.542 60.275c-.4 0-.836 0-1.236.036-.291 0-.545.218-.654.509l-1.744 6.069c-.763 2.617-.472 5.015.8 6.796 1.163 1.635 3.125 2.58 5.488 2.689l9.522.581c.291 0 .545.145.691.363.145.218.182.545.109.8-.145.436-.581.763-1.054.8l-9.924.582c-5.379.254-11.157 4.579-13.192 9.885l-.727 1.853c-.145.363.109.727.509.727h34.089c.4 0 .763-.254.872-.654.581-2.108.909-4.325.909-6.614 0-13.447-10.975-24.422-24.458-24.422"/></svg>
|
After Width: | Height: | Size: 1.1 KiB |
4
resources/favicon.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-ethernet" viewBox="0 0 16 16">
|
||||||
|
<path d="M14 13.5v-7a.5.5 0 0 0-.5-.5H12V4.5a.5.5 0 0 0-.5-.5h-1v-.5A.5.5 0 0 0 10 3H6a.5.5 0 0 0-.5.5V4h-1a.5.5 0 0 0-.5.5V6H2.5a.5.5 0 0 0-.5.5v7a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5M3.75 11h.5a.25.25 0 0 1 .25.25v1.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-1.5a.25.25 0 0 1 .25-.25m2 0h.5a.25.25 0 0 1 .25.25v1.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-1.5a.25.25 0 0 1 .25-.25m1.75.25a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v1.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25zM9.75 11h.5a.25.25 0 0 1 .25.25v1.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-1.5a.25.25 0 0 1 .25-.25m1.75.25a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v1.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25z"/>
|
||||||
|
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zM1 2a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 993 B |
1
resources/github-icon.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><g fill="#181616"><path fill-rule="evenodd" 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"/><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"/></g></svg>
|
After Width: | Height: | Size: 1.8 KiB |
1
resources/godot-icon.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path d="M114.906 84.145s-.172-1.04-.27-1.028l-18.823 1.817a3.062 3.062 0 00-2.77 2.84l-.516 7.413-14.566 1.04-.988-6.72a3.089 3.089 0 00-3.04-2.62H54.067a3.089 3.089 0 00-3.039 2.62l-.988 6.72-14.566-1.04-.516-7.414a3.058 3.058 0 00-2.77-2.84l-18.835-1.816c-.094-.012-.168 1.028-.266 1.028l-.024 4.074 15.954 2.574.52 7.477a3.084 3.084 0 002.843 2.847l20.059 1.434c.078.004.152.008.226.008a3.087 3.087 0 003.031-2.621l1.02-6.915h14.57l1.02 6.915a3.088 3.088 0 003.254 2.613l20.062-1.434a3.084 3.084 0 002.844-2.847l.52-7.477 15.945-2.586zm0 0" fill="#fff"/><path d="M13.086 53.422v30.723c.059 0 .113.003.168.007L32.09 85.97a2.027 2.027 0 011.828 1.875l.582 8.316 16.426 1.172 1.133-7.672a2.03 2.03 0 012.007-1.734h19.868a2.03 2.03 0 012.007 1.734l1.133 7.672 16.43-1.172.578-8.316a2.027 2.027 0 011.828-1.875l18.828-1.817c.055-.004.11-.007.168-.007V81.69h.008V53.42c2.652-3.335 5.16-7.019 7.086-10.116-2.941-5.008-6.543-9.48-10.395-13.625a101.543 101.543 0 00-10.316 6.004c-1.64-1.633-3.484-2.965-5.3-4.36-1.782-1.43-3.79-2.48-5.696-3.703.566-4.223.848-8.379.96-12.719-4.913-2.476-10.155-4.113-15.456-5.293-2.117 3.559-4.055 7.41-5.738 11.176-2-.332-4.008-.457-6.02-.48V20.3c-.016 0-.027.004-.039.004s-.023-.004-.04-.004v.004c-2.01.023-4.019.148-6.019.48-1.683-3.765-3.62-7.617-5.738-11.176-5.3 1.18-10.543 2.817-15.457 5.293.113 4.34.395 8.496.961 12.72-1.906 1.222-3.914 2.273-5.695 3.702-1.813 1.395-3.66 2.727-5.301 4.36a101.543 101.543 0 00-10.316-6.004C12.543 33.824 8.94 38.297 6 43.305c2.313 3.629 4.793 7.273 7.086 10.117zm0 0" fill="#478cbf"/><path d="M98.008 89.84l-.582 8.36a2.024 2.024 0 01-1.88 1.878l-20.062 1.434c-.046.004-.097.004-.144.004-.996 0-1.86-.73-2.004-1.73l-1.152-7.806H55.816l-1.152 7.805a2.026 2.026 0 01-2.148 1.727l-20.063-1.434a2.024 2.024 0 01-1.879-1.879l-.582-8.36-16.937-1.632c.008 1.82.03 3.816.03 4.211 0 17.887 22.692 26.484 50.88 26.582h.07c28.188-.098 50.871-8.695 50.871-26.582 0-.402.024-2.39.031-4.211zm0 0" fill="#478cbf"/><path d="M48.652 65.895c0 6.27-5.082 11.351-11.351 11.351-6.266 0-11.348-5.082-11.348-11.351 0-6.266 5.082-11.344 11.348-11.344 6.27 0 11.351 5.078 11.351 11.344" fill="#fff"/><path d="M45.922 66.566a7.531 7.531 0 01-7.535 7.532 7.534 7.534 0 01-7.535-7.532 7.534 7.534 0 017.535-7.53 7.531 7.531 0 017.535 7.53" fill="#414042"/><path d="M64 78.277c-2.02 0-3.652-1.488-3.652-3.32v-10.45c0-1.831 1.632-3.32 3.652-3.32 2.016 0 3.656 1.489 3.656 3.32v10.45c0 1.832-1.64 3.32-3.656 3.32m15.348-12.382c0 6.27 5.082 11.351 11.351 11.351 6.266 0 11.348-5.082 11.348-11.351 0-6.266-5.082-11.344-11.348-11.344-6.27 0-11.351 5.078-11.351 11.344" fill="#fff"/><path d="M82.078 66.566a7.53 7.53 0 007.531 7.532 7.531 7.531 0 100-15.063 7.53 7.53 0 00-7.53 7.531" fill="#414042"/></svg>
|
After Width: | Height: | Size: 2.7 KiB |
1
resources/html5-icon.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#E44D26" d="M19.037 113.876L9.032 1.661h109.936l-10.016 112.198-45.019 12.48z"/><path fill="#F16529" d="M64 116.8l36.378-10.086 8.559-95.878H64z"/><path fill="#EBEBEB" d="M64 52.455H45.788L44.53 38.361H64V24.599H29.489l.33 3.692 3.382 37.927H64zm0 35.743l-.061.017-15.327-4.14-.979-10.975H33.816l1.928 21.609 28.193 7.826.063-.017z"/><path fill="#fff" d="M63.952 52.455v13.763h16.947l-1.597 17.849-15.35 4.143v14.319l28.215-7.82.207-2.325 3.234-36.233.335-3.696h-3.708zm0-27.856v13.762h33.244l.276-3.092.628-6.978.329-3.692z"/></svg>
|
After Width: | Height: | Size: 607 B |
1
resources/java-icon.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#0074BD" d="M52.581 67.817s-3.284 1.911 2.341 2.557c6.814.778 10.297.666 17.805-.753 0 0 1.979 1.237 4.735 2.309-16.836 7.213-38.104-.418-24.881-4.113zm-2.059-9.415s-3.684 2.729 1.945 3.311c7.28.751 13.027.813 22.979-1.103 0 0 1.373 1.396 3.536 2.157-20.352 5.954-43.021.469-28.46-4.365z"/><path fill="#EA2D2E" d="M67.865 42.431c4.151 4.778-1.088 9.074-1.088 9.074s10.533-5.437 5.696-12.248c-4.519-6.349-7.982-9.502 10.771-20.378.001 0-29.438 7.35-15.379 23.552z"/><path fill="#0074BD" d="M90.132 74.781s2.432 2.005-2.678 3.555c-9.716 2.943-40.444 3.831-48.979.117-3.066-1.335 2.687-3.187 4.496-3.576 1.887-.409 2.965-.334 2.965-.334-3.412-2.403-22.055 4.719-9.469 6.762 34.324 5.563 62.567-2.506 53.665-6.524zm-35.97-26.134s-15.629 3.713-5.534 5.063c4.264.57 12.758.439 20.676-.225 6.469-.543 12.961-1.704 12.961-1.704s-2.279.978-3.93 2.104c-15.874 4.175-46.533 2.23-37.706-2.038 7.463-3.611 13.533-3.2 13.533-3.2zM82.2 64.317c16.135-8.382 8.674-16.438 3.467-15.353-1.273.266-1.845.496-1.845.496s.475-.744 1.378-1.063c10.302-3.62 18.223 10.681-3.322 16.345 0 0 .247-.224.322-.425z"/><path fill="#EA2D2E" d="M72.474 1.313s8.935 8.939-8.476 22.682c-13.962 11.027-3.184 17.313-.006 24.498-8.15-7.354-14.128-13.828-10.118-19.852 5.889-8.842 22.204-13.131 18.6-27.328z"/><path fill="#0074BD" d="M55.749 87.039c15.484.99 39.269-.551 39.832-7.878 0 0-1.082 2.777-12.799 4.981-13.218 2.488-29.523 2.199-39.191.603 0 0 1.98 1.64 12.158 2.294z"/><path fill="#EA2D2E" d="M94.866 100.181h-.472v-.264h1.27v.264h-.47v1.317h-.329l.001-1.317zm2.535.066h-.006l-.468 1.251h-.216l-.465-1.251h-.005v1.251h-.312v-1.581h.457l.431 1.119.432-1.119h.454v1.581h-.302v-1.251zm-44.19 14.79c-1.46 1.266-3.004 1.978-4.391 1.978-1.974 0-3.045-1.186-3.045-3.085 0-2.055 1.146-3.56 5.738-3.56h1.697v4.667h.001zm4.031 4.548v-14.077c0-3.599-2.053-5.973-6.997-5.973-2.886 0-5.416.714-7.473 1.622l.592 2.493c1.62-.595 3.715-1.147 5.771-1.147 2.85 0 4.075 1.147 4.075 3.521v1.779h-1.424c-6.921 0-10.044 2.685-10.044 6.723 0 3.479 2.058 5.456 5.933 5.456 2.49 0 4.351-1.028 6.088-2.533l.316 2.137h3.163v-.001zm13.452 0h-5.027l-6.051-19.689h4.391l3.756 12.099.835 3.635c1.896-5.258 3.24-10.596 3.912-15.733h4.271c-1.143 6.481-3.203 13.598-6.087 19.688zm19.288-4.548c-1.465 1.266-3.01 1.978-4.392 1.978-1.976 0-3.046-1.186-3.046-3.085 0-2.055 1.149-3.56 5.736-3.56h1.701v4.667h.001zm4.033 4.548v-14.077c0-3.599-2.059-5.973-6.999-5.973-2.889 0-5.418.714-7.475 1.622l.593 2.493c1.62-.595 3.718-1.147 5.774-1.147 2.846 0 4.074 1.147 4.074 3.521v1.779h-1.424c-6.923 0-10.045 2.685-10.045 6.723 0 3.479 2.056 5.456 5.93 5.456 2.491 0 4.349-1.028 6.091-2.533l.318 2.137h3.163v-.001zm-56.693 3.346c-1.147 1.679-3.005 3.008-5.037 3.757l-1.989-2.345c1.547-.794 2.872-2.075 3.489-3.269.532-1.063.753-2.43.753-5.701V92.891h4.284v22.173c0 4.375-.348 6.144-1.5 7.867z"/></svg>
|
After Width: | Height: | Size: 2.8 KiB |
BIN
resources/mona_lisa.gif
Normal file
After Width: | Height: | Size: 6.4 MiB |
1
resources/python-icon.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#FFD845" d="M49.33 62h29.159C86.606 62 93 55.132 93 46.981V19.183c0-7.912-6.632-13.856-14.555-15.176-5.014-.835-10.195-1.215-15.187-1.191-4.99.023-9.612.448-13.805 1.191C37.098 6.188 35 10.758 35 19.183V30h29v4H23.776c-8.484 0-15.914 5.108-18.237 14.811-2.681 11.12-2.8 17.919 0 29.53C7.614 86.983 12.569 93 21.054 93H31V79.952C31 70.315 39.428 62 49.33 62zm-1.838-39.11c-3.026 0-5.478-2.479-5.478-5.545 0-3.079 2.451-5.581 5.478-5.581 3.015 0 5.479 2.502 5.479 5.581-.001 3.066-2.465 5.545-5.479 5.545zm74.789 25.921C120.183 40.363 116.178 34 107.682 34H97v12.981C97 57.031 88.206 65 78.489 65H49.33C41.342 65 35 72.326 35 80.326v27.8c0 7.91 6.745 12.564 14.462 14.834 9.242 2.717 17.994 3.208 29.051 0C85.862 120.831 93 116.549 93 108.126V97H64v-4h43.682c8.484 0 11.647-5.776 14.599-14.66 3.047-9.145 2.916-17.799 0-29.529zm-41.955 55.606c3.027 0 5.479 2.479 5.479 5.547 0 3.076-2.451 5.579-5.479 5.579-3.015 0-5.478-2.502-5.478-5.579 0-3.068 2.463-5.547 5.478-5.547z"/></svg>
|
After Width: | Height: | Size: 1 KiB |
BIN
resources/raylib-logo.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
1
resources/rust-icon.svg
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
resources/starry_night.gif
Normal file
After Width: | Height: | Size: 7 MiB |
BIN
resources/wanderer.gif
Normal file
After Width: | Height: | Size: 6.6 MiB |
5
resources/web-assembly-icon.svg
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 107.62 107.62"><defs><style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #654ff0;
|
||||||
|
}
|
||||||
|
</style></defs><title>web-assembly-icon</title><g id="Layer_2" data-name="Layer 2"><g id="Notch_-_Purple" data-name="Notch - Purple"><g id="icon"><path class="cls-1" d="M66.12,0c0,.19,0,.38,0,.58a12.34,12.34,0,1,1-24.68,0c0-.2,0-.39,0-.58H0V107.62H107.62V0ZM51.38,96.1,46.14,70.17H46L40.39,96.1H33.18L25,58h7.13L37,83.93h.09L42.94,58h6.67L54.9,84.25H55L60.55,58h7L58.46,96.1Zm39.26,0-2.43-8.48H75.4L73.53,96.1H66.36L75.59,58H86.83L98,96.1Z"/><polygon class="cls-1" points="79.87 67.39 76.76 81.37 86.44 81.37 82.87 67.39 79.87 67.39"/></g></g></g></svg>
|
After Width: | Height: | Size: 685 B |
1
resources/wgpu-logo.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg height="298.90323" viewBox="0 0 61.657897 79.084803" width="233.03775" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="67.733323" x2="67.733323" y1="31.883125" y2="50.933122"><stop offset="0" stop-color="#91e12a"/><stop offset="1" stop-color="#7b8a49"/></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="84.666658" x2="84.666658" y1="23.416458" y2="65.749786"><stop offset="0" stop-color="#2263ea"/><stop offset="1" stop-color="#33c0f4"/></linearGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="50.799992" x2="50.799992" y1="23.416458" y2="65.749786"><stop offset="0" stop-color="#e12400"/><stop offset="1" stop-color="#febe00"/></linearGradient><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="52.91666" x2="52.91666" y1="40.349789" y2="65.749786"><stop offset="0" stop-color="#f6d339"/><stop offset="1" stop-color="#ffd55f"/></linearGradient><linearGradient id="e" gradientUnits="userSpaceOnUse" x1="82.549988" x2="82.549988" y1="40.349789" y2="65.749786"><stop offset="0" stop-color="#99def2"/><stop offset="1" stop-color="#84d2f8"/></linearGradient><g transform="translate(-36.904387 -20.770627)"><text font-family="Aria" font-size="33.8667" letter-spacing="-1.03188" stroke-width=".264583" x="38.161156" y="90.082375"><tspan font-family="Arial" font-weight="bold" letter-spacing="-.978958" stroke-width=".264583" x="38.161156" y="90.082375">gpu</tspan></text><path d="m61.383325 40.34979 6.35-8.466666 6.34999 8.466666-6.34999 10.583331z" fill="url(#a)"/><path d="m50.799994 23.41646-8.466667 42.333327 19.049997-25.399997z" fill="url(#c)"/><path d="m93.133323 65.749787-19.049999-25.399997 10.583329-16.93333z" fill="url(#b)"/><path d="m42.333327 65.749787 19.049997-25.399997 6.35 10.583331-8.466666 14.816666z" fill="url(#d)"/><path d="m67.733324 50.933121 6.349998-10.583331 19.049999 25.399997h-16.933333z" fill="url(#e)"/></g></svg>
|
After Width: | Height: | Size: 1.9 KiB |
BIN
resume.pdf
Normal file
121
src/forms.rs
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
type Parse<'a, T> = Option<(T, &'a [u8])>;
|
||||||
|
|
||||||
|
fn parse_byte(i: &[u8], byte: u8) -> Parse<u8> {
|
||||||
|
let first = i.first()?;
|
||||||
|
if first == &byte {
|
||||||
|
Some((byte, &i[1..]))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_escaped_codepoint(i: &[u8]) -> Parse<u8> {
|
||||||
|
let (_, i) = parse_byte(i, b'%')?;
|
||||||
|
let slice = i.get(..2)?;
|
||||||
|
let i = i.get(2..)?;
|
||||||
|
let num = u8::from_str_radix(std::str::from_utf8(slice).ok()?, 16).ok()?;
|
||||||
|
Some((num, i))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn url_unescape(mut i: &[u8]) -> Option<Vec<u8>> {
|
||||||
|
let mut bytes = Vec::with_capacity(i.len());
|
||||||
|
while let Some(b) = i.first() {
|
||||||
|
if b == &b'%' {
|
||||||
|
let (escaped, new_i) = parse_escaped_codepoint(i)?;
|
||||||
|
bytes.push(escaped);
|
||||||
|
i = new_i;
|
||||||
|
} else {
|
||||||
|
bytes.push(*b);
|
||||||
|
i = &i[1..];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses form bodies with enctype=x-www-form-urlencoded
|
||||||
|
/// (default)
|
||||||
|
pub fn parse_urlencoded(i: &[u8]) -> Option<HashMap<String, Vec<u8>>> {
|
||||||
|
let mut output = HashMap::new();
|
||||||
|
let args = i.split(|b| b == &b'&');
|
||||||
|
for arg in args {
|
||||||
|
let unescaped = url_unescape(arg)?;
|
||||||
|
let (key, value) = unescaped.split_once(|b| b == &b'=')?;
|
||||||
|
output.insert(String::from_utf8_lossy(key).to_string(), value.to_vec());
|
||||||
|
}
|
||||||
|
Some(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_form(request: &mut tiny_http::Request) -> Option<HashMap<String, Vec<u8>>> {
|
||||||
|
let content_type = request.headers().iter().find_map(|header| {
|
||||||
|
if header.field.as_str().to_ascii_lowercase() == "content-type" {
|
||||||
|
Some(header.value.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
if let Some(s) = content_type
|
||||||
|
.as_str()
|
||||||
|
.strip_prefix("multipart/form-data; boundary=")
|
||||||
|
{
|
||||||
|
let mut boundary = vec![b'-', b'-'];
|
||||||
|
boundary.append(&mut s.as_bytes().to_vec());
|
||||||
|
return parse_multipart_form(request, boundary);
|
||||||
|
} else if content_type.as_str() == "application/x-www-form-urlencoded" {
|
||||||
|
let mut bytes = vec![];
|
||||||
|
request.as_reader().read_to_end(&mut bytes).ok()?;
|
||||||
|
return parse_urlencoded(&bytes);
|
||||||
|
} else {
|
||||||
|
log::warn!("Unrecognized form type: {}", content_type.as_str());
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn position_of(slice: &[u8], pattern: &[u8]) -> Option<usize> {
|
||||||
|
for index in 0..slice.len() {
|
||||||
|
if slice[..index].ends_with(pattern) {
|
||||||
|
return Some(index - pattern.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses forms with enctype=multipart/form-data
|
||||||
|
pub fn parse_multipart_form(
|
||||||
|
request: &mut tiny_http::Request,
|
||||||
|
boundary: Vec<u8>,
|
||||||
|
) -> Option<HashMap<String, Vec<u8>>> {
|
||||||
|
let mut bytes = vec![];
|
||||||
|
request.as_reader().read_to_end(&mut bytes).unwrap();
|
||||||
|
// Strip out cruft to make it easy to parse
|
||||||
|
let mut bytes = bytes
|
||||||
|
.strip_prefix(b"--")?
|
||||||
|
.strip_suffix(b"--\r\n")?
|
||||||
|
.strip_suffix(boundary.as_slice())?;
|
||||||
|
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
|
||||||
|
while !bytes.is_empty() {
|
||||||
|
bytes = &bytes[boundary.len()..];
|
||||||
|
// Find next boundary, or end of body
|
||||||
|
let end = position_of(bytes, &boundary).unwrap_or(bytes.len());
|
||||||
|
let slice = &bytes[..end].strip_suffix(b"\r\n")?;
|
||||||
|
bytes = &bytes[end..];
|
||||||
|
// Find end of headers
|
||||||
|
let end = position_of(slice, b"\r\n\r\n").unwrap_or(0);
|
||||||
|
let headers = &slice[..end];
|
||||||
|
let string = String::from_utf8_lossy(headers);
|
||||||
|
let name = string
|
||||||
|
.lines()
|
||||||
|
.find_map(|line| line.strip_prefix("Content-Disposition: "))?
|
||||||
|
.split("; ")
|
||||||
|
.find_map(|arg| arg.strip_prefix("name="))?
|
||||||
|
.trim();
|
||||||
|
let name = name.strip_prefix('"').unwrap_or(name);
|
||||||
|
let name = name.strip_suffix('"').unwrap_or(name);
|
||||||
|
let body = &slice[end + 4..];
|
||||||
|
map.insert(name.into(), body.to_vec());
|
||||||
|
}
|
||||||
|
Some(map)
|
||||||
|
}
|
106
src/get.rs
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
path::{Component, Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use tiny_http::Header;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
use crate::util::*;
|
||||||
|
|
||||||
|
/// Makes the uri less spooky - removes leading /, rejects
|
||||||
|
/// ../ and ./
|
||||||
|
pub fn rewrite_url(url: &str) -> String {
|
||||||
|
let (url, query) = url.split_once('?').unwrap_or((url, ""));
|
||||||
|
let target_path = Path::new(url);
|
||||||
|
let mut corrected_path = PathBuf::new();
|
||||||
|
for c in target_path.components() {
|
||||||
|
match c {
|
||||||
|
Component::Normal(_) | Component::RootDir => {
|
||||||
|
corrected_path.push(c);
|
||||||
|
},
|
||||||
|
Component::ParentDir => {
|
||||||
|
corrected_path.pop();
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
corrected_path.to_string_lossy().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Router {
|
||||||
|
/// Reinterpret A as B server-side
|
||||||
|
route_table: RwLock<HashMap<String, String>>,
|
||||||
|
/// Send a 301 redirect from A to B
|
||||||
|
routing_table: RwLock<HashMap<String, String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Router {
|
||||||
|
const NOT_FOUND: &'static [u8] = include_bytes!("../hyper-build/404.html");
|
||||||
|
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
route_table: RwLock::new(Default::default()),
|
||||||
|
routing_table: RwLock::new(Default::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn redirect(to: &str) -> Response {
|
||||||
|
Response::from_data(&[])
|
||||||
|
.with_status_code(301)
|
||||||
|
.with_header(header("Location", to))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn not_found() -> Response {
|
||||||
|
Response::from_data(Self::NOT_FOUND).with_status_code(404)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn construct_response(&self, request_url: &str) -> Response {
|
||||||
|
let sanitized_url = rewrite_url(request_url);
|
||||||
|
if sanitized_url != request_url {
|
||||||
|
return Self::redirect(&sanitized_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
let routed_url = self.perform_routing(&sanitized_url).await;
|
||||||
|
if let Some(response) = self.perform_redirecting(&routed_url).await {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mime_type = mime_from_file_ext(&routed_url);
|
||||||
|
|
||||||
|
match self.get_resource(&routed_url).await {
|
||||||
|
Some(bytes) => Response::from_data(bytes)
|
||||||
|
.with_header(header("Content-Type", &mime_type)),
|
||||||
|
None => Self::not_found(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_route(&self, from: &str, to: &str) {
|
||||||
|
let from = rewrite_url(from);
|
||||||
|
let to = rewrite_url(to);
|
||||||
|
let mut w = self.route_table.write().await;
|
||||||
|
w.insert(from.into(), to.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn perform_routing(&self, uri: &str) -> String {
|
||||||
|
let r = self.route_table.read().await;
|
||||||
|
r.get(uri).cloned().unwrap_or(uri.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_redirect(&self, from: &str, to: &str) {
|
||||||
|
let from = rewrite_url(from);
|
||||||
|
let to = rewrite_url(to);
|
||||||
|
let mut w = self.routing_table.write().await;
|
||||||
|
w.insert(from.into(), to.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn perform_redirecting(&self, uri: &str) -> Option<Response> {
|
||||||
|
let r = self.routing_table.read().await;
|
||||||
|
r.get(uri).map(|url| Self::redirect(url))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_resource(&self, uri: &str) -> Option<Vec<u8>> {
|
||||||
|
let uri = uri.trim_start_matches('/');
|
||||||
|
tokio::fs::read(&uri).await.ok()
|
||||||
|
}
|
||||||
|
}
|
46
src/logging.rs
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
pub fn init() -> log4rs::Handle {
|
||||||
|
use log4rs::append::console::ConsoleAppender;
|
||||||
|
use log4rs::append::rolling_file::{
|
||||||
|
policy::compound::*, RollingFileAppender,
|
||||||
|
};
|
||||||
|
use log4rs::config::{Appender, Config, Logger, Root};
|
||||||
|
use log4rs::encode::pattern::PatternEncoder;
|
||||||
|
|
||||||
|
let stdout = ConsoleAppender::builder()
|
||||||
|
.encoder(Box::new(PatternEncoder::new("{d(%H:%M)} {l} - {m}{n}")))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let connections = RollingFileAppender::builder()
|
||||||
|
.encoder(Box::new(PatternEncoder::new(
|
||||||
|
"{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}",
|
||||||
|
)))
|
||||||
|
.build(
|
||||||
|
"logs/connections.log",
|
||||||
|
Box::new(CompoundPolicy::new(
|
||||||
|
Box::new(trigger::size::SizeTrigger::new(1_000_000)),
|
||||||
|
Box::new(
|
||||||
|
roll::fixed_window::FixedWindowRoller::builder()
|
||||||
|
.base(1)
|
||||||
|
.build("logs/connections-{}.log", 5)
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let config = Config::builder()
|
||||||
|
.appender(Appender::builder().build("stdout", Box::new(stdout)))
|
||||||
|
.appender(Appender::builder().build("connections", Box::new(connections)))
|
||||||
|
.logger(
|
||||||
|
Logger::builder()
|
||||||
|
.appender("connections")
|
||||||
|
.build("server", log::LevelFilter::Info),
|
||||||
|
)
|
||||||
|
.build(
|
||||||
|
Root::builder()
|
||||||
|
.appender("stdout")
|
||||||
|
.build(log::LevelFilter::Info),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
log4rs::init_config(config).unwrap()
|
||||||
|
}
|
130
src/main.rs
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
#![feature(slice_split_once)]
|
||||||
|
mod forms;
|
||||||
|
mod get;
|
||||||
|
mod logging;
|
||||||
|
mod post;
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
use std::{io::Write, sync::Arc};
|
||||||
|
|
||||||
|
use get::Router;
|
||||||
|
use post::PostHandler;
|
||||||
|
use tiny_http::{Request, Server};
|
||||||
|
|
||||||
|
// Exit process if its already running
|
||||||
|
fn graceful_cleanup() {
|
||||||
|
const PID_PATH: &'static str = "./temp/pid";
|
||||||
|
match {
|
||||||
|
let s = std::fs::read_to_string(PID_PATH).unwrap_or("".to_string());
|
||||||
|
s.parse::<u32>().ok()
|
||||||
|
} {
|
||||||
|
Some(last_pid) => {
|
||||||
|
std::process::Command::new("pkill")
|
||||||
|
.arg(format!("{}", last_pid))
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
log::warn!("Did not find previous PID");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let pid = std::process::id();
|
||||||
|
let mut file = std::fs::File::create(PID_PATH).unwrap();
|
||||||
|
file.write(format!("{}", pid).as_bytes()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
// graceful_cleanup();
|
||||||
|
let _h = logging::init();
|
||||||
|
// let ssl = tiny_http::SslConfig {
|
||||||
|
// certificate:
|
||||||
|
// include_bytes!("../lgatlin.dev.cert").to_vec(),
|
||||||
|
// private_key:
|
||||||
|
// include_bytes!("../../lgatlin.dev.priv").to_vec(), };
|
||||||
|
// let server = Server::https("0.0.0.0:443", ssl).unwrap();
|
||||||
|
let server = Server::http("0.0.0.0:8080").unwrap();
|
||||||
|
|
||||||
|
let router = Arc::new(Router::new());
|
||||||
|
let post = Arc::new(PostHandler);
|
||||||
|
router.create_redirect("/", "/home").await;
|
||||||
|
router
|
||||||
|
.create_redirect("/favicon.ico", "/resources/favicon.svg")
|
||||||
|
.await;
|
||||||
|
|
||||||
|
router.create_route("/home", "/hyper-build/home.html").await;
|
||||||
|
router
|
||||||
|
.create_route("/projects", "/hyper-build/projects.html")
|
||||||
|
.await;
|
||||||
|
router
|
||||||
|
.create_route("/about", "hyper-build/about.html")
|
||||||
|
.await;
|
||||||
|
router
|
||||||
|
.create_route(
|
||||||
|
"/projects/http-server",
|
||||||
|
"hyper-build/projects/http-server.html",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
router
|
||||||
|
.create_route(
|
||||||
|
"/projects/html-templating",
|
||||||
|
"hyper-build/projects/html-templating.html",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
router
|
||||||
|
.create_route("/projects/forte", "hyper-build/projects/forte.html")
|
||||||
|
.await;
|
||||||
|
router
|
||||||
|
.create_route("/projects/fishbowl", "hyper-build/projects/fishbowl.html")
|
||||||
|
.await;
|
||||||
|
router
|
||||||
|
.create_route(
|
||||||
|
"/projects/math-interpreter",
|
||||||
|
"hyper-build/projects/math-interpreter.html",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
router
|
||||||
|
.create_route("/projects/nd-range", "hyper-build/projects/nd-range.html")
|
||||||
|
.await;
|
||||||
|
router
|
||||||
|
.create_route(
|
||||||
|
"/projects/fractal-explorer",
|
||||||
|
"hyper-build/projects/fractal-explorer.html",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
router
|
||||||
|
.create_route("/projects/pokedex", "hyper-build/projects/pokedex.html")
|
||||||
|
.await;
|
||||||
|
router
|
||||||
|
.create_route(
|
||||||
|
"/projects/stock-trading",
|
||||||
|
"hyper-build/projects/stocktrading.html",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
for request in server.incoming_requests() {
|
||||||
|
log::info!("{:?} AT {:?}", request.method(), request.url(),);
|
||||||
|
let router = router.clone();
|
||||||
|
let post = post.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
handle_connection(request, router, post).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_connection(
|
||||||
|
mut request: Request,
|
||||||
|
route: Arc<Router>,
|
||||||
|
post: Arc<PostHandler>,
|
||||||
|
) {
|
||||||
|
use tiny_http::*;
|
||||||
|
let response = match request.method() {
|
||||||
|
Method::Get | Method::Head => route.construct_response(request.url()).await,
|
||||||
|
Method::Post => match post.handle_post(&mut request).await {
|
||||||
|
Some(response) => response,
|
||||||
|
None => return,
|
||||||
|
},
|
||||||
|
_ => Response::from_string("Error occurred").with_status_code(405),
|
||||||
|
};
|
||||||
|
let _ = request.respond(response);
|
||||||
|
}
|
27
src/post.rs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
use crate::util::*;
|
||||||
|
|
||||||
|
pub struct PostHandler;
|
||||||
|
|
||||||
|
impl PostHandler {
|
||||||
|
async fn fishbowl_endpoint(&self, file: &[u8]) -> Vec<u8> {
|
||||||
|
fishbowl::make_gif(&file, fishbowl::make_quickdraw().await.unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_post(&self, request: &mut tiny_http::Request) -> Option<Response> {
|
||||||
|
let form = crate::forms::parse_form(request)?;
|
||||||
|
|
||||||
|
match request.url() {
|
||||||
|
"/projects/fishbowl" => {
|
||||||
|
let image = form.get("image")?;
|
||||||
|
let bytes = self.fishbowl_endpoint(&image).await;
|
||||||
|
return Some(Response::from_data(bytes).with_header(header("Content-Type", "image/gif")));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
log::warn!("Unhandled POST at {}", request.url());
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
src/util.rs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use tiny_http::Header;
|
||||||
|
|
||||||
|
pub type Response = tiny_http::Response<std::io::Cursor<Vec<u8>>>;
|
||||||
|
pub fn mime_from_file_ext(url: &str) -> &'static str {
|
||||||
|
let as_path = Path::new(url);
|
||||||
|
match as_path.extension().unwrap_or_default().as_encoded_bytes() {
|
||||||
|
b"css" => "text/css",
|
||||||
|
b"gif" => "image/gif",
|
||||||
|
b"html" | b"htm" => "text/html",
|
||||||
|
b"js" | b"mjs" => "text/javascript",
|
||||||
|
b"svg" => "image/svg+xml",
|
||||||
|
b"woff2" => "font/woff2",
|
||||||
|
b"wasm" => "application/wasm",
|
||||||
|
_ => "application/octet-stream",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn header(key: &str, value: &str) -> Header {
|
||||||
|
Header::from_bytes(key.as_bytes(), value.as_bytes()).unwrap()
|
||||||
|
}
|
5
start.sh
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
#!/bin/bash
|
||||||
|
cargo build --release
|
||||||
|
watchexec -e html,css,js -w hyper-src -w templates -w styles ./html &
|
||||||
|
./target/release/server
|
||||||
|
|
309
styles/extend.css
Normal file
|
@ -0,0 +1,309 @@
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
|