particle_text 0.0.1
particle_text: ^0.0.1 copied to clipboard
Interactive particle text effect for Flutter. Particles form text shapes and scatter on touch/hover, with spring-based physics and full customization.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:particle_text/particle_text.dart';
void main() => runApp(const ExampleApp());
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'particle_text Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData.dark(useMaterial3: true),
home: const DemoScreen(),
);
}
}
class DemoScreen extends StatefulWidget {
const DemoScreen({super.key});
@override
State<DemoScreen> createState() => _DemoScreenState();
}
class _DemoScreenState extends State<DemoScreen> {
String _text = 'Flutter';
bool _isEditing = false;
late TextEditingController _controller;
int _presetIndex = 0;
final List<_Preset> _presets = [
_Preset('Default', const ParticleConfig()),
_Preset('Cosmic', ParticleConfig.cosmic()),
_Preset('Fire', ParticleConfig.fire()),
_Preset('Matrix', ParticleConfig.matrix()),
_Preset('Pastel', ParticleConfig.pastel()),
_Preset('Minimal', ParticleConfig.minimal()),
];
@override
void initState() {
super.initState();
_controller = TextEditingController(text: _text);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final preset = _presets[_presetIndex];
return Scaffold(
backgroundColor: preset.config.backgroundColor,
body: Stack(
children: [
// Particle effect (full screen)
ParticleText(
text: _text,
config: preset.config,
),
// Top: hint text
Positioned(
top: MediaQuery.of(context).padding.top + 12,
left: 0,
right: 0,
child: Center(
child: Text(
'TOUCH & DRAG TO INTERACT',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.15),
fontSize: 11,
letterSpacing: 3,
),
),
),
),
// Bottom: controls
Positioned(
bottom: MediaQuery.of(context).padding.bottom + 16,
left: 16,
right: 16,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Preset selector
SizedBox(
height: 36,
child: ListView.separated(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
itemCount: _presets.length,
separatorBuilder: (_, __) => const SizedBox(width: 8),
itemBuilder: (context, index) {
final isSelected = index == _presetIndex;
return GestureDetector(
onTap: () => setState(() => _presetIndex = index),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 8,
),
decoration: BoxDecoration(
color: isSelected
? Colors.white.withValues(alpha: 0.12)
: Colors.white.withValues(alpha: 0.04),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected
? Colors.white.withValues(alpha: 0.25)
: Colors.white.withValues(alpha: 0.08),
),
),
child: Text(
_presets[index].name,
style: TextStyle(
color: isSelected
? Colors.white.withValues(alpha: 0.8)
: Colors.white.withValues(alpha: 0.35),
fontSize: 12,
letterSpacing: 0.5,
),
),
),
);
},
),
),
const SizedBox(height: 12),
// Text input
_isEditing
? Container(
width: 220,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.06),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.white.withValues(alpha: 0.15),
),
),
child: TextField(
controller: _controller,
autofocus: true,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.8),
fontSize: 14,
),
decoration: const InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
),
onSubmitted: (val) {
if (val.trim().isNotEmpty) {
setState(() {
_text = val.trim();
_isEditing = false;
});
}
},
),
)
: GestureDetector(
onTap: () => setState(() {
_isEditing = true;
_controller.text = _text;
}),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 10,
),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.04),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.white.withValues(alpha: 0.08),
),
),
child: Text(
'change text',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.35),
fontSize: 13,
letterSpacing: 1,
),
),
),
),
],
),
),
],
),
);
}
}
class _Preset {
final String name;
final ParticleConfig config;
const _Preset(this.name, this.config);
}