diff --git a/dist/extension.zip b/dist/extension.zip index fa3a0ec..6fd3eaf 100644 Binary files a/dist/extension.zip and b/dist/extension.zip differ diff --git a/dist/extension/birb.js b/dist/extension/birb.js index bf9aa73..a7291a1 100644 --- a/dist/extension/birb.js +++ b/dist/extension/birb.js @@ -857,6 +857,50 @@ } } + // @ts-check + + class Birdsong { + + /** + * @type {AudioContext} + */ + audioContext; + + chirp() { + if (!this.audioContext) { + this.audioContext = new AudioContext(); + } + + const TIMES = [0, 0.06, 0.10, 0.15]; + const FREQUENCIES = [2200, + 3500 + Math.random() * 600, + 2100 + Math.random() * 200, + 1600 + Math.random() * 400]; + const VOLUMES = [0.0001, 0.3, 0.3, 0.0001]; + + const oscillator = this.audioContext.createOscillator(); + oscillator.type = "sine"; + const gain = this.audioContext.createGain(); + oscillator.connect(gain); + gain.connect(this.audioContext.destination); + + const now = this.audioContext.currentTime; + for (let i = 0; i < TIMES.length; i++) { + const time = TIMES[i] + now; + if (i === 0) { + oscillator.frequency.setValueAtTime(FREQUENCIES[i], time); + gain.gain.setValueAtTime(VOLUMES[i], time); + } else { + oscillator.frequency.exponentialRampToValueAtTime(FREQUENCIES[i], time); + gain.gain.exponentialRampToValueAtTime(VOLUMES[i], time); + } + } + + oscillator.start(now); + oscillator.stop(now + TIMES[TIMES.length - 1]); + } + } + const SAVE_KEY = "birbSaveData"; /** @@ -1165,7 +1209,7 @@ class MenuItem { /** - * @param {string} text + * @param {string|(() => string)} text * @param {() => void} action * @param {boolean} [removeMenu] */ @@ -1214,7 +1258,7 @@ if (item instanceof Separator) { return makeElement("birb-window-separator"); } - let menuItem = makeElement("birb-menu-item", item.text); + let menuItem = makeElement("birb-menu-item", typeof item.text === "function" ? item.text() : item.text); onClick(menuItem, () => { if (item.removeMenu) { removeMenuCallback(); @@ -1321,7 +1365,8 @@ * @typedef {typeof DEFAULT_SETTINGS} Settings */ const DEFAULT_SETTINGS = { - birbMode: false + birbMode: false, + soundEnabled: true }; // Rendering constants @@ -1534,6 +1579,7 @@ .birb-menu-item { width: calc(100% - var(--birb-double-border-size)); + white-space: nowrap; font-size: 14px; padding-top: 4px; padding-bottom: 4px; @@ -1806,12 +1852,16 @@ const settingsItems = [ new MenuItem("Go Back", () => switchMenuItems(menuItems, updateMenuLocation), false), new Separator(), - new MenuItem("Toggle Birb Mode", () => { - userSettings.birbMode = !userSettings.birbMode; + new MenuItem(() => `${settings().soundEnabled ? "Disable" : "Enable"} Sound`, () => { + userSettings.soundEnabled = !settings().soundEnabled; + save(); + }), + new MenuItem(() => `Toggle ${birdBirb(true)} Mode`, () => { + userSettings.birbMode = !settings().birbMode; save(); const message = makeElement("birb-message-content"); message.appendChild(document.createTextNode(`Your ${birdBirb().toLowerCase()} shall now be referred to as "${birdBirb()}"`)); - if (userSettings.birbMode) { + if (settings().birbMode) { message.appendChild(document.createElement("br")); message.appendChild(document.createElement("br")); message.appendChild(document.createTextNode("Welcome back to 2012")); @@ -1819,7 +1869,7 @@ insertModal(`${birdBirb()} Mode`, message); }), new Separator(), - new MenuItem("2026.1.1", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.1"); }, false), + new MenuItem("2026.1.4", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.4"); }, false), ]; const styleElement = document.createElement("style"); @@ -1833,6 +1883,8 @@ FLYING: "flying", }; + const birdsong = new Birdsong(); + let frozen = false; let stateStart = Date.now(); let currentState = States.IDLE; @@ -1923,8 +1975,8 @@ /** * Bird or birb, you decide */ - function birdBirb() { - return settings().birbMode ? "Birb" : "Bird"; + function birdBirb(invert = false) { + return settings().birbMode !== invert ? "Birb" : "Bird"; } function init() { @@ -2513,6 +2565,9 @@ function pet() { if (currentState === States.IDLE && birb.getCurrentAnimation() !== Animations.HEART) { + if (settings().soundEnabled) { + birdsong.chirp(); + } birb.setAnimation(Animations.HEART); lastPetTimestamp = Date.now(); } diff --git a/dist/extension/manifest.json b/dist/extension/manifest.json index d87779c..4584664 100644 --- a/dist/extension/manifest.json +++ b/dist/extension/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Pocket Bird", "description": "It's a pet bird in your browser, what more could you want?", - "version": "2026.1.1", + "version": "2026.1.4", "homepage_url": "https://idreesinc.com", "icons": { "48": "images/icons/transparent/48x48x1.png", diff --git a/dist/obsidian/main.js b/dist/obsidian/main.js index 21af991..e7f808f 100644 --- a/dist/obsidian/main.js +++ b/dist/obsidian/main.js @@ -1,7 +1,7 @@ const { Plugin, Notice } = require('obsidian'); module.exports = class PocketBird extends Plugin { onload() { - console.log("Loading Pocket Bird version 2026.1.1..."); + console.log("Loading Pocket Bird version 2026.1.4..."); const OBSIDIAN_PLUGIN = this; (function () { 'use strict'; @@ -862,6 +862,50 @@ module.exports = class PocketBird extends Plugin { } } + // @ts-check + + class Birdsong { + + /** + * @type {AudioContext} + */ + audioContext; + + chirp() { + if (!this.audioContext) { + this.audioContext = new AudioContext(); + } + + const TIMES = [0, 0.06, 0.10, 0.15]; + const FREQUENCIES = [2200, + 3500 + Math.random() * 600, + 2100 + Math.random() * 200, + 1600 + Math.random() * 400]; + const VOLUMES = [0.0001, 0.3, 0.3, 0.0001]; + + const oscillator = this.audioContext.createOscillator(); + oscillator.type = "sine"; + const gain = this.audioContext.createGain(); + oscillator.connect(gain); + gain.connect(this.audioContext.destination); + + const now = this.audioContext.currentTime; + for (let i = 0; i < TIMES.length; i++) { + const time = TIMES[i] + now; + if (i === 0) { + oscillator.frequency.setValueAtTime(FREQUENCIES[i], time); + gain.gain.setValueAtTime(VOLUMES[i], time); + } else { + oscillator.frequency.exponentialRampToValueAtTime(FREQUENCIES[i], time); + gain.gain.exponentialRampToValueAtTime(VOLUMES[i], time); + } + } + + oscillator.start(now); + oscillator.stop(now + TIMES[TIMES.length - 1]); + } + } + const ROOT_PATH = ""; /** @@ -1208,7 +1252,7 @@ module.exports = class PocketBird extends Plugin { class MenuItem { /** - * @param {string} text + * @param {string|(() => string)} text * @param {() => void} action * @param {boolean} [removeMenu] */ @@ -1257,7 +1301,7 @@ module.exports = class PocketBird extends Plugin { if (item instanceof Separator) { return makeElement("birb-window-separator"); } - let menuItem = makeElement("birb-menu-item", item.text); + let menuItem = makeElement("birb-menu-item", typeof item.text === "function" ? item.text() : item.text); onClick(menuItem, () => { if (item.removeMenu) { removeMenuCallback(); @@ -1364,7 +1408,8 @@ module.exports = class PocketBird extends Plugin { * @typedef {typeof DEFAULT_SETTINGS} Settings */ const DEFAULT_SETTINGS = { - birbMode: false + birbMode: false, + soundEnabled: true }; // Rendering constants @@ -1577,6 +1622,7 @@ module.exports = class PocketBird extends Plugin { .birb-menu-item { width: calc(100% - var(--birb-double-border-size)); + white-space: nowrap; font-size: 14px; padding-top: 4px; padding-bottom: 4px; @@ -1849,12 +1895,16 @@ module.exports = class PocketBird extends Plugin { const settingsItems = [ new MenuItem("Go Back", () => switchMenuItems(menuItems, updateMenuLocation), false), new Separator(), - new MenuItem("Toggle Birb Mode", () => { - userSettings.birbMode = !userSettings.birbMode; + new MenuItem(() => `${settings().soundEnabled ? "Disable" : "Enable"} Sound`, () => { + userSettings.soundEnabled = !settings().soundEnabled; + save(); + }), + new MenuItem(() => `Toggle ${birdBirb(true)} Mode`, () => { + userSettings.birbMode = !settings().birbMode; save(); const message = makeElement("birb-message-content"); message.appendChild(document.createTextNode(`Your ${birdBirb().toLowerCase()} shall now be referred to as "${birdBirb()}"`)); - if (userSettings.birbMode) { + if (settings().birbMode) { message.appendChild(document.createElement("br")); message.appendChild(document.createElement("br")); message.appendChild(document.createTextNode("Welcome back to 2012")); @@ -1862,7 +1912,7 @@ module.exports = class PocketBird extends Plugin { insertModal(`${birdBirb()} Mode`, message); }), new Separator(), - new MenuItem("2026.1.1", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.1"); }, false), + new MenuItem("2026.1.4", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.4"); }, false), ]; const styleElement = document.createElement("style"); @@ -1876,6 +1926,8 @@ module.exports = class PocketBird extends Plugin { FLYING: "flying", }; + const birdsong = new Birdsong(); + let frozen = false; let stateStart = Date.now(); let currentState = States.IDLE; @@ -1966,8 +2018,8 @@ module.exports = class PocketBird extends Plugin { /** * Bird or birb, you decide */ - function birdBirb() { - return settings().birbMode ? "Birb" : "Bird"; + function birdBirb(invert = false) { + return settings().birbMode !== invert ? "Birb" : "Bird"; } function init() { @@ -2556,6 +2608,9 @@ module.exports = class PocketBird extends Plugin { function pet() { if (currentState === States.IDLE && birb.getCurrentAnimation() !== Animations.HEART) { + if (settings().soundEnabled) { + birdsong.chirp(); + } birb.setAnimation(Animations.HEART); lastPetTimestamp = Date.now(); } diff --git a/dist/obsidian/manifest.json b/dist/obsidian/manifest.json index 69435cc..4aae7d6 100644 --- a/dist/obsidian/manifest.json +++ b/dist/obsidian/manifest.json @@ -1,7 +1,7 @@ { "id": "pocket-bird", "name": "Pocket Bird", - "version": "2026.1.1", + "version": "2026.1.4", "minAppVersion": "0.15.0", "description": "Add a pet bird to fly around your notes and keep you company!", "author": "Idrees Hassan", diff --git a/dist/userscript/birb.user.js b/dist/userscript/birb.user.js index fe59ca8..c8984dc 100644 --- a/dist/userscript/birb.user.js +++ b/dist/userscript/birb.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Pocket Bird // @namespace https://idreesinc.com -// @version 2026.1.1 +// @version 2026.1.4 // @description It's a pet bird in your browser, what more could you want? // @author Idrees // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/userscript/birb.user.js @@ -871,6 +871,50 @@ } } + // @ts-check + + class Birdsong { + + /** + * @type {AudioContext} + */ + audioContext; + + chirp() { + if (!this.audioContext) { + this.audioContext = new AudioContext(); + } + + const TIMES = [0, 0.06, 0.10, 0.15]; + const FREQUENCIES = [2200, + 3500 + Math.random() * 600, + 2100 + Math.random() * 200, + 1600 + Math.random() * 400]; + const VOLUMES = [0.0001, 0.3, 0.3, 0.0001]; + + const oscillator = this.audioContext.createOscillator(); + oscillator.type = "sine"; + const gain = this.audioContext.createGain(); + oscillator.connect(gain); + gain.connect(this.audioContext.destination); + + const now = this.audioContext.currentTime; + for (let i = 0; i < TIMES.length; i++) { + const time = TIMES[i] + now; + if (i === 0) { + oscillator.frequency.setValueAtTime(FREQUENCIES[i], time); + gain.gain.setValueAtTime(VOLUMES[i], time); + } else { + oscillator.frequency.exponentialRampToValueAtTime(FREQUENCIES[i], time); + gain.gain.exponentialRampToValueAtTime(VOLUMES[i], time); + } + } + + oscillator.start(now); + oscillator.stop(now + TIMES[TIMES.length - 1]); + } + } + const SAVE_KEY = "birbSaveData"; /** @@ -1170,7 +1214,7 @@ class MenuItem { /** - * @param {string} text + * @param {string|(() => string)} text * @param {() => void} action * @param {boolean} [removeMenu] */ @@ -1219,7 +1263,7 @@ if (item instanceof Separator) { return makeElement("birb-window-separator"); } - let menuItem = makeElement("birb-menu-item", item.text); + let menuItem = makeElement("birb-menu-item", typeof item.text === "function" ? item.text() : item.text); onClick(menuItem, () => { if (item.removeMenu) { removeMenuCallback(); @@ -1326,7 +1370,8 @@ * @typedef {typeof DEFAULT_SETTINGS} Settings */ const DEFAULT_SETTINGS = { - birbMode: false + birbMode: false, + soundEnabled: true }; // Rendering constants @@ -1539,6 +1584,7 @@ .birb-menu-item { width: calc(100% - var(--birb-double-border-size)); + white-space: nowrap; font-size: 14px; padding-top: 4px; padding-bottom: 4px; @@ -1811,12 +1857,16 @@ const settingsItems = [ new MenuItem("Go Back", () => switchMenuItems(menuItems, updateMenuLocation), false), new Separator(), - new MenuItem("Toggle Birb Mode", () => { - userSettings.birbMode = !userSettings.birbMode; + new MenuItem(() => `${settings().soundEnabled ? "Disable" : "Enable"} Sound`, () => { + userSettings.soundEnabled = !settings().soundEnabled; + save(); + }), + new MenuItem(() => `Toggle ${birdBirb(true)} Mode`, () => { + userSettings.birbMode = !settings().birbMode; save(); const message = makeElement("birb-message-content"); message.appendChild(document.createTextNode(`Your ${birdBirb().toLowerCase()} shall now be referred to as "${birdBirb()}"`)); - if (userSettings.birbMode) { + if (settings().birbMode) { message.appendChild(document.createElement("br")); message.appendChild(document.createElement("br")); message.appendChild(document.createTextNode("Welcome back to 2012")); @@ -1824,7 +1874,7 @@ insertModal(`${birdBirb()} Mode`, message); }), new Separator(), - new MenuItem("2026.1.1", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.1"); }, false), + new MenuItem("2026.1.4", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.4"); }, false), ]; const styleElement = document.createElement("style"); @@ -1838,6 +1888,8 @@ FLYING: "flying", }; + const birdsong = new Birdsong(); + let frozen = false; let stateStart = Date.now(); let currentState = States.IDLE; @@ -1928,8 +1980,8 @@ /** * Bird or birb, you decide */ - function birdBirb() { - return settings().birbMode ? "Birb" : "Bird"; + function birdBirb(invert = false) { + return settings().birbMode !== invert ? "Birb" : "Bird"; } function init() { @@ -2518,6 +2570,9 @@ function pet() { if (currentState === States.IDLE && birb.getCurrentAnimation() !== Animations.HEART) { + if (settings().soundEnabled) { + birdsong.chirp(); + } birb.setAnimation(Animations.HEART); lastPetTimestamp = Date.now(); } diff --git a/dist/web/birb.embed.js b/dist/web/birb.embed.js index 1c0c2f9..600bea5 100644 --- a/dist/web/birb.embed.js +++ b/dist/web/birb.embed.js @@ -857,6 +857,50 @@ } } + // @ts-check + + class Birdsong { + + /** + * @type {AudioContext} + */ + audioContext; + + chirp() { + if (!this.audioContext) { + this.audioContext = new AudioContext(); + } + + const TIMES = [0, 0.06, 0.10, 0.15]; + const FREQUENCIES = [2200, + 3500 + Math.random() * 600, + 2100 + Math.random() * 200, + 1600 + Math.random() * 400]; + const VOLUMES = [0.0001, 0.3, 0.3, 0.0001]; + + const oscillator = this.audioContext.createOscillator(); + oscillator.type = "sine"; + const gain = this.audioContext.createGain(); + oscillator.connect(gain); + gain.connect(this.audioContext.destination); + + const now = this.audioContext.currentTime; + for (let i = 0; i < TIMES.length; i++) { + const time = TIMES[i] + now; + if (i === 0) { + oscillator.frequency.setValueAtTime(FREQUENCIES[i], time); + gain.gain.setValueAtTime(VOLUMES[i], time); + } else { + oscillator.frequency.exponentialRampToValueAtTime(FREQUENCIES[i], time); + gain.gain.exponentialRampToValueAtTime(VOLUMES[i], time); + } + } + + oscillator.start(now); + oscillator.stop(now + TIMES[TIMES.length - 1]); + } + } + const SAVE_KEY = "birbSaveData"; /** @@ -1150,7 +1194,7 @@ class MenuItem { /** - * @param {string} text + * @param {string|(() => string)} text * @param {() => void} action * @param {boolean} [removeMenu] */ @@ -1199,7 +1243,7 @@ if (item instanceof Separator) { return makeElement("birb-window-separator"); } - let menuItem = makeElement("birb-menu-item", item.text); + let menuItem = makeElement("birb-menu-item", typeof item.text === "function" ? item.text() : item.text); onClick(menuItem, () => { if (item.removeMenu) { removeMenuCallback(); @@ -1306,7 +1350,8 @@ * @typedef {typeof DEFAULT_SETTINGS} Settings */ const DEFAULT_SETTINGS = { - birbMode: false + birbMode: false, + soundEnabled: true }; // Rendering constants @@ -1519,6 +1564,7 @@ .birb-menu-item { width: calc(100% - var(--birb-double-border-size)); + white-space: nowrap; font-size: 14px; padding-top: 4px; padding-bottom: 4px; @@ -1791,12 +1837,16 @@ const settingsItems = [ new MenuItem("Go Back", () => switchMenuItems(menuItems, updateMenuLocation), false), new Separator(), - new MenuItem("Toggle Birb Mode", () => { - userSettings.birbMode = !userSettings.birbMode; + new MenuItem(() => `${settings().soundEnabled ? "Disable" : "Enable"} Sound`, () => { + userSettings.soundEnabled = !settings().soundEnabled; + save(); + }), + new MenuItem(() => `Toggle ${birdBirb(true)} Mode`, () => { + userSettings.birbMode = !settings().birbMode; save(); const message = makeElement("birb-message-content"); message.appendChild(document.createTextNode(`Your ${birdBirb().toLowerCase()} shall now be referred to as "${birdBirb()}"`)); - if (userSettings.birbMode) { + if (settings().birbMode) { message.appendChild(document.createElement("br")); message.appendChild(document.createElement("br")); message.appendChild(document.createTextNode("Welcome back to 2012")); @@ -1804,7 +1854,7 @@ insertModal(`${birdBirb()} Mode`, message); }), new Separator(), - new MenuItem("2026.1.1", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.1"); }, false), + new MenuItem("2026.1.4", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.4"); }, false), ]; const styleElement = document.createElement("style"); @@ -1818,6 +1868,8 @@ FLYING: "flying", }; + const birdsong = new Birdsong(); + let frozen = false; let stateStart = Date.now(); let currentState = States.IDLE; @@ -1908,8 +1960,8 @@ /** * Bird or birb, you decide */ - function birdBirb() { - return settings().birbMode ? "Birb" : "Bird"; + function birdBirb(invert = false) { + return settings().birbMode !== invert ? "Birb" : "Bird"; } function init() { @@ -2498,6 +2550,9 @@ function pet() { if (currentState === States.IDLE && birb.getCurrentAnimation() !== Animations.HEART) { + if (settings().soundEnabled) { + birdsong.chirp(); + } birb.setAnimation(Animations.HEART); lastPetTimestamp = Date.now(); } diff --git a/dist/web/birb.js b/dist/web/birb.js index 1c0c2f9..600bea5 100644 --- a/dist/web/birb.js +++ b/dist/web/birb.js @@ -857,6 +857,50 @@ } } + // @ts-check + + class Birdsong { + + /** + * @type {AudioContext} + */ + audioContext; + + chirp() { + if (!this.audioContext) { + this.audioContext = new AudioContext(); + } + + const TIMES = [0, 0.06, 0.10, 0.15]; + const FREQUENCIES = [2200, + 3500 + Math.random() * 600, + 2100 + Math.random() * 200, + 1600 + Math.random() * 400]; + const VOLUMES = [0.0001, 0.3, 0.3, 0.0001]; + + const oscillator = this.audioContext.createOscillator(); + oscillator.type = "sine"; + const gain = this.audioContext.createGain(); + oscillator.connect(gain); + gain.connect(this.audioContext.destination); + + const now = this.audioContext.currentTime; + for (let i = 0; i < TIMES.length; i++) { + const time = TIMES[i] + now; + if (i === 0) { + oscillator.frequency.setValueAtTime(FREQUENCIES[i], time); + gain.gain.setValueAtTime(VOLUMES[i], time); + } else { + oscillator.frequency.exponentialRampToValueAtTime(FREQUENCIES[i], time); + gain.gain.exponentialRampToValueAtTime(VOLUMES[i], time); + } + } + + oscillator.start(now); + oscillator.stop(now + TIMES[TIMES.length - 1]); + } + } + const SAVE_KEY = "birbSaveData"; /** @@ -1150,7 +1194,7 @@ class MenuItem { /** - * @param {string} text + * @param {string|(() => string)} text * @param {() => void} action * @param {boolean} [removeMenu] */ @@ -1199,7 +1243,7 @@ if (item instanceof Separator) { return makeElement("birb-window-separator"); } - let menuItem = makeElement("birb-menu-item", item.text); + let menuItem = makeElement("birb-menu-item", typeof item.text === "function" ? item.text() : item.text); onClick(menuItem, () => { if (item.removeMenu) { removeMenuCallback(); @@ -1306,7 +1350,8 @@ * @typedef {typeof DEFAULT_SETTINGS} Settings */ const DEFAULT_SETTINGS = { - birbMode: false + birbMode: false, + soundEnabled: true }; // Rendering constants @@ -1519,6 +1564,7 @@ .birb-menu-item { width: calc(100% - var(--birb-double-border-size)); + white-space: nowrap; font-size: 14px; padding-top: 4px; padding-bottom: 4px; @@ -1791,12 +1837,16 @@ const settingsItems = [ new MenuItem("Go Back", () => switchMenuItems(menuItems, updateMenuLocation), false), new Separator(), - new MenuItem("Toggle Birb Mode", () => { - userSettings.birbMode = !userSettings.birbMode; + new MenuItem(() => `${settings().soundEnabled ? "Disable" : "Enable"} Sound`, () => { + userSettings.soundEnabled = !settings().soundEnabled; + save(); + }), + new MenuItem(() => `Toggle ${birdBirb(true)} Mode`, () => { + userSettings.birbMode = !settings().birbMode; save(); const message = makeElement("birb-message-content"); message.appendChild(document.createTextNode(`Your ${birdBirb().toLowerCase()} shall now be referred to as "${birdBirb()}"`)); - if (userSettings.birbMode) { + if (settings().birbMode) { message.appendChild(document.createElement("br")); message.appendChild(document.createElement("br")); message.appendChild(document.createTextNode("Welcome back to 2012")); @@ -1804,7 +1854,7 @@ insertModal(`${birdBirb()} Mode`, message); }), new Separator(), - new MenuItem("2026.1.1", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.1"); }, false), + new MenuItem("2026.1.4", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.4"); }, false), ]; const styleElement = document.createElement("style"); @@ -1818,6 +1868,8 @@ FLYING: "flying", }; + const birdsong = new Birdsong(); + let frozen = false; let stateStart = Date.now(); let currentState = States.IDLE; @@ -1908,8 +1960,8 @@ /** * Bird or birb, you decide */ - function birdBirb() { - return settings().birbMode ? "Birb" : "Bird"; + function birdBirb(invert = false) { + return settings().birbMode !== invert ? "Birb" : "Bird"; } function init() { @@ -2498,6 +2550,9 @@ function pet() { if (currentState === States.IDLE && birb.getCurrentAnimation() !== Animations.HEART) { + if (settings().soundEnabled) { + birdsong.chirp(); + } birb.setAnimation(Animations.HEART); lastPetTimestamp = Date.now(); } diff --git a/src/application.js b/src/application.js index c194c03..396e7fb 100644 --- a/src/application.js +++ b/src/application.js @@ -2,6 +2,7 @@ import Frame from './frame.js'; import Layer from './layer.js'; import Anim from './anim.js'; import { Birb, Animations } from './birb.js'; +import { Birdsong } from './sound.js'; import { Context, ObsidianContext } from './context.js'; import { @@ -60,7 +61,8 @@ import { * @typedef {typeof DEFAULT_SETTINGS} Settings */ const DEFAULT_SETTINGS = { - birbMode: false + birbMode: false, + soundEnabled: true }; // Rendering constants @@ -176,12 +178,16 @@ function startApplication(birbPixels, featherPixels) { const settingsItems = [ new MenuItem("Go Back", () => switchMenuItems(menuItems, updateMenuLocation), false), new Separator(), - new MenuItem("Toggle Birb Mode", () => { - userSettings.birbMode = !userSettings.birbMode; + new MenuItem(() => `${settings().soundEnabled ? "Disable" : "Enable"} Sound`, () => { + userSettings.soundEnabled = !settings().soundEnabled; + save(); + }), + new MenuItem(() => `Toggle ${birdBirb(true)} Mode`, () => { + userSettings.birbMode = !settings().birbMode; save(); const message = makeElement("birb-message-content"); message.appendChild(document.createTextNode(`Your ${birdBirb().toLowerCase()} shall now be referred to as "${birdBirb()}"`)); - if (userSettings.birbMode) { + if (settings().birbMode) { message.appendChild(document.createElement("br")); message.appendChild(document.createElement("br")); message.appendChild(document.createTextNode("Welcome back to 2012")); @@ -203,6 +209,8 @@ function startApplication(birbPixels, featherPixels) { FLYING: "flying", }; + const birdsong = new Birdsong(); + let frozen = false; let stateStart = Date.now(); let currentState = States.IDLE; @@ -293,8 +301,8 @@ function startApplication(birbPixels, featherPixels) { /** * Bird or birb, you decide */ - function birdBirb() { - return settings().birbMode ? "Birb" : "Bird"; + function birdBirb(invert = false) { + return settings().birbMode !== invert ? "Birb" : "Bird"; } function init() { @@ -897,6 +905,9 @@ function startApplication(birbPixels, featherPixels) { function pet() { if (currentState === States.IDLE && birb.getCurrentAnimation() !== Animations.HEART) { + if (settings().soundEnabled) { + birdsong.chirp(); + } birb.setAnimation(Animations.HEART); lastPetTimestamp = Date.now(); } diff --git a/src/menu.js b/src/menu.js index 1d67c18..09b7878 100644 --- a/src/menu.js +++ b/src/menu.js @@ -12,7 +12,7 @@ export const MENU_EXIT_ID = "birb-menu-exit"; export class MenuItem { /** - * @param {string} text + * @param {string|(() => string)} text * @param {() => void} action * @param {boolean} [removeMenu] */ @@ -61,7 +61,7 @@ function makeMenuItem(item, removeMenuCallback) { if (item instanceof Separator) { return makeElement("birb-window-separator"); } - let menuItem = makeElement("birb-menu-item", item.text); + let menuItem = makeElement("birb-menu-item", typeof item.text === "function" ? item.text() : item.text); onClick(menuItem, () => { if (item.removeMenu) { removeMenuCallback(); diff --git a/src/sound.js b/src/sound.js new file mode 100644 index 0000000..1756d8d --- /dev/null +++ b/src/sound.js @@ -0,0 +1,43 @@ +// @ts-check + +export class Birdsong { + + /** + * @type {AudioContext} + */ + audioContext; + + chirp() { + if (!this.audioContext) { + this.audioContext = new AudioContext(); + } + + const TIMES = [0, 0.06, 0.10, 0.15]; + const FREQUENCIES = [2200, + 3500 + Math.random() * 600, + 2100 + Math.random() * 200, + 1600 + Math.random() * 400]; + const VOLUMES = [0.0001, 0.3, 0.3, 0.0001]; + + const oscillator = this.audioContext.createOscillator(); + oscillator.type = "sine"; + const gain = this.audioContext.createGain(); + oscillator.connect(gain); + gain.connect(this.audioContext.destination); + + const now = this.audioContext.currentTime; + for (let i = 0; i < TIMES.length; i++) { + const time = TIMES[i] + now; + if (i === 0) { + oscillator.frequency.setValueAtTime(FREQUENCIES[i], time); + gain.gain.setValueAtTime(VOLUMES[i], time); + } else { + oscillator.frequency.exponentialRampToValueAtTime(FREQUENCIES[i], time); + gain.gain.exponentialRampToValueAtTime(VOLUMES[i], time); + } + } + + oscillator.start(now); + oscillator.stop(now + TIMES[TIMES.length - 1]); + } +} \ No newline at end of file diff --git a/src/stylesheet.css b/src/stylesheet.css index fd82706..917c0b8 100644 --- a/src/stylesheet.css +++ b/src/stylesheet.css @@ -198,6 +198,7 @@ .birb-menu-item { width: calc(100% - var(--birb-double-border-size)); + white-space: nowrap; font-size: 14px; padding-top: 4px; padding-bottom: 4px;