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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more