Skip to content

Instantly share code, notes, and snippets.

@bazzargh
Created February 2, 2025 18:24
Show Gist options
  • Save bazzargh/d4728b26df6c9062878d9e85f808817f to your computer and use it in GitHub Desktop.
Save bazzargh/d4728b26df6c9062878d9e85f808817f to your computer and use it in GitHub Desktop.
"Landing The Nostromo" implemented in p5.js
// based on Landing the Nostromo by Alan Sutcliffe
// https://archive.org/details/creativecomputing-1981-06/page/n51/mode/2up?ui=embed&view=theater
let aspect = 46
let probe = 7
let line_stat = 14
let z=[]
let panel1=""
let panel2=""
// font data
let ss="77116b6e5c3e17697f7c6f1f0b4f7b397e1d0147038d0d0f797c093e1b0707875e6b08".match(/../g).map(x=>parseInt(x, 16))
// there is a limited character set
// mine has added q, v, w; also evidence from message panel show that the alphabet
// list likley didn't include m, but that was specifically drawn for "nostromo"
let az="0123456789abcdefghijlmnopqrstuvwyz-"
// 7 of the 8 display segments
let s7=[[0,1],[1,0],[0,-1],[-1,0],[0,-1],[1,0],[0,1]]
// print string s at left top, w, h and return left bottom+spacer
function prn(s,x0,y0,w,h) {
//rect(x0, y0, w, h)
// w = s.length*dx + (s.length-1)*(dx/3)
let dx = 3*w/(s.length*4-1)
let dy = h/2
let x = x0
let y = y0 + dy
for(let c of s) {
let p = az.indexOf(c)
let q = p >= 0 ? ss[p] : 0
for(let i = 0; i<7;i++) {
let x1 = x + dx*s7[i][0]
let y1 = y + dy*s7[i][1]
if (q & (1<<i)) {
line(x, y, x1, y1)
}
x = x1
y = y1
}
if (q & 128) {
line(x - dx/2, y, x - dx/2, y + dy)
}
x += dx/3
}
return y0 + h + 10
}
function setup() {
// "There was a bug that should have been fixed. At one point, the top of the
// tallest peak spilled over the line representing the edge of the display
// screen, as there was no clipping"
// The random seed/scaling was chosen to reproduce this; it visble in the
// movie too.
randomSeed(1)
// 1) Generation. On an equally spaced x-y grid of 50 x 50 points,
// a value of z was generated for the height of the ground at
// each point.
for (let y = 0; y < 50; y++) {
z[y]=[]
for (let x = 0; x < 50; x++) {
// "There was a valley running from front to back achieved by applying
// a flattened sine-wave..."
z[y][x]=10*(1-sin(x*PI/50)*sin((x+y)*PI/100))
// "There were three small ridges running diagonally across the area."
z[y][x]+=2*(y-x>30?1:0)
z[y][x]+=2*(y-x>10?1:0)
z[y][x]-=2*(y-x>-25?1:0)
// "The fourth term in the expression was a small random perturbation
// of each point, to add a little texture and interest."
z[y][x]+=random(0,0.5)
z[y][x] *= 4
}
}
// There were 400 hills altogether. Each normal hill covered an area
// of 5 x 5 grid points, and had a central peak with roughly symmetrical
// sides. About 20% of the hills were made into mountains covering 7x9
// grid points, with a higher limit on the size of the peak. About 15%
// of the hills were made asymmeterical, and a further 20% were made
// into holes rather than hills by changing the sign of the expression.
let hills = 0
while(hills < 400) {
let x0 = Math.floor(random(0, 50))
let y0 = Math.floor(random(0, 50))
// "The placing of the hills was controlled to give a roughly flat mountain-free
// area at the front of the picture"
let r = (x0-25)*(x0-25)+(y0-5)*(y0-5)/5
if (r < 120) {
continue
}
hills++
// "Each normal hill covered an area of 5 x 5 grid points, and had a central
// peak with roughly symmetrical sides. About 20% of the hills were made
// into mountains covering 7x9 grid points, with a higher limit on the
// size of the peak.""
// "a further 20% were made into holes rather than hills by changing
// the sign of the expression."
let [dx, dy, h0] = [[2,2,5],[2,2,5],[2,2,5],[4,3,10],[2,2,-5]][Math.floor(random(5))]
// "the hills at the back tended to be somewhat higher than those at the front."
let h = 20*h0*(y0+10)/r
for (let x = - dx; x < dx + 1; x++) {
if (x0 + x < 0 || x0 + x >= 50) {
continue
}
for (let y = - dy; y < dy + 1; y++) {
if (y0 + y < 0 || y0 + y >= 50) {
continue
}
// Sutcliffe also made some of these small hills asymmetrical,
// but with 400 hills already, I'm not sure how visible that would be.
z[y0+y][x0+x] += h*(1-abs(x)/dx)*(1-abs(y)/dy)
}
}
}
// the two columns on the right aren't described but one is
// a column of single letters stretched horizontally. Since Sutcliffe
// reused the letter drawing code a lot, the second is likely numbers
// stretched vertically.
// left colum is 25 random letters.
panel1 = Array.from("0123456789abcdefghijlmnopqrstuvwyz".substr(0,25)).reverse()
// second column looks like stretched numbers, possibly overlapping?
// just having a single stretched number gives an interesting barcode effect.
panel2 = Array.from("012345679012")
// "a loop of 720 frames (30 seconds at 24 frames a second)"
frameRate(24)
createCanvas(600, 400);
//saveGif("nostromo.gif",2)
}
function hlr(x0, x1, hz, y, altitude) {
if (y >= z.length) {
return
}
let dx = Math.ceil(x1)-Math.floor(x0)
let dzh = z[hz][Math.ceil(x1)] - z[hz][Math.floor(x0)]
let zh0 = z[hz][Math.floor(x0)]+dzh*(x0 - Math.floor(x0))/dx
// 2) View point. This three-dimensional information was converted,
// for each of a series of angles and distances, into two
// dimensional positions on the display screen. The series of
// angles corresponded to the view from the descending spaceship."
// "...from diminishing height and viewing angle to get the two-dimensional
// data for each viewpoint..."
// I don't think he calculated angles here, since that would have meant
// dealing with perspective: the clip just shows the gap between lines
// narrowing with decreasing altitude, so that's what I did.
let yh0 = 370-hz*6*((altitude+200)/920)-zh0
let zh1 = z[hz][Math.floor(x0)]+dzh*(x1 - Math.floor(x0))/dx
let yh1 = 370-hz*6*((altitude+200)/920)-zh1
let dz = z[y][Math.ceil(x1)] - z[y][Math.floor(x0)]
let z0 = z[y][Math.floor(x0)]+dz*(x0 - Math.floor(x0))/dx
let y0 = 370-y*6*((altitude+200)/920)-z0
let z1 = z[y][Math.floor(x0)]+dz*(x1 - Math.floor(x0))/dx
let y1 = 370-y*6*((altitude+200)/920)-z1
// "3) Hidden line removal. For each view, the scenery was displayed
// by drawing a line through each set of points with the same
// x-values in the original three dimensional form, but leaving
// out any lines that were visually below the horizon formed by
// the nearer lines. On this scheme, a flat plain would be
// represented by a set of parallel lines running across the screen.
// Finally the lines had to be plotted, a trivial step using Frolic."
// I swap x and y from Sutcliffe's description.
if (y0 <= yh0 && y1 <= yh1) {
// line is above horizon, draw it
line(x0 * 370/49 + 130, y0, x1*370/49+130, y1)
hlr(x0, x1, y, y+1, altitude)
} else if (yh0 <= y0 && yh1 <= y1) {
// horizon already drawn. recurse.
hlr(x0, x1, hz, y+1, altitude)
} else {
// the lines cross. find the crossing point,
// and only draw one side.
let k = (y0-yh0)/(yh1-yh0-y1+y0)
let x2 = x0 + (x1 - x0)*k
let y2 = y0 + (y1 - y0)*k
if (yh0 < y0) {
line(x2 * 370/49 + 130, y2, x1*370/49+130, y1)
// from the description, Sutcliffe may have split
// the array, but it's naturally written recursively
hlr(x0, x2, hz, y+1, altitude)
hlr(x2, x1, y, y+1, altitude)
} else {
line(x0 * 370/49 + 130, y0, x2*370/49+130, y2)
hlr(x0, x2, y, y+1, altitude)
hlr(x2, x1, hz, y+1, altitude)
}
}
}
function draw() {
background(0);
let loopFrame = frameCount%720;
let altitude = 719 - loopFrame
let spd = Math.ceil(altitude/9)
let angle = Math.floor(loopFrame/40+31)
aspect = (aspect+100 + (loopFrame % 13 == 0 ? round(random(-1, 1)) : 0))%100
line_stat = (line_stat+20 + (loopFrame % 7 == 0 ? round(random(-1, 1)) : 0))%20
probe = (probe+10 + (loopFrame % 11 == 0 ? round(random(-1, 1)) : 0))%10
stroke(255,180,15)
strokeWeight(1.8)
fill(0,0,0,0)
// The rectangular frames. Sutcliffe did this by redrawing '0'
rect(10,10,580,380)
rect(130,15,370,370)
// text at very top is illegible. maybe this? Sutcliffe said the
// alphabet had no "w" but the second word does match this.
prn("weyland yutani", 25, 15, 100, 5)
// The number here is Nostromo's registration number per the wiki.
// I'm not sure if that was real movie lore or someone read it
// from this image originally? Either way, it matches what's on screen
prn("nostromo 1809246", 140, 370, 120, 10)
// bottom right is completely illegible so I chose to have Alan sign it.
prn("alan sutcliffe", 410, 370, 80, 10)
rect(20,25,105,350)
rect(20,25,105,175)
let w1 = 95
let h1 = 12
let h2 = 25
let x = 25
let y = 30
y = prn("angle", x, y, w1, h1)
y = prn(`j${str(angle).padStart(2, " ")}r`, x, y, w1, h2)
y = prn("altitude", x, y, w1, h1)
y = prn(str(altitude).padStart(4, " "), x, y, w1, h2)
y = prn("aspect", x, y, w1, h1)
y = prn(`-${str(aspect).padStart("0", 2)}${(Math.floor(loopFrame/10))%2==0?"y":"-"}`, x, y, w1, h2)
// second frame
h1 = 20
y = prn("status", x, 205, w1, h1)
y = prn(`line${str(line_stat).padStart(2, "0")}`, x, y, w1, h1)
y = prn("lens o", x, y, w1, h1)
y = prn(`probe${probe}`, x, y, w1, h1)
y = prn(`speed${str(spd).padStart(3, " ")}`, x, y, w1, h1)
y = prn("hidden line", x, 360, w1, 10)
// the map. draw a column of lines at a time, with hidden line removal.
for(let x = 0; x < 49; x++) {
hlr(x, x+1, 0, 0, altitude)
}
// right hand panels.This was never on screen, only in the screenshots
// Sutcliffe preserved, so I can't tell how it moved, but he did say this:
// "Another type of animation was to make a word appear progressively.
// One new character every six frames, say, would give a rate of four characters
// per second, until the word or phrase was complete, then rub them out and start
// again. This gave the appearance of an urgent message. Another dodge was to
// synthesize a row of buttons or lights, each one consisting of several
// concentric letter "0"s."
// panel1 would be the message? and panel 2 must be the '0s'?
// I don't think the messages had meaningful text: one screenshot we have
// shows 'rponljihgfedcba0987654321', the other is the same except '0'
// replaces 'r'.
// I just go for randomly changing letters in those displays every frame.
panel1[Math.floor(random(panel1.length))] = az[Math.floor(random(33))]
panel2[Math.floor(random(panel2.length))] = az[Math.floor(random(10))]
for(let i = 0; i<panel1.length; i++) {
prn(panel1[i], 510, 20+14.5*i, 30, 9)
}
prn(panel2.join(""), 550,20,30,360)
// jittered scan lines
stroke(0)
strokeWeight(0.5)
for(let i= 1+random(0,0.1); i<600; i+=2) {
line(i, 0, i, 400)
}
//filter(BLUR,0.5)
filter(DILATE)
}
@bazzargh
Copy link
Author

bazzargh commented Feb 2, 2025

nostromo(3)

Rendered image.

@bazzargh
Copy link
Author

bazzargh commented Feb 2, 2025

Correcting myself: 1809246 was the registration number of the Nostromo, but the wiki most likely got that off the crew badges, not the blink-and-you'll-miss-it landing display; it's prominent along the bottom of each badge. I guess Alan and crew must have been given some kind of production bible to have got that detail on-screen.

The other easter egg in here was the bottom left text, "hidden line" which I guess is Sutcliffe signing the algorithm's name to the footage instead of his own.

@bazzargh
Copy link
Author

bazzargh commented Feb 3, 2025

Ah! Just saw a clip showing the chestburster scene and found out that in the original film the company was referred to as "welyan yutani" (no d on weylan); it's on the beer can. That explains why the words in the top left didn't have quite the right shape compared to the screenshots; it really actually says weylan.

Also, I think I've finally figured what the text is at the bottom right - it says "channel 67c" (or possibly channel 67r). From the letter shapes the first word would match the regex '^[co][hb][aefgpsyz][hbn][nd][aefgpsyz]l$' and channel was the only match in my dictionary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment