/*
categories: relative
files:
../point_src/core/head.js
../point_src/pointpen.js
../point_src/pointdraw.js
../point_src/extras.js
../point_src/math.js
../point_src/point-content.js
../point_src/stage.js
point
dragging
stroke
../point_src/distances.js
pointlist
../point_src/events.js
../point_src/automouse.js
../point_src/relative.js
../point_src/keyboard.js
../point_src/constrain-distance.js
../point_src/screenwrap.js
The arrow keys pushes the ship in a frictionless 2D space.
Keydown performs an `impartOnRads` to _push_ the ship in the pointing direction
*/
// Function to convert angle to velocity vector
function angleToVelocity(theta, speed) {
return {
x: speed * Math.cos(theta),
y: speed * Math.sin(theta)
};
}
class MainStage extends Stage {
canvas = 'playspace'
mounted() {
console.log('mounted')
// this.screenwrap = new ScreenWrap
this.mouse.position.vy = this.mouse.position.vx = 0
// Create the virtual "ship" body (center of mass)
this.ship = new Point({
x: 200,
y: 225, // midpoint between a and b
vx: 0,
vy: 0,
radians: -Math.PI/2, // -90 degrees
rotationSpeed: 0,
mass: 10,
radius: 5
})
// Create the engines with offsets from ship center
this.a = new Point({ x: 200, y: 200, vx: 0, vy: 0, rotation: -90, radius: 10, mass: 1, force: 0})
this.b = new Point({ x: 200, y: 250, vx: 0, vy: 0, rotation: -90, radius: 10, mass: 1, force: 0})
this.c = new Point({ x: 225, y: 225, vx: 0, vy: 0, rotation: 0, radius: 10, mass: 1, force: 0})
// Store initial offsets (in ship's local space)
// radians is the LOCAL rotation offset (0 = forward, Math.PI/2 = right, etc.)
this.engineOffsets = [
{ x: 0, y: -25, radians: .10 }, // engine 'a' - top, pointing forward
{ x: 0, y: 25, radians: -.10 }, // engine 'b' - bottom, pointing forward
{ x: 25, y: 0, radians: 0 } // engine 'c' - right side, pointing right
]
this.engines = [this.a, this.b, this.c]
// Add additional mass points to shift center of mass
// These are "virtual" mass points that don't render but affect physics
// For a top-heavy VTOL: put heavy mass at the top
this.massPoints = [
{ x: 80, y: 0, mass: 10 } // Heavy payload at the top (15 mass units)
// { x: 30, y: 0, mass: 8 }, // Additional mass slightly lower
// , { x: 0, y: 40, mass: 20 } // Light fuel tank at bottom (uncomment to test)
]
this.asteroids = new PointList(
[250, 200]
, [200, 250]
, [200, 350]
).cast()
this.asteroids.update({vx: 0, vy: 0, mass: 1})
this.keyboard.onKeydown(KC.UP, this.onUpKeydown.bind(this))
this.keyboard.onKeyup(KC.UP, this.onUpKeyup.bind(this))
this.keyboard.onKeydown(KC.LEFT, this.onLeftKeydown.bind(this))
this.keyboard.onKeydown(KC.RIGHT, this.onRightKeydown.bind(this))
this.keyboard.onKeydown(KC.DOWN, this.onDownKeydown.bind(this))
this.keyboard.onKeyup(KC.DOWN, this.onDownKeyup.bind(this))
this.rotationSpeed = 0
this.power = 0
this.powerDown = false
this.dragging.add(...this.asteroids)
}
updateEnginePositions() {
/* Update the visual positions of engines based on ship position and rotation */
const ship = this.ship
const cos = Math.cos(ship.radians)
const sin = Math.sin(ship.radians)
this.engines.forEach((engine, i) => {
const offset = this.engineOffsets[i]
// Rotate the offset by the ship's current rotation
const rotatedX = offset.x * cos - offset.y * sin
const rotatedY = offset.x * sin + offset.y * cos
// Position engine relative to ship
engine.x = ship.x + rotatedX
engine.y = ship.y + rotatedY
// Sync engine rotation with ship + local offset
engine.radians = ship.radians + offset.radians
engine.rotation = (engine.radians * 180 / Math.PI)
})
}
computeCenterOfMass() {
/* Calculate the center of mass of the ship + engines + mass points system */
let totalMass = this.ship.mass
let x = this.ship.x * this.ship.mass
let y = this.ship.y * this.ship.mass
// Add engine masses
for (let engine of this.engines) {
totalMass += engine.mass
x += engine.x * engine.mass
y += engine.y * engine.mass
}
// Add additional mass points (e.g., payload, fuel tanks)
// These are in local space, so rotate them relative to ship orientation
const cos = Math.cos(this.ship.radians)
const sin = Math.sin(this.ship.radians)
for (let massPoint of this.massPoints) {
// Rotate the mass point offset
const rotatedX = massPoint.x * cos - massPoint.y * sin
const rotatedY = massPoint.x * sin + massPoint.y * cos
// Calculate world position
const worldX = this.ship.x + rotatedX
const worldY = this.ship.y + rotatedY
totalMass += massPoint.mass
x += worldX * massPoint.mass
y += worldY * massPoint.mass
}
return {
x: x / totalMass,
y: y / totalMass
}
}
computeMomentOfInertia(com) {
/* Calculate rotational inertia around center of mass */
let I = 0
// Ship body contribution
const dx = this.ship.x - com.x
const dy = this.ship.y - com.y
I += this.ship.mass * (dx * dx + dy * dy)
// Engine contributions
for (let engine of this.engines) {
const dx = engine.x - com.x
const dy = engine.y - com.y
I += engine.mass * (dx * dx + dy * dy)
}
// Mass point contributions (they also affect rotational inertia!)
const cos = Math.cos(this.ship.radians)
const sin = Math.sin(this.ship.radians)
for (let massPoint of this.massPoints) {
// Rotate the mass point offset
const rotatedX = massPoint.x * cos - massPoint.y * sin
const rotatedY = massPoint.x * sin + massPoint.y * cos
// Calculate world position
const worldX = this.ship.x + rotatedX
const worldY = this.ship.y + rotatedY
const dx = worldX - com.x
const dy = worldY - com.y
I += massPoint.mass * (dx * dx + dy * dy)
}
return I
}
applyGravityGradientTorque(com, I) {
/* Simulate gravity-gradient torque - heavier masses farther from COM
create instability when not aligned with gravity */
const gravityStrength = 0.01 // Gravity force per unit mass
const cos = Math.cos(this.ship.radians)
const sin = Math.sin(this.ship.radians)
let gravityTorque = 0
// Apply gravity to each mass point and calculate resulting torque
for (let massPoint of this.massPoints) {
// Rotate mass point to world space
const rotatedX = massPoint.x * cos - massPoint.y * sin
const rotatedY = massPoint.x * sin + massPoint.y * cos
const worldX = this.ship.x + rotatedX
const worldY = this.ship.y + rotatedY
// Gravity force on this mass point (downward)
const gravityForce = massPoint.mass * gravityStrength
// Distance from COM
const dx = worldX - com.x
const dy = worldY - com.y
// Torque = r × F (only y-component of force matters for vertical gravity)
gravityTorque += dx * gravityForce
}
// Apply gravity torque to engines too
for (let engine of this.engines) {
const gravityForce = engine.mass * gravityStrength
const dx = engine.x - com.x
gravityTorque += dx * gravityForce
}
// Ship body gravity torque
const gravityForce = this.ship.mass * gravityStrength
const dx = this.ship.x - com.x
gravityTorque += dx * gravityForce
// Apply the gravity-induced torque
if (I > 0) {
this.ship.rotationSpeed += gravityTorque / I
}
}
applyEngineForces() {
/* Apply forces from each engine to the ship's linear and angular velocity */
const com = this.computeCenterOfMass()
const I = this.computeMomentOfInertia(com)
let fxTotal = 0
let fyTotal = 0
let torqueTotal = 0
for (let engine of this.engines) {
const force = engine.force
// Calculate force vector in the direction the engine is pointing
const fx = Math.cos(engine.radians) * force
const fy = Math.sin(engine.radians) * force
fxTotal += fx
fyTotal += fy
// Calculate torque (rotational force) around center of mass
const dx = engine.x - com.x
const dy = engine.y - com.y
// Cross product in 2D: torque = r × F
torqueTotal += dx * fy - dy * fx
}
// Apply forces to ship - must include ALL masses (ship + engines + mass points)
let totalMass = this.ship.mass + this.engines.reduce((sum, e) => sum + e.mass, 0)
totalMass += this.massPoints.reduce((sum, mp) => sum + mp.mass, 0)
this.ship.vx += fxTotal / totalMass
this.ship.vy += fyTotal / totalMass
// Apply torque (with moment of inertia)
if (I > 0) {
this.ship.rotationSpeed += torqueTotal / I
}
// Apply gravity-gradient torque (makes top-heavy configurations unstable)
this.applyGravityGradientTorque(com, I)
}
addMotion(point, speed=1) {
/* Because we're in a zero-gravity space, the velocity is simply _added_
to the current XY, pushing the point in the direction of forced. */
point.x += point.vx
point.y += point.vy
return
}
performPower(){
if(this.powerDown === true) {
/* Applied here, bcause a spaceship only applied force when the thottle is on.*/
this.impart(.06)
return
}
this.power = 0
if(this.reverseDown === true) {
this.impart(-.01)
}
}
onUpKeydown(ev) {
/* On keydown we add some to the throttle.
As keydown first repeatedly, this will raise the power until
keyup */
this.powerDown = true
}
onUpKeyup(ev) {
/* Reset the throttle */
this.powerDown = false
}
impart(speed=1, direction=new Point(1,0)){
/* Impart _speed_ for momentum relative to the direction the the point.
For example - pointing _right_ and applying the _{1,0}_ direction (denoting forward)
will push the point further right, applying _{0, 1}_ pushes the point _left_
relative to its direction.
Or to rephase, imagine a engine on the back of the point - pushing _forward_.
*/
// Apply force to each engine individually
this.engines.forEach(engine => {
engine.force += speed
})
}
onDownKeydown(ev) {
this.reverseDown = true
}
onDownKeyup(ev) {
this.reverseDown = false
}
onLeftKeydown(ev) {
/* Rotate the ship as if spinning on the spot.
This rotation Speed is applied constantly in `this.updateShip`
*/
if(ev.shiftKey || ev.ctrlKey) {
/* Perform a _crab_ left - apply differential thrust */
this.a.force += 0.02
this.b.force -= 0.02
return
}
// Don't apply rotation - that's cheating. Instead, use the side engine to rotate.
// this.ship.rotationSpeed -= 0.01
this.a.force -= 0.15
}
onRightKeydown(ev) {
/* Rotate the ship as if spinning on the spot.
This rotation Speed is applied constantly in `this.updateShip`
*/
if(ev.shiftKey || ev.ctrlKey) {
/* Perform a _crab_ right - apply differential thrust */
this.a.force -= 0.02
this.b.force += 0.02
return
}
// this.ship.rotationSpeed += 0.01
this.b.force -= 0.15
}
updateShip(){
// CRITICAL FIX: Store the COM offset BEFORE updating positions
const comBefore = this.computeCenterOfMass()
const offsetBeforeX = comBefore.x - this.ship.x
const offsetBeforeY = comBefore.y - this.ship.y
// Apply rotation FIRST (before updating engine positions)
this.ship.radians += this.ship.rotationSpeed
this.ship.rotation = this.ship.radians * 180 / Math.PI
// Dampen rotation
// this.ship.rotationSpeed *= .99
// Update engine positions based on NEW ship orientation
this.updateEnginePositions()
// Now calculate COM with new engine positions
const comAfter = this.computeCenterOfMass()
const offsetAfterX = comAfter.x - this.ship.x
const offsetAfterY = comAfter.y - this.ship.y
// The ship reference point needs to move so that COM stays consistent
// This keeps the ship rotating around its true center of mass
this.ship.x += (offsetBeforeX - offsetAfterX)
this.ship.y += (offsetBeforeY - offsetAfterY)
// Re-update engine positions with corrected ship position
this.updateEnginePositions()
// Apply forces from engines to ship
this.applyEngineForces()
// Apply gravity to ship
this.ship.vy += .01 // Simulated gravity
// Move the ship based on its velocity (this moves the whole system)
this.addMotion(this.ship, this.speed)
// Screen wrap
this.screenWrap.perform(this.ship)
// Apply throttle/reverse
this.performPower()
// Decay engine forces
this.engines.forEach(e => e.force *= 0.9)
}
draw(ctx) {
this.clear(ctx)
this.updateShip()
this.asteroids.pen.indicators(ctx)
// Calculate and draw the center of mass
const com = this.computeCenterOfMass()
ctx.fillStyle = '#ff0000'
ctx.beginPath()
ctx.arc(com.x, com.y, 8, 0, Math.PI * 2)
ctx.fill()
// Draw mass points (visualize the payload/fuel tanks)
const cos = Math.cos(this.ship.radians)
const sin = Math.sin(this.ship.radians)
ctx.fillStyle = '#ffff00'
for (let massPoint of this.massPoints) {
const rotatedX = massPoint.x * cos - massPoint.y * sin
const rotatedY = massPoint.x * sin + massPoint.y * cos
const worldX = this.ship.x + rotatedX
const worldY = this.ship.y + rotatedY
// Size based on mass
const radius = Math.sqrt(massPoint.mass) * 2
ctx.beginPath()
ctx.arc(worldX, worldY, radius, 0, Math.PI * 2)
ctx.fill()
}
// Draw the ship center (green - this is the reference point, not COM)
this.ship.pen.indicator(ctx, '#00ff00')
// Draw the engines
this.a.pen.indicator(ctx)
this.b.pen.indicator(ctx)
this.c.pen.indicator(ctx, '#ff00ff') // Purple for the side engine
this.a.pen.line(ctx, this.ship, 'purple')
this.b.pen.line(ctx, this.ship, 'purple')
this.c.pen.line(ctx, this.ship, 'purple')
// Draw lines connecting engines to show rigid body
// ctx.strokeStyle = '#ffffff44'
// ctx.lineWidth = 2
// ctx.beginPath()
// ctx.moveTo(this.a.x, this.a.y)
// ctx.lineTo(this.b.x, this.b.y)
// ctx.lineTo(this.c.x, this.c.y)
// ctx.lineTo(this.a.x, this.a.y)
// ctx.stroke()
}
}
stage = MainStage.go()