162 lines
5.3 KiB
JavaScript
162 lines
5.3 KiB
JavaScript
import { ButtonEffect } from "../effect-base.js";
|
|
|
|
/**
|
|
* Spinning text animation effect
|
|
* Makes each character rotate independently
|
|
*/
|
|
export class SpinTextEffect extends ButtonEffect {
|
|
constructor(textLineNumber = 1) {
|
|
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",
|
|
renderOrder: 8, // Before wave, after rainbow
|
|
textLineNumber: textLineNumber,
|
|
});
|
|
this.textLineNumber = textLineNumber;
|
|
}
|
|
|
|
defineControls() {
|
|
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,
|
|
},
|
|
{
|
|
id: `spin-speed${suffix}`,
|
|
type: "range",
|
|
label: "Spin Speed",
|
|
defaultValue: 1,
|
|
min: 1,
|
|
max: 5,
|
|
step: 1,
|
|
showWhen: `animate-text-spin${suffix}`,
|
|
description: "Speed of character rotation",
|
|
},
|
|
{
|
|
id: `spin-stagger${suffix}`,
|
|
type: "range",
|
|
label: "Spin Stagger",
|
|
defaultValue: 0.3,
|
|
min: 0,
|
|
max: 1,
|
|
step: 0.1,
|
|
showWhen: `animate-text-spin${suffix}`,
|
|
description: "Delay between characters",
|
|
},
|
|
];
|
|
}
|
|
|
|
isEnabled(controlValues) {
|
|
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}`] || "";
|
|
|
|
if (!text || text.trim() === '') return;
|
|
if (!animState) return;
|
|
|
|
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";
|
|
|
|
context.font = `${fontStyle} ${fontWeight} ${fontSize}px "${fontFamily}"`;
|
|
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 centerX = renderData.centerX;
|
|
const centerY = renderData.centerY;
|
|
const x1 = centerX + Math.cos(angle) * 20;
|
|
const y1 = centerY + Math.sin(angle) * 20;
|
|
const x2 = centerX - Math.cos(angle) * 20;
|
|
const y2 = centerY - Math.sin(angle) * 20;
|
|
const gradient = context.createLinearGradient(x1, y1, x2, y2);
|
|
gradient.addColorStop(0, color1);
|
|
gradient.addColorStop(1, color2);
|
|
fillStyle = gradient;
|
|
} else {
|
|
fillStyle = controlValues[`text${suffix}-color`] || "#ffffff";
|
|
}
|
|
|
|
// Calculate base position
|
|
const x = controlValues[`text${suffix}-x`] || 50;
|
|
const y = controlValues[`text${suffix}-y`] || 50;
|
|
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 < chars.length; i++) {
|
|
const char = chars[i];
|
|
const charWidth = context.measureText(char).width;
|
|
const charCenterX = currentX + charWidth / 2;
|
|
|
|
// Calculate rotation for this character
|
|
const phase = animState.getPhase(speed) + i * stagger * Math.PI * 2;
|
|
const rotation = phase % (Math.PI * 2);
|
|
|
|
context.save();
|
|
context.translate(charCenterX, baseY);
|
|
context.rotate(rotation);
|
|
|
|
// Apply outline if enabled
|
|
if (controlValues[`text${suffix}-outline`]) {
|
|
context.strokeStyle =
|
|
controlValues[`text${suffix}-outline-color`] || "#000000";
|
|
context.lineWidth = 2;
|
|
context.strokeText(char, 0, 0);
|
|
}
|
|
|
|
context.fillStyle = fillStyle;
|
|
context.fillText(char, 0, 0);
|
|
context.restore();
|
|
|
|
currentX += charWidth;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Auto-register effects for both text lines
|
|
export function register(generator) {
|
|
generator.registerEffect(new SpinTextEffect(1));
|
|
generator.registerEffect(new SpinTextEffect(2));
|
|
}
|