feat: rise and fade animation for construct levels
This commit is contained in:
parent
16fbc4a52e
commit
096ba06367
6 changed files with 206 additions and 12 deletions
|
|
@ -39,6 +39,7 @@ import 'package:fluffychat/pangea/common/utils/firebase_analytics.dart';
|
|||
import 'package:fluffychat/pangea/common/utils/overlay.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/transparent_backdrop.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
|
||||
|
|
@ -530,10 +531,14 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
final updater = Matrix.of(context).analyticsDataService.updateDispatcher;
|
||||
|
||||
_constructLevelSubscription =
|
||||
updater.constructLevelUpdateStream.stream.listen((entry) {
|
||||
debugPrint(
|
||||
"Construct level update received: ${entry.key.string} -> ${entry.value}",
|
||||
);
|
||||
updater.constructLevelUpdateStream.stream.listen((update) {
|
||||
if (update.targetID != null) {
|
||||
OverlayUtil.showGrowthOverlay(
|
||||
context,
|
||||
update.targetID!,
|
||||
level: update.level,
|
||||
);
|
||||
}
|
||||
});
|
||||
_levelSubscription = updater.levelUpdateStream.stream.listen(_onLevelUp);
|
||||
|
||||
|
|
@ -2340,6 +2345,17 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
ConstructTypeEnum.vocab,
|
||||
);
|
||||
|
||||
// Show growth animation for new vocab (seeds level)
|
||||
if (newVocabConstructs > 0) {
|
||||
for (int i = 0; i < newVocabConstructs; i++) {
|
||||
OverlayUtil.showGrowthOverlay(
|
||||
context,
|
||||
eventId,
|
||||
level: ConstructLevelEnum.seeds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
OverlayUtil.showOverlay(
|
||||
overlayKey: "msg_analytics_feedback_$eventId",
|
||||
followerAnchor: Alignment.bottomRight,
|
||||
|
|
|
|||
|
|
@ -453,6 +453,7 @@ class AnalyticsDataService {
|
|||
ConstructLevelUpEvent(
|
||||
entry.key,
|
||||
newLevel,
|
||||
update.targetID,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,18 @@ class LevelUpdate {
|
|||
});
|
||||
}
|
||||
|
||||
class ConstructLevelUpdate {
|
||||
final ConstructIdentifier constructId;
|
||||
final ConstructLevelEnum level;
|
||||
final String? targetID;
|
||||
|
||||
ConstructLevelUpdate({
|
||||
required this.constructId,
|
||||
required this.level,
|
||||
this.targetID,
|
||||
});
|
||||
}
|
||||
|
||||
class AnalyticsUpdate {
|
||||
final List<OneConstructUse> addedConstructs;
|
||||
final ConstructIdentifier? blockedConstruct;
|
||||
|
|
@ -47,9 +59,8 @@ class AnalyticsUpdateDispatcher {
|
|||
final StreamController<Set<ConstructIdentifier>> newConstructsStream =
|
||||
StreamController<Set<ConstructIdentifier>>.broadcast();
|
||||
|
||||
final StreamController<MapEntry<ConstructIdentifier, ConstructLevelEnum>>
|
||||
constructLevelUpdateStream = StreamController<
|
||||
MapEntry<ConstructIdentifier, ConstructLevelEnum>>.broadcast();
|
||||
final StreamController<ConstructLevelUpdate> constructLevelUpdateStream =
|
||||
StreamController<ConstructLevelUpdate>.broadcast();
|
||||
|
||||
final StreamController<MapEntry<ConstructIdentifier, UserSetLemmaInfo>>
|
||||
_lemmaInfoUpdateStream = StreamController<
|
||||
|
|
@ -108,7 +119,7 @@ class AnalyticsUpdateDispatcher {
|
|||
_onBlockedConstruct(e.blockedConstruct);
|
||||
break;
|
||||
case final ConstructLevelUpEvent e:
|
||||
_onConstructLevelUp(e.constructId, e.level);
|
||||
_onConstructLevelUp(e.constructId, e.level, e.targetID);
|
||||
break;
|
||||
case final NewConstructsEvent e:
|
||||
_onNewConstruct(e.newConstructs);
|
||||
|
|
@ -156,9 +167,14 @@ class AnalyticsUpdateDispatcher {
|
|||
void _onConstructLevelUp(
|
||||
ConstructIdentifier constructId,
|
||||
ConstructLevelEnum level,
|
||||
String? targetID,
|
||||
) {
|
||||
constructLevelUpdateStream.add(
|
||||
MapEntry(constructId, level),
|
||||
ConstructLevelUpdate(
|
||||
constructId: constructId,
|
||||
level: level,
|
||||
targetID: targetID,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,9 +17,11 @@ class MorphUnlockedEvent extends AnalyticsUpdateEvent {
|
|||
class ConstructLevelUpEvent extends AnalyticsUpdateEvent {
|
||||
final ConstructIdentifier constructId;
|
||||
final ConstructLevelEnum level;
|
||||
final String? targetID;
|
||||
ConstructLevelUpEvent(
|
||||
this.constructId,
|
||||
this.level,
|
||||
this.targetID,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
98
lib/pangea/analytics_misc/growth_animation.dart
Normal file
98
lib/pangea/analytics_misc/growth_animation.dart
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class GrowthAnimation extends StatefulWidget {
|
||||
final String targetID;
|
||||
final ConstructLevelEnum level;
|
||||
final double horizontalOffset;
|
||||
final VoidCallback? onComplete;
|
||||
|
||||
final int durationMs;
|
||||
final double riseDistance;
|
||||
|
||||
const GrowthAnimation({
|
||||
super.key,
|
||||
required this.targetID,
|
||||
required this.level,
|
||||
this.horizontalOffset = 0,
|
||||
this.onComplete,
|
||||
this.durationMs = 1600,
|
||||
this.riseDistance = 72,
|
||||
});
|
||||
|
||||
@override
|
||||
State<GrowthAnimation> createState() => _GrowthAnimationState();
|
||||
}
|
||||
|
||||
class _GrowthAnimationState extends State<GrowthAnimation>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
late final Animation<double> _progress;
|
||||
late final Random _random;
|
||||
late final double _wiggleAmplitude;
|
||||
late final double _wiggleFrequency;
|
||||
late final int _actualDuration;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Create random variations for animation
|
||||
_random = Random();
|
||||
_wiggleAmplitude = 4.0 + _random.nextDouble() * 4.0;
|
||||
_wiggleFrequency = 1.5 + _random.nextDouble() * 1.0;
|
||||
_actualDuration = widget.durationMs + (_random.nextInt(400) - 200);
|
||||
|
||||
_controller = AnimationController(
|
||||
duration: Duration(milliseconds: _actualDuration),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_progress = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
|
||||
_controller.forward().then((_) {
|
||||
if (!mounted) return;
|
||||
MatrixState.pAnyState
|
||||
.closeOverlay("${widget.targetID}_growth_${widget.hashCode}");
|
||||
widget.onComplete?.call();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: AnimatedBuilder(
|
||||
animation: _progress,
|
||||
builder: (context, child) {
|
||||
final clampedT = _progress.value.clamp(0.0, 1.0);
|
||||
final dy = -widget.riseDistance * clampedT;
|
||||
final opacity = clampedT < 0.5 ? clampedT * 2 : (1.0 - clampedT) * 2;
|
||||
|
||||
final wiggle =
|
||||
sin(clampedT * pi * _wiggleFrequency) * _wiggleAmplitude;
|
||||
|
||||
return Opacity(
|
||||
opacity: opacity,
|
||||
child: Transform.translate(
|
||||
offset: Offset(widget.horizontalOffset + wiggle, dy),
|
||||
child: widget.level.icon(24),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/growth_animation.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/level_up/star_rain_widget.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/choreo_constants.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/choreographer.dart';
|
||||
|
|
@ -13,7 +11,11 @@ import 'package:fluffychat/pangea/common/utils/any_state_holder.dart';
|
|||
import 'package:fluffychat/pangea/common/widgets/anchored_overlay_widget.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/overlay_container.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/transparent_backdrop.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/language_mismatch_popup.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../config/themes.dart';
|
||||
import '../../../widgets/matrix.dart';
|
||||
import 'error_handler.dart';
|
||||
|
|
@ -25,6 +27,9 @@ enum OverlayPositionEnum {
|
|||
}
|
||||
|
||||
class OverlayUtil {
|
||||
static int _growthAnimationCount = 0;
|
||||
static final Set<String> _activeGrowthAnimations = {};
|
||||
|
||||
static bool showOverlay({
|
||||
required BuildContext context,
|
||||
required Widget child,
|
||||
|
|
@ -312,6 +317,62 @@ class OverlayUtil {
|
|||
);
|
||||
}
|
||||
|
||||
static void showGrowthOverlay(
|
||||
BuildContext context,
|
||||
String targetId, {
|
||||
required ConstructLevelEnum level,
|
||||
}) {
|
||||
// Clean up any stale entries from dismissed overlays
|
||||
_cleanupStaleGrowthAnimations();
|
||||
|
||||
final animationKey = "${targetId}_growth_${_growthAnimationCount + 1}";
|
||||
|
||||
// Calculate offset based on how many are currently active
|
||||
final activeCount = _activeGrowthAnimations.length;
|
||||
final double horizontalOffset;
|
||||
|
||||
if (activeCount == 0) {
|
||||
// Start in the middle if no animations are running
|
||||
horizontalOffset = 0.0;
|
||||
} else {
|
||||
final side = activeCount % 2 == 0 ? 1 : -1;
|
||||
final distance = ((activeCount + 1) ~/ 2) * 30.0;
|
||||
horizontalOffset = side * distance;
|
||||
}
|
||||
|
||||
_growthAnimationCount++;
|
||||
_activeGrowthAnimations.add(animationKey);
|
||||
|
||||
showOverlay(
|
||||
overlayKey: animationKey,
|
||||
followerAnchor: Alignment.topCenter,
|
||||
targetAnchor: Alignment.topCenter,
|
||||
context: context,
|
||||
child: GrowthAnimation(
|
||||
targetID: targetId,
|
||||
level: level,
|
||||
horizontalOffset: horizontalOffset,
|
||||
onComplete: () {
|
||||
_activeGrowthAnimations.remove(animationKey);
|
||||
},
|
||||
),
|
||||
transformTargetId: targetId,
|
||||
closePrevOverlay: false,
|
||||
backDropToDismiss: false,
|
||||
ignorePointer: true,
|
||||
offset: Offset(horizontalOffset, 0),
|
||||
);
|
||||
}
|
||||
|
||||
static void _cleanupStaleGrowthAnimations() {
|
||||
// Remove any tracked animations that are no longer in the overlay system
|
||||
_activeGrowthAnimations.removeWhere((key) {
|
||||
final isActive =
|
||||
MatrixState.pAnyState.entries.any((entry) => entry.key == key);
|
||||
return !isActive;
|
||||
});
|
||||
}
|
||||
|
||||
static void showLanguageMismatchPopup({
|
||||
required BuildContext context,
|
||||
required String targetId,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue