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 {
constructor() {
this.players = {
"lyrics": new Player("lyrics","monkeydo_lyrics.json")//,
//"credits": new Player("credits","monkeydo_credits.json"),
//"art": new Player("art")
"lyrics": new Player("lyrics","monkeydo_lyrics.json"),
"credits": new Player("credits","monkeydo_credits.json"),
"art": new Player("art")
};
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
windowOpenFailed() {
// Close all opened windows (some browsers allow one window to open)
//for(const player of Object.values(this.players)) {
// player.window?.close();
//}
console.log("failed to open a window");
this.closeAll();
throw new Error("WINDOW_OPEN_FAILED");
}
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.hash = name;
// Path to Monkeydo manifest to load in this window
if(manifest) this.url.searchParams.append("manifest",manifest);
// 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
getWindowFeatures() {
let output = [];
for(let [key,value] of Object.entries(this.features)) {
// Coercive boolean
if(typeof key === "boolean") {
value = value ? "yes" : "no";
}
@ -52,6 +54,11 @@ export default class PlayerWindow {
return output.join(",");
}
// Close this window
close() {
return this.window?.close() ?? false;
}
// Compile windowFeatures and open the window
open() {
const features = this.getWindowFeatures();

View file

@ -19,6 +19,7 @@ export default class StillAlivePlayer {
const self = this;
this.name = name;
this.player = element;
// Open a BroadcastChannel to listen to messages for me
this.channel = new BroadcastChannel(this.name);
@ -28,44 +29,97 @@ export default class StillAlivePlayer {
const methods = {
// Clear the screen from elements
blank: () => {
while(this.player.firstChild) {
this.player.removeChild(this.player.lastChild);
while(self.firstChild) {
self.removeChild(self.lastChild);
}
},
// Create a new paragraph and make it the target for textFeed calls
lineFeed: () => {
this.target = document.createElement("p");
this.player.appendChild(this.target);
self.target = document.createElement("p");
self.player.appendChild(self.target);
},
// Append text to the current target element
textFeed: (text) => {
this.target.innerText = this.target.innerText + text;
self.target.innerText = self.target.innerText + text;
},
// Decode and draw art from artset by key
drawArt: (index) => {
// Decode and draw art from artset by index key
drawArt: (idx) => {
this.blank();
self.target = document.createElement("pre");
self.target.innerText = window.decodeURIComponent(artset[key]);
self.target.innerText = window.decodeURIComponent(artset[idx]);
self.player.appendChild(self.target);
},
playCredits: () => {
self.players.credits.play();
play: () => {
if(!self.monkey) return console.warn("This player has no manifest loaded");
self.monkey.play();
}
}
// Execute or relay Monkeydo methods
const proxiedMethods = this.getMethods(methods);
console.log(proxiedMethods.lineFeed("lyrics"));
this.player = element;
// Monkeydo
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) {
const handler = {
name: this.name,
relay: (...args) => this.relay(...args),
// Get and call methods from object
get(target,propKey,receiver) {
const origMethod = target[propKey];
return function (...args) {
console.log(this);
// Relay function call if it's not for me
const channel = args[args.length - 1] ?? null;
if(channel && channel !== handler.name) return handler.relay(args);
let result = origMethod.apply(this,args);
return result;
};
@ -74,7 +128,7 @@ export default class StillAlivePlayer {
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) {
const channel = new BroadcastChannel(channelName);
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;
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");
if(typeof BroadcastChannel !== "function") {
const message = document.getElementById("message");
play.classList.add("unsupported");
play.innerText = "Your browser can not play this demo";
message.innerText = error;
function info(text = "") {
const element = document.getElementById("message");
element.innerText = text;
}
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.">
<title>Still Alive</title>
<link rel="stylesheet" href="assets/css/style.css">
<style>body { text-align: center; }</style>
</head>
<body>
<main>

File diff suppressed because it is too large Load diff