Created
February 2, 2025 18:24
-
-
Save bazzargh/d4728b26df6c9062878d9e85f808817f to your computer and use it in GitHub Desktop.
"Landing The Nostromo" implemented in p5.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.