feat: bloom tap lens with dock follow and soft edge overflow
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user