-
-
Save bazzargh/d4728b26df6c9062878d9e85f808817f to your computer and use it in GitHub Desktop.
// 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) | |
} |
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.
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.
Rendered image.