Galactic Explorer

Every journey starts with a single step.

Source Code for this Sketch

class Timer{
	constructor(delay) {
		this.count = 0
		this.delta = 0
		this.delay = delay
	}

	update() {
		this.delta += deltaTime
		if(this.delta > this.delay) {
			this.count++
			this.delta %= this.delay
		}
	}
}

class StepToMouse{
	// x and y are measured from the canvas origin (top left)
	x = 0
	y = 0
	// cx and cy are centered coordinates; i.e. 0,0 is
	// at the midpoint in the canvas
	cx = 0
	cy = 0

	static def(input, defaultValue) {
		if (typeof input === "number") {
			return input
		}
		return defaultValue
	}

	constructor(step, minX, minY, maxX, maxY) {
		// step determines how quickly we orient towards
		// the mouse position. Each frame, we move at
		// most this many units in any direction.
		this.step = StepToMouse.def(step, 3)

		// Set the bounds for valid input values
		this.minX = StepToMouse.def(minX, 0)
		this.minY = StepToMouse.def(minY, 0)
		this.maxX = StepToMouse.def(maxX, width)
		this.maxY = StepToMouse.def(maxY, height)
	}

	update() {
		let deltaX = mouseX-this.x
		let deltaY = mouseY-this.y

		// Calculate the hypotenuse of our delta triangle
		// so we can limit angular movement by this.step
		let c = sqrt(pow(deltaX, 2)+pow(deltaY, 2))

		// Multiply our x & y movement by a ratio of their
		// length to the hypotenuse. For lateral or
		// horizonal movement we'll have 1:0 or 0:1.
		let ratioX = abs(deltaX) / c || 0
		let ratioY = abs(deltaY) / c || 0

		this.x += max(min(deltaX, this.step), -this.step)*ratioX
		this.y += max(min(deltaY, this.step), -this.step)*ratioY

		// Constrain x and y to valid area
		this.x = min(this.x, this.maxX)
		this.x = max(this.x, this.minX)
		this.y = min(this.y, this.maxY)
		this.y = max(this.y, this.minY)

		this.cx = this.x - width/2
		this.cy = this.y - height/2
	}
}

class Star {
	static minSize = 3
	static maxSize = 6

	// speedFactor controls how quickly stars approach
	static speedFactor = 1

	constructor() {
		this.reset()

		// More evenly distribute stars in their initial
		// starting positions
		this.r = pow(random(0, sqrt(this.limit)), 1.8)
	}

	reset() {
		this.r = 0
		this.x = 0
		this.y = 0
		this.speed = sqrt(random(.1, 8)) * Star.speedFactor
		this.size = random(Star.minSize, Star.maxSize)
		this.deg = random(0, 360)
		this.hue = random(0, 360)
		this.limit = max(width, height)
	}

	// multiplier returns a ratio of this.r to this.limit
	// with the given minimum value. min should be between
	// 0 and 1
	multiplier(min) {
		if (typeof min !== 'number') {
			min = 0
		}
		return min + (this.r / this.limit * (1 - min))
	}

	draw() {
		// Position from center
		this.r += this.speed * this.multiplier(.2)

		fill(this.hue, 70, 90)
		this.x = cos(this.deg - 90) * this.r * width / this.limit
		this.y = sin(this.deg - 90) * this.r * height / this.limit
		circle(this.x, this.y, this.size * this.multiplier())

		// detect when stars are outside the visible area and
		// reset. note: assumes 0,0 coordinates are centered
		if (this.x < -(width + this.size) ||
			this.x > (width + this.size)) {
			this.reset()
		}
		if (this.y < -(height + this.size) *.75 ||
			this.y > (height + this.size) *.75) {
			this.reset()
		}
	}
}

class Alien{
	constructor() {
		this.lights = new Timer(250)
	}

	radX(deg, r) {
		return cos(deg-90)*r
	}
	radY(deg, r) {
		return sin(deg-90)*r
	}

	eye(x, y) {
		let deg = round(abs(atan2(mouseX-width/2, mouseY-height/2+30)-180))

		// Stalks
		stroke(80, 100, 50)
		noFill()
		bezier(x, 0, x+this.radX(deg, 5), this.radY(deg, 10), x-this.radX(deg, 15), y-this.radY(deg, 20), x, y)

		// Eyeballs
		noStroke()
		fill(80, 100, 50)
		circle(x+this.radX(deg, 2), y+this.radY(deg, 2), 10)
		fill( 100)
		circle(x+this.radX(deg, 3), y+this.radY(deg, 3), 6)
		fill( 0)
		circle(x+this.radX(deg, 4), y+this.radY(deg, 4), 3)
	}

	draw(input) {
		this.lights.update()

		push()
		rectMode(CENTER)

		// Position the ship
		translate(width/2+input.cx/2, height/2+input.cy/2+30)

		// Tilt for movement
		let deltaX = min(max(mouseX, width*.1), width*.9) - width/2
		rotate((deltaX-input.cx)/10)


		// Cockpit
		strokeWeight(1)
		stroke(200, 100, 50, .7)
		fill(200, 100, 50, .3)
		arc(0, -10, 100, 100, 170, 10)

		// Pilot
		let alienElevation = 16
		noStroke()
		fill(80, 100, 50)
		arc(0, -alienElevation, 40, 20, 180, 0)
		rect(0, -alienElevation, 40, 5, 0, 0, 5, 5)

		this.eye(-10, -45)
		this.eye(10, -45)

		// Hull
		strokeWeight(1)
		stroke(20)
		fill(40)
		for(let i = 0; i < 6; i++) {
			ellipse(0, 0, 120-i*20, 30)
		}
		rect(0, 0, 120, 10, 5, 5, 5, 5)

		// Hull lights
		for(let i = 0; i < 11; i++) {
			if(i%4 === this.lights.count) {
				fill(65, 100, 50)
			} else if ((i+1)%4 === this.lights.count) {
				fill(310, 100, 50)
			} else {
				fill(200, 100, 50)
			}
			circle(-50+i*10, 0, 5)

			if(this.lights.count >=4) {
				this.lights.count = 0
			}
		}
		pop()
	}
}

let inputStarfield
let inputShip
let stars = []
let alien

function setup() {
	createCanvas(800, 400, P2D, canvas)
	colorMode(HSL)
	angleMode(DEGREES)
	ellipseMode(CENTER)
	noStroke()

	inputStarfield = new StepToMouse(.5, width*.4, height*.4, width*.6, height*.6)
	// Initialize at center instead of 0,0
	inputStarfield.x = width / 2
	inputStarfield.y = height / 2

	inputShip = new StepToMouse(5, width*.2, height*.2, width*.8, height*.8)
	// Initialize at center instead of 0,0
	inputShip.x = width / 2
	inputShip.y = height / 2

	for (const x of Array(400).keys()) {
		stars.push(new Star())
	}

	alien = new Alien()
}

function draw() {
	inputStarfield.update()
	inputShip.update()
	background(0)

	push()
	translate(width/2+inputStarfield.cx*4, inputStarfield.y)
	// Flight sim controls, maybe
	shearY(inputStarfield.cx/16)
	shearX(inputStarfield.cy/16)
	stars.map((star) => { star.draw() })
	pop()

	alien.draw(inputShip)
}

You may use all or part of the p5 code seen on this page for your own creations, without restriction. Attribution is appreciated but not required.

This sketch also includes the following sources: p5common.js . Sources are available under their respective licenses.

See all p5.js sketches