Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,13 @@ jobs:
with:
filters: |
code:
- "lib/**"
- "test/**"
- "example/**"
- "lib/spoiler_*.dart"
- "lib/widgets/**"
- "lib/models/**"
- "lib/utils/**"
- "lib/extension/**"
- "shaders/**"
- "assets/**"
- "pubspec.yaml"
- "analysis_options.yaml"
- "analysis_options.yml"

- name: Setup Flutter
uses: subosito/flutter-action@v2
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 1.0.25
* **Performance:**
* Added `ParticleConfig.updateInterval` to throttle particle updates.
* **DevX:**
* Added `CONTRIBUTING.md` and release check scripts.
* **Docs:**
* Expanded configuration model documentation.

## 1.0.24

* **Packaging:**
Expand Down
20 changes: 20 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Contributing

Thanks for helping improve `spoiler_widget`!

## Setup

1. Install Flutter (or use FVM).
2. Run `scripts/setup_hooks.sh` to enable git hooks.
3. Run `flutter pub get`.

## Development workflow

- Format: `dart format .`
- Analyze: `flutter analyze` and `flutter analyze` inside `example/`
- Tests: `flutter test` and `flutter test test/golden_test.dart`

## Release checks

Use `scripts/release_check.sh` to run format, analyze, tests, goldens, and
`dart pub publish --dry-run` in one go.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ In your `pubspec.yaml`:

```yaml
dependencies:
spoiler_widget: ^1.0.24
spoiler_widget: ^1.0.25
```

Then run:
Expand Down Expand Up @@ -249,6 +249,7 @@ These fields are kept for backward compatibility; prefer `particleConfig` and `f
| `enableWaves` | bool | Enables ripple waves that push particles. |
| `maxWaveRadius` | double | Wave radius limit in pixels. |
| `maxWaveCount` | int | Maximum number of simultaneous waves. |
| `updateInterval` | double | Minimum seconds between particle updates (0 = every frame). |

#### TextSpoilerConfig

Expand Down Expand Up @@ -345,6 +346,7 @@ SpoilerTextWrapper(
### Contributing

Contributions are welcome! Whether it’s bug fixes, new features, or documentation improvements, open a [Pull Request](https://github.com/zhukeev/spoiler_widget/pulls) or [Issue](https://github.com/zhukeev/spoiler_widget/issues).
See [CONTRIBUTING.md](https://github.com/zhukeev/spoiler_widget/blob/main/CONTRIBUTING.md) for setup and release checks.

---

Expand Down
2 changes: 2 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'pages/spoiler_overlay_page.dart';
import 'pages/spoiler_performance_page.dart';
import 'pages/spoiler_text_field_page.dart';
import 'pages/spoiler_text_page.dart';
import 'pages/spoiler_text_wrapper_page.dart';
Expand Down Expand Up @@ -37,6 +38,7 @@ class _DemoListPage extends StatelessWidget {
_DemoEntry('SpoilerOverlay', () => const SpoilerOverlayPage()),
_DemoEntry('SpoilerOverlay Full',
() => const SpoilerOverlayPage(fullPage: true)),
_DemoEntry('Performance', () => const SpoilerPerformancePage()),
];

return Scaffold(
Expand Down
133 changes: 133 additions & 0 deletions example/lib/pages/spoiler_performance_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import 'package:flutter/material.dart';
import 'package:spoiler_widget/spoiler_widget.dart';

class SpoilerPerformancePage extends StatefulWidget {
const SpoilerPerformancePage({super.key});

@override
State<SpoilerPerformancePage> createState() => _SpoilerPerformancePageState();
}

class _SpoilerPerformancePageState extends State<SpoilerPerformancePage> {
double _density = 0.1;
double _updateInterval = 0.0;
double _maxParticleSize = 1.0;

Widget _buildSlider({
required String label,
required double value,
required double min,
required double max,
required int divisions,
required ValueChanged<double> onChanged,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$label: ${value.toStringAsFixed(2)}',
style: const TextStyle(color: Colors.white),
),
Slider(
value: value,
min: min,
max: max,
divisions: divisions,
label: value.toStringAsFixed(2),
onChanged: onChanged,
),
],
);
}

@override
Widget build(BuildContext context) {
final config = TextSpoilerConfig(
isEnabled: true,
enableGestureReveal: true,
fadeConfig: const FadeConfig(padding: 10.0, edgeThickness: 20.0),
particleConfig: ParticleConfig(
density: _density,
speed: 0.25,
color: Colors.white,
maxParticleSize: _maxParticleSize,
updateInterval: _updateInterval,
),
);

return Scaffold(
appBar: AppBar(title: const Text('Performance')),
backgroundColor: Colors.black,
body: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
_buildSlider(
label: 'Density',
value: _density,
min: 0.0,
max: 1.0,
divisions: 20,
onChanged: (value) {
setState(() {
_density = value;
});
},
),
_buildSlider(
label: 'Update interval (s)',
value: _updateInterval,
min: 0.0,
max: 0.5,
divisions: 10,
onChanged: (value) {
setState(() {
_updateInterval = value;
});
},
),
_buildSlider(
label: 'Max particle size',
value: _maxParticleSize,
min: 1.0,
max: 6.0,
divisions: 10,
onChanged: (value) {
setState(() {
_maxParticleSize = value;
});
},
),
],
),
),
Expanded(
child: Center(
child: SpoilerTextWrapper(
config: config,
child: const Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Performance tuning',
style: TextStyle(fontSize: 28, color: Colors.white),
),
SizedBox(height: 12),
Text(
'Adjust density, update interval, and particle size.',
style: TextStyle(fontSize: 16, color: Colors.white70),
),
],
),
),
),
),
],
),
),
);
}
}
2 changes: 1 addition & 1 deletion example/macos/Flutter/GeneratedPluginRegistrant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import FlutterMacOS
import Foundation

import path_provider_foundation
import sqflite
import sqflite_darwin

func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
Expand Down
2 changes: 1 addition & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ packages:
path: ".."
relative: true
source: path
version: "1.0.18"
version: "1.0.25"
sqflite:
dependency: transitive
description:
Expand Down
16 changes: 11 additions & 5 deletions lib/models/particle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ class Particle extends Offset {
///
/// This method is used to move the particle.
/// It calculates the next position of the particle based on the current position, speed, and angle.
Particle moveToRandomAngle() {
return moveWithAngle(angle).copyWith(
///
/// [timeScale] is a multiplier where 1.0 represents a single 60fps frame.
Particle moveToRandomAngle([double timeScale = 1.0]) {
return moveWithAngle(angle, timeScale).copyWith(
// Random angle
angle: angle + (Random().nextDouble() - 0.5),
);
Expand All @@ -72,10 +74,14 @@ class Particle extends Offset {
/// Move the particle
///
/// This method is used to move the particle to given angle.
Particle moveWithAngle(double angle) {
final next = this + Offset.fromDirection(angle, speed);
///
/// [timeScale] is a multiplier where 1.0 represents a single 60fps frame.
Particle moveWithAngle(double angle, [double timeScale = 1.0]) {
final clampedScale = timeScale <= 0 ? 0.0 : timeScale;
final next = this + Offset.fromDirection(angle, speed * clampedScale);

final lifetime = life - 0.01;
// Lifetime decreases by 0.01 per 60fps frame, resulting in a ~1.67s lifespan.
final lifetime = life - (0.01 * clampedScale);

final color = this.color.withValues(alpha: lifetime.clamp(0, 1));

Expand Down
25 changes: 24 additions & 1 deletion lib/models/particle_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,36 @@ double _estimatePathAreaFactor(Path path, {int samples = 48}) {
return areaFactor;
}

/// Configuration for particle appearance, motion, and update cadence.
@immutable
class ParticleConfig {
/// Fraction of area covered by particles (0..1 => 0%..100%).
final double density;

/// Particle speed in logical pixels per frame.
final double speed;

/// Base color used for particle rendering.
final Color color;

/// Particle diameter in logical pixels.
final double maxParticleSize;

/// Optional preset shape used by both atlas and shader rendering.
final ParticlePathPreset? shapePreset;

/// Enables ripple waves that push particles.
final bool enableWaves;

/// Maximum radius for a single wave animation.
final double maxWaveRadius;

/// Maximum number of simultaneous wave animations.
final int maxWaveCount;

/// Minimum seconds between particle updates. Use 0 for every frame.
final double updateInterval;

const ParticleConfig({
required this.density,
required this.speed,
Expand All @@ -61,6 +79,7 @@ class ParticleConfig {
this.enableWaves = false,
this.maxWaveRadius = 0.0,
this.maxWaveCount = 3,
this.updateInterval = 0.0,
});

factory ParticleConfig.defaultConfig() => ParticleConfig(
Expand All @@ -69,8 +88,10 @@ class ParticleConfig {
color: Colors.white,
maxParticleSize: 1.0,
shapePreset: ParticlePathPreset.circle,
updateInterval: 0.0,
);

/// Area normalization factor derived from [shapePreset].
double get areaFactor {
if (shapePreset == null) return 1.0;
final presetArea = shapePreset!.areaFactor;
Expand All @@ -87,6 +108,7 @@ class ParticleConfig {
enableWaves,
maxWaveRadius,
maxWaveCount,
updateInterval,
);

@override
Expand All @@ -100,5 +122,6 @@ class ParticleConfig {
shapePreset == other.shapePreset &&
enableWaves == other.enableWaves &&
maxWaveRadius == other.maxWaveRadius &&
maxWaveCount == other.maxWaveCount;
maxWaveCount == other.maxWaveCount &&
updateInterval == other.updateInterval;
}
Loading