361 lines
14 KiB
JavaScript
361 lines
14 KiB
JavaScript
// Shared template designer utility helpers.
|
|
|
|
(function () {
|
|
function escapeHtml(value) {
|
|
return String(value === undefined || value === null ? '' : value)
|
|
.replace(/&/g, '&')
|
|
.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));
|
|
}
|
|
|
|
var MAX_ANIMATION_REPEAT_COUNT = 999;
|
|
|
|
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
|
|
? clamp(Math.round(Number(value.iterations)), 1, MAX_ANIMATION_REPEAT_COUNT)
|
|
: 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
|
|
};
|
|
}());
|