Fix Monkeydo events and #3

Monkeydo events will now be executed through the Proxy created in 8b659066cc

This commit also implements a solution for #3 by having each window report to its PlayerManager when it has been closed.
This commit is contained in:
Victor Westerlund 2021-12-05 22:09:56 +01:00
parent 8b659066cc
commit 7a6ed67d92
8 changed files with 1784 additions and 1702 deletions

View file

@ -3,9 +3,9 @@ import { default as Player } from "./PlayerWindow.mjs";
export default class PlayerManager { export default class PlayerManager {
constructor() { constructor() {
this.players = { this.players = {
"lyrics": new Player("lyrics","monkeydo_lyrics.json")//, "lyrics": new Player("lyrics","monkeydo_lyrics.json"),
//"credits": new Player("credits","monkeydo_credits.json"), "credits": new Player("credits","monkeydo_credits.json"),
//"art": new Player("art") "art": new Player("art")
}; };
this.channels = new WeakMap(); this.channels = new WeakMap();
@ -20,16 +20,20 @@ export default class PlayerManager {
} }
} }
// Close all opened windows (some browsers allow one window to open)
closeAll() {
for(const player of Object.values(this.players)) {
player.window?.close();
}
}
// Window blocked from opening, show "allow popups" message // Window blocked from opening, show "allow popups" message
windowOpenFailed() { windowOpenFailed() {
// Close all opened windows (some browsers allow one window to open) this.closeAll();
//for(const player of Object.values(this.players)) { throw new Error("WINDOW_OPEN_FAILED");
// player.window?.close();
//}
console.log("failed to open a window");
} }
message(event) { message(event) {
console.log(event); if(event.data === "PLAYER_CLOSED") return this.closeAll();
} }
} }

View file

@ -34,16 +34,18 @@ export default class PlayerWindow {
this.url = new URL(window.location.href + "player"); this.url = new URL(window.location.href + "player");
this.url.hash = name; this.url.hash = name;
// Path to Monkeydo manifest to load in this window
if(manifest) this.url.searchParams.append("manifest",manifest); if(manifest) this.url.searchParams.append("manifest",manifest);
// Copy window size rect into windowFeatures // Copy window size rect into windowFeatures
Object.assign(this.features,windowPositions[this.url.hash]); Object.assign(this.features,windowPositions[name]);
} }
// Convert windowFeatures object into a CSV DOMString // Convert windowFeatures object into a CSV DOMString
getWindowFeatures() { getWindowFeatures() {
let output = []; let output = [];
for(let [key,value] of Object.entries(this.features)) { for(let [key,value] of Object.entries(this.features)) {
// Coercive boolean
if(typeof key === "boolean") { if(typeof key === "boolean") {
value = value ? "yes" : "no"; value = value ? "yes" : "no";
} }
@ -52,6 +54,11 @@ export default class PlayerWindow {
return output.join(","); return output.join(",");
} }
// Close this window
close() {
return this.window?.close() ?? false;
}
// Compile windowFeatures and open the window // Compile windowFeatures and open the window
open() { open() {
const features = this.getWindowFeatures(); const features = this.getWindowFeatures();

View file

@ -19,6 +19,7 @@ export default class StillAlivePlayer {
const self = this; const self = this;
this.name = name; this.name = name;
this.player = element;
// Open a BroadcastChannel to listen to messages for me // Open a BroadcastChannel to listen to messages for me
this.channel = new BroadcastChannel(this.name); this.channel = new BroadcastChannel(this.name);
@ -28,45 +29,98 @@ export default class StillAlivePlayer {
const methods = { const methods = {
// Clear the screen from elements // Clear the screen from elements
blank: () => { blank: () => {
while(this.player.firstChild) { while(self.firstChild) {
this.player.removeChild(this.player.lastChild); self.removeChild(self.lastChild);
} }
}, },
// Create a new paragraph and make it the target for textFeed calls // Create a new paragraph and make it the target for textFeed calls
lineFeed: () => { lineFeed: () => {
this.target = document.createElement("p"); self.target = document.createElement("p");
this.player.appendChild(this.target); self.player.appendChild(self.target);
}, },
// Append text to the current target element // Append text to the current target element
textFeed: (text) => { textFeed: (text) => {
this.target.innerText = this.target.innerText + text; self.target.innerText = self.target.innerText + text;
}, },
// Decode and draw art from artset by key // Decode and draw art from artset by index key
drawArt: (index) => { drawArt: (idx) => {
this.blank(); this.blank();
self.target = document.createElement("pre"); self.target = document.createElement("pre");
self.target.innerText = window.decodeURIComponent(artset[key]); self.target.innerText = window.decodeURIComponent(artset[idx]);
self.player.appendChild(self.target); self.player.appendChild(self.target);
}, },
playCredits: () => { play: () => {
self.players.credits.play(); if(!self.monkey) return console.warn("This player has no manifest loaded");
self.monkey.play();
} }
} }
// Execute or relay Monkeydo methods // Execute or relay Monkeydo methods
const proxiedMethods = this.getMethods(methods); const proxiedMethods = this.getMethods(methods);
console.log(proxiedMethods.lineFeed("lyrics")); // Monkeydo
this.player = element; const manifest = this.getManifest();
if(manifest) {
this.monkey = new Monkeydo(proxiedMethods);
this.monkey.load(manifest).then(() => {
// Start playback of the first manifest
if(this.name === "lyrics") this.host();
});
}
}
// Get parent directory of this file
getParentURL() {
let pathname = window.location.pathname.split("/");
pathname.pop();
pathname = pathname[pathname.length - 1];
return pathname;
}
// This player is the "host", it will play the music and send events to other players
host() {
const parent = this.getParentURL();
const pathname = [parent,"assets","media","still-alive.webm"].join("/");
const musicURL = new URL(window.location.origin);
musicURL.pathname = pathname;
// Load and play the Still Alive when ready
const stillalive = new Audio(musicURL.toString());
stillalive.addEventListener("canplaythrough",() => {
this.monkey.play(); // Start the visuals
stillalive.play(); // Start the music
},{ once: true });
}
getManifest() {
const searchParams = new URLSearchParams(window.location.search);
if(!searchParams.has("manifest")) return false;
// Relative pathname to manifest JSON
const parent = this.getParentURL();
const pathname = [parent,searchParams.get("manifest")].join("/");
// Create a URL to the manifest
const manifest = new URL(window.location.origin);
manifest.pathname = pathname;
return manifest.toString();
} }
getMethods(methods) { getMethods(methods) {
const handler = { const handler = {
name: this.name,
relay: (...args) => this.relay(...args),
// Get and call methods from object
get(target,propKey,receiver) { get(target,propKey,receiver) {
const origMethod = target[propKey]; const origMethod = target[propKey];
return function (...args) { return function (...args) {
console.log(this); // Relay function call if it's not for me
let result = origMethod.apply(this, args); const channel = args[args.length - 1] ?? null;
if(channel && channel !== handler.name) return handler.relay(args);
let result = origMethod.apply(this,args);
return result; return result;
}; };
} }
@ -74,7 +128,7 @@ export default class StillAlivePlayer {
return new Proxy(methods,handler); return new Proxy(methods,handler);
} }
// Open a channel to a different player to relay a task // Open a channel to a different player to forward a task
relay(channelName,message) { relay(channelName,message) {
const channel = new BroadcastChannel(channelName); const channel = new BroadcastChannel(channelName);
channel.postMessage(message); channel.postMessage(message);

@ -1 +1 @@
Subproject commit 22eda9780099b77bfd755f214534bba24a881894 Subproject commit 7ca095a0d7a91aff50be42efb380fdbda05b2320

View file

@ -11,4 +11,7 @@ const name = window.location.hash.substring(1);
document.body.className = name; document.body.className = name;
const target = document.getElementById("player"); const target = document.getElementById("player");
new Player(target,name); const player = new Player(target,name);
// Notify PlayerManager that I was closed
window.addEventListener("beforeunload",() => player.channel.postMessage("PLAYER_CLOSED"));

View file

@ -2,11 +2,24 @@ import { default as Player } from "./modules/PlayerManager.mjs";
const play = document.getElementById("play"); const play = document.getElementById("play");
if(typeof BroadcastChannel !== "function") { function info(text = "") {
const message = document.getElementById("message"); const element = document.getElementById("message");
play.classList.add("unsupported"); element.innerText = text;
play.innerText = "Your browser can not play this demo";
message.innerText = error;
} }
play.addEventListener("click",() => new Player()); if(typeof BroadcastChannel !== "function") {
play.classList.add("unsupported");
info("Your browser can not play this demo");
}
play.addEventListener("click",() => {
try {
info();
new Player();
}
catch(except) {
if(except.message === "WINDOW_OPEN_FAILED") return info("Yikes! Player windows could not be created. Allow this page to open pop-ups, it will be worth it!");
// Cannot handle this exception, rethrow
throw except;
}
});

View file

@ -7,6 +7,7 @@
<meta name="description" content="End credits from 'Portal' - the video game, recreated with JS and browser windows."> <meta name="description" content="End credits from 'Portal' - the video game, recreated with JS and browser windows.">
<title>Still Alive</title> <title>Still Alive</title>
<link rel="stylesheet" href="assets/css/style.css"> <link rel="stylesheet" href="assets/css/style.css">
<style>body { text-align: center; }</style>
</head> </head>
<body> <body>
<main> <main>

File diff suppressed because it is too large Load diff