-
Notifications
You must be signed in to change notification settings - Fork 973
/
Copy pathillustration_piece.dart
142 lines (122 loc) · 5.28 KB
/
illustration_piece.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
import 'dart:ui' as ui;
import 'package:wonders/common_libs.dart';
import 'package:wonders/ui/wonder_illustrations/common/wonder_illustration_builder.dart';
/// Combines [Align], [FractionalBoxWithMinSize], [Image] and [Transform.translate]
/// to standardize behavior across the various wonder illustrations
class IllustrationPiece extends StatefulWidget {
const IllustrationPiece({
super.key,
required this.fileName,
required this.heightFactor,
this.alignment = Alignment.center,
this.minHeight,
this.offset = Offset.zero,
this.fractionalOffset,
this.zoomAmt = 0,
this.initialOffset = Offset.zero,
this.enableHero = false,
this.initialScale = 1,
this.dynamicHzOffset = 0,
this.top,
this.bottom,
});
final String fileName;
final Alignment alignment;
/// Will animate from this position to Offset.zero, eg is value is Offset(0, 100), the piece will slide up vertically 100px as it enters the screen
final Offset initialOffset;
/// Will animate from this scale to 1, eg if scale is .7, the piece will scale from .7 to 1.0 as it enters the screen.
final double initialScale;
/// % height, will be overridden by minHeight
final double heightFactor;
/// min height in pixels, piece will not be allowed to go below this height in px, unless it has to (available height is too small)
final double? minHeight;
/// px offset for this piece
final Offset offset;
/// offset based on a fraction of the piece size
final Offset? fractionalOffset;
/// The % amount that this object should scale up as the user drags their finger up the screen
final double zoomAmt;
/// Adds a hero tag to this piece, made from wonderType + fileName
final bool enableHero;
/// Max px offset of the piece as the screen size grows horizontally
final double dynamicHzOffset;
final Widget Function(BuildContext context)? top;
final Widget Function(BuildContext context)? bottom;
@override
State<IllustrationPiece> createState() => _IllustrationPieceState();
}
class _IllustrationPieceState extends State<IllustrationPiece> {
double? aspectRatio;
ui.Image? uiImage;
@override
Widget build(BuildContext context) {
final wonderBuilder = context.watch<WonderIllustrationBuilderState>();
final type = wonderBuilder.widget.wonderType;
final imgPath = '${type.assetPath}/${widget.fileName}';
// Dynamically determine the aspect ratio of the image, so we can more easily position it
if (aspectRatio == null) {
aspectRatio == 0; // indicates load has started, so we don't run twice
rootBundle.load(imgPath).then((img) async {
uiImage = await decodeImageFromList(img.buffer.asUint8List());
if (!mounted) return;
setState(() => aspectRatio = uiImage!.width / uiImage!.height);
});
}
return Align(
alignment: widget.alignment,
child: LayoutBuilder(
key: ValueKey(aspectRatio),
builder: (_, constraints) {
final anim = wonderBuilder.anim;
final curvedAnim = Curves.easeOut.transform(anim.value);
final config = wonderBuilder.widget.config;
Widget img = Image.asset(imgPath, opacity: anim, fit: BoxFit.fitHeight);
// Add overflow box so image doesn't get clipped as we translate it around
img = OverflowBox(maxWidth: 2500, child: img);
final double introZoom = (widget.initialScale - 1) * (1 - curvedAnim);
/// Determine target height
final double height = max(widget.minHeight ?? 0, constraints.maxHeight * widget.heightFactor);
/// Combine all the translations, initial + offset + dynamicHzOffset + fractionalOffset
Offset finalTranslation = widget.offset;
// Initial
if (widget.initialOffset != Offset.zero) {
finalTranslation += widget.initialOffset * (1 - curvedAnim);
}
// Dynamic
final dynamicOffsetAmt = ((context.widthPx - 400) / 1100).clamp(0, 1);
finalTranslation += Offset(dynamicOffsetAmt * widget.dynamicHzOffset, 0);
// Fractional
final width = height * (aspectRatio ?? 0);
if (widget.fractionalOffset != null) {
finalTranslation += Offset(
widget.fractionalOffset!.dx * width,
height * widget.fractionalOffset!.dy,
);
}
Widget? content;
if (uiImage != null) {
content = Transform.translate(
offset: finalTranslation,
child: Transform.scale(
scale: 1 + (widget.zoomAmt * config.zoom) + introZoom,
child: SizedBox(
height: height,
width: height * aspectRatio!,
child: img,
),
),
);
}
return Stack(
children: [
if (widget.bottom != null) Positioned.fill(child: widget.bottom!.call(context)),
if (uiImage != null) ...[
widget.enableHero ? Hero(tag: '$type-${widget.fileName}', child: content!) : content!,
],
if (widget.top != null) Positioned.fill(child: widget.top!.call(context)),
],
);
}),
);
}
}