ritual.sh/static/js/button-generator/effects/background-plasma.js

208 lines
5.7 KiB
JavaScript

import { ButtonEffect } from "../effect-base.js";
/**
* Plasma background effect
* Classic demoscene sine wave plasma - iconic 90s visual
*/
export class PlasmaEffect extends ButtonEffect {
constructor() {
super({
id: "bg-plasma",
name: "Plasma",
type: "background-animation",
category: "Background Animations",
renderOrder: 5,
});
}
defineControls() {
return [
{
id: "animate-plasma",
type: "checkbox",
label: "Plasma Effect",
defaultValue: false,
},
{
id: "plasma-scale",
type: "range",
label: "Pattern Scale",
defaultValue: 1.5,
min: 0.5,
max: 3,
step: 0.1,
showWhen: "animate-plasma",
description: "Size of plasma patterns",
},
{
id: "plasma-speed",
type: "range",
label: "Animation Speed",
defaultValue: 1,
min: 0.5,
max: 3,
step: 0.1,
showWhen: "animate-plasma",
description: "Speed of plasma movement",
},
{
id: "plasma-color-scheme",
type: "select",
label: "Color Scheme",
defaultValue: "classic",
options: [
{ value: "classic", label: "Classic (Purple/Cyan)" },
{ value: "fire", label: "Fire (Red/Orange)" },
{ value: "ocean", label: "Ocean (Blue/Green)" },
{ value: "psychedelic", label: "Psychedelic (Rainbow)" },
{ value: "matrix", label: "Matrix (Green)" },
],
showWhen: "animate-plasma",
description: "Color palette for plasma",
},
];
}
isEnabled(controlValues) {
return controlValues["animate-plasma"] === true;
}
/**
* Get color based on scheme and value
*/
getColor(value, scheme) {
// Normalize value from -4..4 to 0..1
const normalized = (value + 4) / 8;
switch (scheme) {
case "fire": {
// Black -> Red -> Orange -> Yellow -> White
const r = Math.min(255, normalized * 400);
const g = Math.max(0, Math.min(255, (normalized - 0.3) * 400));
const b = Math.max(0, Math.min(255, (normalized - 0.7) * 400));
return { r, g, b };
}
case "ocean": {
// Deep blue -> Cyan -> Light green
const r = Math.max(0, Math.min(100, normalized * 100));
const g = Math.min(255, normalized * 300);
const b = Math.min(255, 100 + normalized * 155);
return { r, g, b };
}
case "psychedelic": {
// Full rainbow cycling
const hue = normalized * 360;
return this.hslToRgb(hue, 100, 50);
}
case "matrix": {
// Black -> Dark green -> Bright green -> White
const intensity = normalized;
const r = Math.max(0, Math.min(255, (intensity - 0.8) * 1000));
const g = Math.min(255, intensity * 300);
const b = Math.max(0, Math.min(255, (intensity - 0.9) * 500));
return { r, g, b };
}
case "classic":
default: {
// Classic demoscene: purple/magenta/cyan
const r = Math.min(255, 128 + Math.sin(normalized * Math.PI * 2) * 127);
const g = Math.min(255, 64 + Math.sin(normalized * Math.PI * 2 + 2) * 64);
const b = Math.min(255, 128 + Math.sin(normalized * Math.PI * 2 + 4) * 127);
return { r, g, b };
}
}
}
/**
* Convert HSL to RGB
*/
hslToRgb(h, s, l) {
s /= 100;
l /= 100;
const c = (1 - Math.abs(2 * l - 1)) * s;
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
const m = l - c / 2;
let r, g, b;
if (h < 60) {
r = c; g = x; b = 0;
} else if (h < 120) {
r = x; g = c; b = 0;
} else if (h < 180) {
r = 0; g = c; b = x;
} else if (h < 240) {
r = 0; g = x; b = c;
} else if (h < 300) {
r = x; g = 0; b = c;
} else {
r = c; g = 0; b = x;
}
return {
r: Math.round((r + m) * 255),
g: Math.round((g + m) * 255),
b: Math.round((b + m) * 255),
};
}
apply(context, controlValues, animState, renderData) {
if (!animState) return;
const scale = controlValues["plasma-scale"] || 1.5;
const speed = controlValues["plasma-speed"] || 1;
const scheme = controlValues["plasma-color-scheme"] || "classic";
const { width, height } = renderData;
// Perfect looping via ping-pong: animate forward first half, reverse second half
// progress goes 0->1, we convert to 0->1->0 (triangle wave)
const progress = animState.progress;
let pingPong;
if (progress < 0.5) {
// First half: 0->1
pingPong = progress * 2;
} else {
// Second half: 1->0
pingPong = (1 - progress) * 2;
}
// Scale the time value for visible animation
const time = pingPong * Math.PI * 2 * speed;
// Get image data for pixel manipulation
const imageData = context.getImageData(0, 0, width, height);
const data = imageData.data;
// Scale factor for the plasma pattern (smaller = larger patterns)
const scaleFactor = 10 / scale;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
// Classic plasma formula
const value =
Math.sin(x / scaleFactor + time) +
Math.sin(y / scaleFactor + time * 1.3) +
Math.sin((x + y) / scaleFactor + time * 0.7) +
Math.sin(Math.sqrt(x * x + y * y) / scaleFactor + time);
const color = this.getColor(value, scheme);
const idx = (y * width + x) * 4;
data[idx] = color.r;
data[idx + 1] = color.g;
data[idx + 2] = color.b;
data[idx + 3] = 255;
}
}
context.putImageData(imageData, 0, 0);
}
}
export function register(generator) {
generator.registerEffect(new PlasmaEffect());
}