UNPKG

react-theme-switch-animation

Version:

React Theme Switch Animation for ReactJS, NextJS App Router

177 lines (176 loc) 12.3 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; import { useEffect, useRef, useState } from 'react'; import { flushSync } from 'react-dom'; var isBrowser = typeof window !== 'undefined'; // Inject base CSS for view transitions var injectBaseStyles = function () { if (isBrowser) { var styleId = 'theme-switch-base-style'; if (!document.getElementById(styleId)) { var style = document.createElement('style'); style.id = styleId; var isHighResolution = window.innerWidth >= 3000 || window.innerHeight >= 2000; style.textContent = "\n ::view-transition-old(root),\n ::view-transition-new(root) {\n animation: none;\n mix-blend-mode: normal;\n ".concat(isHighResolution ? 'transform: translateZ(0);' : '', "\n }\n \n ").concat(isHighResolution ? "\n ::view-transition-group(root),\n ::view-transition-image-pair(root),\n ::view-transition-old(root),\n ::view-transition-new(root) {\n backface-visibility: hidden;\n perspective: 1000px;\n transform: translate3d(0, 0, 0);\n }\n " : '', "\n "); document.head.appendChild(style); } } }; export var ThemeAnimationType; (function (ThemeAnimationType) { ThemeAnimationType["CIRCLE"] = "circle"; ThemeAnimationType["BLUR_CIRCLE"] = "blur-circle"; })(ThemeAnimationType || (ThemeAnimationType = {})); export var useModeAnimation = function (props) { var _a = props || {}, _b = _a.duration, propsDuration = _b === void 0 ? 750 : _b, _c = _a.easing, easing = _c === void 0 ? 'ease-in-out' : _c, _d = _a.pseudoElement, pseudoElement = _d === void 0 ? '::view-transition-new(root)' : _d, _e = _a.globalClassName, globalClassName = _e === void 0 ? 'dark' : _e, _f = _a.animationType, animationType = _f === void 0 ? ThemeAnimationType.CIRCLE : _f, _g = _a.blurAmount, blurAmount = _g === void 0 ? 2 : _g, _h = _a.styleId, styleId = _h === void 0 ? 'theme-switch-style' : _h, externalDarkMode = _a.isDarkMode, onDarkModeChange = _a.onDarkModeChange; var isHighResolution = typeof window !== 'undefined' && (window.innerWidth >= 3000 || window.innerHeight >= 2000); var duration = isHighResolution ? Math.max(propsDuration * 0.8, 500) : propsDuration; // Inject base styles when the hook is initialized useEffect(function () { injectBaseStyles(); }, []); var _j = useState(isBrowser ? localStorage.getItem('theme') === 'dark' : false), internalDarkMode = _j[0], setInternalDarkMode = _j[1]; var isDarkMode = externalDarkMode !== null && externalDarkMode !== void 0 ? externalDarkMode : internalDarkMode; var setIsDarkMode = function (value) { var newValue = typeof value === 'function' ? value(isDarkMode) : value; if (onDarkModeChange) { onDarkModeChange(newValue); } else { setInternalDarkMode(newValue); } }; var ref = useRef(null); var createBlurCircleMask = function (blur) { // Using a larger viewBox and centered circle for better scaling var isHighResolution = typeof window !== 'undefined' && (window.innerWidth >= 3000 || window.innerHeight >= 2000); var blurFilter = isHighResolution ? "<filter id=\"blur\"><feGaussianBlur stdDeviation=\"".concat(blur, "\" /></filter>") : "<filter id=\"blur\"><feGaussianBlur stdDeviation=\"".concat(blur, "\" /></filter>"); var circleRadius = isHighResolution ? 20 : 25; return "url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"-50 -50 100 100\"><defs>".concat(blurFilter, "</defs><circle cx=\"0\" cy=\"0\" r=\"").concat(circleRadius, "\" fill=\"white\" filter=\"url(%23blur)\"/></svg>')"); }; var toggleSwitchTheme = function () { return __awaiter(void 0, void 0, void 0, function () { var existingStyle, _a, top, left, width, height, x, y, right, bottom, maxRadius, viewportSize, isHighResolution, scaleFactor, optimalMaskSize, finalMaskPosition, styleElement; return __generator(this, function (_b) { switch (_b.label) { case 0: if (!ref.current || !document.startViewTransition || window.matchMedia('(prefers-reduced-motion: reduce)').matches) { setIsDarkMode(function (isDarkMode) { return !isDarkMode; }); return [2 /*return*/]; } existingStyle = document.getElementById(styleId); if (existingStyle) { existingStyle.remove(); } _a = ref.current.getBoundingClientRect(), top = _a.top, left = _a.left, width = _a.width, height = _a.height; x = left + width / 2; y = top + height / 2; right = window.innerWidth - left; bottom = window.innerHeight - top; maxRadius = Math.hypot(Math.max(left, right), Math.max(top, bottom)); viewportSize = Math.max(window.innerWidth, window.innerHeight); isHighResolution = window.innerWidth >= 3000 || window.innerHeight >= 2000; scaleFactor = isHighResolution ? 2.5 : 4; optimalMaskSize = isHighResolution ? Math.min(viewportSize * scaleFactor, 5000) : viewportSize * scaleFactor; finalMaskPosition = { x: x - optimalMaskSize / 2, y: y - optimalMaskSize / 2, }; if (animationType === ThemeAnimationType.BLUR_CIRCLE) { styleElement = document.createElement('style'); styleElement.id = styleId; styleElement.textContent = "\n ::view-transition-group(root) {\n animation-duration: ".concat(duration, "ms;\n animation-timing-function: ").concat(isHighResolution ? 'cubic-bezier(0.2, 0, 0.2, 1)' : 'linear(' + '0 0%, 0.2342 12.49%, 0.4374 24.99%,' + '0.6093 37.49%, 0.6835 43.74%,' + '0.7499 49.99%, 0.8086 56.25%,' + '0.8593 62.5%, 0.9023 68.75%, 0.9375 75%,' + '0.9648 81.25%, 0.9844 87.5%,' + '0.9961 93.75%, 1 100%' + ')', ";\n will-change: transform;\n }\n \n ::view-transition-new(root) {\n mask: ").concat(createBlurCircleMask(blurAmount), " 0 0 / 100% 100% no-repeat;\n mask-position: ").concat(x, "px ").concat(y, "px;\n animation: maskScale ").concat(duration, "ms ").concat(easing, ";\n transform-origin: ").concat(x, "px ").concat(y, "px;\n will-change: mask-size, mask-position;\n }\n \n ::view-transition-old(root),\n .dark::view-transition-old(root) {\n animation: maskScale ").concat(duration, "ms ").concat(easing, ";\n transform-origin: ").concat(x, "px ").concat(y, "px;\n z-index: -1;\n will-change: mask-size, mask-position;\n }\n \n @keyframes maskScale {\n 0% {\n mask-size: 0px;\n mask-position: ").concat(x, "px ").concat(y, "px;\n }\n 100% {\n mask-size: ").concat(optimalMaskSize, "px;\n mask-position: ").concat(finalMaskPosition.x, "px ").concat(finalMaskPosition.y, "px;\n }\n }\n "); document.head.appendChild(styleElement); } return [4 /*yield*/, document.startViewTransition(function () { flushSync(function () { setIsDarkMode(function (isDarkMode) { return !isDarkMode; }); }); }).ready]; case 1: _b.sent(); if (animationType === ThemeAnimationType.CIRCLE) { document.documentElement.animate({ clipPath: ["circle(0px at ".concat(x, "px ").concat(y, "px)"), "circle(".concat(maxRadius, "px at ").concat(x, "px ").concat(y, "px)")], }, { duration: duration, easing: easing, pseudoElement: pseudoElement, }); } if (animationType === ThemeAnimationType.BLUR_CIRCLE) { setTimeout(function () { var styleElement = document.getElementById(styleId); if (styleElement) { styleElement.remove(); } }, duration); } return [2 /*return*/]; } }); }); }; useEffect(function () { if (isDarkMode) { document.documentElement.classList.add(globalClassName); localStorage.theme = 'dark'; } else { document.documentElement.classList.remove(globalClassName); localStorage.theme = 'light'; } }, [isDarkMode, globalClassName]); return { ref: ref, toggleSwitchTheme: toggleSwitchTheme, isDarkMode: isDarkMode, }; };