supermosh
Version:
Datamosh in the browser
181 lines (180 loc) • 7.51 kB
JavaScript
const fps = 30;
const size = 16;
const shifts = [0, 1, -1, 2, -2, 4, -4, 8, -8];
const getMinShifts = (previous, real) => {
const { width, height } = previous;
const minShifts = {};
for (let xOffset = 0; xOffset < width; xOffset += size) {
if (!minShifts[xOffset])
minShifts[xOffset] = [];
for (let yOffset = 0; yOffset < height; yOffset += size) {
if (!minShifts[xOffset][yOffset])
minShifts[xOffset][yOffset] = { x: NaN, y: NaN };
const xMax = Math.min(xOffset + size, width);
const yMax = Math.min(yOffset + size, height);
let minDiff = +Infinity;
for (const xShift of shifts) {
for (const yShift of shifts) {
let diff = 0;
for (let x = xOffset; x < xMax; x++) {
for (let y = yOffset; y < yMax; y++) {
const xsrc = (x + xShift + width) % width;
const ysrc = (y + yShift + height) % height;
const isrc = 4 * (width * ysrc + xsrc);
const idst = 4 * (width * y + x);
diff += Math.abs(previous.data[isrc + 0] - real.data[idst + 0]);
diff += Math.abs(previous.data[isrc + 1] - real.data[idst + 1]);
diff += Math.abs(previous.data[isrc + 2] - real.data[idst + 2]);
}
}
if (diff < minDiff) {
minDiff = diff;
minShifts[xOffset][yOffset].x = xShift;
minShifts[xOffset][yOffset].y = yShift;
}
}
}
}
}
return minShifts;
};
const approximate = (previous, minShifts) => {
const { width, height } = previous;
const out = new ImageData(width, height);
for (let i = 3; i < out.data.length; i += 4) {
out.data[i] = 255;
}
for (let xOffset = 0; xOffset < width; xOffset += size) {
for (let yOffset = 0; yOffset < height; yOffset += size) {
const xMax = Math.min(xOffset + size, width);
const yMax = Math.min(yOffset + size, height);
for (let x = xOffset; x < xMax; x++) {
for (let y = yOffset; y < yMax; y++) {
const xsrc = (x + minShifts[xOffset][yOffset].x + width) % width;
const ysrc = (y + minShifts[xOffset][yOffset].y + height) % height;
const isrc = 4 * (width * ysrc + xsrc);
const idst = 4 * (width * y + x);
out.data[idst + 0] = previous.data[isrc + 0];
out.data[idst + 1] = previous.data[isrc + 1];
out.data[idst + 2] = previous.data[isrc + 2];
}
}
}
}
return out;
};
export const elementEvent = (element, eventName) => new Promise((resolve) => {
element.addEventListener(eventName, resolve, { once: true });
});
export const getDimensions = async (segments) => {
const allDimensions = await Promise.all(segments.map(async (segment) => {
const video = document.createElement('video');
video.src = segment.src;
await elementEvent(video, 'canplay');
const width = video.videoWidth;
const height = video.videoHeight;
return { width, height };
}));
const widths = new Set(allDimensions.map((d) => d.width));
const heights = new Set(allDimensions.map((d) => d.height));
if (widths.size > 1 || heights.size > 1) {
throw new Error('Videos do not all have the same dimensions');
}
return {
width: [...widths][0],
height: [...heights][0],
};
};
export const prepareGlideSegment = async (segment, renderRoot) => {
const video = document.createElement('video');
renderRoot.append(video);
video.src = segment.src;
await elementEvent(video, 'canplaythrough');
const width = video.videoWidth;
const height = video.videoHeight;
const canvas = document.createElement('canvas');
renderRoot.append(canvas);
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
video.currentTime = segment.time;
await elementEvent(video, 'seeked');
ctx.drawImage(video, 0, 0);
const previous = ctx.getImageData(0, 0, width, height);
const real = await (async () => {
while (!video.ended) {
video.currentTime += 1 / fps;
await elementEvent(video, 'seeked');
await new Promise((resolve) => requestAnimationFrame(resolve));
ctx.drawImage(video, 0, 0);
const ret = ctx.getImageData(0, 0, width, height);
if (ret.data.filter((r, i) => r !== previous.data[i]).length > 100) {
return ret;
}
}
throw new Error('All frames are almost identical');
})();
const minShifts = getMinShifts(previous, real);
video.remove();
canvas.remove();
return { ...segment, minShifts };
};
export const prepareMovementSegment = async (segment, renderRoot) => {
const video = document.createElement('video');
renderRoot.append(video);
video.src = segment.src;
await elementEvent(video, 'canplaythrough');
const width = video.videoWidth;
const height = video.videoHeight;
const canvas = document.createElement('canvas');
renderRoot.append(canvas);
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
video.currentTime = segment.start;
await elementEvent(video, 'seeked');
const minShiftss = [];
while (video.currentTime < segment.end) {
ctx.drawImage(video, 0, 0);
const previous = ctx.getImageData(0, 0, width, height);
video.currentTime += 1 / fps;
await elementEvent(video, 'seeked');
ctx.drawImage(video, 0, 0);
const real = ctx.getImageData(0, 0, width, height);
minShiftss.push(getMinShifts(previous, real));
}
video.remove();
canvas.remove();
return { ...segment, minShiftss };
};
export const runCopySegment = async (segment, ctx, renderRoot) => {
const video = document.createElement('video');
video.src = segment.src;
renderRoot.append(video);
await elementEvent(video, 'canplaythrough');
video.currentTime = segment.start;
await elementEvent(video, 'seeked');
while (video.currentTime < segment.end) {
ctx.drawImage(video, 0, 0);
video.currentTime += 1 / fps;
await elementEvent(video, 'seeked');
await new Promise((resolve) => requestAnimationFrame(resolve));
}
video.remove();
};
export const runGlideSegment = async (segment, ctx) => {
for (let i = 0; i < segment.length * fps; i++) {
const previous = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
const next = approximate(previous, segment.minShifts);
ctx.putImageData(next, 0, 0);
await new Promise((resolve) => requestAnimationFrame(resolve));
}
};
export const runMovementSegment = async (segment, ctx) => {
for (const minShifts of segment.minShiftss) {
const previous = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
const next = approximate(previous, minShifts);
ctx.putImageData(next, 0, 0);
await new Promise((resolve) => requestAnimationFrame(resolve));
}
};