Compare commits

...

7 commits

Author SHA1 Message Date
a419685121 nice 2023-03-25 21:36:14 -07:00
5196adef43 tweak 2023-03-25 20:26:45 -07:00
7fa606743f tweak 2023-03-25 20:19:27 -07:00
b0e7d1f579 wip mostly fixed 2023-03-25 20:15:34 -07:00
9631e2e026 fixes 2023-03-25 19:43:57 -07:00
38e9af3843 wip 2023-03-25 18:57:09 -07:00
e5adb72851 wip cats 2023-03-25 18:28:00 -07:00
11 changed files with 253 additions and 34 deletions

View file

@ -5,14 +5,18 @@ import {
import Jimp from 'Jimp'; import Jimp from 'Jimp';
function zeroes(n) { function repeat(val, n) {
let arr = []; let arr = [];
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
arr.push(0); arr.push(val);
} }
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
@ -584,29 +588,60 @@ 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);
return [bucket.slice(0, half), bucket.slice(half)]; let [bottom, top] = [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);
@ -622,21 +657,26 @@ 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
@ -723,10 +763,7 @@ 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);
@ -742,11 +779,25 @@ 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 = 160; height = Math.round((width * image.bitmap.height / image.bitmap.width) * dar);
image = image.resize(width, height); if (height & 1) {
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 {
@ -783,13 +834,12 @@ async function convert(source) {
rgba rgba
} = await loadImage(source); } = await loadImage(source);
if (width !== 160) { if (width > 160) {
throw new Error(`expected 160px-compatible width, got ${width} pixels`); throw new Error(`expected <160px width, got ${width} pixels`);
} }
if (height !== 160) { if (height > 192) {
// @fixme support up to 240px throw new Error(`expected <192px height, got ${height} pixels`);
throw new Error(`expected 160px height, got ${height} pixels`);
} }
if (rgba.length != width * 4 * height) { if (rgba.length != width * 4 * height) {
@ -817,19 +867,33 @@ 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])); inputLine = inputLine.map((rgb, x) => rgb.add(error[x]).clamp());
} }
let line = decimate(inputLine, allColors, 4, y); let line = decimate(inputLine, allColors, 4, y);
lines.push(line); lines.push(line);
} }
return { return {
width, width: width + padding,
height, height,
lines lines
}; };
@ -890,12 +954,14 @@ 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"
@ -904,6 +970,10 @@ 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))}
@ -928,22 +998,19 @@ ${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:
; 40 lines overscan ; 24 lines overscan
.repeat 4 .repeat 2
.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
; 160 lines graphics ; ${height} 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

View file

@ -28,12 +28,13 @@ 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 = 46 scanline_offset = 30
scanline_max = (lines_per_frame - scanline_offset) / 2 scanline_max = (lines_per_frame - scanline_offset) / 2
.data .data

62
video-bulk/atarifiy.sh Normal file
View file

@ -0,0 +1,62 @@
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

10
video-cat/combine.sh Normal file
View file

@ -0,0 +1,10 @@
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

17
video-cat/extract.sh Normal file
View file

@ -0,0 +1,17 @@
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'

15
video-cat/video.sh Normal file
View file

@ -0,0 +1,15 @@
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

10
video-doom/combine.sh Normal file
View file

@ -0,0 +1,10 @@
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

22
video-doom/extract.sh Normal file
View file

@ -0,0 +1,22 @@
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'

15
video-doom/video.sh Normal file
View file

@ -0,0 +1,15 @@
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

View file

@ -4,7 +4,7 @@ ffmpeg \
-i 'colamath-audio.wav' \ -i 'colamath-audio.wav' \
-ac 2 \ -ac 2 \
-ar 48000 \ -ar 48000 \
-vf 'pad=w=640:h=360:x=52:y=20' \ -vf 'pad=w=534' \
-pix_fmt yuv420p \ -pix_fmt yuv420p \
-movflags +faststart \ -movflags +faststart \
-y colamath-dither.mp4 -y colamath-dither.mp4

View file

@ -4,7 +4,7 @@ mkdir -p frames
ffmpeg \ ffmpeg \
-i colamath-dv.avi \ -i colamath-dv.avi \
-vf 'yadif=1,scale=160:200,crop=h=160' \ -vf 'yadif=1,scale=256:192' \
-an \ -an \
-y 'frames/colamath-%04d.png' -y 'frames/colamath-%04d.png'