Add browser-specific entry point

This commit is contained in:
Idrees Hassan
2025-11-16 09:51:46 -05:00
parent a5e81e4265
commit 6ee9efd5a8
14 changed files with 1441 additions and 2343 deletions

View File

@@ -2,9 +2,11 @@ import Frame from './frame.js';
import Layer from './layer.js';
import Anim from './anim.js';
import { Birb, Animations } from './birb.js';
import { getContext, ObsidianContext } from './context.js';
import { Context, ObsidianContext } from './context.js';
import {
getContext,
setContext,
Directions,
isDebug,
setDebug,
@@ -109,68 +111,24 @@ const MIN_FOCUS_ELEMENT_WIDTH = 100;
/** @type {Partial<Settings>} */
let userSettings = {};
/**
* Load the sprite sheet and return the pixel-map template
* @param {string} dataUri
* @param {boolean} [templateColors]
* @returns {Promise<string[][]>}
/**
* @param {Context} context
*/
function loadSpriteSheetPixels(dataUri, templateColors = true) {
return new Promise((resolve, reject) => {
const img = new Image();
img.src = dataUri;
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
if (!ctx) {
reject(new Error('Failed to get canvas context'));
return;
}
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, img.width, img.height);
const pixels = imageData.data;
const hexArray = [];
for (let y = 0; y < img.height; y++) {
const row = [];
for (let x = 0; x < img.width; x++) {
const index = (y * img.width + x) * 4;
const r = pixels[index];
const g = pixels[index + 1];
const b = pixels[index + 2];
const a = pixels[index + 3];
if (a === 0) {
row.push(Sprite.TRANSPARENT);
continue;
}
const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
if (!templateColors) {
row.push(hex);
continue;
}
if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) {
error(`Unknown color: ${hex}`);
row.push(Sprite.TRANSPARENT);
}
row.push(SPRITE_SHEET_COLOR_MAP[hex]);
}
hexArray.push(row);
}
resolve(hexArray);
};
img.onerror = (err) => {
reject(err);
};
});
export async function initializeApplication(context) {
log("birbOS booting up...");
setContext(context);
log("Loading sprite sheets...");
const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET);
const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET);
startApplication(birbPixels, featherPixels);
}
log("Loading sprite sheets...");
Promise.all([
loadSpriteSheetPixels(SPRITE_SHEET),
loadSpriteSheetPixels(FEATHER_SPRITE_SHEET)
]).then(([birbPixels, featherPixels]) => {
/**
* @param {string[][]} birbPixels
* @param {string[][]} featherPixels
*/
function startApplication(birbPixels, featherPixels) {
const SPRITE_SHEET = birbPixels;
const FEATHER_SPRITE_SHEET = featherPixels;
@@ -851,10 +809,11 @@ Promise.all([
return true;
});
/** @type {HTMLElement[]} */
// @ts-expect-error
const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH);
// Ensure the bird doesn't land on fixed or sticky elements
const fixedAllowed = getContext() instanceof ObsidianContext;
// const fixedAllowed = getContext() instanceof ObsidianContext;
// TODO: FIX
const fixedAllowed = true;
const nonFixedElements = largeElements.filter((el) => {
if (fixedAllowed) {
return true;
@@ -1008,6 +967,60 @@ Promise.all([
// Run the birb
init();
draw();
}).catch((e) => {
error("Error while loading sprite sheets: ", e);
});
}
/**
* Load the sprite sheet and return the pixel-map template
* @param {string} dataUri
* @param {boolean} [templateColors]
* @returns {Promise<string[][]>}
*/
function loadSpriteSheetPixels(dataUri, templateColors = true) {
return new Promise((resolve, reject) => {
const img = new Image();
img.src = dataUri;
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
if (!ctx) {
reject(new Error('Failed to get canvas context'));
return;
}
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, img.width, img.height);
const pixels = imageData.data;
const hexArray = [];
for (let y = 0; y < img.height; y++) {
const row = [];
for (let x = 0; x < img.width; x++) {
const index = (y * img.width + x) * 4;
const r = pixels[index];
const g = pixels[index + 1];
const b = pixels[index + 2];
const a = pixels[index + 3];
if (a === 0) {
row.push(Sprite.TRANSPARENT);
continue;
}
const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
if (!templateColors) {
row.push(hex);
continue;
}
if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) {
error(`Unknown color: ${hex}`);
row.push(Sprite.TRANSPARENT);
}
row.push(SPRITE_SHEET_COLOR_MAP[hex]);
}
hexArray.push(row);
}
resolve(hexArray);
};
img.onerror = (err) => {
reject(err);
};
});
}

View File

@@ -1,6 +1,6 @@
import { debug, log, error } from "./shared.js";
const SAVE_KEY = "birbSaveData";
export const SAVE_KEY = "birbSaveData";
const ROOT_PATH = "";
const SET_CONTEXT = "__CONTEXT__"
@@ -17,9 +17,9 @@ export class Context {
* @abstract
* @returns {boolean} Whether this context is applicable
*/
isContextActive() {
throw new Error("Method not implemented");
}
// isContextActive() {
// throw new Error("Method not implemented");
// }
/**
* @abstract
@@ -102,54 +102,8 @@ export class Context {
}
}
export class LocalContext extends Context {
/**
* @override
* @returns {boolean}
*/
isContextActive() {
return window.location.hostname === "127.0.0.1"
|| window.location.hostname === "localhost"
|| window.location.hostname.startsWith("192.168.");
}
/**
* @override
* @returns {Promise<BirbSaveData|{}>}
*/
async getSaveData() {
log("Loading save data from localStorage");
return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}");
}
/**
* @override
* @param {BirbSaveData} saveData
*/
async putSaveData(saveData) {
log("Saving data to localStorage");
localStorage.setItem(SAVE_KEY, JSON.stringify(saveData));
}
/** @override */
resetSaveData() {
log("Resetting save data in localStorage");
localStorage.removeItem(SAVE_KEY);
}
}
export class UserScriptContext extends Context {
/**
* @override
* @returns {boolean}
*/
isContextActive() {
// @ts-expect-error
return typeof GM_getValue === "function";
}
/**
* @override
* @returns {Promise<BirbSaveData|{}>}
@@ -183,15 +137,6 @@ export class UserScriptContext extends Context {
class BrowserExtensionContext extends Context {
/**
* @override
* @returns {boolean}
*/
isContextActive() {
// @ts-expect-error
return typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.sync !== "undefined";
}
/**
* @override
* @returns {Promise<BirbSaveData|{}>}
@@ -234,15 +179,6 @@ class BrowserExtensionContext extends Context {
export class ObsidianContext extends Context {
/**
* @override
* @returns {boolean}
*/
isContextActive() {
// @ts-expect-error
return typeof app !== "undefined" && typeof app.vault !== "undefined";
}
/**
* @override
* @returns {Promise<BirbSaveData|{}>}
@@ -325,11 +261,10 @@ const contextProcessingOrder = [
new UserScriptContext(),
new ObsidianContext(),
new BrowserExtensionContext(),
new LocalContext()
];
const CONTEXTS_BY_KEY = {
"local": LocalContext,
// "local": LocalContext,
"userscript": UserScriptContext,
"browser-extension": BrowserExtensionContext,
"obsidian": ObsidianContext
@@ -339,18 +274,19 @@ const CONTEXTS_BY_KEY = {
* Determines and returns the current context
* @returns {Context}
*/
export function getContext() {
if (CONTEXTS_BY_KEY[SET_CONTEXT]) {
return new CONTEXTS_BY_KEY[SET_CONTEXT]();
}
for (const context of contextProcessingOrder) {
if (context.isContextActive()) {
return context;
}
}
error("No applicable context found, defaulting to LocalContext");
return new LocalContext();
}
// export function getContext() {
// if (CONTEXTS_BY_KEY[SET_CONTEXT]) {
// return new CONTEXTS_BY_KEY[SET_CONTEXT]();
// }
// for (const context of contextProcessingOrder) {
// if (context.isContextActive()) {
// return context;
// }
// }
// error("No applicable context found");
// // return new LocalContext();
// return null;
// }
/**
* Parse URL parameters into a key-value map

36
src/platforms/browser.js Normal file
View File

@@ -0,0 +1,36 @@
import { Context, SAVE_KEY } from "../context.js";
import { log } from "../shared.js";
import { initializeApplication } from "../application";
/**
* @typedef {import('../application.js').BirbSaveData} BirbSaveData
*/
export class LocalContext extends Context {
/**
* @override
* @returns {Promise<BirbSaveData|{}>}
*/
async getSaveData() {
log("Loading save data from localStorage");
return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}");
}
/**
* @override
* @param {BirbSaveData} saveData
*/
async putSaveData(saveData) {
log("Saving data to localStorage");
localStorage.setItem(SAVE_KEY, JSON.stringify(saveData));
}
/** @override */
resetSaveData() {
log("Resetting save data in localStorage");
localStorage.removeItem(SAVE_KEY);
}
}
initializeApplication(new LocalContext());

View File

@@ -4,6 +4,7 @@ export const Directions = {
};
let debugMode = location.hostname === "127.0.0.1";
let context = null;
/**
* @returns {boolean} Whether debug mode is enabled
@@ -19,6 +20,17 @@ export function setDebug(value) {
debugMode = value;
}
export function getContext() {
if (!context) {
throw new Error("Context requested before being set");
}
return context;
}
export function setContext(newContext) {
context = newContext;
}
/**
* Create an HTML element with the specified parameters
* @param {string} className

View File

@@ -1,9 +1,9 @@
import {
getContext,
makeElement,
makeDraggable,
makeClosable
} from './shared.js';
import { getContext } from './context.js';
/**
* @typedef {Object} SavedStickyNote