From 913bcb0f0998ac6f6392914fb12828f49e1607b2 Mon Sep 17 00:00:00 2001 From: Carlos Polop Date: Sun, 19 Apr 2026 12:54:23 +0200 Subject: [PATCH] f --- book.toml | 1 + theme/css/general.css | 43 ++++++++++ theme/motion.js | 187 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 231 insertions(+) create mode 100644 theme/motion.js diff --git a/book.toml b/book.toml index 83df9b306..df301ab6f 100644 --- a/book.toml +++ b/book.toml @@ -21,6 +21,7 @@ additional-js = [ "theme/pagetoc.js", "theme/ht_searcher.js", "theme/sponsor.js", + "theme/motion.js", "theme/ai.js" ] no-section-label = true diff --git a/theme/css/general.css b/theme/css/general.css index e0b922b6e..8c8c99040 100644 --- a/theme/css/general.css +++ b/theme/css/general.css @@ -70,6 +70,49 @@ body { animation: float-particle 16s infinite; } +html.motion-paused .bg-animation, +html.motion-paused .grid-overlay, +html.motion-paused .particle, +html.motion-paused #ht-ai-btn { + animation-play-state: paused !important; +} + +html.motion-reduced .bg-animation, +html.motion-reduced .grid-overlay, +html.motion-reduced .particles { + display: none !important; +} + +html.motion-reduced .particle, +html.motion-reduced #ht-ai-btn { + animation: none !important; +} + +html.motion-reduced #ht-ai-btn { + background: linear-gradient(45deg, #b31328, #d42d3f) !important; + background-size: auto !important; + box-shadow: 0 8px 18px rgba(0, 0, 0, 0.3) !important; +} + +@media (prefers-reduced-motion: reduce) { + .bg-animation, + .grid-overlay, + .particles { + display: none !important; + } + + .particle, + #ht-ai-btn { + animation: none !important; + } + + #ht-ai-btn { + background: linear-gradient(45deg, #b31328, #d42d3f) !important; + background-size: auto !important; + box-shadow: 0 8px 18px rgba(0, 0, 0, 0.3) !important; + } +} + @keyframes bg-drift { 0% { transform: translate3d(0, 0, 0) scale(1); } 100% { transform: translate3d(-2%, 1%, 0) scale(1.04); } diff --git a/theme/motion.js b/theme/motion.js new file mode 100644 index 000000000..b3a082e9e --- /dev/null +++ b/theme/motion.js @@ -0,0 +1,187 @@ +(function () { + var docEl = document.documentElement; + var REDUCED_CLASS = "motion-reduced"; + var PAUSED_CLASS = "motion-paused"; + var motionState = { mode: "normal", reason: "default" }; + + function setMode(mode, reason) { + var nextMode = mode === "reduced" ? "reduced" : "normal"; + if (motionState.mode === nextMode && motionState.reason === reason) { + return; + } + + motionState.mode = nextMode; + motionState.reason = reason || motionState.reason; + docEl.classList.toggle(REDUCED_CLASS, nextMode === "reduced"); + docEl.dataset.motionMode = nextMode; + window.__hacktricksMotion = { + mode: motionState.mode, + reason: motionState.reason, + shouldReduceMotion: function () { + return motionState.mode === "reduced"; + } + }; + + document.dispatchEvent(new CustomEvent("hacktricks:motionchange", { + detail: { + mode: motionState.mode, + reason: motionState.reason + } + })); + } + + function updateVisibilityState() { + docEl.classList.toggle(PAUSED_CLASS, document.visibilityState === "hidden"); + } + + function getMediaQuery() { + if (typeof window.matchMedia !== "function") { + return null; + } + return window.matchMedia("(prefers-reduced-motion: reduce)"); + } + + function getDeviceHints() { + var hints = { + lowCapability: false, + hardwareConcurrency: null, + deviceMemory: null + }; + + if (typeof navigator.hardwareConcurrency === "number") { + hints.hardwareConcurrency = navigator.hardwareConcurrency; + } + + if (typeof navigator.deviceMemory === "number") { + hints.deviceMemory = navigator.deviceMemory; + } + + hints.lowCapability = + (hints.hardwareConcurrency !== null && hints.hardwareConcurrency <= 4) || + (hints.deviceMemory !== null && hints.deviceMemory <= 4); + + return hints; + } + + function monitorAnimationHealth() { + if ( + document.visibilityState === "hidden" || + typeof window.requestAnimationFrame !== "function" + ) { + return; + } + + var mediaQuery = getMediaQuery(); + if (mediaQuery && mediaQuery.matches) { + setMode("reduced", "prefers-reduced-motion"); + return; + } + + var hints = getDeviceHints(); + var perfState = { + frameCount: 0, + firstFrameAt: 0, + longTaskTime: 0, + worstFrameGap: 0 + }; + var observer = null; + + if (typeof PerformanceObserver === "function") { + try { + observer = new PerformanceObserver(function (list) { + list.getEntries().forEach(function (entry) { + perfState.longTaskTime += entry.duration; + }); + }); + observer.observe({ type: "longtask", buffered: true }); + } catch (error) { + observer = null; + } + } + + var targetDuration = 2500; + var fpsThreshold = hints.lowCapability ? 50 : 42; + var longTaskThreshold = hints.lowCapability ? 160 : 240; + var frameGapThreshold = hints.lowCapability ? 90 : 120; + var lastFrameAt = 0; + + function finish(now) { + if (observer) { + observer.disconnect(); + } + + var elapsed = now - perfState.firstFrameAt; + if (elapsed <= 0) { + return; + } + + var fps = (perfState.frameCount * 1000) / elapsed; + var shouldReduce = + fps < fpsThreshold || + perfState.longTaskTime > longTaskThreshold || + perfState.worstFrameGap > frameGapThreshold; + + if (shouldReduce) { + setMode("reduced", "runtime-performance"); + } + } + + function sample(now) { + if (!perfState.firstFrameAt) { + perfState.firstFrameAt = now; + } + + perfState.frameCount += 1; + if (lastFrameAt) { + perfState.worstFrameGap = Math.max( + perfState.worstFrameGap, + now - lastFrameAt + ); + } + lastFrameAt = now; + + if (now - perfState.firstFrameAt >= targetDuration) { + finish(now); + return; + } + + window.requestAnimationFrame(sample); + } + + window.requestAnimationFrame(sample); + } + + var mediaQuery = getMediaQuery(); + if (mediaQuery && mediaQuery.matches) { + setMode("reduced", "prefers-reduced-motion"); + } else { + setMode("normal", "default"); + if (document.readyState === "complete") { + window.setTimeout(monitorAnimationHealth, 1200); + } else { + window.addEventListener("load", function () { + window.setTimeout(monitorAnimationHealth, 1200); + }, { once: true }); + } + } + + if (mediaQuery) { + var handlePreferenceChange = function (event) { + if (event.matches) { + setMode("reduced", "prefers-reduced-motion"); + } else if (motionState.reason === "prefers-reduced-motion") { + setMode("normal", "preference-restored"); + window.setTimeout(monitorAnimationHealth, 600); + } + }; + + if (typeof mediaQuery.addEventListener === "function") { + mediaQuery.addEventListener("change", handlePreferenceChange); + } else if (typeof mediaQuery.addListener === "function") { + mediaQuery.addListener(handlePreferenceChange); + } + } + + updateVisibilityState(); + document.addEventListener("visibilitychange", updateVisibilityState); +})();