UNPKG

loading-snakes

Version:

Three loading dots that turn into a snake game - The elevator music of chatbots

390 lines (389 loc) 21.2 kB
"use strict"; var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } return cooked; }; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; Object.defineProperty(exports, "__esModule", { value: true }); var react_1 = __importStar(require("react")); var styled_components_1 = __importStar(require("styled-components")); // Constants var ANIMATION_DURATION = 2; var GRID_SIZE = 20; var UPDATE_INTERVAL = 100; var ANIMATION_DELAY_INCREMENT = 0.15; var DOT_SIZE = 12; // Slightly larger than regular dots var STORAGE_KEY = "loadingSnakesCount"; // Key for localStorage var DEFAULT_DOT_COUNT = 3; // Default starting dot count // Keyframes for the loading animation var dotAnimation = (0, styled_components_1.keyframes)(templateObject_1 || (templateObject_1 = __makeTemplateObject(["\n 0%, 20% {\n opacity: 1;\n }\n 50% {\n opacity: 0;\n }\n 100% {\n opacity: 1;\n }\n"], ["\n 0%, 20% {\n opacity: 1;\n }\n 50% {\n opacity: 0;\n }\n 100% {\n opacity: 1;\n }\n"]))); // Keyframes for the dying animation var dyingAnimation = (0, styled_components_1.keyframes)(templateObject_2 || (templateObject_2 = __makeTemplateObject(["\n 0% {\n opacity: 1;\n transform: scale(1);\n }\n 50% {\n opacity: 0.5;\n transform: scale(0.8) rotate(180deg);\n }\n 100% {\n opacity: 0;\n transform: scale(0) rotate(360deg);\n }\n"], ["\n 0% {\n opacity: 1;\n transform: scale(1);\n }\n 50% {\n opacity: 0.5;\n transform: scale(0.8) rotate(180deg);\n }\n 100% {\n opacity: 0;\n transform: scale(0) rotate(360deg);\n }\n"]))); // Styled component for the loading dots var DotsContainer = styled_components_1.default.div(templateObject_3 || (templateObject_3 = __makeTemplateObject(["\n position: relative;\n display: flex;\n justify-content: center;\n align-items: center;\n min-height: 20px;\n"], ["\n position: relative;\n display: flex;\n justify-content: center;\n align-items: center;\n min-height: 20px;\n"]))); var StyledDot = styled_components_1.default.div(templateObject_6 || (templateObject_6 = __makeTemplateObject(["\n position: fixed;\n width: ", "px;\n height: ", "px;\n margin: 0 2px;\n border-radius: 50%;\n background-color: white;\n left: ", "px;\n top: ", "px;\n transition: ", ";\n\n ", "\n\n ", "\n"], ["\n position: fixed;\n width: ", "px;\n height: ", "px;\n margin: 0 2px;\n border-radius: 50%;\n background-color: white;\n left: ", "px;\n top: ", "px;\n transition: ", ";\n\n ", "\n\n ", "\n"])), DOT_SIZE, DOT_SIZE, function (props) { return props.$left; }, function (props) { return props.$top; }, function (props) { return props.$isTeleporting ? "none" : "left 0.1s linear, top 0.1s linear"; }, function (_a) { var $isAnimating = _a.$isAnimating, $animationDelay = _a.$animationDelay; return $isAnimating && (0, styled_components_1.css)(templateObject_4 || (templateObject_4 = __makeTemplateObject(["\n position: relative; /* Back to relative for animation */\n left: auto; /* Let the flex container handle positioning */\n top: auto;\n animation: ", " ", "s infinite;\n animation-delay: ", "s;\n "], ["\n position: relative; /* Back to relative for animation */\n left: auto; /* Let the flex container handle positioning */\n top: auto;\n animation: ", " ", "s infinite;\n animation-delay: ", "s;\n "])), dotAnimation, ANIMATION_DURATION, $animationDelay); }, function (_a) { var $isDying = _a.$isDying, $animationDelay = _a.$animationDelay; return $isDying && (0, styled_components_1.css)(templateObject_5 || (templateObject_5 = __makeTemplateObject(["\n animation: ", " 1s forwards;\n animation-delay: ", "s;\n "], ["\n animation: ", " 1s forwards;\n animation-delay: ", "s;\n "])), dyingAnimation, $animationDelay * 0.3); }); // Create a forwardRef wrapper for the StyledDot component var Dot = react_1.default.forwardRef(function (props, ref) { return react_1.default.createElement(StyledDot, __assign({}, props, { ref: ref })); }); var generateRandomTarget = function (screenWidth, screenHeight) { var x = Math.floor(Math.random() * screenWidth); var y = Math.floor(Math.random() * screenHeight); // Round to the nearest GRID_SIZE var roundedX = Math.round(x / GRID_SIZE) * GRID_SIZE; var roundedY = Math.round(y / GRID_SIZE) * GRID_SIZE; return { x: roundedX, y: roundedY }; }; // Add this helper function to check for collision var checkCollision = function (snakeHead, target) { // Create hit boxes based on dot size var hitBoxSize = DOT_SIZE; // Calculate distances between centers var distanceX = Math.abs(snakeHead.x - target.x); var distanceY = Math.abs(snakeHead.y - target.y); // Check if the distance is less than or equal to the combined hit box sizes // We consider it a hit if the distance between centers is less than the hitBoxSize return distanceX <= hitBoxSize && distanceY <= hitBoxSize; }; // Helper function to get the dot count from localStorage var getSavedDotCount = function () { try { // Check if we're in a browser environment if (typeof window !== "undefined" && window.localStorage) { var savedCount = localStorage.getItem(STORAGE_KEY); return savedCount ? parseInt(savedCount, 10) : DEFAULT_DOT_COUNT; } return DEFAULT_DOT_COUNT; } catch (error) { console.error("Error reading from localStorage:", error); return DEFAULT_DOT_COUNT; } }; // Helper function to save the dot count to localStorage var saveDotCount = function (count) { try { // Check if we're in a browser environment if (typeof window !== "undefined" && window.localStorage) { localStorage.setItem(STORAGE_KEY, count.toString()); } } catch (error) { console.error("Error writing to localStorage:", error); } }; var TargetDot = styled_components_1.default.div(templateObject_7 || (templateObject_7 = __makeTemplateObject(["\n position: fixed;\n width: ", "px;\n height: ", "px;\n border-radius: 50%;\n background-color: white;\n left: ", "px;\n top: ", "px;\n"], ["\n position: fixed;\n width: ", "px;\n height: ", "px;\n border-radius: 50%;\n background-color: white;\n left: ", "px;\n top: ", "px;\n"])), DOT_SIZE, DOT_SIZE, function (props) { return props.$left; }, function (props) { return props.$top; }); var Target = react_1.default.forwardRef(function (props, ref) { var x = props.x, y = props.y, color = props.color, rest = __rest(props, ["x", "y", "color"]); return react_1.default.createElement(TargetDot, __assign({ "$left": x, "$top": y, "$color": color, ref: ref }, rest)); }); var LoadingSnakes = function () { // Use saved count from localStorage or use default var _a = (0, react_1.useState)(function () { return getSavedDotCount(); }), numDots = _a[0], setNumDots = _a[1]; var directionRef = (0, react_1.useRef)({ x: 1, y: 0 }); var _b = (0, react_1.useState)(false), hasGameStarted = _b[0], setHasGameStarted = _b[1]; var _c = (0, react_1.useState)(false), isDying = _c[0], setIsDying = _c[1]; var _d = (0, react_1.useState)(null), targetPosition = _d[0], setTargetPosition = _d[1]; // Initialize with default values to avoid window access during initial render var _e = (0, react_1.useState)({ width: 1024, // Default fallback width height: 768, // Default fallback height }), screenDimensions = _e[0], setScreenDimensions = _e[1]; var headDotRef = (0, react_1.useRef)(null); var targetDotRef = (0, react_1.useRef)(null); var dotsContainerRef = (0, react_1.useRef)(null); var dotColor = "white"; var getDefaultPositions = (0, react_1.useCallback)(function () { return Array(numDots) .fill(null) .map(function (_, index) { return ({ x: (numDots - 1 - index) * GRID_SIZE, y: 0, }); }); }, [numDots]); var _f = (0, react_1.useState)(getDefaultPositions), positions = _f[0], setPositions = _f[1]; // Function to reset the game when snake hits itself var resetGame = (0, react_1.useCallback)(function () { // Reset to DEFAULT_DOT_COUNT dots setNumDots(DEFAULT_DOT_COUNT); saveDotCount(DEFAULT_DOT_COUNT); // Stop the game and return to loading animation setHasGameStarted(false); setIsDying(false); // Clear target setTargetPosition(null); // Reset direction directionRef.current = { x: -1, y: 0 }; // Reset positions to default setPositions(Array(DEFAULT_DOT_COUNT) .fill(null) .map(function (_, index) { return ({ x: (DEFAULT_DOT_COUNT - 1 - index) * GRID_SIZE, y: 0, }); })); }, []); // Check if snake collides with itself var checkSelfCollision = (0, react_1.useCallback)(function (head, body) { // Start from 4th segment (index 3) because it's impossible to collide with the first few segments for (var i = 3; i < body.length; i++) { // Using similar collision detection as with target var distanceX = Math.abs(head.x - body[i].x); var distanceY = Math.abs(head.y - body[i].y); // Consider it a collision if centers are very close if (distanceX < DOT_SIZE / 2 && distanceY < DOT_SIZE / 2) { return true; } } return false; }, []); // Calculate positions of dots based on container position and flex layout var calculateDotsPositions = (0, react_1.useCallback)(function () { if (!dotsContainerRef.current) { throw new Error("Container ref not set"); } // Get container rectangle var containerRect = dotsContainerRef.current.getBoundingClientRect(); // In a flex container with justify-content: center, dots will be centered horizontally var singleDotWidth = DOT_SIZE + 4; // DOT_SIZE + margin var totalDotsWidth = numDots * singleDotWidth; // Calculate starting X position for first dot (center alignment logic) var startX = containerRect.left + (containerRect.width - totalDotsWidth) / 2; // Create positions array with calculated values // Reverse the order so head (index 0) is on the right return Array(numDots) .fill(null) .map(function (_, index) { return ({ x: startX + (numDots - 1 - index) * singleDotWidth, y: containerRect.top + containerRect.height / 2 - DOT_SIZE / 2, }); }); }, [numDots]); // Update screen dimensions on component mount and window resize (0, react_1.useEffect)(function () { // Safe to access window here as this only runs on the client setScreenDimensions({ width: window.innerWidth, height: window.innerHeight, }); var handleResize = function () { setScreenDimensions({ width: window.innerWidth, height: window.innerHeight, }); }; window.addEventListener("resize", handleResize); return function () { return window.removeEventListener("resize", handleResize); }; }, []); // Update positions during game play var updatePositions = (0, react_1.useCallback)(function () { if (!hasGameStarted || isDying) return; setPositions(function (prevPositions) { // Create copy for modification var newPositions = __spreadArray([], prevPositions, true); // Save the last position in case we need to grow var lastPosition = __assign({}, newPositions[newPositions.length - 1]); // Calculate new head position var newHead = { x: newPositions[0].x + directionRef.current.x * GRID_SIZE, y: newPositions[0].y + directionRef.current.y * GRID_SIZE, isTeleporting: false, }; // Apply screen wrap-around - if the snake goes off the screen, it appears on the opposite side if (newHead.x < 0) { newHead.x = screenDimensions.width - GRID_SIZE; newHead.isTeleporting = true; } else if (newHead.x >= screenDimensions.width) { newHead.x = 0; newHead.isTeleporting = true; } if (newHead.y < 0) { newHead.y = screenDimensions.height - GRID_SIZE; newHead.isTeleporting = true; } else if (newHead.y >= screenDimensions.height) { newHead.y = 0; newHead.isTeleporting = true; } // Move body segments - each segment takes the position of the segment in front of it for (var i = newPositions.length - 1; i > 0; i--) { newPositions[i] = __assign(__assign({}, newPositions[i - 1]), { // If the segment ahead is teleporting, this segment should also teleport // This propagates the teleporting state through the entire snake isTeleporting: newPositions[i - 1].isTeleporting || (i === 1 && newHead.isTeleporting) }); } // Set new head position newPositions[0] = newHead; // Check for collision with target if (targetPosition && checkCollision(newHead, targetPosition)) { setTargetPosition(generateRandomTarget(screenDimensions.width, screenDimensions.height)); var newDotCount = numDots + 1; setNumDots(newDotCount); saveDotCount(newDotCount); // Save to localStorage when count changes newPositions.push(lastPosition); } // Check for collision with self - use the updated positions that include the new head if (checkSelfCollision(newHead, newPositions)) { // Set dying state and schedule reset with a delay setIsDying(true); setTimeout(resetGame, 1500); // 1.5 second delay to allow animation to play return newPositions; } return newPositions; }); }, [ hasGameStarted, isDying, targetPosition, numDots, screenDimensions, checkSelfCollision, resetGame, ]); (0, react_1.useEffect)(function () { if (!hasGameStarted) return; var interval = setInterval(updatePositions, UPDATE_INTERVAL); return function () { return clearInterval(interval); }; }, [updatePositions, hasGameStarted]); (0, react_1.useEffect)(function () { // Only set up event listeners if in browser environment if (typeof window === "undefined") return; var handleKeyDown = function (e) { if (!["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) { return; } // Prevent default scrolling behavior e.preventDefault(); // Don't process inputs if the snake is dying if (isDying) return; if (!hasGameStarted) { // Calculate positions based on container and flex layout var calculatedPositions = calculateDotsPositions(); // Update positions state with calculated values setPositions(calculatedPositions); // Start the game setHasGameStarted(true); // Set initial target setTargetPosition(generateRandomTarget(screenDimensions.width, screenDimensions.height)); } switch (e.key) { case "ArrowUp": if (directionRef.current.y !== 1) { directionRef.current = { x: 0, y: -1 }; } break; case "ArrowDown": if (directionRef.current.y !== -1) { directionRef.current = { x: 0, y: 1 }; } break; case "ArrowLeft": if (directionRef.current.x !== 1) { directionRef.current = { x: -1, y: 0 }; } break; case "ArrowRight": if (directionRef.current.x !== -1) { directionRef.current = { x: 1, y: 0 }; } break; } }; window.addEventListener("keydown", handleKeyDown); return function () { return window.removeEventListener("keydown", handleKeyDown); }; }, [hasGameStarted, isDying, screenDimensions, calculateDotsPositions]); // Save dot count when component unmounts (0, react_1.useEffect)(function () { return function () { saveDotCount(numDots); }; }, [numDots]); // Validate target position is within screen bounds (0, react_1.useEffect)(function () { if (targetPosition && targetDotRef.current) { var rect = targetDotRef.current.getBoundingClientRect(); // Check if target is outside visible area if (rect.right > screenDimensions.width || rect.left < 0 || rect.bottom > screenDimensions.height || rect.top < 0) { console.log("Target outside visible area resetting"); setTargetPosition(generateRandomTarget(screenDimensions.width, screenDimensions.height)); } } }, [targetPosition, screenDimensions]); return (react_1.default.createElement(DotsContainer, { ref: dotsContainerRef }, targetPosition && !isDying && (react_1.default.createElement(Target, { ref: targetDotRef, x: targetPosition.x, y: targetPosition.y, color: dotColor })), positions.map(function (pos, index) { return (react_1.default.createElement(Dot, { key: index, ref: index === 0 ? headDotRef : null, "$isAnimating": !hasGameStarted && !isDying, "$isDying": isDying, "$left": pos.x, "$top": pos.y, "$isTeleporting": pos.isTeleporting, "$animationDelay": index * ANIMATION_DELAY_INCREMENT, style: { backgroundColor: dotColor } })); }))); }; // const LoadingSnakes: React.FC = () => { // const [numDots, setNumDots] = useState(5); // return <div>LoadingSnakes numDots: {numDots}</div>; // }; exports.default = LoadingSnakes; var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7;