1
0
Fork 0

Compare commits

...

7 Commits

Author SHA1 Message Date
André Jaenisch f5eca1e296
feat: control gravity 1 year ago
André Jaenisch fdbdd3aaef
feat: add range input to control gravity 1 year ago
André Jaenisch f6aad4cd6f
refactor: apply gravity on update of position after draw 1 year ago
André Jaenisch 74bfda593e
refactor: don't attach acceleration to shapes anymore 1 year ago
André Jaenisch f42cc2895f
refactor: don't compute same divisor twice 1 year ago
André Jaenisch ef4cf54f11
feat: add top boundary 1 year ago
André Jaenisch 3fb776ca78
refactor: boundary factory is now agnostic on top / bottom 1 year ago
  1. BIN
      js13kgames.zip
  2. 3
      src/css/main.css
  3. 13
      src/index.html
  4. 80
      src/js/app.js
  5. 16
      src/js/collisions.js
  6. 16
      src/js/draw.js
  7. 3
      src/js/shape.js
  8. 4
      src/js/world.js
  9. 6
      test/js/world.test.js
  10. 1
      types/collisions.d.ts
  11. 3
      types/draw.d.ts
  12. 2
      types/shape.d.ts
  13. 4
      types/world.d.ts

BIN
js13kgames.zip

Binary file not shown.

3
src/css/main.css

@ -261,3 +261,6 @@ textarea {
}
}
label span {
display: block;
}

13
src/index.html

@ -17,6 +17,19 @@
</section>
<section data-scene="game">
<canvas id="game" width="300" height="450"></canvas>
<div>
<label>
<span>Manipulate the gravity!</span>
<input
id="gravity"
type="range"
min="-100"
max="100"
value="0"
step="any"
/>
</label>
</div>
</section>
<section data-scene="credits">
<p>

80
src/js/app.js

@ -1,18 +1,21 @@
import { testCollision } from './collisions.js'
import { drawShape } from './draw.js'
import { makeAstronaut, makeBottomBoundary } from './world.js'
import { Vec2 } from './vector.js'
import { makeAstronaut, makeBoundary } from './world.js'
/** @typedef {import('./shape').Shape} Shape */
// Global on purpose
/** @type {Shape} */
let astronaut
/** @type {Shape} */
let bottomBoundary
/** @type {Array<Shape>} */
const boundaries = []
/** @type {HTMLCanvasElement} */
let canvas
/** @type {CanvasRenderingContext2D} */
let context
/** @type {number} */
let gravity
/**
* Entry point into the game.
@ -26,6 +29,14 @@ export function app () {
return
}
const input = document.getElementById('gravity')
if (!input) {
console.error('Could not start game!')
return
}
input.addEventListener('change', updateGravity)
canvas = /** @type {HTMLCanvasElement} */(game)
const ctx = canvas.getContext('2d')
@ -35,15 +46,8 @@ export function app () {
}
context = ctx
astronaut = makeAstronaut()
const boundaryHeight = 2
bottomBoundary = makeBottomBoundary({
x: 0,
y: canvas.height - boundaryHeight,
height: boundaryHeight,
width: canvas.width
})
gravity = parseFloat(/** @type {HTMLInputElement} */(input).value)
createObjectsInWorld()
tick()
}
@ -51,12 +55,54 @@ export function app () {
// See https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame#notes
function tick () {
context.clearRect(0, 0, canvas.width, canvas.height)
drawShape(context, bottomBoundary)
drawShape(context, astronaut)
boundaries.forEach(function (boundary) {
drawShape(context, boundary)
})
if (testCollision(bottomBoundary, astronaut)) {
throw new Error('Game Over!')
}
drawShape(context, astronaut, Vec2(0, gravity))
boundaries.forEach(function (boundary) {
if (testCollision(boundary, astronaut)) {
throw new Error('Game Over!')
}
})
window.requestAnimationFrame(tick)
}
function createObjectsInWorld () {
astronaut = makeAstronaut()
const boundaryHeight = 2
boundaries.push(
makeBoundary({
x: 0,
y: canvas.height - boundaryHeight,
height: boundaryHeight,
width: canvas.width
})
)
boundaries.push(
makeBoundary({
x: 0,
y: 0 + boundaryHeight,
height: boundaryHeight,
width: canvas.width
})
)
}
/**
* Updates the applied gravity.
*
* @param {Event} event
*/
function updateGravity (event) {
if (!event.target) {
return
}
const input = /** @type {HTMLInputElement} */(event.target)
gravity = parseFloat(input.value)
}

16
src/js/collisions.js

@ -49,18 +49,12 @@ export function testCollision (shape1, shape2) {
* @param {Array<Vector2D>} vertices
* @returns {boolean}
* @see {@link https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line_segment}
* @see {@link https://en.wikipedia.org/wiki/Cramer%27s_rule}
*/
export function testIntersection ([v1, v2, v3, v4]) {
const tdivisor = determinant(subtract(v1, v2), subtract(v3, v4))
const divisor = determinant(subtract(v1, v2), subtract(v3, v4))
if (tdivisor === 0) {
// Parallel lines never intersect
return false
}
const udivisor = determinant(subtract(v1, v2), subtract(v3, v4))
if (udivisor === 0) {
if (divisor === 0) {
// Parallel lines never intersect
return false
}
@ -68,8 +62,8 @@ export function testIntersection ([v1, v2, v3, v4]) {
const tdivident = determinant(subtract(v1, v3), subtract(v3, v4))
const udivident = determinant(subtract(v2, v1), subtract(v1, v3))
// + 0 turns -0 into +0
const t = tdivident / tdivisor + 0
const u = udivident / udivisor + 0
const t = tdivident / divisor + 0
const u = udivident / divisor + 0
return (t >= 0 && t <= 1) && (u >= 0 && u <= 1)
}

16
src/js/draw.js

@ -1,6 +1,6 @@
import { FPS } from './constants.js'
import { FPS, gravity } from './constants.js'
import { computeNormals } from './shape.js'
import { add, rotate, scale } from './vector.js'
import { add, rotate, scale, Vec2 } from './vector.js'
/** @typedef {import('./shape').Shape} Shape */
/** @typedef {import('./vector').Vector2D} Vector2D */
@ -10,14 +10,15 @@ import { add, rotate, scale } from './vector.js'
*
* @param {CanvasRenderingContext2D} context
* @param {Shape} shape
* @param {Vector2D} [g=gravity]
*/
export function drawShape (context, shape) {
export function drawShape (context, shape, g = gravity) {
context.save()
prepareCanvas(context, shape)
draw(context, shape)
context.restore()
updatePosition(shape)
updatePosition(shape, g)
updateRotation(shape)
}
@ -49,9 +50,12 @@ function draw (context, shape) {
* Update the position of the shape.
*
* @param {Shape} shape
* @param {Vector2D} g
*/
function updatePosition (shape) {
shape.V = add(shape.V, scale(shape.A, 1 / FPS))
function updatePosition (shape, g) {
// Apply gravity to all shapes with mass
const acceleration = shape.M > 0 ? g : Vec2(0, 0)
shape.V = add(shape.V, scale(acceleration, 1 / FPS))
moveShape(shape, scale(shape.V, 1 / FPS))
}

3
src/js/shape.js

@ -1,4 +1,3 @@
import { gravity } from './constants.js'
import { normalize, subtract, Vec2 } from './vector.js'
/** @typedef {import('./vector.js').Vector2D} Vector2D
@ -10,7 +9,6 @@ import { normalize, subtract, Vec2 } from './vector.js'
* @property {number} R
* @property {number} M
* @property {Vector2D} V
* @property {Vector2D} A
* @property {number} G
* @property {number} v
* @property {number} a
@ -51,7 +49,6 @@ export function RigidShape ({
R: restitution,
M: mass ? 1 / mass : immobile,
V: Vec2(0, 0), // velocity, i.e. speed
A: mass ? gravity : Vec2(0, 0), // acceleration
G: 0, // angle
v: 0, // angle velocity
a: 0, // angle acceleration

4
src/js/world.js

@ -31,7 +31,7 @@ export function makeAstronaut () {
}
/**
* Creates a boundary at the bottom of the screen.
* Creates a boundary of the screen.
*
* @param {object} config
* @param {number} config.x
@ -40,7 +40,7 @@ export function makeAstronaut () {
* @param {number} config.width
* @returns {Shape}
*/
export function makeBottomBoundary ({ x, y, height, width }) {
export function makeBoundary ({ x, y, height, width }) {
const center = Vec2(x + width / 2, y + height / 2)
const friction = 20
const restitution = 0

6
test/js/world.test.js

@ -1,7 +1,7 @@
import { expect } from 'chai'
import { Vec2 } from '../../src/js/vector.js'
import { makeAstronaut, makeBottomBoundary } from '../../src/js/world.js'
import { makeAstronaut, makeBoundary } from '../../src/js/world.js'
describe('World', function () {
describe('makeAstronaut', function () {
@ -17,7 +17,7 @@ describe('World', function () {
})
})
describe('makeBottomBoundary', function () {
describe('makeBoundary', function () {
it('should make an immobil shape', function () {
// Arrange
const x = 0
@ -26,7 +26,7 @@ describe('World', function () {
const width = 200
// Act
const boundary = makeBottomBoundary({ x, y, height, width })
const boundary = makeBoundary({ x, y, height, width })
// Assert
expect(boundary.X[0]).to.deep.equal(Vec2(x, y))

1
types/collisions.d.ts vendored

@ -22,6 +22,7 @@ export function testCollision(shape1: Shape, shape2: Shape): boolean;
* @param {Array<Vector2D>} vertices
* @returns {boolean}
* @see {@link https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line_segment}
* @see {@link https://en.wikipedia.org/wiki/Cramer%27s_rule}
*/
export function testIntersection([v1, v2, v3, v4]: Array<Vector2D>): boolean;
export type Shape = import('./shape').Shape;

3
types/draw.d.ts vendored

@ -5,7 +5,8 @@
*
* @param {CanvasRenderingContext2D} context
* @param {Shape} shape
* @param {Vector2D} [g=gravity]
*/
export function drawShape(context: CanvasRenderingContext2D, shape: Shape): void;
export function drawShape(context: CanvasRenderingContext2D, shape: Shape, g?: import("./vector.js").Vector2D | undefined): void;
export type Shape = import('./shape').Shape;
export type Vector2D = import('./vector').Vector2D;

2
types/shape.d.ts vendored

@ -7,7 +7,6 @@
* @property {number} R
* @property {number} M
* @property {Vector2D} V
* @property {Vector2D} A
* @property {number} G
* @property {number} v
* @property {number} a
@ -55,7 +54,6 @@ export type Shape = {
R: number;
M: number;
V: Vector2D;
A: Vector2D;
G: number;
v: number;
a: number;

4
types/world.d.ts vendored

@ -6,7 +6,7 @@
*/
export function makeAstronaut(): Shape;
/**
* Creates a boundary at the bottom of the screen.
* Creates a boundary of the screen.
*
* @param {object} config
* @param {number} config.x
@ -15,7 +15,7 @@ export function makeAstronaut(): Shape;
* @param {number} config.width
* @returns {Shape}
*/
export function makeBottomBoundary({ x, y, height, width }: {
export function makeBoundary({ x, y, height, width }: {
x: number;
y: number;
height: number;

Loading…
Cancel
Save