| 1 | (function () { |
| 2 | 'use strict'; |
| 3 | // @ohmyppt-index-runtim:arcsin1:v2.0.19 |
| 4 | |
| 5 | var pages = JSON.parse(document.getElementById('pages-data')?.textContent || '[]'); |
| 6 | var deckMetadata = (function () { |
| 7 | try { |
| 8 | return JSON.parse(document.getElementById('deck-metadata')?.textContent || '{}'); |
| 9 | } catch (_) { |
| 10 | return {}; |
| 11 | } |
| 12 | })(); |
| 13 | var slideWidth = Number(deckMetadata.width) > 0 ? Number(deckMetadata.width) : 1600; |
| 14 | var slideHeight = Number(deckMetadata.height) > 0 ? Number(deckMetadata.height) : 900; |
| 15 | var frameViewport = document.getElementById('frameViewport'); |
| 16 | var thumbs = document.getElementById('thumbs'); |
| 17 | var deckSwitcher = document.getElementById('deckSwitcher'); |
| 18 | var indicator = document.getElementById('indicator'); |
| 19 | var prevBtn = document.getElementById('prevBtn'); |
| 20 | var nextBtn = document.getElementById('nextBtn'); |
| 21 | var tabsBtn = document.getElementById('tabsBtn'); |
| 22 | var presentBtn = document.getElementById('presentBtn'); |
| 23 | var fullscreenBtn = document.getElementById('fullscreenBtn'); |
| 24 | var search = new URLSearchParams(window.location.search); |
| 25 | var embedMode = search.get('embed') === '1'; |
| 26 | var presentMode = search.get('present') === '1'; |
| 27 | var playbackMode = presentMode && !embedMode; |
| 28 | var currentPageId = ''; |
| 29 | var fitRaf = 0; |
| 30 | var indexTransitionType = 'fade'; // default, overridden by container build |
| 31 | var indexTransitionDuration = 600; // ms |
| 32 | var playbackRequestSeq = 0; |
| 33 | var pendingPlaybackRequests = {}; |
| 34 | var pageSwitchSeq = 0; |
| 35 | var isPageSwitching = false; |
| 36 | var prefetchedPageUrls = new Set(); |
| 37 | var wheelDeltaBuffer = 0; |
| 38 | var wheelGestureLocked = false; |
| 39 | var wheelGestureLockDirection = 0; |
| 40 | var lastWheelNavigateAt = 0; |
| 41 | var wheelGestureUnlockTimer = 0; |
| 42 | var queuedNavigationOffset = 0; |
| 43 | var WHEEL_NAV_THRESHOLD = 80; |
| 44 | var WHEEL_NAV_COOLDOWN = 520; |
| 45 | var WHEEL_GESTURE_IDLE = 260; |
| 46 | var FRAME_LOAD_TIMEOUT = 3000; |
| 47 | var INDEX_TRANSITION_TYPES = { |
| 48 | none: true, |
| 49 | fade: true, |
| 50 | 'slide-left': true, |
| 51 | 'slide-up': true, |
| 52 | push: true, |
| 53 | wipe: true, |
| 54 | zoom: true, |
| 55 | flip: true, |
| 56 | stack: true, |
| 57 | rotate: true, |
| 58 | cube: true, |
| 59 | 'cover-flow': true, |
| 60 | blur: true, |
| 61 | iris: true, |
| 62 | swing: true, |
| 63 | 'center-reveal': true |
| 64 | }; |
| 65 | |
| 66 | function normalizeIndexTransitionType(type) { |
| 67 | var value = String(type || '').trim(); |
| 68 | return INDEX_TRANSITION_TYPES[value] ? value : 'fade'; |
| 69 | } |
| 70 | |
| 71 | function clampTransitionDuration(type, durationMs) { |
| 72 | if (type === 'none') return 0; |
| 73 | var duration = Number(durationMs); |
| 74 | if (!Number.isFinite(duration)) duration = 600; |
| 75 | return Math.max(120, Math.min(1200, Math.round(duration))); |
| 76 | } |
| 77 | |
| 78 | function prefersReducedMotion() { |
| 79 | try { |
| 80 | return Boolean(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); |
| 81 | } catch (_) { |
| 82 | return false; |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | function ensureIndexTransitionBaseStyles() { |
| 87 | if (document.getElementById('ppt-index-transition-base-styles')) return; |
| 88 | var style = document.createElement('style'); |
| 89 | style.id = 'ppt-index-transition-base-styles'; |
| 90 | style.textContent = |
| 91 | '.ppt-preview-viewport { perspective: 2400px; }' + |
| 92 | '.ppt-preview-frame {' + |
| 93 | ' --ppt-fit-transform: translate(0px, 0px) scale(1);' + |
| 94 | ' --ppt-transition-x: 0px;' + |
| 95 | ' --ppt-transition-y: 0px;' + |
| 96 | ' --ppt-transition-scale: 1;' + |
| 97 | ' --ppt-transition-rotate-y: 0deg;' + |
| 98 | ' --ppt-transition-rotate-z: 0deg;' + |
| 99 | ' transform: var(--ppt-fit-transform) translate3d(var(--ppt-transition-x), var(--ppt-transition-y), 0) scale(var(--ppt-transition-scale)) rotateY(var(--ppt-transition-rotate-y)) rotate(var(--ppt-transition-rotate-z));' + |
| 100 | ' transform-style: preserve-3d;' + |
| 101 | ' backface-visibility: hidden;' + |
| 102 | '}' + |
| 103 | '.ppt-preview-frame.ppt-transitioning {' + |
| 104 | ' display: block !important;' + |
| 105 | ' pointer-events: none !important;' + |
| 106 | ' will-change: transform, opacity, clip-path, filter;' + |
| 107 | '}'; |
| 108 | document.head.appendChild(style); |
| 109 | } |
| 110 | |
| 111 | function ensurePresentBackgroundStyles() { |
| 112 | if (document.getElementById('ppt-present-background-style')) return; |
| 113 | var style = document.createElement('style'); |
| 114 | style.id = 'ppt-present-background-style'; |
| 115 | style.textContent = |
| 116 | 'body.present { background: #000000 !important; }' + |
| 117 | 'body.present .ppt-layout,' + |
| 118 | 'body.present .ppt-stage,' + |
| 119 | 'body.present .ppt-preview-viewport {' + |
| 120 | ' background: #000000 !important;' + |
| 121 | '}'; |
| 122 | document.head.appendChild(style); |
| 123 | } |
| 124 | |
| 125 | function clearPendingPlaybackRequests() { |
| 126 | Object.keys(pendingPlaybackRequests).forEach(function (requestId) { |
| 127 | window.clearTimeout(pendingPlaybackRequests[requestId]); |
| 128 | delete pendingPlaybackRequests[requestId]; |
| 129 | }); |
| 130 | if (wheelGestureUnlockTimer) { |
| 131 | window.clearTimeout(wheelGestureUnlockTimer); |
| 132 | wheelGestureUnlockTimer = 0; |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | function resetWheelGestureState() { |
| 137 | wheelDeltaBuffer = 0; |
| 138 | wheelGestureLocked = false; |
| 139 | wheelGestureLockDirection = 0; |
| 140 | lastWheelNavigateAt = 0; |
| 141 | } |
| 142 | |
| 143 | function getPageKey(page) { |
| 144 | return String((page && (page.id || page.pageId)) || ''); |
| 145 | } |
| 146 | |
| 147 | function getLegacyPageId(page) { |
| 148 | return String((page && page.pageId) || getPageKey(page)); |
| 149 | } |
| 150 | |
| 151 | var pageByKey = new Map(); |
| 152 | var pageIndexByKey = new Map(); |
| 153 | var legacyPageKeyById = new Map(); |
| 154 | pages.forEach(function (page, index) { |
| 155 | var pageKey = getPageKey(page); |
| 156 | if (!pageKey) return; |
| 157 | pageByKey.set(pageKey, page); |
| 158 | pageIndexByKey.set(pageKey, index); |
| 159 | legacyPageKeyById.set(getLegacyPageId(page), pageKey); |
| 160 | }); |
| 161 | |
| 162 | // ── iframe pool: one per page, lazy-loaded on first visit ── |
| 163 | var framePool = new Map(); |
| 164 | var loadedPages = new Set(); |
| 165 | var loadingFrames = new Map(); |
| 166 | var allFrames = frameViewport |
| 167 | ? Array.from(frameViewport.querySelectorAll('.ppt-preview-frame')) |
| 168 | : []; |
| 169 | allFrames.forEach(function (el) { |
| 170 | var pid = el.getAttribute('data-page-id'); |
| 171 | if (pid) framePool.set(pid, el); |
| 172 | }); |
| 173 | |
| 174 | // Build URL for a page iframe |
| 175 | function buildPageUrl(page) { |
| 176 | var url = new URL(page.htmlPath, window.location.href); |
| 177 | url.searchParams.set('fit', 'off'); |
| 178 | if (embedMode) url.searchParams.set('embed', '1'); |
| 179 | url.searchParams.set('pptPlayback', playbackMode ? '1' : '0'); |
| 180 | if (playbackMode) url.searchParams.delete('print'); |
| 181 | else url.searchParams.set('print', '1'); |
| 182 | return url.toString(); |
| 183 | } |
| 184 | |
| 185 | function postPlaybackAdvanceToFrame(offset) { |
| 186 | var frame = getActiveFrame(); |
| 187 | if (!frame) return false; |
| 188 | try { |
| 189 | var frameWindow = frame.contentWindow; |
| 190 | if (!frameWindow || typeof frameWindow.postMessage !== 'function') return false; |
| 191 | var requestId = 'playback-' + (++playbackRequestSeq); |
| 192 | pendingPlaybackRequests[requestId] = window.setTimeout(function () { |
| 193 | delete pendingPlaybackRequests[requestId]; |
| 194 | gotoOffset(offset || 1); |
| 195 | }, 160); |
| 196 | frameWindow.postMessage({ |
| 197 | type: 'ohmyppt:playback:advance', |
| 198 | offset: offset || 1, |
| 199 | requestId: requestId |
| 200 | }, '*'); |
| 201 | return true; |
| 202 | } catch (_) {} |
| 203 | return false; |
| 204 | } |
| 205 | |
| 206 | function requestNavigation(offset, options) { |
| 207 | var navOffset = Number.isFinite(offset) && offset !== 0 ? offset : 1; |
| 208 | var opts = options || {}; |
| 209 | if (isPageSwitching) { |
| 210 | if (opts.queueWhileSwitching) queuedNavigationOffset = navOffset; |
| 211 | return false; |
| 212 | } |
| 213 | if (navOffset > 0 && opts.allowPlaybackAdvance !== false) { |
| 214 | if (playbackMode && postPlaybackAdvanceToFrame(1)) return true; |
| 215 | } |
| 216 | return gotoOffset(navOffset); |
| 217 | } |
| 218 | |
| 219 | function resetWheelGestureSoon() { |
| 220 | if (wheelGestureUnlockTimer) window.clearTimeout(wheelGestureUnlockTimer); |
| 221 | wheelGestureUnlockTimer = window.setTimeout(function () { |
| 222 | wheelGestureUnlockTimer = 0; |
| 223 | wheelDeltaBuffer = 0; |
| 224 | wheelGestureLocked = false; |
| 225 | wheelGestureLockDirection = 0; |
| 226 | }, WHEEL_GESTURE_IDLE); |
| 227 | } |
| 228 | |
| 229 | function handlePresentationKey(event) { |
| 230 | // Forward click advance to iframe first (for click-triggered animations) |
| 231 | var clickForwardKeys = ['ArrowRight', 'ArrowDown', 'PageDown', ' ']; |
| 232 | if (clickForwardKeys.indexOf(event.key) >= 0) { |
| 233 | if (isPageSwitching) { |
| 234 | event.preventDefault(); |
| 235 | return; |
| 236 | } |
| 237 | if (playbackMode && postPlaybackAdvanceToFrame(1)) { |
| 238 | event.preventDefault(); |
| 239 | return; |
| 240 | } |
| 241 | } |
| 242 | |
| 243 | if (event.key === 'ArrowRight' || event.key === 'ArrowDown' || event.key === 'PageDown' || event.key === ' ') { |
| 244 | event.preventDefault(); |
| 245 | requestNavigation(1, { allowPlaybackAdvance: false }); |
| 246 | return; |
| 247 | } |
| 248 | if (event.key === 'ArrowLeft' || event.key === 'ArrowUp' || event.key === 'PageUp') { |
| 249 | event.preventDefault(); |
| 250 | if (isPageSwitching) return; |
| 251 | requestNavigation(-1, { allowPlaybackAdvance: false }); |
| 252 | return; |
| 253 | } |
| 254 | if (event.key === 'Escape') { |
| 255 | if (deckSwitcher) deckSwitcher.classList.remove('open'); |
| 256 | } |
| 257 | if (event.key === 'Escape' && presentMode) { |
| 258 | event.preventDefault(); |
| 259 | exitPresentMode(); |
| 260 | } |
| 261 | } |
| 262 | |
| 263 | function isEditableTarget(target) { |
| 264 | if (!target || target.nodeType !== 1) return false; |
| 265 | var tagName = String(target.tagName || '').toLowerCase(); |
| 266 | var closest = typeof target.closest === 'function' |
| 267 | ? target.closest.bind(target) |
| 268 | : function () { return null; }; |
| 269 | return Boolean( |
| 270 | target.isContentEditable || |
| 271 | tagName === 'input' || |
| 272 | tagName === 'textarea' || |
| 273 | tagName === 'select' || |
| 274 | closest('[contenteditable="true"]') |
| 275 | ); |
| 276 | } |
| 277 | |
| 278 | function isDeckSwitcherWheelTarget(target) { |
| 279 | if (!target || typeof target.nodeType !== 'number') return false; |
| 280 | try { |
| 281 | return Boolean(deckSwitcher && deckSwitcher.contains(target)); |
| 282 | } catch (_) { |
| 283 | return false; |
| 284 | } |
| 285 | } |
| 286 | |
| 287 | function normalizeWheelDelta(event) { |
| 288 | var delta = Math.abs(event.deltaY) >= Math.abs(event.deltaX) ? event.deltaY : event.deltaX; |
| 289 | if (event.deltaMode === 1) delta *= 16; |
| 290 | else if (event.deltaMode === 2) delta *= 900; |
| 291 | return delta; |
| 292 | } |
| 293 | |
| 294 | function handlePresentationWheel(event) { |
| 295 | if (event.ctrlKey || event.metaKey) return; |
| 296 | if (isEditableTarget(event.target)) return; |
| 297 | if (isDeckSwitcherWheelTarget(event.target)) return; |
| 298 | |
| 299 | var delta = normalizeWheelDelta(event); |
| 300 | if (!Number.isFinite(delta) || Math.abs(delta) < 1) return; |
| 301 | var direction = delta > 0 ? 1 : -1; |
| 302 | var isReverseGesture = |
| 303 | wheelGestureLocked && wheelGestureLockDirection && direction !== wheelGestureLockDirection; |
| 304 | |
| 305 | if (Math.sign(delta) !== Math.sign(wheelDeltaBuffer)) wheelDeltaBuffer = 0; |
| 306 | wheelDeltaBuffer += delta; |
| 307 | resetWheelGestureSoon(); |
| 308 | |
| 309 | if (Math.abs(wheelDeltaBuffer) < WHEEL_NAV_THRESHOLD) return; |
| 310 | |
| 311 | if (isReverseGesture) { |
| 312 | wheelGestureLocked = false; |
| 313 | wheelGestureLockDirection = 0; |
| 314 | lastWheelNavigateAt = 0; |
| 315 | } |
| 316 | |
| 317 | if (isPageSwitching) { |
| 318 | wheelDeltaBuffer = 0; |
| 319 | wheelGestureLocked = true; |
| 320 | wheelGestureLockDirection = direction; |
| 321 | if (isReverseGesture) queuedNavigationOffset = direction; |
| 322 | event.preventDefault(); |
| 323 | return; |
| 324 | } |
| 325 | |
| 326 | if (wheelGestureLocked) { |
| 327 | wheelDeltaBuffer = 0; |
| 328 | event.preventDefault(); |
| 329 | return; |
| 330 | } |
| 331 | |
| 332 | var now = Date.now(); |
| 333 | if (now - lastWheelNavigateAt < WHEEL_NAV_COOLDOWN) { |
| 334 | wheelDeltaBuffer = 0; |
| 335 | wheelGestureLocked = true; |
| 336 | wheelGestureLockDirection = direction; |
| 337 | event.preventDefault(); |
| 338 | return; |
| 339 | } |
| 340 | |
| 341 | var offset = wheelDeltaBuffer > 0 ? 1 : -1; |
| 342 | wheelDeltaBuffer = 0; |
| 343 | event.preventDefault(); |
| 344 | if (requestNavigation(offset)) { |
| 345 | wheelGestureLocked = true; |
| 346 | wheelGestureLockDirection = offset; |
| 347 | lastWheelNavigateAt = now; |
| 348 | } else { |
| 349 | resetWheelGestureState(); |
| 350 | } |
| 351 | } |
| 352 | |
| 353 | function bindFrameKeyboard(frame) { |
| 354 | try { |
| 355 | var frameWindow = frame.contentWindow; |
| 356 | var frameDocument = frameWindow && frameWindow.document; |
| 357 | if (!frameWindow || !frameDocument || frame.__ohmypptKeyboardDocument === frameDocument) return; |
| 358 | frameWindow.addEventListener('keydown', handlePresentationKey); |
| 359 | frame.__ohmypptKeyboardDocument = frameDocument; |
| 360 | } catch (_) {} |
| 361 | } |
| 362 | |
| 363 | function handleFramePlaybackMessage(event) { |
| 364 | var frame = getActiveFrame(); |
| 365 | if (!frame) return; |
| 366 | if (event.source && event.source !== frame.contentWindow) return; |
| 367 | var data = event.data; |
| 368 | if (!data) return; |
| 369 | if (data.requestId && pendingPlaybackRequests[data.requestId]) { |
| 370 | window.clearTimeout(pendingPlaybackRequests[data.requestId]); |
| 371 | delete pendingPlaybackRequests[data.requestId]; |
| 372 | } |
| 373 | if (data.type === 'ohmyppt:playback:handled') return; |
| 374 | if (data.type !== 'ohmyppt:playback:goto') return; |
| 375 | var offset = Number(data.offset); |
| 376 | var navigated = requestNavigation( |
| 377 | Number.isFinite(offset) && offset !== 0 ? offset : 1, |
| 378 | { allowPlaybackAdvance: false, queueWhileSwitching: true } |
| 379 | ); |
| 380 | if (data.requestId) { |
| 381 | try { |
| 382 | frame.contentWindow.postMessage({ |
| 383 | type: 'ohmyppt:playback:navigation-result', |
| 384 | requestId: data.requestId, |
| 385 | navigated: navigated |
| 386 | }, '*'); |
| 387 | } catch (_) {} |
| 388 | } |
| 389 | } |
| 390 | |
| 391 | function prefetchPage(page) { |
| 392 | if (!page || embedMode) return; |
| 393 | try { |
| 394 | var url = buildPageUrl(page); |
| 395 | if (prefetchedPageUrls.has(url)) return; |
| 396 | prefetchedPageUrls.add(url); |
| 397 | var link = document.createElement('link'); |
| 398 | link.rel = 'prefetch'; |
| 399 | link.as = 'document'; |
| 400 | link.href = url; |
| 401 | document.head.appendChild(link); |
| 402 | } catch (_) {} |
| 403 | } |
| 404 | |
| 405 | function prefetchAdjacentPages(pageId) { |
| 406 | if (!Array.isArray(pages) || pages.length <= 1) return; |
| 407 | var index = pageIndexByKey.has(pageId) ? pageIndexByKey.get(pageId) : -1; |
| 408 | if (index < 0) return; |
| 409 | [-1, 1].forEach(function (offset) { |
| 410 | var page = pages[index + offset]; |
| 411 | if (page) prefetchPage(page); |
| 412 | }); |
| 413 | } |
| 414 | |
| 415 | // Load or reload a page iframe so slide-level animations replay on revisit. |
| 416 | function ensureFrameLoaded(pageId, forceReload, onReady) { |
| 417 | var page = pageByKey.get(pageId); |
| 418 | var frame = framePool.get(pageId); |
| 419 | if (!page || !frame) return; |
| 420 | if (loadedPages.has(pageId) && !forceReload) { |
| 421 | bindFrameKeyboard(frame); |
| 422 | if (typeof onReady === 'function') onReady(frame); |
| 423 | return; |
| 424 | } |
| 425 | if (!forceReload && loadingFrames.has(pageId)) { |
| 426 | var waitingCallbacks = loadingFrames.get(pageId); |
| 427 | if (typeof onReady === 'function') waitingCallbacks.push(onReady); |
| 428 | return; |
| 429 | } |
| 430 | var callbacks = typeof onReady === 'function' ? [onReady] : []; |
| 431 | loadingFrames.set(pageId, callbacks); |
| 432 | var pageUrl = new URL(buildPageUrl(page)); |
| 433 | if (forceReload) pageUrl.searchParams.set('_pptReplay', String(Date.now())); |
| 434 | var loadToken = String(Date.now()) + '-' + Math.random(); |
| 435 | var loadTimer = 0; |
| 436 | var ready = false; |
| 437 | frame.__ohmypptLoadToken = loadToken; |
| 438 | function finishLoad() { |
| 439 | if (ready || frame.__ohmypptLoadToken !== loadToken) return; |
| 440 | ready = true; |
| 441 | if (loadTimer) window.clearTimeout(loadTimer); |
| 442 | loadedPages.add(pageId); |
| 443 | var readyCallbacks = loadingFrames.get(pageId) || []; |
| 444 | loadingFrames.delete(pageId); |
| 445 | bindFrameKeyboard(frame); |
| 446 | if (pageId === currentPageId) scheduleFitFrame(); |
| 447 | readyCallbacks.forEach(function (callback) { |
| 448 | if (typeof callback === 'function') callback(frame); |
| 449 | }); |
| 450 | } |
| 451 | frame.addEventListener('load', finishLoad, { once: true }); |
| 452 | loadTimer = window.setTimeout(finishLoad, FRAME_LOAD_TIMEOUT); |
| 453 | frame.src = pageUrl.toString(); |
| 454 | } |
| 455 | |
| 456 | if (embedMode) document.body.classList.add('embed'); |
| 457 | |
| 458 | function applyPresentMode(nextPresentMode, syncQuery) { |
| 459 | var nextPresentModeValue = Boolean(nextPresentMode); |
| 460 | var playbackModeChanged = playbackMode !== (nextPresentModeValue && !embedMode); |
| 461 | presentMode = nextPresentModeValue; |
| 462 | playbackMode = presentMode && !embedMode; |
| 463 | document.body.classList.toggle('present', presentMode); |
| 464 | if (presentBtn) { |
| 465 | presentBtn.textContent = presentMode ? '退出演示' : '演示模式(ESC退出)'; |
| 466 | } |
| 467 | if (fullscreenBtn) { |
| 468 | fullscreenBtn.style.display = 'none'; |
| 469 | } |
| 470 | if (syncQuery) { |
| 471 | try { |
| 472 | var next = new URLSearchParams(window.location.search); |
| 473 | if (presentMode) next.set('present', '1'); |
| 474 | else next.delete('present'); |
| 475 | var query = next.toString(); |
| 476 | window.history.replaceState( |
| 477 | null, |
| 478 | '', |
| 479 | window.location.pathname + (query ? '?' + query : '') + (window.location.hash || '') |
| 480 | ); |
| 481 | } catch (_) {} |
| 482 | } |
| 483 | scheduleFitFrame(); |
| 484 | if (playbackModeChanged && currentPageId) { |
| 485 | applyPage(currentPageId, false, true); |
| 486 | } |
| 487 | } |
| 488 | |
| 489 | function normalizePageId(hashValue) { |
| 490 | var raw = (hashValue || '').replace(/^#/, '').trim(); |
| 491 | if (!raw && pages.length > 0) return getPageKey(pages[0]); |
| 492 | var decoded = decodeURIComponent(raw || ''); |
| 493 | if (pageByKey.has(decoded)) return decoded; |
| 494 | var legacyMatch = legacyPageKeyById.get(decoded); |
| 495 | if (legacyMatch) return legacyMatch; |
| 496 | return (pages[0] ? getPageKey(pages[0]) : ''); |
| 497 | } |
| 498 | |
| 499 | function getActiveFrame() { |
| 500 | return currentPageId ? framePool.get(currentPageId) : null; |
| 501 | } |
| 502 | |
| 503 | function applyFrameFit(frame) { |
| 504 | if (!frame || !frameViewport) return; |
| 505 | var rect = frameViewport.getBoundingClientRect(); |
| 506 | var rawScale = Math.min(rect.width / slideWidth, rect.height / slideHeight); |
| 507 | var scale = Number.isFinite(rawScale) && rawScale > 0 ? rawScale : 1; |
| 508 | var offsetX = Math.max(0, (rect.width - slideWidth * scale) / 2); |
| 509 | var offsetY = Math.max(0, (rect.height - slideHeight * scale) / 2); |
| 510 | frame.style.setProperty( |
| 511 | '--ppt-fit-transform', |
| 512 | 'translate(' + offsetX + 'px, ' + offsetY + 'px) scale(' + scale + ')' |
| 513 | ); |
| 514 | } |
| 515 | |
| 516 | function fitFrame() { |
| 517 | applyFrameFit(getActiveFrame()); |
| 518 | } |
| 519 | |
| 520 | function scheduleFitFrame() { |
| 521 | if (fitRaf) cancelAnimationFrame(fitRaf); |
| 522 | fitRaf = requestAnimationFrame(function () { |
| 523 | fitRaf = 0; |
| 524 | fitFrame(); |
| 525 | }); |
| 526 | } |
| 527 | |
| 528 | function renderThumbs(activePageId) { |
| 529 | if (!thumbs || embedMode) return; |
| 530 | Array.from(thumbs.querySelectorAll('.ppt-thumb-item')).forEach(function (item) { |
| 531 | item.classList.toggle('active', item.getAttribute('data-page-id') === activePageId); |
| 532 | }); |
| 533 | } |
| 534 | |
| 535 | function bindThumbEvents() { |
| 536 | if (!thumbs) return; |
| 537 | Array.from(thumbs.querySelectorAll('.ppt-thumb-item')).forEach(function (item) { |
| 538 | item.addEventListener('click', function () { |
| 539 | var pageId = item.getAttribute('data-page-id'); |
| 540 | if (!pageId) return; |
| 541 | if (deckSwitcher) deckSwitcher.classList.remove('open'); |
| 542 | window.location.hash = '#' + encodeURIComponent(pageId); |
| 543 | }); |
| 544 | }); |
| 545 | } |
| 546 | |
| 547 | function currentIndex() { |
| 548 | return pageIndexByKey.has(currentPageId) ? pageIndexByKey.get(currentPageId) : -1; |
| 549 | } |
| 550 | |
| 551 | function updateIndicator() { |
| 552 | if (!indicator) return; |
| 553 | var index = currentIndex(); |
| 554 | indicator.textContent = index >= 0 ? (index + 1) + ' / ' + pages.length : '--'; |
| 555 | } |
| 556 | |
| 557 | function resetTransitionFrame(frame) { |
| 558 | if (!frame) return; |
| 559 | frame.classList.remove('ppt-transitioning'); |
| 560 | frame.style.opacity = ''; |
| 561 | frame.style.zIndex = ''; |
| 562 | frame.style.clipPath = ''; |
| 563 | frame.style.filter = ''; |
| 564 | frame.style.transformOrigin = ''; |
| 565 | frame.style.setProperty('--ppt-transition-x', '0px'); |
| 566 | frame.style.setProperty('--ppt-transition-y', '0px'); |
| 567 | frame.style.setProperty('--ppt-transition-scale', '1'); |
| 568 | frame.style.setProperty('--ppt-transition-rotate-y', '0deg'); |
| 569 | frame.style.setProperty('--ppt-transition-rotate-z', '0deg'); |
| 570 | } |
| 571 | |
| 572 | function runAnime(targets, params) { |
| 573 | var api = window.anime; |
| 574 | try { |
| 575 | if (api && typeof api.animate === 'function') return api.animate(targets, params); |
| 576 | if (typeof api === 'function') return api(Object.assign({ targets: targets }, params)); |
| 577 | } catch (_) {} |
| 578 | return null; |
| 579 | } |
| 580 | |
| 581 | function waitForAnimeAnimations(animations, duration, callback) { |
| 582 | var settled = false; |
| 583 | var pending = 0; |
| 584 | function done() { |
| 585 | if (settled) return; |
| 586 | settled = true; |
| 587 | callback(); |
| 588 | } |
| 589 | animations.forEach(function (animation) { |
| 590 | var promise = animation && animation.finished && typeof animation.finished.then === 'function' |
| 591 | ? animation.finished |
| 592 | : (animation && typeof animation.then === 'function' ? animation : null); |
| 593 | if (!promise) return; |
| 594 | pending += 1; |
| 595 | promise.then(function () { |
| 596 | pending -= 1; |
| 597 | if (pending <= 0) done(); |
| 598 | }, done); |
| 599 | }); |
| 600 | if (pending <= 0) { |
| 601 | window.setTimeout(done, Math.max(0, duration)); |
| 602 | return; |
| 603 | } |
| 604 | window.setTimeout(done, Math.max(80, duration + 120)); |
| 605 | } |
| 606 | |
| 607 | function transitionValue(percent) { |
| 608 | return (Math.round(percent * 1000) / 1000) + '%'; |
| 609 | } |
| 610 | |
| 611 | function buildTransitionParams(type, direction, duration) { |
| 612 | var d = direction < 0 ? -1 : 1; |
| 613 | var ease = 'out(3)'; |
| 614 | var base = { duration: duration, ease: ease }; |
| 615 | if (type === 'slide-left') { |
| 616 | return { |
| 617 | oldParams: Object.assign({}, base, { |
| 618 | '--ppt-transition-x': ['0%', transitionValue(-18 * d)], |
| 619 | opacity: [1, 0.65] |
| 620 | }), |
| 621 | newParams: Object.assign({}, base, { |
| 622 | '--ppt-transition-x': [transitionValue(100 * d), '0%'], |
| 623 | opacity: [1, 1] |
| 624 | }) |
| 625 | }; |
| 626 | } |
| 627 | if (type === 'slide-up') { |
| 628 | return { |
| 629 | oldParams: Object.assign({}, base, { |
| 630 | '--ppt-transition-y': ['0%', transitionValue(-18 * d)], |
| 631 | opacity: [1, 0.65] |
| 632 | }), |
| 633 | newParams: Object.assign({}, base, { |
| 634 | '--ppt-transition-y': [transitionValue(100 * d), '0%'], |
| 635 | opacity: [1, 1] |
| 636 | }) |
| 637 | }; |
| 638 | } |
| 639 | if (type === 'push') { |
| 640 | return { |
| 641 | oldParams: Object.assign({}, base, { |
| 642 | '--ppt-transition-x': ['0%', transitionValue(-100 * d)] |
| 643 | }), |
| 644 | newParams: Object.assign({}, base, { |
| 645 | '--ppt-transition-x': [transitionValue(100 * d), '0%'] |
| 646 | }) |
| 647 | }; |
| 648 | } |
| 649 | if (type === 'wipe') { |
| 650 | return { |
| 651 | oldParams: Object.assign({}, base, { opacity: [1, 0.88] }), |
| 652 | newParams: Object.assign({}, base, { |
| 653 | clipPath: d > 0 |
| 654 | ? ['inset(0 0 0 100%)', 'inset(0 0 0 0%)'] |
| 655 | : ['inset(0 100% 0 0)', 'inset(0 0% 0 0)'], |
| 656 | opacity: [1, 1] |
| 657 | }) |
| 658 | }; |
| 659 | } |
| 660 | if (type === 'zoom') { |
| 661 | return { |
| 662 | oldParams: Object.assign({}, base, { |
| 663 | '--ppt-transition-scale': [1, 0.88], |
| 664 | opacity: [1, 0] |
| 665 | }), |
| 666 | newParams: Object.assign({}, base, { |
| 667 | '--ppt-transition-scale': [1.08, 1], |
| 668 | opacity: [0, 1] |
| 669 | }) |
| 670 | }; |
| 671 | } |
| 672 | if (type === 'flip') { |
| 673 | return { |
| 674 | oldParams: Object.assign({}, base, { |
| 675 | '--ppt-transition-rotate-y': ['0deg', (-72 * d) + 'deg'], |
| 676 | opacity: [1, 0] |
| 677 | }), |
| 678 | newParams: Object.assign({}, base, { |
| 679 | '--ppt-transition-rotate-y': [(72 * d) + 'deg', '0deg'], |
| 680 | opacity: [0, 1] |
| 681 | }) |
| 682 | }; |
| 683 | } |
| 684 | if (type === 'stack') { |
| 685 | return { |
| 686 | oldParams: Object.assign({}, base, { |
| 687 | '--ppt-transition-y': ['0px', (18 * d) + 'px'], |
| 688 | '--ppt-transition-scale': [1, 0.96], |
| 689 | opacity: [1, 0.55] |
| 690 | }), |
| 691 | newParams: Object.assign({}, base, { |
| 692 | '--ppt-transition-y': [(-28 * d) + 'px', '0px'], |
| 693 | '--ppt-transition-scale': [0.96, 1], |
| 694 | opacity: [0, 1] |
| 695 | }) |
| 696 | }; |
| 697 | } |
| 698 | if (type === 'rotate') { |
| 699 | return { |
| 700 | oldParams: Object.assign({}, base, { |
| 701 | '--ppt-transition-rotate-z': ['0deg', (-8 * d) + 'deg'], |
| 702 | '--ppt-transition-scale': [1, 0.9], |
| 703 | opacity: [1, 0] |
| 704 | }), |
| 705 | newParams: Object.assign({}, base, { |
| 706 | '--ppt-transition-rotate-z': [(10 * d) + 'deg', '0deg'], |
| 707 | '--ppt-transition-scale': [1.08, 1], |
| 708 | opacity: [0, 1] |
| 709 | }) |
| 710 | }; |
| 711 | } |
| 712 | if (type === 'cube') { |
| 713 | return { |
| 714 | oldParams: Object.assign({}, base, { |
| 715 | '--ppt-transition-x': ['0%', transitionValue(-34 * d)], |
| 716 | '--ppt-transition-rotate-y': ['0deg', (-88 * d) + 'deg'], |
| 717 | opacity: [1, 0.12] |
| 718 | }), |
| 719 | newParams: Object.assign({}, base, { |
| 720 | '--ppt-transition-x': [transitionValue(34 * d), '0%'], |
| 721 | '--ppt-transition-rotate-y': [(88 * d) + 'deg', '0deg'], |
| 722 | opacity: [0.12, 1] |
| 723 | }) |
| 724 | }; |
| 725 | } |
| 726 | if (type === 'cover-flow') { |
| 727 | return { |
| 728 | oldParams: Object.assign({}, base, { |
| 729 | '--ppt-transition-x': ['0%', transitionValue(-42 * d)], |
| 730 | '--ppt-transition-scale': [1, 0.82], |
| 731 | '--ppt-transition-rotate-y': ['0deg', (42 * d) + 'deg'], |
| 732 | opacity: [1, 0.32] |
| 733 | }), |
| 734 | newParams: Object.assign({}, base, { |
| 735 | '--ppt-transition-x': [transitionValue(42 * d), '0%'], |
| 736 | '--ppt-transition-scale': [0.82, 1], |
| 737 | '--ppt-transition-rotate-y': [(-42 * d) + 'deg', '0deg'], |
| 738 | opacity: [0.32, 1] |
| 739 | }) |
| 740 | }; |
| 741 | } |
| 742 | if (type === 'blur') { |
| 743 | return { |
| 744 | oldParams: Object.assign({}, base, { |
| 745 | '--ppt-transition-scale': [1, 1.035], |
| 746 | filter: ['blur(0px)', 'blur(16px)'], |
| 747 | opacity: [1, 0] |
| 748 | }), |
| 749 | newParams: Object.assign({}, base, { |
| 750 | '--ppt-transition-scale': [0.965, 1], |
| 751 | filter: ['blur(16px)', 'blur(0px)'], |
| 752 | opacity: [0, 1] |
| 753 | }) |
| 754 | }; |
| 755 | } |
| 756 | if (type === 'iris') { |
| 757 | return { |
| 758 | oldParams: Object.assign({}, base, { |
| 759 | opacity: [1, 0.75] |
| 760 | }), |
| 761 | newParams: Object.assign({}, base, { |
| 762 | clipPath: ['circle(0% at 50% 50%)', 'circle(76% at 50% 50%)'], |
| 763 | opacity: [1, 1] |
| 764 | }) |
| 765 | }; |
| 766 | } |
| 767 | if (type === 'swing') { |
| 768 | return { |
| 769 | oldParams: Object.assign({}, base, { |
| 770 | '--ppt-transition-x': ['0%', transitionValue(-16 * d)], |
| 771 | '--ppt-transition-rotate-z': ['0deg', (-12 * d) + 'deg'], |
| 772 | '--ppt-transition-scale': [1, 0.94], |
| 773 | opacity: [1, 0] |
| 774 | }), |
| 775 | newParams: Object.assign({}, base, { |
| 776 | '--ppt-transition-x': [transitionValue(18 * d), '0%'], |
| 777 | '--ppt-transition-rotate-z': [(12 * d) + 'deg', '0deg'], |
| 778 | '--ppt-transition-scale': [0.96, 1], |
| 779 | opacity: [0, 1] |
| 780 | }) |
| 781 | }; |
| 782 | } |
| 783 | if (type === 'center-reveal') { |
| 784 | return { |
| 785 | oldParams: Object.assign({}, base, { |
| 786 | '--ppt-transition-scale': [1, 0.98], |
| 787 | opacity: [1, 0.82] |
| 788 | }), |
| 789 | newParams: Object.assign({}, base, { |
| 790 | clipPath: ['inset(0 50% 0 50%)', 'inset(0 0% 0 0%)'], |
| 791 | '--ppt-transition-scale': [1.02, 1], |
| 792 | opacity: [1, 1] |
| 793 | }) |
| 794 | }; |
| 795 | } |
| 796 | return { |
| 797 | oldParams: Object.assign({}, base, { opacity: [1, 0] }), |
| 798 | newParams: Object.assign({}, base, { opacity: [0, 1] }) |
| 799 | }; |
| 800 | } |
| 801 | |
| 802 | function applyPage(pageId, syncHash, forceReload) { |
| 803 | if (!Array.isArray(pages) || pages.length === 0) { |
| 804 | document.body.classList.add('empty'); |
| 805 | if (indicator) indicator.textContent = '0 / 0'; |
| 806 | return; |
| 807 | } |
| 808 | document.body.classList.remove('empty'); |
| 809 | var page = pageByKey.get(pageId) || pages[0]; |
| 810 | if (!page) return; |
| 811 | |
| 812 | var previousPageId = currentPageId; |
| 813 | var prevFrame = previousPageId ? framePool.get(previousPageId) : null; |
| 814 | var nextPageId = getPageKey(page); |
| 815 | var samePage = previousPageId === nextPageId; |
| 816 | var switchSeq = ++pageSwitchSeq; |
| 817 | if (previousPageId && !samePage) clearPendingPlaybackRequests(); |
| 818 | |
| 819 | function finishSwitch() { |
| 820 | if (switchSeq !== pageSwitchSeq) return; |
| 821 | isPageSwitching = false; |
| 822 | if (!queuedNavigationOffset) return; |
| 823 | var nextOffset = queuedNavigationOffset; |
| 824 | queuedNavigationOffset = 0; |
| 825 | window.setTimeout(function () { |
| 826 | requestNavigation(nextOffset, { allowPlaybackAdvance: false }); |
| 827 | }, 0); |
| 828 | } |
| 829 | |
| 830 | function updatePageState() { |
| 831 | if (switchSeq !== pageSwitchSeq) return; |
| 832 | currentPageId = nextPageId; |
| 833 | if (syncHash && window.location.hash !== '#' + encodeURIComponent(currentPageId)) { |
| 834 | window.history.replaceState(null, '', '#' + encodeURIComponent(currentPageId)); |
| 835 | } |
| 836 | renderThumbs(currentPageId); |
| 837 | updateIndicator(); |
| 838 | prefetchAdjacentPages(currentPageId); |
| 839 | } |
| 840 | |
| 841 | function switchFrame() { |
| 842 | if (switchSeq !== pageSwitchSeq) return; |
| 843 | if (prevFrame) { |
| 844 | prevFrame.classList.remove('active'); |
| 845 | resetTransitionFrame(prevFrame); |
| 846 | } |
| 847 | var nextFrame = framePool.get(nextPageId); |
| 848 | if (nextFrame) { |
| 849 | nextFrame.classList.add('active'); |
| 850 | resetTransitionFrame(nextFrame); |
| 851 | } |
| 852 | updatePageState(); |
| 853 | scheduleFitFrame(); |
| 854 | } |
| 855 | |
| 856 | function animateFrameSwitch(done) { |
| 857 | if (switchSeq !== pageSwitchSeq) return; |
| 858 | var nextFrame = framePool.get(nextPageId); |
| 859 | var canAnimate = |
| 860 | presentMode && |
| 861 | indexTransitionType !== 'none' && |
| 862 | !prefersReducedMotion() && |
| 863 | previousPageId && |
| 864 | !samePage && |
| 865 | prevFrame && |
| 866 | nextFrame; |
| 867 | if (!canAnimate) { |
| 868 | switchFrame(); |
| 869 | done(); |
| 870 | return; |
| 871 | } |
| 872 | var previousIndex = pageIndexByKey.has(previousPageId) ? pageIndexByKey.get(previousPageId) : -1; |
| 873 | var nextIndex = pageIndexByKey.has(nextPageId) ? pageIndexByKey.get(nextPageId) : previousIndex + 1; |
| 874 | var direction = previousIndex >= 0 && nextIndex < previousIndex ? -1 : 1; |
| 875 | var duration = clampTransitionDuration(indexTransitionType, indexTransitionDuration); |
| 876 | |
| 877 | resetTransitionFrame(prevFrame); |
| 878 | resetTransitionFrame(nextFrame); |
| 879 | applyFrameFit(prevFrame); |
| 880 | applyFrameFit(nextFrame); |
| 881 | prevFrame.classList.add('active', 'ppt-transitioning'); |
| 882 | nextFrame.classList.add('active', 'ppt-transitioning'); |
| 883 | prevFrame.style.zIndex = '1'; |
| 884 | nextFrame.style.zIndex = '2'; |
| 885 | updatePageState(); |
| 886 | |
| 887 | var params = buildTransitionParams(indexTransitionType, direction, duration); |
| 888 | var oldAnimation = runAnime(prevFrame, params.oldParams); |
| 889 | var newAnimation = runAnime(nextFrame, params.newParams); |
| 890 | var animations = [oldAnimation, newAnimation]; |
| 891 | if (!animations.some(Boolean)) { |
| 892 | if (switchSeq !== pageSwitchSeq) return; |
| 893 | prevFrame.classList.remove('active'); |
| 894 | resetTransitionFrame(prevFrame); |
| 895 | resetTransitionFrame(nextFrame); |
| 896 | scheduleFitFrame(); |
| 897 | done(); |
| 898 | return; |
| 899 | } |
| 900 | waitForAnimeAnimations(animations, duration, function () { |
| 901 | if (switchSeq !== pageSwitchSeq) return; |
| 902 | prevFrame.classList.remove('active'); |
| 903 | resetTransitionFrame(prevFrame); |
| 904 | resetTransitionFrame(nextFrame); |
| 905 | scheduleFitFrame(); |
| 906 | done(); |
| 907 | }); |
| 908 | } |
| 909 | |
| 910 | function commitWhenReady() { |
| 911 | if (switchSeq !== pageSwitchSeq) return; |
| 912 | animateFrameSwitch(finishSwitch); |
| 913 | } |
| 914 | |
| 915 | isPageSwitching = !samePage; |
| 916 | ensureFrameLoaded( |
| 917 | nextPageId, |
| 918 | Boolean(forceReload) || (loadedPages.has(nextPageId) && !samePage), |
| 919 | commitWhenReady |
| 920 | ); |
| 921 | if (samePage) finishSwitch(); |
| 922 | } |
| 923 | |
| 924 | function gotoOffset(offset) { |
| 925 | if (!Array.isArray(pages) || pages.length === 0) return false; |
| 926 | if (isPageSwitching) return false; |
| 927 | var index = currentIndex(); |
| 928 | if (index < 0) return false; |
| 929 | var target = Math.max(0, Math.min(pages.length - 1, index + offset)); |
| 930 | if (target === index) return false; |
| 931 | var targetPage = pages[target]; |
| 932 | if (!targetPage) return false; |
| 933 | window.location.hash = '#' + encodeURIComponent(getPageKey(targetPage)); |
| 934 | return true; |
| 935 | } |
| 936 | |
| 937 | function onHashChange() { |
| 938 | var pageId = normalizePageId(window.location.hash); |
| 939 | applyPage(pageId, false); |
| 940 | } |
| 941 | |
| 942 | function togglePresentMode() { |
| 943 | applyPresentMode(!presentMode, true); |
| 944 | if (presentMode && !document.fullscreenElement) { |
| 945 | try { document.documentElement.requestFullscreen(); } catch (_) {} |
| 946 | } else if (!presentMode && document.fullscreenElement) { |
| 947 | try { document.exitFullscreen(); } catch (_) {} |
| 948 | } |
| 949 | } |
| 950 | |
| 951 | function exitPresentMode() { |
| 952 | if (!presentMode) return; |
| 953 | applyPresentMode(false, true); |
| 954 | if (document.fullscreenElement) { |
| 955 | try { document.exitFullscreen(); } catch (_) {} |
| 956 | } |
| 957 | } |
| 958 | |
| 959 | function toggleFullscreen() { |
| 960 | togglePresentMode(); |
| 961 | } |
| 962 | |
| 963 | // Read transition config from container data attribute |
| 964 | try { |
| 965 | var transitionConfig = document.getElementById('ppt-index-transition-config'); |
| 966 | if (transitionConfig) { |
| 967 | var config = JSON.parse(transitionConfig.textContent || '{}'); |
| 968 | indexTransitionType = normalizeIndexTransitionType(config.type); |
| 969 | indexTransitionDuration = clampTransitionDuration(indexTransitionType, config.durationMs); |
| 970 | } |
| 971 | } catch (_) {} |
| 972 | |
| 973 | ensureIndexTransitionBaseStyles(); |
| 974 | ensurePresentBackgroundStyles(); |
| 975 | |
| 976 | bindThumbEvents(); |
| 977 | if (prevBtn) prevBtn.addEventListener('click', function () { gotoOffset(-1); }); |
| 978 | if (nextBtn) nextBtn.addEventListener('click', function () { gotoOffset(1); }); |
| 979 | if (tabsBtn) tabsBtn.addEventListener('click', function () { if (deckSwitcher) deckSwitcher.classList.toggle('open'); }); |
| 980 | if (presentBtn) presentBtn.addEventListener('click', function () { togglePresentMode(); }); |
| 981 | if (fullscreenBtn) fullscreenBtn.addEventListener('click', function () { toggleFullscreen(); }); |
| 982 | window.addEventListener('resize', function () { scheduleFitFrame(); }); |
| 983 | window.addEventListener('hashchange', onHashChange); |
| 984 | window.addEventListener('keydown', handlePresentationKey); |
| 985 | window.addEventListener('wheel', handlePresentationWheel, { passive: false }); |
| 986 | window.addEventListener('message', handleFramePlaybackMessage); |
| 987 | window.addEventListener('pagehide', clearPendingPlaybackRequests); |
| 988 | window.addEventListener('beforeunload', clearPendingPlaybackRequests); |
| 989 | document.addEventListener('fullscreenchange', function () { |
| 990 | if (!document.fullscreenElement && presentMode) { |
| 991 | exitPresentMode(); |
| 992 | } |
| 993 | }); |
| 994 | document.addEventListener('click', function (event) { |
| 995 | var target = event.target; |
| 996 | if (!(target instanceof Node)) return; |
| 997 | if (!deckSwitcher || !deckSwitcher.classList.contains('open')) return; |
| 998 | var inSwitcher = deckSwitcher.contains(target); |
| 999 | var inTabsButton = tabsBtn && tabsBtn.contains(target); |
| 1000 | if (!inSwitcher && !inTabsButton) { |
| 1001 | deckSwitcher.classList.remove('open'); |
| 1002 | } |
| 1003 | }); |
| 1004 | |
| 1005 | applyPresentMode(presentMode, false); |
| 1006 | applyPage(normalizePageId(window.location.hash), true); |
| 1007 | scheduleFitFrame(); |
| 1008 | })(); |
| 1009 |