summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLizzy Hunt <loganhunt@simponic.xyz>2023-02-24 15:02:20 -0700
committerGitHub <noreply@github.com>2023-02-24 15:02:20 -0700
commit445af5d0be53375355b9dad02510f6b331fd99ec (patch)
treec9012d8108d3803bbeb8178163210b482188ff3c
parent93c8bbeb6a0b3574cb66c25656e4da6d0b936a66 (diff)
downloadsimponic.xyz-445af5d0be53375355b9dad02510f6b331fd99ec.tar.gz
simponic.xyz-445af5d0be53375355b9dad02510f6b331fd99ec.zip
Euler golf (#1)
-rw-r--r--euler-golf/css/styles.css74
-rw-r--r--euler-golf/index.html74
-rw-r--r--euler-golf/js/controls.js32
-rw-r--r--euler-golf/js/cx.js308
-rw-r--r--euler-golf/js/game.js276
-rw-r--r--euler-golf/js/json-ds.js19
-rw-r--r--euler-golf/js/modal-vanilla.min.js1
-rw-r--r--euler-golf/js/sol.js39
-rw-r--r--index.html100
9 files changed, 891 insertions, 32 deletions
diff --git a/euler-golf/css/styles.css b/euler-golf/css/styles.css
new file mode 100644
index 0000000..e9885c5
--- /dev/null
+++ b/euler-golf/css/styles.css
@@ -0,0 +1,74 @@
+body {
+ margin: 0;
+ padding: 0;
+ overflow: scroll;
+ font-family: Lucida Console, Lucida Sans Typewriter, monaco,
+ Bitstream Vera Sans Mono, monospace;
+ width: 100vw;
+ height: 100vh;
+ background: rgb(238, 174, 202);
+ background: radial-gradient(
+ circle,
+ rgba(238, 174, 202, 1) 0%,
+ rgba(148, 187, 233, 1) 100%
+ );
+}
+
+.canvas {
+ padding: 0;
+ margin: auto;
+ display: block;
+ border: 1px solid black;
+
+ width: 100vw;
+ height: 100vw;
+}
+
+button {
+ border-radius: 5px;
+ padding: 5px;
+ cursor: pointer;
+ margin-left: 5px;
+}
+
+.controls {
+ cursor: pointer;
+ padding: 12px;
+ position: fixed;
+ bottom: 0;
+ right: 0;
+ background-color: rgba(255, 255, 255, 0.8);
+ border: 1px solid white;
+ border-radius: 8px;
+ margin-right: 6px;
+ margin-bottom: 6px;
+}
+
+.buttons {
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+}
+
+.modal {
+ display: flex;
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+
+ width: 80vw;
+ max-width: 500px;
+ min-height: 200px;
+
+ padding: 12px;
+
+ background-color: rgba(255, 255, 255, 0.8);
+ border: 1px solid black;
+ border-radius: 15px;
+}
+
+.modal-body {
+ display: flex;
+ justify-content: center;
+}
diff --git a/euler-golf/index.html b/euler-golf/index.html
new file mode 100644
index 0000000..7679a34
--- /dev/null
+++ b/euler-golf/index.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Euler Golf 2</title>
+ <link rel="stylesheet" type="text/css" href="css/styles.css" />
+ </head>
+ <body>
+ <canvas id="canvas"></canvas>
+
+ <div class="controls" id="controls-container">
+ <div id="controls" style="display: none">
+ <div class="buttons">
+ <button id="reset">Reset</button>
+ <button id="solve">Solve</button>
+ <button id="directions">Directions</button>
+ </div>
+ </div>
+ <span id="expand-show">โ†‘โ†‘</span>
+ </div>
+
+ <div
+ id="directions-modal"
+ class="modal"
+ style="display: none"
+ tabindex="-1"
+ role="dialog"
+ >
+ <button
+ type="button"
+ class="close"
+ data-dismiss="modal"
+ aria-label="Close"
+ >
+ <span aria-hidden="true">X</span>
+ </button>
+
+ <div class="modal-body">
+ <div style="margin: 0; display: inline-block">
+ <h1 style="text-align: center">Euler Golf 2</h1>
+ <p>
+ Use the left and right arrow keys as navigation & hover over the
+ bottom right corner for controls.
+ </p>
+ <p>Rules</p>
+ <ul>
+ <li>
+ Every move consists of a 90 degree rotation around your last
+ position.
+ </li>
+ <li>You begin at the point one unit right from the center.</li>
+ <li>
+ The inital point that you rotate around is the origin (blue).
+ </li>
+ <li>You must navigate to the target point (green).</li>
+ </ul>
+ <p>
+ Initial game by
+ <a href="https://kylehovey.github.io/EulerGolf/">speleo</a>,
+ reimplemented & solved by
+ <a href="https://github.com/Simponic">simponic</a>.
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <script src="js/modal-vanilla.min.js"></script>
+
+ <script src="js/cx.js"></script>
+ <script src="js/json-ds.js"></script>
+ <script src="js/sol.js"></script>
+ <script src="js/game.js"></script>
+ <script src="js/controls.js"></script>
+ </body>
+</html>
diff --git a/euler-golf/js/controls.js b/euler-golf/js/controls.js
new file mode 100644
index 0000000..d89c53d
--- /dev/null
+++ b/euler-golf/js/controls.js
@@ -0,0 +1,32 @@
+const directions_modal = new Modal({
+ el: document.getElementById("directions-modal"),
+});
+
+document
+ .getElementById("controls-container")
+ .addEventListener("mouseover", () => {
+ document.getElementById("controls").style.display = "block";
+ document.getElementById("expand-show").style.display = "none";
+ });
+document
+ .getElementById("controls-container")
+ .addEventListener("mouseout", () => {
+ document.getElementById("controls").style.display = "none";
+ document.getElementById("expand-show").style.display = "inline";
+ });
+
+document.getElementById("reset").addEventListener("click", () => {
+ state = reset_state(state);
+
+ state.target = rand_target(state.rows, state.cols);
+});
+
+document.getElementById("solve").addEventListener("click", () => {
+ if (!cx.eq(state.path.at(-2), new cx(0, 0))) state = reset_state(state);
+
+ state.solution = sol(state.target);
+});
+
+document
+ .getElementById("directions")
+ .addEventListener("click", () => directions_modal.show());
diff --git a/euler-golf/js/cx.js b/euler-golf/js/cx.js
new file mode 100644
index 0000000..371415d
--- /dev/null
+++ b/euler-golf/js/cx.js
@@ -0,0 +1,308 @@
+// http://www.russellcottrell.com/fractalsEtc/cx.js
+
+class cx {
+ static degrees(d) {
+ cx._RD = d ? Math.PI / 180 : 1;
+ }
+ // Math.PI/180 for degrees, 1 for radians
+ // applies to i/o (constructor, get/set arg, and toString etc.)
+
+ constructor(x, y, polar) {
+ if (!polar) {
+ this.re = x;
+ this.im = y;
+ } else {
+ y *= cx._RD; // may be radians or degrees
+ this.re = x * Math.cos(y);
+ this.im = x * Math.sin(y);
+ }
+ }
+
+ get abs() {
+ return Math.sqrt(this.re * this.re + this.im * this.im);
+ }
+
+ set abs(r) {
+ var theta = this._arg;
+ this.re = r * Math.cos(theta);
+ this.im = r * Math.sin(theta);
+ }
+
+ get arg() {
+ // returns radians or degrees, non-negative
+ return (
+ ((Math.atan2(this.im, this.re) + 2 * Math.PI) % (2 * Math.PI)) / cx._RD
+ );
+ }
+
+ set arg(theta) {
+ // may be radians or degrees
+ var r = this.abs;
+ this.re = r * Math.cos(theta * cx._RD);
+ this.im = r * Math.sin(theta * cx._RD);
+ }
+
+ get _arg() {
+ // internal; returns radians
+ return Math.atan2(this.im, this.re);
+ }
+
+ static get i() {
+ return new cx(0, 1);
+ }
+
+ static set i(x) {
+ throw new Error("i is read-only");
+ }
+
+ toString(polar) {
+ if (!polar)
+ return (
+ this.re.toString() +
+ (this.im >= 0 ? " + " : " - ") +
+ Math.abs(this.im).toString() +
+ "i"
+ );
+ else return this.abs.toString() + " cis " + this.arg.toString();
+ }
+
+ toPrecision(n, polar) {
+ if (!polar)
+ return (
+ this.re.toPrecision(n) +
+ (this.im >= 0 ? " + " : " - ") +
+ Math.abs(this.im).toPrecision(n) +
+ "i"
+ );
+ else return this.abs.toPrecision(n) + " cis " + this.arg.toPrecision(n);
+ }
+
+ toPrecis(n, polar) {
+ // trims trailing zeros
+ if (!polar)
+ return (
+ parseFloat(this.re.toPrecision(n)).toString() +
+ (this.im >= 0 ? " + " : " - ") +
+ parseFloat(Math.abs(this.im).toPrecision(n)).toString() +
+ "i"
+ );
+ else
+ return (
+ parseFloat(this.abs.toPrecision(n)).toString() +
+ " cis " +
+ parseFloat(this.arg.toPrecision(n)).toString()
+ );
+ }
+
+ toFixed(n, polar) {
+ if (!polar)
+ return (
+ this.re.toFixed(n) +
+ (this.im >= 0 ? " + " : " - ") +
+ Math.abs(this.im).toFixed(n) +
+ "i"
+ );
+ else return this.abs.toFixed(n) + " cis " + this.arg.toFixed(n);
+ }
+
+ toExponential(n, polar) {
+ if (!polar)
+ return (
+ this.re.toExponential(n) +
+ (this.im >= 0 ? " + " : " - ") +
+ Math.abs(this.im).toExponential(n) +
+ "i"
+ );
+ else return this.abs.toExponential(n) + " cis " + this.arg.toExponential(n);
+ }
+
+ static getReals(c, d) {
+ // when c or d may be simple or complex
+ var x, y, u, v;
+ if (c instanceof cx) {
+ x = c.re;
+ y = c.im;
+ } else {
+ x = c;
+ y = 0;
+ }
+ if (d instanceof cx) {
+ u = d.re;
+ v = d.im;
+ } else {
+ u = d;
+ v = 0;
+ }
+ return [x, y, u, v];
+ }
+
+ static conj(c) {
+ return new cx(c.re, -c.im);
+ }
+
+ static neg(c) {
+ return new cx(-c.re, -c.im);
+ }
+
+ static add(c, d) {
+ var a = cx.getReals(c, d);
+ var x = a[0];
+ var y = a[1];
+ var u = a[2];
+ var v = a[3];
+ return new cx(x + u, y + v);
+ }
+
+ static sub(c, d) {
+ var a = cx.getReals(c, d);
+ var x = a[0];
+ var y = a[1];
+ var u = a[2];
+ var v = a[3];
+ return new cx(x - u, y - v);
+ }
+
+ static mult(c, d) {
+ var a = cx.getReals(c, d);
+ var x = a[0];
+ var y = a[1];
+ var u = a[2];
+ var v = a[3];
+ return new cx(x * u - y * v, x * v + y * u);
+ }
+
+ static div(c, d) {
+ var a = cx.getReals(c, d);
+ var x = a[0];
+ var y = a[1];
+ var u = a[2];
+ var v = a[3];
+ return new cx(
+ (x * u + y * v) / (u * u + v * v),
+ (y * u - x * v) / (u * u + v * v)
+ );
+ }
+
+ static pow(c, int) {
+ if (Number.isInteger(int) && int >= 0) {
+ var r = Math.pow(c.abs, int);
+ var theta = int * c._arg;
+ return new cx(r * Math.cos(theta), r * Math.sin(theta));
+ } else return NaN;
+ }
+
+ static root(c, int, k) {
+ if (!k) k = 0;
+ if (
+ Number.isInteger(int) &&
+ int >= 2 &&
+ Number.isInteger(k) &&
+ k >= 0 &&
+ k < int
+ ) {
+ var r = Math.pow(c.abs, 1 / int);
+ var theta = (c._arg + 2 * k * Math.PI) / int;
+ return new cx(r * Math.cos(theta), r * Math.sin(theta));
+ } else return NaN;
+ }
+
+ static log(c) {
+ return new cx(Math.log(c.abs), c._arg);
+ }
+
+ static exp(c) {
+ var r = Math.exp(c.re);
+ var theta = c.im;
+ return new cx(r * Math.cos(theta), r * Math.sin(theta));
+ }
+
+ static sin(c) {
+ var a = c.re;
+ var b = c.im;
+ return new cx(Math.sin(a) * Math.cosh(b), Math.cos(a) * Math.sinh(b));
+ }
+
+ static cos(c) {
+ var a = c.re;
+ var b = c.im;
+ return new cx(Math.cos(a) * Math.cosh(b), -Math.sin(a) * Math.sinh(b));
+ }
+
+ static tan(c) {
+ return cx.div(cx.sin(c), cx.cos(c));
+ }
+
+ static asin(c, k) {
+ if (!k) k = 0;
+ var ic = cx.mult(cx.i, c);
+ var c2 = cx.pow(c, 2);
+ return cx.mult(
+ cx.neg(cx.i),
+ cx.log(cx.add(ic, cx.root(cx.sub(1, c2), 2, k)))
+ );
+ }
+
+ static acos(c, k) {
+ if (!k) k = 0;
+ var c2 = cx.pow(c, 2);
+ return cx.mult(
+ cx.neg(cx.i),
+ cx.log(cx.add(c, cx.mult(cx.i, cx.root(cx.sub(1, c2), 2, k))))
+ );
+ }
+
+ static atan(c) {
+ return cx.mult(
+ cx.div(cx.i, 2),
+ cx.log(cx.div(cx.add(cx.i, c), cx.sub(cx.i, c)))
+ );
+ }
+
+ static sinh(c) {
+ var a = c.re;
+ var b = c.im;
+ return new cx(Math.sinh(a) * Math.cos(b), Math.cosh(a) * Math.sin(b));
+ }
+
+ static cosh(c) {
+ var a = c.re;
+ var b = c.im;
+ return new cx(Math.cosh(a) * Math.cos(b), Math.sinh(a) * Math.sin(b));
+ }
+
+ static tanh(c) {
+ return cx.div(cx.sinh(c), cx.cosh(c));
+ }
+
+ static asinh(c, k) {
+ if (!k) k = 0;
+ var c2 = cx.pow(c, 2);
+ return cx.log(cx.add(c, cx.root(cx.add(c2, 1), 2, k)));
+ }
+
+ static acosh(c, k) {
+ if (!k) k = 0;
+ var c2 = cx.pow(c, 2);
+ return cx.log(cx.add(c, cx.root(cx.sub(c2, 1), 2, k)));
+ }
+
+ static atanh(c) {
+ return cx.mult(cx.div(1, 2), cx.log(cx.div(cx.add(1, c), cx.sub(1, c))));
+ }
+
+ static copy(c) {
+ return new cx(c.re, c.im);
+ }
+
+ static eq(c, d, epsilon) {
+ if (!epsilon) {
+ if (c.re == d.re && c.im == d.im) return true;
+ } else {
+ if (Math.abs(c.re - d.re) < epsilon && Math.abs(c.im - d.im) < epsilon)
+ return true;
+ }
+ return false;
+ }
+}
+
+cx.degrees(true); // need to call this
diff --git a/euler-golf/js/game.js b/euler-golf/js/game.js
new file mode 100644
index 0000000..68fca94
--- /dev/null
+++ b/euler-golf/js/game.js
@@ -0,0 +1,276 @@
+const DEFAULTS = {
+ max_rows: 80,
+ max_cols: 80,
+ min_gap: 30,
+ angle_multiplier: 10e-4,
+};
+const CANVAS = document.getElementById("canvas");
+
+let state = {
+ grid_padding: 30,
+ canvas: CANVAS,
+ ctx: CANVAS.getContext("2d"),
+ last_render: 0,
+ keys: {},
+ changes: {},
+};
+
+// Rendering
+CanvasRenderingContext2D.prototype.circle = function (x, y, r, color) {
+ this.beginPath();
+ this.arc(x, y, r, 0, Math.PI * 2);
+ this.fillStyle = color;
+ this.fill();
+ this.closePath();
+};
+
+CanvasRenderingContext2D.prototype.line = function (
+ { x_pos: x1, y_pos: y1 },
+ { x_pos: x2, y_pos: y2 },
+ width,
+ color,
+ cap = "round"
+) {
+ this.lineWidth = width;
+ this.strokeStyle = color;
+ this.lineCap = cap;
+
+ this.beginPath();
+ this.moveTo(x1, y1);
+ this.lineTo(x2, y2);
+ this.stroke();
+ this.closePath();
+};
+
+CanvasRenderingContext2D.prototype.draw_cartesian_path = function (
+ grid_spec,
+ cartesian_path,
+ width = 2,
+ color = "#000"
+) {
+ const path = cartesian_path.map((coord) => grid_to_canvas(coord, grid_spec));
+ path.slice(1).forEach((coord, i) => {
+ this.line(path[i], coord, width, color);
+ });
+};
+
+CanvasRenderingContext2D.prototype.do_grid = function (
+ rows,
+ cols,
+ draw_at_grid_pos = (ctx, x, y) => ctx.circle(x, y, 10, "#44ff44")
+) {
+ for (let y = 0; y < rows; y++) {
+ for (let x = 0; x < cols; x++) {
+ draw_at_grid_pos(this, x, y);
+ }
+ }
+};
+
+CanvasRenderingContext2D.prototype.cartesian_grid = function (
+ rows,
+ cols,
+ grid_spec,
+ circle_spec_at_coords = (_x, _y) => ({ radius: 5, color: "#000" })
+) {
+ this.do_grid(rows, cols, (ctx, x, y) => {
+ const { x_pos, y_pos } = grid_to_canvas({ x, y }, grid_spec);
+ const { radius, color } = circle_spec_at_coords(x, y);
+
+ ctx.circle(x_pos, y_pos, radius, color);
+ });
+};
+
+// Utilities
+const move = (prev, curr, c) => cx.add(prev, cx.mult(c, cx.sub(curr, prev)));
+
+const rand_between = (min, max) =>
+ Math.floor(Math.random() * (max - min + 1)) + min;
+
+const rand_target = (rows, cols) => {
+ const r = Math.floor(rows / 2);
+ const c = Math.floor(cols / 2);
+ const res = new cx(rand_between(-c, c), rand_between(-r, r));
+ if (!sol(res)) return rand_target(rows, cols);
+
+ return res;
+};
+
+const calculate_grid_spec = ({ rows, cols, width, height, grid_padding }) => {
+ const dx = (width - 2 * grid_padding) / cols;
+ const dy = (height - 2 * grid_padding) / rows;
+
+ return {
+ dx,
+ dy,
+ start_x: grid_padding + dx / 2,
+ start_y: grid_padding + dy / 2,
+ };
+};
+
+const grid_to_canvas = ({ x, y }, { dx, dy, start_x, start_y }) => ({
+ x_pos: x * dx + start_x,
+ y_pos: y * dy + start_y,
+});
+
+const complex_to_grid = (c, rows, cols) => {
+ const { re, im } = c;
+ return {
+ x: re + Math.floor(cols / 2),
+ y: Math.floor(rows / 2) - im,
+ };
+};
+
+// Game loop
+
+const maybe_add_state_angle_move = ({ angle } = state) => {
+ if (angle.im <= -1 || angle.im >= 1) {
+ angle.im = angle.im <= -1 ? -1 : 1;
+ state.path.push(move(state.path.at(-2), state.path.at(-1), angle));
+ state.angle = new cx(0, 0);
+ }
+ return state;
+};
+
+const handle_input = (state, dt) => {
+ if (state.keys.ArrowLeft) {
+ state.angle.im += DEFAULTS.angle_multiplier * dt;
+ } else if (state.keys.ArrowRight) {
+ state.angle.im -= DEFAULTS.angle_multiplier * dt;
+ }
+ state = maybe_add_state_angle_move(state);
+};
+
+const render = ({ width, height, ctx, rows, cols, target } = state) => {
+ ctx.clearRect(0, 0, width, height);
+ ctx.fillStyle = "rgba(0, 0, 0, 0)";
+ ctx.fillRect(0, 0, width, height);
+
+ const grid_spec = calculate_grid_spec(state);
+
+ const curr = state.path.at(-1);
+ const prev = state.path.at(-2);
+
+ const v_diff = cx.sub(curr, prev);
+ const theta = (state.angle.im * Math.PI) / 2;
+
+ const angle_re = Math.cos(theta) * v_diff.re - Math.sin(theta) * v_diff.im;
+ const angle_im = Math.sin(theta) * v_diff.re + Math.cos(theta) * v_diff.im;
+
+ ctx.draw_cartesian_path(grid_spec, [
+ ...state.path.map((c) => complex_to_grid(c, rows, cols)),
+ complex_to_grid(cx.add(new cx(angle_re, angle_im), prev), rows, cols),
+ ]);
+
+ if (!(state.angle.im == state.angle.re && state.angle.re == 0)) {
+ // Draw path to next player's target
+ const [a, b] = [
+ curr,
+ move(prev, curr, new cx(0, state.angle.im < 0 ? -1 : 1)),
+ ].map((c) => grid_to_canvas(complex_to_grid(c, rows, cols), grid_spec));
+
+ ctx.line(a, b, 6, "#aaa");
+ }
+
+ const grid_target = complex_to_grid(target, rows, cols);
+
+ ctx.cartesian_grid(rows, cols, grid_spec, (x, y) => {
+ if (x == Math.floor(cols / 2) && y == Math.floor(rows / 2)) {
+ return {
+ radius: 7,
+ color: "#2f9c94",
+ };
+ } else if (x == grid_target.x && y == grid_target.y) {
+ return {
+ radius: 8,
+ color: "#00ff00",
+ };
+ } else {
+ return {
+ radius: 3,
+ color: `rgb(${255 * (x / cols)}, 100, 100)`, // todo: animate with last_render
+ };
+ }
+ });
+};
+
+const loop = (now) => {
+ const dt = now - state.last_render;
+ state.changes.last_render = now;
+
+ if (Object.keys(state.changes).length > 0) {
+ if (state.changes.width || state.changes.height) {
+ state.changes.rows = Math.floor(
+ Math.min(DEFAULTS.max_rows, state.changes.height / DEFAULTS.min_gap)
+ );
+ state.changes.cols = Math.floor(
+ Math.min(DEFAULTS.max_cols, state.changes.width / DEFAULTS.min_gap)
+ );
+ }
+
+ state = { ...state, ...state.changes };
+
+ state.changes = {};
+ }
+
+ if (!state.target) {
+ state.target = rand_target(state.rows, state.cols);
+ }
+
+ if (!state.solution) {
+ handle_input(state, dt);
+ } else {
+ if (!state?.solution.length) {
+ delete state.solution;
+ } else {
+ state.angle.im +=
+ (state.solution[0] === "-" ? 1 : -1) * DEFAULTS.angle_multiplier * dt;
+
+ state = maybe_add_state_angle_move(state);
+
+ if (cx.eq(state.angle, new cx(0, 0))) state.solution.shift();
+ }
+ }
+ render(state);
+ requestAnimationFrame(loop);
+};
+
+const reset_state = ({ rows, cols } = state) => ({
+ ...state,
+ solution: null,
+ path: [new cx(0, 0), new cx(1, 0)],
+ angle: new cx(0, 0),
+});
+
+// DOM
+const on_resize = () => {
+ CANVAS.width = document.body.clientWidth;
+ CANVAS.height = document.body.clientHeight;
+ state.changes.width = CANVAS.width;
+ state.changes.height = CANVAS.height;
+};
+
+const on_keyup = (e) => {
+ delete state.keys[e.key];
+};
+
+const on_keydown = (e) => {
+ state.keys[e.key] = true;
+};
+
+window.addEventListener("resize", on_resize);
+window.addEventListener("keydown", on_keydown);
+window.addEventListener("keyup", on_keyup);
+
+// main
+on_resize();
+state = reset_state(state);
+
+if (!sessionStorage.getItem("seen-instructions")) {
+ new Modal({
+ el: document.getElementById("directions-modal"),
+ }).show();
+
+ sessionStorage.setItem("seen-instructions", true);
+}
+
+requestAnimationFrame(loop);
diff --git a/euler-golf/js/json-ds.js b/euler-golf/js/json-ds.js
new file mode 100644
index 0000000..dc7e88e
--- /dev/null
+++ b/euler-golf/js/json-ds.js
@@ -0,0 +1,19 @@
+class JSONSet {
+ items = new Set();
+
+ constructor(initial) {
+ if (Array.isArray(initial)) {
+ initial.map((x) => this.apply_set_function("add", x));
+ } else {
+ this.apply_set_function("add", initial);
+ }
+
+ ["add", "has", "remove"].forEach(
+ (f_name) => (this[f_name] = (x) => this.apply_set_function(f_name, x))
+ );
+ }
+
+ apply_set_function(f_name, x) {
+ return this.items[f_name](JSON.stringify(x));
+ }
+}
diff --git a/euler-golf/js/modal-vanilla.min.js b/euler-golf/js/modal-vanilla.min.js
new file mode 100644
index 0000000..0d314c7
--- /dev/null
+++ b/euler-golf/js/modal-vanilla.min.js
@@ -0,0 +1 @@
+var Modal=function(e){function t(i){if(n[i])return n[i].exports;var o=n[i]={i:i,l:!1,exports:{}};return e[i].call(o.exports,o,o.exports,t),o.l=!0,o.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,i){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:i})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=0)}([function(e,t,n){e.exports=n(1).default},function(e,t,n){"use strict";function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function r(e){for(var t in e)Array.isArray(e[t])?e[t].forEach(function(e){r(e)}):null!==e[t]&&"object"===p(e[t])&&Object.freeze(e[t]);return Object.freeze(e)}function a(){return(65536*(1+Math.random())|0).toString(16)+(65536*(1+Math.random())|0).toString(16)}function l(e,t,n){var i=e.data||{};if(void 0===n){if(e.data&&e.data[t])return e.data[t];var o=e.getAttribute("data-"+t);return void 0!==o?o:null}return i[t]=n,e.data=i,e}function d(e,t){return e.nodeName?e:(e=e.replace(/(\t|\n$)/g,""),_||(_=document.createElement("div")),_.innerHTML="",_.innerHTML=e,!0===t?_.childNodes:_.childNodes[0])}function c(){var e=void 0,t=void 0,n=void 0,i=document.createElement("div");return v(i.style,{visibility:"hidden",width:"100px"}),document.body.appendChild(i),n=i.offsetWidth,i.style.overflow="scroll",e=document.createElement("div"),e.style.width="100%",i.appendChild(e),t=n-e.offsetWidth,document.body.removeChild(i),t}function h(e){for(var t=[e];e.parentNode;)e=e.parentNode,t.push(e);return t}Object.defineProperty(t,"__esModule",{value:!0});var u=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n];i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),v=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e},p="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},f=n(2),m=function(e){return e&&e.__esModule?e:{default:e}}(f),_=null,y=Object.freeze({el:null,animate:!0,animateClass:"fade",animateInClass:"show",appendTo:"body",backdrop:!0,keyboard:!0,title:!1,header:!0,content:!1,footer:!0,buttons:null,headerClose:!0,construct:!1,transition:300,backdropTransition:150}),b=r({dialog:[{text:"Cancel",value:!1,attr:{class:"btn btn-default","data-dismiss":"modal"}},{text:"OK",value:!0,attr:{class:"btn btn-primary","data-dismiss":"modal"}}],alert:[{text:"OK",attr:{class:"btn btn-primary","data-dismiss":"modal"}}],confirm:[{text:"Cancel",value:!1,attr:{class:"btn btn-default","data-dismiss":"modal"}},{text:"OK",value:!0,attr:{class:"btn btn-primary","data-dismiss":"modal"}}]}),g={container:'<div class="modal"></div>',dialog:'<div class="modal-dialog"></div>',content:'<div class="modal-content"></div>',header:'<div class="modal-header"></div>',headerClose:'<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">ร—</span></button>',body:'<div class="modal-body"></div>',footer:'<div class="modal-footer"></div>',backdrop:'<div class="modal-backdrop"></div>'},k=function(e){function t(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};i(this,t);var n=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this));if(n.id=a(),n.el=null,n._html={},n._events={},n._visible=!1,n._pointerInContent=!1,n._options=v({},t.options,e),n._templates=v({},t.templates,e.templates||{}),n._html.appendTo=document.querySelector(n._options.appendTo),n._scrollbarWidth=c(),null===n._options.buttons&&(n._options.buttons=t.buttons.dialog),n._options.el){var s=n._options.el;if("string"==typeof n._options.el&&!(s=document.querySelector(n._options.el)))throw new Error("Selector: DOM Element "+n._options.el+" not found.");l(s,"modal",n),n.el=s}else n._options.construct=!0;return n._options.construct?n._render():n._mapDom(),n}return s(t,e),u(t,null,[{key:"alert",value:function(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return new t(v({},y,{title:e,content:!1,construct:!0,headerClose:!1,buttons:t.buttons.alert},n))}},{key:"confirm",value:function(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return new t(v({},y,{title:e,content:!1,construct:!0,headerClose:!1,buttons:t.buttons.confirm},n))}},{key:"templates",set:function(e){this._baseTemplates=e},get:function(){return v({},g,t._baseTemplates||{})}},{key:"buttons",set:function(e){this._baseButtons=e},get:function(){return v({},b,t._baseButtons||{})}},{key:"options",set:function(e){this._baseOptions=e},get:function(){return v({},y,t._baseOptions||{})}},{key:"version",get:function(){return"0.12.0"}}]),u(t,[{key:"_render",value:function(){var e=this._html,t=this._options,n=this._templates,i=!!t.animate&&t.animateClass;return e.container=d(n.container),e.dialog=d(n.dialog),e.content=d(n.content),e.header=d(n.header),e.headerClose=d(n.headerClose),e.body=d(n.body),e.footer=d(n.footer),i&&e.container.classList.add(i),this._setHeader(),this._setContent(),this._setFooter(),this.el=e.container,e.dialog.appendChild(e.content),e.container.appendChild(e.dialog),this}},{key:"_mapDom",value:function(){var e=this._html,t=this._options;return this.el.classList.contains(t.animateClass)&&(t.animate=!0),e.container=this.el,e.dialog=this.el.querySelector(".modal-dialog"),e.content=this.el.querySelector(".modal-content"),e.header=this.el.querySelector(".modal-header"),e.headerClose=this.el.querySelector(".modal-header .close"),e.body=this.el.querySelector(".modal-body"),e.footer=this.el.querySelector(".modal-footer"),this._setHeader(),this._setContent(),this._setFooter(),this}},{key:"_setHeader",value:function(){var e=this._html,t=this._options;t.header&&e.header&&(t.title.nodeName?e.header.innerHTML=t.title.outerHTML:"string"==typeof t.title&&(e.header.innerHTML='<h4 class="modal-title">'+t.title+"</h4>"),null===this.el&&e.headerClose&&t.headerClose&&e.header.appendChild(e.headerClose),t.construct&&e.content.appendChild(e.header))}},{key:"_setContent",value:function(){var e=this._html,t=this._options;t.content&&e.body&&("string"==typeof t.content?e.body.innerHTML=t.content:e.body.innerHTML=t.content.outerHTML,t.construct&&e.content.appendChild(e.body))}},{key:"_setFooter",value:function(){var e=this._html,t=this._options;t.footer&&e.footer&&(t.footer.nodeName?e.footer.ineerHTML=t.footer.outerHTML:"string"==typeof t.footer?e.footer.innerHTML=t.footer:e.footer.children.length||t.buttons.forEach(function(t){var n=document.createElement("button");l(n,"button",t),n.innerHTML=t.text,n.setAttribute("type","button");for(var i in t.attr)n.setAttribute(i,t.attr[i]);e.footer.appendChild(n)}),t.construct&&e.content.appendChild(e.footer))}},{key:"_setEvents",value:function(){var e=(this._options,this._html);this._events.keydownHandler=this._handleKeydownEvent.bind(this),document.body.addEventListener("keydown",this._events.keydownHandler),this._events.mousedownHandler=this._handleMousedownEvent.bind(this),e.container.addEventListener("mousedown",this._events.mousedownHandler),this._events.clickHandler=this._handleClickEvent.bind(this),e.container.addEventListener("click",this._events.clickHandler),this._events.resizeHandler=this._handleResizeEvent.bind(this),window.addEventListener("resize",this._events.resizeHandler)}},{key:"_handleMousedownEvent",value:function(e){var t=this;this._pointerInContent=!1,h(e.target).every(function(e){return!e.classList||!e.classList.contains("modal-content")||(t._pointerInContent=!0,!1)})}},{key:"_handleClickEvent",value:function(e){var t=this;h(e.target).every(function(n){return!("HTML"===n.tagName||!0!==t._options.backdrop&&n.classList.contains("modal")||n.classList.contains("modal-content")||("modal"===n.getAttribute("data-dismiss")?(t.emit("dismiss",t,e,l(e.target,"button")),t.hide(),1):!t._pointerInContent&&n.classList.contains("modal")&&(t.emit("dismiss",t,e,null),t.hide(),1)))}),this._pointerInContent=!1}},{key:"_handleKeydownEvent",value:function(e){27===e.which&&this._options.keyboard&&(this.emit("dismiss",this,e,null),this.hide())}},{key:"_handleResizeEvent",value:function(e){this._resize()}},{key:"show",value:function(){var e=this,t=this._options,n=this._html;return this.emit("show",this),this._checkScrollbar(),this._setScrollbar(),document.body.classList.add("modal-open"),t.construct&&n.appendTo.appendChild(n.container),n.container.style.display="block",n.container.scrollTop=0,!1!==t.backdrop?(this.once("showBackdrop",function(){e._setEvents(),t.animate&&n.container.offsetWidth,n.container.classList.add(t.animateInClass),setTimeout(function(){e._visible=!0,e.emit("shown",e)},t.transition)}),this._backdrop()):(this._setEvents(),t.animate&&n.container.offsetWidth,n.container.classList.add(t.animateInClass),setTimeout(function(){e._visible=!0,e.emit("shown",e)},t.transition)),this._resize(),this}},{key:"toggle",value:function(){this._visible?this.hide():this.show()}},{key:"_resize",value:function(){var e=this._html.container.scrollHeight>document.documentElement.clientHeight;this._html.container.style.paddingLeft=!this.bodyIsOverflowing&&e?this._scrollbarWidth+"px":"",this._html.container.style.paddingRight=this.bodyIsOverflowing&&!e?this._scrollbarWidth+"px":""}},{key:"_backdrop",value:function(){var e=this,t=this._html,n=this._templates,i=this._options,o=!!i.animate&&i.animateClass;t.backdrop=d(n.backdrop),o&&t.backdrop.classList.add(o),t.appendTo.appendChild(t.backdrop),o&&t.backdrop.offsetWidth,t.backdrop.classList.add(i.animateInClass),setTimeout(function(){e.emit("showBackdrop",e)},this._options.backdropTransition)}},{key:"hide",value:function(){var e=this,t=this._html,n=this._options,i=t.container.classList;if(this.emit("hide",this),i.remove(n.animateInClass),n.backdrop){t.backdrop.classList.remove(n.animateInClass)}return this._removeEvents(),setTimeout(function(){document.body.classList.remove("modal-open"),document.body.style.paddingRight=e.originalBodyPad},n.backdropTransition),setTimeout(function(){n.backdrop&&t.backdrop.parentNode.removeChild(t.backdrop),t.container.style.display="none",n.construct&&t.container.parentNode.removeChild(t.container),e._visible=!1,e.emit("hidden",e)},n.transition),this}},{key:"_removeEvents",value:function(){this._events.keydownHandler&&document.body.removeEventListener("keydown",this._events.keydownHandler),this._html.container.removeEventListener("mousedown",this._events.mousedownHandler),this._html.container.removeEventListener("click",this._events.clickHandler),window.removeEventListener("resize",this._events.resizeHandler)}},{key:"_checkScrollbar",value:function(){this.bodyIsOverflowing=document.body.clientWidth<window.innerWidth}},{key:"_setScrollbar",value:function(){if(this.originalBodyPad=document.body.style.paddingRight||"",this.bodyIsOverflowing){var e=parseInt(this.originalBodyPad||0,10);document.body.style.paddingRight=e+this._scrollbarWidth+"px"}}}]),t}(m.default);t.default=k},function(e,t){function n(){this._events=this._events||{},this._maxListeners=this._maxListeners||void 0}function i(e){return"function"==typeof e}function o(e){return"number"==typeof e}function s(e){return"object"==typeof e&&null!==e}function r(e){return void 0===e}e.exports=n,n.EventEmitter=n,n.prototype._events=void 0,n.prototype._maxListeners=void 0,n.defaultMaxListeners=10,n.prototype.setMaxListeners=function(e){if(!o(e)||e<0||isNaN(e))throw TypeError("n must be a positive number");return this._maxListeners=e,this},n.prototype.emit=function(e){var t,n,o,a,l,d;if(this._events||(this._events={}),"error"===e&&(!this._events.error||s(this._events.error)&&!this._events.error.length)){if((t=arguments[1])instanceof Error)throw t;var c=new Error('Uncaught, unspecified "error" event. ('+t+")");throw c.context=t,c}if(n=this._events[e],r(n))return!1;if(i(n))switch(arguments.length){case 1:n.call(this);break;case 2:n.call(this,arguments[1]);break;case 3:n.call(this,arguments[1],arguments[2]);break;default:a=Array.prototype.slice.call(arguments,1),n.apply(this,a)}else if(s(n))for(a=Array.prototype.slice.call(arguments,1),d=n.slice(),o=d.length,l=0;l<o;l++)d[l].apply(this,a);return!0},n.prototype.addListener=function(e,t){var o;if(!i(t))throw TypeError("listener must be a function");return this._events||(this._events={}),this._events.newListener&&this.emit("newListener",e,i(t.listener)?t.listener:t),this._events[e]?s(this._events[e])?this._events[e].push(t):this._events[e]=[this._events[e],t]:this._events[e]=t,s(this._events[e])&&!this._events[e].warned&&(o=r(this._maxListeners)?n.defaultMaxListeners:this._maxListeners)&&o>0&&this._events[e].length>o&&(this._events[e].warned=!0,console.error("(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.",this._events[e].length),"function"==typeof console.trace&&console.trace()),this},n.prototype.on=n.prototype.addListener,n.prototype.once=function(e,t){function n(){this.removeListener(e,n),o||(o=!0,t.apply(this,arguments))}if(!i(t))throw TypeError("listener must be a function");var o=!1;return n.listener=t,this.on(e,n),this},n.prototype.removeListener=function(e,t){var n,o,r,a;if(!i(t))throw TypeError("listener must be a function");if(!this._events||!this._events[e])return this;if(n=this._events[e],r=n.length,o=-1,n===t||i(n.listener)&&n.listener===t)delete this._events[e],this._events.removeListener&&this.emit("removeListener",e,t);else if(s(n)){for(a=r;a-- >0;)if(n[a]===t||n[a].listener&&n[a].listener===t){o=a;break}if(o<0)return this;1===n.length?(n.length=0,delete this._events[e]):n.splice(o,1),this._events.removeListener&&this.emit("removeListener",e,t)}return this},n.prototype.removeAllListeners=function(e){var t,n;if(!this._events)return this;if(!this._events.removeListener)return 0===arguments.length?this._events={}:this._events[e]&&delete this._events[e],this;if(0===arguments.length){for(t in this._events)"removeListener"!==t&&this.removeAllListeners(t);return this.removeAllListeners("removeListener"),this._events={},this}if(n=this._events[e],i(n))this.removeListener(e,n);else if(n)for(;n.length;)this.removeListener(e,n[n.length-1]);return delete this._events[e],this},n.prototype.listeners=function(e){return this._events&&this._events[e]?i(this._events[e])?[this._events[e]]:this._events[e].slice():[]},n.prototype.listenerCount=function(e){if(this._events){var t=this._events[e];if(i(t))return 1;if(t)return t.length}return 0},n.listenerCount=function(e,t){return e.listenerCount(t)}}]);
diff --git a/euler-golf/js/sol.js b/euler-golf/js/sol.js
new file mode 100644
index 0000000..bc403fc
--- /dev/null
+++ b/euler-golf/js/sol.js
@@ -0,0 +1,39 @@
+const DEPTH = 15;
+
+const DIRECTION = {
+ 0: new cx(0, 1),
+ 1: new cx(0, -1),
+};
+
+const construct_moves = (curr, prev) =>
+ Object.keys(DIRECTION).map((x) => move(curr, prev, DIRECTION[x]));
+
+const backtrack = (local_index, depth) =>
+ local_index
+ .toString(2)
+ .padStart(depth, "0")
+ .split("")
+ .map((direction) => (Number(direction) ? "+" : "-"));
+
+const sol = (target, start_from = new cx(0, 0), start_to = new cx(1, 0)) => {
+ let moves = [start_to, ...construct_moves(start_from, start_to)];
+ let curr_depth = 2;
+
+ while (curr_depth < DEPTH) {
+ for (let i = 0; i < Math.pow(2, curr_depth); i++) {
+ const direction = DIRECTION[Number(i.toString(2).at(-1))];
+ // Current element is at i >> 1 + the offset for the previous group (which is
+ // the sum of the geometric series 2**n until curr_depth - 1)
+ const current_i = (i >> 1) + (1 - Math.pow(2, curr_depth - 1)) / (1 - 2);
+ const previous_i = (i >> 2) + (1 - Math.pow(2, curr_depth - 2)) / (1 - 2);
+
+ const new_move = move(moves[previous_i], moves[current_i], direction);
+
+ moves.push(new_move);
+ if (cx.eq(new_move, target)) return backtrack(i, curr_depth);
+ }
+ curr_depth++;
+ }
+
+ return null;
+};
diff --git a/index.html b/index.html
index 3eb01e3..0381a91 100644
--- a/index.html
+++ b/index.html
@@ -3,78 +3,114 @@
<html>
<head>
<title>Simponic's Static Sites</title>
- <link href="css/styles.css" rel="stylesheet">
+ <link href="css/styles.css" rel="stylesheet" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"
/>
- <script src="https://kit.fontawesome.com/d7e97ed48f.js" crossorigin="anonymous"></script>
-
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <script
+ src="https://kit.fontawesome.com/d7e97ed48f.js"
+ crossorigin="anonymous"
+ ></script>
+
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<div class="top-container animate__animated animate__fadeIn">
- <img src="images/profile.png" class="profile-picture">
+ <img src="images/profile.png" class="profile-picture" />
<h3>
- ๐Ÿ‘‹ Hello, I'm Simponic!
- <br>
- ๐Ÿ“– This page hosts strictly static content.
- <br>
- ๐Ÿ”” My "real website" is at <a href="https://simponic.xyz">simponic.xyz</a>.
+ ๐Ÿ‘‹ Hello, I'm Simponic!
+ <br />
+ ๐Ÿ“– This page hosts strictly static content.
+ <br />
+ ๐Ÿ”” My "real website" is at
+ <a href="https://simponic.xyz">simponic.xyz</a>.
</h3>
<div class="projects-grid">
- <div class="project" onclick="window.location='dvd-logo/index.html'">
+ <div class="project" onclick="window.location='euler-golf/index.html'">
<div class="project-logo-container">
- <i class="fa-solid fa-compact-disc"></i>
+ <i class="fa-solid fa-golf-ball-tee"></i>
</div>
<div class="project-body">
- <h1>DVD Logo Bouncing Animation</h1>
- <p>Brings back the nostalgia of old-school DVD players with an intersection predictor. The twist: no Canvas API! Only svg's and absolute positioned images!</p>
+ <h1>Euler Golf 2</h1>
+ <p>
+ A puzzle game (with solver) to explore rotations in the complex
+ plane.
+ </p>
</div>
</div>
- <div class="project" onclick="window.location='maize-maze/index.html'">
+ <div class="project" onclick="window.location='julia/index.html'">
<div class="project-logo-container">
- <i class="fa-solid fa-diagram-project"></i>
+ <i class="fa-solid fa-square-root-variable"></i>
</div>
- <div class="project-body" >
- <h1>The A-maze-ing Maize Maze</h1>
- <p>A Randomized Kruskal's Maze game with BFS path-finding. You play as a ๐ŸŒฝcorn stalk trying to become ๐Ÿฟpopcorn.</p>
+ <div class="project-body">
+ <h1>Julia Set Explorer</h1>
+ <p>
+ Zoom, pan, and "c" complex changes in this fun GPU-accelerated
+ playground!
+ </p>
</div>
</div>
- <div class="project" onclick="window.location='centipede/index.html'">
+ <div
+ class="project"
+ onclick="window.location='ft-visualizer/index.html'"
+ >
<div class="project-logo-container">
- <i class="fa-solid fa-mosquito"></i>
+ <i class="fa-solid fa-wave-square"></i>
</div>
- <div class="project-body" >
- <h1>Centipede</h1>
- <p>In this game, shoot all the centipede bodies and score points and go up levels.</p>
+ <div class="project-body">
+ <h1>Discrete Fourier Visualizer</h1>
+ <p>
+ Draw how your year has gone and view a reactive graph containing
+ its DFT by dragging your mouse over the canvas!
+ </p>
</div>
</div>
+ <div class="project" onclick="window.location='maize-maze/index.html'">
+ <div class="project-logo-container">
+ <i class="fa-solid fa-diagram-project"></i>
+ </div>
- <div class="project" onclick="window.location='julia/index.html'">
+ <div class="project-body">
+ <h1>The A-maze-ing Maize Maze</h1>
+ <p>
+ A Randomized Kruskal's Maze game with BFS path-finding. You play
+ as a ๐ŸŒฝcorn stalk trying to become ๐Ÿฟpopcorn.
+ </p>
+ </div>
+ </div>
+
+ <div class="project" onclick="window.location='centipede/index.html'">
<div class="project-logo-container">
- <i class="fa-solid fa-square-root-variable"></i>
+ <i class="fa-solid fa-mosquito"></i>
</div>
<div class="project-body">
- <h1>Julia Set Explorer</h1>
- <p>Zoom, pan, and "c" complex changes in this fun GPU-accelerated playground!</p>
+ <h1>Centipede</h1>
+ <p>
+ In this game, shoot all the centipede bodies and score points and
+ go up levels.
+ </p>
</div>
</div>
- <div class="project" onclick="window.location='ft-visualizer/index.html'">
+ <div class="project" onclick="window.location='dvd-logo/index.html'">
<div class="project-logo-container">
- <i class="fa-solid fa-wave-square"></i>
+ <i class="fa-solid fa-compact-disc"></i>
</div>
<div class="project-body">
- <h1>Discrete Fourier Visualizer</h1>
- <p>Draw how your year has gone and view a reactive graph containing its DFT by dragging your mouse over the canvas!</p>
+ <h1>DVD Logo Bouncing Animation</h1>
+ <p>
+ Brings back the nostalgia of old-school DVD players with an
+ intersection predictor. The twist: no Canvas API! Only svg's and
+ absolute positioned images!
+ </p>
</div>
</div>
</div>