yumi-flutter/lib/ui_kit/widgets/room/rocket/room_rocket_dialog.dart
roxy bd6d35ee1b 🚀 UI
2026-05-14 17:10:53 +08:00

1204 lines
33 KiB
Dart

import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_status_res.dart';
import 'package:yumi/ui_kit/components/custom_cached_image.dart';
class RoomRocketDialog extends StatelessWidget {
const RoomRocketDialog({
super.key,
this.status,
this.progressStage,
this.bottomStage,
this.levels,
this.selectedLevel = 0,
this.onLevelSelected,
this.rewards,
this.crew,
this.onClose,
this.onOpenRank,
this.onRewardSelected,
this.onOpenRecord,
this.onOpenNote,
});
final SCRoomRocketStatusRes? status;
final RoomRocketProgressStage? progressStage;
final RoomRocketBottomStage? bottomStage;
final List<RoomRocketLevelItem>? levels;
final int selectedLevel;
final ValueChanged<int>? onLevelSelected;
final List<RoomRocketRewardItem>? rewards;
final List<RoomRocketCrewMember>? crew;
final VoidCallback? onClose;
final VoidCallback? onOpenRank;
final ValueChanged<RoomRocketRewardItem>? onRewardSelected;
final VoidCallback? onOpenRecord;
final VoidCallback? onOpenNote;
@override
Widget build(BuildContext context) {
final resolvedLevels = _normalizeLevels(levels);
final resolvedRewards = _normalizeRewards(rewards);
final resolvedCrew = _normalizeCrew(crew, status);
final percent = _normalizePercent(status?.energyPercent);
final resolvedProgressStage =
progressStage ?? RoomRocketProgressStage.fromPercent(percent);
final resolvedBottomStage =
bottomStage ??
(resolvedCrew.isEmpty
? RoomRocketBottomStage.empty
: RoomRocketBottomStage.active);
final round = (status?.roundNo ?? status?.level ?? 1).clamp(1, 99).toInt();
return Directionality(
textDirection: TextDirection.ltr,
child: Material(
color: Colors.transparent,
child: Stack(
children: [
Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onClose,
child: Container(color: Colors.black.withValues(alpha: 0.60)),
),
),
Positioned.fill(
child: LayoutBuilder(
builder: (context, constraints) {
final scale = math.min(
constraints.maxWidth / _designWidth,
constraints.maxHeight / _designHeight,
);
return Align(
alignment: Alignment.topCenter,
child: SizedBox(
width: _designWidth * scale,
height: _designHeight * scale,
child: _RocketCanvas(
scale: scale,
percent: percent,
progressStage: resolvedProgressStage,
bottomStage: resolvedBottomStage,
round: round,
levels: resolvedLevels,
selectedLevel: selectedLevel,
onLevelSelected: onLevelSelected,
rewards: resolvedRewards,
crew: resolvedCrew,
onClose: onClose,
onOpenRank: onOpenRank,
onRewardSelected: onRewardSelected,
onOpenRecord: onOpenRecord,
onOpenNote: onOpenNote,
),
),
);
},
),
),
],
),
),
);
}
static List<RoomRocketLevelItem> _normalizeLevels(
List<RoomRocketLevelItem>? levels,
) {
final source =
levels
?.where((item) => item.label.trim().isNotEmpty)
.toList(growable: false) ??
const <RoomRocketLevelItem>[];
if (source.isNotEmpty) {
return source;
}
return List<RoomRocketLevelItem>.generate(
6,
(index) => RoomRocketLevelItem(label: 'Lv.${index + 1}'),
growable: false,
);
}
static List<RoomRocketRewardItem> _normalizeRewards(
List<RoomRocketRewardItem>? rewards,
) {
final source =
rewards?.where((item) => item.amount.trim().isNotEmpty).toList() ??
const <RoomRocketRewardItem>[];
if (source.isNotEmpty) {
return source;
}
return const [
RoomRocketRewardItem(asset: _RocketAssets.rewardThumb, amount: '10000'),
RoomRocketRewardItem(asset: _RocketAssets.rewardThumb, amount: '10000'),
RoomRocketRewardItem(asset: _RocketAssets.rewardThumb, amount: '10000'),
RoomRocketRewardItem(asset: _RocketAssets.rewardThumb, amount: '10000'),
];
}
static List<RoomRocketCrewMember> _normalizeCrew(
List<RoomRocketCrewMember>? crew,
SCRoomRocketStatusRes? status,
) {
final hasNoContributors =
status != null && (status.totalContributors ?? 0) <= 0;
if (hasNoContributors) {
return const <RoomRocketCrewMember>[];
}
final source =
crew?.where((item) => item.asset != null || item.imageUrl != null) ??
const <RoomRocketCrewMember>[];
if (source.isNotEmpty) {
return source.take(3).toList();
}
return const <RoomRocketCrewMember>[];
}
static double _normalizePercent(num? value) {
if (value == null) {
return 100;
}
final raw = value.toDouble();
final percent = raw > 0 && raw <= 1 ? raw * 100 : raw;
return percent.clamp(0, 100).toDouble();
}
}
class RoomRocketRewardItem {
const RoomRocketRewardItem({this.asset, this.imageUrl, required this.amount});
final String? asset;
final String? imageUrl;
final String amount;
}
class RoomRocketLevelItem {
const RoomRocketLevelItem({
required this.label,
this.asset,
this.imageUrl,
this.enabled = true,
});
final String label;
final String? asset;
final String? imageUrl;
final bool enabled;
}
class RoomRocketCrewMember {
const RoomRocketCrewMember({this.asset, this.imageUrl});
final String? asset;
final String? imageUrl;
}
enum RoomRocketProgressStage {
empty,
charging,
full;
static RoomRocketProgressStage fromPercent(double percent) {
if (percent <= 0) {
return RoomRocketProgressStage.empty;
}
if (percent >= 99.5) {
return RoomRocketProgressStage.full;
}
return RoomRocketProgressStage.charging;
}
}
enum RoomRocketBottomStage { empty, active }
class _RocketCanvas extends StatelessWidget {
const _RocketCanvas({
required this.scale,
required this.percent,
required this.progressStage,
required this.bottomStage,
required this.round,
required this.levels,
required this.selectedLevel,
this.onLevelSelected,
required this.rewards,
required this.crew,
this.onClose,
this.onOpenRank,
this.onRewardSelected,
this.onOpenRecord,
this.onOpenNote,
});
final double scale;
final double percent;
final RoomRocketProgressStage progressStage;
final RoomRocketBottomStage bottomStage;
final int round;
final List<RoomRocketLevelItem> levels;
final int selectedLevel;
final ValueChanged<int>? onLevelSelected;
final List<RoomRocketRewardItem> rewards;
final List<RoomRocketCrewMember> crew;
final VoidCallback? onClose;
final VoidCallback? onOpenRank;
final ValueChanged<RoomRocketRewardItem>? onRewardSelected;
final VoidCallback? onOpenRecord;
final VoidCallback? onOpenNote;
double s(double value) => value * scale;
@override
Widget build(BuildContext context) {
return Stack(
clipBehavior: Clip.hardEdge,
children: [
_image(_RocketAssets.rocketStage, 15, 361, 344, 97),
_image(_RocketAssets.rocketBody, 88, 111, 200, 432),
_image(_RocketAssets.sidePanel, 309, 202, 66, 207),
_image(_RocketAssets.sidePanel, 0, 202, 66, 207),
_buildLevelRail(),
_buildProgress(),
_buildSideActions(),
_buildRoundTitle(),
_buildResetTime(),
_buildRewardPanel(),
_buildBottomStrip(),
],
);
}
Widget _image(
String asset,
double left,
double top,
double width,
double height, {
double opacity = 1,
BoxFit fit = BoxFit.fill,
}) {
return Positioned(
left: s(left),
top: s(top),
width: s(width),
height: s(height),
child: Opacity(
opacity: opacity,
child: Image.asset(asset, fit: fit, gaplessPlayback: true),
),
);
}
Widget _buildRoundTitle() {
return Positioned(
left: 0,
top: s(120),
width: s(_designWidth),
child: _GradientText(
'Round $round Today',
fontSize: s(16),
fontWeight: FontWeight.w600,
textAlign: TextAlign.center,
),
);
}
Widget _buildLevelRail() {
return Positioned(
left: s(13),
top: s(212),
width: s(40),
height: s(190),
child: ScrollConfiguration(
behavior: const _NoGlowScrollBehavior(),
child: ListView.separated(
padding: EdgeInsets.zero,
primary: false,
physics: const BouncingScrollPhysics(),
itemCount: levels.length,
separatorBuilder: (_, __) => SizedBox(height: s(4)),
itemBuilder: (context, index) {
final item = levels[index];
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: item.enabled ? () => onLevelSelected?.call(index) : null,
child: SizedBox(
width: s(40),
height: s(52),
child: _LevelBadge(
scale: scale,
item: item,
selected: index == selectedLevel,
),
),
);
},
),
),
);
}
Widget _buildProgress() {
final asset = switch (progressStage) {
RoomRocketProgressStage.empty => _RocketAssets.progressEmpty,
RoomRocketProgressStage.charging => _RocketAssets.progressPartial,
RoomRocketProgressStage.full => _RocketAssets.progressFull,
};
return Stack(
children: [
_image(asset, 320, 227, 44, 134),
Positioned(
left: s(316),
top: s(373),
width: s(56),
child: _GradientText(
'${percent.round()}%',
fontSize: s(16),
fontWeight: FontWeight.w600,
textAlign: TextAlign.center,
colors: const [Color(0xFFA4FFE3), Color(0xFF21C593)],
),
),
],
);
}
Widget _buildSideActions() {
return Stack(
children: [
_image(_RocketAssets.pillBg, 306, 122, 220, 33),
Positioned(
left: s(335),
top: s(127),
width: s(22),
height: s(22),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onOpenNote,
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: const Color(0xFFA4FFE3).withValues(alpha: 0.95),
width: s(1),
),
),
child: Center(
child: Text(
'?',
textScaler: TextScaler.noScaling,
style: TextStyle(
color: Colors.white,
fontSize: s(15),
height: 1,
fontWeight: FontWeight.w700,
decoration: TextDecoration.none,
),
),
),
),
),
),
_image(_RocketAssets.pillBg, 306, 159, 220, 33),
Positioned(
left: s(306),
top: s(159),
width: s(69),
height: s(33),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onOpenRecord,
child: Center(
child: _GradientText(
'Record',
fontSize: s(14),
fontWeight: FontWeight.w600,
textAlign: TextAlign.center,
),
),
),
),
],
);
}
Widget _buildResetTime() {
return Stack(
children: [
_image(_RocketAssets.pillBg, 78, 458, 220, 33),
Positioned(
left: s(109),
top: s(468),
child: _RocketText(
'Reset Time',
fontSize: s(12),
color: Colors.white,
),
),
for (final item in const [
_TimerToken('23', 193),
_TimerToken('23', 223),
_TimerToken('23', 253),
])
Positioned(
left: s(item.left),
top: s(468),
child: _RocketText(
item.value,
fontSize: s(12),
color: Color(0xFF2DFFC1),
),
),
for (final left in const [209.0, 239.0])
Positioned(
left: s(left),
top: s(468),
child: _RocketText(
':',
fontSize: s(12),
color: const Color(0xFF2DFFC1),
),
),
],
);
}
Widget _buildRewardPanel() {
return Stack(
children: [
_image(_RocketAssets.rewardTop, 8, 499, 360, 63),
_image(_RocketAssets.rewardMiddle, 8, 559, 360, 59),
_image(_RocketAssets.rewardMiddle, 8, 594, 360, 59),
_image(_RocketAssets.rewardBottom, 8, 653, 360, 76),
Positioned(
left: 0,
top: s(519),
width: s(_designWidth),
child: const _GradientText(
'Reward',
fontSize: 16,
fontWeight: FontWeight.w600,
textAlign: TextAlign.center,
).scaled(scale),
),
Positioned(
left: s(28),
top: s(543),
width: s(320),
height: math.max(1, s(1)),
child: DecoratedBox(
decoration: BoxDecoration(
color: const Color(0xFFB2FFE7).withValues(alpha: 0.16),
),
),
),
_RewardTab(
scale: scale,
left: 31,
top: 552,
label: 'Top1',
active: true,
),
_RewardTab(scale: scale, left: 147, top: 552, label: 'Set off'),
_RewardTab(scale: scale, left: 252, top: 552, label: 'In the room'),
Positioned(
left: s(28),
top: s(578),
width: s(336),
height: s(135),
child: ClipRect(
child: ScrollConfiguration(
behavior: const _NoGlowScrollBehavior(),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
child: Row(
children: [
for (var index = 0; index < rewards.length; index++) ...[
SizedBox(
width: s(97),
height: s(135),
child: _RewardCard(
scale: scale,
item: rewards[index],
onTap: () => onRewardSelected?.call(rewards[index]),
),
),
if (index != rewards.length - 1) SizedBox(width: s(12)),
],
],
),
),
),
),
),
],
);
}
Widget _buildBottomStrip() {
final activeBottom = bottomStage == RoomRocketBottomStage.active;
final showCrew = activeBottom && crew.isNotEmpty;
return Stack(
children: [
Positioned(
left: s(0),
top: s(729),
width: s(375),
height: s(83),
child: Transform.rotate(
angle: math.pi,
child: Image.asset(
activeBottom
? _RocketAssets.bottomStripActive
: _RocketAssets.bottomStripEmpty,
fit: BoxFit.fill,
gaplessPlayback: true,
),
),
),
_image(_RocketAssets.bottomTrophy, 31, 740, 52, 69),
if (!activeBottom)
Positioned(
left: s(108),
top: s(750),
width: s(169),
height: s(36),
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.center,
child: Text(
'Not up to Rocket rank; no\n“Rocket King” title',
textScaler: TextScaler.noScaling,
maxLines: 2,
softWrap: false,
overflow: TextOverflow.visible,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: s(14),
height: 16 / 14,
fontWeight: FontWeight.w400,
decoration: TextDecoration.none,
),
),
),
),
if (showCrew)
for (var index = 0; index < crew.length.clamp(0, 3); index++)
Positioned(
left: s(97 + 71.0 * index),
top: s(753),
width: s(50),
height: s(50),
child: _CrewAvatar(scale: scale, member: crew[index]),
),
Positioned(
left: s(315),
top: s(753),
width: s(52),
height: s(52),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onOpenRank ?? onClose,
child: Center(
child: Image.asset(
_RocketAssets.backArrow,
width: s(32),
height: s(32),
fit: BoxFit.fill,
gaplessPlayback: true,
),
),
),
),
Positioned(
left: s(0),
top: s(729),
width: s(375),
height: s(83),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onOpenRank ?? onClose,
child: const SizedBox.expand(),
),
),
],
);
}
}
class _RewardCard extends StatelessWidget {
const _RewardCard({required this.scale, required this.item, this.onTap});
final double scale;
final RoomRocketRewardItem item;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Stack(
children: [
Positioned.fill(child: CustomPaint(painter: _RewardCardPainter())),
Positioned(
left: 23 * scale,
top: 24 * scale,
width: 50 * scale,
height: 50 * scale,
child: _RocketImage(
asset: item.asset,
imageUrl: item.imageUrl,
fit: BoxFit.cover,
),
),
Positioned(
left: 21 * scale,
top: 106 * scale,
width: 14 * scale,
height: 14 * scale,
child: Image.asset(_RocketAssets.coin, fit: BoxFit.contain),
),
Positioned(
left: 37 * scale,
top: 105 * scale,
width: 46 * scale,
child: _RocketText(
item.amount,
fontSize: 14 * scale,
color: Colors.white,
textAlign: TextAlign.left,
),
),
],
),
);
}
}
class _LevelBadge extends StatelessWidget {
const _LevelBadge({
required this.scale,
required this.item,
required this.selected,
});
final double scale;
final RoomRocketLevelItem item;
final bool selected;
@override
Widget build(BuildContext context) {
return SizedBox(
width: 40 * scale,
height: 52 * scale,
child: Stack(
children: [
Positioned.fill(
child: CustomPaint(
painter: _LevelBadgePainter(
selected: selected,
enabled: item.enabled,
),
),
),
Positioned(
left: 10 * scale,
top: 6 * scale,
width: 20 * scale,
height: 29 * scale,
child: ClipRRect(
borderRadius: BorderRadius.circular(4 * scale),
child:
item.imageUrl != null || item.asset != null
? _RocketImage(
asset: item.asset,
imageUrl: item.imageUrl,
fit: BoxFit.cover,
)
: Stack(
clipBehavior: Clip.hardEdge,
children: [
Positioned(
left: -5.45 * scale,
top: -10.92 * scale,
width: 30.86 * scale,
height: 66.82 * scale,
child: Image.asset(
_RocketAssets.rocketBody,
fit: BoxFit.cover,
gaplessPlayback: true,
),
),
],
),
),
),
Positioned(
left: 0,
right: 0,
top: 38 * scale,
child: _RocketText(
item.label,
fontSize: 10 * scale,
color:
item.enabled
? Colors.white
: Colors.white.withValues(alpha: 0.52),
),
),
],
),
);
}
}
class _CrewAvatar extends StatelessWidget {
const _CrewAvatar({required this.scale, required this.member});
final double scale;
final RoomRocketCrewMember member;
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.center,
children: [
Image.asset(
_RocketAssets.bottomAvatarRing,
width: 40 * scale,
height: 40 * scale,
fit: BoxFit.fill,
),
ClipOval(
child: SizedBox(
width: 42 * scale,
height: 42 * scale,
child: _RocketImage(
asset: member.asset,
imageUrl: member.imageUrl,
fit: BoxFit.cover,
),
),
),
],
);
}
}
class _RocketImage extends StatelessWidget {
const _RocketImage({this.asset, this.imageUrl, this.fit = BoxFit.cover});
final String? asset;
final String? imageUrl;
final BoxFit fit;
@override
Widget build(BuildContext context) {
if (imageUrl != null && imageUrl!.trim().isNotEmpty) {
return CustomCachedImage(imageUrl: imageUrl!, fit: fit);
}
if (asset != null && asset!.trim().isNotEmpty) {
return Image.asset(asset!, fit: fit, gaplessPlayback: true);
}
return const SizedBox.shrink();
}
}
class _RewardTab extends StatelessWidget {
const _RewardTab({
required this.scale,
required this.left,
required this.top,
required this.label,
this.active = false,
});
final double scale;
final double left;
final double top;
final String label;
final bool active;
@override
Widget build(BuildContext context) {
final color =
active
? const Color(0xFF75F3CB)
: const Color(0xFF75F3CB).withValues(alpha: 0.52);
final width =
label == 'In the room'
? 93.0
: label == 'Set off'
? 76.0
: 68.0;
return Positioned(
left: left * scale,
top: top * scale,
width: width * scale,
height: 14 * scale,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_TabSlash(color: color, scale: scale),
SizedBox(width: 3 * scale),
Flexible(
child: Text(
label,
textScaler: TextScaler.noScaling,
maxLines: 1,
overflow: TextOverflow.visible,
textAlign: TextAlign.center,
style: TextStyle(
color: color,
fontSize: 12 * scale,
height: 1,
decoration: TextDecoration.none,
),
),
),
SizedBox(width: 3 * scale),
Transform.rotate(
angle: math.pi,
child: _TabSlash(color: color, scale: scale),
),
],
),
);
}
}
class _TabSlash extends StatelessWidget {
const _TabSlash({required this.color, required this.scale});
final Color color;
final double scale;
@override
Widget build(BuildContext context) {
return Text(
'///',
textScaler: TextScaler.noScaling,
style: TextStyle(
color: color,
fontSize: 10 * scale,
height: 1,
letterSpacing: 0,
decoration: TextDecoration.none,
),
);
}
}
class _NoGlowScrollBehavior extends ScrollBehavior {
const _NoGlowScrollBehavior();
@override
Widget buildOverscrollIndicator(
BuildContext context,
Widget child,
ScrollableDetails details,
) {
return child;
}
}
class _LevelBadgePainter extends CustomPainter {
const _LevelBadgePainter({required this.selected, required this.enabled});
final bool selected;
final bool enabled;
@override
void paint(Canvas canvas, Size size) {
final path = _scaledPath(
const [
Offset(35.625, 0.5),
Offset(39.5, 4.39941),
Offset(39.5, 47.5996),
Offset(35.625, 51.5),
Offset(5.18164, 51.5),
Offset(0.5, 47.5732),
Offset(0.5, 4.39941),
Offset(4.375, 0.5),
],
size,
const Size(40, 52),
);
canvas.drawPath(
path,
Paint()
..shader = const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF077C60), Color(0xFF08251E)],
).createShader(Offset.zero & size),
);
canvas.drawPath(
path,
Paint()
..style = PaintingStyle.stroke
..strokeWidth = selected ? size.width / 22 : size.width / 40
..shader = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
enabled
? const Color(0xFFB2FBCC)
: const Color(0xFFB2FBCC).withValues(alpha: 0.42),
const Color(0xFFB2FBCC).withValues(alpha: 0),
],
).createShader(Offset.zero & size),
);
}
@override
bool shouldRepaint(covariant _LevelBadgePainter oldDelegate) {
return oldDelegate.selected != selected || oldDelegate.enabled != enabled;
}
}
class _RewardCardPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final panelPath = _scaledPath(
const [
Offset(88.293, 0.5),
Offset(96.5, 8.70703),
Offset(96.5, 126.293),
Offset(88.293, 134.5),
Offset(8.70703, 134.5),
Offset(0.5, 126.293),
Offset(0.5, 8.70703),
Offset(8.70703, 0.5),
],
size,
const Size(97, 135),
);
canvas.drawPath(
panelPath,
Paint()
..shader = const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF077C60), Color(0xFF08251E)],
).createShader(Offset.zero & size),
);
canvas.drawPath(
panelPath,
Paint()
..style = PaintingStyle.stroke
..strokeWidth = size.width / 97
..shader = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
const Color(0xFFB2FBCC),
const Color(0xFFB2FBCC).withValues(alpha: 0),
],
).createShader(Offset.zero & size),
);
final pillPath = _scaledPath(
const [
Offset(8, 107.367),
Offset(14.5, 101),
Offset(82.5, 101),
Offset(89, 107.367),
Offset(89, 118.633),
Offset(82.5, 125),
Offset(14.5, 125),
Offset(8, 118.633),
],
size,
const Size(97, 135),
);
canvas.drawPath(
pillPath,
Paint()
..shader = const LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [Color(0xFF136648), Color(0xFF06412B), Color(0xFF136648)],
).createShader(Offset.zero & size),
);
canvas.drawPath(
pillPath,
Paint()
..style = PaintingStyle.stroke
..strokeWidth = size.width / 97
..color = const Color(0xFF8BF9FE).withValues(alpha: 0.85),
);
final circleCenter = Offset(
size.width * 48.5 / 97,
size.height * 48.5 / 135,
);
final circleRadius = math.min(size.width / 97, size.height / 135) * 37;
canvas.drawCircle(
circleCenter,
circleRadius,
Paint()
..style = PaintingStyle.stroke
..strokeWidth = size.width / 97
..maskFilter = MaskFilter.blur(BlurStyle.normal, size.width / 24)
..color = const Color(0xFFEFFBFF).withValues(alpha: 0.6),
);
canvas.drawCircle(
circleCenter,
circleRadius,
Paint()
..style = PaintingStyle.stroke
..strokeWidth = size.width / 97
..color = const Color(0xFF75F3CB),
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
Path _scaledPath(List<Offset> points, Size size, Size viewBox) {
final path = Path();
for (var index = 0; index < points.length; index++) {
final point = Offset(
points[index].dx / viewBox.width * size.width,
points[index].dy / viewBox.height * size.height,
);
if (index == 0) {
path.moveTo(point.dx, point.dy);
} else {
path.lineTo(point.dx, point.dy);
}
}
return path..close();
}
class _RocketText extends StatelessWidget {
const _RocketText(
this.text, {
required this.fontSize,
required this.color,
this.textAlign = TextAlign.center,
});
final String text;
final double fontSize;
final Color color;
final TextAlign textAlign;
@override
Widget build(BuildContext context) {
return Text(
text,
textScaler: TextScaler.noScaling,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: textAlign,
style: TextStyle(
color: color,
fontSize: fontSize,
height: 1,
decoration: TextDecoration.none,
),
);
}
}
class _GradientText extends StatelessWidget {
const _GradientText(
this.text, {
required this.fontSize,
this.fontWeight = FontWeight.w400,
this.textAlign = TextAlign.left,
this.colors = const [Colors.white, Color(0xFFA4FFE3)],
});
final String text;
final double fontSize;
final FontWeight fontWeight;
final TextAlign textAlign;
final List<Color> colors;
_GradientText scaled(double scale) {
return _GradientText(
text,
fontSize: fontSize * scale,
fontWeight: fontWeight,
textAlign: textAlign,
colors: colors,
);
}
@override
Widget build(BuildContext context) {
return ShaderMask(
shaderCallback:
(bounds) => LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: colors,
).createShader(bounds),
blendMode: BlendMode.srcIn,
child: Text(
text,
textScaler: TextScaler.noScaling,
maxLines: 1,
textAlign: textAlign,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: fontSize,
height: 1,
fontWeight: fontWeight,
decoration: TextDecoration.none,
),
),
);
}
}
class _TimerToken {
const _TimerToken(this.value, this.left);
final String value;
final double left;
}
class _RocketAssets {
const _RocketAssets._();
static const String rocketStage = 'sc_images/room/rocket/rocket_stage.png';
static const String rocketBody = 'sc_images/room/rocket/rocket_body.png';
static const String sidePanel = 'sc_images/room/rocket/side_panel.png';
static const String bottomStripActive =
'sc_images/room/rocket/bottom_strip_active.png';
static const String bottomStripEmpty =
'sc_images/room/rocket/bottom_strip_active.png';
static const String bottomTrophy = 'sc_images/room/rocket/bottom_trophy.png';
static const String backArrow = 'sc_images/room/rocket/back_arrow.png';
static const String bottomAvatarRing =
'sc_images/room/rocket/bottom_avatar_ring.png';
static const String pillBg = 'sc_images/room/rocket/pill_bg.png';
static const String progressFull = 'sc_images/room/rocket/progress_full.png';
static const String progressPartial =
'sc_images/room/rocket/progress_partial.png';
static const String progressEmpty =
'sc_images/room/rocket/progress_empty.png';
static const String rewardTop = 'sc_images/room/rocket/reward_dialog_top.png';
static const String rewardMiddle =
'sc_images/room/rocket/reward_dialog_middle.png';
static const String rewardBottom =
'sc_images/room/rocket/reward_dialog_bottom.png';
static const String rewardThumb = 'sc_images/room/rocket/reward_thumb.png';
static const String coin = 'sc_images/register_reward/yumi_coin.png';
}
const double _designWidth = 375;
const double _designHeight = 812;