diff --git a/CHANGELOG.md b/CHANGELOG.md index ada093b..6b2a994 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,36 @@ All notable changes to this project will be documented in this file. +## 2.5.1 - 2026-08-03 + +### Added + +- Template regions now include a dedicated advanced animation modal for editing intro, outro, and Attention Seekers settings directly. + +### Changed + +- Template region animation settings now store as a single JSON object with `intro`, `outro`, and `loop` keys. +- The template editor advanced animation field now edits the JSON object directly instead of separate preset selects. +- Animate.css is now loaded from vendored assets so template previews and player rendering use the local copy. + +### Fixed + +- Existing databases now migrate legacy animation columns into `animation_json` and then drop the old columns. + +## 2.5.0 - 2026-08-03 + +### Added + +- Template regions now support intro, exit, and Attention Seekers animation presets in the editor, with in-canvas preview controls. + +### Changed + +- The template editor now locks editing while preview playback is running so animation changes are easier to inspect. + +### Fixed + +- Existing databases now receive the template region animation columns through the versioned migration path. + ## 2.4.2 - 2026-08-02 ### Added diff --git a/Dockerfile b/Dockerfile index 3ae367a..1bf6bfb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,9 @@ RUN npm install --omit=dev --no-audit --no-fund COPY src ./src +RUN mkdir -p /app/src/web/public/vendor/animate.css && cp /app/node_modules/animate.css/animate.min.css /app/src/web/public/vendor/animate.css/animate.min.css +RUN mkdir -p /app/src/player/public/vendor/animate.css && cp /app/node_modules/animate.css/animate.min.css /app/src/player/public/vendor/animate.css/animate.min.css + RUN mkdir -p /app/media EXPOSE 3000 diff --git a/package-lock.json b/package-lock.json index dc59762..221cc66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "pulse-signage", - "version": "2.1.0", + "version": "2.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pulse-signage", - "version": "2.0.0", + "version": "2.5.1", "dependencies": { "@sparticuz/chromium": "^137.0.0", + "animate.css": "^4.1.1", "bootstrap-icons": "1.11.3", "cropperjs": "^1.6.2", "dotenv": "^17.4.2", @@ -755,6 +756,12 @@ "node": ">= 14" } }, + "node_modules/animate.css": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz", + "integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==", + "license": "MIT" + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", diff --git a/package.json b/package.json index e71b894..ef8d50b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-signage", - "version": "2.4.2", + "version": "2.5.1", "private": false, "description": "Pulse Signage application with MySQL and media storage", "repository": { @@ -13,10 +13,12 @@ "start:web": "node -r dotenv/config src/web.js", "start:player": "node -r dotenv/config src/player.js", "dev:web": "nodemon -r dotenv/config src/web.js", - "dev:player": "nodemon -r dotenv/config src/player.js" + "dev:player": "nodemon -r dotenv/config src/player.js", + "test": "node --test" }, "dependencies": { "@sparticuz/chromium": "^137.0.0", + "animate.css": "^4.1.1", "bootstrap-icons": "1.11.3", "cropperjs": "^1.6.2", "dotenv": "^17.4.2", diff --git a/src/data/templates.js b/src/data/templates.js index 4184be5..17d5d08 100644 --- a/src/data/templates.js +++ b/src/data/templates.js @@ -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), diff --git a/src/db/index.js b/src/db/index.js index 95d49f8..44d84a9 100644 --- a/src/db/index.js +++ b/src/db/index.js @@ -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, diff --git a/src/db/migrations.js b/src/db/migrations.js index fa2ad6c..8f21bac 100644 --- a/src/db/migrations.js +++ b/src/db/migrations.js @@ -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; + } + } } } diff --git a/src/player/player-page-announcements.js b/src/player/player-page-announcements.js index 6c14c5d..e2d5b01 100644 --- a/src/player/player-page-announcements.js +++ b/src/player/player-page-announcements.js @@ -1,6 +1,10 @@ // Announcement overlay polling and rendering helpers. (function () { + if (window.__pulseThumbnailPreview) { + return; + } + var announcementLayer = null; var announcementRefreshTimer = null; var announcementPollTimer = null; diff --git a/src/player/player-page.template.html b/src/player/player-page.template.html index 92621c3..e65d528 100644 --- a/src/player/player-page.template.html +++ b/src/player/player-page.template.html @@ -6,6 +6,7 @@ {{TITLE}} + {{{STYLESHEETS}}} diff --git a/src/player/playlist.js b/src/player/playlist.js index 7bb92b3..b61d053 100644 --- a/src/player/playlist.js +++ b/src/player/playlist.js @@ -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; diff --git a/src/player/public/css/player.css b/src/player/public/css/player.css index 66fd1b2..bef4abb 100644 --- a/src/player/public/css/player.css +++ b/src/player/public/css/player.css @@ -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%; diff --git a/src/player/public/js/player-page-commands.js b/src/player/public/js/player-page-commands.js index 715c2c6..d6cdbff 100644 --- a/src/player/public/js/player-page-commands.js +++ b/src/player/public/js/player-page-commands.js @@ -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 () { diff --git a/src/player/public/js/player-page-rendering.js b/src/player/public/js/player-page-rendering.js index 42c6c9a..673b1d3 100644 --- a/src/player/public/js/player-page-rendering.js +++ b/src/player/public/js/player-page-rendering.js @@ -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) { diff --git a/src/player/public/vendor/animate.css/animate.min.css b/src/player/public/vendor/animate.css/animate.min.css new file mode 100644 index 0000000..76d2fe1 --- /dev/null +++ b/src/player/public/vendor/animate.css/animate.min.css @@ -0,0 +1,7 @@ +@charset "UTF-8";/*! + * animate.css - https://animate.style/ + * Version - 4.1.1 + * Licensed under the MIT license - http://opensource.org/licenses/MIT + * + * Copyright (c) 2020 Animate.css + */:root{--animate-duration:1s;--animate-delay:1s;--animate-repeat:1}.animate__animated{-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-duration:var(--animate-duration);animation-duration:var(--animate-duration);-webkit-animation-fill-mode:both;animation-fill-mode:both}.animate__animated.animate__infinite{-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.animate__animated.animate__repeat-1{-webkit-animation-iteration-count:1;animation-iteration-count:1;-webkit-animation-iteration-count:var(--animate-repeat);animation-iteration-count:var(--animate-repeat)}.animate__animated.animate__repeat-2{-webkit-animation-iteration-count:2;animation-iteration-count:2;-webkit-animation-iteration-count:calc(var(--animate-repeat)*2);animation-iteration-count:calc(var(--animate-repeat)*2)}.animate__animated.animate__repeat-3{-webkit-animation-iteration-count:3;animation-iteration-count:3;-webkit-animation-iteration-count:calc(var(--animate-repeat)*3);animation-iteration-count:calc(var(--animate-repeat)*3)}.animate__animated.animate__delay-1s{-webkit-animation-delay:1s;animation-delay:1s;-webkit-animation-delay:var(--animate-delay);animation-delay:var(--animate-delay)}.animate__animated.animate__delay-2s{-webkit-animation-delay:2s;animation-delay:2s;-webkit-animation-delay:calc(var(--animate-delay)*2);animation-delay:calc(var(--animate-delay)*2)}.animate__animated.animate__delay-3s{-webkit-animation-delay:3s;animation-delay:3s;-webkit-animation-delay:calc(var(--animate-delay)*3);animation-delay:calc(var(--animate-delay)*3)}.animate__animated.animate__delay-4s{-webkit-animation-delay:4s;animation-delay:4s;-webkit-animation-delay:calc(var(--animate-delay)*4);animation-delay:calc(var(--animate-delay)*4)}.animate__animated.animate__delay-5s{-webkit-animation-delay:5s;animation-delay:5s;-webkit-animation-delay:calc(var(--animate-delay)*5);animation-delay:calc(var(--animate-delay)*5)}.animate__animated.animate__faster{-webkit-animation-duration:.5s;animation-duration:.5s;-webkit-animation-duration:calc(var(--animate-duration)/2);animation-duration:calc(var(--animate-duration)/2)}.animate__animated.animate__fast{-webkit-animation-duration:.8s;animation-duration:.8s;-webkit-animation-duration:calc(var(--animate-duration)*0.8);animation-duration:calc(var(--animate-duration)*0.8)}.animate__animated.animate__slow{-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-duration:calc(var(--animate-duration)*2);animation-duration:calc(var(--animate-duration)*2)}.animate__animated.animate__slower{-webkit-animation-duration:3s;animation-duration:3s;-webkit-animation-duration:calc(var(--animate-duration)*3);animation-duration:calc(var(--animate-duration)*3)}@media (prefers-reduced-motion:reduce),print{.animate__animated{-webkit-animation-duration:1ms!important;animation-duration:1ms!important;-webkit-transition-duration:1ms!important;transition-duration:1ms!important;-webkit-animation-iteration-count:1!important;animation-iteration-count:1!important}.animate__animated[class*=Out]{opacity:0}}@-webkit-keyframes bounce{0%,20%,53%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0);transform:translateZ(0)}40%,43%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06);-webkit-transform:translate3d(0,-30px,0) scaleY(1.1);transform:translate3d(0,-30px,0) scaleY(1.1)}70%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06);-webkit-transform:translate3d(0,-15px,0) scaleY(1.05);transform:translate3d(0,-15px,0) scaleY(1.05)}80%{-webkit-transition-timing-function:cubic-bezier(.215,.61,.355,1);transition-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0) scaleY(.95);transform:translateZ(0) scaleY(.95)}90%{-webkit-transform:translate3d(0,-4px,0) scaleY(1.02);transform:translate3d(0,-4px,0) scaleY(1.02)}}@keyframes bounce{0%,20%,53%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0);transform:translateZ(0)}40%,43%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06);-webkit-transform:translate3d(0,-30px,0) scaleY(1.1);transform:translate3d(0,-30px,0) scaleY(1.1)}70%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06);-webkit-transform:translate3d(0,-15px,0) scaleY(1.05);transform:translate3d(0,-15px,0) scaleY(1.05)}80%{-webkit-transition-timing-function:cubic-bezier(.215,.61,.355,1);transition-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0) scaleY(.95);transform:translateZ(0) scaleY(.95)}90%{-webkit-transform:translate3d(0,-4px,0) scaleY(1.02);transform:translate3d(0,-4px,0) scaleY(1.02)}}.animate__bounce{-webkit-animation-name:bounce;animation-name:bounce;-webkit-transform-origin:center bottom;transform-origin:center bottom}@-webkit-keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}@keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}.animate__flash{-webkit-animation-name:flash;animation-name:flash}@-webkit-keyframes pulse{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes pulse{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.animate__pulse{-webkit-animation-name:pulse;animation-name:pulse;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}@-webkit-keyframes rubberBand{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes rubberBand{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.animate__rubberBand{-webkit-animation-name:rubberBand;animation-name:rubberBand}@-webkit-keyframes shakeX{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}@keyframes shakeX{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}.animate__shakeX{-webkit-animation-name:shakeX;animation-name:shakeX}@-webkit-keyframes shakeY{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}20%,40%,60%,80%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}}@keyframes shakeY{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}20%,40%,60%,80%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}}.animate__shakeY{-webkit-animation-name:shakeY;animation-name:shakeY}@-webkit-keyframes headShake{0%{-webkit-transform:translateX(0);transform:translateX(0)}6.5%{-webkit-transform:translateX(-6px) rotateY(-9deg);transform:translateX(-6px) rotateY(-9deg)}18.5%{-webkit-transform:translateX(5px) rotateY(7deg);transform:translateX(5px) rotateY(7deg)}31.5%{-webkit-transform:translateX(-3px) rotateY(-5deg);transform:translateX(-3px) rotateY(-5deg)}43.5%{-webkit-transform:translateX(2px) rotateY(3deg);transform:translateX(2px) rotateY(3deg)}50%{-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes headShake{0%{-webkit-transform:translateX(0);transform:translateX(0)}6.5%{-webkit-transform:translateX(-6px) rotateY(-9deg);transform:translateX(-6px) rotateY(-9deg)}18.5%{-webkit-transform:translateX(5px) rotateY(7deg);transform:translateX(5px) rotateY(7deg)}31.5%{-webkit-transform:translateX(-3px) rotateY(-5deg);transform:translateX(-3px) rotateY(-5deg)}43.5%{-webkit-transform:translateX(2px) rotateY(3deg);transform:translateX(2px) rotateY(3deg)}50%{-webkit-transform:translateX(0);transform:translateX(0)}}.animate__headShake{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-name:headShake;animation-name:headShake}@-webkit-keyframes swing{20%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}40%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}60%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}80%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@keyframes swing{20%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}40%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}60%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}80%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}.animate__swing{-webkit-transform-origin:top center;transform-origin:top center;-webkit-animation-name:swing;animation-name:swing}@-webkit-keyframes tada{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate(-3deg);transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(3deg);transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(-3deg);transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes tada{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate(-3deg);transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(3deg);transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(-3deg);transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.animate__tada{-webkit-animation-name:tada;animation-name:tada}@-webkit-keyframes wobble{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}15%{-webkit-transform:translate3d(-25%,0,0) rotate(-5deg);transform:translate3d(-25%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate(3deg);transform:translate3d(20%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate(-3deg);transform:translate3d(-15%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate(2deg);transform:translate3d(10%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate(-1deg);transform:translate3d(-5%,0,0) rotate(-1deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes wobble{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}15%{-webkit-transform:translate3d(-25%,0,0) rotate(-5deg);transform:translate3d(-25%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate(3deg);transform:translate3d(20%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate(-3deg);transform:translate3d(-15%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate(2deg);transform:translate3d(10%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate(-1deg);transform:translate3d(-5%,0,0) rotate(-1deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__wobble{-webkit-animation-name:wobble;animation-name:wobble}@-webkit-keyframes jello{0%,11.1%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}22.2%{-webkit-transform:skewX(-12.5deg) skewY(-12.5deg);transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{-webkit-transform:skewX(6.25deg) skewY(6.25deg);transform:skewX(6.25deg) skewY(6.25deg)}44.4%{-webkit-transform:skewX(-3.125deg) skewY(-3.125deg);transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{-webkit-transform:skewX(1.5625deg) skewY(1.5625deg);transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{-webkit-transform:skewX(-.78125deg) skewY(-.78125deg);transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{-webkit-transform:skewX(.390625deg) skewY(.390625deg);transform:skewX(.390625deg) skewY(.390625deg)}88.8%{-webkit-transform:skewX(-.1953125deg) skewY(-.1953125deg);transform:skewX(-.1953125deg) skewY(-.1953125deg)}}@keyframes jello{0%,11.1%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}22.2%{-webkit-transform:skewX(-12.5deg) skewY(-12.5deg);transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{-webkit-transform:skewX(6.25deg) skewY(6.25deg);transform:skewX(6.25deg) skewY(6.25deg)}44.4%{-webkit-transform:skewX(-3.125deg) skewY(-3.125deg);transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{-webkit-transform:skewX(1.5625deg) skewY(1.5625deg);transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{-webkit-transform:skewX(-.78125deg) skewY(-.78125deg);transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{-webkit-transform:skewX(.390625deg) skewY(.390625deg);transform:skewX(.390625deg) skewY(.390625deg)}88.8%{-webkit-transform:skewX(-.1953125deg) skewY(-.1953125deg);transform:skewX(-.1953125deg) skewY(-.1953125deg)}}.animate__jello{-webkit-animation-name:jello;animation-name:jello;-webkit-transform-origin:center;transform-origin:center}@-webkit-keyframes heartBeat{0%{-webkit-transform:scale(1);transform:scale(1)}14%{-webkit-transform:scale(1.3);transform:scale(1.3)}28%{-webkit-transform:scale(1);transform:scale(1)}42%{-webkit-transform:scale(1.3);transform:scale(1.3)}70%{-webkit-transform:scale(1);transform:scale(1)}}@keyframes heartBeat{0%{-webkit-transform:scale(1);transform:scale(1)}14%{-webkit-transform:scale(1.3);transform:scale(1.3)}28%{-webkit-transform:scale(1);transform:scale(1)}42%{-webkit-transform:scale(1.3);transform:scale(1.3)}70%{-webkit-transform:scale(1);transform:scale(1)}}.animate__heartBeat{-webkit-animation-name:heartBeat;animation-name:heartBeat;-webkit-animation-duration:1.3s;animation-duration:1.3s;-webkit-animation-duration:calc(var(--animate-duration)*1.3);animation-duration:calc(var(--animate-duration)*1.3);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}@-webkit-keyframes backInDown{0%{-webkit-transform:translateY(-1200px) scale(.7);transform:translateY(-1200px) scale(.7);opacity:.7}80%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes backInDown{0%{-webkit-transform:translateY(-1200px) scale(.7);transform:translateY(-1200px) scale(.7);opacity:.7}80%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.animate__backInDown{-webkit-animation-name:backInDown;animation-name:backInDown}@-webkit-keyframes backInLeft{0%{-webkit-transform:translateX(-2000px) scale(.7);transform:translateX(-2000px) scale(.7);opacity:.7}80%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes backInLeft{0%{-webkit-transform:translateX(-2000px) scale(.7);transform:translateX(-2000px) scale(.7);opacity:.7}80%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.animate__backInLeft{-webkit-animation-name:backInLeft;animation-name:backInLeft}@-webkit-keyframes backInRight{0%{-webkit-transform:translateX(2000px) scale(.7);transform:translateX(2000px) scale(.7);opacity:.7}80%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes backInRight{0%{-webkit-transform:translateX(2000px) scale(.7);transform:translateX(2000px) scale(.7);opacity:.7}80%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.animate__backInRight{-webkit-animation-name:backInRight;animation-name:backInRight}@-webkit-keyframes backInUp{0%{-webkit-transform:translateY(1200px) scale(.7);transform:translateY(1200px) scale(.7);opacity:.7}80%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes backInUp{0%{-webkit-transform:translateY(1200px) scale(.7);transform:translateY(1200px) scale(.7);opacity:.7}80%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.animate__backInUp{-webkit-animation-name:backInUp;animation-name:backInUp}@-webkit-keyframes backOutDown{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:translateY(700px) scale(.7);transform:translateY(700px) scale(.7);opacity:.7}}@keyframes backOutDown{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:translateY(700px) scale(.7);transform:translateY(700px) scale(.7);opacity:.7}}.animate__backOutDown{-webkit-animation-name:backOutDown;animation-name:backOutDown}@-webkit-keyframes backOutLeft{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:translateX(-2000px) scale(.7);transform:translateX(-2000px) scale(.7);opacity:.7}}@keyframes backOutLeft{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:translateX(-2000px) scale(.7);transform:translateX(-2000px) scale(.7);opacity:.7}}.animate__backOutLeft{-webkit-animation-name:backOutLeft;animation-name:backOutLeft}@-webkit-keyframes backOutRight{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:translateX(2000px) scale(.7);transform:translateX(2000px) scale(.7);opacity:.7}}@keyframes backOutRight{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:translateX(2000px) scale(.7);transform:translateX(2000px) scale(.7);opacity:.7}}.animate__backOutRight{-webkit-animation-name:backOutRight;animation-name:backOutRight}@-webkit-keyframes backOutUp{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:translateY(-700px) scale(.7);transform:translateY(-700px) scale(.7);opacity:.7}}@keyframes backOutUp{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:translateY(-700px) scale(.7);transform:translateY(-700px) scale(.7);opacity:.7}}.animate__backOutUp{-webkit-animation-name:backOutUp;animation-name:backOutUp}@-webkit-keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}to{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}to{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}.animate__bounceIn{-webkit-animation-duration:.75s;animation-duration:.75s;-webkit-animation-duration:calc(var(--animate-duration)*0.75);animation-duration:calc(var(--animate-duration)*0.75);-webkit-animation-name:bounceIn;animation-name:bounceIn}@-webkit-keyframes bounceInDown{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0) scaleY(3);transform:translate3d(0,-3000px,0) scaleY(3)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0) scaleY(.9);transform:translate3d(0,25px,0) scaleY(.9)}75%{-webkit-transform:translate3d(0,-10px,0) scaleY(.95);transform:translate3d(0,-10px,0) scaleY(.95)}90%{-webkit-transform:translate3d(0,5px,0) scaleY(.985);transform:translate3d(0,5px,0) scaleY(.985)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInDown{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0) scaleY(3);transform:translate3d(0,-3000px,0) scaleY(3)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0) scaleY(.9);transform:translate3d(0,25px,0) scaleY(.9)}75%{-webkit-transform:translate3d(0,-10px,0) scaleY(.95);transform:translate3d(0,-10px,0) scaleY(.95)}90%{-webkit-transform:translate3d(0,5px,0) scaleY(.985);transform:translate3d(0,5px,0) scaleY(.985)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__bounceInDown{-webkit-animation-name:bounceInDown;animation-name:bounceInDown}@-webkit-keyframes bounceInLeft{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0) scaleX(3);transform:translate3d(-3000px,0,0) scaleX(3)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0) scaleX(1);transform:translate3d(25px,0,0) scaleX(1)}75%{-webkit-transform:translate3d(-10px,0,0) scaleX(.98);transform:translate3d(-10px,0,0) scaleX(.98)}90%{-webkit-transform:translate3d(5px,0,0) scaleX(.995);transform:translate3d(5px,0,0) scaleX(.995)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInLeft{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0) scaleX(3);transform:translate3d(-3000px,0,0) scaleX(3)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0) scaleX(1);transform:translate3d(25px,0,0) scaleX(1)}75%{-webkit-transform:translate3d(-10px,0,0) scaleX(.98);transform:translate3d(-10px,0,0) scaleX(.98)}90%{-webkit-transform:translate3d(5px,0,0) scaleX(.995);transform:translate3d(5px,0,0) scaleX(.995)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__bounceInLeft{-webkit-animation-name:bounceInLeft;animation-name:bounceInLeft}@-webkit-keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0) scaleX(3);transform:translate3d(3000px,0,0) scaleX(3)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0) scaleX(1);transform:translate3d(-25px,0,0) scaleX(1)}75%{-webkit-transform:translate3d(10px,0,0) scaleX(.98);transform:translate3d(10px,0,0) scaleX(.98)}90%{-webkit-transform:translate3d(-5px,0,0) scaleX(.995);transform:translate3d(-5px,0,0) scaleX(.995)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0) scaleX(3);transform:translate3d(3000px,0,0) scaleX(3)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0) scaleX(1);transform:translate3d(-25px,0,0) scaleX(1)}75%{-webkit-transform:translate3d(10px,0,0) scaleX(.98);transform:translate3d(10px,0,0) scaleX(.98)}90%{-webkit-transform:translate3d(-5px,0,0) scaleX(.995);transform:translate3d(-5px,0,0) scaleX(.995)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__bounceInRight{-webkit-animation-name:bounceInRight;animation-name:bounceInRight}@-webkit-keyframes bounceInUp{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0) scaleY(5);transform:translate3d(0,3000px,0) scaleY(5)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0) scaleY(.9);transform:translate3d(0,-20px,0) scaleY(.9)}75%{-webkit-transform:translate3d(0,10px,0) scaleY(.95);transform:translate3d(0,10px,0) scaleY(.95)}90%{-webkit-transform:translate3d(0,-5px,0) scaleY(.985);transform:translate3d(0,-5px,0) scaleY(.985)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInUp{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0) scaleY(5);transform:translate3d(0,3000px,0) scaleY(5)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0) scaleY(.9);transform:translate3d(0,-20px,0) scaleY(.9)}75%{-webkit-transform:translate3d(0,10px,0) scaleY(.95);transform:translate3d(0,10px,0) scaleY(.95)}90%{-webkit-transform:translate3d(0,-5px,0) scaleY(.985);transform:translate3d(0,-5px,0) scaleY(.985)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__bounceInUp{-webkit-animation-name:bounceInUp;animation-name:bounceInUp}@-webkit-keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}to{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}@keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}to{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}.animate__bounceOut{-webkit-animation-duration:.75s;animation-duration:.75s;-webkit-animation-duration:calc(var(--animate-duration)*0.75);animation-duration:calc(var(--animate-duration)*0.75);-webkit-animation-name:bounceOut;animation-name:bounceOut}@-webkit-keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0) scaleY(.985);transform:translate3d(0,10px,0) scaleY(.985)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0) scaleY(.9);transform:translate3d(0,-20px,0) scaleY(.9)}to{opacity:0;-webkit-transform:translate3d(0,2000px,0) scaleY(3);transform:translate3d(0,2000px,0) scaleY(3)}}@keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0) scaleY(.985);transform:translate3d(0,10px,0) scaleY(.985)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0) scaleY(.9);transform:translate3d(0,-20px,0) scaleY(.9)}to{opacity:0;-webkit-transform:translate3d(0,2000px,0) scaleY(3);transform:translate3d(0,2000px,0) scaleY(3)}}.animate__bounceOutDown{-webkit-animation-name:bounceOutDown;animation-name:bounceOutDown}@-webkit-keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0) scaleX(.9);transform:translate3d(20px,0,0) scaleX(.9)}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0) scaleX(2);transform:translate3d(-2000px,0,0) scaleX(2)}}@keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0) scaleX(.9);transform:translate3d(20px,0,0) scaleX(.9)}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0) scaleX(2);transform:translate3d(-2000px,0,0) scaleX(2)}}.animate__bounceOutLeft{-webkit-animation-name:bounceOutLeft;animation-name:bounceOutLeft}@-webkit-keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0) scaleX(.9);transform:translate3d(-20px,0,0) scaleX(.9)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0) scaleX(2);transform:translate3d(2000px,0,0) scaleX(2)}}@keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0) scaleX(.9);transform:translate3d(-20px,0,0) scaleX(.9)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0) scaleX(2);transform:translate3d(2000px,0,0) scaleX(2)}}.animate__bounceOutRight{-webkit-animation-name:bounceOutRight;animation-name:bounceOutRight}@-webkit-keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0) scaleY(.985);transform:translate3d(0,-10px,0) scaleY(.985)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0) scaleY(.9);transform:translate3d(0,20px,0) scaleY(.9)}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0) scaleY(3);transform:translate3d(0,-2000px,0) scaleY(3)}}@keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0) scaleY(.985);transform:translate3d(0,-10px,0) scaleY(.985)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0) scaleY(.9);transform:translate3d(0,20px,0) scaleY(.9)}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0) scaleY(3);transform:translate3d(0,-2000px,0) scaleY(3)}}.animate__bounceOutUp{-webkit-animation-name:bounceOutUp;animation-name:bounceOutUp}@-webkit-keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.animate__fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn}@-webkit-keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInDown{-webkit-animation-name:fadeInDown;animation-name:fadeInDown}@-webkit-keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInDownBig{-webkit-animation-name:fadeInDownBig;animation-name:fadeInDownBig}@-webkit-keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInLeft{-webkit-animation-name:fadeInLeft;animation-name:fadeInLeft}@-webkit-keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInLeftBig{-webkit-animation-name:fadeInLeftBig;animation-name:fadeInLeftBig}@-webkit-keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInRight{-webkit-animation-name:fadeInRight;animation-name:fadeInRight}@-webkit-keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInRightBig{-webkit-animation-name:fadeInRightBig;animation-name:fadeInRightBig}@-webkit-keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInUp{-webkit-animation-name:fadeInUp;animation-name:fadeInUp}@-webkit-keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInUpBig{-webkit-animation-name:fadeInUpBig;animation-name:fadeInUpBig}@-webkit-keyframes fadeInTopLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,-100%,0);transform:translate3d(-100%,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInTopLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,-100%,0);transform:translate3d(-100%,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInTopLeft{-webkit-animation-name:fadeInTopLeft;animation-name:fadeInTopLeft}@-webkit-keyframes fadeInTopRight{0%{opacity:0;-webkit-transform:translate3d(100%,-100%,0);transform:translate3d(100%,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInTopRight{0%{opacity:0;-webkit-transform:translate3d(100%,-100%,0);transform:translate3d(100%,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInTopRight{-webkit-animation-name:fadeInTopRight;animation-name:fadeInTopRight}@-webkit-keyframes fadeInBottomLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,100%,0);transform:translate3d(-100%,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInBottomLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,100%,0);transform:translate3d(-100%,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInBottomLeft{-webkit-animation-name:fadeInBottomLeft;animation-name:fadeInBottomLeft}@-webkit-keyframes fadeInBottomRight{0%{opacity:0;-webkit-transform:translate3d(100%,100%,0);transform:translate3d(100%,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInBottomRight{0%{opacity:0;-webkit-transform:translate3d(100%,100%,0);transform:translate3d(100%,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInBottomRight{-webkit-animation-name:fadeInBottomRight;animation-name:fadeInBottomRight}@-webkit-keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.animate__fadeOut{-webkit-animation-name:fadeOut;animation-name:fadeOut}@-webkit-keyframes fadeOutDown{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes fadeOutDown{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.animate__fadeOutDown{-webkit-animation-name:fadeOutDown;animation-name:fadeOutDown}@-webkit-keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}.animate__fadeOutDownBig{-webkit-animation-name:fadeOutDownBig;animation-name:fadeOutDownBig}@-webkit-keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.animate__fadeOutLeft{-webkit-animation-name:fadeOutLeft;animation-name:fadeOutLeft}@-webkit-keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}.animate__fadeOutLeftBig{-webkit-animation-name:fadeOutLeftBig;animation-name:fadeOutLeftBig}@-webkit-keyframes fadeOutRight{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes fadeOutRight{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.animate__fadeOutRight{-webkit-animation-name:fadeOutRight;animation-name:fadeOutRight}@-webkit-keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.animate__fadeOutRightBig{-webkit-animation-name:fadeOutRightBig;animation-name:fadeOutRightBig}@-webkit-keyframes fadeOutUp{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes fadeOutUp{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.animate__fadeOutUp{-webkit-animation-name:fadeOutUp;animation-name:fadeOutUp}@-webkit-keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}.animate__fadeOutUpBig{-webkit-animation-name:fadeOutUpBig;animation-name:fadeOutUpBig}@-webkit-keyframes fadeOutTopLeft{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(-100%,-100%,0);transform:translate3d(-100%,-100%,0)}}@keyframes fadeOutTopLeft{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(-100%,-100%,0);transform:translate3d(-100%,-100%,0)}}.animate__fadeOutTopLeft{-webkit-animation-name:fadeOutTopLeft;animation-name:fadeOutTopLeft}@-webkit-keyframes fadeOutTopRight{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(100%,-100%,0);transform:translate3d(100%,-100%,0)}}@keyframes fadeOutTopRight{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(100%,-100%,0);transform:translate3d(100%,-100%,0)}}.animate__fadeOutTopRight{-webkit-animation-name:fadeOutTopRight;animation-name:fadeOutTopRight}@-webkit-keyframes fadeOutBottomRight{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(100%,100%,0);transform:translate3d(100%,100%,0)}}@keyframes fadeOutBottomRight{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(100%,100%,0);transform:translate3d(100%,100%,0)}}.animate__fadeOutBottomRight{-webkit-animation-name:fadeOutBottomRight;animation-name:fadeOutBottomRight}@-webkit-keyframes fadeOutBottomLeft{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(-100%,100%,0);transform:translate3d(-100%,100%,0)}}@keyframes fadeOutBottomLeft{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(-100%,100%,0);transform:translate3d(-100%,100%,0)}}.animate__fadeOutBottomLeft{-webkit-animation-name:fadeOutBottomLeft;animation-name:fadeOutBottomLeft}@-webkit-keyframes flip{0%{-webkit-transform:perspective(400px) scaleX(1) translateZ(0) rotateY(-1turn);transform:perspective(400px) scaleX(1) translateZ(0) rotateY(-1turn);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-190deg);transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-190deg);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}50%{-webkit-transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-170deg);transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-170deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95) translateZ(0) rotateY(0deg);transform:perspective(400px) scale3d(.95,.95,.95) translateZ(0) rotateY(0deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}to{-webkit-transform:perspective(400px) scaleX(1) translateZ(0) rotateY(0deg);transform:perspective(400px) scaleX(1) translateZ(0) rotateY(0deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}@keyframes flip{0%{-webkit-transform:perspective(400px) scaleX(1) translateZ(0) rotateY(-1turn);transform:perspective(400px) scaleX(1) translateZ(0) rotateY(-1turn);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-190deg);transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-190deg);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}50%{-webkit-transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-170deg);transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-170deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95) translateZ(0) rotateY(0deg);transform:perspective(400px) scale3d(.95,.95,.95) translateZ(0) rotateY(0deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}to{-webkit-transform:perspective(400px) scaleX(1) translateZ(0) rotateY(0deg);transform:perspective(400px) scaleX(1) translateZ(0) rotateY(0deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}.animate__animated.animate__flip{-webkit-backface-visibility:visible;backface-visibility:visible;-webkit-animation-name:flip;animation-name:flip}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateX(-5deg);transform:perspective(400px) rotateX(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateX(-5deg);transform:perspective(400px) rotateX(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}.animate__flipInX{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInX;animation-name:flipInX}@-webkit-keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotateY(-20deg);transform:perspective(400px) rotateY(-20deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateY(-5deg);transform:perspective(400px) rotateY(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotateY(-20deg);transform:perspective(400px) rotateY(-20deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateY(-5deg);transform:perspective(400px) rotateY(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}.animate__flipInY{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInY;animation-name:flipInY}@-webkit-keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);opacity:1}to{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}}@keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);opacity:1}to{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}}.animate__flipOutX{-webkit-animation-duration:.75s;animation-duration:.75s;-webkit-animation-duration:calc(var(--animate-duration)*0.75);animation-duration:calc(var(--animate-duration)*0.75);-webkit-animation-name:flipOutX;animation-name:flipOutX;-webkit-backface-visibility:visible!important;backface-visibility:visible!important}@-webkit-keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateY(-15deg);transform:perspective(400px) rotateY(-15deg);opacity:1}to{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}}@keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateY(-15deg);transform:perspective(400px) rotateY(-15deg);opacity:1}to{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}}.animate__flipOutY{-webkit-animation-duration:.75s;animation-duration:.75s;-webkit-animation-duration:calc(var(--animate-duration)*0.75);animation-duration:calc(var(--animate-duration)*0.75);-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipOutY;animation-name:flipOutY}@-webkit-keyframes lightSpeedInRight{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg);opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes lightSpeedInRight{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg);opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__lightSpeedInRight{-webkit-animation-name:lightSpeedInRight;animation-name:lightSpeedInRight;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}@-webkit-keyframes lightSpeedInLeft{0%{-webkit-transform:translate3d(-100%,0,0) skewX(30deg);transform:translate3d(-100%,0,0) skewX(30deg);opacity:0}60%{-webkit-transform:skewX(-20deg);transform:skewX(-20deg);opacity:1}80%{-webkit-transform:skewX(5deg);transform:skewX(5deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes lightSpeedInLeft{0%{-webkit-transform:translate3d(-100%,0,0) skewX(30deg);transform:translate3d(-100%,0,0) skewX(30deg);opacity:0}60%{-webkit-transform:skewX(-20deg);transform:skewX(-20deg);opacity:1}80%{-webkit-transform:skewX(5deg);transform:skewX(5deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__lightSpeedInLeft{-webkit-animation-name:lightSpeedInLeft;animation-name:lightSpeedInLeft;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}@-webkit-keyframes lightSpeedOutRight{0%{opacity:1}to{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}@keyframes lightSpeedOutRight{0%{opacity:1}to{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}.animate__lightSpeedOutRight{-webkit-animation-name:lightSpeedOutRight;animation-name:lightSpeedOutRight;-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}@-webkit-keyframes lightSpeedOutLeft{0%{opacity:1}to{-webkit-transform:translate3d(-100%,0,0) skewX(-30deg);transform:translate3d(-100%,0,0) skewX(-30deg);opacity:0}}@keyframes lightSpeedOutLeft{0%{opacity:1}to{-webkit-transform:translate3d(-100%,0,0) skewX(-30deg);transform:translate3d(-100%,0,0) skewX(-30deg);opacity:0}}.animate__lightSpeedOutLeft{-webkit-animation-name:lightSpeedOutLeft;animation-name:lightSpeedOutLeft;-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}@-webkit-keyframes rotateIn{0%{-webkit-transform:rotate(-200deg);transform:rotate(-200deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@keyframes rotateIn{0%{-webkit-transform:rotate(-200deg);transform:rotate(-200deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}.animate__rotateIn{-webkit-animation-name:rotateIn;animation-name:rotateIn;-webkit-transform-origin:center;transform-origin:center}@-webkit-keyframes rotateInDownLeft{0%{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@keyframes rotateInDownLeft{0%{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}.animate__rotateInDownLeft{-webkit-animation-name:rotateInDownLeft;animation-name:rotateInDownLeft;-webkit-transform-origin:left bottom;transform-origin:left bottom}@-webkit-keyframes rotateInDownRight{0%{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@keyframes rotateInDownRight{0%{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}.animate__rotateInDownRight{-webkit-animation-name:rotateInDownRight;animation-name:rotateInDownRight;-webkit-transform-origin:right bottom;transform-origin:right bottom}@-webkit-keyframes rotateInUpLeft{0%{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@keyframes rotateInUpLeft{0%{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}.animate__rotateInUpLeft{-webkit-animation-name:rotateInUpLeft;animation-name:rotateInUpLeft;-webkit-transform-origin:left bottom;transform-origin:left bottom}@-webkit-keyframes rotateInUpRight{0%{-webkit-transform:rotate(-90deg);transform:rotate(-90deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@keyframes rotateInUpRight{0%{-webkit-transform:rotate(-90deg);transform:rotate(-90deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}.animate__rotateInUpRight{-webkit-animation-name:rotateInUpRight;animation-name:rotateInUpRight;-webkit-transform-origin:right bottom;transform-origin:right bottom}@-webkit-keyframes rotateOut{0%{opacity:1}to{-webkit-transform:rotate(200deg);transform:rotate(200deg);opacity:0}}@keyframes rotateOut{0%{opacity:1}to{-webkit-transform:rotate(200deg);transform:rotate(200deg);opacity:0}}.animate__rotateOut{-webkit-animation-name:rotateOut;animation-name:rotateOut;-webkit-transform-origin:center;transform-origin:center}@-webkit-keyframes rotateOutDownLeft{0%{opacity:1}to{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}@keyframes rotateOutDownLeft{0%{opacity:1}to{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}.animate__rotateOutDownLeft{-webkit-animation-name:rotateOutDownLeft;animation-name:rotateOutDownLeft;-webkit-transform-origin:left bottom;transform-origin:left bottom}@-webkit-keyframes rotateOutDownRight{0%{opacity:1}to{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}@keyframes rotateOutDownRight{0%{opacity:1}to{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}.animate__rotateOutDownRight{-webkit-animation-name:rotateOutDownRight;animation-name:rotateOutDownRight;-webkit-transform-origin:right bottom;transform-origin:right bottom}@-webkit-keyframes rotateOutUpLeft{0%{opacity:1}to{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}@keyframes rotateOutUpLeft{0%{opacity:1}to{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}.animate__rotateOutUpLeft{-webkit-animation-name:rotateOutUpLeft;animation-name:rotateOutUpLeft;-webkit-transform-origin:left bottom;transform-origin:left bottom}@-webkit-keyframes rotateOutUpRight{0%{opacity:1}to{-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}}@keyframes rotateOutUpRight{0%{opacity:1}to{-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}}.animate__rotateOutUpRight{-webkit-animation-name:rotateOutUpRight;animation-name:rotateOutUpRight;-webkit-transform-origin:right bottom;transform-origin:right bottom}@-webkit-keyframes hinge{0%{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate(80deg);transform:rotate(80deg);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}40%,80%{-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}to{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}@keyframes hinge{0%{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate(80deg);transform:rotate(80deg);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}40%,80%{-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}to{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}.animate__hinge{-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-duration:calc(var(--animate-duration)*2);animation-duration:calc(var(--animate-duration)*2);-webkit-animation-name:hinge;animation-name:hinge;-webkit-transform-origin:top left;transform-origin:top left}@-webkit-keyframes jackInTheBox{0%{opacity:0;-webkit-transform:scale(.1) rotate(30deg);transform:scale(.1) rotate(30deg);-webkit-transform-origin:center bottom;transform-origin:center bottom}50%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}70%{-webkit-transform:rotate(3deg);transform:rotate(3deg)}to{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes jackInTheBox{0%{opacity:0;-webkit-transform:scale(.1) rotate(30deg);transform:scale(.1) rotate(30deg);-webkit-transform-origin:center bottom;transform-origin:center bottom}50%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}70%{-webkit-transform:rotate(3deg);transform:rotate(3deg)}to{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}.animate__jackInTheBox{-webkit-animation-name:jackInTheBox;animation-name:jackInTheBox}@-webkit-keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate(-120deg);transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate(-120deg);transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__rollIn{-webkit-animation-name:rollIn;animation-name:rollIn}@-webkit-keyframes rollOut{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate(120deg);transform:translate3d(100%,0,0) rotate(120deg)}}@keyframes rollOut{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate(120deg);transform:translate3d(100%,0,0) rotate(120deg)}}.animate__rollOut{-webkit-animation-name:rollOut;animation-name:rollOut}@-webkit-keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}@keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}.animate__zoomIn{-webkit-animation-name:zoomIn;animation-name:zoomIn}@-webkit-keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomInDown{-webkit-animation-name:zoomInDown;animation-name:zoomInDown}@-webkit-keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomInLeft{-webkit-animation-name:zoomInLeft;animation-name:zoomInLeft}@-webkit-keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomInRight{-webkit-animation-name:zoomInRight;animation-name:zoomInRight}@-webkit-keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomInUp{-webkit-animation-name:zoomInUp;animation-name:zoomInUp}@-webkit-keyframes zoomOut{0%{opacity:1}50%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}to{opacity:0}}@keyframes zoomOut{0%{opacity:1}50%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}to{opacity:0}}.animate__zoomOut{-webkit-animation-name:zoomOut;animation-name:zoomOut}@-webkit-keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomOutDown{-webkit-animation-name:zoomOutDown;animation-name:zoomOutDown;-webkit-transform-origin:center bottom;transform-origin:center bottom}@-webkit-keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0)}}@keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0)}}.animate__zoomOutLeft{-webkit-animation-name:zoomOutLeft;animation-name:zoomOutLeft;-webkit-transform-origin:left center;transform-origin:left center}@-webkit-keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0)}}@keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0)}}.animate__zoomOutRight{-webkit-animation-name:zoomOutRight;animation-name:zoomOutRight;-webkit-transform-origin:right center;transform-origin:right center}@-webkit-keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomOutUp{-webkit-animation-name:zoomOutUp;animation-name:zoomOutUp;-webkit-transform-origin:center bottom;transform-origin:center bottom}@-webkit-keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__slideInDown{-webkit-animation-name:slideInDown;animation-name:slideInDown}@-webkit-keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__slideInLeft{-webkit-animation-name:slideInLeft;animation-name:slideInLeft}@-webkit-keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__slideInRight{-webkit-animation-name:slideInRight;animation-name:slideInRight}@-webkit-keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__slideInUp{-webkit-animation-name:slideInUp;animation-name:slideInUp}@-webkit-keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.animate__slideOutDown{-webkit-animation-name:slideOutDown;animation-name:slideOutDown}@-webkit-keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.animate__slideOutLeft{-webkit-animation-name:slideOutLeft;animation-name:slideOutLeft}@-webkit-keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.animate__slideOutRight{-webkit-animation-name:slideOutRight;animation-name:slideOutRight}@-webkit-keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.animate__slideOutUp{-webkit-animation-name:slideOutUp;animation-name:slideOutUp} \ No newline at end of file diff --git a/src/player/render.js b/src/player/render.js index 841778d..80477e6 100644 --- a/src/player/render.js +++ b/src/player/render.js @@ -120,6 +120,7 @@ function renderPlayerPage(slug, initialData) { const hlsScriptTag = ''; 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: '
Loading screen...
', stylesheets: fontStylesheetHref ? [fontStylesheetHref] : [], script: createPageFetchAuthScript(pageAuthToken) + hlsScriptTag + serviceWorkerScript + createThumbnailPreviewBootstrapScript(initialData) + '' + '' + '' + '' + '' + '' + '' + '' + onboardingScript + script + '' diff --git a/src/web/handlebars.js b/src/web/handlebars.js index e95b676..7b484a9 100644 --- a/src/web/handlebars.js +++ b/src/web/handlebars.js @@ -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')); } diff --git a/src/web/public/css/theme-custom.css b/src/web/public/css/theme-custom.css index 0f5f00d..72032e7 100644 --- a/src/web/public/css/theme-custom.css +++ b/src/web/public/css/theme-custom.css @@ -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; } } diff --git a/src/web/public/js/templates/animation-presets.js b/src/web/public/js/templates/animation-presets.js new file mode 100644 index 0000000..f686a78 --- /dev/null +++ b/src/web/public/js/templates/animation-presets.js @@ -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 + }; +}); \ No newline at end of file diff --git a/src/web/public/js/templates/template-designer-utils.js b/src/web/public/js/templates/template-designer-utils.js index 8457ae8..8144e7a 100644 --- a/src/web/public/js/templates/template-designer-utils.js +++ b/src/web/public/js/templates/template-designer-utils.js @@ -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, diff --git a/src/web/public/js/templates/template-designer.js b/src/web/public/js/templates/template-designer.js index 164ed23..9a48877 100644 --- a/src/web/public/js/templates/template-designer.js +++ b/src/web/public/js/templates/template-designer.js @@ -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 '
' + escapeHtml(region.label || region.region_key || 'Region') + '
'; + return '
' + escapeHtml(region.label || region.region_key || 'Region') + '
'; }).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 += '
'; + overlay.innerHTML += '
'; } + + 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(); diff --git a/src/web/public/vendor/animate.css/animate.min.css b/src/web/public/vendor/animate.css/animate.min.css new file mode 100644 index 0000000..76d2fe1 --- /dev/null +++ b/src/web/public/vendor/animate.css/animate.min.css @@ -0,0 +1,7 @@ +@charset "UTF-8";/*! + * animate.css - https://animate.style/ + * Version - 4.1.1 + * Licensed under the MIT license - http://opensource.org/licenses/MIT + * + * Copyright (c) 2020 Animate.css + */:root{--animate-duration:1s;--animate-delay:1s;--animate-repeat:1}.animate__animated{-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-duration:var(--animate-duration);animation-duration:var(--animate-duration);-webkit-animation-fill-mode:both;animation-fill-mode:both}.animate__animated.animate__infinite{-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.animate__animated.animate__repeat-1{-webkit-animation-iteration-count:1;animation-iteration-count:1;-webkit-animation-iteration-count:var(--animate-repeat);animation-iteration-count:var(--animate-repeat)}.animate__animated.animate__repeat-2{-webkit-animation-iteration-count:2;animation-iteration-count:2;-webkit-animation-iteration-count:calc(var(--animate-repeat)*2);animation-iteration-count:calc(var(--animate-repeat)*2)}.animate__animated.animate__repeat-3{-webkit-animation-iteration-count:3;animation-iteration-count:3;-webkit-animation-iteration-count:calc(var(--animate-repeat)*3);animation-iteration-count:calc(var(--animate-repeat)*3)}.animate__animated.animate__delay-1s{-webkit-animation-delay:1s;animation-delay:1s;-webkit-animation-delay:var(--animate-delay);animation-delay:var(--animate-delay)}.animate__animated.animate__delay-2s{-webkit-animation-delay:2s;animation-delay:2s;-webkit-animation-delay:calc(var(--animate-delay)*2);animation-delay:calc(var(--animate-delay)*2)}.animate__animated.animate__delay-3s{-webkit-animation-delay:3s;animation-delay:3s;-webkit-animation-delay:calc(var(--animate-delay)*3);animation-delay:calc(var(--animate-delay)*3)}.animate__animated.animate__delay-4s{-webkit-animation-delay:4s;animation-delay:4s;-webkit-animation-delay:calc(var(--animate-delay)*4);animation-delay:calc(var(--animate-delay)*4)}.animate__animated.animate__delay-5s{-webkit-animation-delay:5s;animation-delay:5s;-webkit-animation-delay:calc(var(--animate-delay)*5);animation-delay:calc(var(--animate-delay)*5)}.animate__animated.animate__faster{-webkit-animation-duration:.5s;animation-duration:.5s;-webkit-animation-duration:calc(var(--animate-duration)/2);animation-duration:calc(var(--animate-duration)/2)}.animate__animated.animate__fast{-webkit-animation-duration:.8s;animation-duration:.8s;-webkit-animation-duration:calc(var(--animate-duration)*0.8);animation-duration:calc(var(--animate-duration)*0.8)}.animate__animated.animate__slow{-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-duration:calc(var(--animate-duration)*2);animation-duration:calc(var(--animate-duration)*2)}.animate__animated.animate__slower{-webkit-animation-duration:3s;animation-duration:3s;-webkit-animation-duration:calc(var(--animate-duration)*3);animation-duration:calc(var(--animate-duration)*3)}@media (prefers-reduced-motion:reduce),print{.animate__animated{-webkit-animation-duration:1ms!important;animation-duration:1ms!important;-webkit-transition-duration:1ms!important;transition-duration:1ms!important;-webkit-animation-iteration-count:1!important;animation-iteration-count:1!important}.animate__animated[class*=Out]{opacity:0}}@-webkit-keyframes bounce{0%,20%,53%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0);transform:translateZ(0)}40%,43%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06);-webkit-transform:translate3d(0,-30px,0) scaleY(1.1);transform:translate3d(0,-30px,0) scaleY(1.1)}70%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06);-webkit-transform:translate3d(0,-15px,0) scaleY(1.05);transform:translate3d(0,-15px,0) scaleY(1.05)}80%{-webkit-transition-timing-function:cubic-bezier(.215,.61,.355,1);transition-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0) scaleY(.95);transform:translateZ(0) scaleY(.95)}90%{-webkit-transform:translate3d(0,-4px,0) scaleY(1.02);transform:translate3d(0,-4px,0) scaleY(1.02)}}@keyframes bounce{0%,20%,53%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0);transform:translateZ(0)}40%,43%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06);-webkit-transform:translate3d(0,-30px,0) scaleY(1.1);transform:translate3d(0,-30px,0) scaleY(1.1)}70%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06);-webkit-transform:translate3d(0,-15px,0) scaleY(1.05);transform:translate3d(0,-15px,0) scaleY(1.05)}80%{-webkit-transition-timing-function:cubic-bezier(.215,.61,.355,1);transition-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0) scaleY(.95);transform:translateZ(0) scaleY(.95)}90%{-webkit-transform:translate3d(0,-4px,0) scaleY(1.02);transform:translate3d(0,-4px,0) scaleY(1.02)}}.animate__bounce{-webkit-animation-name:bounce;animation-name:bounce;-webkit-transform-origin:center bottom;transform-origin:center bottom}@-webkit-keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}@keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}.animate__flash{-webkit-animation-name:flash;animation-name:flash}@-webkit-keyframes pulse{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes pulse{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.animate__pulse{-webkit-animation-name:pulse;animation-name:pulse;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}@-webkit-keyframes rubberBand{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes rubberBand{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.animate__rubberBand{-webkit-animation-name:rubberBand;animation-name:rubberBand}@-webkit-keyframes shakeX{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}@keyframes shakeX{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}.animate__shakeX{-webkit-animation-name:shakeX;animation-name:shakeX}@-webkit-keyframes shakeY{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}20%,40%,60%,80%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}}@keyframes shakeY{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}20%,40%,60%,80%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}}.animate__shakeY{-webkit-animation-name:shakeY;animation-name:shakeY}@-webkit-keyframes headShake{0%{-webkit-transform:translateX(0);transform:translateX(0)}6.5%{-webkit-transform:translateX(-6px) rotateY(-9deg);transform:translateX(-6px) rotateY(-9deg)}18.5%{-webkit-transform:translateX(5px) rotateY(7deg);transform:translateX(5px) rotateY(7deg)}31.5%{-webkit-transform:translateX(-3px) rotateY(-5deg);transform:translateX(-3px) rotateY(-5deg)}43.5%{-webkit-transform:translateX(2px) rotateY(3deg);transform:translateX(2px) rotateY(3deg)}50%{-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes headShake{0%{-webkit-transform:translateX(0);transform:translateX(0)}6.5%{-webkit-transform:translateX(-6px) rotateY(-9deg);transform:translateX(-6px) rotateY(-9deg)}18.5%{-webkit-transform:translateX(5px) rotateY(7deg);transform:translateX(5px) rotateY(7deg)}31.5%{-webkit-transform:translateX(-3px) rotateY(-5deg);transform:translateX(-3px) rotateY(-5deg)}43.5%{-webkit-transform:translateX(2px) rotateY(3deg);transform:translateX(2px) rotateY(3deg)}50%{-webkit-transform:translateX(0);transform:translateX(0)}}.animate__headShake{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-name:headShake;animation-name:headShake}@-webkit-keyframes swing{20%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}40%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}60%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}80%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@keyframes swing{20%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}40%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}60%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}80%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}.animate__swing{-webkit-transform-origin:top center;transform-origin:top center;-webkit-animation-name:swing;animation-name:swing}@-webkit-keyframes tada{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate(-3deg);transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(3deg);transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(-3deg);transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes tada{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate(-3deg);transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(3deg);transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(-3deg);transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.animate__tada{-webkit-animation-name:tada;animation-name:tada}@-webkit-keyframes wobble{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}15%{-webkit-transform:translate3d(-25%,0,0) rotate(-5deg);transform:translate3d(-25%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate(3deg);transform:translate3d(20%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate(-3deg);transform:translate3d(-15%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate(2deg);transform:translate3d(10%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate(-1deg);transform:translate3d(-5%,0,0) rotate(-1deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes wobble{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}15%{-webkit-transform:translate3d(-25%,0,0) rotate(-5deg);transform:translate3d(-25%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate(3deg);transform:translate3d(20%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate(-3deg);transform:translate3d(-15%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate(2deg);transform:translate3d(10%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate(-1deg);transform:translate3d(-5%,0,0) rotate(-1deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__wobble{-webkit-animation-name:wobble;animation-name:wobble}@-webkit-keyframes jello{0%,11.1%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}22.2%{-webkit-transform:skewX(-12.5deg) skewY(-12.5deg);transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{-webkit-transform:skewX(6.25deg) skewY(6.25deg);transform:skewX(6.25deg) skewY(6.25deg)}44.4%{-webkit-transform:skewX(-3.125deg) skewY(-3.125deg);transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{-webkit-transform:skewX(1.5625deg) skewY(1.5625deg);transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{-webkit-transform:skewX(-.78125deg) skewY(-.78125deg);transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{-webkit-transform:skewX(.390625deg) skewY(.390625deg);transform:skewX(.390625deg) skewY(.390625deg)}88.8%{-webkit-transform:skewX(-.1953125deg) skewY(-.1953125deg);transform:skewX(-.1953125deg) skewY(-.1953125deg)}}@keyframes jello{0%,11.1%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}22.2%{-webkit-transform:skewX(-12.5deg) skewY(-12.5deg);transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{-webkit-transform:skewX(6.25deg) skewY(6.25deg);transform:skewX(6.25deg) skewY(6.25deg)}44.4%{-webkit-transform:skewX(-3.125deg) skewY(-3.125deg);transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{-webkit-transform:skewX(1.5625deg) skewY(1.5625deg);transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{-webkit-transform:skewX(-.78125deg) skewY(-.78125deg);transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{-webkit-transform:skewX(.390625deg) skewY(.390625deg);transform:skewX(.390625deg) skewY(.390625deg)}88.8%{-webkit-transform:skewX(-.1953125deg) skewY(-.1953125deg);transform:skewX(-.1953125deg) skewY(-.1953125deg)}}.animate__jello{-webkit-animation-name:jello;animation-name:jello;-webkit-transform-origin:center;transform-origin:center}@-webkit-keyframes heartBeat{0%{-webkit-transform:scale(1);transform:scale(1)}14%{-webkit-transform:scale(1.3);transform:scale(1.3)}28%{-webkit-transform:scale(1);transform:scale(1)}42%{-webkit-transform:scale(1.3);transform:scale(1.3)}70%{-webkit-transform:scale(1);transform:scale(1)}}@keyframes heartBeat{0%{-webkit-transform:scale(1);transform:scale(1)}14%{-webkit-transform:scale(1.3);transform:scale(1.3)}28%{-webkit-transform:scale(1);transform:scale(1)}42%{-webkit-transform:scale(1.3);transform:scale(1.3)}70%{-webkit-transform:scale(1);transform:scale(1)}}.animate__heartBeat{-webkit-animation-name:heartBeat;animation-name:heartBeat;-webkit-animation-duration:1.3s;animation-duration:1.3s;-webkit-animation-duration:calc(var(--animate-duration)*1.3);animation-duration:calc(var(--animate-duration)*1.3);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}@-webkit-keyframes backInDown{0%{-webkit-transform:translateY(-1200px) scale(.7);transform:translateY(-1200px) scale(.7);opacity:.7}80%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes backInDown{0%{-webkit-transform:translateY(-1200px) scale(.7);transform:translateY(-1200px) scale(.7);opacity:.7}80%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.animate__backInDown{-webkit-animation-name:backInDown;animation-name:backInDown}@-webkit-keyframes backInLeft{0%{-webkit-transform:translateX(-2000px) scale(.7);transform:translateX(-2000px) scale(.7);opacity:.7}80%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes backInLeft{0%{-webkit-transform:translateX(-2000px) scale(.7);transform:translateX(-2000px) scale(.7);opacity:.7}80%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.animate__backInLeft{-webkit-animation-name:backInLeft;animation-name:backInLeft}@-webkit-keyframes backInRight{0%{-webkit-transform:translateX(2000px) scale(.7);transform:translateX(2000px) scale(.7);opacity:.7}80%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes backInRight{0%{-webkit-transform:translateX(2000px) scale(.7);transform:translateX(2000px) scale(.7);opacity:.7}80%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.animate__backInRight{-webkit-animation-name:backInRight;animation-name:backInRight}@-webkit-keyframes backInUp{0%{-webkit-transform:translateY(1200px) scale(.7);transform:translateY(1200px) scale(.7);opacity:.7}80%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes backInUp{0%{-webkit-transform:translateY(1200px) scale(.7);transform:translateY(1200px) scale(.7);opacity:.7}80%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.animate__backInUp{-webkit-animation-name:backInUp;animation-name:backInUp}@-webkit-keyframes backOutDown{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:translateY(700px) scale(.7);transform:translateY(700px) scale(.7);opacity:.7}}@keyframes backOutDown{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:translateY(700px) scale(.7);transform:translateY(700px) scale(.7);opacity:.7}}.animate__backOutDown{-webkit-animation-name:backOutDown;animation-name:backOutDown}@-webkit-keyframes backOutLeft{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:translateX(-2000px) scale(.7);transform:translateX(-2000px) scale(.7);opacity:.7}}@keyframes backOutLeft{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:translateX(-2000px) scale(.7);transform:translateX(-2000px) scale(.7);opacity:.7}}.animate__backOutLeft{-webkit-animation-name:backOutLeft;animation-name:backOutLeft}@-webkit-keyframes backOutRight{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:translateX(2000px) scale(.7);transform:translateX(2000px) scale(.7);opacity:.7}}@keyframes backOutRight{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:translateX(2000px) scale(.7);transform:translateX(2000px) scale(.7);opacity:.7}}.animate__backOutRight{-webkit-animation-name:backOutRight;animation-name:backOutRight}@-webkit-keyframes backOutUp{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:translateY(-700px) scale(.7);transform:translateY(-700px) scale(.7);opacity:.7}}@keyframes backOutUp{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:translateY(-700px) scale(.7);transform:translateY(-700px) scale(.7);opacity:.7}}.animate__backOutUp{-webkit-animation-name:backOutUp;animation-name:backOutUp}@-webkit-keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}to{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}to{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}.animate__bounceIn{-webkit-animation-duration:.75s;animation-duration:.75s;-webkit-animation-duration:calc(var(--animate-duration)*0.75);animation-duration:calc(var(--animate-duration)*0.75);-webkit-animation-name:bounceIn;animation-name:bounceIn}@-webkit-keyframes bounceInDown{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0) scaleY(3);transform:translate3d(0,-3000px,0) scaleY(3)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0) scaleY(.9);transform:translate3d(0,25px,0) scaleY(.9)}75%{-webkit-transform:translate3d(0,-10px,0) scaleY(.95);transform:translate3d(0,-10px,0) scaleY(.95)}90%{-webkit-transform:translate3d(0,5px,0) scaleY(.985);transform:translate3d(0,5px,0) scaleY(.985)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInDown{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0) scaleY(3);transform:translate3d(0,-3000px,0) scaleY(3)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0) scaleY(.9);transform:translate3d(0,25px,0) scaleY(.9)}75%{-webkit-transform:translate3d(0,-10px,0) scaleY(.95);transform:translate3d(0,-10px,0) scaleY(.95)}90%{-webkit-transform:translate3d(0,5px,0) scaleY(.985);transform:translate3d(0,5px,0) scaleY(.985)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__bounceInDown{-webkit-animation-name:bounceInDown;animation-name:bounceInDown}@-webkit-keyframes bounceInLeft{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0) scaleX(3);transform:translate3d(-3000px,0,0) scaleX(3)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0) scaleX(1);transform:translate3d(25px,0,0) scaleX(1)}75%{-webkit-transform:translate3d(-10px,0,0) scaleX(.98);transform:translate3d(-10px,0,0) scaleX(.98)}90%{-webkit-transform:translate3d(5px,0,0) scaleX(.995);transform:translate3d(5px,0,0) scaleX(.995)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInLeft{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0) scaleX(3);transform:translate3d(-3000px,0,0) scaleX(3)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0) scaleX(1);transform:translate3d(25px,0,0) scaleX(1)}75%{-webkit-transform:translate3d(-10px,0,0) scaleX(.98);transform:translate3d(-10px,0,0) scaleX(.98)}90%{-webkit-transform:translate3d(5px,0,0) scaleX(.995);transform:translate3d(5px,0,0) scaleX(.995)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__bounceInLeft{-webkit-animation-name:bounceInLeft;animation-name:bounceInLeft}@-webkit-keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0) scaleX(3);transform:translate3d(3000px,0,0) scaleX(3)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0) scaleX(1);transform:translate3d(-25px,0,0) scaleX(1)}75%{-webkit-transform:translate3d(10px,0,0) scaleX(.98);transform:translate3d(10px,0,0) scaleX(.98)}90%{-webkit-transform:translate3d(-5px,0,0) scaleX(.995);transform:translate3d(-5px,0,0) scaleX(.995)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0) scaleX(3);transform:translate3d(3000px,0,0) scaleX(3)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0) scaleX(1);transform:translate3d(-25px,0,0) scaleX(1)}75%{-webkit-transform:translate3d(10px,0,0) scaleX(.98);transform:translate3d(10px,0,0) scaleX(.98)}90%{-webkit-transform:translate3d(-5px,0,0) scaleX(.995);transform:translate3d(-5px,0,0) scaleX(.995)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__bounceInRight{-webkit-animation-name:bounceInRight;animation-name:bounceInRight}@-webkit-keyframes bounceInUp{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0) scaleY(5);transform:translate3d(0,3000px,0) scaleY(5)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0) scaleY(.9);transform:translate3d(0,-20px,0) scaleY(.9)}75%{-webkit-transform:translate3d(0,10px,0) scaleY(.95);transform:translate3d(0,10px,0) scaleY(.95)}90%{-webkit-transform:translate3d(0,-5px,0) scaleY(.985);transform:translate3d(0,-5px,0) scaleY(.985)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInUp{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0) scaleY(5);transform:translate3d(0,3000px,0) scaleY(5)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0) scaleY(.9);transform:translate3d(0,-20px,0) scaleY(.9)}75%{-webkit-transform:translate3d(0,10px,0) scaleY(.95);transform:translate3d(0,10px,0) scaleY(.95)}90%{-webkit-transform:translate3d(0,-5px,0) scaleY(.985);transform:translate3d(0,-5px,0) scaleY(.985)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__bounceInUp{-webkit-animation-name:bounceInUp;animation-name:bounceInUp}@-webkit-keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}to{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}@keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}to{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}.animate__bounceOut{-webkit-animation-duration:.75s;animation-duration:.75s;-webkit-animation-duration:calc(var(--animate-duration)*0.75);animation-duration:calc(var(--animate-duration)*0.75);-webkit-animation-name:bounceOut;animation-name:bounceOut}@-webkit-keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0) scaleY(.985);transform:translate3d(0,10px,0) scaleY(.985)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0) scaleY(.9);transform:translate3d(0,-20px,0) scaleY(.9)}to{opacity:0;-webkit-transform:translate3d(0,2000px,0) scaleY(3);transform:translate3d(0,2000px,0) scaleY(3)}}@keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0) scaleY(.985);transform:translate3d(0,10px,0) scaleY(.985)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0) scaleY(.9);transform:translate3d(0,-20px,0) scaleY(.9)}to{opacity:0;-webkit-transform:translate3d(0,2000px,0) scaleY(3);transform:translate3d(0,2000px,0) scaleY(3)}}.animate__bounceOutDown{-webkit-animation-name:bounceOutDown;animation-name:bounceOutDown}@-webkit-keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0) scaleX(.9);transform:translate3d(20px,0,0) scaleX(.9)}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0) scaleX(2);transform:translate3d(-2000px,0,0) scaleX(2)}}@keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0) scaleX(.9);transform:translate3d(20px,0,0) scaleX(.9)}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0) scaleX(2);transform:translate3d(-2000px,0,0) scaleX(2)}}.animate__bounceOutLeft{-webkit-animation-name:bounceOutLeft;animation-name:bounceOutLeft}@-webkit-keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0) scaleX(.9);transform:translate3d(-20px,0,0) scaleX(.9)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0) scaleX(2);transform:translate3d(2000px,0,0) scaleX(2)}}@keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0) scaleX(.9);transform:translate3d(-20px,0,0) scaleX(.9)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0) scaleX(2);transform:translate3d(2000px,0,0) scaleX(2)}}.animate__bounceOutRight{-webkit-animation-name:bounceOutRight;animation-name:bounceOutRight}@-webkit-keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0) scaleY(.985);transform:translate3d(0,-10px,0) scaleY(.985)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0) scaleY(.9);transform:translate3d(0,20px,0) scaleY(.9)}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0) scaleY(3);transform:translate3d(0,-2000px,0) scaleY(3)}}@keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0) scaleY(.985);transform:translate3d(0,-10px,0) scaleY(.985)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0) scaleY(.9);transform:translate3d(0,20px,0) scaleY(.9)}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0) scaleY(3);transform:translate3d(0,-2000px,0) scaleY(3)}}.animate__bounceOutUp{-webkit-animation-name:bounceOutUp;animation-name:bounceOutUp}@-webkit-keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.animate__fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn}@-webkit-keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInDown{-webkit-animation-name:fadeInDown;animation-name:fadeInDown}@-webkit-keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInDownBig{-webkit-animation-name:fadeInDownBig;animation-name:fadeInDownBig}@-webkit-keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInLeft{-webkit-animation-name:fadeInLeft;animation-name:fadeInLeft}@-webkit-keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInLeftBig{-webkit-animation-name:fadeInLeftBig;animation-name:fadeInLeftBig}@-webkit-keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInRight{-webkit-animation-name:fadeInRight;animation-name:fadeInRight}@-webkit-keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInRightBig{-webkit-animation-name:fadeInRightBig;animation-name:fadeInRightBig}@-webkit-keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInUp{-webkit-animation-name:fadeInUp;animation-name:fadeInUp}@-webkit-keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInUpBig{-webkit-animation-name:fadeInUpBig;animation-name:fadeInUpBig}@-webkit-keyframes fadeInTopLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,-100%,0);transform:translate3d(-100%,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInTopLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,-100%,0);transform:translate3d(-100%,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInTopLeft{-webkit-animation-name:fadeInTopLeft;animation-name:fadeInTopLeft}@-webkit-keyframes fadeInTopRight{0%{opacity:0;-webkit-transform:translate3d(100%,-100%,0);transform:translate3d(100%,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInTopRight{0%{opacity:0;-webkit-transform:translate3d(100%,-100%,0);transform:translate3d(100%,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInTopRight{-webkit-animation-name:fadeInTopRight;animation-name:fadeInTopRight}@-webkit-keyframes fadeInBottomLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,100%,0);transform:translate3d(-100%,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInBottomLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,100%,0);transform:translate3d(-100%,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInBottomLeft{-webkit-animation-name:fadeInBottomLeft;animation-name:fadeInBottomLeft}@-webkit-keyframes fadeInBottomRight{0%{opacity:0;-webkit-transform:translate3d(100%,100%,0);transform:translate3d(100%,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInBottomRight{0%{opacity:0;-webkit-transform:translate3d(100%,100%,0);transform:translate3d(100%,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInBottomRight{-webkit-animation-name:fadeInBottomRight;animation-name:fadeInBottomRight}@-webkit-keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.animate__fadeOut{-webkit-animation-name:fadeOut;animation-name:fadeOut}@-webkit-keyframes fadeOutDown{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes fadeOutDown{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.animate__fadeOutDown{-webkit-animation-name:fadeOutDown;animation-name:fadeOutDown}@-webkit-keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}.animate__fadeOutDownBig{-webkit-animation-name:fadeOutDownBig;animation-name:fadeOutDownBig}@-webkit-keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.animate__fadeOutLeft{-webkit-animation-name:fadeOutLeft;animation-name:fadeOutLeft}@-webkit-keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}.animate__fadeOutLeftBig{-webkit-animation-name:fadeOutLeftBig;animation-name:fadeOutLeftBig}@-webkit-keyframes fadeOutRight{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes fadeOutRight{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.animate__fadeOutRight{-webkit-animation-name:fadeOutRight;animation-name:fadeOutRight}@-webkit-keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.animate__fadeOutRightBig{-webkit-animation-name:fadeOutRightBig;animation-name:fadeOutRightBig}@-webkit-keyframes fadeOutUp{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes fadeOutUp{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.animate__fadeOutUp{-webkit-animation-name:fadeOutUp;animation-name:fadeOutUp}@-webkit-keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}.animate__fadeOutUpBig{-webkit-animation-name:fadeOutUpBig;animation-name:fadeOutUpBig}@-webkit-keyframes fadeOutTopLeft{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(-100%,-100%,0);transform:translate3d(-100%,-100%,0)}}@keyframes fadeOutTopLeft{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(-100%,-100%,0);transform:translate3d(-100%,-100%,0)}}.animate__fadeOutTopLeft{-webkit-animation-name:fadeOutTopLeft;animation-name:fadeOutTopLeft}@-webkit-keyframes fadeOutTopRight{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(100%,-100%,0);transform:translate3d(100%,-100%,0)}}@keyframes fadeOutTopRight{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(100%,-100%,0);transform:translate3d(100%,-100%,0)}}.animate__fadeOutTopRight{-webkit-animation-name:fadeOutTopRight;animation-name:fadeOutTopRight}@-webkit-keyframes fadeOutBottomRight{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(100%,100%,0);transform:translate3d(100%,100%,0)}}@keyframes fadeOutBottomRight{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(100%,100%,0);transform:translate3d(100%,100%,0)}}.animate__fadeOutBottomRight{-webkit-animation-name:fadeOutBottomRight;animation-name:fadeOutBottomRight}@-webkit-keyframes fadeOutBottomLeft{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(-100%,100%,0);transform:translate3d(-100%,100%,0)}}@keyframes fadeOutBottomLeft{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(-100%,100%,0);transform:translate3d(-100%,100%,0)}}.animate__fadeOutBottomLeft{-webkit-animation-name:fadeOutBottomLeft;animation-name:fadeOutBottomLeft}@-webkit-keyframes flip{0%{-webkit-transform:perspective(400px) scaleX(1) translateZ(0) rotateY(-1turn);transform:perspective(400px) scaleX(1) translateZ(0) rotateY(-1turn);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-190deg);transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-190deg);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}50%{-webkit-transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-170deg);transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-170deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95) translateZ(0) rotateY(0deg);transform:perspective(400px) scale3d(.95,.95,.95) translateZ(0) rotateY(0deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}to{-webkit-transform:perspective(400px) scaleX(1) translateZ(0) rotateY(0deg);transform:perspective(400px) scaleX(1) translateZ(0) rotateY(0deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}@keyframes flip{0%{-webkit-transform:perspective(400px) scaleX(1) translateZ(0) rotateY(-1turn);transform:perspective(400px) scaleX(1) translateZ(0) rotateY(-1turn);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-190deg);transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-190deg);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}50%{-webkit-transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-170deg);transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-170deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95) translateZ(0) rotateY(0deg);transform:perspective(400px) scale3d(.95,.95,.95) translateZ(0) rotateY(0deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}to{-webkit-transform:perspective(400px) scaleX(1) translateZ(0) rotateY(0deg);transform:perspective(400px) scaleX(1) translateZ(0) rotateY(0deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}.animate__animated.animate__flip{-webkit-backface-visibility:visible;backface-visibility:visible;-webkit-animation-name:flip;animation-name:flip}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateX(-5deg);transform:perspective(400px) rotateX(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateX(-5deg);transform:perspective(400px) rotateX(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}.animate__flipInX{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInX;animation-name:flipInX}@-webkit-keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotateY(-20deg);transform:perspective(400px) rotateY(-20deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateY(-5deg);transform:perspective(400px) rotateY(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotateY(-20deg);transform:perspective(400px) rotateY(-20deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateY(-5deg);transform:perspective(400px) rotateY(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}.animate__flipInY{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInY;animation-name:flipInY}@-webkit-keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);opacity:1}to{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}}@keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);opacity:1}to{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}}.animate__flipOutX{-webkit-animation-duration:.75s;animation-duration:.75s;-webkit-animation-duration:calc(var(--animate-duration)*0.75);animation-duration:calc(var(--animate-duration)*0.75);-webkit-animation-name:flipOutX;animation-name:flipOutX;-webkit-backface-visibility:visible!important;backface-visibility:visible!important}@-webkit-keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateY(-15deg);transform:perspective(400px) rotateY(-15deg);opacity:1}to{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}}@keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateY(-15deg);transform:perspective(400px) rotateY(-15deg);opacity:1}to{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}}.animate__flipOutY{-webkit-animation-duration:.75s;animation-duration:.75s;-webkit-animation-duration:calc(var(--animate-duration)*0.75);animation-duration:calc(var(--animate-duration)*0.75);-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipOutY;animation-name:flipOutY}@-webkit-keyframes lightSpeedInRight{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg);opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes lightSpeedInRight{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg);opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__lightSpeedInRight{-webkit-animation-name:lightSpeedInRight;animation-name:lightSpeedInRight;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}@-webkit-keyframes lightSpeedInLeft{0%{-webkit-transform:translate3d(-100%,0,0) skewX(30deg);transform:translate3d(-100%,0,0) skewX(30deg);opacity:0}60%{-webkit-transform:skewX(-20deg);transform:skewX(-20deg);opacity:1}80%{-webkit-transform:skewX(5deg);transform:skewX(5deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes lightSpeedInLeft{0%{-webkit-transform:translate3d(-100%,0,0) skewX(30deg);transform:translate3d(-100%,0,0) skewX(30deg);opacity:0}60%{-webkit-transform:skewX(-20deg);transform:skewX(-20deg);opacity:1}80%{-webkit-transform:skewX(5deg);transform:skewX(5deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__lightSpeedInLeft{-webkit-animation-name:lightSpeedInLeft;animation-name:lightSpeedInLeft;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}@-webkit-keyframes lightSpeedOutRight{0%{opacity:1}to{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}@keyframes lightSpeedOutRight{0%{opacity:1}to{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}.animate__lightSpeedOutRight{-webkit-animation-name:lightSpeedOutRight;animation-name:lightSpeedOutRight;-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}@-webkit-keyframes lightSpeedOutLeft{0%{opacity:1}to{-webkit-transform:translate3d(-100%,0,0) skewX(-30deg);transform:translate3d(-100%,0,0) skewX(-30deg);opacity:0}}@keyframes lightSpeedOutLeft{0%{opacity:1}to{-webkit-transform:translate3d(-100%,0,0) skewX(-30deg);transform:translate3d(-100%,0,0) skewX(-30deg);opacity:0}}.animate__lightSpeedOutLeft{-webkit-animation-name:lightSpeedOutLeft;animation-name:lightSpeedOutLeft;-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}@-webkit-keyframes rotateIn{0%{-webkit-transform:rotate(-200deg);transform:rotate(-200deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@keyframes rotateIn{0%{-webkit-transform:rotate(-200deg);transform:rotate(-200deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}.animate__rotateIn{-webkit-animation-name:rotateIn;animation-name:rotateIn;-webkit-transform-origin:center;transform-origin:center}@-webkit-keyframes rotateInDownLeft{0%{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@keyframes rotateInDownLeft{0%{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}.animate__rotateInDownLeft{-webkit-animation-name:rotateInDownLeft;animation-name:rotateInDownLeft;-webkit-transform-origin:left bottom;transform-origin:left bottom}@-webkit-keyframes rotateInDownRight{0%{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@keyframes rotateInDownRight{0%{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}.animate__rotateInDownRight{-webkit-animation-name:rotateInDownRight;animation-name:rotateInDownRight;-webkit-transform-origin:right bottom;transform-origin:right bottom}@-webkit-keyframes rotateInUpLeft{0%{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@keyframes rotateInUpLeft{0%{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}.animate__rotateInUpLeft{-webkit-animation-name:rotateInUpLeft;animation-name:rotateInUpLeft;-webkit-transform-origin:left bottom;transform-origin:left bottom}@-webkit-keyframes rotateInUpRight{0%{-webkit-transform:rotate(-90deg);transform:rotate(-90deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@keyframes rotateInUpRight{0%{-webkit-transform:rotate(-90deg);transform:rotate(-90deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}.animate__rotateInUpRight{-webkit-animation-name:rotateInUpRight;animation-name:rotateInUpRight;-webkit-transform-origin:right bottom;transform-origin:right bottom}@-webkit-keyframes rotateOut{0%{opacity:1}to{-webkit-transform:rotate(200deg);transform:rotate(200deg);opacity:0}}@keyframes rotateOut{0%{opacity:1}to{-webkit-transform:rotate(200deg);transform:rotate(200deg);opacity:0}}.animate__rotateOut{-webkit-animation-name:rotateOut;animation-name:rotateOut;-webkit-transform-origin:center;transform-origin:center}@-webkit-keyframes rotateOutDownLeft{0%{opacity:1}to{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}@keyframes rotateOutDownLeft{0%{opacity:1}to{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}.animate__rotateOutDownLeft{-webkit-animation-name:rotateOutDownLeft;animation-name:rotateOutDownLeft;-webkit-transform-origin:left bottom;transform-origin:left bottom}@-webkit-keyframes rotateOutDownRight{0%{opacity:1}to{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}@keyframes rotateOutDownRight{0%{opacity:1}to{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}.animate__rotateOutDownRight{-webkit-animation-name:rotateOutDownRight;animation-name:rotateOutDownRight;-webkit-transform-origin:right bottom;transform-origin:right bottom}@-webkit-keyframes rotateOutUpLeft{0%{opacity:1}to{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}@keyframes rotateOutUpLeft{0%{opacity:1}to{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}.animate__rotateOutUpLeft{-webkit-animation-name:rotateOutUpLeft;animation-name:rotateOutUpLeft;-webkit-transform-origin:left bottom;transform-origin:left bottom}@-webkit-keyframes rotateOutUpRight{0%{opacity:1}to{-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}}@keyframes rotateOutUpRight{0%{opacity:1}to{-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}}.animate__rotateOutUpRight{-webkit-animation-name:rotateOutUpRight;animation-name:rotateOutUpRight;-webkit-transform-origin:right bottom;transform-origin:right bottom}@-webkit-keyframes hinge{0%{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate(80deg);transform:rotate(80deg);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}40%,80%{-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}to{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}@keyframes hinge{0%{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate(80deg);transform:rotate(80deg);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}40%,80%{-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}to{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}.animate__hinge{-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-duration:calc(var(--animate-duration)*2);animation-duration:calc(var(--animate-duration)*2);-webkit-animation-name:hinge;animation-name:hinge;-webkit-transform-origin:top left;transform-origin:top left}@-webkit-keyframes jackInTheBox{0%{opacity:0;-webkit-transform:scale(.1) rotate(30deg);transform:scale(.1) rotate(30deg);-webkit-transform-origin:center bottom;transform-origin:center bottom}50%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}70%{-webkit-transform:rotate(3deg);transform:rotate(3deg)}to{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes jackInTheBox{0%{opacity:0;-webkit-transform:scale(.1) rotate(30deg);transform:scale(.1) rotate(30deg);-webkit-transform-origin:center bottom;transform-origin:center bottom}50%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}70%{-webkit-transform:rotate(3deg);transform:rotate(3deg)}to{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}.animate__jackInTheBox{-webkit-animation-name:jackInTheBox;animation-name:jackInTheBox}@-webkit-keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate(-120deg);transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate(-120deg);transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__rollIn{-webkit-animation-name:rollIn;animation-name:rollIn}@-webkit-keyframes rollOut{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate(120deg);transform:translate3d(100%,0,0) rotate(120deg)}}@keyframes rollOut{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate(120deg);transform:translate3d(100%,0,0) rotate(120deg)}}.animate__rollOut{-webkit-animation-name:rollOut;animation-name:rollOut}@-webkit-keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}@keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}.animate__zoomIn{-webkit-animation-name:zoomIn;animation-name:zoomIn}@-webkit-keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomInDown{-webkit-animation-name:zoomInDown;animation-name:zoomInDown}@-webkit-keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomInLeft{-webkit-animation-name:zoomInLeft;animation-name:zoomInLeft}@-webkit-keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomInRight{-webkit-animation-name:zoomInRight;animation-name:zoomInRight}@-webkit-keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomInUp{-webkit-animation-name:zoomInUp;animation-name:zoomInUp}@-webkit-keyframes zoomOut{0%{opacity:1}50%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}to{opacity:0}}@keyframes zoomOut{0%{opacity:1}50%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}to{opacity:0}}.animate__zoomOut{-webkit-animation-name:zoomOut;animation-name:zoomOut}@-webkit-keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomOutDown{-webkit-animation-name:zoomOutDown;animation-name:zoomOutDown;-webkit-transform-origin:center bottom;transform-origin:center bottom}@-webkit-keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0)}}@keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0)}}.animate__zoomOutLeft{-webkit-animation-name:zoomOutLeft;animation-name:zoomOutLeft;-webkit-transform-origin:left center;transform-origin:left center}@-webkit-keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0)}}@keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0)}}.animate__zoomOutRight{-webkit-animation-name:zoomOutRight;animation-name:zoomOutRight;-webkit-transform-origin:right center;transform-origin:right center}@-webkit-keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomOutUp{-webkit-animation-name:zoomOutUp;animation-name:zoomOutUp;-webkit-transform-origin:center bottom;transform-origin:center bottom}@-webkit-keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__slideInDown{-webkit-animation-name:slideInDown;animation-name:slideInDown}@-webkit-keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__slideInLeft{-webkit-animation-name:slideInLeft;animation-name:slideInLeft}@-webkit-keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__slideInRight{-webkit-animation-name:slideInRight;animation-name:slideInRight}@-webkit-keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__slideInUp{-webkit-animation-name:slideInUp;animation-name:slideInUp}@-webkit-keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.animate__slideOutDown{-webkit-animation-name:slideOutDown;animation-name:slideOutDown}@-webkit-keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.animate__slideOutLeft{-webkit-animation-name:slideOutLeft;animation-name:slideOutLeft}@-webkit-keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.animate__slideOutRight{-webkit-animation-name:slideOutRight;animation-name:slideOutRight}@-webkit-keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.animate__slideOutUp{-webkit-animation-name:slideOutUp;animation-name:slideOutUp} \ No newline at end of file diff --git a/src/web/routes/admin/content.js b/src/web/routes/admin/content.js index 5dc6275..6929831 100644 --- a/src/web/routes/admin/content.js +++ b/src/web/routes/admin/content.js @@ -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({ diff --git a/src/web/routes/signage/templates/form-view-model.js b/src/web/routes/signage/templates/form-view-model.js index b0391ea..e076e6f 100644 --- a/src/web/routes/signage/templates/form-view-model.js +++ b/src/web/routes/signage/templates/form-view-model.js @@ -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]) }; } diff --git a/src/web/views/shared/layout.hbs b/src/web/views/shared/layout.hbs index 80834a2..114e1d2 100644 --- a/src/web/views/shared/layout.hbs +++ b/src/web/views/shared/layout.hbs @@ -11,6 +11,7 @@ + {{#if stylesheets.length}} {{#each stylesheets}} diff --git a/src/web/views/signage/templates/animation-advanced-modal.hbs b/src/web/views/signage/templates/animation-advanced-modal.hbs new file mode 100644 index 0000000..aa31d41 --- /dev/null +++ b/src/web/views/signage/templates/animation-advanced-modal.hbs @@ -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}} + + + +{{/modal-shell}} \ No newline at end of file diff --git a/src/web/views/signage/templates/form.hbs b/src/web/views/signage/templates/form.hbs index efa5719..ed8f321 100644 --- a/src/web/views/signage/templates/form.hbs +++ b/src/web/views/signage/templates/form.hbs @@ -31,10 +31,17 @@
-
+
{{{saveActionButtons formId="template-form" saveUrl=formAction cancelUrl=cancelUrl deleteUrl=deleteUrl deleteDisabled=deleteDisabled showSaveAndClose=showSaveSecondaryActions showSaveAndNew=showSaveSecondaryActions}}}
+
@@ -148,10 +155,57 @@ - + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ +
+
+
+{{> signage/templates/animation-advanced-modal}} + \ No newline at end of file diff --git a/test/client-name-check.test.js b/test/client-name-check.test.js new file mode 100644 index 0000000..4c33214 --- /dev/null +++ b/test/client-name-check.test.js @@ -0,0 +1,91 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const crypto = require('node:crypto'); + +const { + collectLiveConnections, + isClientNameAvailable, + normalizeClientName, + normalizeDeviceId, + withClientNameReservation +} = require('../src/data/client-name-check'); + +test('normalize helpers trim names and sanitize device ids', () => { + assert.equal(normalizeClientName(' Screen A '), 'Screen A'); + assert.equal(normalizeClientName(''), ''); + assert.equal(normalizeDeviceId(' device-01 /abc!? '), 'device-01abc'); + assert.equal(normalizeDeviceId('x'.repeat(200)).length, 128); +}); + +test('collectLiveConnections returns an array safely', () => { + assert.deepEqual(collectLiveConnections(null), []); + assert.deepEqual(collectLiveConnections([{ clientName: 'A' }]), [{ clientName: 'A' }]); +}); + +test('isClientNameAvailable rejects matching db rows and live connections', async () => { + const pool = { + async query(sql, params) { + assert.match(sql, /FROM d_onboarding_devices/); + assert.deepEqual(params, ['Screen A', 'device-01']); + return [[{ device_id: 'device-99' }]]; + } + }; + + assert.equal(await isClientNameAvailable(pool, ' Screen A ', 'device-01', []), false); + assert.equal(await isClientNameAvailable(null, 'Screen A', 'device-01', [{ clientName: 'screen a', clientId: 'device-99' }]), false); + assert.equal(await isClientNameAvailable(null, 'Screen A', 'device-01', [{ clientName: 'screen a', clientId: 'device-01' }]), true); + assert.equal(await isClientNameAvailable(null, ' ', 'device-01', []), false); +}); + +test('withClientNameReservation acquires and releases locks around the handler', async () => { + const calls = []; + const lockName = `ps_client_name_${crypto.createHash('sha1').update('screen a').digest('hex')}`; + const connection = { + async query(sql, params) { + calls.push({ sql, params }); + if (sql.includes('GET_LOCK')) { + return [[{ lock_result: 1 }]]; + } + return [[{ released: 1 }]]; + }, + release() { + calls.push({ sql: 'RELEASE_CONNECTION' }); + } + }; + const pool = { + async getConnection() { + return connection; + } + }; + + const result = await withClientNameReservation(pool, ' Screen A ', async () => 'ok'); + + assert.equal(result, 'ok'); + assert.deepEqual(calls, [ + { sql: 'SELECT GET_LOCK(?, 5) AS lock_result', params: [lockName] }, + { sql: 'SELECT RELEASE_LOCK(?)', params: [lockName] }, + { sql: 'RELEASE_CONNECTION' } + ]); +}); + +test('withClientNameReservation rejects busy names', async () => { + const connection = { + async query(sql) { + if (sql.includes('GET_LOCK')) { + return [[{ lock_result: 0 }]]; + } + throw new Error('unexpected query'); + }, + release() {} + }; + const pool = { + async getConnection() { + return connection; + } + }; + + await assert.rejects( + () => withClientNameReservation(pool, 'Screen A', async () => 'ok'), + (error) => error && error.statusCode === 409 && error.message === 'Client name is busy. Please try again.' + ); +}); \ No newline at end of file diff --git a/test/data-utils.test.js b/test/data-utils.test.js new file mode 100644 index 0000000..b065ff8 --- /dev/null +++ b/test/data-utils.test.js @@ -0,0 +1,105 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const { + buildSearchFilter, + buildSortOrderClause, + fetchPagedRows, + findTopLevelOrderByIndex, + normalizePageNumber, + normalizeSortDirection, + parseJsonSafe, + readFormArray +} = require('../src/data/utils'); + +test('parseJsonSafe returns null for invalid JSON and preserves objects', () => { + assert.equal(parseJsonSafe(''), null); + assert.equal(parseJsonSafe('not-json'), null); + assert.deepEqual(parseJsonSafe('{"value":true}'), { value: true }); + const value = { nested: ['a'] }; + assert.equal(parseJsonSafe(value), value); +}); + +test('readFormArray always returns an array shape', () => { + assert.deepEqual(readFormArray({}, 'roles'), []); + assert.deepEqual(readFormArray({ roles: 'admin' }, 'roles'), ['admin']); + assert.deepEqual(readFormArray({ roles: ['admin', 'editor'] }, 'roles'), ['admin', 'editor']); +}); + +test('normalize helpers clamp pagination and sort direction', () => { + assert.equal(normalizePageNumber('0'), 1); + assert.equal(normalizePageNumber('3.9'), 3); + assert.equal(normalizePageNumber('abc'), 1); + assert.equal(normalizeSortDirection('DESC'), 'desc'); + assert.equal(normalizeSortDirection('anything else'), 'asc'); +}); + +test('buildSortOrderClause applies multiple columns and ignores unknown keys', () => { + assert.deepEqual(buildSortOrderClause({ name: 'title' }, 'name', 'desc'), { + clause: ' ORDER BY title DESC', + sortKey: 'name', + sortDirection: 'desc' + }); + assert.deepEqual(buildSortOrderClause({ name: ['title', 'id'] }, 'name', 'asc'), { + clause: ' ORDER BY title ASC, id ASC', + sortKey: 'name', + sortDirection: 'asc' + }); + assert.deepEqual(buildSortOrderClause({ name: 'title' }, 'missing', 'desc'), { + clause: '', + sortKey: 'missing', + sortDirection: 'desc' + }); +}); + +test('buildSearchFilter generates escaped LIKE clauses', () => { + assert.deepEqual(buildSearchFilter(['name', 'description'], '100%_ready'), { + clause: " WHERE (LOWER(COALESCE(CAST(name AS CHAR), '')) LIKE ? ESCAPE '\\\\' OR LOWER(COALESCE(CAST(description AS CHAR), '')) LIKE ? ESCAPE '\\\\')", + params: ['%100\\%\\_ready%', '%100\\%\\_ready%'] + }); + assert.deepEqual(buildSearchFilter([], 'anything'), { clause: '', params: [] }); +}); + +test('findTopLevelOrderByIndex skips nested subqueries', () => { + const sql = 'SELECT * FROM (SELECT * FROM items ORDER BY created_at DESC) AS nested ORDER BY id ASC'; + assert.equal(findTopLevelOrderByIndex(sql), sql.lastIndexOf('ORDER BY id ASC')); +}); + +test('fetchPagedRows combines search, ordering, and pagination parameters', async () => { + const queries = []; + const pool = { + async query(sql, params) { + queries.push({ sql, params }); + if (sql.startsWith('SELECT COUNT(*)')) { + return [[{ count: 3 }]]; + } + return [[{ id: 1 }]]; + } + }; + + const result = await fetchPagedRows(pool, { + selectSql: 'SELECT id, name FROM items WHERE active = 1 ORDER BY name ASC', + countSql: 'SELECT COUNT(*) AS count FROM items WHERE active = 1', + params: ['active'], + searchColumns: ['name'], + searchTerm: 'alpha', + sortColumns: { name: 'name' }, + sortKey: 'name', + sortDirection: 'desc', + pageSize: 2, + page: 2 + }); + + assert.deepEqual(result, { + rows: [{ id: 1 }], + totalItems: 3, + totalPages: 2, + currentPage: 2, + pageSize: 2 + }); + assert.equal(queries.length, 2); + assert.equal(queries[0].sql, "SELECT COUNT(*) AS count FROM (SELECT id, name FROM items WHERE active = 1 AND (LOWER(COALESCE(CAST(name AS CHAR), '')) LIKE ? ESCAPE '\\\\')) AS filtered_rows"); + assert.deepEqual(queries[0].params, ['active', '%alpha%']); + assert.equal(queries[1].sql, "SELECT id, name FROM items WHERE active = 1 AND (LOWER(COALESCE(CAST(name AS CHAR), '')) LIKE ? ESCAPE '\\\\') ORDER BY name DESC LIMIT ? OFFSET ?"); + assert.deepEqual(queries[1].params, ['active', '%alpha%', 2, 2]); +}); \ No newline at end of file diff --git a/test/player-playlist.test.js b/test/player-playlist.test.js new file mode 100644 index 0000000..f2d0472 --- /dev/null +++ b/test/player-playlist.test.js @@ -0,0 +1,148 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); + +const { createPlayerPlaylistService } = require('../src/player/playlist'); + +function createFsStub() { + const original = { + readFile: fs.promises.readFile, + writeFile: fs.promises.writeFile, + mkdir: fs.promises.mkdir + }; + const calls = []; + + fs.promises.readFile = async function (filePath) { + calls.push({ method: 'readFile', filePath }); + if (filePath.endsWith('fallback.json')) { + return JSON.stringify({ fromSnapshot: true, screen: { slug: 'test2' } }); + } + const error = new Error('missing'); + error.code = 'ENOENT'; + throw error; + }; + + fs.promises.writeFile = async function (filePath, content) { + calls.push({ method: 'writeFile', filePath, content }); + }; + + fs.promises.mkdir = async function (dirPath, options) { + calls.push({ method: 'mkdir', dirPath, options }); + }; + + return { + calls, + restore() { + fs.promises.readFile = original.readFile; + fs.promises.writeFile = original.writeFile; + fs.promises.mkdir = original.mkdir; + } + }; +} + +test('buildScreenPlaylist assembles slides, templates, and derived values', async () => { + const pool = { + async query(sql, params) { + if (sql.includes('FROM d_screens')) { + assert.deepEqual(params, ['test2']); + return [[{ id: 7, name: 'Screen 7', slug: 'test2', playlist_id: 22, modified_at: '2026-08-03T00:00:00.000Z' }]]; + } + if (sql.includes('FROM c_playlists')) { + assert.deepEqual(params, [22]); + return [[{ id: 22, name: 'Playlist 22', fade_between_slides: 1, skip_unavailable_rtmp: 0, modified_at: '2026-08-02T00:00:00.000Z' }]]; + } + if (sql.includes('FROM c_playlist_slides ps')) { + return [[{ + id: 101, + title: 'Intro', + template_id: 33, + content_json: '{"videoRegion":{"type":"video","duration_seconds":12.3456},"textRegion":{"type":"text","value":"Hello"}}', + modified_at: '2026-08-03T00:00:01.000Z', + position: 1, + duration_seconds: 9, + use_video_duration: 1, + disable_audio: null, + template_name: 'Template 33', + canvas_size_name: 'HD', + canvas_size_width: 1920, + canvas_size_height: 1080, + canvas_width: 1920, + canvas_height: 1080 + }]]; + } + if (sql.includes('FROM c_playlist_slide_schedule_rules')) { + return [[ + { id: 500, playlist_slide_id: 101, position: 1, start_datetime: null, end_datetime: null, start_time: '08:00', end_time: '12:00', schedule_days_json: '[1,2,3]' }, + { id: 501, playlist_slide_id: 101, position: 2, start_datetime: null, end_datetime: null, start_time: '13:00', end_time: '17:00', schedule_days_json: '[4,5]' } + ]]; + } + if (sql.includes('FROM c_templates st')) { + assert.deepEqual(params, [[33]]); + return [[{ id: 33, name: 'Template 33', canvas_size_id: 4, canvas_size_width: 1920, canvas_size_height: 1080, background_image_path: '/media/bg.png', background_color: '#111111', modified_at: '2026-08-03T00:00:02.000Z' }]]; + } + if (sql.includes('FROM c_template_regions')) { + assert.deepEqual(params, [[33]]); + return [[{ id: 900, template_id: 33, region_key: 'textRegion', region_type: 'text', label: 'Text Region', font_family: 'Arial', x: 10, y: 20, width: 300, height: 200, z_index: 1, modified_at: '2026-08-03T00:00:03.000Z' }]]; + } + throw new Error(`unexpected query: ${sql}`); + } + }; + const common = { + parseJsonSafe(value) { + return JSON.parse(value); + }, + fetchRssFeedsData: async () => ({ rssFeeds: [{ id: 1, title: 'Feed 1' }] }), + fetchRssFeedItemsByFeedId: async () => ([{ title: 'Item 1', link: 'https://example.com' }]), + normalizeRssFeedItem(item) { + return Object.assign({}, item, { normalized: true }); + }, + fetchApiSourcesData: async () => ({ apiSources: [{ id: 2, name: 'Source 2', last_response_json: '{"ok":true}' }] }), + fetchTimetablesData: async () => ({ timetableGroups: [{ id: 3, name: 'Group 3' }] }) + }; + const fsStub = createFsStub(); + + try { + const service = createPlayerPlaylistService({ pool, common, snapshotDir: 'C:\\tmp\\snapshots' }); + const payload = await service.buildScreenPlaylist('test2'); + + assert.equal(payload.screen.slug, 'test2'); + assert.equal(payload.playlist.name, 'Playlist 22'); + assert.equal(payload.slides[0].duration_seconds, 12.346); + assert.equal(payload.slides[0].disable_audio, true); + assert.equal(payload.slides[0].content.videoRegion.disable_audio, true); + assert.equal(payload.slides[0].content.videoRegion.cache_bust, '2026-08-03T00:00:01.000Z'); + assert.equal(payload.slides[0].scheduleRules.length, 2); + assert.equal(payload.slides[0].template.regions[0].label, 'Text Region'); + assert.equal(payload.rssFeeds[0].items[0].normalized, true); + assert.deepEqual(payload.apiSources[0].responseJson, { ok: true }); + assert.deepEqual(payload.timetableGroups, [{ id: 3, name: 'Group 3' }]); + assert.match(payload.revision, /^[a-f0-9]{40}$/); + assert.ok(fsStub.calls.some((call) => call.method === 'writeFile')); + } finally { + fsStub.restore(); + } +}); + +test('buildScreenPlaylist falls back to a snapshot when queries fail', async () => { + const pool = { + async query() { + throw new Error('database unavailable'); + } + }; + const common = { + parseJsonSafe() { + throw new Error('not expected'); + } + }; + const fsStub = createFsStub(); + + try { + const service = createPlayerPlaylistService({ pool, common, snapshotDir: 'C:\\tmp\\snapshots' }); + const payload = await service.buildScreenPlaylist('fallback'); + + assert.deepEqual(payload, { fromSnapshot: true, screen: { slug: 'test2' } }); + assert.ok(fsStub.calls.some((call) => call.method === 'readFile')); + } finally { + fsStub.restore(); + } +}); \ No newline at end of file diff --git a/test/player-render-helpers.test.js b/test/player-render-helpers.test.js new file mode 100644 index 0000000..a10a8cb --- /dev/null +++ b/test/player-render-helpers.test.js @@ -0,0 +1,81 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); + +require('../src/common'); + +const { + mediaKind, + normalizeSlide, + renderEditorJsContent, + sanitizeRichText +} = require('../src/player/render-helpers'); + +test('mediaKind classifies player media by extension', () => { + assert.equal(mediaKind('poster.PNG'), 'image'); + assert.equal(mediaKind('intro.mp4'), 'video'); + assert.equal(mediaKind('manual.pdf'), 'pdf'); + assert.equal(mediaKind('notes.txt'), 'file'); +}); + +test('sanitizeRichText strips unsafe content but preserves allowed markup', () => { + const html = '
LinkText
'; + + assert.equal( + sanitizeRichText(html), + '
LinkText
' + ); +}); + +test('normalizeSlide normalizes nested content without mutating the source', () => { + const slide = { + id: 12, + content: { + hero: { + value: '{"headline":"Hello"}', + font_family: ' Open Sans! ', + font_size: '42', + font_color: 'not-a-color', + type: 'text' + }, + footer: 'plain text' + } + }; + + const normalized = normalizeSlide(slide); + + assert.notEqual(normalized.content, slide.content); + assert.deepEqual(normalized.content.hero, { + value: { headline: 'Hello' }, + font_family: ' Open Sans! ', + font_size: '42', + font_color: 'not-a-color', + type: 'text' + }); + assert.deepEqual(normalized.content.footer, { + type: 'text', + value: 'plain text' + }); + assert.deepEqual(slide.content.hero, { + value: '{"headline":"Hello"}', + font_family: ' Open Sans! ', + font_size: '42', + font_color: 'not-a-color', + type: 'text' + }); +}); + +test('renderEditorJsContent sanitizes editor blocks and wraps legacy text', () => { + const editorJson = { + blocks: [ + { type: 'header', data: { level: 2, text: 'Title' } }, + { type: 'paragraph', data: { text: 'badok' } }, + { type: 'list', data: { style: 'ordered', items: ['One', { text: 'Two' }] } } + ] + }; + + assert.equal( + renderEditorJsContent(editorJson), + '

Title

badok

  1. One
  2. Two
' + ); + assert.equal(renderEditorJsContent('plain text'), '

plain text

'); +}); \ No newline at end of file diff --git a/test/player-routes.test.js b/test/player-routes.test.js new file mode 100644 index 0000000..70768c5 --- /dev/null +++ b/test/player-routes.test.js @@ -0,0 +1,396 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); + +require('../src/common'); + +const { registerPlayerRoutes } = require('../src/player/routes'); + +function createAppAndHandlers() { + const handlers = {}; + const app = { + use() {}, + get(path, ...routeHandlers) { + handlers[path] = function (req, res) { + return new Promise((resolve, reject) => { + const runHandler = (index) => { + const handler = routeHandlers[index]; + if (!handler) { + resolve(); + return; + } + + let nextCalled = false; + const next = function () { + nextCalled = true; + return runHandler(index + 1); + }; + + let result; + try { + result = handler(req, res, next); + } catch (error) { + reject(error); + return; + } + + Promise.resolve(result).then(function (value) { + if (!nextCalled) { + resolve(value); + } + }, reject); + }; + + runHandler(0); + }); + }; + }, + post() {}, + put() {}, + delete() {} + }; + + return { app, handlers }; +} + +function createResponse() { + const headers = {}; + return { + headers, + statusCode: 200, + body: null, + ended: false, + set(name, value) { + headers[name] = value; + return this; + }, + status(code) { + this.statusCode = code; + return this; + }, + json(value) { + this.body = value; + return this; + }, + send(value) { + this.body = value; + return this; + }, + end() { + this.ended = true; + return this; + } + }; +} + +function createPlayerRouteOptions(overrides) { + return Object.assign({ + mediaDir: 'e:\\Projects Git\\pulse-signage\\media', + assetDir: 'e:\\Projects Git\\pulse-signage\\src\\player\\public', + playerRuntime: { broadcastAnnouncementRefresh() {}, snapshotConnections() { return []; } }, + playerPlaylistService: { async buildScreenPlaylist() { return { screen: { slug: 'test2' } }; } }, + rtmpStreamService: { + async getSessionStatus() { return { ready: false, live: false, session: {} }; }, + async getManifestFilePath() { return null; }, + async getSegmentFilePath() { return null; } + }, + playerIdentifier: '' + }, overrides); +} + +test('screen route falls back to offline rendering when playlist build fails', async () => { + const { app, handlers } = createAppAndHandlers(); + const renderCalls = []; + const originalConsoleError = console.error; + console.error = () => {}; + const pool = { + async query() { + return [[{ id: 1 }]]; + }, + async getConnection() { + return { + async beginTransaction() {}, + async query() {}, + async commit() {}, + async rollback() {}, + release() {} + }; + } + }; + const common = { + renderPlayerPage(slug, data) { + renderCalls.push({ slug, data }); + return data ? 'online' : 'offline'; + } + }; + + registerPlayerRoutes(app, { + app, + pool, + common, + ...createPlayerRouteOptions({ + playerPlaylistService: { + async buildScreenPlaylist() { + const error = new Error('db unavailable'); + error.code = 'ECONNREFUSED'; + throw error; + } + } + }) + }); + + const handler = handlers['/screen/:slug']; + assert.equal(typeof handler, 'function'); + + const res = createResponse(); + + try { + await handler({ params: { slug: 'test2' } }, res); + await new Promise((resolve) => setImmediate(resolve)); + + assert.equal(res.headers['X-Player-Offline'], '1'); + assert.equal(res.body, 'offline'); + assert.deepEqual(renderCalls, [ + { slug: 'test2', data: null } + ]); + } finally { + console.error = originalConsoleError; + } +}); + +test('screen route renders the shell when playlist data is missing', async () => { + const { app, handlers } = createAppAndHandlers(); + const renderCalls = []; + const pool = { + async query() { + return [[{ id: 1 }]]; + }, + async getConnection() { + return { + async beginTransaction() {}, + async query() {}, + async commit() {}, + async rollback() {}, + release() {} + }; + } + }; + const common = { + renderPlayerPage(slug, data) { + renderCalls.push({ slug, data }); + return '
Loading screen...
'; + } + }; + + registerPlayerRoutes(app, { + app, + pool, + common, + ...createPlayerRouteOptions({ + playerPlaylistService: { + async buildScreenPlaylist() { + return { + screen: { id: 7, slug: 'test2' }, + playlist: null, + slides: [], + rssFeeds: [], + apiSources: [], + timetableGroups: [], + revision: 'abc123' + }; + } + } + }) + }); + + const handler = handlers['/screen/:slug']; + const res = createResponse(); + + await handler({ params: { slug: 'test2' } }, res); + + assert.equal(res.statusCode, 200); + assert.match(String(res.body), /Loading screen/); + assert.deepEqual(renderCalls, [ + { + slug: 'test2', + data: { + screen: { id: 7, slug: 'test2' }, + playlist: null, + slides: [], + rssFeeds: [], + apiSources: [], + timetableGroups: [], + revision: 'abc123' + } + } + ]); +}); + +test('playlist api route returns 404, etag, and 304 responses', async () => { + const { app, handlers } = createAppAndHandlers(); + const playlistData = { + screen: { id: 7, slug: 'test2' }, + playlist: { id: 22 }, + slides: [], + rssFeeds: [], + apiSources: [], + timetableGroups: [], + revision: 'rev-123' + }; + + registerPlayerRoutes(app, { + app, + pool: { async query() { return [[{ id: 1 }]]; } }, + common: { renderPlayerPage() { return ''; } }, + ...createPlayerRouteOptions({ + playerPlaylistService: { + async buildScreenPlaylist(slug) { + if (slug === 'missing') { + return { screen: null, playlist: null, slides: [], rssFeeds: [], apiSources: [], timetableGroups: [], revision: 'none' }; + } + return playlistData; + } + } + }) + }); + + const handler = handlers['/api/screens/:slug/playlist']; + assert.equal(typeof handler, 'function'); + + const missingRes = createResponse(); + await handler({ params: { slug: 'missing' }, headers: {} }, missingRes); + assert.equal(missingRes.statusCode, 404); + assert.deepEqual(missingRes.body, { error: 'Screen not found' }); + + const okRes = createResponse(); + await handler({ params: { slug: 'test2' }, headers: {} }, okRes); + assert.equal(okRes.statusCode, 200); + assert.equal(okRes.headers.ETag, '"rev-123"'); + assert.deepEqual(okRes.body, playlistData); + + const notModifiedRes = createResponse(); + await handler({ params: { slug: 'test2' }, headers: { 'if-none-match': '"rev-123"' } }, notModifiedRes); + assert.equal(notModifiedRes.statusCode, 304); + assert.equal(notModifiedRes.ended, true); +}); + +test('announcement route returns 503 for transient failures and 304 on matching etag', async () => { + const { app, handlers } = createAppAndHandlers(); + const announcement = { + id: 9, + modified_at: '2026-08-03T00:00:00.000Z', + expires_at: '2026-08-04T00:00:00.000Z', + enabled: true + }; + + registerPlayerRoutes(app, { + app, + pool: { async query() { return [[{ id: 1 }]]; } }, + common: { + async fetchActiveAnnouncement() { + const error = new Error('db unavailable'); + error.code = 'ECONNREFUSED'; + throw error; + }, + renderPlayerPage() { return ''; } + }, + ...createPlayerRouteOptions({ + rtmpStreamService: { + async getSessionStatus() { return { ready: false, live: false, session: {} }; }, + async getManifestFilePath() { return null; }, + async getSegmentFilePath() { return null; } + } + }) + }); + + const handler = handlers['/api/screens/:slug/announcement']; + const unavailableRes = createResponse(); + await handler({ params: { slug: 'test2' }, headers: {} }, unavailableRes); + assert.equal(unavailableRes.statusCode, 503); + assert.deepEqual(unavailableRes.body, { error: 'Announcement state unavailable.' }); + + registerPlayerRoutes(app, { + app, + pool: { async query() { return [[{ id: 1 }]]; } }, + common: { + async fetchActiveAnnouncement() { + return announcement; + }, + renderPlayerPage() { return ''; } + }, + ...createPlayerRouteOptions({ + rtmpStreamService: { + async getSessionStatus() { return { ready: false, live: false, session: {} }; }, + async getManifestFilePath() { return null; }, + async getSegmentFilePath() { return null; } + } + }) + }); + + const okHandler = handlers['/api/screens/:slug/announcement']; + const okRes = createResponse(); + await okHandler({ params: { slug: 'test2' }, headers: {} }, okRes); + assert.equal(okRes.statusCode, 200); + assert.equal(okRes.headers.ETag, '"9:2026-08-03T00:00:00.000Z:2026-08-04T00:00:00.000Z:1"'); + assert.deepEqual(okRes.body, { announcement, revision: '9:2026-08-03T00:00:00.000Z:2026-08-04T00:00:00.000Z:1' }); + + const notModifiedRes = createResponse(); + await okHandler({ params: { slug: 'test2' }, headers: { 'if-none-match': '"9:2026-08-03T00:00:00.000Z:2026-08-04T00:00:00.000Z:1"' } }, notModifiedRes); + assert.equal(notModifiedRes.statusCode, 304); + assert.equal(notModifiedRes.ended, true); +}); + +test('rtmp session route returns not-ready and ready payloads', async () => { + const { app, handlers } = createAppAndHandlers(); + const sessionCalls = []; + + registerPlayerRoutes(app, { + app, + pool: { async query() { return [[{ id: 1 }]]; } }, + common: { renderPlayerPage() { return ''; } }, + ...createPlayerRouteOptions({ + rtmpStreamService: { + async getSessionStatus(source, useMutedOutput) { + sessionCalls.push({ source, useMutedOutput }); + if (source === 'ready') { + return { + ready: true, + live: true, + session: { key: 'abc', playlistUrl: 'http://example.com/live.m3u8', disableAudio: useMutedOutput } + }; + } + return { + ready: false, + live: false, + timedOut: true, + stderr: 'starting', + session: {} + }; + }, + async getManifestFilePath() { return null; }, + async getSegmentFilePath() { return null; } + } + }) + }); + + const handler = handlers['/api/rtmp/session']; + + const unavailableRes = createResponse(); + await handler({ headers: {}, query: { source: 'cold', disableAudio: 'true' } }, unavailableRes); + assert.equal(unavailableRes.statusCode, 503); + assert.deepEqual(unavailableRes.body, { ready: false, live: false, timedOut: true, stderr: 'starting' }); + + const readyRes = createResponse(); + await handler({ headers: {}, query: { source: 'ready', disableAudio: 'yes' } }, readyRes); + assert.equal(readyRes.statusCode, 200); + assert.deepEqual(readyRes.body, { + key: 'abc', + playlistUrl: 'http://example.com/live.m3u8', + disableAudio: true, + ready: true, + live: true + }); + assert.deepEqual(sessionCalls, [ + { source: 'cold', useMutedOutput: true }, + { source: 'ready', useMutedOutput: true } + ]); +}); \ No newline at end of file diff --git a/test/player-runtime.test.js b/test/player-runtime.test.js new file mode 100644 index 0000000..623c6c9 --- /dev/null +++ b/test/player-runtime.test.js @@ -0,0 +1,156 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const http = require('http'); +const WebSocket = require('ws'); + +require('../src/common'); + +const { createPageAuthToken } = require('../src/request-auth'); +const { createPlayerRuntime } = require('../src/player/runtime'); + +const originalSecret = process.env.PULSE_SIGNAGE_SHARED_SECRET; + +test.after(() => { + if (originalSecret === undefined) { + delete process.env.PULSE_SIGNAGE_SHARED_SECRET; + } else { + process.env.PULSE_SIGNAGE_SHARED_SECRET = originalSecret; + } +}); + +function waitFor(predicate, timeoutMs = 1000) { + const start = Date.now(); + return new Promise((resolve, reject) => { + const tick = () => { + const value = predicate(); + if (value) { + return resolve(value); + } + if (Date.now() - start > timeoutMs) { + return reject(new Error('Timed out waiting for runtime state.')); + } + setTimeout(tick, 25); + }; + tick(); + }); +} + +test('player runtime snapshots websocket state and checks live names', async () => { + process.env.PULSE_SIGNAGE_SHARED_SECRET = 'runtime-secret'; + + const runtime = createPlayerRuntime({ pool: null }); + const server = http.createServer(); + runtime.installWebsocket(server); + + await new Promise((resolve) => server.listen(0, resolve)); + const { port } = server.address(); + const token = createPageAuthToken({ scope: 'player', slug: 'test2' }); + const client = new WebSocket(`ws://127.0.0.1:${port}/ws/screens/test2?auth=${encodeURIComponent(token)}`); + + try { + await new Promise((resolve, reject) => { + client.once('open', resolve); + client.once('error', reject); + }); + + client.send(JSON.stringify({ + type: 'state', + clientId: 'client-1', + clientName: ' Lobby Player ', + deviceId: 'device 123!?', + userAgent: 'Mozilla/5.0 (unit test)', + viewport: { width: 1920, height: 1080 }, + page: 'http://localhost:8081/screen/test2', + paused: true, + blackout: false, + currentSlide: { id: 9, title: 'Intro', kind: 'slide', playlistSignature: 'sig' } + })); + + await waitFor(() => { + const snapshot = runtime.snapshotConnections('test2')[0]; + return snapshot && snapshot.clientName === 'Lobby Player' ? snapshot : null; + }); + const snapshot = runtime.snapshotConnections('test2')[0]; + + assert.equal(snapshot.clientName, 'Lobby Player'); + assert.equal(snapshot.deviceId, 'device123'); + assert.match(snapshot.label, /Lobby Player/); + assert.equal(snapshot.paused, true); + assert.equal(snapshot.currentSlideId, 9); + assert.equal(snapshot.currentSlideTitle, 'Intro'); + assert.equal(await runtime.isClientNameAvailableOnScreen(null, 'Lobby Player', 'other-device'), false); + assert.equal(await runtime.isClientNameAvailableOnScreen(null, 'Lobby Player', 'device123'), true); + assert.equal(await runtime.isClientNameAvailableOnScreen(null, 'Other Name', 'device123'), true); + } finally { + client.close(); + await new Promise((resolve) => server.close(resolve)); + } +}); + +test('player runtime sends targeted and broadcast commands to live sockets', async () => { + process.env.PULSE_SIGNAGE_SHARED_SECRET = 'runtime-secret'; + + const runtime = createPlayerRuntime({ pool: null }); + const server = http.createServer(); + runtime.installWebsocket(server); + + await new Promise((resolve) => server.listen(0, resolve)); + const { port } = server.address(); + const token = createPageAuthToken({ scope: 'player', slug: 'test2' }); + const clientA = new WebSocket(`ws://127.0.0.1:${port}/ws/screens/test2?auth=${encodeURIComponent(token)}`); + const clientB = new WebSocket(`ws://127.0.0.1:${port}/ws/screens/test2?auth=${encodeURIComponent(token)}`); + + function openClient(client) { + return new Promise((resolve, reject) => { + client.once('open', resolve); + client.once('error', reject); + }); + } + + function waitForMessage(client) { + return new Promise((resolve) => { + client.once('message', (raw) => { + resolve(JSON.parse(String(raw))); + }); + }); + } + + try { + await Promise.all([openClient(clientA), openClient(clientB)]); + + clientA.send(JSON.stringify({ type: 'state', clientId: 'client-a', clientName: 'Alpha', deviceId: 'device-a', page: 'http://localhost:8081/screen/test2' })); + clientB.send(JSON.stringify({ type: 'state', clientId: 'client-b', clientName: 'Beta', deviceId: 'device-b', page: 'http://localhost:8081/screen/test2' })); + + await waitFor(() => { + const connections = runtime.snapshotConnections('test2'); + return connections.length === 2 && connections.every((connection) => connection.clientName); + }); + const [firstConnection, secondConnection] = runtime.snapshotConnections('test2'); + + const targetedMessagePromise = waitForMessage(clientA); + const targetedCount = await runtime.sendCommandToConnection('test2', firstConnection.id, { action: 'pause' }); + assert.equal(targetedCount, 1); + const targetedMessage = await targetedMessagePromise; + assert.equal(targetedMessage.type, 'command'); + assert.equal(targetedMessage.action, 'pause'); + assert.equal(targetedMessage.targetConnectionId, firstConnection.id); + + const broadcastPromises = [waitForMessage(clientA), waitForMessage(clientB)]; + const broadcastCount = await runtime.broadcastCommand('test2', 'resume'); + assert.equal(broadcastCount, 2); + const [broadcastA, broadcastB] = await Promise.all(broadcastPromises); + assert.equal(broadcastA.type, 'command'); + assert.equal(broadcastA.command, 'resume'); + assert.equal(broadcastB.type, 'command'); + assert.equal(broadcastB.command, 'resume'); + assert.equal(broadcastA.targetConnectionId, undefined); + assert.equal(broadcastB.targetConnectionId, undefined); + + assert.equal(await runtime.sendCommandToConnection('test2', 'missing', 'pause'), 0); + assert.equal(secondConnection.clientName, 'Beta'); + } finally { + clientA.close(); + clientB.close(); + await new Promise((resolve) => server.close(resolve)); + } +}); \ No newline at end of file diff --git a/test/request-auth.test.js b/test/request-auth.test.js new file mode 100644 index 0000000..6e9a0ac --- /dev/null +++ b/test/request-auth.test.js @@ -0,0 +1,103 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const { + createPageAuthBundle, + createPageAuthToken, + createPageFetchAuthScript, + createRequestAuthHeaders, + verifyPageAuthToken, + verifyRequestAuth, + PAGE_TOKEN_HEADER, + REQUEST_SIGNATURE_HEADER, + REQUEST_TIMESTAMP_HEADER +} = require('../src/request-auth'); + +const originalSecret = process.env.PULSE_SIGNAGE_SHARED_SECRET; + +function setSecret(value) { + process.env.PULSE_SIGNAGE_SHARED_SECRET = value; +} + +test.after(() => { + if (originalSecret === undefined) { + delete process.env.PULSE_SIGNAGE_SHARED_SECRET; + } else { + process.env.PULSE_SIGNAGE_SHARED_SECRET = originalSecret; + } +}); + +test('request auth headers verify empty parser bodies as null payloads', () => { + setSecret('test-secret'); + + const originalNow = Date.now; + Date.now = () => 1700000000000; + try { + const headers = createRequestAuthHeaders({ method: 'POST', pathname: '/api/media/config', body: null, timestamp: 1700000000000 }); + const req = { + method: 'POST', + path: '/api/media/config', + body: {}, + headers: { + [REQUEST_TIMESTAMP_HEADER]: headers[REQUEST_TIMESTAMP_HEADER], + [REQUEST_SIGNATURE_HEADER]: headers[REQUEST_SIGNATURE_HEADER] + } + }; + + assert.equal(verifyRequestAuth(req), true); + } finally { + Date.now = originalNow; + } +}); + +test('page auth bundles round-trip and expire as expected', () => { + setSecret('test-secret'); + + const originalNow = Date.now; + Date.now = () => 1700000000000; + try { + const bundle = createPageAuthBundle({ scope: 'player', slug: 'test2' }); + assert.equal(typeof bundle.token, 'string'); + assert.equal(bundle.issuedAt, 1700000000000); + assert.equal(bundle.expiresAt, 1700043200000); + assert.deepEqual(verifyPageAuthToken(bundle.token), { + scope: 'player', + slug: 'test2', + issuedAt: 1700000000000, + expiresAt: 1700043200000 + }); + assert.equal(createPageAuthToken({ scope: 'player' }).split('.').length, 2); + + Date.now = () => 1700043200001; + assert.equal(verifyPageAuthToken(bundle.token), null); + } finally { + Date.now = originalNow; + } +}); + +test('request auth rejects mismatched signatures', () => { + setSecret('test-secret'); + + const headers = createRequestAuthHeaders({ method: 'PUT', pathname: '/api/media/config', body: { hello: 'world' }, timestamp: 1700000000000 }); + const req = { + method: 'PUT', + path: '/api/media/config', + body: { hello: 'changed' }, + headers: { + [REQUEST_TIMESTAMP_HEADER]: headers[REQUEST_TIMESTAMP_HEADER], + [REQUEST_SIGNATURE_HEADER]: headers[REQUEST_SIGNATURE_HEADER] + } + }; + + assert.equal(verifyRequestAuth(req), false); + assert.equal(PAGE_TOKEN_HEADER, 'x-pulse-page-auth'); +}); + +test('page fetch auth script injects renew and header logic', () => { + const script = createPageFetchAuthScript({ token: 'abc123', expiresAt: 1700000000000 }); + + assert.match(script, /window\.__pulsePageAuthToken = pageAuthToken/); + assert.match(script, /"x-pulse-page-auth"/); + assert.match(script, /\/api\/auth\/page/); + assert.match(script, /abc123/); +}); \ No newline at end of file diff --git a/test/templates.test.js b/test/templates.test.js new file mode 100644 index 0000000..11bb50a --- /dev/null +++ b/test/templates.test.js @@ -0,0 +1,109 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const { + buildTemplatePayload, + extractTemplateRegions +} = require('../src/data/templates'); + +test('extractTemplateRegions normalizes JSON regions and filters invalid rows', () => { + const regions = extractTemplateRegions({ + regions_json: JSON.stringify([ + { + region_name: 'Hero', + region_type: 'TEXT', + lock_ratio: '16 : 9', + animation_json: '{"intro":{"preset":"fadeIn"},"out":{"preset":"bounceOut"}}', + x: '10', + y: '20', + width: '320', + height: '180', + z_index: '2' + }, + { + region_name: ' ', + region_type: 'image' + } + ]) + }); + + assert.deepEqual(regions, [{ + region_key: 'Hero', + region_type: 'TEXT', + label: 'Hero', + lock_ratio: '16:9', + animation_json: { + intro: { preset: 'fadeIn' }, + outro: { preset: 'bounceOut' }, + loop: { preset: 'none' } + }, + x: 10, + y: 20, + width: 320, + height: 180, + z_index: 2 + }]); +}); + +test('buildTemplatePayload resolves canvas size and rejects duplicate region names', async () => { + const pool = { + async query(sql, params) { + if (sql.includes('FROM c_canvas_sizes WHERE id = ?')) { + assert.deepEqual(params, [4]); + return [[{ id: 4, name: 'HD', width: 1280, height: 720 }]]; + } + throw new Error(`unexpected query: ${sql}`); + } + }; + + const payload = await buildTemplatePayload(pool, { + body: { + name: ' Main Template ', + canvas_size_id: '4', + background_color: 'not-a-color', + regions_json: JSON.stringify([ + { region_name: 'Header', region_type: 'text', lock_ratio: '4:3' } + ]) + }, + files: [] + }, null); + + assert.deepEqual(payload, { + name: 'Main Template', + canvasSizeId: 4, + canvasSizeWidth: 1280, + canvasSizeHeight: 720, + backgroundImagePath: null, + backgroundColor: '#111111', + regions: [{ + region_key: 'Header', + region_type: 'text', + label: 'Header', + lock_ratio: '4:3', + animation_json: { + intro: { preset: 'none' }, + outro: { preset: 'none' }, + loop: { preset: 'none' } + }, + x: 0, + y: 0, + width: 100, + height: 100, + z_index: 0 + }] + }); + + await assert.rejects( + () => buildTemplatePayload(pool, { + body: { + name: 'Dupes', + regions_json: JSON.stringify([ + { region_name: 'One' }, + { region_name: 'one' } + ]) + }, + files: [] + }, null), + (error) => error && error.statusCode === 400 && error.message === 'Region names must be unique on this template.' + ); +}); \ No newline at end of file diff --git a/test/web-manage-routes.test.js b/test/web-manage-routes.test.js new file mode 100644 index 0000000..d09d10d --- /dev/null +++ b/test/web-manage-routes.test.js @@ -0,0 +1,96 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const registerManageRoutes = require('../src/web/routes/admin/manage'); + +test('screen update redirects and forwards redirect when the slug changes', async () => { + const handlers = {}; + const app = { + post(path, ...routeHandlers) { + handlers[path] = routeHandlers; + }, + get() {} + }; + + const pool = { + async query(sql) { + if (sql.includes('SELECT slug FROM d_screens ORDER BY slug ASC')) { + return [[{ slug: 'alpha' }, { slug: 'beta' }]]; + } + if (sql.includes('SELECT id, name, slug, playlist_id')) { + return [[{ id: 42, name: 'Old Screen', slug: 'alpha', playlist_id: null }]]; + } + if (sql.includes('UPDATE d_screens SET name = ?, slug = ?, playlist_id = ?, player_id = ?, modified_by = ? WHERE id = ?')) { + return [{ affectedRows: 1 }]; + } + return [[]]; + } + }; + + const calls = []; + const pages = { renderScreenFormPage() {}, renderScreenEditPage() {} }; + const deps = { + pool, + common: { + slugify(value) { return String(value || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); }, + uniqueScreenSlug: async () => 'beta', + fetchDuplicateName: async () => null, + fetchScreenById: async () => ({ id: 42, name: 'Old Screen', slug: 'alpha', playlist_id: null }), + fetchScreenPlayerRecord: async () => ({ public_base_url: 'http://player.local' }) + }, + pages, + getAuditUserId() { return 7; }, + redirectAfterSave(req, res, url) { + calls.push({ kind: 'redirectAfterSave', url }); + res.redirectedTo = url; + }, + notifyPlayerScreens: async (slugs, payload) => { + calls.push({ kind: 'notifyPlayerScreens', slugs, payload }); + return 1; + }, + broadcastDashboardState: async () => { + calls.push({ kind: 'broadcastDashboardState' }); + }, + getScreenDeleteBlockMessage: async () => '', + getScreenConnections: async () => [], + forwardPlayerCommand: async (slug, payload) => { + calls.push({ kind: 'forwardPlayerCommand', slug, payload }); + return { ok: true }; + }, + playerPublicBaseUrl: 'http://player.example', + requirePermission() { + return function (_req, _res, next) { + next(); + }; + } + }; + + registerManageRoutes(app, deps); + + const routeHandlers = handlers['/screens/:id']; + assert.equal(Array.isArray(routeHandlers), true); + + const req = { + params: { id: '42' }, + body: { name: 'Updated Screen', slug: 'beta' } + }; + const res = { + redirect(url) { + this.redirectedTo = url; + }, + status() { + return this; + }, + send() { + return this; + } + }; + + await routeHandlers[1](req, res, () => {}); + + assert.equal(res.redirectedTo, '/screens?edit=42'); + assert.deepEqual(calls, [ + { kind: 'forwardPlayerCommand', slug: 'alpha', payload: { command: 'redirect', url: 'http://player.local/screen/beta' } }, + { kind: 'redirectAfterSave', url: '/screens?edit=42' } + ]); +}); \ No newline at end of file