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)); }