class TiltExample extends StatelessWidget {
const TiltExample({super.key});
static const double width = 236.0 * 1.2;
static const double height = 330.0 * 1.2;
@override
Widget build(BuildContext context) {
return Tilt(
tiltConfig: const TiltConfig(
angle: 15,
leaveCurve: Curves.fastEaseInToSlowEaseOut,
leaveDuration: Duration(milliseconds: 900),
),
child: TiltAnimatedBuilder(
builder: (BuildContext context, TiltAnimatedState tiltAnimatedState, Widget? child) {
final animatedTiltData = tiltAnimatedState.animatedTiltData;
return Transform(
alignment: Alignment.center,
transform: animatedTiltData.transform,
filterQuality: FilterQuality.high,
child: HolofoilContainer(
width: width,
height: height,
progress: animatedTiltData.areaProgress,
child: child!,
),
);
},
child: SizedBox(
width: width,
height: height,
child: Image.asset('assets/tilt_animated_builder/froakie.jpg', fit: BoxFit.cover),
),
),
);
}
}
class HolofoilContainer extends StatelessWidget {
const HolofoilContainer({
super.key,
required this.width,
required this.height,
required this.progress,
required this.child,
});
final double width;
final double height;
final Offset progress;
final Widget child;
double get tiltMagnitude => (progress.distance / math.sqrt2).clamp(0.0, 1.0);
@override
Widget build(BuildContext context) {
return Stack(
alignment: AlignmentDirectional.center,
clipBehavior: Clip.hardEdge,
children: [
DecoratedBox(
decoration: const BoxDecoration(color: Colors.transparent),
child: SizedBox(width: width * 1.2, height: height * 1.2),
),
ClipRRect(
borderRadius: BorderRadius.circular(16),
clipBehavior: Clip.antiAlias,
child: SizedBox(
width: width,
height: height,
child: Stack(
alignment: AlignmentDirectional.center,
clipBehavior: Clip.hardEdge,
children: [
child,
IgnorePointer(
child: CustomPaint(
willChange: true,
size: Size(width, height),
painter: HolofoilPainter(progress: progress),
),
),
IgnorePointer(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment(-progress.dx * 0.65, -progress.dy * 0.65 - 1.0),
end: Alignment(progress.dx * 0.65, progress.dy * 0.65 + 1.0),
colors: [
Colors.white.withValues(alpha: 0.06 + tiltMagnitude * 0.10),
Colors.transparent,
Colors.transparent,
Colors.black.withValues(alpha: 0.04 + tiltMagnitude * 0.06),
],
stops: const [0.0, 0.38, 0.62, 1.0],
),
),
child: SizedBox(width: width, height: height),
),
),
],
),
),
),
],
);
}
}
class HolofoilPainter extends CustomPainter {
const HolofoilPainter({required this.progress});
final Offset progress;
double get tiltMagnitude => (progress.distance / math.sqrt2).clamp(0.0, 1.0);
@override
void paint(Canvas canvas, Size size) {
final rect = Offset.zero & size;
paintDominantHue(canvas, rect);
paintSpectralBands(canvas, rect);
}
void paintDominantHue(Canvas canvas, Rect rect) {
final tiltAngle = progress.distance < 0.001 ? 0.0 : math.atan2(progress.dy, progress.dx);
final primaryHue = (tiltAngle / (2 * math.pi) * 360 + 180) % 360;
final opacity = (0.03 + tiltMagnitude * 0.22).clamp(0.0, 1.0);
final hueCenterX = rect.left + (0.5 + progress.dx * 0.58) * rect.width;
final hueCenterY = rect.top + (0.5 + progress.dy * 0.58) * rect.height;
canvas.drawRect(
rect,
Paint()
..shader =
RadialGradient(
colors: [
HSVColor.fromAHSV(opacity, primaryHue, 0.90, 1.0).toColor(),
HSVColor.fromAHSV(opacity * 0.60, (primaryHue + 50) % 360, 0.82, 1.0).toColor(),
HSVColor.fromAHSV(opacity * 0.22, (primaryHue + 160) % 360, 0.70, 1.0).toColor(),
Colors.transparent,
],
stops: const [0.0, 0.32, 0.64, 1.0],
).createShader(
Rect.fromCircle(
center: Offset(hueCenterX, hueCenterY),
radius: rect.longestSide * 0.90,
),
),
);
}
void paintSpectralBands(Canvas canvas, Rect rect) {
final normDx = progress.distance < 0.001
? 0.0
: (progress.dx / progress.distance).clamp(-1.0, 1.0);
final normDy = progress.distance < 0.001
? 0.0
: (progress.dy / progress.distance).clamp(-1.0, 1.0);
final phase = progress.dx * 215.0 + progress.dy * 108.0;
const cyclesA = 3;
final opacityA = (0.04 + tiltMagnitude * 0.28).clamp(0.0, 1.0);
final alignmentXA = 1.00 + normDy * 0.07;
final alignmentYA = 0.50 - normDx * 0.05;
canvas.drawRect(
rect,
Paint()
..shader = LinearGradient(
begin: Alignment(-alignmentXA, -alignmentYA),
end: Alignment(alignmentXA, alignmentYA),
colors: organicColors(
n: cyclesA * 10,
cycles: cyclesA,
phase: phase,
maxOpacity: opacityA,
warpAmplitude: 0.060,
warpFrequency: 8.0,
),
).createShader(rect),
);
const cyclesB = 2;
final opacityB = (0.01 + tiltMagnitude * 0.12).clamp(0.0, 1.0);
final alignmentXB = 0.50 + normDy * 0.04;
final alignmentYB = 1.00 - normDx * 0.07;
canvas.drawRect(
rect,
Paint()
..shader = LinearGradient(
begin: Alignment(-alignmentXB, -alignmentYB),
end: Alignment(alignmentXB, alignmentYB),
colors: organicColors(
n: cyclesB * 8,
cycles: cyclesB,
phase: phase + 90.0,
maxOpacity: opacityB,
warpAmplitude: 0.090,
warpFrequency: 5.5,
),
).createShader(rect),
);
}
List<Color> organicColors({
required int n,
required int cycles,
required double phase,
required double maxOpacity,
required double warpAmplitude,
required double warpFrequency,
}) => List<Color>.generate(n + 1, (i) {
final tLinear = i / n;
final tWarped = (tLinear + warpAmplitude * math.sin(tLinear * math.pi * warpFrequency)).clamp(
0.0,
1.0,
);
final hue = (phase + tWarped * 360.0 * cycles) % 360;
final saturation = (0.60 + 0.28 * math.sin(tWarped * math.pi * cycles * 2)).clamp(0.0, 1.0);
final envelope =
(math.sin(tLinear * math.pi) * 0.55 + math.sin(tLinear * math.pi * 2.0) * 0.27 + 0.18)
.clamp(0.0, 1.0);
return HSVColor.fromAHSV(
(maxOpacity * envelope).clamp(0.0, 1.0),
hue,
saturation,
1.0,
).toColor();
});
@override
bool shouldRepaint(HolofoilPainter old) => old.progress != progress;
}