Public
Edited
May 6, 2021
1 fork
Insert cell
Insert cell
Insert cell
image = FileAttachment("rescue-raiders-words.png").image()
Insert cell
Insert cell
Insert cell
Insert cell
sourceImage = {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = width;
canvas.height = height;
context.fillStyle = 'rgba(0,0,0,1)';
context.fillRect(0, 0, width, height);
const topLeft = C.topLeft;
context.drawImage(image, topLeft.x, topLeft.y);
return canvas;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
phaseInfo = {
const c = C.colorBurst / 2 / Math.PI;
return c - Math.floor(c)
}
Insert cell
sampleRate = C.NTSC_4FSC
Insert cell
subcarrier = C.NTSC_FSC / sampleRate
Insert cell
w = normalize(chebyshevWindow(17, 50))
Insert cell
Plot.plot({
height: 150,
marks: [Plot.barY(w.map((d,i) => ({index: i, w: d})), { x: "index", y: "w" })],
x: {label: "chebyshevWindow(17,50)"},
})
Insert cell
yBandwidth = luma_bandwidth / sampleRate
Insert cell
uBandwidth = chroma_bandwidth / sampleRate
Insert cell
vBandwidth = uBandwidth
Insert cell
wy = normalize(mul(w, lanczosWindow(17, yBandwidth)))
Insert cell
Insert cell
Insert cell
wu = scale(normalize(mul(w, lanczosWindow(17, uBandwidth))), 2)
Insert cell
Insert cell
Insert cell
wv = scale(normalize(mul(w, lanczosWindow(17, vBandwidth))), 2)
Insert cell
cs = [8, 7, 6, 5, 4, 3, 2, 1, 0].map(i => [wy[i], wu[i], wv[i]])
Insert cell
Insert cell
matrix0 = Matrix3.identity()
Insert cell
matrix1 = new Matrix3(1, 0, 0,
0, saturation, 0,
0, 0, saturation).mul(matrix0)
Insert cell
matrix2 = {
const h = 2 * Math.PI * Hue;
return new Matrix3(1, 0, 0,
0, Math.cos(h), -Math.sin(h),
0, Math.sin(h), Math.cos(h)).mul(matrix1);
}
Insert cell
// Decode Y'UV decoder matrix
matrix3 = new Matrix3(1, 1, 1,
0, -0.394642, 2.032062,
1.139883, -0.580622, 0).mul(matrix2)
Insert cell
matrix4 = matrix3.mul(Math.max(contrast, 0))
Insert cell
decoderMatrix = matrix4
Insert cell
decoderTranspose = decoderMatrix.transpose()
Insert cell
Insert cell
sourceData = sourceImage.getContext('2d').getImageData(0, 0, width, height).data
Insert cell
function getRawPixel(x, y) {
x = Math.min(Math.max(x, 0), width - 1);
y = Math.min(Math.max(y, 0), height - 1);

let red = (y * width + x) * 4;
return [sourceData[red], sourceData[red + 1], sourceData[red + 2]];
}
Insert cell
function pixel(x, y) {
let c = scale(getRawPixel(x, y), 1.0 / 255.0);
let p = phaseInfo;
let phase = 2.0 * Math.PI * (subcarrier * (x + 0.5) + p);
return mul(c, [1.0, Math.sin(phase), Math.cos(phase)]);
}
Insert cell
pixels = (x, y, i) => add(pixel(x + i, y), pixel(x - i, y))
Insert cell
function colorAt(x, y) {
let c = mul(pixel(x, y), cs[0]);
c = add(c, mul(pixels(x, y, 1), cs[1]));
c = add(c, mul(pixels(x, y, 2), cs[2]));
c = add(c, mul(pixels(x, y, 3), cs[3]));
c = add(c, mul(pixels(x, y, 4), cs[4]));
c = add(c, mul(pixels(x, y, 5), cs[5]));
c = add(c, mul(pixels(x, y, 6), cs[6]));
c = add(c, mul(pixels(x, y, 7), cs[7]));
c = add(c, mul(pixels(x, y, 8), cs[8]));
return decoderTranspose.mul(c);
return c;
}
Insert cell
decoderMatrix.mul([0.5, 0.5, 0.5])
Insert cell
newImage = {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const width = sourceImage.width;
const height = sourceImage.height;
canvas.width = width;
canvas.height = height * 2;
ctx.fillStyle = "rgba(0,0,0,1)";
ctx.fillRect(0, 0, width, height * 2);
let imageData = ctx.getImageData(0, 0, width, height * 2);
const data = imageData.data;
const setPixel = (x, y, r, g, b) => {
let red = (y * width + x) * 4;
data[red] = r;
data[red + 1] = g;
data[red + 2] = b;
};
for (let x = 84; x < 660; x++) {
for (let y = 19; y < 211; y++) {
const sourceColor = scale(colorAt(x, y), 255.0);
setPixel(x, y * 2, sourceColor[0], sourceColor[1], sourceColor[2]);
setPixel(x, y * 2 + 1, sourceColor[0], sourceColor[1], sourceColor[2]);
}
}
ctx.putImageData(imageData, 0, 0);
return canvas;
}
Insert cell
Insert cell
function realIDFT(ary) {
const size = ary.length;
const w = Array(size).fill(0);
for (let i = 0; i < size; i++) {
const omega = 2 * Math.PI * i / size;
for (let j = 0; j < size; j++) {
w[i] += ary[j] * Math.cos(j * omega);
}
}

return scale(w, 1/size);
}
Insert cell
// Chebyshev Window
// Based on ideas at http://www.dsprelated.com/showarticle/42.php
function chebyshevWindow(n, sidelobeDb) {
const m = n - 1;
let w = Array(m).fill(0);
const alpha = Math.cosh(Math.acosh(Math.pow(10, sidelobeDb / 20)) / m);
for (let i = 0; i < m; i++) {
const a = Math.abs(alpha * Math.cos(Math.PI * i / m));
if (a > 1)
w[i] = Math.pow(-1, i) * Math.cosh(m * Math.acosh(a));
else
w[i] = Math.pow(-1, i) * Math.cos(m * Math.acos(a));
}

w = realIDFT(w);
w = resize(w, n);
w[0] /= 2;
w[n - 1] = w[0];
const max = w.reduce((prev, cur) => Math.max(prev, Math.abs(cur)));
return scale(w, 1/max);
}
Insert cell
// Lanczos Window
function lanczosWindow(n, fc) {
let v = Array(n).fill(0);
fc = Math.min(fc, 0.5);
const halfN = Math.floor(n / 2);
for (let i = 0; i < n; i++) {
const x = 2 * Math.PI * fc * (i - halfN);
v[i] = (x == 0.0) ? 1.0 : Math.sin(x) / x;
}
return v;
}
Insert cell
Insert cell
C = (() => {
const HORIZ_START = 16;
const HORIZ_BLANK = (9 + HORIZ_START) // 25;
const HORIZ_DISPLAY = 40;
const HORIZ_TOTAL = (HORIZ_BLANK + HORIZ_DISPLAY) // 65;
const CELL_WIDTH = 14;
const VERT_NTSC_START = 38;
const VERT_DISPLAY = 192;

// From CanvasInterface.h

// See https://en.wikipedia.org/wiki/Colorburst
const NTSC_FSC = 315 / 88 * 1e6; // 3579545 = 3.5 Mhz: Color Subcarrier
const NTSC_4FSC = 4 * NTSC_FSC; // 14318180 = 14.3 Mhz
const NTSC_HLENGTH = (52 + 8 / 9) * 1e-6;
const NTSC_HHALF = (35 + 2 / 3) * 1e-6;
const NTSC_HSTART = NTSC_HHALF - NTSC_HLENGTH / 2;
const NTSC_VLENGTH = 240;
const NTSC_VSTART = 19;

const ntscClockFrequency = NTSC_4FSC * HORIZ_TOTAL / 912;
const ntscVisibleRect = {x: ntscClockFrequency * NTSC_HSTART, y: NTSC_VSTART,
width: ntscClockFrequency * NTSC_HLENGTH, height: NTSC_VLENGTH};
const ntscDisplayRect = {x: HORIZ_START, y: VERT_NTSC_START,
width: HORIZ_DISPLAY, height: VERT_DISPLAY};

const vertStart = ntscDisplayRect.y;
// first displayed column.
const horizStart = Math.floor(ntscDisplayRect.x);
// imageSize is [14 * visible rect width in cells, visible lines]
const imageSize = {width: Math.floor(CELL_WIDTH * ntscVisibleRect.width),
height: Math.floor(ntscVisibleRect.height)};
// imageLeft is # of pixels from first visible point to first displayed point.
const imageLeft = Math.floor((horizStart - ntscVisibleRect.x) * CELL_WIDTH);
const colorBurst = 2 * Math.PI * (-33 / 360 + (imageLeft % 4) / 4);
// First pixel that OpenEmulator draws when painting normally.
const topLeft = {x: imageLeft, y: vertStart - ntscVisibleRect.y};
// First pixel that OpenEmulator draws when painting 80-column mode.
const topLeft80Col = {x: imageLeft - CELL_WIDTH / 2, y: vertStart - ntscVisibleRect.y};

return {
imageSize,
colorBurst,
topLeft,
NTSC_FSC,
NTSC_4FSC,
}
})()
Insert cell
Insert cell
Insert cell
d3 = require("d3@6")
Insert cell
// Scale elements of an array so they sum to 1.
function normalize(ary) {
let sum = ary.reduce((a, b) => a + b, 0)
return scale(ary, 1.0/sum);
}
Insert cell
mul = (ary1, ary2) => ary1.map((e, i) => e * ary2[i])
Insert cell
add = (ary1, ary2) => ary1.map((e, i) => e + ary2[i])
Insert cell
scale = (ary, factor) => ary.map((x) => x * factor)
Insert cell
function resize(ary, n) {
let w = Array(n).fill(0);
for (let i = 0; i < Math.min(n, ary.length); i++) {
w[i] = ary[i];
}
return w;
}
Insert cell
class Matrix3 {
constructor(c00, c01, c02, c10, c11, c12, c20, c21, c22) {
this.data = [
c00 || 0,
c01 || 0,
c02 || 0,
c10 || 0,
c11 || 0,
c12 || 0,
c20 || 0,
c21 || 0,
c22 || 0
];
}

static identity() {
return new Matrix3(1, 0, 0, 0, 1, 0, 0, 0, 1);
}

mul(val) {
if (typeof val == "number") {
return new Matrix3(...this.data.map((x) => x * val));
}
if (val instanceof Array) {
// vector
let v = Array(3).fill(0);
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
v[i] += this.data[i * 3 + j] * val[j];
}
}
return v;
}
let m = new Matrix3();
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
for (let k = 0; k < 3; k++) {
m.data[i * 3 + j] += val.data[i * 3 + k] * this.data[k * 3 + j];
}
}
}
return m;
}

transpose() {
let m = new Matrix3();
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
m.data[j * 3 + i] = this.data[i * 3 + j];
}
}
return m;
}
}
Insert cell
Plot = require("@observablehq/plot@0.1")
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more