// Template designer bootstrap and form wiring. (function () { var dataElement = document.getElementById('template-editor-data'); if (!dataElement) { return; } var utils = window.templateDesignerUtils || {}; var templateData = {}; try { templateData = JSON.parse(dataElement.getAttribute('data-json') || dataElement.textContent || '{}') || {}; } catch (_error) { templateData = {}; } var existingRegions = Array.isArray(templateData.regions) ? templateData.regions : (Array.isArray(templateData) ? templateData : []); var stage = document.getElementById('designer-stage'); var overlay = document.getElementById('designer-overlay'); var previewCard = document.getElementById('template-preview-card'); var previewIntroButton = document.getElementById('template-preview-intro'); var previewOutButton = document.getElementById('template-preview-out'); var previewContinuousButton = document.getElementById('template-preview-continuous'); var regionList = document.getElementById('region-list'); var regionSelect = document.getElementById('region-select'); var canvasSizeSelect = document.getElementById('canvas-size-select'); var canvasSizeIdInput = document.getElementById('canvas-size-id'); var canvasSizeSummary = document.getElementById('canvas-size-summary'); var canvasWidthInput = document.getElementById('canvas-width'); var canvasHeightInput = document.getElementById('canvas-height'); var backgroundInput = document.getElementById('background-image'); var backgroundColorInput = document.getElementById('background-color'); var backgroundPreview = document.getElementById('background-preview'); var backgroundEmpty = document.getElementById('background-empty'); var removeBackgroundButton = document.getElementById('remove-background-image'); var removeBackgroundFlag = document.getElementById('remove-background-image-flag'); var addRegionButton = document.getElementById('add-region-button'); var regionAddModal = document.getElementById('region-add-modal'); var regionAddOptions = document.getElementById('region-add-options'); var animationAdvancedModal = document.getElementById('animation-advanced-modal'); var regionCardTemplate = document.getElementById('region-card-template'); var regionsJsonInput = document.getElementById('regions-json'); var templateForm = document.getElementById('template-form'); var templateRegionUsageElement = document.getElementById('template-region-usage'); var regionUsageSet = new Set(); var draft = null; var selectedIndex = -1; var activeAnimationCard = null; var overlayRenderFrame = 0; var stageResizeObserver = null; var regionNameValidationToastShown = false; var previewState = { active: false, mode: '', stopTimer: 0, token: 0, animations: [] }; var previewButtons = [ { button: previewIntroButton, mode: 'intro' }, { button: previewOutButton, mode: 'out' }, { button: previewContinuousButton, mode: 'continuous' } ]; try { var regionUsageData = JSON.parse((templateRegionUsageElement && templateRegionUsageElement.textContent) || '[]'); if (Array.isArray(regionUsageData)) { regionUsageData.forEach(function (regionKey) { var normalized = String(regionKey || '').trim(); if (normalized) { regionUsageSet.add(normalized); } }); } } catch (_error) { regionUsageSet = new Set(); } function escapeHtml(value) { return utils.escapeHtml ? utils.escapeHtml(value) : String(value === undefined || value === null ? '' : value); } function valueOrDefault(value, fallback) { return utils.valueOrDefault ? utils.valueOrDefault(value, fallback) : (value === undefined || value === null || value === '' ? fallback : value); } function getCanvasSize() { return { width: Math.max(1, Number(canvasWidthInput.value || 1920)), height: Math.max(1, Number(canvasHeightInput.value || 1080)) }; } function clamp(value, min, max) { return utils.clamp ? utils.clamp(value, min, max) : Math.max(min, Math.min(max, value)); } function normalizeLockRatio(value) { return utils.normalizeLockRatio ? utils.normalizeLockRatio(value) : String(value || '').trim().replace(/\s+/g, ''); } function parseLockRatio(value) { if (utils.parseLockRatio) { return utils.parseLockRatio(value); } var normalized = normalizeLockRatio(value); if (!normalized || !/^\d+:\d+$/.test(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 (utils.getDefaultRegionSize) { return utils.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 getRegionTypeEntries() { if (window.pulseRegionTypes && typeof window.pulseRegionTypes.list === 'function') { return window.pulseRegionTypes.list(); } return ['api', 'html', 'image', 'rss', 'rtmp', 'text', 'time-date', 'video', 'webpage'].map(function (type) { return { type: type, definition: window.pulseRegionTypes && typeof window.pulseRegionTypes.get === 'function' ? window.pulseRegionTypes.get(type) || {} : {} }; }); } function formatRegionTypeLabel(regionType) { return String(regionType || '') .trim() .replace(/(^|[_-])(\w)/g, function (_match, _prefix, letter) { return String(letter || '').toUpperCase(); }) || 'Region'; } function renderAddRegionOptions() { if (!regionAddOptions) { return; } regionAddOptions.innerHTML = getRegionTypeEntries().map(function (entry) { var type = String(entry && entry.type ? entry.type : '').trim(); if (!type) { return ''; } var definition = entry.definition || {}; var label = definition.label || formatRegionTypeLabel(type); return ''; }).join(''); } function getCards() { return Array.prototype.slice.call(regionList.querySelectorAll('.region-item')); } function cardAt(index) { return getCards()[index] || null; } function getRegionName(card) { return utils.getRegionName ? utils.getRegionName(card) : String(card.querySelector('[name="region_name[]"]').value || '').trim(); } function syncRegionIdentity(card, value) { if (utils.syncRegionIdentity) { utils.syncRegionIdentity(card, value); return; } 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 normalizeRegionNamePrefix(regionType) { var prefix = String(regionType || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, ''); return prefix || 'region'; } function buildDefaultRegionName(regionType, index) { return normalizeRegionNamePrefix(regionType) + '_' + index; } function getNextDefaultRegionName(regionType) { var normalizedType = normalizeRegionNamePrefix(regionType); var nextIndex = getCards().reduce(function (count, card) { var currentType = normalizeRegionNamePrefix(card.querySelector('[name="region_type[]"]').value || ''); if (currentType !== normalizedType) { return count; } var currentName = getRegionName(card); var expectedPrefix = normalizedType + '_'; if (currentName.indexOf(expectedPrefix) !== 0) { return count; } var suffix = Number(currentName.slice(expectedPrefix.length)); if (!Number.isFinite(suffix) || suffix < 1) { return count; } return Math.max(count, suffix); }, 0) + 1; return buildDefaultRegionName(normalizedType, nextIndex); } function validateRegionNames() { var cards = getCards(); var names = {}; var hasDuplicate = false; cards.forEach(function (card) { var input = card.querySelector('[name="region_name[]"]'); if (!input) { return; } var normalized = String(input.value || '').trim().toLowerCase(); if (!normalized) { input.setCustomValidity('Region name is required.'); return; } if (!names[normalized]) { names[normalized] = []; } names[normalized].push(input); }); Object.keys(names).forEach(function (key) { var inputs = names[key]; if (inputs.length > 1) { hasDuplicate = true; inputs.forEach(function (input) { input.setCustomValidity('Region names must be unique on this template.'); }); } else { inputs[0].setCustomValidity(''); } }); return !hasDuplicate; } function notifyRegionNameValidationError(message) { if (regionNameValidationToastShown) { return; } regionNameValidationToastShown = true; if (typeof showToast === 'function') { showToast(message || 'Region names must be unique on this template.', 'warning'); } } function readCard(card) { return utils.readCard ? utils.readCard(card) : { region_key: getRegionName(card), label: getRegionName(card), 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 hasAnyAnimationCards() { return getCards().some(function (card) { var hiddenAnimationInput = card.querySelector('[name="region_animation_json[]"]'); var animationConfig = normalizeAnimationConfig(hiddenAnimationInput ? hiddenAnimationInput.value || '{}' : '{}'); return animationConfig && ['intro', 'outro', 'loop'].some(function (stepName) { var step = animationConfig[stepName] || {}; return normalizePreviewAnimation(step.preset) !== 'none'; }); }); } function hasPreviewAnimationForMode(mode) { var previewMode = String(mode || '').trim().toLowerCase(); if (!previewMode) { return false; } return getCards().some(function (card) { var step = getPreviewRegionAnimationStep(readCard(card), previewMode); return normalizePreviewAnimation(step && step.preset) !== 'none'; }); } function getPreviewRects() { if (!overlay) { return []; } return Array.prototype.slice.call(overlay.querySelectorAll('.designer-rect')); } function clearPreviewAnimations() { previewState.animations.forEach(function (animation) { if (animation && typeof animation.cancel === 'function') { animation.cancel(); } }); previewState.animations = []; getPreviewRects().forEach(function (rect) { rect.classList.remove('animate__animated', 'animate__infinite'); rect.classList.remove('animate__repeat-1', 'animate__repeat-2', 'animate__repeat-3'); if (window.templateAnimationPresets && Array.isArray(window.templateAnimationPresets.animateCssValues)) { window.templateAnimationPresets.animateCssValues.forEach(function (value) { if (value && value !== 'none') { rect.classList.remove('animate__' + value); } }); } rect.style.removeProperty('--animate-duration'); rect.style.removeProperty('--animate-delay'); rect.style.removeProperty('--animate-repeat'); rect.style.opacity = ''; rect.style.transform = ''; rect.style.filter = ''; rect.style.willChange = ''; }); } function updatePreviewButtonIcon(button, iconClass) { if (!button) { return; } var icon = button.querySelector('[data-preview-icon]'); if (!icon) { return; } icon.className = 'bi ' + iconClass + ' me-1'; } function updatePreviewControlState() { var activeMode = previewState.active ? previewState.mode : ''; previewButtons.forEach(function (entry) { if (!entry.button) { return; } var available = hasPreviewAnimationForMode(entry.mode); var disabled = !available || (previewState.active && activeMode !== entry.mode); entry.button.disabled = disabled; entry.button.classList.toggle('btn-outline-secondary', disabled); entry.button.classList.toggle('btn-info', !disabled); updatePreviewButtonIcon(entry.button, previewState.active && activeMode === entry.mode ? 'bi-stop-fill' : 'bi-play-fill'); entry.button.setAttribute('aria-pressed', previewState.active && activeMode === entry.mode ? 'true' : 'false'); }); } function normalizePreviewAnimation(value) { if (window.templateAnimationPresets && typeof window.templateAnimationPresets.normalize === 'function') { return window.templateAnimationPresets.normalize(value); } var allowed = {}; var presets = window.templateAnimationPresets || {}; var values = Array.isArray(presets.allValues) && presets.allValues.length ? presets.allValues : [ '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' ]; values.forEach(function (valueName) { allowed[valueName] = true; }); var raw = String(value || '').trim().toLowerCase(); return allowed[raw] ? raw : 'none'; } function isAnimateCssPreset(value) { var presets = window.templateAnimationPresets || {}; if (Array.isArray(presets.animateCssValues) && presets.animateCssValues.length) { return presets.animateCssValues.indexOf(normalizePreviewAnimation(value)) !== -1; } return false; } function applyAnimateCssPreview(rect, preset, step, mode) { var animateClass = 'animate__' + preset; var durationMs = Number(step && step.duration_ms); var delayMs = Number(step && step.delay_ms); var repeatCount = Number(step && step.iterations); var isAttentionSeeker = preset === 'bounce' || preset === 'flash' || preset === 'pulse' || preset === 'rubberBand' || preset === 'shakeX' || preset === 'shakeY' || preset === 'headShake' || preset === 'swing' || preset === 'tada' || preset === 'wobble' || preset === 'jello' || preset === 'heartBeat'; rect.classList.remove('animate__animated', 'animate__infinite'); rect.style.removeProperty('--animate-duration'); rect.style.removeProperty('--animate-delay'); rect.style.removeProperty('--animate-repeat'); rect.style.setProperty('--animate-duration', Math.max(0, Number.isFinite(durationMs) && durationMs > 0 ? durationMs : 1000) + 'ms'); if (Number.isFinite(delayMs) && delayMs > 0) { rect.style.setProperty('--animate-delay', Math.max(0, delayMs) + 'ms'); } rect.classList.add('animate__animated', animateClass); if (isAttentionSeeker) { var normalizedRepeatCount = Math.max(1, Number.isFinite(repeatCount) && repeatCount > 0 ? repeatCount : 1); rect.classList.remove('animate__repeat-1', 'animate__repeat-2', 'animate__repeat-3'); rect.classList.add('animate__repeat-1'); rect.style.setProperty('--animate-repeat', String(normalizedRepeatCount)); } } var MAX_ANIMATION_REPEAT_COUNT = 999; function getPreviewAnimationStepTimingMs(step, mode, animateCssMode) { var normalizedMode = String(mode || '').trim().toLowerCase(); var durationMs = Number(step && step.duration_ms); var delayMs = Number(step && step.delay_ms); var iterations = Number(step && step.iterations); if (!Number.isFinite(durationMs) || durationMs <= 0) { durationMs = animateCssMode ? 1000 : 900; } if (!Number.isFinite(delayMs) || delayMs < 0) { delayMs = 0; } if (!Number.isFinite(iterations) || iterations < 1) { iterations = 1; } if (normalizedMode === 'continuous') { return delayMs + (durationMs * iterations); } return delayMs + durationMs; } function normalizeAnimationStep(value, fallbackPreset) { if (utils.normalizeAnimationStep) { return utils.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.min(MAX_ANIMATION_REPEAT_COUNT, Math.max(1, Math.round(Number(value.iterations)))) : null; 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, fill_mode: '' }; } function normalizeAnimationConfig(value) { if (utils.normalizeAnimationConfig) { return utils.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 getPreviewRegionAnimationStep(region, mode) { if (!region) { return normalizeAnimationStep({}, 'none'); } var animationConfig = normalizeAnimationConfig(region.animation_json !== undefined ? region.animation_json : region); if (mode === 'intro') { return animationConfig.intro; } if (mode === 'out') { return animationConfig.outro; } return animationConfig.loop; } 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 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); } updatePreviewCardVisibility(); return normalized; } function setAnimationPreset(card, stepName, preset) { var config = getAnimationConfig(card); if (config[stepName]) { config[stepName].preset = normalizePreviewAnimation(preset); config[stepName].duration_ms = null; config[stepName].delay_ms = null; config[stepName].iterations = null; config[stepName].easing = ''; config[stepName].fill_mode = ''; writeAnimationConfig(card, config); if (activeAnimationCard === card) { populateAnimationModal(card); } } } function getAnimationModalField(stepName, fieldName) { if (!animationAdvancedModal) { return null; } return animationAdvancedModal.querySelector('[data-animation-modal-step="' + stepName + '"][data-animation-modal-field="' + fieldName + '"]'); } function populateAnimationModal(card) { if (!animationAdvancedModal || !card) { return; } var title = animationAdvancedModal.querySelector('[data-animation-modal-title]'); var label = card.querySelector('[data-region-title]'); var config = getAnimationConfig(card); if (title) { title.textContent = (label ? label.textContent : 'Region') + ' animation settings'; } ['intro', 'outro', 'loop'].forEach(function (stepName) { var step = config[stepName] || normalizeAnimationStep({}, 'none'); var presetInput = getAnimationModalField(stepName, 'preset'); var durationInput = getAnimationModalField(stepName, 'duration_ms'); var delayInput = getAnimationModalField(stepName, 'delay_ms'); if (presetInput) { presetInput.value = step.preset; } if (durationInput) { durationInput.value = String(getAnimationDurationPercent(step.duration_ms)); } if (delayInput) { delayInput.value = step.delay_ms === null || step.delay_ms === undefined ? '0' : String(step.delay_ms); } if (stepName === 'loop') { var iterationsInput = getAnimationModalField(stepName, 'iterations'); if (iterationsInput) { iterationsInput.value = step.iterations === null || step.iterations === undefined ? '1' : String(Math.min(MAX_ANIMATION_REPEAT_COUNT, Math.max(1, Number(step.iterations) || 1))); } } }); } function readNumberField(value) { var raw = String(value || '').trim(); if (!raw) { return null; } var next = Number(raw); return Number.isFinite(next) ? Math.round(next) : null; } function clampAnimationModalField(field) { if (!field || !field.matches) { return; } var isLoopIterationsField = field.matches('[data-animation-modal-step="loop"][data-animation-modal-field="iterations"]'); var isDurationField = field.matches('[data-animation-modal-field="duration_ms"]'); if (!isLoopIterationsField && !isDurationField) { return; } var fieldValue = readNumberField(field.value); if (fieldValue === null) { return; } var normalizedValue = isLoopIterationsField ? Math.min(MAX_ANIMATION_REPEAT_COUNT, Math.max(1, fieldValue)) : Math.min(200, Math.max(1, fieldValue)); if (String(normalizedValue) !== String(field.value)) { field.value = String(normalizedValue); } } function getAnimationDurationPercent(durationMs) { var value = Number(durationMs); if (!Number.isFinite(value) || value <= 0) { return 100; } return Math.min(200, Math.max(1, Math.round(value / 10))); } function getAnimationDurationMs(durationPercent) { var percent = Math.min(200, Math.max(1, readNumberField(durationPercent) || 100)); return percent * 10; } function readAnimationModalConfig() { if (!animationAdvancedModal) { return null; } var config = getAnimationConfig(activeAnimationCard) || { intro: {}, outro: {}, loop: {} }; ['intro', 'outro', 'loop'].forEach(function (stepName) { var presetInput = getAnimationModalField(stepName, 'preset'); var durationInput = getAnimationModalField(stepName, 'duration_ms'); var delayInput = getAnimationModalField(stepName, 'delay_ms'); var nextStep = Object.assign({}, config[stepName] || {}); nextStep.preset = presetInput ? presetInput.value : nextStep.preset; nextStep.duration_ms = getAnimationDurationMs(durationInput ? durationInput.value : ''); nextStep.delay_ms = readNumberField(delayInput ? delayInput.value : '') || 0; if (stepName === 'loop') { var iterationsInput = getAnimationModalField(stepName, 'iterations'); nextStep.iterations = Math.min(MAX_ANIMATION_REPEAT_COUNT, Math.max(1, readNumberField(iterationsInput ? iterationsInput.value : '') || 1)); } config[stepName] = normalizeAnimationStep(nextStep, 'none'); }); return config; } function saveAnimationModal() { if (!activeAnimationCard) { return; } var config = readAnimationModalConfig(); if (!config) { return; } writeAnimationConfig(activeAnimationCard, config); renderOverlay(); } function setPreviewControlState(active) { updatePreviewControlState(); } function updatePreviewCardVisibility() { if (!previewCard) { return; } previewCard.hidden = !hasAnyAnimationCards(); } function setTemplateEditorLocked(locked) { if (templateForm) { templateForm.classList.toggle('template-designer-form--previewing', locked); } if (stage) { stage.classList.toggle('is-previewing', locked); } if (previewCard) { previewCard.classList.toggle('is-previewing', locked); } if (overlay) { overlay.style.pointerEvents = locked ? 'none' : ''; } var controls = templateForm ? templateForm.querySelectorAll('input, select, textarea, button') : []; Array.prototype.forEach.call(controls, function (control) { if (!control) { return; } if (control.matches && control.matches('[data-preview-control]')) { return; } if (locked) { if (!control.hasAttribute('data-preview-disabled')) { control.setAttribute('data-preview-disabled', control.disabled ? '1' : '0'); } control.disabled = true; } else if (control.hasAttribute('data-preview-disabled')) { control.disabled = control.getAttribute('data-preview-disabled') === '1'; control.removeAttribute('data-preview-disabled'); } }); setPreviewControlState(locked); } function stopTemplatePreview(skipRender) { if (previewState.stopTimer) { window.clearTimeout(previewState.stopTimer); previewState.stopTimer = 0; } previewState.token += 1; previewState.active = false; previewState.mode = ''; clearPreviewAnimations(); setTemplateEditorLocked(false); if (!skipRender) { renderOverlay(); } } function schedulePreviewStop(delayMs, token) { if (previewState.stopTimer) { window.clearTimeout(previewState.stopTimer); } previewState.stopTimer = window.setTimeout(function () { if (token !== previewState.token) { return; } stopTemplatePreview(false); }, Math.max(0, Number(delayMs || 0))); } function animatePreviewRects(mode, regions) { var rects = getPreviewRects(); var token = previewState.token; var stagger = 90; var duration = 900; var overlayRect = getOverlayRect(); var canvasMovement = Math.max(12, Math.round(overlayRect.height * 0.02)); var longestTimingMs = 0; previewState.animations = []; rects.forEach(function (rect, index) { var region = regions && regions[index] ? regions[index] : null; var step = getPreviewRegionAnimationStep(region, mode); var preset = normalizePreviewAnimation(step && step.preset); var delay = index * stagger; var animation = null; var animateCssMode = isAnimateCssPreset(preset); var timingMs = delay + getPreviewAnimationStepTimingMs(step, mode, animateCssMode); rect.style.willChange = 'transform, opacity, filter'; if (timingMs > longestTimingMs) { longestTimingMs = timingMs; } if (preset === 'none') { return; } if (animateCssMode) { applyAnimateCssPreview(rect, preset, step, mode); return; } if (mode === 'intro') { animation = rect.animate([ { opacity: 0, transform: 'translate3d(0, ' + canvasMovement + 'px, 0) scale(0.96)', filter: 'blur(1px)' }, { opacity: 1, transform: 'translate3d(0, 0, 0) scale(1)', filter: 'blur(0)' } ], { duration: duration, delay: delay, easing: 'cubic-bezier(0.22, 1, 0.36, 1)', fill: 'both' }); } else if (mode === 'out') { animation = rect.animate([ { opacity: 1, transform: 'translate3d(0, 0, 0) scale(1)', filter: 'blur(0)' }, { opacity: 0, transform: 'translate3d(0, -' + canvasMovement + 'px, 0) scale(0.96)', filter: 'blur(1px)' } ], { duration: duration, delay: delay, easing: 'cubic-bezier(0.4, 0, 1, 1)', fill: 'both' }); } if (animation) { previewState.animations.push(animation); } }); schedulePreviewStop(longestTimingMs + 140, token); } function startTemplatePreview(mode) { var previewMode = String(mode || '').trim().toLowerCase(); if (!previewMode) { return; } var previewRegions = getCards().map(readCard); stopTemplatePreview(true); previewState.active = true; previewState.mode = previewMode; previewState.token += 1; setTemplateEditorLocked(true); if (!getPreviewRects().length) { schedulePreviewStop(120, previewState.token); return; } animatePreviewRects(previewMode, previewRegions); } function writeCard(card, values) { if (utils.writeCard) { utils.writeCard(card, values); return; } 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 getOverlayRect() { return utils.getOverlayRect ? utils.getOverlayRect(overlay) : overlay.getBoundingClientRect(); } function toCanvasPoint(event) { return utils.toCanvasPoint ? utils.toCanvasPoint(event, overlay, getCanvasSize()) : { x: 0, y: 0 }; } function canvasRectToPixels(region) { return utils.canvasRectToPixels ? utils.canvasRectToPixels(region, overlay, getCanvasSize()) : { left: 0, top: 0, width: 0, height: 0 }; } function updateAspectRatio() { var size = getCanvasSize(); stage.style.aspectRatio = size.width + ' / ' + size.height; } function updateCanvasSizeSummary() { var option = canvasSizeSelect.options[canvasSizeSelect.selectedIndex]; canvasSizeSummary.textContent = option ? option.textContent : ''; } function updateCanvasSizeLock() { var lockOnExistingTemplate = canvasSizeSelect.dataset.lockOnExistingTemplate === 'true'; var locked = lockOnExistingTemplate && getCards().length > 0; canvasSizeSelect.disabled = locked; canvasSizeSummary.classList.toggle('is-locked', locked); } function syncCanvasSizeSelection() { var option = canvasSizeSelect.options[canvasSizeSelect.selectedIndex]; if (!option) { return; } if (canvasSizeIdInput) { canvasSizeIdInput.value = option.value; } canvasWidthInput.value = Math.max(1, Number(option.dataset.width || canvasWidthInput.value || 1920)); canvasHeightInput.value = Math.max(1, Number(option.dataset.height || canvasHeightInput.value || 1080)); updateAspectRatio(); updateCanvasSizeSummary(); getCards().forEach(function (card) { normalizeRegionCard(card, 'width'); }); } function fitRegionToCanvas(region, aspect) { var size = getCanvasSize(); var minSize = 12; var next = { x: Math.round(Number(region && region.x !== undefined ? region.x : 0)), y: Math.round(Number(region && region.y !== undefined ? region.y : 0)), width: Math.max(minSize, Math.round(Number(region && region.width !== undefined ? region.width : minSize))), height: Math.max(minSize, Math.round(Number(region && region.height !== undefined ? region.height : minSize))) }; if (aspect && Number.isFinite(aspect.ratio) && aspect.ratio > 0) { if (next.width > size.width || next.height > size.height) { var canvasRatio = size.width / Math.max(1, size.height); if (canvasRatio >= aspect.ratio) { next.height = Math.max(minSize, Math.min(size.height, next.height, Math.round(size.height))); next.width = Math.max(minSize, Math.round(next.height * aspect.ratio)); if (next.width > size.width) { next.width = size.width; next.height = Math.max(minSize, Math.round(next.width / aspect.ratio)); } } else { next.width = Math.max(minSize, Math.min(size.width, next.width, Math.round(size.width))); next.height = Math.max(minSize, Math.round(next.width / aspect.ratio)); if (next.height > size.height) { next.height = size.height; next.width = Math.max(minSize, Math.round(next.height * aspect.ratio)); } } } } else { if (next.width > size.width) { next.width = size.width; next.x = 0; } if (next.height > size.height) { next.height = size.height; next.y = 0; } } if (next.x + next.width > size.width) { next.x = size.width - next.width; } if (next.y + next.height > size.height) { next.y = size.height - next.height; } next.x = clamp(next.x, 0, Math.max(0, size.width - next.width)); next.y = clamp(next.y, 0, Math.max(0, size.height - next.height)); return next; } function normalizeRegionCard(card, changedField) { if (!card) { return null; } var current = readCard(card); var next = { x: current.x, y: current.y, width: Math.max(12, Math.round(current.width || 0)), height: Math.max(12, Math.round(current.height || 0)) }; var lockRatio = normalizeLockRatio(current.lock_ratio); var aspect = lockRatio ? parseLockRatio(lockRatio) : null; var size = getCanvasSize(); if (aspect) { if (changedField === 'height') { next.width = Math.max(12, Math.round(next.height * aspect.ratio)); } else { next.height = Math.max(12, Math.round(next.width / aspect.ratio)); } if (next.width > size.width || next.height > size.height) { var scale = Math.min(size.width / Math.max(1, next.width), size.height / Math.max(1, next.height), 1); if (scale < 1) { next.width = Math.max(12, Math.round(next.width * scale)); next.height = Math.max(12, Math.round(next.height * scale)); if (changedField === 'height') { next.width = Math.max(12, Math.round(next.height * aspect.ratio)); } else { next.height = Math.max(12, Math.round(next.width / aspect.ratio)); } } } } next = fitRegionToCanvas(next, aspect); writeCard(card, next); return next; } function updateBackgroundPreview(file) { if (!file) { return; } if (removeBackgroundFlag) { removeBackgroundFlag.checked = false; } var reader = new FileReader(); reader.onload = function () { backgroundPreview.src = reader.result; backgroundPreview.style.display = 'block'; backgroundEmpty.style.display = 'none'; }; reader.readAsDataURL(file); } function updateStageBackgroundColor() { if (!stage) { return; } stage.style.backgroundColor = backgroundColorInput && backgroundColorInput.value ? backgroundColorInput.value : '#111111'; } function updateRegionLockBadge(card) { var lockChip = card.querySelector('[data-region-lock-chip]'); if (!lockChip) { return; } var lockRatio = normalizeLockRatio(card.querySelector('[name="region_lock_ratio[]"]').value); if (!lockRatio) { lockChip.hidden = true; lockChip.textContent = ''; return; } lockChip.hidden = false; lockChip.textContent = 'Locked ' + lockRatio; } function updateRegionLockRatioState(card) { var lockRatioInput = card.querySelector('[name="region_lock_ratio[]"]'); var regionTypeInput = card.querySelector('[name="region_type[]"]'); if (!lockRatioInput || !regionTypeInput) { return; } var isQrCode = String(regionTypeInput.value || '').trim() === 'qr-code'; lockRatioInput.disabled = isQrCode; lockRatioInput.setAttribute('aria-disabled', isQrCode ? 'true' : 'false'); if (isQrCode) { lockRatioInput.value = '1:1'; lockRatioInput.title = 'QR code regions are fixed to 1:1.'; } else { lockRatioInput.removeAttribute('title'); } } function getRegionLockRatio(card) { return normalizeLockRatio(card.querySelector('[name="region_lock_ratio[]"]').value); } function isRegionLocked(card) { return Boolean(getRegionLockRatio(card)); } function syncLockedDimensions(card, changedField) { normalizeRegionCard(card, changedField); updateRegionLockBadge(card); markTemplateFormDirty(); requestOverlayRender(); } function getRegionChipLabel(regionType) { if (window.pulseRegionTypes && typeof window.pulseRegionTypes.getRegionChipLabel === 'function') { return window.pulseRegionTypes.getRegionChipLabel(regionType); } return 'Text'; } function populateRegionCard(card, region) { var chip = card.querySelector('[data-region-chip]'); var title = card.querySelector('[data-region-title]'); var nameInput = card.querySelector('[name="region_name[]"]'); var regionTypeInput = card.querySelector('[name="region_type[]"]'); var regionKeyInput = card.querySelector('[name="region_key[]"]'); var regionLabelInput = card.querySelector('[name="region_label[]"]'); if (title) { title.textContent = region.label || region.region_key || 'Region'; } if (chip) { chip.textContent = getRegionChipLabel(region.region_type); } if (nameInput) { nameInput.value = region.region_key || region.label || ''; } if (regionTypeInput) { regionTypeInput.value = region.region_type || 'text'; } if (regionKeyInput) { regionKeyInput.value = region.region_key || region.label || ''; } if (regionLabelInput) { regionLabelInput.value = region.label || region.region_key || ''; } var regionLockRatioInput = card.querySelector('[name="region_lock_ratio[]"]'); if (regionLockRatioInput) { regionLockRatioInput.value = normalizeLockRatio(region.lock_ratio || (String(region.region_type || '').trim() === 'qr-code' ? '1:1' : '')); } writeAnimationConfig(card, region.animation_json); card.querySelector('[name="region_x[]"]').value = valueOrDefault(region.x, 80); card.querySelector('[name="region_y[]"]').value = valueOrDefault(region.y, 80); card.querySelector('[name="region_z[]"]').value = valueOrDefault(region.z_index, 1); card.querySelector('[name="region_width[]"]').value = valueOrDefault(region.width, 300); card.querySelector('[name="region_height[]"]').value = valueOrDefault(region.height, 120); updateRegionLockRatioState(card); updateRegionLockBadge(card); } function setRegionRemovalState(card) { var removeButton = card.querySelector('[data-region-remove-button]'); if (!removeButton) { return; } var regionKey = String(card.dataset.regionKey || getRegionName(card) || '').trim(); var regionDeletionLocked = regionUsageSet.has(regionKey); removeButton.disabled = regionDeletionLocked; if (regionDeletionLocked) { removeButton.title = 'Regions cannot be deleted while this template is used by slides.'; } else { removeButton.removeAttribute('title'); } } function updateRegionLabel(card) { var label = getRegionName(card) || 'Region'; var cards = getCards(); var index = cards.indexOf(card); var title = card.querySelector('.template-field-head strong'); if (title) { title.textContent = label; } if (index >= 0 && regionSelect.options[index]) { regionSelect.options[index].textContent = label; } } function makeRegionCard(region) { var card; if (regionCardTemplate && regionCardTemplate.content) { card = regionCardTemplate.content.firstElementChild.cloneNode(true); } else { card = document.createElement('div'); card.className = 'card card-outline card-secondary admin-form-card region-item mb-3'; } populateRegionCard(card, region); card.dataset.regionKey = String(region.region_key || region.label || getRegionName(card) || '').trim(); setRegionRemovalState(card); var nameInput = card.querySelector('[name="region_name[]"]'); var lockRatioInput = card.querySelector('[name="region_lock_ratio[]"]'); var widthInput = card.querySelector('[name="region_width[]"]'); var heightInput = card.querySelector('[name="region_height[]"]'); var animationPresetInputs = card.querySelectorAll('[data-animation-preset]'); var animationResetButtons = card.querySelectorAll('[data-animation-reset-button]'); var animationAdvancedButton = card.querySelector('[data-animation-advanced-button]'); nameInput.addEventListener('input', function () { regionNameValidationToastShown = false; syncRegionIdentity(card, nameInput.value); updateRegionLabel(card); validateRegionNames(); renderRegionSidebar(); renderOverlay(); }); Array.prototype.forEach.call(animationPresetInputs, function (input) { input.addEventListener('change', function () { setAnimationPreset(card, input.getAttribute('data-animation-preset'), input.value); markTemplateFormDirty(); requestOverlayRender(); }); }); Array.prototype.forEach.call(animationResetButtons, function (button) { button.addEventListener('click', function (event) { event.preventDefault(); setAnimationPreset(card, button.getAttribute('data-animation-reset-button'), 'none'); markTemplateFormDirty(); requestOverlayRender(); }); }); if (animationAdvancedButton && animationAdvancedModal) { animationAdvancedButton.addEventListener('click', function () { activeAnimationCard = card; populateAnimationModal(card); if (window.pulseModal) { window.pulseModal.show(animationAdvancedModal); } }); } if (lockRatioInput) { lockRatioInput.addEventListener('input', function () { syncLockedDimensions(card, 'width'); }); } if (widthInput) { widthInput.addEventListener('input', function () { syncLockedDimensions(card, 'width'); }); } if (heightInput) { heightInput.addEventListener('input', function () { syncLockedDimensions(card, 'height'); }); } card.addEventListener('click', function (event) { if (event.target && event.target.closest && event.target.closest('[data-region-remove-button]')) { return; } setSelected(getCards().indexOf(card)); }); card.querySelector('[data-region-remove-button]').addEventListener('click', function (event) { event.preventDefault(); card.remove(); markTemplateFormDirty(); if (!getCards().length) { selectedIndex = -1; } else if (selectedIndex >= getCards().length) { selectedIndex = getCards().length - 1; } renderRegionSidebar(); renderOverlay(); }); return card; } function renderRegionList(initialRegions) { regionList.innerHTML = ''; initialRegions.forEach(function (region) { regionList.appendChild(makeRegionCard(region)); }); } function renderRegionSidebar() { var cards = getCards(); regionSelect.innerHTML = ''; if (!cards.length) { regionSelect.disabled = true; regionList.innerHTML = ''; updateCanvasSizeLock(); return; } regionSelect.disabled = false; if (selectedIndex < 0 || selectedIndex >= cards.length) { selectedIndex = 0; } cards.forEach(function (card, index) { var option = document.createElement('option'); option.value = String(index); option.textContent = getRegionName(card) || ('Region ' + (index + 1)); if (index === selectedIndex) { option.selected = true; } regionSelect.appendChild(option); card.hidden = index !== selectedIndex; }); regionSelect.value = String(selectedIndex); updateCanvasSizeLock(); validateRegionNames(); updatePreviewCardVisibility(); } function renderOverlay() { if (previewState.active) { return; } var cards = getCards(); var selectedCard = selectedIndex >= 0 ? cards[selectedIndex] : null; var selectedNow = selectedCard ? cards.indexOf(selectedCard) : -1; var regions = cards.map(readCard); var canvasSize = getCanvasSize(); var overlayRect = getOverlayRect(); var overlayScale = overlayRect.width / Math.max(1, canvasSize.width); overlay.style.setProperty('--designer-overlay-scale', String(overlayScale)); regionsJsonInput.value = JSON.stringify(regions); overlay.innerHTML = regions.map(function (region, index) { var box = canvasRectToPixels(region); var renderedBox = { left: Math.round(box.left), top: Math.round(box.top), width: Math.round(box.width), height: Math.round(box.height) }; var selected = index === selectedNow ? ' selected' : ''; return '
' + escapeHtml(region.label || region.region_key || 'Region') + '
'; }).join(''); if (draft) { var rect = overlayRect; var size = canvasSize; var draftBox = { x: Math.min(draft.start.x, draft.end.x), y: Math.min(draft.start.y, draft.end.y), width: Math.abs(draft.end.x - draft.start.x), height: Math.abs(draft.end.y - draft.start.y) }; overlay.innerHTML += '
'; } updatePreviewControlState(); } function requestOverlayRender() { if (previewState.active) { return; } if (overlayRenderFrame) { return; } overlayRenderFrame = window.requestAnimationFrame(function () { overlayRenderFrame = 0; renderOverlay(); }); } function render() { updateAspectRatio(); renderRegionSidebar(); renderOverlay(); } function observeStageResize() { if (!stage) { return; } if (window.ResizeObserver) { stageResizeObserver = new window.ResizeObserver(function () { requestOverlayRender(); }); stageResizeObserver.observe(stage); return; } window.addEventListener('resize', requestOverlayRender); } function markTemplateFormDirty() { if (!templateForm) { return; } templateForm.dataset.dirty = 'true'; } function setSelected(index) { if (previewState.active) { return; } var cards = getCards(); if (!cards.length) { selectedIndex = -1; } else if (index < 0) { selectedIndex = 0; } else { selectedIndex = clamp(index, 0, cards.length - 1); } renderRegionSidebar(); renderOverlay(); } function addRegion(region) { if (previewState.active) { return; } var hint = regionList.querySelector('.muted'); if (hint) { hint.remove(); } var card = makeRegionCard(region); setRegionRemovalState(card); regionList.appendChild(card); markTemplateFormDirty(); setSelected(getCards().length - 1); } function openAddRegionModal() { if (!window.pulseModal) { return; } window.pulseModal.show(regionAddModal); } function createDefaultRegion(type) { var name = getNextDefaultRegionName(type); var lockRatio = String(type || '').trim() === 'qr-code' ? '1:1' : ''; var size = getDefaultRegionSize(type, lockRatio); return { region_key: name, label: name, region_type: type, animation_json: normalizeAnimationConfig('{}'), x: 80, y: 80, width: size.width, height: size.height, lock_ratio: lockRatio, z_index: 1 }; } function clampRegion(region) { var size = getCanvasSize(); var minSize = 12; var x = clamp(region.x, 0, size.width - minSize); var y = clamp(region.y, 0, size.height - minSize); var width = Math.max(minSize, region.width); var height = Math.max(minSize, region.height); if (x + width > size.width) { width = size.width - x; } if (y + height > size.height) { height = size.height - y; } return { x: Math.round(x), y: Math.round(y), width: Math.round(Math.max(minSize, width)), height: Math.round(Math.max(minSize, height)) }; } function startDraw(event) { if (previewState.active) { return; } var start = toCanvasPoint(event); draft = { start: start, end: start }; renderOverlay(); function moveHandler(moveEvent) { draft.end = toCanvasPoint(moveEvent); requestOverlayRender(); } function upHandler(upEvent) { draft.end = toCanvasPoint(upEvent); var width = Math.abs(draft.end.x - draft.start.x); var height = Math.abs(draft.end.y - draft.start.y); if (width >= 8 && height >= 8) { var region = fitRegionToCanvas({ x: Math.min(draft.start.x, draft.end.x), y: Math.min(draft.start.y, draft.end.y), width: width, height: height }); var name = getNextDefaultRegionName('text'); addRegion({ region_key: name, label: name, region_type: 'text', x: region.x, y: region.y, width: region.width, height: region.height, z_index: 1 }); } draft = null; requestOverlayRender(); document.removeEventListener('mousemove', moveHandler); document.removeEventListener('mouseup', upHandler); } document.addEventListener('mousemove', moveHandler); document.addEventListener('mouseup', upHandler); } function startMove(index, event) { if (previewState.active) { return; } var startPoint = toCanvasPoint(event); var startRegion = readCard(cardAt(index)); function moveHandler(moveEvent) { var currentPoint = toCanvasPoint(moveEvent); var dx = currentPoint.x - startPoint.x; var dy = currentPoint.y - startPoint.y; var size = getCanvasSize(); var maxX = Math.max(0, size.width - startRegion.width); var maxY = Math.max(0, size.height - startRegion.height); var nextX = clamp(startRegion.x + dx, 0, maxX); var nextY = clamp(startRegion.y + dy, 0, maxY); writeCard(cardAt(index), { x: nextX, y: nextY, width: startRegion.width, height: startRegion.height }); markTemplateFormDirty(); requestOverlayRender(); } function upHandler() { document.removeEventListener('mousemove', moveHandler); document.removeEventListener('mouseup', upHandler); } document.addEventListener('mousemove', moveHandler); document.addEventListener('mouseup', upHandler); } function resizeFromHandle(index, dir, event) { if (previewState.active) { return; } var startPoint = toCanvasPoint(event); var startRegion = readCard(cardAt(index)); var lockRatio = normalizeLockRatio(startRegion.lock_ratio); var aspect = lockRatio ? parseLockRatio(lockRatio) : null; var canvasSize = getCanvasSize(); var minSize = 12; function clampResizeDelta(dx, dy) { if (dir.indexOf('w') !== -1) { dx = clamp(dx, -startRegion.x, startRegion.width - minSize); } if (dir.indexOf('e') !== -1) { dx = clamp(dx, minSize - startRegion.width, canvasSize.width - startRegion.x - startRegion.width); } if (dir.indexOf('n') !== -1) { dy = clamp(dy, -startRegion.y, startRegion.height - minSize); } if (dir.indexOf('s') !== -1) { dy = clamp(dy, minSize - startRegion.height, canvasSize.height - startRegion.y - startRegion.height); } return { dx: dx, dy: dy }; } function fitFromWidth(width) { var nextWidth = Math.max(12, Math.round(width)); return { width: nextWidth, height: aspect ? Math.max(12, Math.round(nextWidth / aspect.ratio)) : startRegion.height }; } function fitFromHeight(height) { var nextHeight = Math.max(12, Math.round(height)); return { width: aspect ? Math.max(12, Math.round(nextHeight * aspect.ratio)) : startRegion.width, height: nextHeight }; } function moveHandler(moveEvent) { var currentPoint = toCanvasPoint(moveEvent); var dx = currentPoint.x - startPoint.x; var dy = currentPoint.y - startPoint.y; var constrainedDelta = clampResizeDelta(dx, dy); dx = constrainedDelta.dx; dy = constrainedDelta.dy; var next = { x: startRegion.x, y: startRegion.y, width: startRegion.width, height: startRegion.height }; if (aspect) { if (dir === 'e') { var eastSize = fitFromWidth(startRegion.width + dx); next.width = eastSize.width; next.height = eastSize.height; next.y = startRegion.y + Math.round((startRegion.height - next.height) / 2); } else if (dir === 'w') { var westSize = fitFromWidth(startRegion.width - dx); next.width = westSize.width; next.height = westSize.height; next.x = startRegion.x + startRegion.width - next.width; next.y = startRegion.y + Math.round((startRegion.height - next.height) / 2); } else if (dir === 'n') { var northSize = fitFromHeight(startRegion.height - dy); next.width = northSize.width; next.height = northSize.height; next.x = startRegion.x + Math.round((startRegion.width - next.width) / 2); next.y = startRegion.y + startRegion.height - next.height; } else if (dir === 's') { var southSize = fitFromHeight(startRegion.height + dy); next.width = southSize.width; next.height = southSize.height; next.x = startRegion.x + Math.round((startRegion.width - next.width) / 2); } else { var useWidth = Math.abs(dx) >= Math.abs(dy * aspect.ratio); var cornerSize = useWidth ? fitFromWidth(dir.indexOf('w') !== -1 ? startRegion.width - dx : startRegion.width + dx) : fitFromHeight(dir.indexOf('n') !== -1 ? startRegion.height - dy : startRegion.height + dy); next.width = cornerSize.width; next.height = cornerSize.height; if (dir.indexOf('w') !== -1) { next.x = startRegion.x + startRegion.width - next.width; } if (dir.indexOf('n') !== -1) { next.y = startRegion.y + startRegion.height - next.height; } if (dir.indexOf('e') !== -1) { next.x = startRegion.x; } if (dir.indexOf('s') !== -1) { next.y = startRegion.y; } } } else { if (dir.indexOf('w') !== -1) { next.x = startRegion.x + dx; next.width = startRegion.width - dx; } if (dir.indexOf('e') !== -1) { next.width = startRegion.width + dx; } if (dir.indexOf('n') !== -1) { next.y = startRegion.y + dy; next.height = startRegion.height - dy; } if (dir.indexOf('s') !== -1) { next.height = startRegion.height + dy; } } if (next.width < 12) { if (dir.indexOf('w') !== -1) { next.x -= 12 - next.width; } next.width = 12; } if (next.height < 12) { if (dir.indexOf('n') !== -1) { next.y -= 12 - next.height; } next.height = 12; } next = fitRegionToCanvas(next, aspect); writeCard(cardAt(index), { x: next.x, y: next.y, width: next.width, height: next.height }); markTemplateFormDirty(); requestOverlayRender(); } function upHandler() { document.removeEventListener('mousemove', moveHandler); document.removeEventListener('mouseup', upHandler); } document.addEventListener('mousemove', moveHandler); document.addEventListener('mouseup', upHandler); } if (addRegionButton && regionAddModal) { addRegionButton.addEventListener('click', function () { renderAddRegionOptions(); openAddRegionModal(); }); if (regionAddOptions) { regionAddOptions.addEventListener('click', function (event) { var button = event.target && event.target.closest ? event.target.closest('[data-add-region-type]') : null; if (!button) { return; } var regionType = button.getAttribute('data-add-region-type'); addRegion(createDefaultRegion(regionType)); if (window.pulseModal) { window.pulseModal.hide(regionAddModal); } }); } renderAddRegionOptions(); } backgroundInput.addEventListener('change', function () { var file = backgroundInput.files && backgroundInput.files[0]; if (file) { updateBackgroundPreview(file); } }); if (removeBackgroundButton && removeBackgroundFlag) { removeBackgroundButton.addEventListener('click', function () { removeBackgroundFlag.checked = true; backgroundInput.value = ''; backgroundPreview.removeAttribute('src'); backgroundPreview.style.display = 'none'; backgroundEmpty.style.display = 'block'; }); } if (animationAdvancedModal) { animationAdvancedModal.addEventListener('hidden.bs.modal', function () { activeAnimationCard = null; }); animationAdvancedModal.addEventListener('input', function (event) { clampAnimationModalField(event.target); markTemplateFormDirty(); }); animationAdvancedModal.addEventListener('change', function () { markTemplateFormDirty(); }); var animationModalApplyButton = animationAdvancedModal.querySelector('[data-animation-modal-apply]'); var animationModalResetButton = animationAdvancedModal.querySelector('[data-animation-modal-reset]'); if (animationModalApplyButton) { animationModalApplyButton.addEventListener('click', function () { saveAnimationModal(); markTemplateFormDirty(); if (window.pulseModal) { window.pulseModal.hide(animationAdvancedModal); } }); } if (animationModalResetButton) { animationModalResetButton.addEventListener('click', function () { if (!activeAnimationCard) { return; } writeAnimationConfig(activeAnimationCard, normalizeAnimationConfig('{}')); markTemplateFormDirty(); populateAnimationModal(activeAnimationCard); requestOverlayRender(); }); } } if (backgroundColorInput) { backgroundColorInput.addEventListener('input', updateStageBackgroundColor); } canvasSizeSelect.addEventListener('change', function () { syncCanvasSizeSelection(); render(); }); canvasWidthInput.addEventListener('input', render); canvasHeightInput.addEventListener('input', render); regionSelect.addEventListener('change', function () { setSelected(Number(regionSelect.value || 0)); }); overlay.addEventListener('mousedown', function (event) { var rect = event.target.closest('.designer-rect'); if (rect) { var index = Number(rect.getAttribute('data-index')); var handle = event.target.closest('.resize-handle'); event.preventDefault(); setSelected(index); if (handle) { resizeFromHandle(index, handle.getAttribute('data-dir'), event); } else { startMove(index, event); } return; } if (event.target !== overlay && !event.target.classList.contains('designer-overlay')) { return; } event.preventDefault(); setSelected(-1); }); if (templateForm) { templateForm.addEventListener('invalid', function (event) { var target = event.target; if (!target || !target.validationMessage || !target.validationMessage.match(/Region names must be unique on this template\.|Region name is required\./)) { return; } if (target.closest && !target.closest('.region-item')) { return; } notifyRegionNameValidationError(target.validationMessage); }, true); templateForm.addEventListener('formdata', function (event) { if (previewState.active) { event.preventDefault(); return; } if (!validateRegionNames()) { event.preventDefault(); return; } getCards().forEach(function (card) { normalizeRegionCard(card, 'width'); }); syncCanvasSizeSelection(); event.formData.set('regions_json', JSON.stringify(getCards().map(readCard))); }); templateForm.addEventListener('submit', function (event) { if (previewState.active) { return; } if (!validateRegionNames()) { return; } getCards().forEach(function (card) { normalizeRegionCard(card, 'width'); }); syncCanvasSizeSelection(); regionsJsonInput.value = JSON.stringify(getCards().map(readCard)); }); } if (previewIntroButton) { previewIntroButton.addEventListener('click', function () { if (previewState.active && previewState.mode === 'intro') { stopTemplatePreview(false); return; } startTemplatePreview('intro'); }); } if (previewOutButton) { previewOutButton.addEventListener('click', function () { if (previewState.active && previewState.mode === 'out') { stopTemplatePreview(false); return; } startTemplatePreview('out'); }); } if (previewContinuousButton) { previewContinuousButton.addEventListener('click', function () { if (previewState.active && previewState.mode === 'continuous') { stopTemplatePreview(false); return; } startTemplatePreview('continuous'); }); } renderRegionList(existingRegions); syncCanvasSizeSelection(); updateStageBackgroundColor(); observeStageResize(); render(); })();