From 04b1c3e98761a1af41f9bd8e2b7db68a6f9b79f1 Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Wed, 3 Dec 2025 18:55:49 +0900 Subject: [PATCH] =?UTF-8?q?fix(splash):=20=EB=B0=B0=EA=B2=BD=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EC=BD=98=20=EA=B2=B9=EC=B9=A8=20=EC=B5=9C=EC=86=8C?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/splash/splash_screen.dart | 77 +++++++++++++++++-- 1 file changed, 72 insertions(+), 5 deletions(-) diff --git a/lib/presentation/pages/splash/splash_screen.dart b/lib/presentation/pages/splash/splash_screen.dart index 849b006..796422e 100644 --- a/lib/presentation/pages/splash/splash_screen.dart +++ b/lib/presentation/pages/splash/splash_screen.dart @@ -19,6 +19,8 @@ class _SplashScreenState extends State late List _foodControllers; late AnimationController _questionMarkController; late AnimationController _centerIconController; + List? _iconPositions; + Size? _lastScreenSize; final List foodIcons = [ Icons.rice_bowl, @@ -62,6 +64,64 @@ class _SplashScreenState extends State )..repeat(reverse: true); } + List _generateIconPositions(Size screenSize) { + final random = math.Random(); + const iconSize = 40.0; + const maxScale = 1.5; + const margin = iconSize * maxScale; + const overlapThreshold = 0.3; + final effectiveSize = iconSize * maxScale; + final center = Offset(screenSize.width / 2, screenSize.height / 2); + final centerSafeRadius = + math.min(screenSize.width, screenSize.height) * 0.18; + + Offset randomPosition() { + final x = + margin + + random.nextDouble() * (screenSize.width - margin * 2).clamp(1, 9999); + final y = + margin + + random.nextDouble() * (screenSize.height - margin * 2).clamp(1, 9999); + return Offset(x, y); + } + + final positions = []; + var attempts = 0; + const maxAttempts = 500; + + while (positions.length < foodIcons.length && attempts < maxAttempts) { + attempts++; + final candidate = randomPosition(); + if ((candidate - center).distance < centerSafeRadius) { + continue; + } + final hasHeavyOverlap = positions.any( + (p) => _isOverlapTooHigh(p, candidate, effectiveSize, overlapThreshold), + ); + if (hasHeavyOverlap) { + continue; + } + positions.add(candidate); + } + + while (positions.length < foodIcons.length) { + positions.add(randomPosition()); + } + + return positions; + } + + bool _isOverlapTooHigh(Offset a, Offset b, double size, double maxRatio) { + final dx = (a.dx - b.dx).abs(); + final dy = (a.dy - b.dy).abs(); + final overlapX = math.max(0.0, size - dx); + final overlapY = math.max(0.0, size - dy); + final overlapArea = overlapX * overlapY; + final maxArea = size * size; + if (maxArea == 0) return false; + return overlapArea / maxArea > maxRatio; + } + @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; @@ -145,15 +205,22 @@ class _SplashScreenState extends State } List _buildFoodIcons() { - final random = math.Random(); + final screenSize = MediaQuery.of(context).size; + final sameSize = + _lastScreenSize != null && + (_lastScreenSize!.width - screenSize.width).abs() < 1 && + (_lastScreenSize!.height - screenSize.height).abs() < 1; + if (_iconPositions == null || !sameSize) { + _iconPositions = _generateIconPositions(screenSize); + _lastScreenSize = screenSize; + } return List.generate(foodIcons.length, (index) { - final left = random.nextDouble() * 0.8 + 0.1; - final top = random.nextDouble() * 0.7 + 0.1; + final position = _iconPositions![index]; return Positioned( - left: MediaQuery.of(context).size.width * left, - top: MediaQuery.of(context).size.height * top, + left: position.dx, + top: position.dy, child: FadeTransition( opacity: Tween(begin: 0.2, end: 0.8).animate( CurvedAnimation(