Euler golf #1
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;
|
||||||
|
};
|
108
index.html
108
index.html
@ -3,56 +3,41 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Simponic's Static Sites</title>
|
<title>Simponic's Static Sites</title>
|
||||||
<link href="css/styles.css" rel="stylesheet">
|
<link href="css/styles.css" rel="stylesheet" />
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"
|
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>
|
<script
|
||||||
|
src="https://kit.fontawesome.com/d7e97ed48f.js"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
></script>
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="top-container animate__animated animate__fadeIn">
|
<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>
|
<h3>
|
||||||
👋 Hello, I'm Simponic!
|
👋 Hello, I'm Simponic!
|
||||||
<br>
|
<br />
|
||||||
📖 This page hosts strictly static content.
|
📖 This page hosts strictly static content.
|
||||||
<br>
|
<br />
|
||||||
🔔 My "real website" is at <a href="https://simponic.xyz">simponic.xyz</a>.
|
🔔 My "real website" is at
|
||||||
|
<a href="https://simponic.xyz">simponic.xyz</a>.
|
||||||
</h3>
|
</h3>
|
||||||
<div class="projects-grid">
|
<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">
|
<div class="project-logo-container">
|
||||||
<i class="fa-solid fa-compact-disc"></i>
|
<i class="fa-solid fa-golf-ball-tee"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="project-body">
|
<div class="project-body">
|
||||||
<h1>DVD Logo Bouncing Animation</h1>
|
<h1>Euler Golf 2</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>
|
<p>
|
||||||
</div>
|
A puzzle game (with solver) to explore rotations in the complex
|
||||||
</div>
|
plane.
|
||||||
|
</p>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
@ -63,18 +48,69 @@
|
|||||||
|
|
||||||
<div class="project-body">
|
<div class="project-body">
|
||||||
<h1>Julia Set Explorer</h1>
|
<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>
|
</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">
|
<div class="project-logo-container">
|
||||||
<i class="fa-solid fa-wave-square"></i>
|
<i class="fa-solid fa-wave-square"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="project-body">
|
<div class="project-body">
|
||||||
<h1>Discrete Fourier Visualizer</h1>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user