187 lines
5.5 KiB
JavaScript
187 lines
5.5 KiB
JavaScript
import { ButtonEffect } from "../effect-base.js";
|
|
|
|
/**
|
|
* Starfield background effect
|
|
* Twinkling stars with optional shooting stars
|
|
*/
|
|
export class StarfieldEffect extends ButtonEffect {
|
|
constructor() {
|
|
super({
|
|
id: "bg-starfield",
|
|
name: "Starfield",
|
|
type: "background-animation",
|
|
category: "Background Animations",
|
|
renderOrder: 10,
|
|
});
|
|
|
|
this.stars = [];
|
|
this.initialized = false;
|
|
}
|
|
|
|
defineControls() {
|
|
return [
|
|
{
|
|
id: "animate-starfield",
|
|
type: "checkbox",
|
|
label: "Starfield Effect",
|
|
defaultValue: false,
|
|
},
|
|
{
|
|
id: "star-density",
|
|
type: "range",
|
|
label: "Star Density",
|
|
defaultValue: 30,
|
|
min: 10,
|
|
max: 80,
|
|
step: 5,
|
|
showWhen: "animate-starfield",
|
|
description: "Number of stars",
|
|
},
|
|
{
|
|
id: "star-twinkle-speed",
|
|
type: "range",
|
|
label: "Twinkle Speed",
|
|
defaultValue: 1,
|
|
min: 1,
|
|
max: 3,
|
|
step: 0.1,
|
|
showWhen: "animate-starfield",
|
|
description: "Speed of twinkling",
|
|
},
|
|
{
|
|
id: "star-shooting-enabled",
|
|
type: "checkbox",
|
|
label: "Shooting Stars",
|
|
defaultValue: true,
|
|
showWhen: "animate-starfield",
|
|
description: "Enable shooting stars",
|
|
},
|
|
{
|
|
id: "star-color",
|
|
type: "color",
|
|
label: "Star Color",
|
|
defaultValue: "#ffffff",
|
|
showWhen: "animate-starfield",
|
|
description: "Color of stars",
|
|
},
|
|
];
|
|
}
|
|
|
|
isEnabled(controlValues) {
|
|
return controlValues["animate-starfield"] === true;
|
|
}
|
|
|
|
apply(context, controlValues, animState, renderData) {
|
|
if (!animState) return;
|
|
|
|
const density = controlValues["star-density"] || 30;
|
|
const twinkleSpeed = controlValues["star-twinkle-speed"] || 1;
|
|
const shootingEnabled = controlValues["star-shooting-enabled"] !== false;
|
|
const starColor = controlValues["star-color"] || "#ffffff";
|
|
|
|
// Initialize stars with deterministic positions (seed-based)
|
|
if (!this.initialized || this.stars.length !== density) {
|
|
this.stars = [];
|
|
for (let i = 0; i < density; i++) {
|
|
// Use density as part of the seed so pattern changes with density
|
|
// Use multiple large prime seeds for better distribution
|
|
const densitySeed = density * 97531; // Density affects all stars
|
|
const seed1 = (i * 2654435761) + densitySeed;
|
|
const seed2 = (i * 2246822519) + 3141592653 + densitySeed;
|
|
const seed3 = (i * 3266489917) + 1618033988 + densitySeed;
|
|
const seed4 = (i * 374761393) + 2718281828 + densitySeed;
|
|
const seed5 = (i * 1103515245) + 12345 + densitySeed;
|
|
|
|
// Hash-like function for better pseudo-random distribution
|
|
const hash = (s) => {
|
|
const x = Math.sin(s * 0.0001) * 10000;
|
|
return x - Math.floor(x);
|
|
};
|
|
|
|
this.stars.push({
|
|
x: hash(seed1) * renderData.width,
|
|
y: hash(seed2) * renderData.height,
|
|
size: 0.5 + hash(seed3) * 1.5,
|
|
twinkleOffset: hash(seed4) * Math.PI * 2,
|
|
twinkleSpeed: 0.5 + hash(seed5) * 1.5,
|
|
});
|
|
}
|
|
this.initialized = true;
|
|
}
|
|
|
|
// Draw twinkling stars
|
|
this.stars.forEach((star) => {
|
|
const twinkle =
|
|
Math.sin(
|
|
animState.getPhase(twinkleSpeed * star.twinkleSpeed) +
|
|
star.twinkleOffset,
|
|
) *
|
|
0.5 +
|
|
0.5;
|
|
const opacity = 0.3 + twinkle * 0.7;
|
|
|
|
context.fillStyle = starColor;
|
|
context.globalAlpha = opacity;
|
|
context.beginPath();
|
|
context.arc(star.x, star.y, star.size, 0, Math.PI * 2);
|
|
context.fill();
|
|
context.globalAlpha = 1.0;
|
|
});
|
|
|
|
// Shooting stars - deterministic based on frame number
|
|
if (shootingEnabled) {
|
|
// Define shooting star spawn schedule (deterministic)
|
|
const shootingStarSpawns = [
|
|
{ startFrame: 5, seed: 12345 },
|
|
{ startFrame: 18, seed: 67890 },
|
|
{ startFrame: 31, seed: 24680 },
|
|
];
|
|
|
|
shootingStarSpawns.forEach((spawn) => {
|
|
const framesSinceSpawn = animState.frame - spawn.startFrame;
|
|
const duration = 25; // Frames the shooting star is visible
|
|
|
|
if (framesSinceSpawn >= 0 && framesSinceSpawn < duration) {
|
|
// Calculate deterministic properties from seed
|
|
const hash = (s) => {
|
|
const x = Math.sin(s * 0.0001) * 10000;
|
|
return x - Math.floor(x);
|
|
};
|
|
|
|
const startX = hash(spawn.seed * 1) * renderData.width;
|
|
const startY = -10;
|
|
const vx = (hash(spawn.seed * 2) - 0.5) * 2;
|
|
const vy = 3 + hash(spawn.seed * 3) * 2;
|
|
|
|
// Calculate position based on frames elapsed
|
|
const x = startX + vx * framesSinceSpawn;
|
|
const y = startY + vy * framesSinceSpawn;
|
|
const life = 1.0 - framesSinceSpawn / duration;
|
|
|
|
if (life > 0) {
|
|
// Draw shooting star trail
|
|
const gradient = context.createLinearGradient(
|
|
x,
|
|
y,
|
|
x - vx * 5,
|
|
y - vy * 5,
|
|
);
|
|
gradient.addColorStop(0, `rgba(255, 255, 255, ${life * 0.8})`);
|
|
gradient.addColorStop(1, "rgba(255, 255, 255, 0)");
|
|
|
|
context.strokeStyle = gradient;
|
|
context.lineWidth = 2;
|
|
context.beginPath();
|
|
context.moveTo(x, y);
|
|
context.lineTo(x - vx * 5, y - vy * 5);
|
|
context.stroke();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
export function register(generator) {
|
|
generator.registerEffect(new StarfieldEffect());
|
|
}
|