Skip to main content

ambient_border_glow 0.1.0 copy "ambient_border_glow: ^0.1.0" to clipboard
ambient_border_glow: ^0.1.0 copied to clipboard

An animated glow that hugs the edges of a box - the whole screen, a widget you point it at, or any shape you can express as an SVG path.

example/lib/main.dart

import 'package:ambient_border_glow/ambient_border_glow.dart';
import 'package:flutter/material.dart';

import 'shapes.dart';

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

const _bg = Color(0xFF0B0B0F);
const _card = Color(0xFF16161D);
const _text = Color(0xFFF2F2F7);
const _dim = Color(0xFF8E8E9A);
const _accent = Color(0xFF5E5CE6);

class DemoApp extends StatelessWidget {
  const DemoApp({super.key});

  @override
  Widget build(BuildContext context) => MaterialApp(
    title: 'Ambient Border Glow',
    debugShowCheckedModeBanner: false,
    theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: _bg),
    home: const DemoPage(),
  );
}

/// Rounded shapes, tracked by key. The glow reads each one's corners from the
/// same radius it is drawn with, so the two cannot drift.
const _rectShapes = <String, (Size, GlowRadius)>{
  'Pill': (Size(104, 52), GlowRadius.all(999)),
  'Circle': (Size(72, 72), GlowRadius.all(999)),
  'Square': (Size(72, 72), GlowRadius.all(0)),
  'Card': (Size(104, 72), GlowRadius.all(16)),
  'Tall pill': (Size(48, 104), GlowRadius.all(999)),
  'Bubble': (
    Size(104, 72),
    GlowRadius.only(topLeft: 22, topRight: 22, bottomRight: 22, bottomLeft: 4),
  ),
  'Leaf': (Size(84, 84), GlowRadius.only(topLeft: 34, bottomRight: 34)),
  'Wedge': (
    Size(104, 72),
    GlowRadius.only(topLeft: 4, topRight: 36, bottomRight: 4, bottomLeft: 36),
  ),
};

class DemoPage extends StatefulWidget {
  const DemoPage({super.key});
  @override
  State<DemoPage> createState() => _DemoPageState();
}

class _DemoPageState extends State<DemoPage> {
  bool _pinned = true;
  Set<GlowEdge> _edges = {...allEdges};
  GlowDirection _direction = GlowDirection.cw;
  double _thickness = 7;
  double _smoothing = 0.6;
  String _palette = 'rainbow';

  String? _rect;
  String? _shape;
  final _rectKeys = {for (final k in _rectShapes.keys) k: GlobalKey()};
  final _shapeKeys = {for (final k in customShapes.keys) k: GlobalKey()};
  final _rectHost = GlobalKey();
  final _shapeHost = GlobalKey();

  static const _palettes = <String, List<Color>>{
    'rainbow': GlowPalettes.rainbow,
    'aurora': GlowPalettes.aurora,
    'ember': GlowPalettes.ember,
    'ocean': GlowPalettes.ocean,
    'mono': GlowPalettes.mono,
  };

  @override
  Widget build(BuildContext context) {
    final visible = _pinned;
    return Scaffold(
      body: Stack(
        children: [
          ListView(
            padding: const EdgeInsets.fromLTRB(20, 60, 20, 60),
            children: [
              const Text(
                'Ambient Border Glow',
                style: TextStyle(
                  color: _text,
                  fontSize: 28,
                  fontWeight: FontWeight.w800,
                ),
              ),
              const SizedBox(height: 6),
              const Text(
                'The full-screen glow reacts to every control below. Corners '
                'round only where two selected edges meet.',
                style: TextStyle(color: _dim, fontSize: 14, height: 1.4),
              ),
              const SizedBox(height: 20),
              _section('Full-screen glow', [
                Row(
                  children: [
                    _button(
                      _pinned ? 'Turn off' : 'Turn on',
                      _pinned,
                      () => setState(() => _pinned = !_pinned),
                    ),
                  ],
                ),
              ]),
              _section('Edges', [
                Wrap(
                  spacing: 8,
                  runSpacing: 8,
                  children: [
                    for (final e in allEdges)
                      _chip(e.name, _edges.contains(e), () {
                        setState(() {
                          _edges = {..._edges};
                          _edges.contains(e) ? _edges.remove(e) : _edges.add(e);
                        });
                      }),
                  ],
                ),
                const SizedBox(height: 10),
                Text(
                  '${_edges.length} of 4 selected.',
                  style: const TextStyle(color: _dim, fontSize: 12),
                ),
              ]),
              _section('Motion', [
                Wrap(
                  spacing: 8,
                  children: [
                    for (final d in GlowDirection.values)
                      _chip(
                        d.name,
                        _direction == d,
                        () => setState(() => _direction = d),
                      ),
                  ],
                ),
                const SizedBox(height: 12),
                _slider(
                  'thickness',
                  _thickness,
                  1,
                  20,
                  (v) => setState(() => _thickness = v),
                ),
                _slider(
                  'cornerSmoothing',
                  _smoothing,
                  0,
                  1,
                  (v) => setState(() => _smoothing = v),
                ),
              ]),
              _section('Palette', [
                Wrap(
                  spacing: 8,
                  runSpacing: 8,
                  children: [
                    for (final name in _palettes.keys)
                      _chip(
                        name,
                        _palette == name,
                        () => setState(() => _palette = name),
                      ),
                  ],
                ),
              ]),
              _sectionKeyed(
                'Rounded shapes - tracked by key',
                _rectHost,
                overlay: _rect == null
                    ? null
                    : AmbientBorderGlow(
                        visible: visible,
                        targetKey: _rectKeys[_rect],
                        relativeTo: _rectHost,
                        radius: _rectShapes[_rect]!.$2,
                        thickness: 4,
                        bleed: 16,
                        direction: _direction,
                        colors: _palettes[_palette]!,
                      ),
                [
                  const Text(
                    'Tap a shape. The glow measures that widget and wraps its '
                    'box, with the same radius it is drawn with.',
                    style: TextStyle(color: _dim, fontSize: 12, height: 1.5),
                  ),
                  const SizedBox(height: 16),
                  Wrap(
                    spacing: 18,
                    runSpacing: 18,
                    children: [
                      for (final entry in _rectShapes.entries)
                        _swatch(
                          label: entry.key,
                          selected: _rect == entry.key,
                          onTap: () => setState(
                            () => _rect = _rect == entry.key ? null : entry.key,
                          ),
                          child: Container(
                            key: _rectKeys[entry.key],
                            width: entry.value.$1.width,
                            height: entry.value.$1.height,
                            decoration: BoxDecoration(
                              color: _rect == entry.key
                                  ? _accent
                                  : const Color(0xFF242430),
                              borderRadius: _borderRadius(entry.value.$2),
                            ),
                          ),
                        ),
                    ],
                  ),
                ],
              ),
              _sectionKeyed(
                'Custom outlines - any shape',
                _shapeHost,
                overlay: _shape == null
                    ? null
                    : AmbientBorderGlow(
                        visible: visible,
                        targetKey: _shapeKeys[_shape],
                        relativeTo: _shapeHost,
                        path: customShapes[_shape],
                        thickness: 4,
                        bleed: 20,
                        direction: _direction,
                        colors: _palettes[_palette]!,
                      ),
                [
                  const Text(
                    'Hand the glow a path and it traces any outline you can '
                    'express as SVG. The swatch and the glow come from the same '
                    'function, so they agree by construction.',
                    style: TextStyle(color: _dim, fontSize: 12, height: 1.5),
                  ),
                  const SizedBox(height: 16),
                  Wrap(
                    spacing: 18,
                    runSpacing: 18,
                    children: [
                      for (final entry in customShapes.entries)
                        _swatch(
                          label: entry.key,
                          selected: _shape == entry.key,
                          onTap: () => setState(
                            () =>
                                _shape = _shape == entry.key ? null : entry.key,
                          ),
                          child: SizedBox(
                            key: _shapeKeys[entry.key],
                            width: 76,
                            height: 76,
                            child: CustomPaint(
                              painter: _ShapeSwatch(
                                entry.value,
                                _shape == entry.key
                                    ? _accent
                                    : const Color(0xFF242430),
                              ),
                            ),
                          ),
                        ),
                    ],
                  ),
                ],
              ),
            ],
          ),

          // Last in the stack, no safe-area padding: it paints over everything
          // and runs right up to the physical screen edges.
          if (_rect == null && _shape == null)
            AmbientBorderGlow(
              visible: visible,
              edges: _edges,
              radius: const GlowRadius.display(fallback: 44),
              cornerSmoothing: _smoothing,
              thickness: _thickness,
              direction: _direction,
              colors: _palettes[_palette]!,
            ),
        ],
      ),
    );
  }

  BorderRadius _borderRadius(GlowRadius r) => BorderRadius.only(
    topLeft: Radius.circular(r.topLeft),
    topRight: Radius.circular(r.topRight),
    bottomRight: Radius.circular(r.bottomRight),
    bottomLeft: Radius.circular(r.bottomLeft),
  );

  Widget _section(String title, List<Widget> children) =>
      _sectionKeyed(title, null, children);

  Widget _sectionKeyed(
    String title,
    Key? key,
    List<Widget> children, {
    Widget? overlay,
  }) => Container(
    margin: const EdgeInsets.only(bottom: 14),
    padding: const EdgeInsets.all(14),
    decoration: BoxDecoration(
      color: _card,
      borderRadius: BorderRadius.circular(16),
      border: Border.all(color: const Color(0xFF2A2A35)),
    ),
    // The key sits on the Stack, not the Container: the glow measures
    // against this box and positions inside it, so both have to be the
    // same box or the two coordinate spaces differ by the padding.
    child: Stack(
      key: key,
      children: [
        Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              title.toUpperCase(),
              style: const TextStyle(
                color: _dim,
                fontSize: 11,
                fontWeight: FontWeight.w700,
                letterSpacing: 1.2,
              ),
            ),
            const SizedBox(height: 10),
            ...children,
          ],
        ),
        ?overlay,
      ],
    ),
  );

  Widget _chip(String label, bool selected, VoidCallback onTap) =>
      GestureDetector(
        onTap: onTap,
        child: Container(
          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
          decoration: BoxDecoration(
            color: selected ? _accent : null,
            borderRadius: BorderRadius.circular(999),
            border: Border.all(
              color: selected ? _accent : const Color(0xFF2A2A35),
            ),
          ),
          child: Text(
            label,
            style: TextStyle(
              color: selected ? Colors.white : _dim,
              fontSize: 13,
              fontWeight: FontWeight.w600,
            ),
          ),
        ),
      );

  Widget _button(String label, bool on, VoidCallback onTap) => Expanded(
    child: GestureDetector(
      onTap: onTap,
      child: Container(
        padding: const EdgeInsets.symmetric(vertical: 11),
        alignment: Alignment.center,
        decoration: BoxDecoration(
          color: on ? null : _accent,
          borderRadius: BorderRadius.circular(12),
          border: Border.all(color: const Color(0xFF2A2A35)),
        ),
        child: Text(
          label,
          style: const TextStyle(
            color: _text,
            fontSize: 14,
            fontWeight: FontWeight.w600,
          ),
        ),
      ),
    ),
  );

  Widget _slider(
    String label,
    double value,
    double min,
    double max,
    ValueChanged<double> onChanged,
  ) => Row(
    children: [
      SizedBox(
        width: 140,
        child: Text(label, style: const TextStyle(color: _text, fontSize: 13)),
      ),
      Expanded(
        child: Slider(
          value: value,
          min: min,
          max: max,
          activeColor: _accent,
          onChanged: onChanged,
        ),
      ),
      SizedBox(
        width: 44,
        child: Text(
          value.toStringAsFixed(2),
          style: const TextStyle(color: _dim, fontSize: 12),
        ),
      ),
    ],
  );

  Widget _swatch({
    required String label,
    required bool selected,
    required VoidCallback onTap,
    required Widget child,
  }) => GestureDetector(
    onTap: onTap,
    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        child,
        const SizedBox(height: 6),
        Text(
          label,
          style: TextStyle(
            color: selected ? _text : _dim,
            fontSize: 11,
            fontWeight: FontWeight.w600,
          ),
        ),
      ],
    ),
  );
}

/// Fills a custom outline from the exact path the glow is handed, so what you
/// see filled is what gets traced.
class _ShapeSwatch extends CustomPainter {
  _ShapeSwatch(this.builder, this.color);
  final GlowPathBuilder builder;
  final Color color;

  @override
  void paint(Canvas canvas, Size size) {
    final box = GlowPathBox(x: 0, y: 0, width: size.width, height: size.height);
    canvas.drawPath(parseSvgPath(builder(box)), Paint()..color = color);
  }

  @override
  bool shouldRepaint(_ShapeSwatch old) =>
      old.color != color || old.builder != builder;
}
1
likes
160
points
92
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

An animated glow that hugs the edges of a box - the whole screen, a widget you point it at, or any shape you can express as an SVG path.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter

More

Packages that depend on ambient_border_glow