Files
holo_shiny/example/lib/main.dart
T
hans 9564096a5c
CI / quality (push) Failing after 5m56s
Optimize shaders
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
2026-04-21 17:40:21 +02:00

471 lines
15 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:holo_shiny/holo_shiny.dart';
/// Entry point for the package demo application.
void main() {
runApp(const ExampleApp());
}
/// Root app widget for the interactive shader demo.
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: const ExampleHome(),
);
}
}
/// Interactive screen used to test style and uniform controls.
class ExampleHome extends StatefulWidget {
const ExampleHome({super.key});
@override
State<ExampleHome> createState() => _ExampleHomeState();
}
class _ExampleHomeState extends State<ExampleHome> {
late final ShinyController _sensorController;
late final StreamController<Offset> _externalTiltController;
late final ShinyController _overrideController;
double _prismatic = 0.8;
double _sparkle = 0.8;
double _specular = 0.8;
double _diffraction = 0.8;
double _opacity = 1.0;
ShinyProfile _profile = ShinyProfile.crackedIce;
@override
void initState() {
super.initState();
_sensorController = ShinyController(useSensor: true);
_externalTiltController = StreamController<Offset>.broadcast();
_overrideController =
ShinyController(tiltStream: _externalTiltController.stream);
}
@override
void dispose() {
_sensorController.dispose();
_overrideController.dispose();
_externalTiltController.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('holo_shiny example'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
const Text('Shiny on Any Widget'),
const SizedBox(height: 8),
Center(
child: SizedBox(
width: 320,
height: 120,
child: Shiny(
controller: _sensorController,
profile: _profile,
prismatic: _prismatic,
sparkle: _sparkle,
specular: _specular,
diffraction: _diffraction,
opacity: _opacity,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF18263E),
borderRadius: BorderRadius.circular(14),
),
child: const Row(
children: <Widget>[
Icon(Icons.auto_awesome, color: Colors.white),
SizedBox(width: 12),
Expanded(
child: Text(
'This can wrap any widget, including app chrome.',
style: TextStyle(color: Colors.white),
),
),
],
),
),
),
),
),
const SizedBox(height: 24),
const Text('ShinyCard (shape + rotation + shine)'),
const SizedBox(height: 8),
Center(
child: ShinyCard(
controller: _sensorController,
profile: _profile,
background: const _DemoCardBackground(),
foreground: const _RareBadge(),
prismatic: _prismatic,
sparkle: _sparkle,
specular: _specular,
diffraction: _diffraction,
opacity: _opacity,
),
),
const SizedBox(height: 16),
_StylePicker(
selectedProfile: _profile,
onChanged: (ShinyProfile profile) {
setState(() {
_profile = profile;
});
},
),
const SizedBox(height: 8),
_LabeledSlider('Prismatic', _prismatic, (double value) {
setState(() {
_prismatic = value;
});
}),
_LabeledSlider('Sparkle', _sparkle, (double value) {
setState(() {
_sparkle = value;
});
}),
_LabeledSlider('Specular', _specular, (double value) {
setState(() {
_specular = value;
});
}),
_LabeledSlider('Diffraction', _diffraction, (double value) {
setState(() {
_diffraction = value;
});
}),
_LabeledSlider('Opacity', _opacity, (double value) {
setState(() {
_opacity = value;
});
}),
const SizedBox(height: 24),
const Text('External Tilt Stream + Custom Shape (card)'),
const SizedBox(height: 8),
_TiltPresetPicker(
onChanged: (Offset tilt) {
_externalTiltController.add(tilt);
},
),
const SizedBox(height: 8),
Center(
child: ShinyCard(
controller: _overrideController,
profile: _profile,
prismatic: _prismatic,
sparkle: _sparkle,
specular: _specular,
diffraction: _diffraction,
opacity: _opacity,
shape: const StadiumBorder(),
background: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: <Color>[Color(0xFF1A2A44), Color(0xFF15263D)],
),
),
),
foreground: const Center(
child: Text(
'EXTERNAL STREAM',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w900,
letterSpacing: 1.2,
),
),
),
),
),
],
),
),
);
}
}
class _LabeledSlider extends StatelessWidget {
/// Creates a slider row with a live numeric label.
const _LabeledSlider(this.label, this.value, this.onChanged);
final String label;
final double value;
final ValueChanged<double> onChanged;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('$label: ${value.toStringAsFixed(2)}'),
Slider(
value: value,
divisions: 20,
label: value.toStringAsFixed(2),
onChanged: onChanged,
),
],
);
}
}
class _StylePicker extends StatelessWidget {
const _StylePicker({
required this.selectedProfile,
required this.onChanged,
});
final ShinyProfile selectedProfile;
final ValueChanged<ShinyProfile> onChanged;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Text('Profile'),
const SizedBox(height: 6),
SegmentedButton<ShinyProfile>(
segments: ShinyProfile.values
.map((ShinyProfile profile) => ButtonSegment<ShinyProfile>(
value: profile,
label: Text(_profileLabel(profile)),
))
.toList(),
selected: <ShinyProfile>{selectedProfile},
onSelectionChanged: (Set<ShinyProfile> value) {
if (value.isNotEmpty) {
onChanged(value.first);
}
},
showSelectedIcon: false,
multiSelectionEnabled: false,
),
],
);
}
String _profileLabel(ShinyProfile profile) {
switch (profile) {
case ShinyProfile.holographicSilver:
return 'Holographic Silver';
case ShinyProfile.crackedIce:
return 'Cracked Ice';
case ShinyProfile.silverMosaic:
return 'Silver Mosaic';
case ShinyProfile.superGoldVinyl:
return 'Super Gold Vinyl';
}
}
}
/// Circular D-pad control for feeding the external tilt stream.
class _TiltPresetPicker extends StatelessWidget {
const _TiltPresetPicker({required this.onChanged});
final ValueChanged<Offset> onChanged;
static const double _tiltAmount = 0.7;
static const double _deadZoneRatio = 0.18;
void _emitDragTilt(Offset localPosition, Size size) {
final Offset center = Offset(size.width / 2.0, size.height / 2.0);
final double radius = size.shortestSide / 2.0;
final Offset delta = localPosition - center;
final double distance = delta.distance;
if (radius <= 0 || distance <= radius * _deadZoneRatio) {
onChanged(Offset.zero);
return;
}
Offset normalized = delta / radius;
final double normalizedDistance = normalized.distance;
if (normalizedDistance > 1.0) {
normalized = normalized / normalizedDistance;
}
onChanged(normalized * _tiltAmount);
}
@override
Widget build(BuildContext context) {
return Center(
child: SizedBox(
width: 176,
height: 176,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final Size size = constraints.biggest;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: (DragDownDetails details) {
_emitDragTilt(details.localPosition, size);
},
onPanUpdate: (DragUpdateDetails details) {
_emitDragTilt(details.localPosition, size);
},
onPanEnd: (_) => onChanged(Offset.zero),
onPanCancel: () => onChanged(Offset.zero),
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.surfaceContainerHighest,
),
child: Stack(
children: <Widget>[
Align(
alignment: Alignment.topCenter,
child: IconButton.filledTonal(
tooltip: 'Tilt Up',
onPressed: () =>
onChanged(const Offset(0.0, -_tiltAmount)),
icon: const Icon(Icons.keyboard_arrow_up),
),
),
Align(
alignment: Alignment.centerLeft,
child: IconButton.filledTonal(
tooltip: 'Tilt Left',
onPressed: () =>
onChanged(const Offset(-_tiltAmount, 0.0)),
icon: const Icon(Icons.keyboard_arrow_left),
),
),
Align(
alignment: Alignment.center,
child: IconButton.outlined(
tooltip: 'Center Tilt',
onPressed: () => onChanged(Offset.zero),
icon: const Icon(Icons.adjust),
),
),
Align(
alignment: Alignment.centerRight,
child: IconButton.filledTonal(
tooltip: 'Tilt Right',
onPressed: () =>
onChanged(const Offset(_tiltAmount, 0.0)),
icon: const Icon(Icons.keyboard_arrow_right),
),
),
Align(
alignment: Alignment.bottomCenter,
child: IconButton.filledTonal(
tooltip: 'Tilt Down',
onPressed: () =>
onChanged(const Offset(0.0, _tiltAmount)),
icon: const Icon(Icons.keyboard_arrow_down),
),
),
],
),
),
);
},
),
),
);
}
}
/// Demonstration card background for the primary example.
class _DemoCardBackground extends StatelessWidget {
const _DemoCardBackground();
@override
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: <Widget>[
Image.asset(
'assets/pokemon.png',
fit: BoxFit.cover,
alignment: Alignment.topCenter,
errorBuilder:
(BuildContext context, Object error, StackTrace? stackTrace) {
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: <Color>[Color(0xFF2D1B1B), Color(0xFF120D18)],
),
),
padding: const EdgeInsets.all(16),
child: const Align(
alignment: Alignment.bottomLeft,
child: Text(
'Demo Card',
style: TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.w900,
),
),
),
);
},
),
DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[
Colors.black.withValues(alpha: 0.10),
Colors.black.withValues(alpha: 0.22),
],
),
),
),
],
);
}
}
/// Small badge overlay placed on top of the shiny card.
class _RareBadge extends StatelessWidget {
const _RareBadge();
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.topRight,
child: Container(
margin: const EdgeInsets.all(12),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(99),
),
child: const Text(
'RARE',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
letterSpacing: 1,
),
),
),
);
}
}