Files
pgames-manager/frontend/src/player/movie.ts
T
mrleo1nid f9eb5c1212
CI / frontend (push) Failing after 15s
CI / backend (push) Failing after 37s
feat: improve RenPy conversion and support MIST runtime
2026-10-25 23:16:33 +03:00

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;
}