flutter_dot_loader 0.0.1 copy "flutter_dot_loader: ^0.0.1" to clipboard
flutter_dot_loader: ^0.0.1 copied to clipboard

A highly customizable, high-performance dot-matrix and LED loading animation package for Flutter with 60 math patterns and custom frame support.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dot_loader/flutter_dot_loader.dart';

void main() => runApp(const DotMatrixGalleryApp());

class DotMatrixGalleryApp extends StatelessWidget {
  const DotMatrixGalleryApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Dot Matrix Studio',
      theme: ThemeData.dark(useMaterial3: true).copyWith(
        scaffoldBackgroundColor: const Color(0xFF050505),
        colorScheme: const ColorScheme.dark(
          primary: Color(0xFFFF3333),
          surface: Color(0xFF0A0A0C),
        ),
      ),
      home: const MainNavigation(),
    );
  }
}

// ─── Shared Components ───

// ─── Blocks Game Animation Logo ───

class _BlocksLogo extends StatelessWidget {
  final double dotSize;
  final double spacing;
  const _BlocksLogo({this.dotSize = 3.5, this.spacing = 1.8});

  static const List<List<List<int>>> _frames = [
    // 1. Empty
    [ [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0] ],
    // 2. L-block appears
    [ [1,0,0,0,0,0], [1,0,0,0,0,0], [1,1,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0] ],
    // 3. L-block falls
    [ [0,0,0,0,0,0], [1,0,0,0,0,0], [1,0,0,0,0,0], [1,1,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0] ],
    // 4. L-block falls
    [ [0,0,0,0,0,0], [0,0,0,0,0,0], [1,0,0,0,0,0], [1,0,0,0,0,0], [1,1,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0] ],
    // 5. L-block falls
    [ [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [1,0,0,0,0,0], [1,0,0,0,0,0], [1,1,0,0,0,0], [0,0,0,0,0,0] ],
    // 6. L-block hits bottom
    [ [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [1,0,0,0,0,0], [1,0,0,0,0,0], [1,1,0,0,0,0] ],
    
    // 7. Square appears
    [ [0,0,0,1,1,0], [0,0,0,1,1,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [1,0,0,0,0,0], [1,0,0,0,0,0], [1,1,0,0,0,0] ],
    // 8. Square falls
    [ [0,0,0,0,0,0], [0,0,0,1,1,0], [0,0,0,1,1,0], [0,0,0,0,0,0], [1,0,0,0,0,0], [1,0,0,0,0,0], [1,1,0,0,0,0] ],
    // 9. Square falls
    [ [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,1,1,0], [0,0,0,1,1,0], [1,0,0,0,0,0], [1,0,0,0,0,0], [1,1,0,0,0,0] ],
    // 10. Square hits bottom
    [ [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [1,0,0,1,1,0], [1,0,0,1,1,0], [1,1,0,0,0,0] ],
    
    // 11. Line appears
    [ [0,1,1,1,1,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [1,0,0,1,1,0], [1,0,0,1,1,0], [1,1,0,0,0,0] ],
    // 12. Line falls
    [ [0,0,0,0,0,0], [0,1,1,1,1,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [1,0,0,1,1,0], [1,0,0,1,1,0], [1,1,0,0,0,0] ],
    // 13. Line falls
    [ [0,0,0,0,0,0], [0,0,0,0,0,0], [0,1,1,1,1,0], [0,0,0,0,0,0], [1,0,0,1,1,0], [1,0,0,1,1,0], [1,1,0,0,0,0] ],
    // 14. Line hits bottom - line clear animation (flash)
    [ [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [0,1,1,1,1,0], [1,1,1,1,1,0], [1,0,0,1,1,0], [1,1,0,0,0,0] ],
    // 15. Flash empty
    [ [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [1,0,0,1,1,0], [1,1,0,0,0,0] ],
    // 16. Flash full
    [ [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [1,1,1,1,1,0], [1,0,0,1,1,0], [1,1,0,0,0,0] ],
    // 17. Cleared and dropped
    [ [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [1,0,0,1,1,0], [1,1,0,0,0,0] ],
    
    // 18. T-Block appears
    [ [0,0,1,1,1,0], [0,0,0,1,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [1,0,0,1,1,0], [1,1,0,0,0,0] ],
    // 19. T-Block falls
    [ [0,0,0,0,0,0], [0,0,1,1,1,0], [0,0,0,1,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [1,0,0,1,1,0], [1,1,0,0,0,0] ],
    // 20. T-Block falls
    [ [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,1,1,1,0], [0,0,0,1,0,0], [0,0,0,0,0,0], [1,0,0,1,1,0], [1,1,0,0,0,0] ],
    // 21. T-Block hits bottom
    [ [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,1,1,1,0], [0,0,0,1,0,0], [1,0,0,1,1,0], [1,1,0,0,0,0] ],
    
    // 22. Wait before loop
    [ [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0], [0,0,1,1,1,0], [0,0,0,1,0,0], [1,0,0,1,1,0], [1,1,0,0,0,0] ],
  ];

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      // Keep width static to match the layout
      width: 6 * (dotSize + spacing) - spacing,
      height: 7 * (dotSize + spacing) - spacing,
      child: MatrixLoader(
        columns: 6,
        rows: 7,
        dotSize: dotSize,
        spacing: spacing,
        pattern: MatrixPattern.custom,
        activeColor: const Color(0xFFFF3333),
        inactiveColor: const Color(0xFF1A1A1E),
        duration: const Duration(seconds: 4),
        customIntensity: (row, col, progress) {
          int idx = (progress * _frames.length).floor();
          if (idx >= _frames.length) idx = _frames.length - 1;
          return _frames[idx][row][col] == 1 ? 1.0 : 0.0;
        },
      ),
    );
  }
}

// ─── Main Navigation ───
class MainNavigation extends StatefulWidget {
  const MainNavigation({super.key});
  @override
  State<MainNavigation> createState() => _MainNavigationState();
}

class _MainNavigationState extends State<MainNavigation> {
  int _currentIndex = 0;

  final List<Widget> _pages = const [
    GalleryScreen(),
    PlaygroundScreen(),
    StudioScreen(),
  ];

  @override
  Widget build(BuildContext context) {
    final isMobile = MediaQuery.of(context).size.width < 600;

    return Scaffold(
      appBar: AppBar(
        backgroundColor: const Color(0xFF0A0A0C),
        elevation: 0,
        title: Row(
          children: [
            const _BlocksLogo(dotSize: 3, spacing: 1.5),
            const SizedBox(width: 16),
            if (!isMobile)
              const Text(
                'DOT MATRIX',
                style: TextStyle(
                  fontSize: 18,
                  fontWeight: FontWeight.w900,
                  letterSpacing: 4,
                  color: Colors.white,
                ),
              ),
          ],
        ),
        actions: [
          _NavBarTab(
            title: 'Gallery',
            isActive: _currentIndex == 0,
            onTap: () => setState(() => _currentIndex = 0),
          ),
          _NavBarTab(
            title: 'Playground',
            isActive: _currentIndex == 1,
            onTap: () => setState(() => _currentIndex = 1),
          ),
          _NavBarTab(
            title: 'Studio',
            isActive: _currentIndex == 2,
            onTap: () => setState(() => _currentIndex = 2),
          ),
          const SizedBox(width: 16),
        ],
      ),
      body: _pages[_currentIndex],
    );
  }
}

class _NavBarTab extends StatelessWidget {
  final String title;
  final bool isActive;
  final VoidCallback onTap;
  const _NavBarTab({required this.title, required this.isActive, required this.onTap});

  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: onTap,
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
        alignment: Alignment.center,
        child: Text(
          title,
          style: TextStyle(
            fontSize: 14,
            fontWeight: isActive ? FontWeight.w600 : FontWeight.w400,
            color: isActive ? Colors.white : Colors.white54,
          ),
        ),
      ),
    );
  }
}

// ─── Data Models ───
class _LoaderEntry {
  final String name;
  final MatrixShape shape;
  final MatrixPattern pattern;
  const _LoaderEntry(this.name, this.shape, this.pattern);
}
const _squareLoaders = [
  _LoaderEntry('Neon Drift', MatrixShape.square, MatrixPattern.square1),
  _LoaderEntry('Pulse Ladder', MatrixShape.square, MatrixPattern.square2),
  _LoaderEntry('Core Spiral', MatrixShape.square, MatrixPattern.square3),
  _LoaderEntry('Twin Orbit', MatrixShape.square, MatrixPattern.square4),
  _LoaderEntry('Prism Sweep', MatrixShape.square, MatrixPattern.square5),
  _LoaderEntry('Checker Shift', MatrixShape.square, MatrixPattern.square6),
  _LoaderEntry('Diamond Pulse', MatrixShape.square, MatrixPattern.square7),
  _LoaderEntry('Snake Trail', MatrixShape.square, MatrixPattern.square8),
  _LoaderEntry('Cross Weave', MatrixShape.square, MatrixPattern.square9),
  _LoaderEntry('Scan Line', MatrixShape.square, MatrixPattern.square10),
  _LoaderEntry('Vortex Spin', MatrixShape.square, MatrixPattern.square11),
  _LoaderEntry('Diagonal Fade', MatrixShape.square, MatrixPattern.square12),
  _LoaderEntry('Grid Bloom', MatrixShape.square, MatrixPattern.square13),
  _LoaderEntry('Spiral Arm', MatrixShape.square, MatrixPattern.square14),
  _LoaderEntry('Interference', MatrixShape.square, MatrixPattern.square15),
  _LoaderEntry('Corner Wave', MatrixShape.square, MatrixPattern.square16),
  _LoaderEntry('Sine Band', MatrixShape.square, MatrixPattern.square17),
  _LoaderEntry('Rail Scan', MatrixShape.square, MatrixPattern.square18),
  _LoaderEntry('Ripple Echo', MatrixShape.square, MatrixPattern.square19),
  _LoaderEntry('Star Burst', MatrixShape.square, MatrixPattern.square20),
];

const _circularLoaders = [
  _LoaderEntry('Halo Drift', MatrixShape.circular, MatrixPattern.circular1),
  _LoaderEntry('Pulse Ring', MatrixShape.circular, MatrixPattern.circular2),
  _LoaderEntry('Orbit Wave', MatrixShape.circular, MatrixPattern.circular3),
  _LoaderEntry('Ripple Out', MatrixShape.circular, MatrixPattern.circular4),
  _LoaderEntry('Galaxy Arm', MatrixShape.circular, MatrixPattern.circular5),
  _LoaderEntry('Tri Sweep', MatrixShape.circular, MatrixPattern.circular6),
  _LoaderEntry('Flower Spin', MatrixShape.circular, MatrixPattern.circular7),
  _LoaderEntry('Beacon Pulse', MatrixShape.circular, MatrixPattern.circular8),
  _LoaderEntry('Helix Curl', MatrixShape.circular, MatrixPattern.circular9),
  _LoaderEntry('Glyph Cycle', MatrixShape.circular, MatrixPattern.circular10),
  _LoaderEntry('Radial Mix', MatrixShape.circular, MatrixPattern.circular11),
  _LoaderEntry('Siren Wave', MatrixShape.circular, MatrixPattern.circular12),
  _LoaderEntry('Bloom Fade', MatrixShape.circular, MatrixPattern.circular13),
  _LoaderEntry('Shock Ring', MatrixShape.circular, MatrixPattern.circular14),
  _LoaderEntry('Petal Drift', MatrixShape.circular, MatrixPattern.circular15),
  _LoaderEntry('Orbit Cell', MatrixShape.circular, MatrixPattern.circular16),
  _LoaderEntry('Aurora Spin', MatrixShape.circular, MatrixPattern.circular17),
  _LoaderEntry('Sonar Ping', MatrixShape.circular, MatrixPattern.circular18),
  _LoaderEntry('Glyph Cluster', MatrixShape.circular, MatrixPattern.circular19),
  _LoaderEntry('Cosmic Halo', MatrixShape.circular, MatrixPattern.circular20),
];

const _triangleLoaders = [
  _LoaderEntry('Core Spokes', MatrixShape.triangle, MatrixPattern.triangle1),
  _LoaderEntry('Altitude Wave', MatrixShape.triangle, MatrixPattern.triangle2),
  _LoaderEntry('Corner Bounce', MatrixShape.triangle, MatrixPattern.triangle3),
  _LoaderEntry('Vertex Chase', MatrixShape.triangle, MatrixPattern.triangle4),
  _LoaderEntry('Twin Helix', MatrixShape.triangle, MatrixPattern.triangle5),
  _LoaderEntry('Rung Shift', MatrixShape.triangle, MatrixPattern.triangle6),
  _LoaderEntry('Tri Vortex', MatrixShape.triangle, MatrixPattern.triangle7),
  _LoaderEntry('Column Wave', MatrixShape.triangle, MatrixPattern.triangle8),
  _LoaderEntry('Apex Pulse', MatrixShape.triangle, MatrixPattern.triangle9),
  _LoaderEntry('Fan Sweep', MatrixShape.triangle, MatrixPattern.triangle10),
  _LoaderEntry('Cascade Fall', MatrixShape.triangle, MatrixPattern.triangle11),
  _LoaderEntry('Cross Hatch', MatrixShape.triangle, MatrixPattern.triangle12),
  _LoaderEntry('Prism Burst', MatrixShape.triangle, MatrixPattern.triangle13),
  _LoaderEntry('Blade Spin', MatrixShape.triangle, MatrixPattern.triangle14),
  _LoaderEntry('Ripple Edge', MatrixShape.triangle, MatrixPattern.triangle15),
  _LoaderEntry('Spiral Glow', MatrixShape.triangle, MatrixPattern.triangle16),
  _LoaderEntry('Ladder Shift', MatrixShape.triangle, MatrixPattern.triangle17),
  _LoaderEntry('Mesh Pulse', MatrixShape.triangle, MatrixPattern.triangle18),
  _LoaderEntry('Storm Spin', MatrixShape.triangle, MatrixPattern.triangle19),
  _LoaderEntry('Harmony', MatrixShape.triangle, MatrixPattern.triangle20),
];

// ─── 1. Gallery Screen ───
class GalleryScreen extends StatefulWidget {
  const GalleryScreen({super.key});
  @override
  State<GalleryScreen> createState() => _GalleryScreenState();
}
class _GalleryScreenState extends State<GalleryScreen> {
  int _selectedTab = 0;
  final _tabs = const ['All', 'Square', 'Circular', 'Triangle'];

  List<_LoaderEntry> get _currentLoaders {
    switch (_selectedTab) {
      case 1: return _squareLoaders;
      case 2: return _circularLoaders;
      case 3: return _triangleLoaders;
      default: return [..._squareLoaders, ..._circularLoaders, ..._triangleLoaders];
    }
  }

  @override
  Widget build(BuildContext context) {
    final screenWidth = MediaQuery.of(context).size.width;
    final isMobile = screenWidth < 600;
    int crossAxisCount = 4;
    if (isMobile) {
      crossAxisCount = 2;
    } else if (screenWidth < 900) {
      crossAxisCount = 3;
    }
    final hPad = isMobile ? 16.0 : 24.0;

    return CustomScrollView(
      slivers: [
        SliverToBoxAdapter(
          child: Container(
            padding: EdgeInsets.fromLTRB(hPad, isMobile ? 24 : 40, hPad, 0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                Text(
                  'DOT MATRIX',
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    fontSize: isMobile ? 20 : 28,
                    fontWeight: FontWeight.w900,
                    color: Colors.white,
                    letterSpacing: 6,
                    shadows: [Shadow(color: const Color(0xFFFF2222).withValues(alpha: 0.3), blurRadius: 20)],
                  ),
                ),
                const SizedBox(height: 6),
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: List.generate(
                    isMobile ? 15 : 25,
                    (i) => Container(
                      margin: const EdgeInsets.symmetric(horizontal: 3),
                      width: 3, height: 3,
                      decoration: BoxDecoration(
                        shape: BoxShape.circle,
                        color: Colors.white.withValues(alpha: 0.1 + (i % 3) * 0.15),
                      ),
                    ),
                  ),
                ),
                const SizedBox(height: 16),
                Text(
                  'Loaders for every app',
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    fontSize: isMobile ? 13 : 15,
                    color: Colors.white.withValues(alpha: 0.4),
                    letterSpacing: 2,
                  ),
                ),
                SizedBox(height: isMobile ? 20 : 32),
              ],
            ),
          ),
        ),
        SliverToBoxAdapter(
          child: Padding(
            padding: EdgeInsets.symmetric(horizontal: hPad),
            child: SizedBox(
              height: 36,
              child: ListView.separated(
                scrollDirection: Axis.horizontal,
                itemCount: _tabs.length,
                separatorBuilder: (_, __) => const SizedBox(width: 6),
                itemBuilder: (context, index) {
                  final isSelected = _selectedTab == index;
                  return GestureDetector(
                    onTap: () => setState(() => _selectedTab = index),
                    child: AnimatedContainer(
                      duration: const Duration(milliseconds: 200),
                      padding: EdgeInsets.symmetric(horizontal: isMobile ? 12 : 16, vertical: 8),
                      decoration: BoxDecoration(
                        color: isSelected ? const Color(0xFF1A1A1E) : Colors.transparent,
                        borderRadius: BorderRadius.circular(8),
                        border: Border.all(color: isSelected ? const Color(0xFF333333) : Colors.transparent),
                      ),
                      child: Text(
                        _tabs[index],
                        style: TextStyle(
                          fontSize: isMobile ? 12 : 13,
                          fontWeight: FontWeight.w500,
                          color: isSelected ? Colors.white : const Color(0xFF71717A),
                        ),
                      ),
                    ),
                  );
                },
              ),
            ),
          ),
        ),
        SliverToBoxAdapter(child: SizedBox(height: isMobile ? 16 : 24)),
        SliverPadding(
          padding: EdgeInsets.symmetric(horizontal: hPad),
          sliver: SliverGrid(
            gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: crossAxisCount,
              mainAxisSpacing: isMobile ? 8 : 12,
              crossAxisSpacing: isMobile ? 8 : 12,
              childAspectRatio: isMobile ? 0.9 : 0.85,
            ),
            delegate: SliverChildBuilderDelegate(
              (context, index) => _LoaderCard(entry: _currentLoaders[index], isMobile: isMobile),
              childCount: _currentLoaders.length,
            ),
          ),
        ),
        const SliverToBoxAdapter(child: SizedBox(height: 48)),
      ],
    );
  }
}

class _LoaderCard extends StatelessWidget {
  final _LoaderEntry entry;
  final bool isMobile;
  const _LoaderCard({required this.entry, this.isMobile = false});

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: const Color(0xFF0A0A0C),
        borderRadius: BorderRadius.circular(isMobile ? 12 : 16),
        border: Border.all(color: const Color(0xFF1A1A1E), width: 1),
      ),
      child: Column(
        children: [
          Expanded(
            child: Center(
              child: MatrixLoader(
                columns: 5, rows: 5,
                dotSize: isMobile ? 3.5 : 4,
                spacing: isMobile ? 2.5 : 3,
                size: isMobile ? 36 : 44,
                shape: entry.shape,
                pattern: entry.pattern,
                activeColor: Colors.white,
              ),
            ),
          ),
          Padding(
            padding: EdgeInsets.only(bottom: isMobile ? 10 : 16),
            child: Text(
              entry.name,
              textAlign: TextAlign.center,
              style: TextStyle(
                fontFamily: 'monospace',
                fontSize: isMobile ? 10 : 11,
                color: const Color(0xFF71717A),
                letterSpacing: 0.2,
              ),
            ),
          ),
        ],
      ),
    );
  }
}


// ─── 2. Playground Screen ───
class PlaygroundScreen extends StatefulWidget {
  const PlaygroundScreen({super.key});
  @override
  State<PlaygroundScreen> createState() => _PlaygroundScreenState();
}

class _PlaygroundScreenState extends State<PlaygroundScreen> {
  int _cols = 7;
  int _rows = 7;
  double _dotSize = 6;
  double _spacing = 2;
  double _speed = 1.5;
  MatrixShape _shape = MatrixShape.square;
  MatrixPattern _pattern = MatrixPattern.square1;

  final List<MatrixShape> _shapes = [MatrixShape.square, MatrixShape.circular, MatrixShape.triangle];

  List<MatrixPattern> get _availablePatterns {
    if (_shape == MatrixShape.square) return _squareLoaders.map((e) => e.pattern).toList();
    if (_shape == MatrixShape.circular) return _circularLoaders.map((e) => e.pattern).toList();
    return _triangleLoaders.map((e) => e.pattern).toList();
  }

  void _onShapeChanged(MatrixShape? val) {
    if (val == null) return;
    setState(() {
      _shape = val;
      _pattern = _availablePatterns.first;
    });
  }

  String _generateCode() {
    return '''
MatrixLoader(
  columns: $_cols,
  rows: $_rows,
  dotSize: ${_dotSize.toStringAsFixed(1)},
  spacing: ${_spacing.toStringAsFixed(1)},
  duration: const Duration(milliseconds: ${(1000 * _speed).toInt()}),
  shape: MatrixShape.${_shape.name},
  pattern: MatrixPattern.${_pattern.name},
  activeColor: Colors.white,
)''';
  }

  @override
  Widget build(BuildContext context) {
    final isMobile = MediaQuery.of(context).size.width < 800;
    
    Widget controls = SingleChildScrollView(
      padding: const EdgeInsets.all(24),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text('CONTROLS', style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1.5)),
          const SizedBox(height: 24),
          _buildDropdown<MatrixShape>('Shape', _shapes, _shape, _onShapeChanged, (s) => s.name.toUpperCase()),
          const SizedBox(height: 16),
          _buildDropdown<MatrixPattern>('Pattern', _availablePatterns, _pattern, (v) => setState(() => _pattern = v!), (p) => p.name),
          const SizedBox(height: 24),
          _buildSlider('Columns', _cols.toDouble(), 1, 20, (v) => setState(() => _cols = v.toInt())),
          _buildSlider('Rows', _rows.toDouble(), 1, 20, (v) => setState(() => _rows = v.toInt())),
          _buildSlider('Dot Size', _dotSize, 2, 20, (v) => setState(() => _dotSize = v)),
          _buildSlider('Spacing', _spacing, 0, 10, (v) => setState(() => _spacing = v)),
          _buildSlider('Duration (s)', _speed, 0.5, 5, (v) => setState(() => _speed = v)),
        ],
      ),
    );

    Widget preview = Container(
      color: const Color(0xFF050505),
      child: Column(
        children: [
          Expanded(
            child: Center(
              child: MatrixLoader(
                columns: _cols,
                rows: _rows,
                dotSize: _dotSize,
                spacing: _spacing,
                duration: Duration(milliseconds: (1000 * _speed).toInt()),
                shape: _shape,
                pattern: _pattern,
                activeColor: Colors.white,
              ),
            ),
          ),
          Container(
            padding: const EdgeInsets.all(16),
            width: double.infinity,
            decoration: const BoxDecoration(
              border: Border(top: BorderSide(color: Color(0xFF1A1A1E))),
              color: Color(0xFF0A0A0C),
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    const Text('Code Snippet', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white70)),
                    TextButton.icon(
                      icon: const Icon(Icons.copy, size: 14, color: Colors.white),
                      label: const Text('Copy', style: TextStyle(color: Colors.white)),
                      onPressed: () {
                        Clipboard.setData(ClipboardData(text: _generateCode()));
                        ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Copied to clipboard!')));
                      },
                    )
                  ],
                ),
                const SizedBox(height: 8),
                SelectableText(
                  _generateCode(),
                  style: const TextStyle(fontFamily: 'monospace', fontSize: 12, color: Color(0xFFA1A1AA)),
                ),
              ],
            ),
          )
        ],
      ),
    );

    if (isMobile) {
      return Column(
        children: [
          Expanded(flex: 3, child: preview),
          const Divider(height: 1, color: Color(0xFF1A1A1E)),
          Expanded(flex: 4, child: controls),
        ],
      );
    }
    return Row(
      children: [
        Expanded(flex: 2, child: preview),
        const VerticalDivider(width: 1, color: Color(0xFF1A1A1E)),
        SizedBox(width: 320, child: controls),
      ],
    );
  }

  Widget _buildDropdown<T>(String label, List<T> items, T value, ValueChanged<T?> onChanged, String Function(T) labeler) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(label, style: const TextStyle(fontSize: 12, color: Colors.white54)),
        const SizedBox(height: 8),
        Container(
          padding: const EdgeInsets.symmetric(horizontal: 12),
          decoration: BoxDecoration(
            color: const Color(0xFF1A1A1E),
            borderRadius: BorderRadius.circular(8),
            border: Border.all(color: const Color(0xFF27272A)),
          ),
          child: DropdownButtonHideUnderline(
            child: DropdownButton<T>(
              isExpanded: true,
              value: value,
              dropdownColor: const Color(0xFF1A1A1E),
              items: items.map((e) => DropdownMenuItem(value: e, child: Text(labeler(e), style: const TextStyle(fontSize: 14)))).toList(),
              onChanged: onChanged,
            ),
          ),
        ),
      ],
    );
  }

  Widget _buildSlider(String label, double value, double min, double max, ValueChanged<double> onChanged) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Text(label, style: const TextStyle(fontSize: 12, color: Colors.white54)),
              Text(value.toStringAsFixed(1), style: const TextStyle(fontSize: 12, fontFamily: 'monospace')),
            ],
          ),
          SliderTheme(
            data: SliderTheme.of(context).copyWith(
              activeTrackColor: const Color(0xFFFF3333),
              inactiveTrackColor: const Color(0xFF27272A),
              thumbColor: Colors.white,
              overlayColor: const Color(0xFFFF3333).withValues(alpha: 0.2),
              trackHeight: 4,
            ),
            child: Slider(value: value, min: min, max: max, onChanged: onChanged),
          ),
        ],
      ),
    );
  }
}

// ─── 3. Studio Screen ───
class StudioScreen extends StatefulWidget {
  const StudioScreen({super.key});
  @override
  State<StudioScreen> createState() => _StudioScreenState();
}

class _StudioScreenState extends State<StudioScreen> {
  final int cols = 7;
  final int rows = 8;
  late List<List<List<int>>> frames;
  int currentFrame = 0;

  @override
  void initState() {
    super.initState();
    // Start with one empty frame
    frames = [List.generate(rows, (_) => List.filled(cols, 0))];
  }

  void _toggleDot(int r, int c) {
    setState(() {
      frames[currentFrame][r][c] = frames[currentFrame][r][c] == 1 ? 0 : 1;
    });
  }

  void _addFrame() {
    setState(() {
      // Copy current frame
      final newFrame = List.generate(rows, (r) => List<int>.from(frames[currentFrame][r]));
      frames.add(newFrame);
      currentFrame = frames.length - 1;
    });
  }

  void _deleteFrame() {
    if (frames.length <= 1) return;
    setState(() {
      frames.removeAt(currentFrame);
      if (currentFrame >= frames.length) currentFrame = frames.length - 1;
    });
  }

  void _clearFrame() {
    setState(() {
      frames[currentFrame] = List.generate(rows, (_) => List.filled(cols, 0));
    });
  }
  
  void _invertFrame() {
    setState(() {
      for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
          frames[currentFrame][r][c] = frames[currentFrame][r][c] == 1 ? 0 : 1;
        }
      }
    });
  }

  String _generateCode() {
    final frameStrs = frames.map((f) {
      final rowsStrs = f.map((r) => '[${r.join(', ')}]').join(',\n      ');
      return '    [\n      $rowsStrs\n    ]';
    }).join(',\n');
    
    return '''
// 1. Define your custom frames
final List<List<List<int>>> customFrames = [
$frameStrs
];

// 2. Use MatrixLoader with customIntensity
MatrixLoader(
  columns: $cols,
  rows: $rows,
  pattern: MatrixPattern.custom,
  customIntensity: (row, col, progress) {
    int frameIndex = (progress * customFrames.length).floor();
    if (frameIndex >= customFrames.length) frameIndex = customFrames.length - 1;
    return customFrames[frameIndex][row][col] == 1 ? 1.0 : 0.0;
  },
)''';
  }

  @override
  Widget build(BuildContext context) {
    final isMobile = MediaQuery.of(context).size.width < 800;

    Widget editor = Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Container(
          padding: const EdgeInsets.all(24),
          decoration: BoxDecoration(
            color: const Color(0xFF0A0A0C),
            borderRadius: BorderRadius.circular(16),
            border: Border.all(color: const Color(0xFF1A1A1E)),
          ),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: List.generate(rows, (r) {
              return Row(
                mainAxisSize: MainAxisSize.min,
                children: List.generate(cols, (c) {
                  final isActive = frames[currentFrame][r][c] == 1;
                  return GestureDetector(
                    onTap: () => _toggleDot(r, c),
                    child: AnimatedContainer(
                      duration: const Duration(milliseconds: 150),
                      margin: const EdgeInsets.all(4),
                      width: 24, height: 24,
                      decoration: BoxDecoration(
                        shape: BoxShape.circle,
                        color: isActive ? const Color(0xFFFF3333) : const Color(0xFF1A1A1E),
                        border: Border.all(color: isActive ? const Color(0xFFFF6666) : const Color(0xFF27272A)),
                        boxShadow: isActive ? [BoxShadow(color: const Color(0xFFFF3333).withValues(alpha: 0.4), blurRadius: 8)] : [],
                      ),
                    ),
                  );
                }),
              );
            }),
          ),
        ),
        const SizedBox(height: 32),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            OutlinedButton.icon(
              onPressed: _clearFrame,
              icon: const Icon(Icons.clear, size: 16),
              label: const Text('Clear'),
            ),
            const SizedBox(width: 12),
            OutlinedButton.icon(
              onPressed: _invertFrame,
              icon: const Icon(Icons.invert_colors, size: 16),
              label: const Text('Invert'),
            ),
          ],
        ),
        const SizedBox(height: 32),
        const Text('TIMELINE', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, letterSpacing: 1.5, color: Colors.white54)),
        const SizedBox(height: 16),
        SingleChildScrollView(
          scrollDirection: Axis.horizontal,
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              for (int i = 0; i < frames.length; i++)
                GestureDetector(
                  onTap: () => setState(() => currentFrame = i),
                  child: Container(
                    margin: const EdgeInsets.symmetric(horizontal: 4),
                    padding: const EdgeInsets.all(4),
                    decoration: BoxDecoration(
                      border: Border.all(color: currentFrame == i ? const Color(0xFFFF3333) : const Color(0xFF27272A), width: 2),
                      borderRadius: BorderRadius.circular(8),
                      color: const Color(0xFF0A0A0C),
                    ),
                    width: 48, height: 48,
                    child: CustomPaint(painter: _MiniFramePainter(frames[i])),
                  ),
                ),
              const SizedBox(width: 8),
              IconButton(
                onPressed: _addFrame,
                icon: const Icon(Icons.add_box),
                color: Colors.white54,
                iconSize: 32,
              ),
              if (frames.length > 1)
                IconButton(
                  onPressed: _deleteFrame,
                  icon: const Icon(Icons.delete),
                  color: Colors.redAccent,
                  iconSize: 32,
                ),
            ],
          ),
        ),
      ],
    );

    Widget rightPanel = Column(
      children: [
        Expanded(
          child: Container(
            color: const Color(0xFF0A0A0C),
            child: Center(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  const Text('LIVE PREVIEW', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, letterSpacing: 1.5, color: Colors.white54)),
                  const SizedBox(height: 24),
                  MatrixLoader(
                    columns: cols, rows: rows, dotSize: 6, spacing: 3,
                    pattern: MatrixPattern.custom,
                    activeColor: const Color(0xFFFF3333),
                    customIntensity: (row, col, progress) {
                      int idx = (progress * frames.length).floor();
                      if (idx >= frames.length) idx = frames.length - 1;
                      return frames[idx][row][col] == 1 ? 1.0 : 0.0;
                    },
                  ),
                ],
              ),
            ),
          ),
        ),
        Container(
          padding: const EdgeInsets.all(16),
          width: double.infinity,
          decoration: const BoxDecoration(
            border: Border(top: BorderSide(color: Color(0xFF1A1A1E))),
            color: Color(0xFF050505),
          ),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  const Text('Generated Code', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white70)),
                  TextButton.icon(
                    icon: const Icon(Icons.copy, size: 14, color: Colors.white),
                    label: const Text('Copy', style: TextStyle(color: Colors.white)),
                    onPressed: () {
                      Clipboard.setData(ClipboardData(text: _generateCode()));
                      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Copied to clipboard!')));
                    },
                  )
                ],
              ),
              const SizedBox(height: 8),
              SizedBox(
                height: 200,
                child: SingleChildScrollView(
                  child: SelectableText(
                    _generateCode(),
                    style: const TextStyle(fontFamily: 'monospace', fontSize: 12, color: Color(0xFFA1A1AA)),
                  ),
                ),
              ),
            ],
          ),
        )
      ],
    );

    if (isMobile) {
      return DefaultTabController(
        length: 2,
        child: Column(
          children: [
            const TabBar(
              tabs: [Tab(text: 'Editor'), Tab(text: 'Preview')],
              indicatorColor: Color(0xFFFF3333),
              labelColor: Colors.white,
            ),
            Expanded(
              child: TabBarView(
                children: [
                  SingleChildScrollView(padding: const EdgeInsets.all(24), child: editor),
                  rightPanel,
                ],
              ),
            ),
          ],
        ),
      );
    }

    return Row(
      children: [
        Expanded(flex: 3, child: editor),
        const VerticalDivider(width: 1, color: Color(0xFF1A1A1E)),
        Expanded(flex: 2, child: rightPanel),
      ],
    );
  }
}

class _MiniFramePainter extends CustomPainter {
  final List<List<int>> frame;
  _MiniFramePainter(this.frame);
  @override
  void paint(Canvas canvas, Size size) {
    final rows = frame.length;
    final cols = frame[0].length;
    final dotW = size.width / cols;
    final dotH = size.height / rows;
    final paint = Paint()..style = PaintingStyle.fill;
    for (int r = 0; r < rows; r++) {
      for (int c = 0; c < cols; c++) {
        if (frame[r][c] == 1) {
          paint.color = const Color(0xFFFF3333);
        } else {
          paint.color = const Color(0xFF1A1A1E);
        }
        canvas.drawRect(Rect.fromLTWH(c * dotW + 0.5, r * dotH + 0.5, dotW - 1, dotH - 1), paint);
      }
    }
  }
  @override bool shouldRepaint(covariant _MiniFramePainter old) => true;
}
19
likes
0
points
332
downloads

Publisher

verified publisherdilacode.com

Weekly Downloads

A highly customizable, high-performance dot-matrix and LED loading animation package for Flutter with 60 math patterns and custom frame support.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter

More

Packages that depend on flutter_dot_loader