Add template animation editor
This commit is contained in:
+61
-2
@@ -1,6 +1,7 @@
|
||||
// Template data access helpers and region normalization logic.
|
||||
|
||||
const { parseJsonSafe, readFormArray } = require('./utils');
|
||||
const animationPresets = require('../web/public/js/templates/animation-presets');
|
||||
|
||||
function sanitizeBackgroundColor(value) {
|
||||
const raw = String(value || '').trim();
|
||||
@@ -23,6 +24,61 @@ function normalizeTemplateRegionLockRatio(value) {
|
||||
return rawRatio.replace(/\s+/g, '');
|
||||
}
|
||||
|
||||
function normalizeTemplateRegionAnimation(value) {
|
||||
if (animationPresets && typeof animationPresets.normalize === 'function') {
|
||||
return animationPresets.normalize(value);
|
||||
}
|
||||
|
||||
const allowed = new Set(
|
||||
Array.isArray(animationPresets && animationPresets.allValues) && animationPresets.allValues.length
|
||||
? animationPresets.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']
|
||||
);
|
||||
const raw = String(value || '').trim().toLowerCase();
|
||||
return allowed.has(raw) ? raw : 'none';
|
||||
}
|
||||
|
||||
function normalizeTemplateRegionAnimationStep(value, fallbackPreset) {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
const normalized = {};
|
||||
Object.keys(value).forEach((key) => {
|
||||
normalized[key] = value[key];
|
||||
});
|
||||
normalized.preset = normalizeTemplateRegionAnimation(value.preset !== undefined ? value.preset : fallbackPreset || 'none');
|
||||
return normalized;
|
||||
}
|
||||
|
||||
return {
|
||||
preset: normalizeTemplateRegionAnimation(typeof value === 'string' ? value : fallbackPreset || 'none')
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTemplateRegionAnimationConfig(value) {
|
||||
let raw = value;
|
||||
if (typeof raw === 'string') {
|
||||
const 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: normalizeTemplateRegionAnimationStep(raw.intro, 'none'),
|
||||
outro: normalizeTemplateRegionAnimationStep(raw.outro !== undefined ? raw.outro : raw.out, 'none'),
|
||||
loop: normalizeTemplateRegionAnimationStep(raw.loop, 'none')
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTemplateRegionName(value) {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
@@ -57,7 +113,7 @@ async function fetchTemplateById(pool, id) {
|
||||
return null;
|
||||
}
|
||||
const template = templates[0];
|
||||
const [regions] = await pool.query('SELECT id, template_id, region_key, region_type, label, lock_ratio, x, y, width, height, z_index, created_at, modified_at, created_by, modified_by FROM c_template_regions WHERE template_id = ? ORDER BY z_index ASC, id ASC', [id]);
|
||||
const [regions] = await pool.query('SELECT id, template_id, region_key, region_type, label, lock_ratio, animation_json, x, y, width, height, z_index, created_at, modified_at, created_by, modified_by FROM c_template_regions WHERE template_id = ? ORDER BY z_index ASC, id ASC', [id]);
|
||||
template.regions = regions;
|
||||
return template;
|
||||
}
|
||||
@@ -70,7 +126,7 @@ async function fetchTemplatesData(pool) {
|
||||
LEFT JOIN c_canvas_sizes cs ON cs.id = st.canvas_size_id
|
||||
ORDER BY st.id DESC
|
||||
`);
|
||||
const [templateRegions] = await pool.query('SELECT id, template_id, region_key, region_type, label, lock_ratio, x, y, width, height, z_index, created_at, modified_at, created_by, modified_by FROM c_template_regions ORDER BY template_id ASC, z_index ASC, id ASC');
|
||||
const [templateRegions] = await pool.query('SELECT id, template_id, region_key, region_type, label, lock_ratio, animation_json, x, y, width, height, z_index, created_at, modified_at, created_by, modified_by FROM c_template_regions ORDER BY template_id ASC, z_index ASC, id ASC');
|
||||
return { templates, templateRegions };
|
||||
}
|
||||
|
||||
@@ -86,6 +142,7 @@ function extractTemplateRegions(body) {
|
||||
region_type: regionType,
|
||||
label: String(region.region_name || region.label || region.region_key || '').trim(),
|
||||
lock_ratio: normalizeTemplateRegionLockRatio(region.lock_ratio),
|
||||
animation_json: normalizeTemplateRegionAnimationConfig(region.animation_json),
|
||||
x: Number(region.x || 0),
|
||||
y: Number(region.y || 0),
|
||||
width: Number(region.width || 100),
|
||||
@@ -101,6 +158,7 @@ function extractTemplateRegions(body) {
|
||||
const labels = readFormArray(body, 'region_label[]');
|
||||
const types = readFormArray(body, 'region_type[]');
|
||||
const ratios = readFormArray(body, 'region_lock_ratio[]');
|
||||
const animationJsons = readFormArray(body, 'region_animation_json[]');
|
||||
const xs = readFormArray(body, 'region_x[]');
|
||||
const ys = readFormArray(body, 'region_y[]');
|
||||
const widths = readFormArray(body, 'region_width[]');
|
||||
@@ -120,6 +178,7 @@ function extractTemplateRegions(body) {
|
||||
region_type: regionType,
|
||||
label: name,
|
||||
lock_ratio: normalizeTemplateRegionLockRatio(ratios[i]),
|
||||
animation_json: normalizeTemplateRegionAnimationConfig(animationJsons[i]),
|
||||
x: Number(xs[i] || 0),
|
||||
y: Number(ys[i] || 0),
|
||||
width: Number(widths[i] || 100),
|
||||
|
||||
@@ -52,6 +52,7 @@ async function ensureSchema(pool, options) {
|
||||
label VARCHAR(255) NOT NULL,
|
||||
font_family VARCHAR(100) NULL,
|
||||
lock_ratio VARCHAR(20) NULL,
|
||||
animation_json JSON NULL,
|
||||
x INT NOT NULL DEFAULT 0,
|
||||
y INT NOT NULL DEFAULT 0,
|
||||
width INT NOT NULL DEFAULT 100,
|
||||
|
||||
+14
-3
@@ -107,9 +107,7 @@ const VERSIONED_MIGRATIONS = [
|
||||
INDEX idx_announcements_modified_at (modified_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
version: '2.4.0',
|
||||
@@ -217,6 +215,13 @@ const VERSIONED_MIGRATIONS = [
|
||||
`);
|
||||
}
|
||||
},
|
||||
{
|
||||
version: '2.5.1',
|
||||
label: 'v2.5.1 template animation json schema',
|
||||
run: async function (pool) {
|
||||
await ensureColumn(pool, 'c_template_regions', 'animation_json', 'JSON NULL', 'lock_ratio');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
async function columnExists(pool, tableName, columnName) {
|
||||
@@ -260,7 +265,13 @@ async function ensureForeignKey(pool, tableName, constraintName, columnName, ref
|
||||
|
||||
async function dropColumnIfExists(pool, tableName, columnName) {
|
||||
if (await columnExists(pool, tableName, columnName)) {
|
||||
await pool.query('ALTER TABLE ' + tableName + ' DROP COLUMN ' + columnName);
|
||||
try {
|
||||
await pool.query('ALTER TABLE ' + tableName + ' DROP COLUMN ' + columnName);
|
||||
} catch (error) {
|
||||
if (!error || (error.code !== 'ER_CANT_DROP_FIELD_OR_KEY' && error.errno !== 1091)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// Announcement overlay polling and rendering helpers.
|
||||
|
||||
(function () {
|
||||
if (window.__pulseThumbnailPreview) {
|
||||
return;
|
||||
}
|
||||
|
||||
var announcementLayer = null;
|
||||
var announcementRefreshTimer = null;
|
||||
var announcementPollTimer = null;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<title>{{TITLE}}</title>
|
||||
<link rel="icon" type="image/png" href="/assets/favicon.png" />
|
||||
<link rel="stylesheet" href="/assets/adminlte/bootstrap-icons/css/bootstrap-icons.min.css" />
|
||||
<link rel="stylesheet" href="/assets/vendor/animate.css/animate.min.css" />
|
||||
<link rel="stylesheet" href="/assets/css/player.css?v=36" />
|
||||
{{{STYLESHEETS}}}
|
||||
</head>
|
||||
|
||||
@@ -121,7 +121,7 @@ function createPlayerPlaylistService(options) {
|
||||
LEFT JOIN c_canvas_sizes cs ON cs.id = st.canvas_size_id
|
||||
WHERE st.id IN (?)
|
||||
`, [templateIds]);
|
||||
[regionRows] = await pool.query('SELECT id, template_id, region_key, region_type, label, lock_ratio, x, y, width, height, z_index, created_at, modified_at, created_by, modified_by FROM c_template_regions WHERE template_id IN (?) ORDER BY template_id ASC, z_index ASC, id ASC', [templateIds]);
|
||||
[regionRows] = await pool.query('SELECT id, template_id, region_key, region_type, label, lock_ratio, animation_json, x, y, width, height, z_index, created_at, modified_at, created_by, modified_by FROM c_template_regions WHERE template_id IN (?) ORDER BY template_id ASC, z_index ASC, id ASC', [templateIds]);
|
||||
templateRows.forEach(function (template) {
|
||||
template.regions = regionRows.filter(function (region) { return region.template_id === template.id; });
|
||||
templatesById[template.id] = template;
|
||||
|
||||
@@ -22,6 +22,21 @@ body.onboarding-page #app {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.thumbnail-preview *,
|
||||
body.thumbnail-preview *::before,
|
||||
body.thumbnail-preview *::after {
|
||||
animation: none !important;
|
||||
animation-delay: 0s !important;
|
||||
animation-duration: 0s !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
body.thumbnail-preview .player-announcement-layer,
|
||||
body.thumbnail-preview .player-offline-banner {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -6,6 +6,8 @@ function getCurrentViewport() {
|
||||
};
|
||||
}
|
||||
|
||||
var slideOutroTimers = [];
|
||||
|
||||
// Command websocket and player-state helpers.
|
||||
// Send the current playback state to the command websocket.
|
||||
function sendCommandState(currentSlide) {
|
||||
@@ -65,6 +67,56 @@ function clearSlideTimer() {
|
||||
window.clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
clearSlideOutroTimer();
|
||||
}
|
||||
|
||||
// Cancel any pending outro triggers for the current slide.
|
||||
function clearSlideOutroTimer() {
|
||||
if (!Array.isArray(slideOutroTimers) || !slideOutroTimers.length) {
|
||||
slideOutroTimers = [];
|
||||
return;
|
||||
}
|
||||
|
||||
slideOutroTimers.forEach(function (timerId) {
|
||||
window.clearTimeout(timerId);
|
||||
});
|
||||
slideOutroTimers = [];
|
||||
}
|
||||
|
||||
// Return the rendered slide root that is currently on screen.
|
||||
function getCurrentSlideRoot() {
|
||||
var shells = Array.prototype.slice.call(app ? app.querySelectorAll('.slide-shell') : []);
|
||||
if (shells.length) {
|
||||
return shells[shells.length - 1];
|
||||
}
|
||||
return app && app.firstElementChild ? app.firstElementChild : app;
|
||||
}
|
||||
|
||||
// Schedule the outgoing slide animation for each region so it finishes before removal.
|
||||
function scheduleSlideOutro(holdDelayMs) {
|
||||
clearSlideOutroTimer();
|
||||
|
||||
var currentRoot = getCurrentSlideRoot();
|
||||
if (!currentRoot || typeof getRegionAnimationPhaseTimings !== 'function' || typeof playRegionAnimation !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
var regionTimings = getRegionAnimationPhaseTimings(currentRoot, 'outro');
|
||||
if (!regionTimings.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var slideDurationMs = Math.max(1, Math.round(Number(holdDelayMs || 0)));
|
||||
slideOutroTimers = regionTimings.map(function (entry) {
|
||||
var timingMs = Math.max(0, Math.round(Number(entry && entry.timingMs || 0)));
|
||||
var triggerDelayMs = Math.max(0, slideDurationMs - timingMs);
|
||||
return window.setTimeout(function () {
|
||||
if (!entry || !entry.element || !entry.element.isConnected) {
|
||||
return;
|
||||
}
|
||||
playRegionAnimation(entry.element, 'outro');
|
||||
}, triggerDelayMs);
|
||||
});
|
||||
}
|
||||
|
||||
// Schedule the next slide transition.
|
||||
@@ -72,10 +124,12 @@ function scheduleSlideAdvance(delayMs) {
|
||||
clearSlideTimer();
|
||||
var holdDelayMs = Math.max(1, Number(delayMs || 0));
|
||||
slideExpiresAt = Date.now() + holdDelayMs;
|
||||
scheduleSlideOutro(holdDelayMs);
|
||||
timer = window.setTimeout(function () {
|
||||
timer = null;
|
||||
slideExpiresAt = null;
|
||||
pausedRemainingMs = null;
|
||||
clearSlideOutroTimer();
|
||||
const activeSlides = getCurrentActiveSlides();
|
||||
if (activeSlides.length < 2) {
|
||||
refresh();
|
||||
@@ -89,13 +143,10 @@ function scheduleSlideAdvance(delayMs) {
|
||||
}, holdDelayMs);
|
||||
}
|
||||
|
||||
// Account for fade only when it is enabled so the transition is centered on the slide boundary.
|
||||
// Return the slide duration without shifting it for fade timing.
|
||||
function getSlideHoldDelay(delayMs) {
|
||||
var holdDelayMs = Math.max(1, Number(delayMs || 0));
|
||||
if (!currentPlaylistFadeBetweenSlides) {
|
||||
return holdDelayMs;
|
||||
}
|
||||
return Math.max(1, holdDelayMs - (slideFadeDurationMs / 2));
|
||||
return holdDelayMs;
|
||||
}
|
||||
|
||||
// Cancel any pending fade-transition cleanup.
|
||||
@@ -114,11 +165,19 @@ function getPlayerRegionModules() {
|
||||
return window.pulsePlayerRegionTypes.list();
|
||||
}
|
||||
|
||||
function isThumbnailPreview() {
|
||||
return Boolean(window.__pulseThumbnailPreview);
|
||||
}
|
||||
|
||||
function runRegionLifecycle(root, lifecycleName) {
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isThumbnailPreview() && lifecycleName === 'initRegion') {
|
||||
return;
|
||||
}
|
||||
|
||||
getPlayerRegionModules().forEach(function (entry) {
|
||||
var module = entry && entry.definition ? entry.definition : null;
|
||||
if (!module || typeof module[lifecycleName] !== 'function') {
|
||||
@@ -208,8 +267,15 @@ function renderSlideMarkup(markup, shouldFade) {
|
||||
if (typeof syncRtmpRegions === 'function') {
|
||||
syncRtmpRegions(app);
|
||||
}
|
||||
initializeRegionInstances(app);
|
||||
if (!isThumbnailPreview()) {
|
||||
initializeRegionInstances(app);
|
||||
}
|
||||
initializeRenderedVideoPlayback(app);
|
||||
if (typeof playRegionAnimations === 'function') {
|
||||
window.requestAnimationFrame(function () {
|
||||
playRegionAnimations(app, 'intro');
|
||||
});
|
||||
}
|
||||
return app.firstElementChild;
|
||||
}
|
||||
|
||||
@@ -277,14 +343,18 @@ function renderSlideMarkup(markup, shouldFade) {
|
||||
window.requestAnimationFrame(function () {
|
||||
nextShell.style.opacity = '1';
|
||||
previousShell.style.opacity = '0';
|
||||
if (typeof playRegionAnimations === 'function') {
|
||||
playRegionAnimations(nextShell, 'intro');
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof syncRtmpRegions === 'function') {
|
||||
syncRtmpRegions(nextShell);
|
||||
}
|
||||
|
||||
initializeRegionInstances(nextShell);
|
||||
|
||||
if (!isThumbnailPreview()) {
|
||||
initializeRegionInstances(nextShell);
|
||||
}
|
||||
initializeRenderedVideoPlayback(nextShell, slideFadeDurationMs / 2);
|
||||
|
||||
slideTransitionTimer = window.setTimeout(function () {
|
||||
|
||||
@@ -53,6 +53,276 @@ function fitCanvasSize(canvasWidth, canvasHeight, maxWidth, maxHeight) {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePlayerAnimationStep(value, fallbackPreset) {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
return {
|
||||
preset: String(value.preset || fallbackPreset || 'none').trim(),
|
||||
duration_ms: Number.isFinite(Number(value.duration_ms)) && Number(value.duration_ms) > 0 ? Math.round(Number(value.duration_ms)) : null,
|
||||
delay_ms: Number.isFinite(Number(value.delay_ms)) && Number(value.delay_ms) >= 0 ? Math.round(Number(value.delay_ms)) : null,
|
||||
iterations: Number.isFinite(Number(value.iterations)) && Number(value.iterations) > 0 ? Math.round(Number(value.iterations)) : null
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
preset: String(typeof value === 'string' ? value : fallbackPreset || 'none').trim(),
|
||||
duration_ms: null,
|
||||
delay_ms: null,
|
||||
iterations: null
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePlayerAnimationConfig(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: normalizePlayerAnimationStep(raw.intro, 'none'),
|
||||
outro: normalizePlayerAnimationStep(raw.outro !== undefined ? raw.outro : raw.out, 'none'),
|
||||
loop: normalizePlayerAnimationStep(raw.loop, 'none')
|
||||
};
|
||||
}
|
||||
|
||||
function hasPlayerAnimation(config) {
|
||||
return Boolean(config && ['intro', 'outro', 'loop'].some(function (stepName) {
|
||||
return String((config[stepName] && config[stepName].preset) || '').trim() && String((config[stepName] && config[stepName].preset) || '').trim() !== 'none';
|
||||
}));
|
||||
}
|
||||
|
||||
function decorateRegionMarkup(markup, region) {
|
||||
if (!markup || !region || !hasPlayerAnimation(region.animationConfig)) {
|
||||
return markup;
|
||||
}
|
||||
|
||||
var animationJson = escapeHtml(JSON.stringify(region.animationConfig));
|
||||
return markup.replace(/^(\s*)<([a-z0-9-]+)(\s[^>]*)?>/i, function (match, leadingWhitespace, tagName, attributes) {
|
||||
return leadingWhitespace + '<' + tagName + (attributes || '') + ' data-animation-json="' + animationJson + '">';
|
||||
});
|
||||
}
|
||||
|
||||
function clearRegionAnimationClasses(root) {
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
|
||||
var elements = [];
|
||||
if (typeof root.matches === 'function' && root.matches('[data-animation-json]')) {
|
||||
elements.push(root);
|
||||
}
|
||||
Array.prototype.forEach.call(root.querySelectorAll('[data-animation-json]'), function (element) {
|
||||
elements.push(element);
|
||||
});
|
||||
|
||||
elements.forEach(function (element) {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
element.classList.remove('animate__animated', 'animate__infinite');
|
||||
element.classList.remove('animate__repeat-1', 'animate__repeat-2', 'animate__repeat-3');
|
||||
Array.prototype.slice.call(element.classList || []).forEach(function (className) {
|
||||
if (String(className || '').indexOf('animate__') === 0) {
|
||||
element.classList.remove(className);
|
||||
}
|
||||
});
|
||||
element.style.removeProperty('--animate-duration');
|
||||
element.style.removeProperty('--animate-delay');
|
||||
element.style.removeProperty('--animate-repeat');
|
||||
});
|
||||
}
|
||||
|
||||
function isAttentionSeekerAnimation(preset) {
|
||||
return ['bounce', 'flash', 'pulse', 'rubberBand', 'shakeX', 'shakeY', 'headShake', 'swing', 'tada', 'wobble', 'jello', 'heartBeat'].indexOf(String(preset || '').trim()) !== -1;
|
||||
}
|
||||
|
||||
function applyAnimationStep(element, step, phase) {
|
||||
if (!element || !step) {
|
||||
return;
|
||||
}
|
||||
|
||||
var preset = String(step.preset || 'none').trim();
|
||||
if (!preset || preset === 'none') {
|
||||
return;
|
||||
}
|
||||
|
||||
element.classList.add('animate__animated', 'animate__' + preset);
|
||||
element.style.setProperty('--animate-duration', String(Math.max(1, Number(step.duration_ms || 0) || 1000)) + 'ms');
|
||||
if (Number(step.delay_ms || 0) > 0) {
|
||||
element.style.setProperty('--animate-delay', String(Math.max(0, Number(step.delay_ms || 0))) + 'ms');
|
||||
} else {
|
||||
element.style.removeProperty('--animate-delay');
|
||||
}
|
||||
|
||||
if (phase === 'loop') {
|
||||
var repeatCount = Number(step.iterations);
|
||||
if (!Number.isFinite(repeatCount) || repeatCount < 1) {
|
||||
repeatCount = 1;
|
||||
}
|
||||
if (repeatCount > 1) {
|
||||
element.classList.add('animate__repeat-1');
|
||||
}
|
||||
element.style.setProperty('--animate-repeat', String(repeatCount));
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAttentionSeekerAnimation(preset) && Number(step.iterations || 0) > 1) {
|
||||
element.style.setProperty('--animate-repeat', String(Math.max(1, Number(step.iterations || 1))));
|
||||
}
|
||||
}
|
||||
|
||||
function getAnimationStepTimingMs(step) {
|
||||
if (!step) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var preset = String(step.preset || 'none').trim();
|
||||
if (!preset || preset === 'none') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var durationMs = Number(step.duration_ms);
|
||||
if (!Number.isFinite(durationMs) || durationMs <= 0) {
|
||||
durationMs = 1000;
|
||||
}
|
||||
|
||||
var delayMs = Number(step.delay_ms);
|
||||
if (!Number.isFinite(delayMs) || delayMs < 0) {
|
||||
delayMs = 0;
|
||||
}
|
||||
|
||||
var iterations = Number(step.iterations);
|
||||
if (!Number.isFinite(iterations) || iterations < 1) {
|
||||
iterations = 1;
|
||||
}
|
||||
|
||||
return delayMs + (durationMs * iterations);
|
||||
}
|
||||
|
||||
function getRegionAnimationPhaseTimingMs(root, phase) {
|
||||
var timings = getRegionAnimationPhaseTimings(root, phase);
|
||||
return timings.reduce(function (maxTimingMs, entry) {
|
||||
return Math.max(maxTimingMs, Number(entry && entry.timingMs || 0));
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function getRegionAnimationPhaseTimings(root, phase) {
|
||||
if (!root || isThumbnailPreview()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
var normalizedPhase = String(phase || 'intro').trim().toLowerCase();
|
||||
if (normalizedPhase !== 'intro' && normalizedPhase !== 'outro') {
|
||||
normalizedPhase = 'intro';
|
||||
}
|
||||
|
||||
var timings = [];
|
||||
Array.prototype.forEach.call(root.querySelectorAll('[data-animation-json]'), function (element) {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
var config;
|
||||
try {
|
||||
config = normalizePlayerAnimationConfig(element.getAttribute('data-animation-json') || '{}');
|
||||
} catch (_error) {
|
||||
config = null;
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
||||
var timingMs = getAnimationStepTimingMs(normalizedPhase === 'outro' ? config.outro : config.intro);
|
||||
if (timingMs > 0) {
|
||||
timings.push({
|
||||
element: element,
|
||||
timingMs: timingMs
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return timings;
|
||||
}
|
||||
|
||||
function playRegionAnimation(element, phase) {
|
||||
if (!element || isThumbnailPreview()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var normalizedPhase = String(phase || 'intro').trim().toLowerCase();
|
||||
if (normalizedPhase !== 'intro' && normalizedPhase !== 'outro') {
|
||||
normalizedPhase = 'intro';
|
||||
}
|
||||
|
||||
var config;
|
||||
try {
|
||||
config = normalizePlayerAnimationConfig(element.getAttribute('data-animation-json') || '{}');
|
||||
} catch (_error) {
|
||||
config = null;
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
||||
element.dataset.animationPhase = normalizedPhase;
|
||||
clearRegionAnimationClasses(element);
|
||||
|
||||
if (normalizedPhase === 'outro') {
|
||||
applyAnimationStep(element, config.outro, 'outro');
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.intro && String(config.intro.preset || '').trim() && String(config.intro.preset || '').trim() !== 'none') {
|
||||
applyAnimationStep(element, config.intro, 'intro');
|
||||
if (config.loop && String(config.loop.preset || '').trim() && String(config.loop.preset || '').trim() !== 'none') {
|
||||
element.addEventListener('animationend', function handleAnimationEnd(event) {
|
||||
if (event.target !== element) {
|
||||
return;
|
||||
}
|
||||
if (String(element.dataset.animationPhase || '').trim() !== 'intro') {
|
||||
return;
|
||||
}
|
||||
element.removeEventListener('animationend', handleAnimationEnd);
|
||||
clearRegionAnimationClasses(element);
|
||||
applyAnimationStep(element, config.loop, 'loop');
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
applyAnimationStep(element, config.loop, 'loop');
|
||||
}
|
||||
|
||||
function playRegionAnimations(root, phase) {
|
||||
if (!root || isThumbnailPreview()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var normalizedPhase = String(phase || 'intro').trim().toLowerCase();
|
||||
if (normalizedPhase !== 'intro' && normalizedPhase !== 'outro') {
|
||||
normalizedPhase = 'intro';
|
||||
}
|
||||
|
||||
var elements = Array.prototype.slice.call(root.querySelectorAll('[data-animation-json]'));
|
||||
elements.forEach(function (element) {
|
||||
playRegionAnimation(element, normalizedPhase);
|
||||
});
|
||||
}
|
||||
|
||||
function setPlayerCanvasDimensions(canvasWidth, canvasHeight) {
|
||||
if (!document || !document.documentElement) {
|
||||
return;
|
||||
@@ -474,6 +744,7 @@ function getTemplateLayout(template) {
|
||||
regionKey: region.region_key,
|
||||
regionType: region.region_type,
|
||||
label: region.label,
|
||||
animationJson: region.animation_json || null,
|
||||
baseStyle: baseStyle,
|
||||
pixelWidth: pixelWidth,
|
||||
pixelHeight: pixelHeight,
|
||||
@@ -578,7 +849,10 @@ function renderTemplateSlideMarkup(slide) {
|
||||
const layout = plan ? plan.layout : null;
|
||||
const regions = layout ? layout.regions.map(function (region) {
|
||||
const regionContent = content[region.regionKey] || {};
|
||||
return plan.renderRegion(region, regionContent);
|
||||
const markup = plan.renderRegion(region, regionContent);
|
||||
return decorateRegionMarkup(markup, {
|
||||
animationConfig: normalizePlayerAnimationConfig(region.animationJson)
|
||||
});
|
||||
}).join('') : '';
|
||||
const stageStyle = layout ? buildBackdropStyle(layout.backgroundColor || '#111111', template.background_image_path) : '';
|
||||
if (layout) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -120,6 +120,7 @@ function renderPlayerPage(slug, initialData) {
|
||||
const hlsScriptTag = '<script src="/assets/vendor/hls.min.js"></script>';
|
||||
const pageAuthToken = createPageAuthBundle({ scope: 'player', slug: String(slug || '').trim() });
|
||||
const fontStylesheetHref = getFontStylesheetHref(PLAYER_MEDIA_DIR);
|
||||
const bodyClass = [initialData && initialData.thumbnailPreview ? 'thumbnail-preview' : '', ''].join(' ').trim();
|
||||
const script = getPlayerPageScript()({
|
||||
SLUG_JSON: new Handlebars.SafeString(JSON.stringify(slug)),
|
||||
INITIAL_DATA_JSON: new Handlebars.SafeString(safeJsonForScript(initialData || null)),
|
||||
@@ -128,6 +129,7 @@ function renderPlayerPage(slug, initialData) {
|
||||
|
||||
return renderPage(template, {
|
||||
title: 'Screen ' + slug,
|
||||
bodyClass: bodyClass,
|
||||
body: '<div id="app"><div class="empty">Loading screen...</div></div>',
|
||||
stylesheets: fontStylesheetHref ? [fontStylesheetHref] : [],
|
||||
script: createPageFetchAuthScript(pageAuthToken) + hlsScriptTag + serviceWorkerScript + createThumbnailPreviewBootstrapScript(initialData) + '<script>' + offlineScript + '</script>' + '<script>' + playlistScript + '</script>' + '<script>' + commandScript + '</script>' + '<script>' + renderingScript + '</script>' + '<script>' + playbackScript + '</script>' + '<script>' + getAnnouncementIconsDataScript() + '</script>' + '<script>' + getPlayerAnnouncementTemplatesDataScript() + '</script>' + '<script>' + getPlayerAnnouncementTemplatesScript() + '</script>' + onboardingScript + script + '<script>' + getPlayerPageAnnouncementsScript()() + '</script>'
|
||||
|
||||
@@ -160,6 +160,7 @@ function registerPartials(Handlebars, viewsRoot) {
|
||||
Handlebars.registerPartial('playlists/form', fs.readFileSync(path.join(viewsRoot, 'signage', 'playlists', 'form.hbs'), 'utf8'));
|
||||
Handlebars.registerPartial('signage/playlists/slide-row', fs.readFileSync(path.join(viewsRoot, 'signage', 'playlists', 'slide-row.hbs'), 'utf8'));
|
||||
Handlebars.registerPartial('signage/playlists/schedule-rule-card', fs.readFileSync(path.join(viewsRoot, 'signage', 'playlists', 'schedule-rule-card.hbs'), 'utf8'));
|
||||
Handlebars.registerPartial('signage/templates/animation-advanced-modal', fs.readFileSync(path.join(viewsRoot, 'signage', 'templates', 'animation-advanced-modal.hbs'), 'utf8'));
|
||||
Handlebars.registerPartial('data-sources/api-sources/form', fs.readFileSync(path.join(viewsRoot, 'data-sources', 'api-sources', 'form.hbs'), 'utf8'));
|
||||
Handlebars.registerPartial('data-sources/rss-feeds/form', fs.readFileSync(path.join(viewsRoot, 'data-sources', 'rss-feeds', 'form.hbs'), 'utf8'));
|
||||
}
|
||||
|
||||
@@ -190,6 +190,82 @@
|
||||
max-width: 14rem;
|
||||
}
|
||||
|
||||
.template-preview-card .card-body {
|
||||
gap: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.template-preview-card.is-previewing {
|
||||
border-color: rgba(var(--bs-primary-rgb), 0.45);
|
||||
}
|
||||
|
||||
.template-preview-card.is-previewing .card-header {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.template-preview-card.is-previewing .card-title {
|
||||
position: relative;
|
||||
padding-right: 6.25rem;
|
||||
}
|
||||
|
||||
.template-preview-card.is-previewing .card-title::after {
|
||||
content: ' previewing';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(100% + 0.35rem);
|
||||
color: var(--bs-primary);
|
||||
font-size: 0.8em;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
white-space: nowrap;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.template-preview-card .btn-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.template-preview-card .btn-group .btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.template-preview-card[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.template-designer-form--previewing .designer-stage {
|
||||
box-shadow: none !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.template-designer-form--previewing .designer-overlay {
|
||||
cursor: progress;
|
||||
}
|
||||
|
||||
.template-designer-form--previewing .designer-rect {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.template-designer-form--previewing .resize-handle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.template-designer-form--previewing .region-item,
|
||||
.template-designer-form--previewing .template-details-card,
|
||||
.template-designer-form--previewing .template-options-card,
|
||||
.template-designer-form--previewing .region-info-card {
|
||||
opacity: 0.84;
|
||||
}
|
||||
|
||||
.template-designer-form--previewing .region-item [disabled],
|
||||
.template-designer-form--previewing .template-details-card [disabled],
|
||||
.template-designer-form--previewing .template-options-card [disabled],
|
||||
.template-designer-form--previewing .region-info-card [disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.announcement-icon-picker {
|
||||
position: relative;
|
||||
}
|
||||
@@ -1401,7 +1477,7 @@ html[data-bs-theme='dark'] .template-field-card .tox .tox-collection {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 18rem;
|
||||
/* overflow: hidden; */
|
||||
overflow: visible;
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-radius: 0;
|
||||
background: var(--bs-body-bg);
|
||||
@@ -1431,11 +1507,16 @@ html[data-bs-theme='dark'] .template-field-card .tox .tox-collection {
|
||||
.designer-overlay {
|
||||
z-index: 2;
|
||||
cursor: default;
|
||||
--designer-overlay-scale: 1;
|
||||
}
|
||||
|
||||
.designer-rect {
|
||||
position: absolute;
|
||||
border: 2px solid rgba(13, 110, 253, 0.95);
|
||||
overflow: visible;
|
||||
z-index: 3;
|
||||
border: 0;
|
||||
outline: 2px solid rgba(13, 110, 253, 0.95);
|
||||
outline-offset: -2px;
|
||||
background: rgba(13, 110, 253, 0.12);
|
||||
box-sizing: border-box;
|
||||
border-radius: 0;
|
||||
@@ -1445,20 +1526,20 @@ html[data-bs-theme='dark'] .template-field-card .tox .tox-collection {
|
||||
}
|
||||
|
||||
.designer-rect.selected {
|
||||
border-color: rgba(25, 135, 84, 1);
|
||||
outline-color: rgba(25, 135, 84, 1);
|
||||
background: rgba(25, 135, 84, 0.14);
|
||||
}
|
||||
|
||||
.designer-rect-label {
|
||||
position: absolute;
|
||||
left: 0.4rem;
|
||||
top: 0.35rem;
|
||||
left: calc(6px * var(--designer-overlay-scale, 1));
|
||||
top: calc(6px * var(--designer-overlay-scale, 1));
|
||||
max-width: 100%;
|
||||
padding: 0.15rem 0.45rem;
|
||||
padding: calc(2px * var(--designer-overlay-scale, 1)) calc(7px * var(--designer-overlay-scale, 1));
|
||||
border-radius: 999px;
|
||||
background: rgba(33, 37, 41, 0.92);
|
||||
color: #fff;
|
||||
font-size: 0.75rem;
|
||||
font-size: calc(0.75rem * var(--designer-overlay-scale, 1));
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -1471,8 +1552,10 @@ html[data-bs-theme='dark'] .template-field-card .tox .tox-collection {
|
||||
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
width: 0.8rem;
|
||||
height: 0.8rem;
|
||||
z-index: 4;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 999px;
|
||||
border: 2px solid #fff;
|
||||
background: rgba(13, 110, 253, 1);
|
||||
@@ -1482,28 +1565,28 @@ html[data-bs-theme='dark'] .template-field-card .tox .tox-collection {
|
||||
.resize-handle.nw {
|
||||
left: 0;
|
||||
top: 0;
|
||||
transform: translate(-50%, -50%);
|
||||
transform: translate(-40%, -40%);
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.resize-handle.ne {
|
||||
right: 0;
|
||||
top: 0;
|
||||
transform: translate(50%, -50%);
|
||||
transform: translate(40%, -40%);
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.resize-handle.sw {
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
transform: translate(-50%, 50%);
|
||||
transform: translate(-40%, 40%);
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.resize-handle.se {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
transform: translate(50%, 50%);
|
||||
transform: translate(40%, 40%);
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
@@ -1641,6 +1724,10 @@ html[data-bs-theme='dark'] .template-field-card .tox .tox-collection {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.region-field-grid--animation {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.region-field-grid > label {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
@@ -1877,7 +1964,8 @@ html[data-bs-theme='dark'] .template-field-card .tox .tox-collection {
|
||||
|
||||
.region-field-grid--identity,
|
||||
.region-field-grid--position,
|
||||
.region-field-grid--size {
|
||||
.region-field-grid--size,
|
||||
.region-field-grid--animation {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
(function (root, factory) {
|
||||
var presets = factory();
|
||||
|
||||
if (typeof module === 'object' && module.exports) {
|
||||
module.exports = presets;
|
||||
}
|
||||
|
||||
if (root) {
|
||||
root.templateAnimationPresets = presets;
|
||||
}
|
||||
})(typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : this), function () {
|
||||
function option(value, label) {
|
||||
return { value: value, label: label };
|
||||
}
|
||||
|
||||
function formatLabel(value) {
|
||||
return String(value || '')
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
||||
.replace(/[_-]+/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.replace(/\b\w/g, function (letter) {
|
||||
return letter.toUpperCase();
|
||||
});
|
||||
}
|
||||
|
||||
function buildAllValues(groups, extraValues) {
|
||||
var values = [];
|
||||
var seen = Object.create(null);
|
||||
|
||||
function addEntry(entry) {
|
||||
if (!entry || typeof entry !== 'object' || !entry.value || seen[entry.value]) {
|
||||
return;
|
||||
}
|
||||
|
||||
seen[entry.value] = true;
|
||||
values.push(entry.value);
|
||||
}
|
||||
|
||||
Object.keys(groups).forEach(function (groupName) {
|
||||
(groups[groupName] || []).forEach(function (entry) {
|
||||
if (Array.isArray(entry)) {
|
||||
entry.forEach(addEntry);
|
||||
return;
|
||||
}
|
||||
|
||||
addEntry(entry);
|
||||
});
|
||||
});
|
||||
|
||||
(extraValues || []).forEach(function (value) {
|
||||
if (!value || seen[value]) {
|
||||
return;
|
||||
}
|
||||
|
||||
seen[value] = true;
|
||||
values.push(value);
|
||||
});
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
function buildLookup(values) {
|
||||
var lookup = Object.create(null);
|
||||
|
||||
values.forEach(function (value) {
|
||||
var key = String(value || '').trim().toLowerCase();
|
||||
if (key && !lookup[key]) {
|
||||
lookup[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return lookup;
|
||||
}
|
||||
|
||||
function normalizeValue(value, lookup) {
|
||||
var raw = String(value || '').trim();
|
||||
if (!raw) {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
return lookup[raw.toLowerCase()] || 'none';
|
||||
}
|
||||
|
||||
function splitAnimateCssValues(values) {
|
||||
var groups = {
|
||||
intro: [],
|
||||
outro: [],
|
||||
attentionSeekers: []
|
||||
};
|
||||
|
||||
(values || []).forEach(function (value) {
|
||||
if (!value || value === 'none') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.indexOf('In') !== -1) {
|
||||
groups.intro.push(value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.indexOf('Out') !== -1) {
|
||||
groups.outro.push(value);
|
||||
return;
|
||||
}
|
||||
|
||||
groups.attentionSeekers.push(value);
|
||||
});
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
function buildCuratedBasic(values) {
|
||||
return [option('none', 'None')].concat(values.map(function (value) {
|
||||
return option(value, formatLabel(value));
|
||||
}));
|
||||
}
|
||||
|
||||
var animateCssValues = [
|
||||
'none',
|
||||
'bounce', 'flash', 'pulse', 'rubberBand', 'shakeX', 'shakeY', 'headShake', 'swing', 'tada', 'wobble', 'jello', 'heartBeat',
|
||||
'backInDown', 'backInLeft', 'backInRight', 'backInUp',
|
||||
'backOutDown', 'backOutLeft', 'backOutRight', 'backOutUp',
|
||||
'bounceIn', 'bounceInDown', 'bounceInLeft', 'bounceInRight', 'bounceInUp',
|
||||
'bounceOut', 'bounceOutDown', 'bounceOutLeft', 'bounceOutRight', 'bounceOutUp',
|
||||
'fadeIn', 'fadeInDown', 'fadeInDownBig', 'fadeInLeft', 'fadeInLeftBig', 'fadeInRight', 'fadeInRightBig', 'fadeInUp', 'fadeInUpBig', 'fadeInTopLeft', 'fadeInTopRight', 'fadeInBottomLeft', 'fadeInBottomRight',
|
||||
'fadeOut', 'fadeOutDown', 'fadeOutDownBig', 'fadeOutLeft', 'fadeOutLeftBig', 'fadeOutRight', 'fadeOutRightBig', 'fadeOutUp', 'fadeOutUpBig', 'fadeOutTopLeft', 'fadeOutTopRight', 'fadeOutBottomLeft', 'fadeOutBottomRight',
|
||||
'flip', 'flipInX', 'flipInY', 'flipOutX', 'flipOutY',
|
||||
'lightSpeedInLeft', 'lightSpeedInRight', 'lightSpeedOutLeft', 'lightSpeedOutRight',
|
||||
'rotateIn', 'rotateInDownLeft', 'rotateInDownRight', 'rotateInUpLeft', 'rotateInUpRight',
|
||||
'rotateOut', 'rotateOutDownLeft', 'rotateOutDownRight', 'rotateOutUpLeft', 'rotateOutUpRight',
|
||||
'hinge', 'jackInTheBox', 'rollIn', 'rollOut',
|
||||
'zoomIn', 'zoomInDown', 'zoomInLeft', 'zoomInRight', 'zoomInUp',
|
||||
'zoomOut', 'zoomOutDown', 'zoomOutLeft', 'zoomOutRight', 'zoomOutUp',
|
||||
'slideInDown', 'slideInLeft', 'slideInRight', 'slideInUp',
|
||||
'slideOutDown', 'slideOutLeft', 'slideOutRight', 'slideOutUp'
|
||||
];
|
||||
|
||||
var splitValues = splitAnimateCssValues(animateCssValues);
|
||||
|
||||
var basic = {
|
||||
intro: [
|
||||
option('none', 'None'),
|
||||
option('fadeIn', formatLabel('fadeIn')),
|
||||
option('fadeInDown', formatLabel('fadeInDown')),
|
||||
option('fadeInLeft', formatLabel('fadeInLeft')),
|
||||
option('fadeInRight', formatLabel('fadeInRight')),
|
||||
option('fadeInUp', formatLabel('fadeInUp')),
|
||||
option('zoomIn', formatLabel('zoomIn')),
|
||||
option('zoomInDown', formatLabel('zoomInDown')),
|
||||
option('zoomInLeft', formatLabel('zoomInLeft')),
|
||||
option('zoomInRight', formatLabel('zoomInRight')),
|
||||
option('zoomInUp', formatLabel('zoomInUp')),
|
||||
option('slideInDown', formatLabel('slideInDown')),
|
||||
option('slideInLeft', formatLabel('slideInLeft')),
|
||||
option('slideInRight', formatLabel('slideInRight')),
|
||||
option('slideInUp', formatLabel('slideInUp'))
|
||||
],
|
||||
outro: [
|
||||
option('none', 'None'),
|
||||
option('fadeOut', formatLabel('fadeOut')),
|
||||
option('fadeOutDown', formatLabel('fadeOutDown')),
|
||||
option('fadeOutLeft', formatLabel('fadeOutLeft')),
|
||||
option('fadeOutRight', formatLabel('fadeOutRight')),
|
||||
option('fadeOutUp', formatLabel('fadeOutUp')),
|
||||
option('zoomOut', formatLabel('zoomOut')),
|
||||
option('zoomOutDown', formatLabel('zoomOutDown')),
|
||||
option('zoomOutLeft', formatLabel('zoomOutLeft')),
|
||||
option('zoomOutRight', formatLabel('zoomOutRight')),
|
||||
option('zoomOutUp', formatLabel('zoomOutUp')),
|
||||
option('slideOutDown', formatLabel('slideOutDown')),
|
||||
option('slideOutLeft', formatLabel('slideOutLeft')),
|
||||
option('slideOutRight', formatLabel('slideOutRight')),
|
||||
option('slideOutUp', formatLabel('slideOutUp'))
|
||||
],
|
||||
attentionSeekers: buildCuratedBasic(['flash', 'pulse', 'rubberBand', 'shakeX', 'shakeY', 'swing', 'tada', 'wobble', 'jello', 'heartBeat', 'bounce'])
|
||||
};
|
||||
|
||||
var advanced = {
|
||||
intro: buildCuratedBasic(splitValues.intro),
|
||||
outro: buildCuratedBasic(splitValues.outro),
|
||||
attentionSeekers: buildCuratedBasic(splitValues.attentionSeekers)
|
||||
};
|
||||
|
||||
var lookup = buildLookup(buildAllValues({ basic: [basic.intro, basic.outro, basic.attentionSeekers], advanced: [advanced.intro, advanced.outro, advanced.attentionSeekers] }));
|
||||
|
||||
function normalize(value) {
|
||||
return normalizeValue(value, lookup);
|
||||
}
|
||||
|
||||
function isAnimateCssPreset(value) {
|
||||
return animateCssValues.indexOf(normalizeValue(value, buildLookup(animateCssValues))) !== -1;
|
||||
}
|
||||
|
||||
return {
|
||||
basic: basic,
|
||||
advanced: advanced,
|
||||
animateCssValues: animateCssValues,
|
||||
allValues: buildAllValues({ basic: [basic.intro, basic.outro, basic.attentionSeekers], advanced: [advanced.intro, advanced.outro, advanced.attentionSeekers] }),
|
||||
normalize: normalize,
|
||||
isAnimateCssPreset: isAnimateCssPreset
|
||||
};
|
||||
});
|
||||
@@ -82,6 +82,174 @@
|
||||
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;
|
||||
@@ -96,6 +264,7 @@
|
||||
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),
|
||||
@@ -114,6 +283,7 @@
|
||||
}
|
||||
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); }
|
||||
@@ -171,6 +341,10 @@
|
||||
syncRegionIdentity: syncRegionIdentity,
|
||||
readCard: readCard,
|
||||
writeCard: writeCard,
|
||||
normalizeAnimationConfig: normalizeAnimationConfig,
|
||||
formatAnimationConfig: formatAnimationConfig,
|
||||
getAnimationConfig: getAnimationConfig,
|
||||
writeAnimationConfig: writeAnimationConfig,
|
||||
clampRegion: clampRegion,
|
||||
getOverlayRect: getOverlayRect,
|
||||
toCanvasPoint: toCanvasPoint,
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
: (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');
|
||||
@@ -36,6 +40,7 @@
|
||||
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');
|
||||
@@ -43,7 +48,20 @@
|
||||
var regionUsageSet = new Set();
|
||||
var draft = null;
|
||||
var selectedIndex = -1;
|
||||
var activeAnimationCard = null;
|
||||
var overlayRenderFrame = 0;
|
||||
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) || '[]');
|
||||
@@ -218,6 +236,7 @@
|
||||
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),
|
||||
@@ -226,6 +245,582 @@
|
||||
};
|
||||
}
|
||||
|
||||
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 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('active', previewState.active && activeMode === entry.mode);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
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.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 = step.duration_ms === null || step.duration_ms === undefined || Number(step.duration_ms) <= 0 ? '1000' : String(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(step.iterations); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 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 = readNumberField(durationInput ? durationInput.value : '') || 1000;
|
||||
nextStep.delay_ms = readNumberField(delayInput ? delayInput.value : '') || 0;
|
||||
if (stepName === 'loop') {
|
||||
var iterationsInput = getAnimationModalField(stepName, 'iterations');
|
||||
nextStep.iterations = 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);
|
||||
@@ -240,6 +835,7 @@
|
||||
}
|
||||
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); }
|
||||
@@ -401,6 +997,7 @@
|
||||
if (regionLockRatioInput) {
|
||||
regionLockRatioInput.value = normalizeLockRatio(region.lock_ratio);
|
||||
}
|
||||
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);
|
||||
@@ -452,6 +1049,9 @@
|
||||
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 () {
|
||||
syncRegionIdentity(card, nameInput.value);
|
||||
updateRegionLabel(card);
|
||||
@@ -459,6 +1059,28 @@
|
||||
renderRegionSidebar();
|
||||
renderOverlay();
|
||||
});
|
||||
Array.prototype.forEach.call(animationPresetInputs, function (input) {
|
||||
input.addEventListener('change', function () {
|
||||
setAnimationPreset(card, input.getAttribute('data-animation-preset'), input.value);
|
||||
requestOverlayRender();
|
||||
});
|
||||
});
|
||||
Array.prototype.forEach.call(animationResetButtons, function (button) {
|
||||
button.addEventListener('click', function (event) {
|
||||
event.preventDefault();
|
||||
setAnimationPreset(card, button.getAttribute('data-animation-reset-button'), 'none');
|
||||
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 () {
|
||||
updateRegionLockBadge(card);
|
||||
@@ -536,28 +1158,49 @@
|
||||
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 '<div class="designer-rect' + selected + '" data-index="' + index + '" style="left:' + box.left + 'px;top:' + box.top + 'px;width:' + box.width + 'px;height:' + box.height + 'px;"><div class="designer-rect-label">' + escapeHtml(region.label || region.region_key || 'Region') + '</div><span class="resize-handle nw" data-dir="nw"></span><span class="resize-handle ne" data-dir="ne"></span><span class="resize-handle sw" data-dir="sw"></span><span class="resize-handle se" data-dir="se"></span></div>';
|
||||
return '<div class="designer-rect' + selected + '" data-index="' + index + '" style="--designer-overlay-scale:1;left:' + renderedBox.left + 'px;top:' + renderedBox.top + 'px;width:' + renderedBox.width + 'px;height:' + renderedBox.height + 'px;"><div class="designer-rect-label">' + escapeHtml(region.label || region.region_key || 'Region') + '</div><span class="resize-handle nw" data-dir="nw"></span><span class="resize-handle ne" data-dir="ne"></span><span class="resize-handle sw" data-dir="sw"></span><span class="resize-handle se" data-dir="se"></span></div>';
|
||||
}).join('');
|
||||
if (draft) {
|
||||
var rect = getOverlayRect();
|
||||
var size = getCanvasSize();
|
||||
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 += '<div class="designer-rect designer-draft" style="left:' + ((draftBox.x / size.width) * rect.width) + 'px;top:' + ((draftBox.y / size.height) * rect.height) + 'px;width:' + ((draftBox.width / size.width) * rect.width) + 'px;height:' + ((draftBox.height / size.height) * rect.height) + 'px;"></div>';
|
||||
overlay.innerHTML += '<div class="designer-rect designer-draft" style="left:' + Math.round((draftBox.x / size.width) * rect.width) + 'px;top:' + Math.round((draftBox.y / size.height) * rect.height) + 'px;width:' + Math.round((draftBox.width / size.width) * rect.width) + 'px;height:' + Math.round((draftBox.height / size.height) * rect.height) + 'px;"></div>';
|
||||
}
|
||||
|
||||
updatePreviewControlState();
|
||||
}
|
||||
|
||||
function requestOverlayRender() {
|
||||
if (previewState.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (overlayRenderFrame) {
|
||||
return;
|
||||
}
|
||||
@@ -575,6 +1218,10 @@
|
||||
}
|
||||
|
||||
function setSelected(index) {
|
||||
if (previewState.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
var cards = getCards();
|
||||
if (!cards.length) {
|
||||
selectedIndex = -1;
|
||||
@@ -588,6 +1235,10 @@
|
||||
}
|
||||
|
||||
function addRegion(region) {
|
||||
if (previewState.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
var hint = regionList.querySelector('.muted');
|
||||
if (hint) {
|
||||
hint.remove();
|
||||
@@ -613,6 +1264,7 @@
|
||||
region_key: name,
|
||||
label: name,
|
||||
region_type: type,
|
||||
animation_json: normalizeAnimationConfig('{}'),
|
||||
x: 80,
|
||||
y: 80,
|
||||
width: size.width,
|
||||
@@ -639,6 +1291,10 @@
|
||||
}
|
||||
|
||||
function startDraw(event) {
|
||||
if (previewState.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
var start = toCanvasPoint(event);
|
||||
draft = { start: start, end: start };
|
||||
renderOverlay();
|
||||
@@ -664,6 +1320,10 @@
|
||||
}
|
||||
|
||||
function startMove(index, event) {
|
||||
if (previewState.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
var startPoint = toCanvasPoint(event);
|
||||
var startRegion = readCard(cardAt(index));
|
||||
function moveHandler(moveEvent) {
|
||||
@@ -687,6 +1347,10 @@
|
||||
}
|
||||
|
||||
function resizeFromHandle(index, dir, event) {
|
||||
if (previewState.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
var startPoint = toCanvasPoint(event);
|
||||
var startRegion = readCard(cardAt(index));
|
||||
var lockRatio = normalizeLockRatio(startRegion.lock_ratio);
|
||||
@@ -824,6 +1488,31 @@
|
||||
backgroundEmpty.style.display = 'block';
|
||||
});
|
||||
}
|
||||
if (animationAdvancedModal) {
|
||||
animationAdvancedModal.addEventListener('hidden.bs.modal', function () {
|
||||
activeAnimationCard = null;
|
||||
});
|
||||
var animationModalApplyButton = animationAdvancedModal.querySelector('[data-animation-modal-apply]');
|
||||
var animationModalResetButton = animationAdvancedModal.querySelector('[data-animation-modal-reset]');
|
||||
if (animationModalApplyButton) {
|
||||
animationModalApplyButton.addEventListener('click', function () {
|
||||
saveAnimationModal();
|
||||
if (window.pulseModal) {
|
||||
window.pulseModal.hide(animationAdvancedModal);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (animationModalResetButton) {
|
||||
animationModalResetButton.addEventListener('click', function () {
|
||||
if (!activeAnimationCard) {
|
||||
return;
|
||||
}
|
||||
writeAnimationConfig(activeAnimationCard, normalizeAnimationConfig('{}'));
|
||||
populateAnimationModal(activeAnimationCard);
|
||||
requestOverlayRender();
|
||||
});
|
||||
}
|
||||
}
|
||||
if (backgroundColorInput) {
|
||||
backgroundColorInput.addEventListener('input', updateStageBackgroundColor);
|
||||
}
|
||||
@@ -853,6 +1542,11 @@
|
||||
});
|
||||
if (templateForm) {
|
||||
templateForm.addEventListener('formdata', function (event) {
|
||||
if (previewState.active) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateRegionNames()) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
@@ -868,6 +1562,10 @@
|
||||
});
|
||||
|
||||
templateForm.addEventListener('submit', function () {
|
||||
if (previewState.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateRegionNames()) {
|
||||
return;
|
||||
}
|
||||
@@ -882,6 +1580,36 @@
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -501,8 +501,8 @@ module.exports = function registerContentRoutes(app, deps) {
|
||||
for (let i = 0; i < payload.regions.length; i += 1) {
|
||||
const region = payload.regions[i];
|
||||
await pool.query(
|
||||
'INSERT INTO c_template_regions (template_id, region_key, region_type, label, lock_ratio, x, y, width, height, z_index, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[result.insertId, region.region_key, region.region_type, region.label, region.lock_ratio, region.x, region.y, region.width, region.height, region.z_index, actorId, actorId]
|
||||
'INSERT INTO c_template_regions (template_id, region_key, region_type, label, lock_ratio, animation_json, x, y, width, height, z_index, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[result.insertId, region.region_key, region.region_type, region.label, region.lock_ratio, JSON.stringify(region.animation_json || {}), region.x, region.y, region.width, region.height, region.z_index, actorId, actorId]
|
||||
);
|
||||
}
|
||||
await syncPlaylistUploadsOnChange({
|
||||
@@ -563,8 +563,8 @@ module.exports = function registerContentRoutes(app, deps) {
|
||||
for (let i = 0; i < payload.regions.length; i += 1) {
|
||||
const region = payload.regions[i];
|
||||
await pool.query(
|
||||
'INSERT INTO c_template_regions (template_id, region_key, region_type, label, lock_ratio, x, y, width, height, z_index, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[template.id, region.region_key, region.region_type, region.label, region.lock_ratio, region.x, region.y, region.width, region.height, region.z_index, actorId, actorId]
|
||||
'INSERT INTO c_template_regions (template_id, region_key, region_type, label, lock_ratio, animation_json, x, y, width, height, z_index, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[template.id, region.region_key, region.region_type, region.label, region.lock_ratio, JSON.stringify(region.animation_json || {}), region.x, region.y, region.width, region.height, region.z_index, actorId, actorId]
|
||||
);
|
||||
}
|
||||
await syncPlaylistUploadsOnChange({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Shared template form view-model builder.
|
||||
|
||||
const { getRegionEditorScripts } = require('../../../lib/region-scripts');
|
||||
const animationPresets = require('../../../public/js/templates/animation-presets');
|
||||
|
||||
function buildDefaultTemplate() {
|
||||
return {
|
||||
@@ -61,7 +62,8 @@ function buildTemplateFormViewModel(template, message, canvasSizes, currentUser,
|
||||
cancelUrl: '/templates',
|
||||
deleteUrl: isEdit && current.id ? '/templates/' + current.id + '/delete' : '',
|
||||
canvasSizes: canvasSizes || [],
|
||||
scripts: ['js/lib/modal.js?v=' + assetVersion].concat(getRegionEditorScripts(assetVersion), ['js/templates/template-designer-utils.js?v=' + assetVersion, 'js/templates/template-designer.js?v=' + assetVersion])
|
||||
animationPresets: animationPresets,
|
||||
scripts: ['js/lib/modal.js?v=' + assetVersion, 'js/templates/animation-presets.js?v=' + assetVersion].concat(getRegionEditorScripts(assetVersion), ['js/templates/template-designer-utils.js?v=' + assetVersion, 'js/templates/template-designer.js?v=' + assetVersion])
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<link rel="stylesheet" href="/assets/adminlte/bootstrap-icons/css/bootstrap-icons.min.css" />
|
||||
<link rel="stylesheet" href="/assets/adminlte/css/adminlte.min.css" />
|
||||
<link rel="stylesheet" href="/assets/css/theme-custom.css?v={{appVersion}}" />
|
||||
<link rel="stylesheet" href="/assets/vendor/animate.css/animate.min.css?v={{appVersion}}" />
|
||||
{{#if stylesheets.length}}
|
||||
{{#each stylesheets}}
|
||||
<link rel="stylesheet" href="{{assetHref this}}" />
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
{{#> modal-shell modalId="animation-advanced-modal" modalLabelId="animation-advanced-modal-label" modalDialogClass="modal-dialog-centered modal-xl modal-dialog-scrollable" modalBackdropStatic=true modalKeyboardDisabled=true}}
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h2 class="modal-title fs-5" id="animation-advanced-modal-label" data-animation-modal-title>Advanced animation settings</h2>
|
||||
<p class="text-body-secondary mb-0">Fine-tune the selected region animation JSON without editing raw data.</p>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body d-grid gap-4">
|
||||
<div class="alert alert-secondary mb-0">Changes here are saved into the region animation JSON and stay in sync with the preset dropdowns.</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card border-secondary-subtle">
|
||||
<div class="card-header bg-transparent">
|
||||
<strong class="text-capitalize">Intro</strong>
|
||||
</div>
|
||||
<div class="card-body d-grid gap-3 align-content-start">
|
||||
<label class="d-grid gap-1">
|
||||
Preset
|
||||
<select class="form-select" data-animation-modal-step="intro" data-animation-modal-field="preset">
|
||||
{{#each animationPresets.advanced.intro}}
|
||||
<option value="{{value}}">{{label}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</label>
|
||||
<label class="d-grid gap-1">
|
||||
Duration ms
|
||||
<input class="form-control" type="number" min="0" step="1" data-animation-modal-step="intro" data-animation-modal-field="duration_ms" placeholder="1000" />
|
||||
</label>
|
||||
<label class="d-grid gap-1">
|
||||
Delay ms
|
||||
<input class="form-control" type="number" min="0" step="1" data-animation-modal-step="intro" data-animation-modal-field="delay_ms" placeholder="0" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card border-secondary-subtle">
|
||||
<div class="card-header bg-transparent">
|
||||
<strong class="text-capitalize">Outro</strong>
|
||||
</div>
|
||||
<div class="card-body d-grid gap-3 align-content-start">
|
||||
<label class="d-grid gap-1">
|
||||
Preset
|
||||
<select class="form-select" data-animation-modal-step="outro" data-animation-modal-field="preset">
|
||||
{{#each animationPresets.advanced.outro}}
|
||||
<option value="{{value}}">{{label}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</label>
|
||||
<label class="d-grid gap-1">
|
||||
Duration ms
|
||||
<input class="form-control" type="number" min="0" step="1" data-animation-modal-step="outro" data-animation-modal-field="duration_ms" placeholder="1000" />
|
||||
</label>
|
||||
<label class="d-grid gap-1">
|
||||
Delay ms
|
||||
<input class="form-control" type="number" min="0" step="1" data-animation-modal-step="outro" data-animation-modal-field="delay_ms" placeholder="0" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card border-secondary-subtle">
|
||||
<div class="card-header bg-transparent">
|
||||
<strong class="text-capitalize">Attention seekers</strong>
|
||||
</div>
|
||||
<div class="card-body d-grid gap-3 align-content-start">
|
||||
<label class="d-grid gap-1">
|
||||
Preset
|
||||
<select class="form-select" data-animation-modal-step="loop" data-animation-modal-field="preset">
|
||||
{{#each animationPresets.advanced.attentionSeekers}}
|
||||
<option value="{{value}}">{{label}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</label>
|
||||
<label class="d-grid gap-1">
|
||||
Duration ms
|
||||
<input class="form-control" type="number" min="0" step="1" data-animation-modal-step="loop" data-animation-modal-field="duration_ms" placeholder="1000" />
|
||||
</label>
|
||||
<label class="d-grid gap-1">
|
||||
Delay ms
|
||||
<input class="form-control" type="number" min="0" step="1" data-animation-modal-step="loop" data-animation-modal-field="delay_ms" placeholder="0" />
|
||||
</label>
|
||||
<label class="d-grid gap-1">
|
||||
Repeat
|
||||
<input class="form-control" type="number" min="1" step="1" data-animation-modal-step="loop" data-animation-modal-field="iterations" placeholder="1" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-between">
|
||||
<button type="button" class="btn btn-outline-secondary" data-animation-modal-reset>Reset to defaults</button>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" data-animation-modal-apply>Apply changes</button>
|
||||
</div>
|
||||
</div>
|
||||
{{/modal-shell}}
|
||||
@@ -31,10 +31,17 @@
|
||||
</div>
|
||||
<div class="template-designer-sidebar">
|
||||
<div class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-body d-grid">
|
||||
<div class="card-body d-grid gap-3 pb-0">
|
||||
<div class="btn-group" role="group" aria-label="Template actions">
|
||||
{{{saveActionButtons formId="template-form" saveUrl=formAction cancelUrl=cancelUrl deleteUrl=deleteUrl deleteDisabled=deleteDisabled showSaveAndClose=showSaveSecondaryActions showSaveAndNew=showSaveSecondaryActions}}}
|
||||
</div>
|
||||
<div class="d-grid gap-2 template-preview-card" id="template-preview-card" hidden>
|
||||
<div class="btn-group flex-wrap gap-0" role="group" aria-label="Template animation preview controls">
|
||||
<button type="button" class="btn btn-outline-secondary" id="template-preview-intro" data-preview-control data-preview-action="intro" aria-pressed="false">Intro</button>
|
||||
<button type="button" class="btn btn-outline-secondary" id="template-preview-out" data-preview-control data-preview-action="out" aria-pressed="false">Outro</button>
|
||||
<button type="button" class="btn btn-outline-secondary" id="template-preview-continuous" data-preview-control data-preview-action="continuous" aria-pressed="false">Attention</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card card-outline card-secondary admin-form-card region-info-card mb-3">
|
||||
@@ -148,10 +155,57 @@
|
||||
<label>Width<input class="form-control" type="number" name="region_width[]" value="300" required /></label>
|
||||
<label>Height<input class="form-control" type="number" name="region_height[]" value="120" required /></label>
|
||||
<label>Lock ratio<input class="form-control" type="text" name="region_lock_ratio[]" value="" placeholder="16:9 or 5:4" /></label>
|
||||
<label>Remove<button type="button" class="btn btn-sm btn-outline-danger w-100" data-region-remove-button{{#if isEdit}} disabled{{/if}}>Remove</button></label>
|
||||
<label>Remove<button type="button" class="btn btn-outline-danger w-100" data-region-remove-button{{#if isEdit}} disabled{{/if}}>Remove</button></label>
|
||||
</div>
|
||||
<div class="region-field-grid region-field-grid--animation">
|
||||
<div class="d-grid gap-1">
|
||||
<label>
|
||||
Intro
|
||||
<select class="form-select" data-animation-preset="intro">
|
||||
{{#each animationPresets.basic.intro}}
|
||||
<option value="{{value}}">{{label}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</label>
|
||||
<button type="button" class="btn btn-outline-secondary w-100" data-animation-reset-button="intro" hidden>Reset to basic</button>
|
||||
</div>
|
||||
<div class="d-grid gap-1">
|
||||
<label>
|
||||
Outro
|
||||
<select class="form-select" data-animation-preset="outro">
|
||||
{{#each animationPresets.basic.outro}}
|
||||
<option value="{{value}}">{{label}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</label>
|
||||
<button type="button" class="btn btn-outline-secondary w-100" data-animation-reset-button="outro" hidden>Reset to basic</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="region-field-grid region-field-grid--animation">
|
||||
<div class="d-grid gap-1">
|
||||
<label>
|
||||
Attention seekers
|
||||
<select class="form-select" data-animation-preset="loop">
|
||||
{{#each animationPresets.basic.attentionSeekers}}
|
||||
<option value="{{value}}">{{label}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</label>
|
||||
<button type="button" class="btn btn-outline-secondary w-100" data-animation-reset-button="loop" hidden>Reset to basic</button>
|
||||
</div>
|
||||
<div class="d-grid gap-1">
|
||||
<label>
|
||||
Advanced
|
||||
<button type="button" class="btn btn-outline-secondary w-100" data-animation-advanced-button>Advanced</button>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<textarea class="visually-hidden" name="region_animation_json[]" data-animation-json-input rows="1"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
{{> signage/templates/animation-advanced-modal}}
|
||||
|
||||
<textarea id="template-editor-data" hidden>{{json template.regions}}</textarea>
|
||||
Reference in New Issue
Block a user