New effects, refactor

This commit is contained in:
Dan 2026-01-09 13:20:06 +00:00
parent 4ac45367e5
commit c0d6bee9c3
14 changed files with 1620 additions and 215 deletions

View file

@ -1,4 +1,4 @@
import { ButtonEffect } from '../effect-base.js';
import { ButtonEffect } from "../effect-base.js";
/**
* Spinning text animation effect
@ -6,61 +6,62 @@ import { ButtonEffect } from '../effect-base.js';
*/
export class SpinTextEffect extends ButtonEffect {
constructor(textLineNumber = 1) {
const suffix = textLineNumber === 1 ? '' : '2';
const suffix = textLineNumber === 1 ? "" : "2";
super({
id: `text-spin${suffix}`,
name: `Spinning Text ${textLineNumber}`,
type: textLineNumber === 1 ? 'text' : 'text2',
category: textLineNumber === 1 ? 'Text Line 1' : 'Text Line 2',
type: textLineNumber === 1 ? "text" : "text2",
category: textLineNumber === 1 ? "Text Line 1" : "Text Line 2",
renderOrder: 8, // Before wave, after rainbow
textLineNumber: textLineNumber
textLineNumber: textLineNumber,
});
this.textLineNumber = textLineNumber;
}
defineControls() {
const textLineNumber = this.textLineNumber || this.config?.textLineNumber || 1;
const suffix = textLineNumber === 1 ? '' : '2';
const textLineNumber =
this.textLineNumber || this.config?.textLineNumber || 1;
const suffix = textLineNumber === 1 ? "" : "2";
return [
{
id: `animate-text-spin${suffix}`,
type: 'checkbox',
label: 'Spinning Animation',
defaultValue: false
type: "checkbox",
label: "Spinning Animation",
defaultValue: false,
},
{
id: `spin-speed${suffix}`,
type: 'range',
label: 'Spin Speed',
type: "range",
label: "Spin Speed",
defaultValue: 1,
min: 0.1,
min: 1,
max: 5,
step: 0.1,
step: 1,
showWhen: `animate-text-spin${suffix}`,
description: 'Speed of character rotation'
description: "Speed of character rotation",
},
{
id: `spin-stagger${suffix}`,
type: 'range',
label: 'Spin Stagger',
type: "range",
label: "Spin Stagger",
defaultValue: 0.3,
min: 0,
max: 1,
step: 0.1,
showWhen: `animate-text-spin${suffix}`,
description: 'Delay between characters'
}
description: "Delay between characters",
},
];
}
isEnabled(controlValues) {
const suffix = this.textLineNumber === 1 ? '' : '2';
const suffix = this.textLineNumber === 1 ? "" : "2";
return controlValues[`animate-text-spin${suffix}`] === true;
}
apply(context, controlValues, animState, renderData) {
const suffix = this.textLineNumber === 1 ? '' : '2';
const text = controlValues[`button-text${suffix}`] || '';
const suffix = this.textLineNumber === 1 ? "" : "2";
const text = controlValues[`button-text${suffix}`] || "";
if (!text || !controlValues[`text${suffix}-enabled`]) return;
if (!animState) return;
@ -68,21 +69,26 @@ export class SpinTextEffect extends ButtonEffect {
const speed = controlValues[`spin-speed${suffix}`] || 1;
const stagger = controlValues[`spin-stagger${suffix}`] || 0.3;
const fontSize = controlValues[`font-size${suffix}`] || 14;
const fontFamily = controlValues[`font-family${suffix}`] || 'Arial';
const fontWeight = controlValues[`text${suffix}-bold`] ? 'bold' : 'normal';
const fontStyle = controlValues[`text${suffix}-italic`] ? 'italic' : 'normal';
const fontFamily = controlValues[`font-family${suffix}`] || "Arial";
const fontWeight = controlValues[`text${suffix}-bold`] ? "bold" : "normal";
const fontStyle = controlValues[`text${suffix}-italic`]
? "italic"
: "normal";
context.font = `${fontStyle} ${fontWeight} ${fontSize}px "${fontFamily}"`;
context.textAlign = 'center';
context.textBaseline = 'middle';
context.textAlign = "center";
context.textBaseline = "middle";
// Get text color
let fillStyle;
const colorType = controlValues[`text${suffix}-color-type`] || 'solid';
if (colorType === 'gradient') {
const color1 = controlValues[`text${suffix}-gradient-color1`] || '#ffffff';
const color2 = controlValues[`text${suffix}-gradient-color2`] || '#00ffff';
const angle = (controlValues[`text${suffix}-gradient-angle`] || 90) * (Math.PI / 180);
const colorType = controlValues[`text${suffix}-color-type`] || "solid";
if (colorType === "gradient") {
const color1 =
controlValues[`text${suffix}-gradient-color1`] || "#ffffff";
const color2 =
controlValues[`text${suffix}-gradient-color2`] || "#00ffff";
const angle =
(controlValues[`text${suffix}-gradient-angle`] || 90) * (Math.PI / 180);
const centerX = renderData.centerX;
const centerY = renderData.centerY;
const x1 = centerX + Math.cos(angle) * 20;
@ -94,7 +100,7 @@ export class SpinTextEffect extends ButtonEffect {
gradient.addColorStop(1, color2);
fillStyle = gradient;
} else {
fillStyle = controlValues[`text${suffix}-color`] || '#ffffff';
fillStyle = controlValues[`text${suffix}-color`] || "#ffffff";
}
// Calculate base position
@ -103,13 +109,24 @@ export class SpinTextEffect extends ButtonEffect {
const baseX = (x / 100) * renderData.width;
const baseY = (y / 100) * renderData.height;
// Split text into grapheme clusters (handles emojis properly)
// Use Intl.Segmenter if available, otherwise fall back to spread operator
let chars;
if (typeof Intl !== 'undefined' && Intl.Segmenter) {
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
chars = Array.from(segmenter.segment(text), s => s.segment);
} else {
// Fallback: spread operator handles basic emoji
chars = [...text];
}
// Measure total text width for centering
const totalWidth = context.measureText(text).width;
let currentX = baseX - totalWidth / 2;
// Draw each character with rotation
for (let i = 0; i < text.length; i++) {
const char = text[i];
for (let i = 0; i < chars.length; i++) {
const char = chars[i];
const charWidth = context.measureText(char).width;
const charCenterX = currentX + charWidth / 2;
@ -123,7 +140,8 @@ export class SpinTextEffect extends ButtonEffect {
// Apply outline if enabled
if (controlValues[`text${suffix}-outline`]) {
context.strokeStyle = controlValues[`text${suffix}-outline-color`] || '#000000';
context.strokeStyle =
controlValues[`text${suffix}-outline-color`] || "#000000";
context.lineWidth = 2;
context.strokeText(char, 0, 0);
}