fix: a screen's video no longer flickers and its button stays clickable while a timer redraws the screen
MIST's cutscene screen has a 0.1s timer that changes a variable, and every change rebuilt the whole screen: the <video> restarted each time and "Skip" was replaced between press and release, so the click never landed. Videos (Movie(play=...) and the movie channel picture) are now kept per shown screen and resumed if the browser paused them, and a redraw requested while a pointer is down waits for its release. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
16ba20816c
commit
8251fca9bf
2 files changed
+93
-20
No files matched your search
@@ -136,6 +136,34 @@ export class ScreenManager {
|
||||
private styles: Record<string, StyleDef>) {
|
||||
this.layer = document.createElement("div");
|
||||
this.layer.className = "pg-usr";
|
||||
// A click needs its button to stay in the page between press and release; a redraw in between would lose it
|
||||
window.addEventListener("pointerdown", this.onPress, true);
|
||||
window.addEventListener("pointerup", this.onRelease, true);
|
||||
window.addEventListener("pointercancel", this.onRelease, true);
|
||||
}
|
||||
|
||||
private pressed = false;
|
||||
private refreshAfterPress = false;
|
||||
private onPress = () => { this.pressed = true; };
|
||||
private onRelease = () => {
|
||||
this.pressed = false;
|
||||
// after the click handlers of this release have run
|
||||
window.setTimeout(() => { if (this.refreshAfterPress && !this.pressed) { this.refreshAfterPress = false; this.refresh(); } }, 0);
|
||||
};
|
||||
|
||||
/** Videos survive a redraw of their screen: a new element would restart the picture. */
|
||||
private movies = new Map<string, HTMLElement>();
|
||||
private moviesSeen = new Set<string>();
|
||||
private movieCount = new Map<string, number>();
|
||||
|
||||
private cachedMovie(key: string, make: () => HTMLElement): HTMLElement {
|
||||
const n = (this.movieCount.get(key) ?? 0) + 1;
|
||||
this.movieCount.set(key, n);
|
||||
const id = `${this.rendering?.name ?? ""}|${key}|${n}`;
|
||||
let element = this.movies.get(id);
|
||||
if (!element) { element = make(); this.movies.set(id, element); }
|
||||
this.moviesSeen.add(id);
|
||||
return element;
|
||||
}
|
||||
|
||||
has(name: string) {
|
||||
@@ -212,6 +240,9 @@ export class ScreenManager {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
window.removeEventListener("pointerdown", this.onPress, true);
|
||||
window.removeEventListener("pointerup", this.onRelease, true);
|
||||
window.removeEventListener("pointercancel", this.onRelease, true);
|
||||
this.clear();
|
||||
this.layer.remove();
|
||||
}
|
||||
@@ -238,12 +269,14 @@ export class ScreenManager {
|
||||
inst.runners.forEach((r) => r.stop());
|
||||
inst.runners = [];
|
||||
inst.el.remove();
|
||||
for (const id of [...this.movies.keys()]) if (id.startsWith(`${inst.name}|`)) this.movies.delete(id);
|
||||
if (forget) this.shown.delete(inst.name);
|
||||
}
|
||||
|
||||
/** Re-evaluate every screen; coalesced so many state changes cost one rebuild per frame. */
|
||||
refresh() {
|
||||
if (this.suspended || this.pending || !this.shown.size) return;
|
||||
if (this.pressed) { this.refreshAfterPress = true; return; }
|
||||
this.pending = requestAnimationFrame(() => {
|
||||
this.pending = 0;
|
||||
if (this.suspended) return;
|
||||
@@ -316,6 +349,8 @@ export class ScreenManager {
|
||||
// --- rendering -----------------------------------------------------------------------
|
||||
|
||||
private render(inst: Instance) {
|
||||
this.moviesSeen.clear();
|
||||
this.movieCount.clear();
|
||||
const previousScroll = [...inst.el.querySelectorAll<HTMLElement>(".pg-sviewport")].map((e) => e.scrollTop);
|
||||
inst.timers.forEach((t) => window.clearTimeout(t));
|
||||
inst.timers = [];
|
||||
@@ -336,6 +371,16 @@ export class ScreenManager {
|
||||
this.nodes(inst.def.body, ctx, children);
|
||||
inst.el.replaceChildren(...children);
|
||||
});
|
||||
// a video taken out of the page pauses by itself; carry on, and forget the ones no screen shows now
|
||||
for (const [id, element] of [...this.movies]) {
|
||||
if (this.moviesSeen.has(id)) {
|
||||
const video = element instanceof HTMLVideoElement ? element : element.querySelector("video");
|
||||
if (video && video.paused && !video.ended) void video.play().catch(() => {});
|
||||
} else if (id.startsWith(`${inst.name}|`)) {
|
||||
if (element instanceof HTMLVideoElement) element.pause();
|
||||
this.movies.delete(id);
|
||||
}
|
||||
}
|
||||
for (const [node, clock] of [...inst.clocks]) {
|
||||
if (!inst.clocksSeen.has(node)) {
|
||||
window.clearTimeout(clock.handle);
|
||||
@@ -811,6 +856,28 @@ export class ScreenManager {
|
||||
|
||||
// --- displayables --------------------------------------------------------------------
|
||||
|
||||
/** The picture of the channel that plays video; one element per shown screen (see cachedMovie). */
|
||||
private channelMovie(value: Displayable): HTMLElement {
|
||||
const box = document.createElement("div");
|
||||
const size = value.kw.size;
|
||||
box.style.width = Array.isArray(size) ? `${size[0]}px` : "100%";
|
||||
box.style.height = Array.isArray(size) ? `${size[1]}px` : "100%";
|
||||
const channel = String(value.kw.channel ?? "movie");
|
||||
const sync = () => {
|
||||
const video = this.host.audio.mediaOf(channel);
|
||||
if (video instanceof HTMLVideoElement && video.parentElement !== box) {
|
||||
video.style.width = video.style.height = "100%";
|
||||
box.replaceChildren(video);
|
||||
} else if (!video && box.firstChild) box.replaceChildren();
|
||||
if (box.isConnected || !box.dataset.seen) {
|
||||
if (box.isConnected) box.dataset.seen = "1";
|
||||
window.setTimeout(sync, 200);
|
||||
}
|
||||
};
|
||||
sync();
|
||||
return box;
|
||||
}
|
||||
|
||||
/** An element for an image name/path or for a Crop/Solid/Frame/Transform built by game code. */
|
||||
private displayable(value: any, depth = 0): HTMLElement | null {
|
||||
if (depth > 8 || value === null || value === undefined) return null;
|
||||
@@ -896,27 +963,10 @@ export class ScreenManager {
|
||||
const src = this.host.fileUrl(pyStr(value.kw.play ?? a));
|
||||
const mask = value.kw.mask ? this.host.fileUrl(pyStr(value.kw.mask)) : null;
|
||||
if (!src || (value.kw.mask && !mask)) throw new Error("Не найдено видео или маска Movie");
|
||||
return movieDisplayable(src, mask, value.kw.loop !== false);
|
||||
return this.cachedMovie(`${src}|${mask}|${value.kw.loop !== false}`, () => movieDisplayable(src, mask, value.kw.loop !== false));
|
||||
}
|
||||
// the picture of the channel that plays video (renpy.music.play(..., channel="movie"))
|
||||
const box = document.createElement("div");
|
||||
const size = value.kw.size;
|
||||
box.style.width = Array.isArray(size) ? `${size[0]}px` : "100%";
|
||||
box.style.height = Array.isArray(size) ? `${size[1]}px` : "100%";
|
||||
const channel = String(value.kw.channel ?? "movie");
|
||||
const sync = () => {
|
||||
const video = this.host.audio.mediaOf(channel);
|
||||
if (video instanceof HTMLVideoElement && video.parentElement !== box) {
|
||||
video.style.width = video.style.height = "100%";
|
||||
box.replaceChildren(video);
|
||||
} else if (!video && box.firstChild) box.replaceChildren();
|
||||
if (box.isConnected || !box.dataset.seen) {
|
||||
if (box.isConnected) box.dataset.seen = "1";
|
||||
window.setTimeout(sync, 200);
|
||||
}
|
||||
};
|
||||
sync();
|
||||
return box;
|
||||
return this.cachedMovie(`channel|${String(value.kw.channel ?? "movie")}|${JSON.stringify(value.kw.size ?? null)}`, () => this.channelMovie(value));
|
||||
}
|
||||
case "alphamask": {
|
||||
const child = this.displayable(a, depth + 1);
|
||||
|
||||
Reference in new issue
Block a user