class TiltExample extends StatelessWidget {
const TiltExample({super.key});
@override
Widget build(BuildContext context) {
return Tilt(
tiltConfig: const TiltConfig(
angle: 25,
initial: Offset(0, -1),
enableRevert: false,
leaveCurve: Curves.linear,
leaveDuration: Duration(milliseconds: 300),
),
child: Padding(
padding: EdgeInsets.symmetric(vertical: 20, horizontal: 140),
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: DecoratedBox(
decoration: const BoxDecoration(color: Colors.transparent),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 40),
child: AspectRatio(
aspectRatio: 1.0,
child: LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth;
final height = constraints.maxHeight;
final spineWidth = width * 0.12;
final trayCenterX = spineWidth + (width - spineWidth) / 2;
final trayRadius = (width - spineWidth) * 0.48;
final cdDiameter = trayRadius * 2;
return Stack(
alignment: AlignmentDirectional.center,
children: [
Positioned.fill(child: child!),
Positioned(
left: trayCenterX - trayRadius,
top: (height / 2) - trayRadius,
width: cdDiameter,
height: cdDiameter,
child: TiltParallax(
offset: const Offset(20, 20),
child: CustomPaint(
willChange: true,
painter: CDPainter(areaProgress: animatedTiltData.areaProgress),
),
),
),
],
);
},
),
),
),
),
);
},
child: Opacity(
opacity: 0.2,
child: AspectRatio(
aspectRatio: 1.0,
child: CustomPaint(willChange: false, painter: CDCasePainter()),
),
),
),
),
);
}
}
class CDPainter extends CustomPainter {
const CDPainter({required this.areaProgress});
final Offset areaProgress;
static const double kHoleRadius = 0.125;
static const double kHubRadius = 0.267;
static const double kClampRadius = 0.425;
static const double kDataInnerRadius = 0.450;
static const double kDataOuterRadius = 0.967;
static final Paint groovePaint = Paint()
..color = Colors.black.withValues(alpha: 0.055)
..style = PaintingStyle.stroke
..strokeWidth = 0.45;
static final Paint rimBrightPaint = Paint()
..color = const Color(0x28FFFFFF)
..style = PaintingStyle.stroke
..strokeWidth = 1.8;
static final Paint rimDarkPaint = Paint()
..color = const Color(0x55101820)
..style = PaintingStyle.stroke
..strokeWidth = 1.0;
static final Paint hubGroovePaint = Paint()
..color = Colors.black.withValues(alpha: 0.08)
..style = PaintingStyle.stroke
..strokeWidth = 0.4;
static final Paint hubRingPaint07 = Paint()
..color = const Color(0x55606870)
..style = PaintingStyle.stroke
..strokeWidth = 0.7;
static final Paint hubRingPaint04 = Paint()
..color = const Color(0x55606870)
..style = PaintingStyle.stroke
..strokeWidth = 0.4;
static final Paint holePaint = Paint()..color = Colors.white;
static final Paint holeEdgePaint = Paint()
..color = const Color(0xFF485058).withValues(alpha: 0.4)
..style = PaintingStyle.stroke
..strokeWidth = 0.6;
static const kSpectrumColors = <Color>[
Color(0xFF8800EE),
Color(0xFF0044FF),
Color(0xFF00CCDD),
Color(0xFF22FF55),
Color(0xFFEEFF00),
Color(0xFFFF6600),
Color(0xFFFF0033),
];
static final List<double> kArcAStops = precomputeArcStops(5, 0.08);
static final List<double> kArcBStops = precomputeArcStops(4, 0.068);
static List<double> precomputeArcStops(int n, double arcHalfWidth) {
final spectrumCount = kSpectrumColors.length;
final stops = <double>[];
final slot = 1.0 / n;
for (var i = 0; i < n; i++) {
final arcCenter = (i + 0.5) * slot;
final arcStart = arcCenter - arcHalfWidth;
final arcEnd = arcCenter + arcHalfWidth;
stops.add(i == 0 ? 0.0 : i * slot);
for (var j = 0; j < spectrumCount; j++) {
final stop = arcStart + (j / (spectrumCount - 1)) * (arcEnd - arcStart);
stops.add(stop);
}
}
stops.add(1.0);
return stops;
}
static List<Color> buildArcColors(int n, double opacity) {
final colors = <Color>[];
for (var i = 0; i < n; i++) {
colors.add(Colors.transparent);
for (var j = 0; j < kSpectrumColors.length; j++) {
final envelope = math.sin(math.pi * j / (kSpectrumColors.length - 1)).clamp(0.0, 1.0);
colors.add(kSpectrumColors[j].withValues(alpha: opacity * envelope));
}
}
colors.add(Colors.transparent);
return colors;
}
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = math.min(size.width, size.height) / 2;
canvas.clipPath(Path()..addOval(Rect.fromCircle(center: center, radius: radius)));
drawMetallicBase(canvas, center, radius);
drawDyeLayer(canvas, center, radius);
drawDiffractionIridescence(canvas, center, radius);
drawReflectionBand(canvas, center, radius);
drawGrooves(canvas, center, radius);
drawSpecular(canvas, center, radius);
drawHub(canvas, center, radius);
drawRim(canvas, center, radius);
}
void drawMetallicBase(Canvas canvas, Offset center, double radius) {
final highlightX = -areaProgress.dx * 0.35;
final highlightY = -areaProgress.dy * 0.35;
final paint = Paint()
..shader = RadialGradient(
center: Alignment(highlightX, highlightY),
radius: 1.15,
colors: const [
Color(0xFFF0F4F8),
Color(0xFFCDD5DC),
Color(0xFFAAB3BB),
Color(0xFF858E96),
],
stops: const [0.0, 0.30, 0.65, 1.0],
).createShader(Rect.fromCircle(center: center, radius: radius));
canvas.drawCircle(center, radius, paint);
}
void drawDyeLayer(Canvas canvas, Offset center, double radius) {
final dataInnerRadius = radius * kDataInnerRadius;
final dataOuterRadius = radius * kDataOuterRadius;
final rect = Rect.fromCircle(center: center, radius: dataOuterRadius);
final goldPaint = Paint()
..shader = const RadialGradient(
radius: 1.0,
colors: [
Color(0x00C8A228),
Color(0x18C8A228),
Color(0x22C8A228),
Color(0x14C8A228),
Color(0x00C8A228),
],
stops: [0.0, 0.20, 0.55, 0.85, 1.0],
).createShader(rect)
..blendMode = BlendMode.multiply;
withDataAreaClip(canvas, center, dataInnerRadius, dataOuterRadius, () {
canvas.drawRect(rect, goldPaint);
});
final dyePaint = Paint()
..shader = const RadialGradient(
radius: 1.0,
colors: [
Color(0x00047A85),
Color(0x16047A85),
Color(0x20047A85),
Color(0x0C047A85),
Color(0x00047A85),
],
stops: [0.0, 0.25, 0.58, 0.88, 1.0],
).createShader(rect)
..blendMode = BlendMode.overlay;
withDataAreaClip(canvas, center, dataInnerRadius, dataOuterRadius, () {
canvas.drawRect(rect, dyePaint);
});
}
void drawDiffractionIridescence(Canvas canvas, Offset center, double radius) {
final magnitude = areaProgress.distance;
final tiltAngle = magnitude < 0.002 ? 0.0 : math.atan2(areaProgress.dy, areaProgress.dx);
final tiltMagnitude = magnitude.clamp(0.0, 1.0);
final dataInnerRadius = radius * kDataInnerRadius;
final dataOuterRadius = radius * kDataOuterRadius;
final rect = Rect.fromCircle(center: center, radius: dataOuterRadius);
withDataAreaClip(canvas, center, dataInnerRadius, dataOuterRadius, () {
final baseOpacity = 0.05 + tiltMagnitude * 0.04;
canvas.drawRect(
rect,
Paint()
..shader = RadialGradient(
center: Alignment(-areaProgress.dx * 0.10, -areaProgress.dy * 0.10),
radius: 1.0,
colors: [
Color.fromRGBO(120, 60, 255, baseOpacity),
Color.fromRGBO(0, 140, 255, baseOpacity),
Color.fromRGBO(0, 230, 160, baseOpacity),
Color.fromRGBO(220, 180, 0, baseOpacity * 0.6),
],
stops: const [0.0, 0.33, 0.66, 1.0],
).createShader(rect)
..blendMode = BlendMode.overlay,
);
});
final opacityA = tiltMagnitude * 0.72;
final colorsA = buildArcColors(5, opacityA);
withDataAreaClip(canvas, center, dataInnerRadius, dataOuterRadius, () {
canvas.drawRect(
rect,
Paint()
..shader = SweepGradient(
center: Alignment.center,
colors: colorsA,
stops: kArcAStops,
transform: GradientRotation(tiltAngle),
).createShader(rect)
..blendMode = BlendMode.screen,
);
});
final opacityB = tiltMagnitude * 0.48;
final colorsB = buildArcColors(4, opacityB);
withDataAreaClip(canvas, center, dataInnerRadius, dataOuterRadius, () {
canvas.drawRect(
rect,
Paint()
..shader = SweepGradient(
center: Alignment.center,
colors: colorsB,
stops: kArcBStops,
transform: GradientRotation(-tiltAngle * 0.75 + math.pi / 4),
).createShader(rect)
..blendMode = BlendMode.screen,
);
});
}
void drawReflectionBand(Canvas canvas, Offset center, double radius) {
final magnitude = areaProgress.distance;
if (magnitude < 0.008) return;
final tiltAngle = math.atan2(areaProgress.dy, areaProgress.dx);
final tiltMagnitude = magnitude.clamp(0.0, 1.0);
final dataInnerRadius = radius * kDataInnerRadius;
final dataOuterRadius = radius * kDataOuterRadius;
final rect = Rect.fromCircle(center: center, radius: dataOuterRadius);
final peakOpacity = tiltMagnitude * 0.82;
void drawArc(double rotOffset, double opacity) {
withDataAreaClip(canvas, center, dataInnerRadius, dataOuterRadius, () {
canvas.drawRect(
rect,
Paint()
..shader = SweepGradient(
center: Alignment.center,
colors: [
Colors.transparent,
Colors.white.withValues(alpha: opacity * 0.08),
Colors.white.withValues(alpha: opacity * 0.45),
Colors.white.withValues(alpha: opacity * 0.55),
Colors.white.withValues(alpha: opacity * 0.45),
Colors.white.withValues(alpha: opacity * 0.08),
Colors.transparent,
Colors.transparent,
],
stops: const [0.0, 0.44, 0.47, 0.50, 0.53, 0.56, 0.62, 1.00],
transform: GradientRotation(tiltAngle - math.pi + rotOffset),
).createShader(rect)
..blendMode = BlendMode.screen,
);
});
}
drawArc(0, peakOpacity);
drawArc(math.pi, peakOpacity * 0.55);
}
void drawGrooves(Canvas canvas, Offset center, double radius) {
const grooveCount = 18;
final innerRadius = radius * kDataInnerRadius;
final outerRadius = radius * kDataOuterRadius;
for (var i = 0; i <= grooveCount; i++) {
final grooveRadius = innerRadius + (outerRadius - innerRadius) * (i / grooveCount);
canvas.drawCircle(center, grooveRadius, groovePaint);
}
}
void drawSpecular(Canvas canvas, Offset center, double radius) {
final specularX = -areaProgress.dx * radius * 0.28;
final specularY = -areaProgress.dy * radius * 0.28;
canvas.drawCircle(
center,
radius,
Paint()
..shader = RadialGradient(
center: Alignment(specularX / radius, specularY / radius),
radius: 0.50,
colors: [Colors.white.withValues(alpha: 0.30), Colors.white.withValues(alpha: 0.0)],
).createShader(Rect.fromCircle(center: center, radius: radius))
..blendMode = BlendMode.screen,
);
}
void drawHub(Canvas canvas, Offset center, double radius) {
final holeRadius = radius * kHoleRadius;
final hubRadius = radius * kHubRadius;
final clampRadius = radius * kClampRadius;
canvas.drawCircle(
center,
clampRadius,
Paint()
..shader = const RadialGradient(
center: Alignment(-0.15, -0.15),
radius: 1.0,
colors: [Color(0xFFDEE6EE), Color(0xFFC4CED8), Color(0xFFAEB8C2)],
stops: [0.0, 0.55, 1.0],
).createShader(Rect.fromCircle(center: center, radius: clampRadius)),
);
for (var i = 1; i <= 3; i++) {
canvas.drawCircle(center, clampRadius * (0.52 + i * 0.13), hubGroovePaint);
}
canvas.drawCircle(
center,
hubRadius,
Paint()
..shader = const RadialGradient(
center: Alignment(-0.25, -0.25),
radius: 1.0,
colors: [Color(0xFFD4DCE5), Color(0xFFBCC6D0), Color(0xFF96A0AA)],
stops: [0.0, 0.50, 1.0],
).createShader(Rect.fromCircle(center: center, radius: hubRadius)),
);
canvas.drawCircle(center, hubRadius, hubRingPaint07);
canvas.drawCircle(center, clampRadius * 0.975, hubRingPaint04);
canvas.drawCircle(center, holeRadius, holePaint);
canvas.drawCircle(center, holeRadius, holeEdgePaint);
}
void drawRim(Canvas canvas, Offset center, double radius) {
canvas.drawCircle(center, radius - 1.0, rimBrightPaint);
canvas.drawCircle(center, radius - 0.4, rimDarkPaint);
}
void withDataAreaClip(
Canvas canvas,
Offset center,
double innerRadius,
double outerRadius,
void Function() draw,
) {
canvas.save();
final clip = Path()
..addOval(Rect.fromCircle(center: center, radius: outerRadius))
..addOval(Rect.fromCircle(center: center, radius: innerRadius));
clip.fillType = PathFillType.evenOdd;
canvas.clipPath(clip);
draw();
canvas.restore();
}
@override
bool shouldRepaint(CDPainter old) => old.areaProgress != areaProgress;
}
class CDCasePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final width = size.width;
final height = size.height;
final linePaint = Paint()
..color = const Color(0xFF1A1A1A)
..style = PaintingStyle.stroke
..strokeWidth = 1.2
..strokeCap = StrokeCap.square;
final outerCase = Rect.fromLTRB(0, 0, width, height);
canvas.drawRect(outerCase, linePaint);
final innerOffset = width * 0.015;
final innerCase = outerCase.deflate(innerOffset);
canvas.drawRect(innerCase, linePaint);
final spineWidth = outerCase.width * 0.12;
final spineX = outerCase.left + spineWidth;
canvas.drawLine(Offset(spineX, outerCase.top), Offset(spineX, outerCase.bottom), linePaint);
canvas.drawLine(
Offset(spineX + innerOffset, innerCase.top),
Offset(spineX + innerOffset, innerCase.bottom),
linePaint,
);
final topNotchY = outerCase.top + height * 0.06;
final bottomNotchY = outerCase.bottom - height * 0.06;
canvas.drawLine(Offset(outerCase.left, topNotchY), Offset(spineX, topNotchY), linePaint);
canvas.drawLine(Offset(outerCase.left, bottomNotchY), Offset(spineX, bottomNotchY), linePaint);
canvas.drawRect(
Rect.fromLTRB(
outerCase.left + innerOffset * 1.5,
topNotchY + innerOffset,
spineX - innerOffset,
topNotchY + innerOffset * 3,
),
linePaint,
);
canvas.drawRect(
Rect.fromLTRB(
outerCase.left + innerOffset * 1.5,
bottomNotchY - innerOffset * 3,
spineX - innerOffset,
bottomNotchY - innerOffset,
),
linePaint,
);
final notches = Path();
final radius = width * 0.025;
notches.addArc(
Rect.fromCircle(center: Offset(spineX + width * 0.1, outerCase.bottom), radius: radius),
math.pi,
math.pi,
);
notches.addArc(
Rect.fromCircle(
center: Offset(outerCase.right - width * 0.1, outerCase.bottom),
radius: radius,
),
math.pi,
math.pi,
);
notches.addArc(
Rect.fromCircle(center: Offset(spineX + width * 0.1, outerCase.top), radius: radius),
0,
math.pi,
);
notches.addArc(
Rect.fromCircle(center: Offset(outerCase.right - width * 0.1, outerCase.top), radius: radius),
0,
math.pi,
);
canvas.drawRect(
Rect.fromLTRB(
outerCase.right - innerOffset * 1.5,
topNotchY,
outerCase.right,
topNotchY + width * 0.04,
),
linePaint,
);
canvas.drawRect(
Rect.fromLTRB(
outerCase.right - innerOffset * 1.5,
bottomNotchY - width * 0.04,
outerCase.right,
bottomNotchY,
),
linePaint,
);
canvas.drawPath(notches, linePaint);
final trayCenterX = spineX + (outerCase.right - spineX) / 2;
final trayCenterY = outerCase.center.dy;
final trayCenter = Offset(trayCenterX, trayCenterY);
final trayRadius = (outerCase.width - spineWidth) * 0.48;
canvas.drawCircle(trayCenter, trayRadius, linePaint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}