diff --git a/euler-golf/css/styles.css b/euler-golf/css/styles.css index f343898..05f2f9f 100644 --- a/euler-golf/css/styles.css +++ b/euler-golf/css/styles.css @@ -4,16 +4,24 @@ body { 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-holder canvas { +.canvas { padding: 0; margin: auto; display: block; - height: 70vh; - width: auto; - max-width: 100%; border: 1px solid black; + + width: 100vw; + height: 100vw; } .button { @@ -22,3 +30,15 @@ body { padding: 5px; cursor: pointer; } + +.controls { + padding: 12px; + position: fixed; + bottom: 0; + right: 0; + background-color: rgba(255, 255, 255, 0.5); + border: 1px solid white; + border-radius: 8px; + margin-right: 6px; + margin-bottom: 6px; +} diff --git a/euler-golf/index.html b/euler-golf/index.html index 2717ed5..fb31d41 100644 --- a/euler-golf/index.html +++ b/euler-golf/index.html @@ -5,8 +5,10 @@ -
- + + +
+
diff --git a/euler-golf/js/game.js b/euler-golf/js/game.js index 25c1632..4edaa37 100644 --- a/euler-golf/js/game.js +++ b/euler-golf/js/game.js @@ -1,31 +1,23 @@ -const CANVAS_ID = "myCanvas"; const DEFAULTS = { - rows: 15, - cols: 15, - grid_padding: 10, + max_rows: 80, + max_cols: 80, + min_gap: 40, + angle_multiplier: 0.0005, }; +const CANVAS = document.getElementById("canvas"); let state = { - ...DEFAULTS, - canvas: document.getElementById("canvas"), - ctx: document.getElementById("canvas").getContext("2d"), + grid_padding: 10, + canvas: CANVAS, + ctx: CANVAS.getContext("2d"), last_render: 0, - changes: { - width: document.body.clientWidth, - height: document.body.clientHeight, - }, -}; - -const rand_between = (min, max) => - Math.floor(Math.random() * (max - min + 1)) + min; - -const rand_target = (cols, rows) => { - const res = new cx(rand_between(0, cols), rand_between(0, rows)); - if (res.re % 2 || res.im % 2) return rand_target(cols, rows); - - return res; + path: [new cx(0, 0), new cx(1, 0)], + angle: new cx(0, 0), + keys: {}, + changes: {}, }; +// Rendering CanvasRenderingContext2D.prototype.circle = function (x, y, r, color) { this.beginPath(); this.arc(x, y, r, 0, Math.PI * 2); @@ -35,22 +27,35 @@ CanvasRenderingContext2D.prototype.circle = function (x, y, r, color) { }; CanvasRenderingContext2D.prototype.line = function ( - { x: x1, y: y1 }, - { x: x2, y: y2 }, + { x_pos: x1, y_pos: y1 }, + { x_pos: x2, y_pos: y2 }, width, color, cap = "round" ) { - this.beginPath(); - this.moveTo(x1, y1); - this.lineTo(x2, y2); 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, @@ -66,33 +71,92 @@ CanvasRenderingContext2D.prototype.do_grid = function ( CanvasRenderingContext2D.prototype.cartesian_grid = function ( rows, cols, - width, - height, - grid_padding, - circle_spec_at_coords = (x, y) => ({ radius: 5, color: "#000" }) + grid_spec, + circle_spec_at_coords = (_x, _y) => ({ radius: 5, color: "#000" }) ) { - const center = { x: Math.floor(cols / 2), y: Math.floor(rows / 2) }; - - const dx = (width - 2 * grid_padding) / cols; - const dy = (height - 2 * grid_padding) / rows; - - const start_x = grid_padding + dx / 2; - const start_y = grid_padding + dy / 2; - this.do_grid(rows, cols, (ctx, x, y) => { - const x_pos = x * dx + start_x; - const y_pos = y * dy + start_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); }); }; -const render = ({ canvas, ctx, rows, cols, grid_padding }) => { - const { width, height } = canvas; +// 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 = (cols, rows) => { + const res = new cx(rand_between(0, cols), rand_between(0, rows)); + if (res.re % 2 || res.im % 2) return rand_target(cols, rows); + + 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 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; + } + + if (state.angle.im <= -1 || state.angle.im >= 1) { + state.angle.im = state.angle.im <= -1 ? -1 : 1; + state.path.push(move(state.path.at(-2), state.path.at(-1), state.angle)); + state.angle = new cx(0, 0); + } +}; + +const render = ({ width, height, ctx, rows, cols } = state) => { ctx.clearRect(0, 0, width, height); - ctx.cartesian_grid(rows, cols, width, height, grid_padding, (x, y) => { + ctx.fillStyle = "rgba(255, 255, 255, 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), + ]); + + ctx.cartesian_grid(rows, cols, grid_spec, (x, y) => { if (x == Math.floor(cols / 2) && y == Math.floor(rows / 2)) { return { radius: 7, @@ -101,7 +165,7 @@ const render = ({ canvas, ctx, rows, cols, grid_padding }) => { } else { return { radius: 3, - color: "#000", + color: `rgb(${255 * (x / cols)}, 100, 100)`, // todo: animate with last_render }; } }); @@ -110,14 +174,48 @@ const render = ({ canvas, ctx, rows, cols, grid_padding }) => { 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.min( + DEFAULTS.max_rows, + state.changes.height / DEFAULTS.min_gap + ); + state.changes.cols = Math.min( + DEFAULTS.max_cols, + state.changes.width / DEFAULTS.min_gap + ); + } + state = { ...state, ...state.changes }; state.changes = {}; - - render(state); } + handle_input(state, dt); + render(state); requestAnimationFrame(loop); }; +// 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(); requestAnimationFrame(loop); diff --git a/euler-golf/js/sol.js b/euler-golf/js/sol.js index a8ce2cf..be4a9aa 100644 --- a/euler-golf/js/sol.js +++ b/euler-golf/js/sol.js @@ -5,8 +5,6 @@ const DIRECTION = { 1: new cx(0, -1), }; -const move = (prev, curr, c) => cx.add(prev, cx.mult(c, cx.sub(curr, prev))); - const construct_moves = (curr, prev) => Object.keys(DIRECTION).map((x) => move(curr, prev, DIRECTION[x]));