wcag_vision 0.0.2 copy "wcag_vision: ^0.0.2" to clipboard
wcag_vision: ^0.0.2 copied to clipboard

Check color contrast, simulate color blindness, and extract dominant colors from images — accessibility tools for Flutter, no expertise required.

example/lib/main.dart

// wcag_vision example
//
// A single-screen demo of the package's three capabilities:
//
// 1. CONTRAST — `evaluateContrast` on two hardcoded colour pairs: dark
//    body text on white (a realistic pass) and light placeholder grey on
//    white (a realistic fail). Shows the computed ratio and AA/AAA
//    pass/fail badges for normal and large text.
//
// 2. CVD SIMULATION — a small fixed palette rendered as swatches, with a
//    dropdown that re-renders every swatch through `simulateCvd` for the
//    selected deficiency (none / protanopia / deuteranopia / tritanopia).
//
// 3. EXTRACTION — decodes the bundled `assets/sample.png` (an original,
//    programmatically generated 64x64 image of four flat colour bars —
//    see the comment on _extractFromSampleAsset) into pixels and runs
//    `extractDominantColorsAsync` on them, displaying the dominant
//    colours with their share percentages.
//
// The UI is intentionally plain Material — this is a functional demo of
// the package API for pub.dev's example tab, not a product.

import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:wcag_vision/wcag_vision.dart';

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

/// Root widget of the example app.
class WcagVisionExampleApp extends StatelessWidget {
  /// Creates the example app.
  const WcagVisionExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'wcag_vision example',
      home: DemoScreen(),
    );
  }
}

/// The single screen hosting the three demo sections.
class DemoScreen extends StatefulWidget {
  /// Creates the demo screen.
  const DemoScreen({super.key});

  @override
  State<DemoScreen> createState() => _DemoScreenState();
}

class _DemoScreenState extends State<DemoScreen> {
  // Local, presentation-only state: which deficiency the CVD section
  // renders. No business logic lives here, so plain setState is fine.
  CvdType _cvdType = CvdType.none;

  late final Future<List<ExtractedColor>> _extraction =
      _extractFromSampleAsset();

  /// Decodes the bundled sample image into pixels and extracts its
  /// dominant colours off the main isolate.
  ///
  /// `assets/sample.png` is an original asset generated programmatically
  /// for this example (a Python stdlib script in the repo's history — no
  /// external image source): a 64x64 PNG of four vertical flat-colour
  /// bars (navy / teal / amber / cream) with deliberately unequal widths,
  /// so the extracted shares are visibly different.
  Future<List<ExtractedColor>> _extractFromSampleAsset() async {
    final data = await rootBundle.load('assets/sample.png');
    final codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
    final frame = await codec.getNextFrame();
    final byteData =
        await frame.image.toByteData(); // defaults to raw RGBA 8888
    frame.image.dispose();
    final rgba = byteData!;
    final pixels = <Color>[
      for (var i = 0; i < rgba.lengthInBytes; i += 4)
        Color.fromARGB(
          rgba.getUint8(i + 3),
          rgba.getUint8(i),
          rgba.getUint8(i + 1),
          rgba.getUint8(i + 2),
        ),
    ];
    return extractDominantColorsAsync(pixels, k: 4);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('wcag_vision example')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          const _SectionTitle('1. Contrast (evaluateContrast)'),
          const _ContrastDemo(
            label: 'Dark text on white',
            foreground: Color(0xFF1F2937),
            background: Color(0xFFFFFFFF),
          ),
          const SizedBox(height: 8),
          const _ContrastDemo(
            label: 'Light grey placeholder on white',
            foreground: Color(0xFF9CA3AF),
            background: Color(0xFFFFFFFF),
          ),
          const Divider(height: 32),
          const _SectionTitle('2. CVD simulation (simulateCvd)'),
          DropdownButton<CvdType>(
            value: _cvdType,
            onChanged: (type) =>
                setState(() => _cvdType = type ?? CvdType.none),
            items: [
              for (final type in CvdType.values)
                DropdownMenuItem(value: type, child: Text(type.name)),
            ],
          ),
          const SizedBox(height: 8),
          Row(
            children: [
              for (final color in _palette) ...[
                _Swatch(color: simulateCvd(color, _cvdType)),
                const SizedBox(width: 8),
              ],
            ],
          ),
          const Divider(height: 32),
          const _SectionTitle(
            '3. Dominant colours (extractDominantColorsAsync)',
          ),
          Row(
            children: [
              Image.asset(
                'assets/sample.png',
                width: 64,
                height: 64,
                filterQuality: FilterQuality.none,
              ),
              const SizedBox(width: 16),
              const Text('sample.png (64x64)'),
            ],
          ),
          const SizedBox(height: 8),
          FutureBuilder<List<ExtractedColor>>(
            future: _extraction,
            builder: (context, snapshot) {
              if (snapshot.hasError) {
                return Text('Extraction failed: ${snapshot.error}');
              }
              if (!snapshot.hasData) {
                return const Center(child: CircularProgressIndicator());
              }
              return Row(
                children: [
                  for (final extracted in snapshot.data!) ...[
                    Column(
                      children: [
                        _Swatch(color: extracted.color),
                        Text(
                          '${(extracted.share * 100).toStringAsFixed(1)}%',
                        ),
                      ],
                    ),
                    const SizedBox(width: 8),
                  ],
                ],
              );
            },
          ),
        ],
      ),
    );
  }
}

/// A fixed demo palette for the CVD section: colours chosen so red–green
/// and blue–yellow deficiencies each produce a visible change.
const List<Color> _palette = [
  Color(0xFFE53935), // red
  Color(0xFFFB8C00), // orange
  Color(0xFF43A047), // green
  Color(0xFF1E88E5), // blue
  Color(0xFF8E24AA), // purple
];

class _SectionTitle extends StatelessWidget {
  const _SectionTitle(this.text);

  final String text;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 8),
      child: Text(text, style: Theme.of(context).textTheme.titleMedium),
    );
  }
}

/// One contrast check row: colour preview, computed ratio, and pass/fail
/// badges for the AA/AAA thresholds.
class _ContrastDemo extends StatelessWidget {
  const _ContrastDemo({
    required this.label,
    required this.foreground,
    required this.background,
  });

  final String label;
  final Color foreground;
  final Color background;

  @override
  Widget build(BuildContext context) {
    final report = evaluateContrast(foreground, background);
    return Row(
      children: [
        DecoratedBox(
          decoration: BoxDecoration(
            color: background,
            border: Border.all(color: Colors.black26),
          ),
          child: Padding(
            padding: const EdgeInsets.all(8),
            child: Text('Aa', style: TextStyle(color: foreground)),
          ),
        ),
        const SizedBox(width: 12),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text('$label — ${report.ratio.toStringAsFixed(2)}:1'),
              Wrap(
                spacing: 6,
                children: [
                  _PassBadge('AA', passes: report.passesAaNormal),
                  _PassBadge('AA large', passes: report.passesAaLarge),
                  _PassBadge('AAA', passes: report.passesAaaNormal),
                  _PassBadge('AAA large', passes: report.passesAaaLarge),
                ],
              ),
            ],
          ),
        ),
      ],
    );
  }
}

/// A small pass/fail chip, e.g. "AA ✓" in green or "AAA ✗" in red.
class _PassBadge extends StatelessWidget {
  const _PassBadge(this.label, {required this.passes});

  final String label;
  final bool passes;

  @override
  Widget build(BuildContext context) {
    return Chip(
      visualDensity: VisualDensity.compact,
      backgroundColor: passes ? Colors.green.shade100 : Colors.red.shade100,
      label: Text('$label ${passes ? '✓' : '✗'}'),
    );
  }
}

/// A fixed-size colour square.
class _Swatch extends StatelessWidget {
  const _Swatch({required this.color});

  final Color color;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: 48,
      height: 48,
      child: ColoredBox(color: color),
    );
  }
}
0
likes
160
points
0
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Check color contrast, simulate color blindness, and extract dominant colors from images — accessibility tools for Flutter, no expertise required.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, meta

More

Packages that depend on wcag_vision