Files
holo_shiny/example/lib/main.dart
T

544 lines
17 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> {
/// Sensor-driven controller used by the first two demo surfaces.
late final ShinyController _sensorController;
/// Manual stream used by tilt preset buttons.
late final StreamController<Offset> _externalTiltController;
/// Controller backed by [_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;
HolographStyle _style = HolographStyle.crackedIce;
SparkleShapeSpec _sparkleShape = SparkleShapeSpec.none;
static const Map<String, SparkleShapeSpec> _sparkleChoices =
<String, SparkleShapeSpec>{
'None': SparkleShapeSpec.none,
'8-Point Star': SparkleShapeSpec.eightPointStar,
'5-Point Star': SparkleShapeSpec.fivePointStar,
'Rectangle': SparkleShapeSpec.rectangle,
'Diamond': SparkleShapeSpec.diamond,
'Hexagon': SparkleShapeSpec.hexagon,
'Random Polygon': SparkleShapeSpec.randomPolygon,
'Confetti': SparkleShapeSpec.confetti,
};
@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,
style: _style,
prismatic: _prismatic,
sparkle: _sparkle,
specular: _specular,
diffraction: _diffraction,
opacity: _opacity,
sparkleShape: _sparkleShape,
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,
style: _style,
sparkleShape: _sparkleShape,
background: const _DemoCardBackground(),
foreground: const _RareBadge(),
prismatic: _prismatic,
sparkle: _sparkle,
specular: _specular,
diffraction: _diffraction,
opacity: _opacity,
),
),
const SizedBox(height: 16),
_StylePicker(
selectedStyle: _style,
onChanged: (HolographStyle style) {
setState(() {
_style = style;
});
},
),
const SizedBox(height: 8),
_SparkleShapePicker(
selectedShape: _sparkleShape,
choices: _sparkleChoices,
onChanged: (SparkleShapeSpec shape) {
setState(() {
_sparkleShape = shape;
});
},
),
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,
style: _style,
sparkleShape: _sparkleShape,
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 {
/// Creates the style selection control.
const _StylePicker({
required this.selectedStyle,
required this.onChanged,
});
final HolographStyle selectedStyle;
final ValueChanged<HolographStyle> onChanged;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Text('Style'),
const SizedBox(height: 6),
SegmentedButton<HolographStyle>(
segments: HolographStyle.values
.map((HolographStyle style) => ButtonSegment<HolographStyle>(
value: style,
label: Text(_styleLabel(style)),
))
.toList(),
selected: <HolographStyle>{selectedStyle},
onSelectionChanged: (Set<HolographStyle> value) {
if (value.isNotEmpty) {
onChanged(value.first);
}
},
showSelectedIcon: false,
multiSelectionEnabled: false,
),
],
);
}
String _styleLabel(HolographStyle style) {
switch (style) {
case HolographStyle.holographicSilver:
return 'Holographic Silver';
case HolographStyle.crackedIce:
return 'Cracked Ice';
case HolographStyle.silverMosaic:
return 'Silver Mosaic';
case HolographStyle.superGoldVinyl:
return 'Super Gold Vinyl';
}
}
}
class _SparkleShapePicker extends StatelessWidget {
/// Creates sparkle shape chips from the provided shape map.
const _SparkleShapePicker({
required this.selectedShape,
required this.choices,
required this.onChanged,
});
final SparkleShapeSpec selectedShape;
final Map<String, SparkleShapeSpec> choices;
final ValueChanged<SparkleShapeSpec> onChanged;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Text('Sparkle Shape'),
const SizedBox(height: 6),
Wrap(
spacing: 8,
runSpacing: 8,
children:
choices.entries.map((MapEntry<String, SparkleShapeSpec> entry) {
return ChoiceChip(
label: Text(entry.key),
selected: selectedShape == entry.value,
onSelected: (bool selected) {
if (!selected) {
return;
}
onChanged(entry.value);
},
);
}).toList(),
),
],
);
}
}
/// 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,
),
),
),
);
}
}