feat: bloom tap lens with dock follow and soft edge overflow

This commit is contained in:
david1465833828
2026-10-28 12:47:47 +08:00
parent c5cfbced5c
commit 93bcfd534c
7 changed files with 140 additions and 26 deletions
+46 -17
View File
@@ -34,7 +34,7 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
);
late final AnimationController _tap = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 560),
duration: const Duration(milliseconds: 620),
);
late final AnimationController _sway = AnimationController.unbounded(
vsync: this,
@@ -67,6 +67,11 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
0.25,
);
_elasticLastTick = elapsed;
if (_drag == null) {
_swayTarget = _tap.isAnimating
? (_motion.velocity / 14).clamp(-0.85, 0.85)
: 0;
}
final sway = _advanceSpring(
_sway.value,
_swayVelocity,
@@ -87,7 +92,8 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
_dragVelocity = drag.$2;
_sway.value = sway.$1;
_dragFollow.value = drag.$1;
if ((_sway.value - _swayTarget).abs() < 0.0005 &&
if (!_tap.isAnimating &&
(_sway.value - _swayTarget).abs() < 0.0005 &&
_swayVelocity.abs() < 0.005 &&
(_dragFollow.value - _dragTarget).abs() < 0.0005 &&
_dragVelocity.abs() < 0.005) {
@@ -136,6 +142,15 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
}
double get _pressure => _press.value.clamp(0.0, 1.18);
double get _tapBloom {
if (_reduceMotion) return 0;
final t = _tap.value;
final tail = t <= 0.5
? 1.0
: Curves.easeInOut.transform((2 * (1 - t)).clamp(0.0, 1.0));
return math.sin(math.pi * t) * tail;
}
void _pressTo(double target) {
if (_reduceMotion) {
_press.value = target;
@@ -196,7 +211,7 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
_motion.animateWith(
SpringSimulation(
springyTap
? const SpringDescription(mass: 1, stiffness: 350, damping: 22)
? const SpringDescription(mass: 1, stiffness: 350, damping: 20)
: const SpringDescription(mass: 1, stiffness: 360, damping: 26),
_position,
index.toDouble(),
@@ -246,7 +261,7 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
}
Widget _glass({required Widget child, bool lens = false}) {
final pressure = _pressure.clamp(0.0, 1.0);
final pressure = math.max(_pressure.clamp(0.0, 1.0), _tapBloom * 0.85);
final content = CustomPaint(
foregroundPainter: _GlassRimPainter(lens: lens, pressure: pressure),
child: DecoratedBox(
@@ -299,6 +314,12 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
constraints.maxWidth,
MediaQuery.sizeOf(context).width >= 600 ? 300.0 : 264.0,
);
final viewport = MediaQuery.sizeOf(context).width;
final availableWidth = math.min(
viewport > 0 ? viewport : constraints.maxWidth,
constraints.maxWidth,
);
final edgeAllowance = math.max(0.0, (availableWidth - _width) / 2 - 8);
return AnimatedBuilder(
animation: Listenable.merge([
_motion,
@@ -316,19 +337,21 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
final tapWave = _reduceMotion
? 0.0
: math.sin(2 * math.pi * _tap.value);
final tapStretch = tapWave >= 0 ? tapWave * 0.24 : tapWave * 0.065;
final tapStretch = tapWave >= 0 ? tapWave * 0.24 : tapWave * 0.08;
final tapRise = tapWave >= 0 ? -tapWave * 0.045 : -tapWave * 0.15;
// Pointer movement wakes a finite ripple; pauses do not start idle loops.
final dragWave = _reduceMotion
? 0.0
: math.sin(2 * math.pi * _ripple.value) * press.clamp(0.0, 1.0);
final dragStretch = dragWave * (dragWave >= 0 ? 0.10 : 0.06);
final tapInflate = _tapBloom * 0.30;
final speed = _reduceMotion
? 0.0
: (_drag != null ? _dragSpeed : _motion.velocity).abs();
final stretch =
(speed / 80).clamp(0.0, 0.08) +
tapStretch +
tapInflate +
press * 0.30 +
dragStretch;
final lensWidth = _travel * (1 + stretch);
@@ -336,14 +359,14 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
(_height - _inset * 2) *
(1 +
press * 0.31 +
tapRise -
tapRise +
tapInflate -
dragWave * 0.08 -
(speed / 180).clamp(0.0, 0.035));
// Resize real glass bounds: sampling stays aligned without scaling text.
final sway = _reduceMotion ? 0.0 : _sway.value.clamp(-1.2, 1.2);
final bodyGrowth = press * 12 + pulse * 3 + sway.abs() * 4;
final bodyHeight =
_height + press * 5 + pulse * 1.5 - sway.abs() * 0.8;
final bodyGrowth = press * 12 + pulse * 7 + sway.abs() * 4;
final bodyHeight = _height + press * 5 + pulse * 3 - sway.abs() * 0.8;
final dockShift = sway * 8;
final leadingPull = sway * 6;
return SizedBox(
@@ -384,7 +407,13 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
(_inset +
_position * _travel -
(lensWidth - _travel) / 2)
.clamp(1.0, _width - lensWidth - 1),
.clamp(
-edgeAllowance - dockShift,
_width +
edgeAllowance -
dockShift -
lensWidth,
),
top: (_height - lensHeight) / 2,
width: lensWidth,
height: lensHeight,
@@ -639,8 +668,8 @@ class _GlassRimPainter extends CustomPainter {
outer,
Paint()
..style = PaintingStyle.stroke
..strokeWidth = lens ? 1.4 : 1.8
..maskFilter = MaskFilter.blur(BlurStyle.normal, lens ? 0.9 : 0.65)
..strokeWidth = lens ? 1.2 : 1.6
..maskFilter = MaskFilter.blur(BlurStyle.normal, lens ? 1.05 : 1.0)
..shader = ui.Gradient.linear(
bounds.topLeft,
bounds.bottomRight,
@@ -656,16 +685,16 @@ class _GlassRimPainter extends CustomPainter {
outer,
Paint()
..style = PaintingStyle.stroke
..strokeWidth = lens ? 1.0 : 0.85
..strokeWidth = lens ? 0.85 : 0.65
..shader = ui.Gradient.sweep(
bounds.center,
[
Colors.white.withValues(alpha: 0.96 * opacity),
Colors.white.withValues(alpha: 0.84 * opacity),
edge.withValues(alpha: (lens ? 0.30 : 0.15) * opacity),
Colors.white.withValues(alpha: 0.10 * opacity),
Colors.white.withValues(alpha: 0.94 * opacity),
Colors.white.withValues(alpha: 0.82 * opacity),
edge.withValues(alpha: 0.12 * opacity),
Colors.white.withValues(alpha: 0.96 * opacity),
Colors.white.withValues(alpha: 0.84 * opacity),
],
[0.0, 0.18, 0.38, 0.60, 0.80, 1.0],
TileMode.clamp,
@@ -677,7 +706,7 @@ class _GlassRimPainter extends CustomPainter {
inner,
Paint()
..style = PaintingStyle.stroke
..strokeWidth = 0.45
..strokeWidth = 0.35
..shader = ui.Gradient.linear(
bounds.topLeft,
bounds.bottomRight,