From 5675a2cf46a37a4c7021e5174180ebc7e756cae6 Mon Sep 17 00:00:00 2001 From: Calcitem Date: Tue, 1 Oct 2024 08:25:28 +0800 Subject: [PATCH] ui: Implement piece animations --- .../models/display_settings.dart | 5 +- .../widgets/appearance_settings_page.dart | 10 - .../ai_response_delay_time_slider.dart | 54 ---- .../services/animation/animation_manager.dart | 155 +++++++-- .../services/controller/game_controller.dart | 9 +- .../controller/history_navigation.dart | 8 + .../services/controller/tap_handler.dart | 5 - .../lib/game_page/services/engine/game.dart | 1 + .../game_page/services/engine/position.dart | 13 + .../game_page/services/painters/painters.dart | 1 + .../services/painters/piece_painter.dart | 303 +++++++++++++++--- .../lib/game_page/widgets/game_board.dart | 16 +- .../tutorial/painters/tutorial_painter.dart | 3 +- src/ui/flutter_app/pubspec.yaml | 1 - 14 files changed, 434 insertions(+), 150 deletions(-) delete mode 100644 src/ui/flutter_app/lib/appearance_settings/widgets/sliders/ai_response_delay_time_slider.dart diff --git a/src/ui/flutter_app/lib/appearance_settings/models/display_settings.dart b/src/ui/flutter_app/lib/appearance_settings/models/display_settings.dart index a6d0a670b..c3f8ecece 100644 --- a/src/ui/flutter_app/lib/appearance_settings/models/display_settings.dart +++ b/src/ui/flutter_app/lib/appearance_settings/models/display_settings.dart @@ -63,7 +63,7 @@ class DisplaySettings { this.fontScale = 1.0, this.boardTop = kToolbarHeight, this.animationDuration = 0.0, - this.aiResponseDelayTime = 0.0, + @Deprecated("Deprecated.") this.aiResponseDelayTime = 0.0, this.isPositionalAdvantageIndicatorShown = false, this.backgroundImagePath = '', this.isNumbersOnPiecesShown = false, @@ -119,7 +119,7 @@ class DisplaySettings { @HiveField(11) final double boardTop; - @HiveField(12) + @HiveField(12, defaultValue: 1.0) final double animationDuration; @HiveField(13) @@ -141,6 +141,7 @@ class DisplaySettings { @HiveField(17, defaultValue: false) final bool isFullScreen; + @Deprecated("Deprecated.") @HiveField(18, defaultValue: 0.0) final double aiResponseDelayTime; diff --git a/src/ui/flutter_app/lib/appearance_settings/widgets/appearance_settings_page.dart b/src/ui/flutter_app/lib/appearance_settings/widgets/appearance_settings_page.dart index fc51d3949..0a50e285a 100644 --- a/src/ui/flutter_app/lib/appearance_settings/widgets/appearance_settings_page.dart +++ b/src/ui/flutter_app/lib/appearance_settings/widgets/appearance_settings_page.dart @@ -39,7 +39,6 @@ part 'package:sanmill/appearance_settings/widgets/modals/point_painting_style_mo part 'package:sanmill/appearance_settings/widgets/pickers/background_image_picker.dart'; part 'package:sanmill/appearance_settings/widgets/pickers/piece_image_picker.dart'; part 'package:sanmill/appearance_settings/widgets/pickers/language_picker.dart'; -part 'package:sanmill/appearance_settings/widgets/sliders/ai_response_delay_time_slider.dart'; part 'package:sanmill/appearance_settings/widgets/sliders/animation_duration_slider.dart'; part 'package:sanmill/appearance_settings/widgets/sliders/board_boarder_line_width_slider.dart'; part 'package:sanmill/appearance_settings/widgets/sliders/board_inner_line_width_slider.dart'; @@ -106,11 +105,6 @@ class AppearanceSettingsPage extends StatelessWidget { builder: (_) => const _AnimationDurationSlider(), ); - void setAiResponseDelayTime(BuildContext context) => showModalBottomSheet( - context: context, - builder: (_) => const _AiResponseDelayTimeSlider(), - ); - void setBackgroundImage(BuildContext context) => showModalBottomSheet( context: context, builder: (_) => const _BackgroundImagePicker(), @@ -428,10 +422,6 @@ class AppearanceSettingsPage extends StatelessWidget { titleString: S.of(context).animationDuration, onTap: () => setAnimationDuration(context), ), - SettingsListTile( - titleString: S.of(context).aiResponseDelayTime, - onTap: () => setAiResponseDelayTime(context), - ), ], ); } diff --git a/src/ui/flutter_app/lib/appearance_settings/widgets/sliders/ai_response_delay_time_slider.dart b/src/ui/flutter_app/lib/appearance_settings/widgets/sliders/ai_response_delay_time_slider.dart deleted file mode 100644 index ec79142ec..000000000 --- a/src/ui/flutter_app/lib/appearance_settings/widgets/sliders/ai_response_delay_time_slider.dart +++ /dev/null @@ -1,54 +0,0 @@ -// This file is part of Sanmill. -// Copyright (C) 2019-2024 The Sanmill developers (see AUTHORS file) -// -// Sanmill is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Sanmill is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -part of 'package:sanmill/appearance_settings/widgets/appearance_settings_page.dart'; - -class _AiResponseDelayTimeSlider extends StatelessWidget { - const _AiResponseDelayTimeSlider(); - - @override - Widget build(BuildContext context) { - return Semantics( - label: S.of(context).aiResponseDelayTime, - child: ValueListenableBuilder>( - valueListenable: DB().listenDisplaySettings, - builder: (BuildContext context, Box box, _) { - final DisplaySettings displaySettings = box.get( - DB.displaySettingsKey, - defaultValue: const DisplaySettings(), - )!; - - return Center( - child: SizedBox( - width: MediaQuery.of(context).size.width * 0.8, - child: Slider( - value: displaySettings.aiResponseDelayTime, - max: 15.0, - divisions: 15, - label: displaySettings.aiResponseDelayTime.toStringAsFixed(1), - onChanged: (double value) { - logger.t("[config] AiResponseDelayTime value: $value"); - DB().displaySettings = - displaySettings.copyWith(aiResponseDelayTime: value); - }, - ), - ), - ); - }, - ), - ); - } -} diff --git a/src/ui/flutter_app/lib/game_page/services/animation/animation_manager.dart b/src/ui/flutter_app/lib/game_page/services/animation/animation_manager.dart index b808c6cbf..847897ff1 100644 --- a/src/ui/flutter_app/lib/game_page/services/animation/animation_manager.dart +++ b/src/ui/flutter_app/lib/game_page/services/animation/animation_manager.dart @@ -17,46 +17,165 @@ import 'package:flutter/material.dart'; import '../../../shared/database/database.dart'; +import '../mill.dart'; class AnimationManager { AnimationManager(this.vsync) { - _initAnimation(); + _initPlaceAnimation(); + _initMoveAnimation(); + _initRemoveAnimation(); } final TickerProvider vsync; - late final AnimationController _animationController; - late final Animation _animation; + bool allowAnimations = true; - AnimationController get animationController => _animationController; - Animation get animation => _animation; + // Place Animation + late final AnimationController _placeAnimationController; + late final Animation _placeAnimation; - void _initAnimation() { - // TODO: Check _initAnimation on branch master - _animationController = AnimationController( + AnimationController get placeAnimationController => _placeAnimationController; + Animation get placeAnimation => _placeAnimation; + + // Move Animation + late final AnimationController _moveAnimationController; + late final Animation _moveAnimation; + + AnimationController get moveAnimationController => _moveAnimationController; + Animation get moveAnimation => _moveAnimation; + + // Remove Animation + late final AnimationController _removeAnimationController; + late final Animation _removeAnimation; + + AnimationController get removeAnimationController => + _removeAnimationController; + Animation get removeAnimation => _removeAnimation; + + void _initPlaceAnimation() { + _placeAnimationController = AnimationController( + vsync: vsync, + duration: Duration( + milliseconds: (DB().displaySettings.animationDuration * 1000).toInt(), + ), + ); + + _placeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _placeAnimationController, + curve: Curves.easeOutCubic, + ), + ); + } + + void _initMoveAnimation() { + _moveAnimationController = AnimationController( vsync: vsync, duration: Duration( - seconds: DB().displaySettings.animationDuration.toInt(), + milliseconds: (DB().displaySettings.animationDuration * 1000).toInt(), ), ); - _animation = - Tween(begin: 1.27, end: 1.0).animate(_animationController); + _moveAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _moveAnimationController, + curve: Curves.easeOutCubic, + ), + ); + } + + void _initRemoveAnimation() { + _removeAnimationController = AnimationController( + vsync: vsync, + duration: Duration( + milliseconds: (DB().displaySettings.animationDuration * 1000).toInt(), + ), + ); + + _removeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _removeAnimationController, + curve: Curves.easeOutCubic, + ), + ); } void dispose() { - _animationController.dispose(); + _placeAnimationController.dispose(); + _moveAnimationController.dispose(); + _removeAnimationController.dispose(); + } + + void resetPlaceAnimation() { + _placeAnimationController.reset(); + } + + void forwardPlaceAnimation() { + _placeAnimationController.forward(); + } + + void resetMoveAnimation() { + _moveAnimationController.reset(); + } + + void forwardMoveAnimation() { + _moveAnimationController.forward(); } - void resetAnimation() { - _animationController.reset(); + void resetRemoveAnimation() { + _removeAnimationController.reset(); } - void forwardAnimation() { - _animationController.forward(); + void forwardRemoveAnimation() { + _removeAnimationController.forward(); } - void animateToEnd() { - _animationController.animateTo(1.0); + void animatePlace() { + if (GameController().isDisposed == true) { + // TODO: See f0c1f3d5df544e5910b194b8479d956dd10fe527 + //return; + } + + if (allowAnimations) { + resetPlaceAnimation(); + forwardPlaceAnimation(); + } + } + + bool isRemoveAnimationAnimating() { + if (_removeAnimationController.isAnimating) { + return true; + } + return false; + } + + void animateMove() { + if (GameController().isDisposed == true) { + // TODO: See f0c1f3d5df544e5910b194b8479d956dd10fe527 + //return; + } + + if (allowAnimations) { + resetMoveAnimation(); + forwardMoveAnimation(); + } + } + + void animateRemove() { + if (GameController().isDisposed == true) { + // TODO: See f0c1f3d5df544e5910b194b8479d956dd10fe527 + //return; + } + if (allowAnimations) { + resetRemoveAnimation(); + + _removeAnimationController.addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.completed) { + GameController().gameInstance.removeIndex = null; + } + }); + + forwardRemoveAnimation(); + } } } diff --git a/src/ui/flutter_app/lib/game_page/services/controller/game_controller.dart b/src/ui/flutter_app/lib/game_page/services/controller/game_controller.dart index cd70e20c8..9e68cc787 100644 --- a/src/ui/flutter_app/lib/game_page/services/controller/game_controller.dart +++ b/src/ui/flutter_app/lib/game_page/services/controller/game_controller.dart @@ -248,7 +248,9 @@ class GameController { 0) { isEngineInDelay = true; await Future.delayed(Duration( - seconds: DB().displaySettings.aiResponseDelayTime.toInt())); + milliseconds: + (DB().displaySettings.animationDuration * 1000).toInt(), + )); isEngineInDelay = false; } @@ -269,11 +271,6 @@ class GameController { loopIsFirst = false; searched = true; - if (GameController().isDisposed == false) { - GameController().animationManager.resetAnimation(); - GameController().animationManager.animateToEnd(); - } - // TODO: Do not use BuildContexts across async gaps. if (DB().generalSettings.screenReaderSupport) { rootScaffoldMessengerKey.currentState!.showSnackBar( diff --git a/src/ui/flutter_app/lib/game_page/services/controller/history_navigation.dart b/src/ui/flutter_app/lib/game_page/services/controller/history_navigation.dart index 0ec447486..7764be56d 100644 --- a/src/ui/flutter_app/lib/game_page/services/controller/history_navigation.dart +++ b/src/ui/flutter_app/lib/game_page/services/controller/history_navigation.dart @@ -81,9 +81,17 @@ class HistoryNavigator { SoundManager().mute(); + if (navMode == HistoryNavMode.takeBackAll || + navMode == HistoryNavMode.takeBackN || + navMode == HistoryNavMode.takeBack) { + GameController().animationManager.allowAnimations = false; + } + final HistoryResponse resp = await doEachMove(navMode, number); // doMove() to index + GameController().animationManager.allowAnimations = true; + switch (resp) { case HistoryOK(): final ExtMove? lastEffectiveMove = controller.gameRecorder.current; diff --git a/src/ui/flutter_app/lib/game_page/services/controller/tap_handler.dart b/src/ui/flutter_app/lib/game_page/services/controller/tap_handler.dart index 4e524620d..7a49005ba 100644 --- a/src/ui/flutter_app/lib/game_page/services/controller/tap_handler.dart +++ b/src/ui/flutter_app/lib/game_page/services/controller/tap_handler.dart @@ -120,8 +120,6 @@ class TapHandler { switch (GameController().position.action) { case Act.place: if (GameController().position._putPiece(sq)) { - GameController().animationManager.resetAnimation(); - GameController().animationManager.animateToEnd(); if (GameController().position.action == Act.remove) { if (GameController() .position @@ -391,9 +389,6 @@ class TapHandler { final GameResponse removeRet = GameController().position._removePiece(sq); - //GameController().animationManager.resetAnimation(); - //GameController().animationManager.animateToEnd(); - switch (removeRet) { case GameResponseOK(): ret = true; diff --git a/src/ui/flutter_app/lib/game_page/services/engine/game.dart b/src/ui/flutter_app/lib/game_page/services/engine/game.dart index f99adc331..3de10e9dd 100644 --- a/src/ui/flutter_app/lib/game_page/services/engine/game.dart +++ b/src/ui/flutter_app/lib/game_page/services/engine/game.dart @@ -39,6 +39,7 @@ class Game { int? focusIndex; int? blurIndex; + int? removeIndex; final List players = [ Player(color: PieceColor.white, isAi: false), diff --git a/src/ui/flutter_app/lib/game_page/services/engine/position.dart b/src/ui/flutter_app/lib/game_page/services/engine/position.dart index 28cb4ed29..0e9da2e9e 100644 --- a/src/ui/flutter_app/lib/game_page/services/engine/position.dart +++ b/src/ui/flutter_app/lib/game_page/services/engine/position.dart @@ -572,6 +572,9 @@ class Position { if (_removePiece(m.to) == const GameResponseOK()) { ret = true; st.rule50 = 0; + + GameController().gameInstance.removeIndex = squareToIndex[m.to]; + GameController().animationManager.animateRemove(); } else { return false; } @@ -584,6 +587,8 @@ class Position { ret = _movePiece(m.from, m.to); if (ret) { ++st.rule50; + GameController().gameInstance.removeIndex = null; + GameController().animationManager.animateMove(); } break; case MoveType.place: @@ -591,6 +596,10 @@ class Position { if (ret) { // Reset rule 50 counter st.rule50 = 0; + GameController().gameInstance.removeIndex = null; + //GameController().gameInstance.focusIndex = squareToIndex[m.to]; + //GameController().gameInstance.blurIndex = squareToIndex[m.from]; + GameController().animationManager.animatePlace(); } break; case MoveType.draw: @@ -1143,6 +1152,10 @@ class Position { logger.i("[position] Game over, $w win, because of $reason"); _updateScore(); + + GameController().gameInstance.focusIndex = null; + GameController().gameInstance.blurIndex = null; + GameController().gameInstance.removeIndex = null; } void _updateScore() { diff --git a/src/ui/flutter_app/lib/game_page/services/painters/painters.dart b/src/ui/flutter_app/lib/game_page/services/painters/painters.dart index 7c9c27a2a..1e2d10f23 100644 --- a/src/ui/flutter_app/lib/game_page/services/painters/painters.dart +++ b/src/ui/flutter_app/lib/game_page/services/painters/painters.dart @@ -17,6 +17,7 @@ /// Although marked as a library this package is tightly integrated into the app library painters; +import 'dart:math'; import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; diff --git a/src/ui/flutter_app/lib/game_page/services/painters/piece_painter.dart b/src/ui/flutter_app/lib/game_page/services/painters/piece_painter.dart index 390493e52..7d8889b5a 100644 --- a/src/ui/flutter_app/lib/game_page/services/painters/piece_painter.dart +++ b/src/ui/flutter_app/lib/game_page/services/painters/piece_painter.dart @@ -24,8 +24,8 @@ class Piece { const Piece({ required this.pieceColor, required this.pos, - required this.animated, required this.diameter, + required this.index, this.squareAttribute, this.image, }); @@ -33,37 +33,43 @@ class Piece { /// The color of the piece. final PieceColor pieceColor; - /// The position the piece is placed at. - /// - /// This represents the final position on the canvas. - /// To extract this information from the board index use [pointFromIndex]. + /// The position of the piece on the canvas. final Offset pos; - final bool animated; + + /// The diameter of the piece. final double diameter; + + /// The index of the piece. + final int index; + final SquareAttribute? squareAttribute; - final ui.Image? image; // Change Image to ui.Image + final ui.Image? image; } /// Custom Piece Painter /// -/// Painter to draw each piece in [GameController.position] on the Board. -/// The board is drawn by [BoardPainter]. +/// Painter to draw each piece on the board. /// It asserts the Canvas to be a square. class PiecePainter extends CustomPainter { PiecePainter({ - required this.animationValue, - required this.pieceImages, // Add pieceImages parameter + required this.placeAnimationValue, + required this.moveAnimationValue, + required this.removeAnimationValue, + required this.pieceImages, }); - /// The value representing the piece animation when placing. - final double animationValue; - final Map? pieceImages; // Add pieceImages field + final double placeAnimationValue; + final double moveAnimationValue; + final double removeAnimationValue; + + final Map? pieceImages; @override void paint(Canvas canvas, Size size) { assert(size.width == size.height); final int? focusIndex = GameController().gameInstance.focusIndex; final int? blurIndex = GameController().gameInstance.blurIndex; + final int? removeIndex = GameController().gameInstance.removeIndex; final Paint paint = Paint(); final Path shadowPath = Path(); @@ -73,18 +79,75 @@ class PiecePainter extends CustomPainter { DB().displaySettings.pieceWidth / 6 - 1; - final double animatedPieceWidth = pieceWidth * animationValue; + + // Variable to hold the current position of the moving piece + Offset? movingPos; // Draw pieces on board for (int row = 0; row < 7; row++) { for (int col = 0; col < 7; col++) { final int index = row * 7 + col; - final PieceColor piece = GameController() - .position - .pieceOnGrid(index); // No Pieces when initial + final PieceColor pieceColor = + GameController().position.pieceOnGrid(index); + + Offset pos; + + // Check if this piece is currently placing + final bool isPlacingPiece = (placeAnimationValue < 1.0) && + (focusIndex != null) && + (blurIndex == null) && + (index == focusIndex); + + // Check if this piece is currently moving + final bool isMovingPiece = (moveAnimationValue < 1.0) && + (focusIndex != null) && + (blurIndex != null) && + (index == focusIndex) && + !GameController().animationManager.isRemoveAnimationAnimating(); + + // Check if this piece is currently being removed + final bool isRemovingPiece = (removeAnimationValue < 1.0) && + (removeIndex != null) && + (index == removeIndex); + + if (isPlacingPiece) { + pos = pointFromIndex(index, size); + + drawPlaceEffect( + canvas, + pos, + pieceWidth, + placeAnimationValue, + ); + //continue; // Skip normal drawing + } + + if (isMovingPiece) { + // Calculate interpolated position between blurIndex and focusIndex + final Offset fromPos = pointFromIndex(blurIndex, size); + final Offset toPos = pointFromIndex(focusIndex, size); + + pos = Offset.lerp(fromPos, toPos, moveAnimationValue)!; + + // Store the moving piece's current position for highlight + movingPos = pos; + } else { + // Use the normal position + pos = pointFromIndex(index, size); + } + + if (isRemovingPiece) { + drawRemoveEffect( + canvas, + pos, + pieceWidth, + removeAnimationValue, + ); + continue; // Skip normal drawing + } - if (piece == PieceColor.none) { + if (pieceColor == PieceColor.none) { continue; } @@ -92,19 +155,17 @@ class PiecePainter extends CustomPainter { final SquareAttribute squareAttribute = GameController().position.sqAttrList[sq]; - final Offset pos = pointFromIndex(index, size); - final bool animated = focusIndex == index; + final ui.Image? image = + pieceImages == null ? null : pieceImages?[pieceColor]; - final ui.Image? image = pieceImages == null - ? null - : pieceImages?[piece]; // Get image from pieceImages + final double adjustedPieceWidth = pieceWidth; piecesToDraw.add( Piece( - pieceColor: piece, + pieceColor: pieceColor, pos: pos, - animated: animated, - diameter: pieceWidth, + diameter: adjustedPieceWidth, + index: index, squareAttribute: squareAttribute, image: image, ), @@ -113,13 +174,13 @@ class PiecePainter extends CustomPainter { shadowPath.addOval( Rect.fromCircle( center: pos, - radius: (animated ? animatedPieceWidth : pieceWidth) / 2, + radius: adjustedPieceWidth / 2, ), ); } } - // Draw shadow of piece if image is not available + // Draw shadow of pieces if image is not available if (pieceImages == null) { canvas.drawShadow(shadowPath, Colors.black, 2, true); } @@ -130,27 +191,24 @@ class PiecePainter extends CustomPainter { for (final Piece piece in piecesToDraw) { blurPositionColor = piece.pieceColor.blurPositionColor; - final double pieceRadius = pieceWidth / 2; - final double pieceInnerRadius = pieceRadius * 0.99; + const double opacity = 1.0; - final double animatedPieceRadius = animatedPieceWidth / 2; - final double animatedPieceInnerRadius = animatedPieceRadius * 0.99; + final double pieceRadius = piece.diameter / 2; + final double pieceInnerRadius = pieceRadius * 0.99; - // Draw the piece image if available, otherwise draw the color if (piece.image != null) { paintImage( canvas: canvas, rect: Rect.fromCircle( center: piece.pos, - radius: - piece.animated ? animatedPieceInnerRadius : pieceInnerRadius, + radius: pieceInnerRadius, ), image: piece.image!, fit: BoxFit.cover, ); } else { - // Draw Border of Piece - paint.color = piece.pieceColor.borderColor; + // Draw border of the piece + paint.color = piece.pieceColor.borderColor.withOpacity(opacity); if (DB().colorSettings.boardBackgroundColor == Colors.white) { paint.style = PaintingStyle.stroke; @@ -161,19 +219,21 @@ class PiecePainter extends CustomPainter { canvas.drawCircle( piece.pos, - piece.animated ? animatedPieceRadius : pieceRadius, + pieceRadius, paint, ); + // Fill the piece with main color paint.style = PaintingStyle.fill; - paint.color = piece.pieceColor.mainColor; + paint.color = piece.pieceColor.mainColor.withOpacity(opacity); canvas.drawCircle( piece.pos, - piece.animated ? animatedPieceInnerRadius : pieceInnerRadius, + pieceInnerRadius, paint, ); } + // Draw numbers on pieces if enabled if (DB().displaySettings.isNumbersOnPiecesShown && piece.squareAttribute?.placedPieceNumber != null && piece.squareAttribute!.placedPieceNumber > 0) { @@ -185,7 +245,7 @@ class PiecePainter extends CustomPainter { color: piece.pieceColor.mainColor.computeLuminance() > 0.5 ? Colors.black : Colors.white, - fontSize: piece.diameter * 0.5, // Adjust font size as needed + fontSize: piece.diameter * 0.5, ), ), textAlign: TextAlign.center, @@ -203,16 +263,19 @@ class PiecePainter extends CustomPainter { } } - // Draw focus and blur position + // Draw focus and blur positions if (focusIndex != null && GameController().gameInstance.gameMode != GameMode.setupPosition) { paint.color = DB().colorSettings.pieceHighlightColor; paint.style = PaintingStyle.stroke; paint.strokeWidth = 2; + // If the piece is moving, use the interpolated position for highlight + final Offset focusPos = movingPos ?? pointFromIndex(focusIndex, size); + canvas.drawCircle( - pointFromIndex(focusIndex, size), - animatedPieceWidth / 2, + focusPos, + pieceWidth / 2, paint, ); } @@ -227,15 +290,161 @@ class PiecePainter extends CustomPainter { paint.color = blurPositionColor; paint.style = PaintingStyle.fill; + // If the piece is moving, optionally update blur position if needed + // Here, assuming blur remains at the original position canvas.drawCircle( pointFromIndex(blurIndex, size), - animatedPieceWidth / 2 * 0.8, + pieceWidth / 2 * 0.8, paint, ); } } + void drawPlaceEffect( + Canvas canvas, + Offset center, + double diameter, + double animationValue, + ) { + if (DB().displaySettings.animationDuration == 0.0) { + return; + } + + // Apply easing to the animation value + final double easedAnimation = Curves.easeOut.transform(animationValue); + + // Calculate the maximum and current radius based on the diameter and animation + final double maxRadius = diameter * 0.25; + final double currentRadius = diameter + maxRadius * easedAnimation; + + // Define the main and secondary opacities + final double mainOpacity = 0.6 * (1.0 - easedAnimation); + final double secondOpacity = mainOpacity * 0.8; + + // Cache the board line color to avoid repeated calls + final ui.Color boardLineColor = DB().colorSettings.boardLineColor; + + // Define the configuration for each effect layer + final List<_EffectLayer> layers = <_EffectLayer>[ + // Main layer + _EffectLayer( + radiusFactor: 1.0, + opacityFactor: 0.8, + ), + // Second layer + _EffectLayer( + radiusFactor: 0.75, + opacityFactor: 0.5, + ), + // Third layer + _EffectLayer( + radiusFactor: 0.5, + opacityFactor: 0.2, + ), + ]; + + // Iterate over each layer configuration to draw the circles + for (final _EffectLayer layer in layers) { + // Determine the radius for the current layer + final double layerRadius = currentRadius * layer.radiusFactor; + + // Determine the opacity for the current layer + double layerOpacity; + if (layer.opacityFactor == 1.0) { + layerOpacity = mainOpacity; + } else if (layer.opacityFactor == 0.8) { + layerOpacity = secondOpacity; + } else { + layerOpacity = mainOpacity * layer.opacityFactor; + } + + // Create the paint with a radial gradient shader + final Paint paint = Paint() + ..shader = RadialGradient( + colors: [ + boardLineColor.withOpacity(layerOpacity), + boardLineColor.withOpacity(0.0), + ], + stops: const [ + 0.0, + 1.0, + ], + ).createShader(Rect.fromCircle(center: center, radius: layerRadius)) + ..style = PaintingStyle.fill; + + // Draw the circle on the canvas + canvas.drawCircle(center, layerRadius, paint); + } + } + + void drawRemoveEffect( + Canvas canvas, + Offset center, + double diameter, + double animationValue, + ) { + if (DB().displaySettings.animationDuration == 0.0) { + return; + } + + final int numParticles = DB().ruleSettings.piecesCount; + final double maxDistance = diameter * 3; + final double particleMaxSize = diameter * 0.12; + final double particleMinSize = diameter * 0.05; + + final double time = Curves.easeOut.transform(animationValue); + + final int seed = DateTime.now().millisecondsSinceEpoch; + final Random random = Random(seed); + + for (int i = 0; i < numParticles; i++) { + final double angle = + (i / numParticles) * 2 * pi + random.nextDouble() * 0.2; + final double speed = 0.5 + random.nextDouble() * 0.4; + + final double distance = speed * time * maxDistance; + final Offset offset = Offset(cos(angle), sin(angle)) * distance; + final Offset particlePos = center + offset; + + final double opacity = (1.0 - time).clamp(0.0, 1.0); + + final Color particleColor = HSVColor.fromAHSV( + opacity, + random.nextDouble() * 360, + 1.0, + 1.0, + ).toColor(); + + final Paint particlePaint = Paint() + ..color = particleColor + ..style = PaintingStyle.fill; + + final double particleSize = particleMinSize + + (particleMaxSize - particleMinSize) * + (1.0 - time) * + (0.8 + random.nextDouble() * 0.4); + + canvas.drawCircle(particlePos, particleSize, particlePaint); + } + } + @override bool shouldRepaint(PiecePainter oldDelegate) => - animationValue != oldDelegate.animationValue; + placeAnimationValue != oldDelegate.placeAnimationValue || + moveAnimationValue != oldDelegate.moveAnimationValue || + removeAnimationValue != oldDelegate.removeAnimationValue; +} + +/// A helper class to define the properties of each effect layer. +class _EffectLayer { + _EffectLayer({ + required this.radiusFactor, + required this.opacityFactor, + }); + + /// The factor by which to multiply the current radius. + final double radiusFactor; + + /// The factor by which to multiply the main opacity. + final double opacityFactor; } diff --git a/src/ui/flutter_app/lib/game_page/widgets/game_board.dart b/src/ui/flutter_app/lib/game_page/widgets/game_board.dart index 16c531b1b..1d2831aa7 100644 --- a/src/ui/flutter_app/lib/game_page/widgets/game_board.dart +++ b/src/ui/flutter_app/lib/game_page/widgets/game_board.dart @@ -30,8 +30,7 @@ class GameBoard extends StatefulWidget { State createState() => _GameBoardState(); } -class _GameBoardState extends State - with SingleTickerProviderStateMixin { +class _GameBoardState extends State with TickerProviderStateMixin { static const String _logTag = "[board]"; late Future> pieceImagesFuture; late AnimationManager animationManager; @@ -153,7 +152,11 @@ class _GameBoardState extends State ); final AnimatedBuilder customPaint = AnimatedBuilder( - animation: animationManager.animation, + animation: Listenable.merge(>[ + animationManager.placeAnimationController, + animationManager.moveAnimationController, + animationManager.removeAnimationController, + ]), builder: (_, Widget? child) { return FutureBuilder>( future: pieceImagesFuture, @@ -166,7 +169,9 @@ class _GameBoardState extends State return CustomPaint( painter: BoardPainter(context), foregroundPainter: PiecePainter( - animationValue: animationManager.animation.value, + placeAnimationValue: animationManager.placeAnimation.value, + moveAnimationValue: animationManager.moveAnimation.value, + removeAnimationValue: animationManager.removeAnimation.value, pieceImages: pieceImages, ), child: DB().generalSettings.screenReaderSupport @@ -182,7 +187,8 @@ class _GameBoardState extends State }, ); - animationManager.forwardAnimation(); + //animationManager.forwardPlaceAnimation(); + //animationManager.forwardMoveAnimation(); return ValueListenableBuilder>( valueListenable: DB().listenDisplaySettings, diff --git a/src/ui/flutter_app/lib/tutorial/painters/tutorial_painter.dart b/src/ui/flutter_app/lib/tutorial/painters/tutorial_painter.dart index a8eef8929..07b4cf5ae 100644 --- a/src/ui/flutter_app/lib/tutorial/painters/tutorial_painter.dart +++ b/src/ui/flutter_app/lib/tutorial/painters/tutorial_painter.dart @@ -48,14 +48,13 @@ class TutorialPainter extends CustomPainter { } final Offset pos = pointFromIndex(index, size); - final bool animated = focusIndex == index; piecesToDraw.add( Piece( pieceColor: piece, pos: pos, - animated: animated, diameter: pieceWidth, + index: index, ), ); diff --git a/src/ui/flutter_app/pubspec.yaml b/src/ui/flutter_app/pubspec.yaml index 90d2de811..fa7876f28 100644 --- a/src/ui/flutter_app/pubspec.yaml +++ b/src/ui/flutter_app/pubspec.yaml @@ -19,7 +19,6 @@ dependencies: fluentui_system_icons: 1.1.257 flutter: sdk: flutter - flutter_colorpicker: 1.1.0 flutter_email_sender: 6.0.3 flutter_localizations: