Compare commits
No commits in common. "a4196851217ead3f94e0c1ef00142a51b2ebbf3d" and "7b194147ef932531e55fec94d3f635370273b3f2" have entirely different histories.
a419685121
...
7b194147ef
11 changed files with 34 additions and 253 deletions
127
dither-image.js
127
dither-image.js
|
@ -5,18 +5,14 @@ import {
|
||||||
import Jimp from 'Jimp';
|
import Jimp from 'Jimp';
|
||||||
|
|
||||||
|
|
||||||
function repeat(val, n) {
|
function zeroes(n) {
|
||||||
let arr = [];
|
let arr = [];
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
arr.push(val);
|
arr.push(0);
|
||||||
}
|
}
|
||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
function zeroes(n) {
|
|
||||||
return repeat(0, n);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toLinear(val) {
|
function toLinear(val) {
|
||||||
// use a 2.4 gamma approximation
|
// use a 2.4 gamma approximation
|
||||||
// this is BT.1886 compatible
|
// this is BT.1886 compatible
|
||||||
|
@ -588,60 +584,29 @@ function decimate(input, palette, n) {
|
||||||
|
|
||||||
// preface the reserved bits
|
// preface the reserved bits
|
||||||
let buckets = reserved.slice().map((c) => [atariRGB[c]]).concat([input.slice()]);
|
let buckets = reserved.slice().map((c) => [atariRGB[c]]).concat([input.slice()]);
|
||||||
if (input.length != 160) {
|
|
||||||
throw new Error('xxx bad input size');
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
let buckets = [input.slice()];
|
|
||||||
if (reserved.length > 0) {
|
|
||||||
let pxPerReserved = input.length;
|
|
||||||
for (let c of reserved) {
|
|
||||||
for (let i = 0; i < pxPerReserved; i++) {
|
|
||||||
buckets[0].unshift(atariRGB[c]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(buckets[0].length, 'xxx');
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
let magicSort = (picker) => (a, b) => {
|
|
||||||
let bychannel = picker(b) - picker(a);
|
|
||||||
if (bychannel) return bychannel;
|
|
||||||
|
|
||||||
let byluma = b.luma() - a.luma();
|
|
||||||
return byluma;
|
|
||||||
};
|
|
||||||
let medianCut = (bucket, range) => {
|
let medianCut = (bucket, range) => {
|
||||||
if (bucket.length < 2) {
|
if (bucket.length < 2) {
|
||||||
|
console.log(bucket);
|
||||||
throw new Error('short bucket');
|
throw new Error('short bucket');
|
||||||
}
|
}
|
||||||
//console.log('medianCut', bucket, range);
|
//console.log('medianCut', bucket, range);
|
||||||
// Sort by the channel with the greatest range,
|
// Sort by the channel with the greatest range,
|
||||||
// then cut the bucket in two at the median.
|
// then cut the bucket in two at the median.
|
||||||
if (range.g >= range.r && range.g >= range.b) {
|
if (range.g >= range.r && range.g >= range.b) {
|
||||||
//bucket.sort((a, b) => b.g - a.g);
|
bucket.sort((a, b) => b.g - a.g);
|
||||||
bucket.sort(magicSort((rgb) => rgb.g));
|
|
||||||
} else if (range.r >= range.g && range.r >= range.b) {
|
} else if (range.r >= range.g && range.r >= range.b) {
|
||||||
//bucket.sort((a, b) => b.r - a.r);
|
bucket.sort((a, b) => b.r - a.r);
|
||||||
bucket.sort(magicSort((rgb) => rgb.r));
|
|
||||||
} else if (range.b >= range.g && range.b >= range.r) {
|
} else if (range.b >= range.g && range.b >= range.r) {
|
||||||
//bucket.sort((a, b) => b.b - a.b);
|
bucket.sort((a, b) => b.b - a.b);
|
||||||
bucket.sort(magicSort((rgb) => rgb.b));
|
|
||||||
}
|
}
|
||||||
let half = bucket.length >> 1;
|
let half = bucket.length >> 1;
|
||||||
//console.log('cutting', half, bucket.length);
|
//console.log('cutting', half, bucket.length);
|
||||||
let [bottom, top] = [bucket.slice(0, half), bucket.slice(half)];
|
return [bucket.slice(0, half), bucket.slice(half)];
|
||||||
//console.log({bottom, top});
|
|
||||||
return [bottom, top];
|
|
||||||
//return [bucket.slice(0, half), bucket.slice(half)];
|
|
||||||
};
|
};
|
||||||
while (buckets.length < n) {
|
while (buckets.length < n) {
|
||||||
// Find the bucket with the greatest range in any channel
|
// Find the bucket with the greatest range in any channel
|
||||||
let ranges = buckets.map((bucket) => {
|
let ranges = buckets.map((bucket) => {
|
||||||
if (bucket.length == 0) {
|
|
||||||
throw new Error('xxx empty bucket');
|
|
||||||
}
|
|
||||||
let red = bucket.map((rgb) => rgb.r);
|
let red = bucket.map((rgb) => rgb.r);
|
||||||
let green = bucket.map((rgb) => rgb.g);
|
let green = bucket.map((rgb) => rgb.g);
|
||||||
let blue = bucket.map((rgb) => rgb.b);
|
let blue = bucket.map((rgb) => rgb.b);
|
||||||
|
@ -657,26 +622,21 @@ function decimate(input, palette, n) {
|
||||||
let greatest = 0;
|
let greatest = 0;
|
||||||
let index = -1;
|
let index = -1;
|
||||||
for (let i = 0; i < topRanges.length; i++) {
|
for (let i = 0; i < topRanges.length; i++) {
|
||||||
//if (topRanges[i] >= greatest) {
|
if (topRanges[i] >= greatest) {
|
||||||
if (topRanges[i] > greatest) {
|
|
||||||
greatest = topRanges[i];
|
greatest = topRanges[i];
|
||||||
index = i;
|
index = i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (index == -1) {
|
if (index == -1) {
|
||||||
// We just ran out of colors! Pad the buckets.
|
// We just ran out of colors! Pad the buckets.
|
||||||
//while (buckets.length < n) {
|
while (buckets.length < n) {
|
||||||
// buckets.push([new RGB(0, 0, 0)]);
|
buckets.push([new RGB(0, 0, 0)]);
|
||||||
//}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let [lo, hi] = medianCut(buckets[index], ranges[index]);
|
let [lo, hi] = medianCut(buckets[index], ranges[index]);
|
||||||
buckets.splice(index, 1, lo, hi);
|
buckets.splice(index, 1, lo, hi);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buckets.length > n) {
|
|
||||||
throw new Error('xxx too many colors assigned');
|
|
||||||
}
|
|
||||||
decimated = buckets.map((bucket) => {
|
decimated = buckets.map((bucket) => {
|
||||||
// Average the RGB colors in this chunk
|
// Average the RGB colors in this chunk
|
||||||
let rgb = bucket
|
let rgb = bucket
|
||||||
|
@ -763,7 +723,10 @@ function decimate(input, palette, n) {
|
||||||
let index = dists.indexOf(closest);
|
let index = dists.indexOf(closest);
|
||||||
return palette[index];
|
return palette[index];
|
||||||
});
|
});
|
||||||
|
// hack
|
||||||
decimated.sort((a, b) => a - b);
|
decimated.sort((a, b) => a - b);
|
||||||
|
//console.log(decimated);
|
||||||
|
decimated[0] = 0;
|
||||||
|
|
||||||
// Palette fits
|
// Palette fits
|
||||||
return dither(decimated);
|
return dither(decimated);
|
||||||
|
@ -779,25 +742,11 @@ async function loadImage(src) {
|
||||||
|
|
||||||
let width = image.bitmap.width;
|
let width = image.bitmap.width;
|
||||||
let height = image.bitmap.height;
|
let height = image.bitmap.height;
|
||||||
|
if (width != 160 || height != 160) {
|
||||||
let aspect = width / height;
|
|
||||||
let dar = 2 / 1.2;
|
|
||||||
if (aspect > ((320 / 1.2) / 192)) {
|
|
||||||
// wide
|
|
||||||
width = 160;
|
width = 160;
|
||||||
height = Math.round((width * image.bitmap.height / image.bitmap.width) * dar);
|
height = 160;
|
||||||
if (height & 1) {
|
image = image.resize(width, height);
|
||||||
height++;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// tall
|
|
||||||
height = 192;
|
|
||||||
width = Math.round((height * image.bitmap.width / image.bitmap.height) / dar);
|
|
||||||
if (width & 1) {
|
|
||||||
width++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
image = image.resize(width, height);
|
|
||||||
|
|
||||||
let rgba = image.bitmap.data.slice();
|
let rgba = image.bitmap.data.slice();
|
||||||
return {
|
return {
|
||||||
|
@ -834,12 +783,13 @@ async function convert(source) {
|
||||||
rgba
|
rgba
|
||||||
} = await loadImage(source);
|
} = await loadImage(source);
|
||||||
|
|
||||||
if (width > 160) {
|
if (width !== 160) {
|
||||||
throw new Error(`expected <160px width, got ${width} pixels`);
|
throw new Error(`expected 160px-compatible width, got ${width} pixels`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (height > 192) {
|
if (height !== 160) {
|
||||||
throw new Error(`expected <192px height, got ${height} pixels`);
|
// @fixme support up to 240px
|
||||||
|
throw new Error(`expected 160px height, got ${height} pixels`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rgba.length != width * 4 * height) {
|
if (rgba.length != width * 4 * height) {
|
||||||
|
@ -867,33 +817,19 @@ async function convert(source) {
|
||||||
allColors.push(i);
|
allColors.push(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
let left = [], right = [];
|
|
||||||
let padding = 0;
|
|
||||||
if (width < 160) {
|
|
||||||
padding = 160 - width;
|
|
||||||
|
|
||||||
let black = new RGB(0, 0, 0);
|
|
||||||
left = repeat(black, padding >> 1);
|
|
||||||
right = repeat(black, padding + 1 >> 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let lines = [];
|
let lines = [];
|
||||||
for (let y = 0; y < height; y++) {
|
for (let y = 0; y < height; y++) {
|
||||||
let inputLine = input
|
let inputLine = input
|
||||||
.slice(y * width, (y + 1) * width);
|
.slice(y * width, (y + 1) * width);
|
||||||
|
|
||||||
if (padding) {
|
|
||||||
inputLine = left.concat(inputLine, right);
|
|
||||||
}
|
|
||||||
if (y > 0) {
|
if (y > 0) {
|
||||||
let error = lines[y - 1].error;
|
let error = lines[y - 1].error;
|
||||||
inputLine = inputLine.map((rgb, x) => rgb.add(error[x]).clamp());
|
inputLine = inputLine.map((rgb, x) => rgb.add(error[x]));
|
||||||
}
|
}
|
||||||
let line = decimate(inputLine, allColors, 4, y);
|
let line = decimate(inputLine, allColors, 4, y);
|
||||||
lines.push(line);
|
lines.push(line);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
width: width + padding,
|
width,
|
||||||
height,
|
height,
|
||||||
lines
|
lines
|
||||||
};
|
};
|
||||||
|
@ -954,14 +890,12 @@ function genAssembly(width, height, nbits, lines) {
|
||||||
return `.data
|
return `.data
|
||||||
.export frame1_top
|
.export frame1_top
|
||||||
.export frame1_bottom
|
.export frame1_bottom
|
||||||
|
|
||||||
.export frame1_palette1_even
|
.export frame1_palette1_even
|
||||||
.export frame1_palette1_odd
|
.export frame1_palette1_odd
|
||||||
.export frame1_palette2_even
|
.export frame1_palette2_even
|
||||||
.export frame1_palette2_odd
|
.export frame1_palette2_odd
|
||||||
.export frame1_palette3_even
|
.export frame1_palette3_even
|
||||||
.export frame1_palette3_odd
|
.export frame1_palette3_odd
|
||||||
|
|
||||||
.export displaylist
|
.export displaylist
|
||||||
|
|
||||||
.segment "BUFFERS"
|
.segment "BUFFERS"
|
||||||
|
@ -970,10 +904,6 @@ function genAssembly(width, height, nbits, lines) {
|
||||||
frame1_top:
|
frame1_top:
|
||||||
${byte2byte(frame.bitmap.slice(0, half))}
|
${byte2byte(frame.bitmap.slice(0, half))}
|
||||||
|
|
||||||
.align 4096
|
|
||||||
frame1_bottom:
|
|
||||||
${byte2byte(frame.bitmap.slice(half))}
|
|
||||||
|
|
||||||
.align 128
|
.align 128
|
||||||
frame1_palette1_even:
|
frame1_palette1_even:
|
||||||
${byte2byte(even(frame.palette1))}
|
${byte2byte(even(frame.palette1))}
|
||||||
|
@ -998,19 +928,22 @@ ${byte2byte(even(frame.palette3))}
|
||||||
frame1_palette3_odd:
|
frame1_palette3_odd:
|
||||||
${byte2byte(odd(frame.palette3))}
|
${byte2byte(odd(frame.palette3))}
|
||||||
|
|
||||||
|
.align 4096
|
||||||
|
frame1_bottom:
|
||||||
|
${byte2byte(frame.bitmap.slice(half))}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.align 1024
|
.align 1024
|
||||||
displaylist:
|
displaylist:
|
||||||
; 24 lines overscan
|
; 40 lines overscan
|
||||||
.repeat 2
|
.repeat 4
|
||||||
.byte $70 ; 8 blank lines
|
.byte $70 ; 8 blank lines
|
||||||
.endrep
|
.endrep
|
||||||
; include a DLI to mark us as frame 0
|
; include a DLI to mark us as frame 0
|
||||||
.byte $f0 ; 8 blank lines
|
.byte $f0 ; 8 blank lines
|
||||||
|
|
||||||
; ${height} lines graphics
|
; 160 lines graphics
|
||||||
; ANTIC mode e (160px 2bpp, 1 scan line per line)
|
; ANTIC mode e (160px 2bpp, 1 scan line per line)
|
||||||
.byte $4e
|
.byte $4e
|
||||||
.addr frame1_top
|
.addr frame1_top
|
||||||
|
|
|
@ -28,13 +28,12 @@ sample_ptr = sample_ptrl
|
||||||
scanline = $86
|
scanline = $86
|
||||||
frame_counter = $89
|
frame_counter = $89
|
||||||
|
|
||||||
;height = 160
|
height = 160
|
||||||
height = 192
|
|
||||||
bytes_per_line = 40
|
bytes_per_line = 40
|
||||||
pages_per_frame = 32
|
pages_per_frame = 32
|
||||||
lines_per_frame = 262
|
lines_per_frame = 262
|
||||||
;scanline_offset = 31 + (40 - 24) / 2
|
;scanline_offset = 31 + (40 - 24) / 2
|
||||||
scanline_offset = 30
|
scanline_offset = 46
|
||||||
scanline_max = (lines_per_frame - scanline_offset) / 2
|
scanline_max = (lines_per_frame - scanline_offset) / 2
|
||||||
|
|
||||||
.data
|
.data
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
set -e
|
|
||||||
|
|
||||||
INFILE="$1"
|
|
||||||
# additional params can be input to the extraction
|
|
||||||
# for time or seek
|
|
||||||
shift
|
|
||||||
|
|
||||||
mkdir -p temp
|
|
||||||
|
|
||||||
ffmpeg \
|
|
||||||
-i "$INFILE" \
|
|
||||||
-r 60000/1001 \
|
|
||||||
-vf 'scale=256:-2' \
|
|
||||||
-an \
|
|
||||||
"$@" \
|
|
||||||
-y "temp/$INFILE-%04d.png"
|
|
||||||
|
|
||||||
ffmpeg \
|
|
||||||
-i "$INFILE" \
|
|
||||||
-vn \
|
|
||||||
-ac 1 \
|
|
||||||
-ar 15734 \
|
|
||||||
-acodec pcm_u8 \
|
|
||||||
"$@" \
|
|
||||||
-y "temp/$INFILE-audio.wav" || echo no audio
|
|
||||||
|
|
||||||
for frame in "temp/$INFILE-"[0-9][0-9][0-9][0-9].png
|
|
||||||
do
|
|
||||||
n="${frame#temp/$INFILE-}"
|
|
||||||
n="${n%.png}"
|
|
||||||
out="temp/$INFILE-dither-${n}"
|
|
||||||
last="${n:0-1}"
|
|
||||||
node ../dither-image.js "$frame" "$out" &
|
|
||||||
if (( last == 9 ))
|
|
||||||
then
|
|
||||||
echo "frame $n"
|
|
||||||
wait
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
wait
|
|
||||||
|
|
||||||
if [ -f "temp/$INFILE-audio.wav" ]
|
|
||||||
then
|
|
||||||
ffmpeg \
|
|
||||||
-r 60000/1001 \
|
|
||||||
-i "temp/$INFILE-dither-%04d.png" \
|
|
||||||
-i "temp/$INFILE-audio.wav" \
|
|
||||||
-ac 2 \
|
|
||||||
-ar 48000 \
|
|
||||||
-vf 'pad=w=534' \
|
|
||||||
-pix_fmt yuv420p \
|
|
||||||
-movflags +faststart \
|
|
||||||
-y "$INFILE-dither.mp4"
|
|
||||||
else
|
|
||||||
ffmpeg \
|
|
||||||
-r 60000/1001 \
|
|
||||||
-i "temp/$INFILE-dither-%04d.png" \
|
|
||||||
-vf 'pad=w=534' \
|
|
||||||
-pix_fmt yuv420p \
|
|
||||||
-movflags +faststart \
|
|
||||||
-y "$INFILE-dither.mp4"
|
|
||||||
fi
|
|
|
@ -1,10 +0,0 @@
|
||||||
ffmpeg \
|
|
||||||
-r 30000/1001 \
|
|
||||||
-i 'frames/dither-%04d.png' \
|
|
||||||
-i 'cats-audio.wav' \
|
|
||||||
-ac 2 \
|
|
||||||
-ar 48000 \
|
|
||||||
-vf 'pad=w=534' \
|
|
||||||
-pix_fmt yuv420p \
|
|
||||||
-movflags +faststart \
|
|
||||||
-y cats-dither.mp4
|
|
|
@ -1,17 +0,0 @@
|
||||||
set -a
|
|
||||||
|
|
||||||
mkdir -p frames
|
|
||||||
|
|
||||||
ffmpeg \
|
|
||||||
-i 'cats computer fun.mp4' \
|
|
||||||
-vf 'scale=256:144' \
|
|
||||||
-an \
|
|
||||||
-y 'frames/cats-%04d.png'
|
|
||||||
|
|
||||||
ffmpeg \
|
|
||||||
-i 'cats computer fun.mp4' \
|
|
||||||
-vn \
|
|
||||||
-ac 1 \
|
|
||||||
-ar 15734 \
|
|
||||||
-acodec pcm_u8 \
|
|
||||||
-y 'cats-audio.wav'
|
|
|
@ -1,15 +0,0 @@
|
||||||
set -e
|
|
||||||
|
|
||||||
for frame in frames/cats-[0-9][0-9][0-9][0-9].png
|
|
||||||
do
|
|
||||||
n="${frame#frames/cats-}"
|
|
||||||
n="${n%.png}"
|
|
||||||
out="frames/dither-${n}"
|
|
||||||
last="${n:0-1}"
|
|
||||||
node ../dither-image.js "$frame" "$out" &
|
|
||||||
if (( last == 9 ))
|
|
||||||
then
|
|
||||||
wait
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
wait
|
|
|
@ -1,10 +0,0 @@
|
||||||
ffmpeg \
|
|
||||||
-r 60000/1001 \
|
|
||||||
-i 'frames/dither-%04d.png' \
|
|
||||||
-i 'doom-audio.wav' \
|
|
||||||
-ac 2 \
|
|
||||||
-ar 48000 \
|
|
||||||
-vf 'pad=w=534' \
|
|
||||||
-pix_fmt yuv420p \
|
|
||||||
-movflags +faststart \
|
|
||||||
-y doom-dither.mp4
|
|
|
@ -1,22 +0,0 @@
|
||||||
set -a
|
|
||||||
|
|
||||||
mkdir -p frames
|
|
||||||
|
|
||||||
TIME=37.5
|
|
||||||
|
|
||||||
ffmpeg \
|
|
||||||
-i 'doom-speedrun.webm' \
|
|
||||||
-t $TIME \
|
|
||||||
-r 60000/1001 \
|
|
||||||
-vf 'scale=256:192' \
|
|
||||||
-an \
|
|
||||||
-y 'frames/doom-%04d.png'
|
|
||||||
|
|
||||||
ffmpeg \
|
|
||||||
-i 'doom-speedrun.webm' \
|
|
||||||
-t $TIME \
|
|
||||||
-vn \
|
|
||||||
-ac 1 \
|
|
||||||
-ar 15734 \
|
|
||||||
-acodec pcm_u8 \
|
|
||||||
-y 'doom-audio.wav'
|
|
|
@ -1,15 +0,0 @@
|
||||||
set -e
|
|
||||||
|
|
||||||
for frame in frames/doom-[0-9][0-9][0-9][0-9].png
|
|
||||||
do
|
|
||||||
n="${frame#frames/doom-}"
|
|
||||||
n="${n%.png}"
|
|
||||||
out="frames/dither-${n}"
|
|
||||||
last="${n:0-1}"
|
|
||||||
node ../dither-image.js "$frame" "$out" &
|
|
||||||
if (( last == 9 ))
|
|
||||||
then
|
|
||||||
wait
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
wait
|
|
|
@ -4,7 +4,7 @@ ffmpeg \
|
||||||
-i 'colamath-audio.wav' \
|
-i 'colamath-audio.wav' \
|
||||||
-ac 2 \
|
-ac 2 \
|
||||||
-ar 48000 \
|
-ar 48000 \
|
||||||
-vf 'pad=w=534' \
|
-vf 'pad=w=640:h=360:x=52:y=20' \
|
||||||
-pix_fmt yuv420p \
|
-pix_fmt yuv420p \
|
||||||
-movflags +faststart \
|
-movflags +faststart \
|
||||||
-y colamath-dither.mp4
|
-y colamath-dither.mp4
|
||||||
|
|
|
@ -4,7 +4,7 @@ mkdir -p frames
|
||||||
|
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
-i colamath-dv.avi \
|
-i colamath-dv.avi \
|
||||||
-vf 'yadif=1,scale=256:192' \
|
-vf 'yadif=1,scale=160:200,crop=h=160' \
|
||||||
-an \
|
-an \
|
||||||
-y 'frames/colamath-%04d.png'
|
-y 'frames/colamath-%04d.png'
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue