diff --git a/docs/README.md b/docs/README.md
index 8b7b0823..48d916c3 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -18,7 +18,7 @@
- [build:firefox](#buildfirefox)
- [build:firefox:zip](#buildfirefoxzip)
- [Architecture](#architecture)
- - [Secure spawn messages](#secure-spawn-messages)
+- [Balloon spawn chances](#balloon-spawn-chances)
- [Balloons](#balloons)
- [Abstract balloon class](#abstract-balloon-class)
- [Default balloon](#default-balloon)
@@ -151,27 +151,13 @@ The zip file will be created in the `build/` directory.
## Architecture
-### Secure spawn messages
-
-To prevent spawning balloons from untrusted sources like devtools, the extension keeps secrets in the content script an background.
+## Balloon spawn chances
```mermaid
-sequenceDiagram
- participant Content
- participant Background
-
- Content->>Background: Get secret
- Background->>Content: Secret
- Note over Background,Content: Secret is stored
- loop Random interval
- Background->>Content: Spawn balloon
- alt Secret is correct
- Note over Content: Balloon is spawned
- Content->>Background: Get secret
- Background->>Content: Secret
- Note over Background,Content: Secret is stored
- end
- end
+pie showdata
+title Balloon spawn chances
+ "Default" : 0.90
+ "Confetti" : 0.10
```
## Balloons
diff --git a/docs/images/Screenshot-1-1280x800.png b/docs/images/Screenshot-1-1280x800.png
new file mode 100644
index 00000000..2242903a
Binary files /dev/null and b/docs/images/Screenshot-1-1280x800.png differ
diff --git a/docs/images/Screenshot-1.png b/docs/images/Screenshot-1.png
deleted file mode 100644
index 7ecbee28..00000000
Binary files a/docs/images/Screenshot-1.png and /dev/null differ
diff --git a/docs/images/Screenshot-2-1280x800.png b/docs/images/Screenshot-2-1280x800.png
new file mode 100644
index 00000000..1af3cd84
Binary files /dev/null and b/docs/images/Screenshot-2-1280x800.png differ
diff --git a/docs/images/Screenshot-2-640x400.png b/docs/images/Screenshot-2-640x400.png
deleted file mode 100644
index 1480cb92..00000000
Binary files a/docs/images/Screenshot-2-640x400.png and /dev/null differ
diff --git a/docs/images/Screenshot-2.png b/docs/images/Screenshot-2.png
deleted file mode 100644
index 9b7d9cad..00000000
Binary files a/docs/images/Screenshot-2.png and /dev/null differ
diff --git a/docs/images/Screenshot-3-1280x800.png b/docs/images/Screenshot-3-1280x800.png
new file mode 100644
index 00000000..ffaab574
Binary files /dev/null and b/docs/images/Screenshot-3-1280x800.png differ
diff --git a/docs/images/Screenshot-3-640x400.png b/docs/images/Screenshot-3-640x400.png
deleted file mode 100644
index 0f387185..00000000
Binary files a/docs/images/Screenshot-3-640x400.png and /dev/null differ
diff --git a/docs/images/Screenshot-3.png b/docs/images/Screenshot-3.png
deleted file mode 100644
index 66f3f6e3..00000000
Binary files a/docs/images/Screenshot-3.png and /dev/null differ
diff --git a/docs/images/Screenshot-4-640x400.png b/docs/images/Screenshot-4-640x400.png
deleted file mode 100644
index 588b28fb..00000000
Binary files a/docs/images/Screenshot-4-640x400.png and /dev/null differ
diff --git a/docs/images/Screenshot-4.png b/docs/images/Screenshot-4.png
deleted file mode 100644
index 13a9634a..00000000
Binary files a/docs/images/Screenshot-4.png and /dev/null differ
diff --git a/package-lock.json b/package-lock.json
index 5ba3a19c..9e1c287d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "pop-a-loon",
- "version": "1.8.1",
+ "version": "1.9.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pop-a-loon",
- "version": "1.8.1",
+ "version": "1.9.0",
"license": "Apache-2.0",
"dependencies": {
"@hookform/resolvers": "^3.3.4",
diff --git a/package.json b/package.json
index 7f566ded..7eb81f42 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "pop-a-loon",
- "version": "1.8.1",
+ "version": "1.9.0",
"description": "The new rising trend (literally) that changes the browser game completely.",
"private": true,
"scripts": {
diff --git a/resources/balloons/confetti/confetti.css b/resources/balloons/confetti/confetti.css
new file mode 100644
index 00000000..d9d91305
--- /dev/null
+++ b/resources/balloons/confetti/confetti.css
@@ -0,0 +1,17 @@
+@keyframes bang {
+ from {
+ transform: translate3d(0, 0, 0);
+ opacity: 1;
+ }
+}
+
+i.particle {
+ animation: bang 750ms ease-out forwards;
+ position: absolute;
+ display: block;
+ left: 50%;
+ top: 0;
+ width: 3px;
+ height: 8px;
+ opacity: 0;
+}
diff --git a/resources/balloons/confetti/mask.png b/resources/balloons/confetti/mask.png
new file mode 100644
index 00000000..9705d96f
Binary files /dev/null and b/resources/balloons/confetti/mask.png differ
diff --git a/resources/balloons/default/icon.png b/resources/balloons/default/icon.png
index 32db35f2..4d6e70dc 100644
Binary files a/resources/balloons/default/icon.png and b/resources/balloons/default/icon.png differ
diff --git a/resources/icons/balloon.svg b/resources/icons/balloon.svg
new file mode 100644
index 00000000..55ab36b5
--- /dev/null
+++ b/resources/icons/balloon.svg
@@ -0,0 +1,46 @@
+
\ No newline at end of file
diff --git a/resources/icons/icon-128.png b/resources/icons/icon-128.png
index 32db35f2..4d6e70dc 100644
Binary files a/resources/icons/icon-128.png and b/resources/icons/icon-128.png differ
diff --git a/resources/icons/icon-16.png b/resources/icons/icon-16.png
index e843b640..d8561290 100644
Binary files a/resources/icons/icon-16.png and b/resources/icons/icon-16.png differ
diff --git a/resources/icons/icon-24.png b/resources/icons/icon-24.png
index e428a9ab..38e3ac2b 100644
Binary files a/resources/icons/icon-24.png and b/resources/icons/icon-24.png differ
diff --git a/resources/icons/icon-32.png b/resources/icons/icon-32.png
index 740179f7..2ad827ae 100644
Binary files a/resources/icons/icon-32.png and b/resources/icons/icon-32.png differ
diff --git a/resources/icons/icon-48.png b/resources/icons/icon-48.png
index 3758b5f6..4246985f 100644
Binary files a/resources/icons/icon-48.png and b/resources/icons/icon-48.png differ
diff --git a/src/background/background.ts b/src/background/background.ts
index e7dcfeb5..fb9417ee 100644
--- a/src/background/background.ts
+++ b/src/background/background.ts
@@ -5,7 +5,7 @@ import storage from '@/storage';
import remote from '@/remote';
import {
calculateBalloonSpawnDelay,
- generateRandomNumber,
+ random,
getBrowser,
isRunningInBackground,
sendMessage,
@@ -102,7 +102,7 @@ const updateBadgeColors = () => {
// Get all active tabs
const tabs = await browser.tabs.query({ active: true });
// Select a random tab
- const num = Math.round(generateRandomNumber(0, tabs.length - 1));
+ const num = Math.round(random(0, tabs.length - 1));
const tab = tabs[num];
if (!tab.id) return skipSpawnMessage('No tab id');
diff --git a/src/balloon.ts b/src/balloon.ts
index ff2e7403..b1a57867 100644
--- a/src/balloon.ts
+++ b/src/balloon.ts
@@ -1,16 +1,20 @@
import browser from 'webextension-polyfill';
import storage from '@/storage';
-import { generateRandomNumber, sendMessage } from '@utils';
+import { random, sendMessage } from '@utils';
export const balloonContainer = document.createElement('div');
balloonContainer.id = 'balloon-container';
-const resourceLocation = browser.runtime.getURL('resources/balloons/');
+export const balloonResourceLocation = browser.runtime.getURL(
+ 'resources/balloons/'
+);
+export const defaultBalloonFolderName = 'default';
+export const defaultBalloonResourceLocation =
+ balloonResourceLocation + `${defaultBalloonFolderName}/`;
const buildBalloonElement = (
element: HTMLDivElement,
props: {
- balloonImage: HTMLImageElement;
size: number;
positionX: number;
riseDuration: number;
@@ -19,9 +23,6 @@ const buildBalloonElement = (
) => {
element.classList.add('balloon');
- // Add an image to the balloon
- element.appendChild(props.balloonImage);
-
// Set the balloon's width and height
element.style.width = props.size + 'px';
element.style.height = element.style.width;
@@ -37,44 +38,62 @@ const buildBalloonElement = (
export default abstract class Balloon {
public abstract readonly name: string;
- public abstract getRandomDuration(): number;
- private readonly element: HTMLDivElement;
private readonly _popSound: HTMLAudioElement = new Audio();
protected readonly balloonImage: HTMLImageElement =
document.createElement('img');
+ public readonly element: HTMLDivElement;
+ public readonly riseDurationThreshold: [number, number] = [10000, 15000];
+
public get popSound(): HTMLAudioElement {
- if (!this._popSound.src) this._popSound.src = this.popSoundUrl;
+ if (!this._popSound.src) {
+ this._popSound.src = this.popSoundUrl;
+ }
return this._popSound;
}
+
public get balloonImageUrl(): string {
- return resourceLocation + this.name + '/icon.png';
+ return balloonResourceLocation + this.name + '/icon.png';
}
+
public get popSoundUrl(): string {
- return resourceLocation + this.name + '/pop.mp3';
+ return balloonResourceLocation + this.name + '/pop.mp3';
}
constructor() {
// Create the balloon element
this.element = document.createElement('div');
+
+ // Add the balloon image to the balloon element
+ this.element.appendChild(this.balloonImage);
+
// Add an event listener to the balloon
- this.element.addEventListener('click', this.pop.bind(this));
+ this.element.addEventListener('click', this._pop.bind(this));
+
+ this.balloonImage.addEventListener('error', (e) => {
+ this.balloonImage.src = defaultBalloonResourceLocation + 'icon.png';
+ });
}
- public isRising() {
+ public isRising(): boolean {
return this.element.style.animationName === 'rise';
}
- public rise() {
+ public getRandomDuration(
+ duration: [number, number] = this.riseDurationThreshold
+ ): number {
+ return random(duration[0], duration[1]);
+ }
+
+ public rise(): void {
// Load the balloon image
this.balloonImage.src = this.balloonImageUrl;
// Build the balloon element
buildBalloonElement(this.element, {
- size: generateRandomNumber(50, 75),
- balloonImage: this.balloonImage,
- positionX: generateRandomNumber(5, 95),
+ size: random(50, 75),
+ positionX: random(5, 95),
riseDuration: this.getRandomDuration(),
onAnimationend: this.remove.bind(this),
});
@@ -82,21 +101,28 @@ export default abstract class Balloon {
balloonContainer.appendChild(this.element);
}
- public remove() {
+ public remove(): void {
this.element.remove();
this.element.style.animationName = 'none';
}
- public async pop() {
+ private async _pop(event: MouseEvent): Promise {
// Remove the balloon
this.remove();
+ // Send message with the new count
+ sendMessage({ action: 'incrementCount' });
+
// Set volume
this.popSound.volume = (await storage.get('config')).popVolume / 100;
// Play the pop sound
- this.popSound.play();
+ this.popSound.play().catch((e) => {
+ this.popSound.src = defaultBalloonResourceLocation + 'pop.mp3';
+ this.popSound.play();
+ });
- // Send message with the new count
- sendMessage({ action: 'incrementCount' });
+ this.pop(event);
}
+
+ public pop(event?: MouseEvent): void | Promise {}
}
diff --git a/src/balloons/confetti.ts b/src/balloons/confetti.ts
new file mode 100644
index 00000000..3d7eab63
--- /dev/null
+++ b/src/balloons/confetti.ts
@@ -0,0 +1,83 @@
+import Balloon, { balloonResourceLocation } from '@/balloon';
+import { random } from '@/utils';
+import '@/../resources/balloons/confetti/confetti.css';
+
+export default class Confetti extends Balloon {
+ public readonly name = 'confetti';
+ public static readonly spawn_chance = 0.1;
+
+ private readonly mask = document.createElement('img');
+
+ constructor() {
+ super();
+
+ this.element.appendChild(this.mask);
+ this.mask.src = balloonResourceLocation + this.name + '/mask.png';
+ this.mask.style.position = 'absolute';
+ this.mask.style.top = '-10px';
+ this.mask.style.left = '0';
+ // Give it a random rotation
+ this.mask.style.transform = `rotate(${Math.random() * 360}deg)`;
+ }
+
+ public pop(event?: MouseEvent) {
+ // Get the click position
+ const x = event?.clientX || window.innerWidth / 2;
+ const y = event?.clientY || window.innerHeight / 2;
+ // Add an element to that position
+ const confetti = document.createElement('div');
+ confetti.style.position = 'absolute';
+ confetti.style.top = `${y}px`;
+ confetti.style.left = `${x}px`;
+ confetti.style.zIndex = '1000';
+ confetti.style.pointerEvents = 'none';
+ document.body.appendChild(confetti);
+ // Throw confetti
+ throwConfetti(confetti, 100);
+ // Remove the confetti after 2 seconds
+ setTimeout(() => {
+ confetti.remove();
+ }, 2000);
+ }
+}
+
+function throwConfetti(element: HTMLElement, amount: number = 100) {
+ const df = document.createDocumentFragment();
+ const offset = 10;
+ const particles: HTMLElement[] = [];
+ for (let i = 0; i < amount; i++) {
+ const c = document.createElement('i');
+ c.className = 'particle';
+ const angle = random(360);
+ const radius = random(250);
+ let x = radius * Math.cos(angle);
+ let y = radius * Math.sin(angle);
+ c.style.cssText = `
+ transform: translate3d(${x}px, ${y}px, 0)
+ rotate(${angle}deg);
+ background: hsla(${random(360)},100%,50%,1);
+ `;
+ df.appendChild(c);
+ particles.push(c);
+ }
+ element.appendChild(df);
+
+ function checkBounds() {
+ particles.forEach((particle, index) => {
+ const rect = particle.getBoundingClientRect();
+ if (
+ rect.bottom < +offset ||
+ rect.top > window.innerHeight - offset ||
+ rect.left > window.innerWidth - offset ||
+ rect.right < +offset
+ ) {
+ particle.remove();
+ particles.splice(index, 1);
+ }
+ });
+ if (particles.length > 0) {
+ requestAnimationFrame(checkBounds);
+ }
+ }
+ requestAnimationFrame(checkBounds);
+}
diff --git a/src/balloons/default.ts b/src/balloons/default.ts
index 79e78c0a..c6f4afef 100644
--- a/src/balloons/default.ts
+++ b/src/balloons/default.ts
@@ -1,11 +1,6 @@
-import Balloon from '@/balloon';
-import { generateRandomNumber } from '@/utils';
+import Balloon, { defaultBalloonFolderName } from '@/balloon';
export default class Default extends Balloon {
- public readonly name = 'default';
- public static readonly spawn_chance = 0.95;
-
- getRandomDuration() {
- return generateRandomNumber(10000, 15000);
- }
+ public readonly name = defaultBalloonFolderName;
+ public static readonly spawn_chance = 0.9;
}
diff --git a/src/balloons/index.ts b/src/balloons/index.ts
index bd4eb513..05d885f6 100644
--- a/src/balloons/index.ts
+++ b/src/balloons/index.ts
@@ -1 +1,2 @@
export { default as Default } from './default';
+export { default as Confetti } from './confetti';
diff --git a/src/content/content.ts b/src/content/content.ts
index edbdd305..cf83e988 100644
--- a/src/content/content.ts
+++ b/src/content/content.ts
@@ -29,8 +29,10 @@ import './style.css';
);
// Create a new balloon and make it rise
- const Balloon =
- weightedRandom(balloonClasses, spawnChances) || balloons.Default;
+ const Balloon = weightedRandom(balloonClasses, spawnChances, {
+ default: balloons.Default,
+ });
+
const balloon = new Balloon();
balloon.rise();
}
diff --git a/src/utils.ts b/src/utils.ts
index d189f74b..fd422f79 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -16,8 +16,11 @@ export function minutesToMilliseconds(minutes: number) {
return secondsToMilliseconds(minutes * 60);
}
-export function generateRandomNumber(min: number, max: number) {
- return Math.random() * (max - min) + min;
+export function random(max: number): number;
+export function random(min: number, max: number): number;
+export function random(minOrMax: number, max?: number): number {
+ if (max === undefined) return Math.random() * (minOrMax - 0) + 0;
+ return Math.random() * (max - minOrMax) + minOrMax;
}
export function sleep(ms: number) {
@@ -58,7 +61,15 @@ export function isRunningInBackground() {
return !isRunningInPopup;
}
-export function weightedRandom(results: T[], weights: number[]): T | null {
+type WeightedRandomOptions = {
+ default?: T;
+};
+
+export function weightedRandom(
+ results: T[],
+ weights: number[],
+ options: WeightedRandomOptions = {}
+): T | D {
// Calculate the total weight
const totalWeight = weights.reduce((sum, weight) => sum + weight, 0);
@@ -74,14 +85,14 @@ export function weightedRandom(results: T[], weights: number[]): T | null {
}
}
- // Return null if no result is found (shouldn't happen if the weights are correct)
- return null;
+ // Return the default value if no result was selected or null if no default was provided
+ return (options.default ?? null) as D;
}
export async function calculateBalloonSpawnDelay() {
const config = await storage.get('config');
// Generate a random delay between the min and max spawn interval
- const randomDelay = generateRandomNumber(
+ const randomDelay = random(
config.spawnInterval.min,
config.spawnInterval.max
);
diff --git a/tests/balloon.test.ts b/tests/balloon.test.ts
index 3d246424..f1dafc0b 100644
--- a/tests/balloon.test.ts
+++ b/tests/balloon.test.ts
@@ -1,11 +1,11 @@
import Balloon from '@/balloon';
-import { generateRandomNumber } from '@/utils';
+import { random } from '@utils';
// Create a concrete subclass of Balloon for testing
class TestBalloon extends Balloon {
public readonly name = 'test';
getRandomDuration() {
- return generateRandomNumber(10000, 15000);
+ return random(10000, 15000);
}
}