55 lines
2.5 KiB
TypeScript
55 lines
2.5 KiB
TypeScript
/** Ren'Py uses the mask movie's red channel as opacity, not its alpha channel. */
|
|
export function applyMovieMask(picture: Uint8ClampedArray, mask: Uint8ClampedArray) {
|
|
if (picture.length !== mask.length) throw new Error("Размеры кадров маски не совпадают");
|
|
for (let i = 3; i < picture.length; i += 4) picture[i] = Math.round(picture[i] * mask[i - 3] / 255);
|
|
}
|
|
|
|
export function movieDisplayable(source: string, mask: string | null, loop = true): HTMLElement {
|
|
const video = document.createElement("video");
|
|
video.src = source;
|
|
video.autoplay = true;
|
|
video.loop = loop;
|
|
video.muted = true;
|
|
video.playsInline = true;
|
|
if (!mask) return video;
|
|
const alpha = document.createElement("video");
|
|
alpha.src = mask;
|
|
alpha.loop = loop;
|
|
alpha.muted = true;
|
|
alpha.playsInline = true;
|
|
const canvas = document.createElement("canvas");
|
|
canvas.className = "pg-masked-movie";
|
|
const maskCanvas = document.createElement("canvas");
|
|
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
const maskCtx = maskCanvas.getContext("2d", { willReadFrequently: true });
|
|
if (!ctx || !maskCtx) throw new Error("Не удалось создать поверхность для видео с маской");
|
|
let last = -1;
|
|
let failed = false;
|
|
const draw = () => {
|
|
if (failed) return;
|
|
if (!canvas.isConnected) { video.pause(); alpha.pause(); return; }
|
|
if (video.readyState >= 2 && alpha.readyState >= 2 && video.videoWidth && video.currentTime !== last) {
|
|
if (Math.abs(alpha.currentTime - video.currentTime) > 0.06) alpha.currentTime = video.currentTime;
|
|
const w = video.videoWidth, h = video.videoHeight;
|
|
if (canvas.width !== w || canvas.height !== h) {
|
|
canvas.width = maskCanvas.width = w;
|
|
canvas.height = maskCanvas.height = h;
|
|
}
|
|
ctx.clearRect(0, 0, w, h);
|
|
ctx.drawImage(video, 0, 0, w, h);
|
|
maskCtx.drawImage(alpha, 0, 0, w, h);
|
|
const pixels = ctx.getImageData(0, 0, w, h);
|
|
applyMovieMask(pixels.data, maskCtx.getImageData(0, 0, w, h).data);
|
|
ctx.putImageData(pixels, 0, 0);
|
|
last = video.currentTime;
|
|
}
|
|
if (video.ended && !loop) { video.pause(); alpha.pause(); return; }
|
|
requestAnimationFrame(draw);
|
|
};
|
|
video.onerror = alpha.onerror = () => { failed = true; canvas.dataset.error = "Не удалось загрузить видео или его маску"; video.pause(); alpha.pause(); };
|
|
void video.play().catch(() => {});
|
|
void alpha.play().catch(() => {});
|
|
requestAnimationFrame(draw);
|
|
return canvas;
|
|
}
|