glow_effects 0.1.0
glow_effects: ^0.1.0 copied to clipboard
GPU-native visual effects for Flutter — Fragment Shaders, CustomPainter & Explicit Animations. 10 shader effects, 4 painters, text effects, page transitions & more.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:glow_effects/glow_effects.dart';
void main() {
runApp(const ShaderEffectsExampleApp());
}
class ShaderEffectsExampleApp extends StatelessWidget {
const ShaderEffectsExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Shader Effects Showcase',
debugShowCheckedModeBanner: false,
theme: ThemeData.dark(
useMaterial3: true,
).copyWith(scaffoldBackgroundColor: const Color(0xFF0A0A0A)),
home: const ShowcaseShell(),
);
}
}
// ---------------------------------------------------------------------------
// Shell — bottom navigation with debug overlay toggle
// ---------------------------------------------------------------------------
class ShowcaseShell extends StatefulWidget {
const ShowcaseShell({super.key});
@override
State<ShowcaseShell> createState() => _ShowcaseShellState();
}
class _ShowcaseShellState extends State<ShowcaseShell> {
int _tabIndex = 0;
bool _showDebug = false;
static const _tabs = <Widget>[
_ShadersTab(),
_PaintersTab(),
_TextTab(),
_TransitionsTab(),
_ComposerTab(),
];
@override
Widget build(BuildContext context) {
Widget body = Scaffold(
body: IndexedStack(index: _tabIndex, children: _tabs),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _tabIndex,
onTap: (i) => setState(() => _tabIndex = i),
type: BottomNavigationBarType.fixed,
selectedItemColor: Colors.cyanAccent,
unselectedItemColor: Colors.white38,
backgroundColor: const Color(0xFF111111),
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.auto_awesome),
label: 'Shaders',
),
BottomNavigationBarItem(icon: Icon(Icons.brush), label: 'Painters'),
BottomNavigationBarItem(icon: Icon(Icons.text_fields), label: 'Text'),
BottomNavigationBarItem(
icon: Icon(Icons.swap_horiz),
label: 'Transitions',
),
BottomNavigationBarItem(icon: Icon(Icons.layers), label: 'Composer'),
],
),
floatingActionButton: FloatingActionButton.small(
onPressed: () => setState(() => _showDebug = !_showDebug),
backgroundColor: _showDebug ? Colors.cyanAccent : Colors.white24,
child: Icon(
Icons.bug_report,
color: _showDebug ? Colors.black : Colors.white,
),
),
);
if (_showDebug) {
body = GKDebugOverlay(showFps: true, showUniforms: true, child: body);
}
return body;
}
}
// ---------------------------------------------------------------------------
// Shared effect card
// ---------------------------------------------------------------------------
class _EffectCard extends StatelessWidget {
const _EffectCard({required this.title, required this.onTap, this.subtitle});
final String title;
final String? subtitle;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Card(
color: Colors.white10,
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
clipBehavior: Clip.antiAlias,
child: ListTile(
title: Text(title, style: const TextStyle(color: Colors.white)),
subtitle: subtitle != null
? Text(
subtitle!,
style: const TextStyle(color: Colors.white38, fontSize: 12),
)
: null,
trailing: const Icon(
Icons.arrow_forward_ios,
size: 14,
color: Colors.white24,
),
onTap: onTap,
),
);
}
}
Widget _demoScaffold(String title, Widget body) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: body,
);
}
// ---------------------------------------------------------------------------
// Tab 1: Shaders
// ---------------------------------------------------------------------------
class _ShadersTab extends StatelessWidget {
const _ShadersTab();
@override
Widget build(BuildContext context) {
final effects = <(String, String, Widget)>[
(
'Glitch',
'Block displacement + RGB split + scanlines',
const _GlitchDemo(),
),
('Aurora', 'Layered sine waves + HSL cycling', const _AuroraDemo()),
(
'Dissolve',
'Noise-based dissolve with spark edge',
const _DissolveDemo(),
),
('Liquid', 'Organic flowing color waves', const _LiquidDemo()),
(
'Chromatic Aberration',
'RGB channel split with radial falloff',
const _ChromaticDemo(),
),
('Pixelate', 'Mosaic grid + color quantization', const _PixelateDemo()),
('VHS', 'Scanlines + noise + tracking errors', const _VHSDemo()),
('Heat Wave', 'Rising sine distortion', const _HeatWaveDemo()),
('Neon Glow', 'Bloom + flicker on neon shapes', const _NeonGlowDemo()),
(
'Holographic',
'Rainbow iridescence + fresnel + sparkle',
const _HolographicDemo(),
),
];
return _demoScaffold(
'Shader Effects',
ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: effects.length,
itemBuilder: (context, i) {
final (title, subtitle, page) = effects[i];
return _EffectCard(
title: title,
subtitle: subtitle,
onTap: () => Navigator.of(
context,
).push(GKPageRoute(page: page, type: GKTransitionType.warp)),
);
},
),
);
}
}
class _GlitchDemo extends StatelessWidget {
const _GlitchDemo();
@override
Widget build(BuildContext context) {
return _demoScaffold(
'Glitch Effect',
const SizedBox.expand(
child: GKWidget(
effect: GlitchEffect(blockSize: 0.05, rgbOffset: 0.015),
trigger: GKTrigger.auto,
),
),
);
}
}
class _AuroraDemo extends StatelessWidget {
const _AuroraDemo();
@override
Widget build(BuildContext context) {
return _demoScaffold(
'Aurora Effect',
const SizedBox.expand(
child: GKWidget(
effect: AuroraEffect(speed: 1.0, colorShift: 0.0),
trigger: GKTrigger.auto,
interactive: true,
),
),
);
}
}
class _DissolveDemo extends StatefulWidget {
const _DissolveDemo();
@override
State<_DissolveDemo> createState() => _DissolveDemoState();
}
class _DissolveDemoState extends State<_DissolveDemo> {
double _progress = 0.0;
@override
Widget build(BuildContext context) {
return _demoScaffold(
'Dissolve Effect',
Column(
children: [
Expanded(
child: GKWidget(
effect: DissolveEffect(
progress: _progress,
noiseScale: 4.0,
edgeSoftness: 0.1,
),
trigger: GKTrigger.auto,
child: Center(
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
gradient: const LinearGradient(
colors: [Colors.deepPurple, Colors.cyanAccent],
),
),
child: const Center(
child: Icon(
Icons.auto_awesome,
size: 64,
color: Colors.white,
),
),
),
),
),
),
Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
Text(
'Progress: ${_progress.toStringAsFixed(2)}',
style: const TextStyle(color: Colors.white70),
),
Slider(
value: _progress,
onChanged: (v) => setState(() => _progress = v),
),
],
),
),
],
),
);
}
}
class _LiquidDemo extends StatelessWidget {
const _LiquidDemo();
@override
Widget build(BuildContext context) {
return _demoScaffold(
'Liquid Effect',
const SizedBox.expand(
child: GKWidget(
effect: LiquidEffect(speed: 1.0, viscosity: 0.5),
trigger: GKTrigger.auto,
),
),
);
}
}
class _ChromaticDemo extends StatelessWidget {
const _ChromaticDemo();
@override
Widget build(BuildContext context) {
return _demoScaffold(
'Chromatic Aberration',
const SizedBox.expand(
child: GKWidget(
effect: ChromaticAberrationEffect(offset: 0.02, angle: 0.0),
trigger: GKTrigger.auto,
),
),
);
}
}
class _PixelateDemo extends StatelessWidget {
const _PixelateDemo();
@override
Widget build(BuildContext context) {
return _demoScaffold(
'Pixelate Effect',
const SizedBox.expand(
child: GKWidget(
effect: PixelateEffect(pixelSize: 10.0, animate: true),
trigger: GKTrigger.auto,
),
),
);
}
}
class _VHSDemo extends StatelessWidget {
const _VHSDemo();
@override
Widget build(BuildContext context) {
return _demoScaffold(
'VHS Effect',
const SizedBox.expand(
child: GKWidget(
effect: VHSEffect(noiseAmount: 0.12, trackingError: 0.06),
trigger: GKTrigger.auto,
),
),
);
}
}
class _HeatWaveDemo extends StatelessWidget {
const _HeatWaveDemo();
@override
Widget build(BuildContext context) {
return _demoScaffold(
'Heat Wave Effect',
const SizedBox.expand(
child: GKWidget(
effect: HeatWaveEffect(frequency: 8.0, amplitude: 0.015),
trigger: GKTrigger.auto,
),
),
);
}
}
class _NeonGlowDemo extends StatelessWidget {
const _NeonGlowDemo();
@override
Widget build(BuildContext context) {
return _demoScaffold(
'Neon Glow Effect',
const SizedBox.expand(
child: GKWidget(
effect: NeonGlowEffect(
glowRadius: 0.05,
flickerSpeed: 3.0,
glowColor: Color(0xFF00FFFF),
),
trigger: GKTrigger.auto,
),
),
);
}
}
class _HolographicDemo extends StatelessWidget {
const _HolographicDemo();
@override
Widget build(BuildContext context) {
return _demoScaffold(
'Holographic Effect',
const SizedBox.expand(
child: GKWidget(
effect: HolographicEffect(speed: 1.0, fresnelPower: 2.0),
trigger: GKTrigger.auto,
interactive: true,
),
),
);
}
}
// ---------------------------------------------------------------------------
// Tab 2: Painters
// ---------------------------------------------------------------------------
class _PaintersTab extends StatelessWidget {
const _PaintersTab();
@override
Widget build(BuildContext context) {
final painters = <(String, String, Widget)>[
(
'Magnetic Particles',
'Spring-physics particles attracted to pointer',
const _ParticlesDemo(),
),
('Morph Shape', 'Circle-to-star path interpolation', const _MorphDemo()),
(
'Constellation',
'Connected star network with pointer interaction',
const _ConstellationDemo(),
),
(
'Lightning',
'Midpoint displacement bolt with branches',
const _LightningDemo(),
),
];
return _demoScaffold(
'CustomPainter Effects',
ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: painters.length,
itemBuilder: (context, i) {
final (title, subtitle, page) = painters[i];
return _EffectCard(
title: title,
subtitle: subtitle,
onTap: () => Navigator.of(
context,
).push(GKPageRoute(page: page, type: GKTransitionType.ripple)),
);
},
),
);
}
}
class _ParticlesDemo extends StatefulWidget {
const _ParticlesDemo();
@override
State<_ParticlesDemo> createState() => _ParticlesDemoState();
}
class _ParticlesDemoState extends State<_ParticlesDemo>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
Offset _pointer = Offset.zero;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 4),
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return _demoScaffold(
'Magnetic Particles',
LayoutBuilder(
builder: (context, constraints) {
final size = Size(constraints.maxWidth, constraints.maxHeight);
return MouseRegion(
onHover: (e) => setState(() => _pointer = e.localPosition),
child: GestureDetector(
onPanUpdate: (d) => setState(() => _pointer = d.localPosition),
child: AnimatedBuilder(
animation: _controller,
builder: (context, _) {
return CustomPaint(
size: size,
painter: MagneticParticlesPainter(
particleCount: 80,
colors: const [
Colors.cyanAccent,
Colors.purpleAccent,
Colors.tealAccent,
Colors.pinkAccent,
],
magnetStrength: 150.0,
animationValue: _controller.value,
pointerPosition: _pointer,
size: size,
),
);
},
),
),
);
},
),
);
}
}
class _MorphDemo extends StatefulWidget {
const _MorphDemo();
@override
State<_MorphDemo> createState() => _MorphDemoState();
}
class _MorphDemoState extends State<_MorphDemo>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat(reverse: true);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Path _circlePath(Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = size.shortestSide * 0.35;
return Path()..addOval(Rect.fromCircle(center: center, radius: radius));
}
Path _starPath(Size size) {
final cx = size.width / 2;
final cy = size.height / 2;
final outerR = size.shortestSide * 0.35;
final innerR = outerR * 0.4;
const points = 5;
final path = Path();
for (var i = 0; i < points * 2; i++) {
final angle = (i * pi / points) - pi / 2;
final r = i.isEven ? outerR : innerR;
final x = cx + r * cos(angle);
final y = cy + r * sin(angle);
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
}
path.close();
return path;
}
@override
Widget build(BuildContext context) {
return _demoScaffold(
'Morph Shape',
LayoutBuilder(
builder: (context, constraints) {
final size = Size(constraints.maxWidth, constraints.maxHeight);
return AnimatedBuilder(
animation: _controller,
builder: (context, _) {
return CustomPaint(
size: size,
painter: MorphShapePainter(
fromPath: _circlePath(size),
toPath: _starPath(size),
progress: _controller.value,
color: Colors.cyanAccent,
strokeWidth: 3.0,
),
);
},
);
},
),
);
}
}
class _ConstellationDemo extends StatefulWidget {
const _ConstellationDemo();
@override
State<_ConstellationDemo> createState() => _ConstellationDemoState();
}
class _ConstellationDemoState extends State<_ConstellationDemo>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
Offset _pointer = Offset.zero;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 8),
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return _demoScaffold(
'Constellation',
LayoutBuilder(
builder: (context, constraints) {
final size = Size(constraints.maxWidth, constraints.maxHeight);
return MouseRegion(
onHover: (e) => setState(() => _pointer = e.localPosition),
child: GestureDetector(
onPanUpdate: (d) => setState(() => _pointer = d.localPosition),
child: AnimatedBuilder(
animation: _controller,
builder: (context, _) {
return CustomPaint(
size: size,
painter: ConstellationPainter(
starCount: 60,
connectionRadius: 120.0,
starColor: Colors.white,
lineColor: Colors.white24,
animationValue: _controller.value,
pointerPosition: _pointer,
size: size,
),
);
},
),
),
);
},
),
);
}
}
class _LightningDemo extends StatefulWidget {
const _LightningDemo();
@override
State<_LightningDemo> createState() => _LightningDemoState();
}
class _LightningDemoState extends State<_LightningDemo>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return _demoScaffold(
'Lightning',
LayoutBuilder(
builder: (context, constraints) {
final size = Size(constraints.maxWidth, constraints.maxHeight);
return AnimatedBuilder(
animation: _controller,
builder: (context, _) {
return CustomPaint(
size: size,
painter: LightningPainter(
start: Offset(size.width / 2, 40),
end: Offset(size.width / 2, size.height - 40),
animationValue: _controller.value,
color: const Color(0xFFE0E0FF),
branches: 4,
roughness: 0.6,
),
);
},
);
},
),
);
}
}
// ---------------------------------------------------------------------------
// Tab 3: Text Effects
// ---------------------------------------------------------------------------
class _TextTab extends StatelessWidget {
const _TextTab();
@override
Widget build(BuildContext context) {
final effects = <(String, GKTextEffectType)>[
('Typewriter Glow', GKTextEffectType.typewriterGlow),
('Neon Flicker', GKTextEffectType.neonFlicker),
('Glitch Reveal', GKTextEffectType.glitchReveal),
('Dissolve In', GKTextEffectType.dissolveIn),
];
return _demoScaffold(
'Text Effects',
ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: effects.length,
itemBuilder: (context, i) {
final (title, type) = effects[i];
return _EffectCard(
title: title,
onTap: () => Navigator.of(context).push(
GKPageRoute(
page: _TextDemo(title: title, type: type),
type: GKTransitionType.glitch,
),
),
);
},
),
);
}
}
class _TextDemo extends StatefulWidget {
const _TextDemo({required this.title, required this.type});
final String title;
final GKTextEffectType type;
@override
State<_TextDemo> createState() => _TextDemoState();
}
class _TextDemoState extends State<_TextDemo> {
int _key = 0;
@override
Widget build(BuildContext context) {
return _demoScaffold(
widget.title,
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
GKTextEffect(
key: ValueKey(_key),
text: 'Shader Effects',
effect: widget.type,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.cyanAccent,
),
duration: const Duration(milliseconds: 2500),
),
const SizedBox(height: 32),
TextButton.icon(
onPressed: () => setState(() => _key++),
icon: const Icon(Icons.replay),
label: const Text('Replay'),
),
],
),
),
);
}
}
// ---------------------------------------------------------------------------
// Tab 4: Transitions
// ---------------------------------------------------------------------------
class _TransitionsTab extends StatelessWidget {
const _TransitionsTab();
@override
Widget build(BuildContext context) {
final types = GKTransitionType.values;
return _demoScaffold(
'Page Transitions',
ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: types.length,
itemBuilder: (context, i) {
final type = types[i];
final name = type.name[0].toUpperCase() + type.name.substring(1);
return _EffectCard(
title: '$name Transition',
onTap: () => Navigator.of(context).push(
GKPageRoute(
page: _TransitionTarget(typeName: name),
type: type,
),
),
);
},
),
);
}
}
class _TransitionTarget extends StatelessWidget {
const _TransitionTarget({required this.typeName});
final String typeName;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('$typeName Transition')),
body: Center(
child: Text(
typeName.toUpperCase(),
style: const TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white54,
),
),
),
);
}
}
// ---------------------------------------------------------------------------
// Tab 5: Composer / Presets
// ---------------------------------------------------------------------------
class _ComposerTab extends StatelessWidget {
const _ComposerTab();
@override
Widget build(BuildContext context) {
final presets = <(String, String, GKPreset)>[
('Cyberpunk', 'Glitch + Chromatic + Neon Glow', GKPreset.cyberpunk),
('Dreamy', 'Aurora + Dissolve', GKPreset.dreamy),
('Retro', 'VHS + Pixelate', GKPreset.retro),
('Minimal', 'Subtle Neon Glow', GKPreset.minimal),
];
return _demoScaffold(
'Effect Composer',
ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
'Presets',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white54,
),
),
),
...presets.map((p) {
final (title, subtitle, preset) = p;
return _EffectCard(
title: title,
subtitle: subtitle,
onTap: () => Navigator.of(context).push(
GKPageRoute(
page: _PresetDemo(title: title, preset: preset),
type: GKTransitionType.warp,
),
),
);
}),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
'Custom Compose',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white54,
),
),
),
_EffectCard(
title: 'Liquid + Neon Glow (Screen)',
subtitle: 'Two effects blended with screen mode',
onTap: () => Navigator.of(context).push(
GKPageRoute(
page: _demoScaffold(
'Liquid + Neon Glow',
const SizedBox.expand(
child: GKWidget(
effect: GKEffectComposer(
effects: [
LiquidEffect(speed: 0.8, viscosity: 0.6),
NeonGlowEffect(
glowRadius: 0.04,
glowColor: Color(0xFFFF00FF),
),
],
blend: GKBlendMode.screen,
),
trigger: GKTrigger.auto,
),
),
),
type: GKTransitionType.ripple,
),
),
),
_EffectCard(
title: 'Aurora + Heat Wave (Overlay)',
subtitle: 'Nature effects blended with overlay mode',
onTap: () => Navigator.of(context).push(
GKPageRoute(
page: _demoScaffold(
'Aurora + Heat Wave',
const SizedBox.expand(
child: GKWidget(
effect: GKEffectComposer(
effects: [
AuroraEffect(speed: 0.6, colorShift: 0.1),
HeatWaveEffect(frequency: 6.0, amplitude: 0.012),
],
blend: GKBlendMode.overlay,
),
trigger: GKTrigger.auto,
),
),
),
type: GKTransitionType.ripple,
),
),
),
],
),
);
}
}
class _PresetDemo extends StatelessWidget {
const _PresetDemo({required this.title, required this.preset});
final String title;
final GKPreset preset;
@override
Widget build(BuildContext context) {
return _demoScaffold(
title,
SizedBox.expand(
child: GKWidget(
effect: GKAnimationPresets.preset(preset),
trigger: GKTrigger.auto,
),
),
);
}
}