// Shared template designer utility helpers. (function () { function escapeHtml(value) { return String(value === undefined || value === null ? '' : value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function valueOrDefault(value, fallback) { return value === undefined || value === null || value === '' ? fallback : value; } function normalizeLockRatio(value) { var raw = String(value || '').trim(); if (!raw) { return ''; } if (!/^\d+\s*:\s*\d+$/.test(raw)) { return ''; } return raw.replace(/\s+/g, ''); } function parseLockRatio(value) { var normalized = normalizeLockRatio(value); if (!normalized) { return null; } var parts = normalized.split(':'); var width = Number(parts[0]); var height = Number(parts[1]); if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) { return null; } return { label: normalized, ratio: width / height }; } function getDefaultRegionSize(regionType, lockRatio) { if (window.pulseRegionTypes && typeof window.pulseRegionTypes.getDefaultRegionSize === 'function') { return window.pulseRegionTypes.getDefaultRegionSize(regionType, lockRatio); } var ratio = parseLockRatio(lockRatio); var locked = false; if (ratio) { if (ratio.ratio >= 1) { var baseWidth = locked ? 420 : 300; return { width: Math.max(12, Math.round(baseWidth)), height: Math.max(12, Math.round(baseWidth / ratio.ratio)) }; } var baseHeight = locked ? 300 : 240; return { width: Math.max(12, Math.round(baseHeight * ratio.ratio)), height: Math.max(12, Math.round(baseHeight)) }; } return { width: locked ? 420 : 300, height: locked ? 240 : 120 }; } function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } function getRegionName(card) { return String(card.querySelector('[name="region_name[]"]').value || '').trim(); } function getAnimationConfig(card) { var hiddenAnimationInput = card.querySelector('[name="region_animation_json[]"]'); var animationConfig = normalizeAnimationConfig(hiddenAnimationInput ? hiddenAnimationInput.value || '{}' : '{}'); var introPresetInput = card.querySelector('[data-animation-preset="intro"]'); var outroPresetInput = card.querySelector('[data-animation-preset="outro"]'); var loopPresetInput = card.querySelector('[data-animation-preset="loop"]'); if (introPresetInput && introPresetInput.dataset.animationPresetCustom !== '1' && introPresetInput.selectedIndex >= 0) { animationConfig.intro.preset = normalizePreviewAnimation(introPresetInput.value); } if (outroPresetInput && outroPresetInput.dataset.animationPresetCustom !== '1' && outroPresetInput.selectedIndex >= 0) { animationConfig.outro.preset = normalizePreviewAnimation(outroPresetInput.value); } if (loopPresetInput && loopPresetInput.dataset.animationPresetCustom !== '1' && loopPresetInput.selectedIndex >= 0) { animationConfig.loop.preset = normalizePreviewAnimation(loopPresetInput.value); } return animationConfig; } function getAnimationPresetValues() { var presets = window.templateAnimationPresets || {}; if (Array.isArray(presets.allValues) && presets.allValues.length) { return presets.allValues; } return [ 'none', 'fadeIn', 'fadeInDown', 'fadeInLeft', 'fadeInRight', 'fadeInUp', 'zoomIn', 'zoomInDown', 'zoomInLeft', 'zoomInRight', 'zoomInUp', 'slideInDown', 'slideInLeft', 'slideInRight', 'slideInUp', 'fadeOut', 'fadeOutDown', 'fadeOutLeft', 'fadeOutRight', 'fadeOutUp', 'zoomOut', 'zoomOutDown', 'zoomOutLeft', 'zoomOutRight', 'zoomOutUp', 'slideOutDown', 'slideOutLeft', 'slideOutRight', 'slideOutUp', 'backInDown', 'backInLeft', 'backInRight', 'backInUp', 'backOutDown', 'backOutLeft', 'backOutRight', 'backOutUp', 'bounceIn', 'bounceInDown', 'bounceInLeft', 'bounceInRight', 'bounceInUp', 'bounceOut', 'bounceOutDown', 'bounceOutLeft', 'bounceOutRight', 'bounceOutUp', 'flip', 'flipInX', 'flipInY', 'flipOutX', 'flipOutY', 'lightSpeedInLeft', 'lightSpeedInRight', 'lightSpeedOutLeft', 'lightSpeedOutRight', 'rotateIn', 'rotateInDownLeft', 'rotateInDownRight', 'rotateInUpLeft', 'rotateInUpRight', 'rotateOut', 'rotateOutDownLeft', 'rotateOutDownRight', 'rotateOutUpLeft', 'rotateOutUpRight', 'hinge', 'jackInTheBox', 'rollIn', 'rollOut', 'flash', 'pulse', 'rubberBand', 'shakeX', 'shakeY', 'headShake', 'swing', 'tada', 'wobble', 'jello', 'heartBeat', 'bounce' ]; } function getAnimationPresetMap() { var lookup = {}; getAnimationPresetValues().forEach(function (value) { lookup[value] = true; }); return lookup; } function syncAnimationPresetCustomState(selectInput, resetButton, preset) { if (!selectInput) { return; } var normalizedPreset = normalizePreviewAnimation(preset); var hasMatchingOption = Array.prototype.some.call(selectInput.options || [], function (option) { return option && option.value === normalizedPreset; }); if (hasMatchingOption) { delete selectInput.dataset.animationPresetCustom; selectInput.hidden = false; if (resetButton) { resetButton.hidden = true; } } else { selectInput.dataset.animationPresetCustom = '1'; selectInput.hidden = true; if (resetButton) { resetButton.hidden = false; resetButton.textContent = 'Reset to basic'; } } } function writeAnimationConfig(card, animationConfig) { var normalized = normalizeAnimationConfig(animationConfig); var hiddenAnimationInput = card.querySelector('[name="region_animation_json[]"]'); var introPresetInput = card.querySelector('[data-animation-preset="intro"]'); var outroPresetInput = card.querySelector('[data-animation-preset="outro"]'); var loopPresetInput = card.querySelector('[data-animation-preset="loop"]'); var introResetButton = card.querySelector('[data-animation-reset-button="intro"]'); var outroResetButton = card.querySelector('[data-animation-reset-button="outro"]'); var loopResetButton = card.querySelector('[data-animation-reset-button="loop"]'); if (hiddenAnimationInput) { hiddenAnimationInput.value = JSON.stringify(normalized, null, 2); } if (introPresetInput) { introPresetInput.value = normalized.intro.preset; syncAnimationPresetCustomState(introPresetInput, introResetButton, normalized.intro.preset); } if (outroPresetInput) { outroPresetInput.value = normalized.outro.preset; syncAnimationPresetCustomState(outroPresetInput, outroResetButton, normalized.outro.preset); } if (loopPresetInput) { loopPresetInput.value = normalized.loop.preset; syncAnimationPresetCustomState(loopPresetInput, loopResetButton, normalized.loop.preset); } return normalized; } function normalizePreviewAnimation(value) { if (window.templateAnimationPresets && typeof window.templateAnimationPresets.normalize === 'function') { return window.templateAnimationPresets.normalize(value); } var allowed = getAnimationPresetMap(); var raw = String(value || '').trim().toLowerCase(); return allowed[raw] ? raw : 'none'; } function normalizeAnimationStep(value, fallbackPreset) { if (value && typeof value === 'object' && !Array.isArray(value)) { var normalized = {}; Object.keys(value).forEach(function (key) { normalized[key] = value[key]; }); normalized.preset = normalizePreviewAnimation(value.preset !== undefined ? value.preset : fallbackPreset || 'none'); normalized.duration_ms = Number.isFinite(Number(value.duration_ms)) && Number(value.duration_ms) > 0 ? Math.round(Number(value.duration_ms)) : null; normalized.delay_ms = Number.isFinite(Number(value.delay_ms)) && Number(value.delay_ms) >= 0 ? Math.round(Number(value.delay_ms)) : null; normalized.iterations = Number.isFinite(Number(value.iterations)) && Number(value.iterations) > 0 ? Math.round(Number(value.iterations)) : null; normalized.easing = String(value.easing || '').trim(); normalized.fill_mode = String(value.fill_mode || '').trim(); return normalized; } return { preset: normalizePreviewAnimation(typeof value === 'string' ? value : fallbackPreset || 'none'), duration_ms: null, delay_ms: null, iterations: null, easing: '', fill_mode: '' }; } function normalizeAnimationConfig(value) { var raw = value; if (typeof raw === 'string') { var text = String(raw || '').trim(); if (!text) { raw = null; } else { try { raw = JSON.parse(text); } catch (_error) { raw = null; } } } if (!raw || typeof raw !== 'object' || Array.isArray(raw)) { raw = {}; } return { intro: normalizeAnimationStep(raw.intro, 'none'), outro: normalizeAnimationStep(raw.outro !== undefined ? raw.outro : raw.out, 'none'), loop: normalizeAnimationStep(raw.loop, 'none') }; } function formatAnimationConfig(value) { return JSON.stringify(normalizeAnimationConfig(value), null, 2); } function syncRegionIdentity(card, value) { var next = String(value || '').trim(); card.querySelector('[name="region_name[]"]').value = next; card.querySelector('[name="region_key[]"]').value = next; card.querySelector('[name="region_label[]"]').value = next; } function readCard(card) { var name = getRegionName(card); return { region_key: name, label: name, region_type: card.querySelector('[name="region_type[]"]').value, lock_ratio: normalizeLockRatio(card.querySelector('[name="region_lock_ratio[]"]').value), animation_json: getAnimationConfig(card), x: Number(card.querySelector('[name="region_x[]"]').value || 0), y: Number(card.querySelector('[name="region_y[]"]').value || 0), width: Number(card.querySelector('[name="region_width[]"]').value || 0), height: Number(card.querySelector('[name="region_height[]"]').value || 0), z_index: Number(card.querySelector('[name="region_z[]"]').value || 0) }; } function writeCard(card, values) { if (values.region_name !== undefined) { syncRegionIdentity(card, values.region_name); } else if (values.region_key !== undefined) { syncRegionIdentity(card, values.region_key); } else if (values.label !== undefined) { syncRegionIdentity(card, values.label); } if (values.region_type !== undefined) { card.querySelector('[name="region_type[]"]').value = values.region_type; } if (values.lock_ratio !== undefined) { card.querySelector('[name="region_lock_ratio[]"]').value = normalizeLockRatio(values.lock_ratio); } if (values.animation_json !== undefined) { writeAnimationConfig(card, values.animation_json); } if (values.x !== undefined) { card.querySelector('[name="region_x[]"]').value = Math.round(values.x); } if (values.y !== undefined) { card.querySelector('[name="region_y[]"]').value = Math.round(values.y); } if (values.width !== undefined) { card.querySelector('[name="region_width[]"]').value = Math.round(values.width); } if (values.height !== undefined) { card.querySelector('[name="region_height[]"]').value = Math.round(values.height); } if (values.z_index !== undefined) { card.querySelector('[name="region_z[]"]').value = Math.round(values.z_index); } } function clampRegion(region, size, minSize) { var bounds = size || { width: 1920, height: 1080 }; var minimum = minSize || 12; var x = clamp(region.x, 0, bounds.width - minimum); var y = clamp(region.y, 0, bounds.height - minimum); var width = Math.max(minimum, region.width); var height = Math.max(minimum, region.height); if (x + width > bounds.width) { width = bounds.width - x; } if (y + height > bounds.height) { height = bounds.height - y; } return { x: Math.round(x), y: Math.round(y), width: Math.round(Math.max(minimum, width)), height: Math.round(Math.max(minimum, height)) }; } function getOverlayRect(overlay) { return overlay.getBoundingClientRect(); } function toCanvasPoint(event, overlay, size) { var rect = getOverlayRect(overlay); var canvasSize = size || { width: 1920, height: 1080 }; var x = clamp(event.clientX - rect.left, 0, rect.width); var y = clamp(event.clientY - rect.top, 0, rect.height); return { x: Math.round((x / Math.max(rect.width, 1)) * canvasSize.width), y: Math.round((y / Math.max(rect.height, 1)) * canvasSize.height) }; } function canvasRectToPixels(region, overlay, size) { var rect = getOverlayRect(overlay); var canvasSize = size || { width: 1920, height: 1080 }; return { left: (region.x / canvasSize.width) * rect.width, top: (region.y / canvasSize.height) * rect.height, width: (region.width / canvasSize.width) * rect.width, height: (region.height / canvasSize.height) * rect.height }; } window.templateDesignerUtils = { escapeHtml: escapeHtml, valueOrDefault: valueOrDefault, clamp: clamp, getRegionName: getRegionName, syncRegionIdentity: syncRegionIdentity, readCard: readCard, writeCard: writeCard, normalizeAnimationConfig: normalizeAnimationConfig, formatAnimationConfig: formatAnimationConfig, getAnimationConfig: getAnimationConfig, writeAnimationConfig: writeAnimationConfig, clampRegion: clampRegion, getOverlayRect: getOverlayRect, toCanvasPoint: toCanvasPoint, canvasRectToPixels: canvasRectToPixels, normalizeLockRatio: normalizeLockRatio, parseLockRatio: parseLockRatio, getDefaultRegionSize: getDefaultRegionSize }; }());