candle_reader

candle_reader demo

An interactive candle reading-light widget for Flutter.

Wrap any widget with CandleLight to get a warm, spring-following animated flame that dims everything around it. Pinch to resize the pool of light, pinch hard to blow the flame out (with rising smoke wisps), tap to relight.

Works over any child: plain text, PageView, ListView, Image, PdfView, or your own custom reader.

Features

  • Layered teardrop flame with organic flicker and sway
  • Spring-damped follow physics (not a lazy lerp)
  • Pinch-to-resize the light pool; pinch below threshold extinguishes
  • Extinguish animation: flame curls into an ember, rising smoke wisps with sine-curl drift and air drag
  • Tap to relight
  • Five built-in FlamePalettes (warm, blue, green, red, violet) plus FlamePalette.fromColor(...) for any hue you want
  • Three interaction modes so the widget composes cleanly with scrollable children
  • Programmatic control via CandleLightController
  • Reduced-motion aware (respects MediaQuery.disableAnimations)
  • Screen-reader friendly
  • Zero dependencies beyond Flutter itself

Install

dependencies:
  candle_reader: ^0.1.0

Quick start

import 'package:candle_reader/candle_reader.dart';

CandleLight(
  child: Text('Once upon a time...'),
)

Over a scrollable child

The default (twoFinger) mode is tuned for readers: one finger scrolls the list, two fingers move and resize the candle, long-press-then-drag lets a single finger pick the candle up, and tap relights it — the iOS pinch-while-scrolling model.

CandleLight(
  child: ListView(
    children: [/* your pages */],
  ),
)

If your content doesn't need to scroll and you want single-finger drag to move the flame, opt into grab:

CandleLight(
  interaction: CandleLightInteraction.grab,
  child: Text('Once upon a time...'),
)

Flame color

Pick a preset:

CandleLight(
  palette: FlamePalette.blue,   // or .warm (default), .green, .red, .violet
  child: myReader,
)

Or derive a palette from any single color — outer/mid/inner/core are generated by darkening and lightening it:

CandleLight(
  palette: FlamePalette.fromColor(Colors.pinkAccent),
  child: myReader,
)

Or build one by hand for full control:

CandleLight(
  palette: const FlamePalette(
    outer: Color(0xFF1E5FD6),
    mid:   Color(0xFF3E88FF),
    inner: Color(0xFFA8D8FF),
    core:  Color(0xFFE8F6FF),
    ember: Color(0xFF1E5FD6),
  ),
  child: myReader,
)

The pool's warm glow follows the palette automatically. If you want a different glow color than the flame body, set warmTint explicitly.

Programmatic control

final controller = CandleLightController();

CandleLight(
  controller: controller,
  interaction: CandleLightInteraction.controlled,
  child: myReader,
);

// Later:
controller.position = Offset(200, 400);  // flame animates there
controller.radius = 220;
controller.blowOut();
controller.relight();

Reacting to events

CandleLight(
  onExtinguished: () => analytics.log('candle_out'),
  onRelit: () => analytics.log('candle_lit'),
  onPositionChanged: (p) => bookmarkService.setCursor(p),
  onRadiusChanged: (r) => prefs.setLastRadius(r),
  child: myBook,
)

Parameters

Parameter Type Default Notes
child Widget required The content the candle illuminates
interaction CandleLightInteraction twoFinger grab / twoFinger / controlled
controller CandleLightController? null Drive or observe the candle externally
initialRadius double 160 Light pool radius at mount, in px
minRadius double 8 Pinch floor while still lit
maxRadius double 560 Pinch ceiling
extinguishRadius double 26 Pinch below this → blow out
relightRadius double 70 Pinch above this (while out) → relight
initialPosition Offset? null Defaults to center of the widget
dimColor Color #050505 Painted outside the pool
warmTint Color? null Additive glow inside the pool. When null, follows palette.mid.
palette FlamePalette FlamePalette.warm Per-layer flame colors. See "Flame color".
enableHaptics bool true System haptics on blow-out / relight
onExtinguished VoidCallback? null Fires on blow-out
onRelit VoidCallback? null Fires on relight
onPositionChanged ValueChanged<Offset>? null Fires on user drag
onRadiusChanged ValueChanged<double>? null Fires on user pinch
semanticLabel String 'Candle reading light' Announced by screen readers

Interaction modes

CandleLightInteraction.twoFinger (default)

One finger → passes through to the child (scroll normally). Two fingers → move candle + pinch pool. Long-press-then-drag → pick the candle up with a single finger. Tap → relight. The right fit when your child is a ListView / PageView / PDF viewer that must keep its own drag handling — which is most reader content.

Heads up: to pinch, place both fingers down together. Flutter's gesture arena hands the first finger to the scroll view the moment it passes touch-slop, so adding a second finger after you've started scrolling won't be recognised as a pinch. Lift, then touch with two fingers.

CandleLightInteraction.grab

All gestures drive the candle; child gets nothing. Single-finger drag moves the flame, pinch resizes, pinch hard blows it out, tap relights. Use for single-page readers or any content that doesn't need to scroll.

CandleLightInteraction.controlled

Widget consumes no gestures. Drive everything via CandleLightController. Use for custom input (keyboard, trackpad, eye-tracking, TTS sync, etc.).

Example app

A full paged-book reader is in example/. Run:

cd example
flutter run

License

MIT. See LICENSE.

Libraries

candle_reader
Candle Reader — interactive candle-flame reading overlay widget.