candle_reader

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) plusFlamePalette.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.