Euler golf (#1)
This commit is contained in:
parent
93c8bbeb6a
commit
445af5d0be
74
euler-golf/css/styles.css
Normal file
74
euler-golf/css/styles.css
Normal file
@ -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;
|
||||
}
|
74
euler-golf/index.html
Normal file
74
euler-golf/index.html
Normal file
@ -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>
|
32
euler-golf/js/controls.js
vendored
Normal file
32
euler-golf/js/controls.js
vendored
Normal file
@ -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());
|
308
euler-golf/js/cx.js
Normal file
308
euler-golf/js/cx.js
Normal file
@ -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
|
276
euler-golf/js/game.js
Normal file
276
euler-golf/js/game.js
Normal file
@ -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);
|
19
euler-golf/js/json-ds.js
Normal file
19
euler-golf/js/json-ds.js
Normal file
@ -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));
|
||||
}
|
||||
}
|
1
euler-golf/js/modal-vanilla.min.js
vendored
Normal file
1
euler-golf/js/modal-vanilla.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
39
euler-golf/js/sol.js
Normal file
39
euler-golf/js/sol.js
Normal file
@ -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;
|
||||
};
|
114
index.html
114
index.html
@ -3,56 +3,41 @@
|
||||
<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>
|
||||
</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-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-mosquito"></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>
|
||||
<h1>Euler Golf 2</h1>
|
||||
<p>
|
||||
A puzzle game (with solver) to explore rotations in the complex
|
||||
plane.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -63,18 +48,69 @@
|
||||
|
||||
<div class="project-body">
|
||||
<h1>Julia Set Explorer</h1>
|
||||
<p>Zoom, pan, and "c" complex changes in this fun GPU-accelerated playground!</p>
|
||||
<p>
|
||||
Zoom, pan, and "c" complex changes in this fun GPU-accelerated
|
||||
playground!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project" onclick="window.location='ft-visualizer/index.html'">
|
||||
<div
|
||||
class="project"
|
||||
onclick="window.location='ft-visualizer/index.html'"
|
||||
>
|
||||
<div class="project-logo-container">
|
||||
<i class="fa-solid fa-wave-square"></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>
|
||||
<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-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-mosquito"></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>
|
||||
</div>
|
||||
|
||||
<div class="project" onclick="window.location='dvd-logo/index.html'">
|
||||
<div class="project-logo-container">
|
||||
<i class="fa-solid fa-compact-disc"></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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user