loading-snakes
Version:
Three loading dots that turn into a snake game - The elevator music of chatbots
390 lines (389 loc) • 21.2 kB
JavaScript
;
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;