Enqvida
Enqvida

Reputation: 155

How to detect luminance of a container with background blur to change the colour of the text

The luminance of the blurred container changes based on the image in the background. Is there a way to detect the luminance of the image or the blurred container to change the text color? Preferably without processing images.

enter image description here

Upvotes: 1

Views: 1196

Answers (1)

WSBT
WSBT

Reputation: 36333

I made a widget as a fun exercise and a proof-of-concept, called PixelDataOverlay. It has a background builder and an overlay builder.

When being used, it'll call background builder first to build the underlying widget (a picture or whatever). And then it will extract pixel data from what's built. Then it will call overlay builder and pass the pixel data back to you, so you can decide how to build the overlay based on the information you received.

Key Usage:

PixelDataOverlay(
  background: (BuildContext context) {
    return ImageFiltered( /* ... */ );
  },
  overlay: (BuildContext context, Uint8List? bytes) {
    final score = PixelDataOverlay.getBrightnessFromBytes(bytes);
    return Text('Background is ${score > 0 ? 'bright' : 'dark'}');
  },
)

Demo:

demo gif

Full source:

import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Demo(),
        ),
      ),
    );
  }
}

class Demo extends StatefulWidget {
  const Demo({Key? key}) : super(key: key);

  @override
  _DemoState createState() => _DemoState();
}

class _DemoState extends State<Demo> {
  final List<double> _sliderValues = [0.8, 0.8, 0.2, 1.0];

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        PixelDataOverlay(
          background: (BuildContext context) {
            return ClipRect(
              child: ImageFiltered(
                imageFilter: ImageFilter.blur(sigmaX: 20.0, sigmaY: 20.0),
                child: Container(
                  color: Color.fromARGB(
                    (_sliderValues[3] * 255).clamp(0, 255).round(),
                    (_sliderValues[0] * 255).clamp(0, 255).round(),
                    (_sliderValues[1] * 255).clamp(0, 255).round(),
                    (_sliderValues[2] * 255).clamp(0, 255).round(),
                  ),
                  child: FlutterLogo(size: 100),
                ),
              ),
            );
          },
          overlay: (BuildContext context, Uint8List bytes) {
            final score = PixelDataOverlay.getBrightnessFromBytes(bytes);
            return Center(
              child: Text(
                'Brightness: \n${score.toStringAsFixed(2)}',
                style: TextStyle(
                  color: score > 0 ? Colors.black : Colors.white,
                ),
              ),
            );
          },
        ),
        const SizedBox(height: 48),
        _buildControls(),
      ],
    );
  }

  _buildControls() {
    return Column(
      children: [
        Text('Adjust the sliders to see the effect of the blur filter.\n'
            'The sliders are: Red, Green, Blue, Alpha.'),
        for (int i = 0; i < 4; i++)
          Slider(
            value: _sliderValues[i],
            onChanged: (v) => setState(() => _sliderValues[i] = v),
          ),
      ],
    );
  }
}

class PixelDataOverlay extends StatefulWidget {
  final WidgetBuilder background;
  final Widget Function(BuildContext context, Uint8List bytes) overlay;

  const PixelDataOverlay(
      {Key? key, required this.background, required this.overlay})
      : super(key: key);

  @override
  _PixelDataOverlayState createState() => _PixelDataOverlayState();

  /// Returns the brightness score for the given [bytes] containing raw image
  /// data. A positive score indicates that the image is (on average) bright,
  /// while a negative score indicates that the image is dark.
  static double getBrightnessFromBytes(Uint8List bytes) {
    // Keep track of total brightness of the image.
    // For each pixel, assign positive value if it's bright.
    // For example: +1 for #FFFFFF, -1 for #000000.
    // So for neutral grey, its score will be close to 0.
    // However, if alpha is not FF, the score is discounted accordingly.
    // For example: `Colors.black.withOpacity(0.5)` has a score of `-0.5`.
    double totalScore = 0.0;

    for (int i = 0; i < bytes.length; i += 4) {
      final r = bytes[i];
      final g = bytes[i + 1];
      final b = bytes[i + 2];
      final a = bytes[i + 3];
      final brightness = (0.2126 * r + 0.7152 * g + 0.0722 * b); // 0 to 255
      final normalized = (brightness / 127.5 - 1) * (a / 255); // -1 to 1
      totalScore += normalized;
    }
    return totalScore;
  }
}

class _PixelDataOverlayState extends State<PixelDataOverlay> {
  final _globalKey = GlobalKey();
  Uint8List? _bytes;

  @override
  void initState() {
    super.initState();
  }

  @override
  void didUpdateWidget(covariant PixelDataOverlay oldWidget) {
    super.didUpdateWidget(oldWidget);
    WidgetsBinding.instance!.addPostFrameCallback((_) {
      _capture();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        RepaintBoundary(
          key: _globalKey,
          child: widget.background(context),
        ),
        if (_bytes != null)
          Positioned(
            top: 0,
            left: 0,
            bottom: 0,
            right: 0,
            child: widget.overlay(context, _bytes!),
          ),
      ],
    );
  }

  void _capture() async {
    final render = (_globalKey.currentContext!.findRenderObject()
        as RenderRepaintBoundary);
    final imageBytes = (await (await render.toImage())
            .toByteData(format: ImageByteFormat.rawStraightRgba))!
        .buffer
        .asUint8List();
    setState(() => _bytes = imageBytes);
  }
}

Upvotes: 4

Related Questions