Update README and code to set default sparkle shape to 'none' and add opacity control
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -13,8 +13,9 @@ It exposes two widget layers:
|
||||
- Optional motion input via sensors or custom tilt stream
|
||||
- Generic wrapper API for any widget via `Shiny(child: ...)`
|
||||
- Card API with `background`, `foreground`, `shape`, and drag tilt via `ShinyCard`
|
||||
- Built-in sparkle presets including 8-point star, 5-point star, rectangle, diamond, hexagon, random polygon, and confetti
|
||||
- Built-in sparkle presets including `none` (default), 8-point star, 5-point star, rectangle, diamond, hexagon, random polygon, and confetti
|
||||
- Custom sparkle shapes via parameterized `SparkleShapeSpec` factories
|
||||
- Global shader opacity control via `opacity`
|
||||
- Cross-platform Flutter support (mobile, web, desktop)
|
||||
|
||||
## Installation
|
||||
@@ -60,7 +61,8 @@ ShinyCard(
|
||||
controller: controller,
|
||||
background: Container(color: const Color(0xFF1B2D4B)),
|
||||
foreground: const Center(child: Text('HOLO')),
|
||||
sparkleShape: SparkleShapeSpec.eightPointStar,
|
||||
sparkleShape: SparkleShapeSpec.none,
|
||||
opacity: 1.0,
|
||||
)
|
||||
```
|
||||
|
||||
@@ -80,6 +82,10 @@ Shiny(
|
||||
Use built-in sparkle presets:
|
||||
|
||||
```dart
|
||||
ShinyCard(
|
||||
sparkleShape: SparkleShapeSpec.none,
|
||||
)
|
||||
|
||||
ShinyCard(
|
||||
sparkleShape: SparkleShapeSpec.hexagon,
|
||||
)
|
||||
@@ -120,6 +126,11 @@ final ShinyController controller = ShinyController(tiltStream: input.stream);
|
||||
- `ShinyController`: optional source selection for tilt
|
||||
- `SensorTiltController`: low-level sensor fusion stream utility
|
||||
|
||||
Important defaults:
|
||||
|
||||
- `sparkleShape` defaults to `SparkleShapeSpec.none` (no sparkles)
|
||||
- `opacity` defaults to `1.0`
|
||||
|
||||
## Platform Notes
|
||||
|
||||
- Shader uses Flutter runtime effects and avoids derivative functions (`dFdx`, `dFdy`, `fwidth`) for web compatibility.
|
||||
|
||||
+91
-35
@@ -3,10 +3,12 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:holo_shiny/holo_shiny.dart';
|
||||
|
||||
/// Entry point for the package demo application.
|
||||
void main() {
|
||||
runApp(const ExampleApp());
|
||||
}
|
||||
|
||||
/// Root app widget for the interactive shader demo.
|
||||
class ExampleApp extends StatelessWidget {
|
||||
const ExampleApp({super.key});
|
||||
|
||||
@@ -15,6 +17,7 @@ class ExampleApp extends StatelessWidget {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF24A6A8)),
|
||||
),
|
||||
home: const ExampleHome(),
|
||||
@@ -22,6 +25,7 @@ class ExampleApp extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Interactive screen used to test style and uniform controls.
|
||||
class ExampleHome extends StatefulWidget {
|
||||
const ExampleHome({super.key});
|
||||
|
||||
@@ -30,19 +34,26 @@ class ExampleHome extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ExampleHomeState extends State<ExampleHome> {
|
||||
/// Sensor-driven controller used by the first two demo surfaces.
|
||||
late final ShinyController _sensorController;
|
||||
|
||||
/// Manual stream used by tilt preset buttons.
|
||||
late final StreamController<Offset> _externalTiltController;
|
||||
|
||||
/// Controller backed by [_externalTiltController].
|
||||
late final ShinyController _overrideController;
|
||||
|
||||
double _prismatic = 0.8;
|
||||
double _sparkle = 0.8;
|
||||
double _specular = 0.8;
|
||||
double _diffraction = 0.8;
|
||||
double _opacity = 1.0;
|
||||
HolographStyle _style = HolographStyle.crackedIce;
|
||||
SparkleShapeSpec _sparkleShape = SparkleShapeSpec.eightPointStar;
|
||||
SparkleShapeSpec _sparkleShape = SparkleShapeSpec.none;
|
||||
|
||||
static const Map<String, SparkleShapeSpec> _sparkleChoices =
|
||||
<String, SparkleShapeSpec>{
|
||||
'None': SparkleShapeSpec.none,
|
||||
'8-Point Star': SparkleShapeSpec.eightPointStar,
|
||||
'5-Point Star': SparkleShapeSpec.fivePointStar,
|
||||
'Rectangle': SparkleShapeSpec.rectangle,
|
||||
@@ -90,6 +101,11 @@ class _ExampleHomeState extends State<ExampleHome> {
|
||||
child: Shiny(
|
||||
controller: _sensorController,
|
||||
style: _style,
|
||||
prismatic: _prismatic,
|
||||
sparkle: _sparkle,
|
||||
specular: _specular,
|
||||
diffraction: _diffraction,
|
||||
opacity: _opacity,
|
||||
sparkleShape: _sparkleShape,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -127,6 +143,7 @@ class _ExampleHomeState extends State<ExampleHome> {
|
||||
sparkle: _sparkle,
|
||||
specular: _specular,
|
||||
diffraction: _diffraction,
|
||||
opacity: _opacity,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -169,27 +186,18 @@ class _ExampleHomeState extends State<ExampleHome> {
|
||||
_diffraction = value;
|
||||
});
|
||||
}),
|
||||
_LabeledSlider('Opacity', _opacity, (double value) {
|
||||
setState(() {
|
||||
_opacity = value;
|
||||
});
|
||||
}),
|
||||
const SizedBox(height: 24),
|
||||
const Text('External Tilt Stream + Custom Shape (card)'),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () =>
|
||||
_externalTiltController.add(const Offset(0.7, 0.0)),
|
||||
child: const Text('Tilt Right'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () =>
|
||||
_externalTiltController.add(const Offset(-0.7, 0.0)),
|
||||
child: const Text('Tilt Left'),
|
||||
),
|
||||
),
|
||||
],
|
||||
_TiltPresetPicker(
|
||||
onChanged: (Offset tilt) {
|
||||
_externalTiltController.add(tilt);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Center(
|
||||
@@ -197,6 +205,11 @@ class _ExampleHomeState extends State<ExampleHome> {
|
||||
controller: _overrideController,
|
||||
style: _style,
|
||||
sparkleShape: _sparkleShape,
|
||||
prismatic: _prismatic,
|
||||
sparkle: _sparkle,
|
||||
specular: _specular,
|
||||
diffraction: _diffraction,
|
||||
opacity: _opacity,
|
||||
shape: const StadiumBorder(),
|
||||
background: Container(
|
||||
decoration: const BoxDecoration(
|
||||
@@ -225,6 +238,7 @@ class _ExampleHomeState extends State<ExampleHome> {
|
||||
}
|
||||
|
||||
class _LabeledSlider extends StatelessWidget {
|
||||
/// Creates a slider row with a live numeric label.
|
||||
const _LabeledSlider(this.label, this.value, this.onChanged);
|
||||
|
||||
final String label;
|
||||
@@ -236,7 +250,7 @@ class _LabeledSlider extends StatelessWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(label),
|
||||
Text('$label: ${value.toStringAsFixed(2)}'),
|
||||
Slider(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
@@ -247,6 +261,7 @@ class _LabeledSlider extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _StylePicker extends StatelessWidget {
|
||||
/// Creates the style selection control.
|
||||
const _StylePicker({
|
||||
required this.selectedStyle,
|
||||
required this.onChanged,
|
||||
@@ -262,21 +277,21 @@ class _StylePicker extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
const Text('Style'),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: HolographStyle.values.map((HolographStyle style) {
|
||||
return ChoiceChip(
|
||||
label: Text(_styleLabel(style)),
|
||||
selected: selectedStyle == style,
|
||||
onSelected: (bool selected) {
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
onChanged(style);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
SegmentedButton<HolographStyle>(
|
||||
segments: HolographStyle.values
|
||||
.map((HolographStyle style) => ButtonSegment<HolographStyle>(
|
||||
value: style,
|
||||
label: Text(_styleLabel(style)),
|
||||
))
|
||||
.toList(),
|
||||
selected: <HolographStyle>{selectedStyle},
|
||||
onSelectionChanged: (Set<HolographStyle> value) {
|
||||
if (value.isNotEmpty) {
|
||||
onChanged(value.first);
|
||||
}
|
||||
},
|
||||
showSelectedIcon: false,
|
||||
multiSelectionEnabled: false,
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -297,6 +312,7 @@ class _StylePicker extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _SparkleShapePicker extends StatelessWidget {
|
||||
/// Creates sparkle shape chips from the provided shape map.
|
||||
const _SparkleShapePicker({
|
||||
required this.selectedShape,
|
||||
required this.choices,
|
||||
@@ -336,6 +352,45 @@ class _SparkleShapePicker extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Preset controls for feeding an external tilt stream.
|
||||
class _TiltPresetPicker extends StatelessWidget {
|
||||
const _TiltPresetPicker({required this.onChanged});
|
||||
|
||||
final ValueChanged<Offset> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: FilledButton.tonalIcon(
|
||||
onPressed: () => onChanged(const Offset(-0.7, 0.0)),
|
||||
icon: const Icon(Icons.keyboard_double_arrow_left),
|
||||
label: const Text('Tilt Left'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => onChanged(Offset.zero),
|
||||
icon: const Icon(Icons.restart_alt),
|
||||
label: const Text('Center'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: FilledButton.tonalIcon(
|
||||
onPressed: () => onChanged(const Offset(0.7, 0.0)),
|
||||
icon: const Icon(Icons.keyboard_double_arrow_right),
|
||||
label: const Text('Tilt Right'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Demonstration card background for the primary example.
|
||||
class _DemoCardBackground extends StatelessWidget {
|
||||
const _DemoCardBackground();
|
||||
|
||||
@@ -390,6 +445,7 @@ class _DemoCardBackground extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Small badge overlay placed on top of the shiny card.
|
||||
class _RareBadge extends StatelessWidget {
|
||||
const _RareBadge();
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/// Public API exports for the holo_shiny package.
|
||||
library;
|
||||
|
||||
export 'src/sensor_tilt_controller.dart';
|
||||
export 'src/shiny_controller.dart';
|
||||
export 'src/shiny_widget.dart';
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Local smoke-test entrypoint used when running the package directly.
|
||||
void main() {
|
||||
runApp(const MainApp());
|
||||
}
|
||||
|
||||
/// Minimal app shell for package-level manual checks.
|
||||
class MainApp extends StatelessWidget {
|
||||
const MainApp({super.key});
|
||||
|
||||
|
||||
+119
-3
@@ -6,22 +6,45 @@ import 'package:flutter/scheduler.dart';
|
||||
|
||||
import 'shiny_controller.dart';
|
||||
|
||||
/// Available holographic material profiles rendered by the fragment shader.
|
||||
enum HolographStyle {
|
||||
/// Smooth silver foil with soft rainbow flow.
|
||||
holographicSilver,
|
||||
|
||||
/// High-contrast crystalline foil with shard-like reflections.
|
||||
crackedIce,
|
||||
|
||||
/// Tiled foil with radial diffraction in each tile.
|
||||
silverMosaic,
|
||||
|
||||
/// Dense dot-grid gold foil with warm highlights.
|
||||
superGoldVinyl,
|
||||
}
|
||||
|
||||
/// Sparkle silhouette families used by the shader when drawing glints.
|
||||
enum SparkleShapeKind {
|
||||
/// Disables sparkle rendering entirely.
|
||||
none,
|
||||
|
||||
/// Star-shaped sparkle with configurable point count and inner radius.
|
||||
star,
|
||||
|
||||
/// Rectangular sparkle streak.
|
||||
rectangle,
|
||||
|
||||
/// Fixed-side polygon sparkle.
|
||||
polygon,
|
||||
|
||||
/// Randomized polygon sparkle per sparkle cell.
|
||||
randomPolygon,
|
||||
|
||||
/// Confetti-like elongated rectangle with random rotation.
|
||||
confetti,
|
||||
}
|
||||
|
||||
/// Declarative sparkle shape configuration for [Shiny] and [ShinyCard].
|
||||
class SparkleShapeSpec {
|
||||
/// Internal constructor that maps directly to shader uniform values.
|
||||
const SparkleShapeSpec._({
|
||||
required this.kind,
|
||||
required this.primary,
|
||||
@@ -29,6 +52,15 @@ class SparkleShapeSpec {
|
||||
required this.tertiary,
|
||||
});
|
||||
|
||||
/// No sparkle output.
|
||||
static const SparkleShapeSpec none = SparkleShapeSpec._(
|
||||
kind: SparkleShapeKind.none,
|
||||
primary: 0.0,
|
||||
secondary: 0.0,
|
||||
tertiary: 0.0,
|
||||
);
|
||||
|
||||
/// Eight-point star sparkle preset.
|
||||
static const SparkleShapeSpec eightPointStar = SparkleShapeSpec._(
|
||||
kind: SparkleShapeKind.star,
|
||||
primary: 8.0,
|
||||
@@ -36,6 +68,7 @@ class SparkleShapeSpec {
|
||||
tertiary: 0.0,
|
||||
);
|
||||
|
||||
/// Five-point star sparkle preset.
|
||||
static const SparkleShapeSpec fivePointStar = SparkleShapeSpec._(
|
||||
kind: SparkleShapeKind.star,
|
||||
primary: 5.0,
|
||||
@@ -43,6 +76,7 @@ class SparkleShapeSpec {
|
||||
tertiary: 0.0,
|
||||
);
|
||||
|
||||
/// Rectangular sparkle preset.
|
||||
static const SparkleShapeSpec rectangle = SparkleShapeSpec._(
|
||||
kind: SparkleShapeKind.rectangle,
|
||||
primary: 0.24,
|
||||
@@ -50,6 +84,7 @@ class SparkleShapeSpec {
|
||||
tertiary: 1.0,
|
||||
);
|
||||
|
||||
/// Diamond sparkle preset.
|
||||
static const SparkleShapeSpec diamond = SparkleShapeSpec._(
|
||||
kind: SparkleShapeKind.polygon,
|
||||
primary: 4.0,
|
||||
@@ -57,6 +92,7 @@ class SparkleShapeSpec {
|
||||
tertiary: 0.78539816339,
|
||||
);
|
||||
|
||||
/// Hexagon sparkle preset.
|
||||
static const SparkleShapeSpec hexagon = SparkleShapeSpec._(
|
||||
kind: SparkleShapeKind.polygon,
|
||||
primary: 6.0,
|
||||
@@ -64,6 +100,7 @@ class SparkleShapeSpec {
|
||||
tertiary: 0.0,
|
||||
);
|
||||
|
||||
/// Randomized polygon sparkle preset.
|
||||
static const SparkleShapeSpec randomPolygon = SparkleShapeSpec._(
|
||||
kind: SparkleShapeKind.randomPolygon,
|
||||
primary: 0.0,
|
||||
@@ -71,6 +108,7 @@ class SparkleShapeSpec {
|
||||
tertiary: 0.0,
|
||||
);
|
||||
|
||||
/// Confetti sparkle preset.
|
||||
static const SparkleShapeSpec confetti = SparkleShapeSpec._(
|
||||
kind: SparkleShapeKind.confetti,
|
||||
primary: 0.34,
|
||||
@@ -78,6 +116,9 @@ class SparkleShapeSpec {
|
||||
tertiary: 1.0,
|
||||
);
|
||||
|
||||
/// Creates a custom star sparkle.
|
||||
///
|
||||
/// [points] is clamped to `[4, 12]` and [innerRatio] to `[0.15, 0.85]`.
|
||||
factory SparkleShapeSpec.customStar(
|
||||
{int points = 8, double innerRatio = 0.42}) {
|
||||
return SparkleShapeSpec._(
|
||||
@@ -88,6 +129,9 @@ class SparkleShapeSpec {
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a custom polygon sparkle.
|
||||
///
|
||||
/// [sides] is clamped to `[3, 10]` and [aspectRatio] to `[0.4, 2.0]`.
|
||||
factory SparkleShapeSpec.customPolygon(
|
||||
{int sides = 6, double aspectRatio = 1.0, double rotation = 0.0}) {
|
||||
return SparkleShapeSpec._(
|
||||
@@ -98,6 +142,9 @@ class SparkleShapeSpec {
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a custom rectangular sparkle.
|
||||
///
|
||||
/// Values are clamped to keep shapes visible and numerically stable.
|
||||
factory SparkleShapeSpec.customRectangle(
|
||||
{double halfWidth = 0.24,
|
||||
double halfHeight = 0.055,
|
||||
@@ -110,6 +157,9 @@ class SparkleShapeSpec {
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a custom confetti sparkle.
|
||||
///
|
||||
/// Values are clamped to keep geometry inside the sparkle cell.
|
||||
factory SparkleShapeSpec.customConfetti(
|
||||
{double length = 0.34,
|
||||
double thickness = 0.045,
|
||||
@@ -122,13 +172,22 @@ class SparkleShapeSpec {
|
||||
);
|
||||
}
|
||||
|
||||
/// Sparkle family used by the shader.
|
||||
final SparkleShapeKind kind;
|
||||
|
||||
/// Shape parameter 1. Meaning depends on [kind].
|
||||
final double primary;
|
||||
|
||||
/// Shape parameter 2. Meaning depends on [kind].
|
||||
final double secondary;
|
||||
|
||||
/// Shape parameter 3. Meaning depends on [kind].
|
||||
final double tertiary;
|
||||
}
|
||||
|
||||
/// Applies holographic shader lighting to any child widget.
|
||||
class Shiny extends StatefulWidget {
|
||||
/// Creates a shiny wrapper.
|
||||
const Shiny({
|
||||
super.key,
|
||||
required this.child,
|
||||
@@ -138,22 +197,47 @@ class Shiny extends StatefulWidget {
|
||||
this.sparkle = 0.8,
|
||||
this.specular = 0.8,
|
||||
this.diffraction = 0.8,
|
||||
this.sparkleShape = SparkleShapeSpec.eightPointStar,
|
||||
this.opacity = 1.0,
|
||||
this.sparkleShape = SparkleShapeSpec.none,
|
||||
this.style = HolographStyle.crackedIce,
|
||||
this.blendMode = BlendMode.screen,
|
||||
this.enableShader = true,
|
||||
});
|
||||
|
||||
/// Child widget receiving the shader mask.
|
||||
final Widget child;
|
||||
|
||||
/// Optional tilt controller stream source.
|
||||
final ShinyController? controller;
|
||||
|
||||
/// Explicit tilt override in normalized `[-1.0, 1.0]` space.
|
||||
final Offset? tilt;
|
||||
|
||||
/// Color separation strength.
|
||||
final double prismatic;
|
||||
|
||||
/// Sparkle strength multiplier.
|
||||
final double sparkle;
|
||||
|
||||
/// Main highlight intensity.
|
||||
final double specular;
|
||||
|
||||
/// Micro-diffraction intensity.
|
||||
final double diffraction;
|
||||
|
||||
/// Global shader opacity multiplier.
|
||||
final double opacity;
|
||||
|
||||
/// Sparkle silhouette specification.
|
||||
final SparkleShapeSpec sparkleShape;
|
||||
|
||||
/// Foil style preset.
|
||||
final HolographStyle style;
|
||||
|
||||
/// Blend mode applied by [ShaderMask].
|
||||
final BlendMode blendMode;
|
||||
|
||||
/// Toggles shader execution.
|
||||
final bool enableShader;
|
||||
|
||||
@override
|
||||
@@ -268,7 +352,8 @@ class _ShinyState extends State<Shiny> with TickerProviderStateMixin {
|
||||
..setFloat(10, widget.sparkleShape.kind.index.toDouble())
|
||||
..setFloat(11, widget.sparkleShape.primary)
|
||||
..setFloat(12, widget.sparkleShape.secondary)
|
||||
..setFloat(13, widget.sparkleShape.tertiary);
|
||||
..setFloat(13, widget.sparkleShape.tertiary)
|
||||
..setFloat(14, widget.opacity);
|
||||
return shader;
|
||||
}
|
||||
|
||||
@@ -289,6 +374,7 @@ class _ShinyState extends State<Shiny> with TickerProviderStateMixin {
|
||||
}
|
||||
|
||||
class ShinyCard extends StatefulWidget {
|
||||
/// Creates a card helper that combines clipping, tilt transform, and [Shiny].
|
||||
const ShinyCard({
|
||||
super.key,
|
||||
this.background,
|
||||
@@ -302,23 +388,52 @@ class ShinyCard extends StatefulWidget {
|
||||
this.sparkle = 0.8,
|
||||
this.specular = 0.8,
|
||||
this.diffraction = 0.8,
|
||||
this.sparkleShape = SparkleShapeSpec.eightPointStar,
|
||||
this.opacity = 1.0,
|
||||
this.sparkleShape = SparkleShapeSpec.none,
|
||||
this.style = HolographStyle.crackedIce,
|
||||
this.enableShader = true,
|
||||
});
|
||||
|
||||
/// Optional card background; defaults to a dark fill.
|
||||
final Widget? background;
|
||||
|
||||
/// Optional widget drawn above the shiny background.
|
||||
final Widget? foreground;
|
||||
|
||||
/// Card clipping shape.
|
||||
final ShapeBorder shape;
|
||||
|
||||
/// Card width.
|
||||
final double width;
|
||||
|
||||
/// Card height.
|
||||
final double height;
|
||||
|
||||
/// Optional sensor or custom tilt controller.
|
||||
final ShinyController? controller;
|
||||
|
||||
/// Color separation strength.
|
||||
final double prismatic;
|
||||
|
||||
/// Sparkle strength multiplier.
|
||||
final double sparkle;
|
||||
|
||||
/// Main highlight intensity.
|
||||
final double specular;
|
||||
|
||||
/// Micro-diffraction intensity.
|
||||
final double diffraction;
|
||||
|
||||
/// Global shader opacity multiplier.
|
||||
final double opacity;
|
||||
|
||||
/// Sparkle silhouette specification.
|
||||
final SparkleShapeSpec sparkleShape;
|
||||
|
||||
/// Foil style preset.
|
||||
final HolographStyle style;
|
||||
|
||||
/// Toggles shader execution.
|
||||
final bool enableShader;
|
||||
|
||||
@override
|
||||
@@ -451,6 +566,7 @@ class _ShinyCardState extends State<ShinyCard>
|
||||
sparkle: widget.sparkle,
|
||||
specular: widget.specular,
|
||||
diffraction: widget.diffraction,
|
||||
opacity: widget.opacity,
|
||||
sparkleShape: widget.sparkleShape,
|
||||
style: widget.style,
|
||||
child: base,
|
||||
|
||||
+24
-35
@@ -15,10 +15,11 @@ uniform float uSparkleShapeKind;
|
||||
uniform float uSparklePrimary;
|
||||
uniform float uSparkleSecondary;
|
||||
uniform float uSparkleTertiary;
|
||||
uniform float uOpacity;
|
||||
|
||||
#define TWO_PI 6.28318530718
|
||||
|
||||
// Precomputed rotation matrices to avoid expensive runtime sin/cos calculations
|
||||
// Precomputed rotation matrices reduce repeated sin/cos work in tiled layers.
|
||||
#define ROT_0_78 mat2( 0.71091, 0.70328, -0.70328, 0.71091)
|
||||
#define ROT_M0_5 mat2( 0.87758, -0.47943, 0.47943, 0.87758)
|
||||
#define ROT_1_2 mat2( 0.36236, 0.93204, -0.93204, 0.36236)
|
||||
@@ -65,7 +66,7 @@ float sdRegularPolygon(vec2 p, float sides, float radius) {
|
||||
|
||||
float sparkleStarMask(vec2 p, float points, float innerRatio) {
|
||||
float angle = atan(p.y, p.x);
|
||||
float radius = dot(p, p); // Optimized: Use squared distance to delay sqrt
|
||||
float radius = dot(p, p);
|
||||
float spikes = 0.5 + 0.5 * cos(angle * points);
|
||||
float starRadius = mix(0.34 * innerRatio, 0.34, pow(spikes, 1.4));
|
||||
return smoothstep(0.0009, -0.0009, radius - (starRadius * starRadius));
|
||||
@@ -100,15 +101,18 @@ float sparklePolygonMask(vec2 p, float sides, float aspectRatio, float rotation)
|
||||
float sparkleShapeMask(vec2 p, vec2 id) {
|
||||
float randomRotation = hash(id + 5.1) * TWO_PI;
|
||||
if (uSparkleShapeKind < 0.5) {
|
||||
return sparkleStarMask(p, uSparklePrimary, uSparkleSecondary);
|
||||
return 0.0;
|
||||
}
|
||||
if (uSparkleShapeKind < 1.5) {
|
||||
return sparkleRectangleMask(p, uSparklePrimary, uSparkleSecondary, randomRotation * uSparkleTertiary);
|
||||
return sparkleStarMask(p, uSparklePrimary, uSparkleSecondary);
|
||||
}
|
||||
if (uSparkleShapeKind < 2.5) {
|
||||
return sparklePolygonMask(p, uSparklePrimary, uSparkleSecondary, uSparkleTertiary);
|
||||
return sparkleRectangleMask(p, uSparklePrimary, uSparkleSecondary, randomRotation * uSparkleTertiary);
|
||||
}
|
||||
if (uSparkleShapeKind < 3.5) {
|
||||
return sparklePolygonMask(p, uSparklePrimary, uSparkleSecondary, uSparkleTertiary);
|
||||
}
|
||||
if (uSparkleShapeKind < 4.5) {
|
||||
float sides = 5.0 + floor(hash(id + 9.4) * 4.0);
|
||||
float aspect = 0.7 + hash(id + 13.1) * 0.7;
|
||||
return sparklePolygonMask(p, sides, aspect, randomRotation);
|
||||
@@ -198,11 +202,11 @@ vec3 styleCrackedIce(vec2 uv, vec2 tilt, float time) {
|
||||
}
|
||||
|
||||
vec3 styleSilverMosaic(vec2 uv, vec2 tilt, float time) {
|
||||
// Setup grid - Scaled up 300% (18.0 -> 6.0)
|
||||
// Mosaic tile grid.
|
||||
vec2 g = uv * 6.0;
|
||||
vec2 id = floor(g);
|
||||
|
||||
// Center coordinates around 0.0 for the radial math
|
||||
// Center local tile coordinates for radial optics.
|
||||
vec2 f = fract(g) - 0.5;
|
||||
|
||||
// Real foil sheets often alternate the grain of the squares to catch light from all angles.
|
||||
@@ -210,50 +214,38 @@ vec3 styleSilverMosaic(vec2 uv, vec2 tilt, float time) {
|
||||
f = vec2(-f.y, f.x);
|
||||
}
|
||||
|
||||
// Get the radial vector (pointing outward from the center of the square)
|
||||
vec2 radialDir = normalize(f + vec2(0.0001));
|
||||
|
||||
// Calculate the simulated light direction based on device tilt.
|
||||
vec2 lightDir = normalize(tilt + vec2(0.001));
|
||||
|
||||
// --- THE OPTICS ---
|
||||
// A radial highlight occurs where the radial vector aligns with the light vector.
|
||||
// Radial highlight where tile direction aligns with light direction.
|
||||
float alignment = abs(dot(radialDir, lightDir));
|
||||
|
||||
// Raise to a high power to narrow the reflection into a sharp, focused beam
|
||||
float highlight = pow(alignment, 12.0);
|
||||
|
||||
// --- THE DIFFRACTION (Rainbow) ---
|
||||
float angleDiff = acos(alignment);
|
||||
|
||||
// 2D cross product to find which side of the highlight we are on
|
||||
// 2D cross product sign decides which side of the highlight receives hue shift.
|
||||
float side = sign(radialDir.x * lightDir.y - radialDir.y * lightDir.x);
|
||||
|
||||
// Phase shift based on tilt magnitude and time.
|
||||
float basePhase = length(tilt) * 2.5 + time * 0.1;
|
||||
|
||||
// Calculate the final color phase.
|
||||
float phase = basePhase + side * angleDiff * 1.8;
|
||||
vec3 holoColor = rainbow(phase, 0.85, 1.0);
|
||||
|
||||
// Add a pure white "specular core" where alignment is absolutely perfect
|
||||
// White specular core appears when alignment is nearly perfect.
|
||||
vec3 core = vec3(1.0) * smoothstep(0.98, 1.0, alignment);
|
||||
|
||||
// Base foil material
|
||||
vec3 foilBase = vec3(0.25, 0.27, 0.30);
|
||||
|
||||
// Combine the light components
|
||||
vec3 finalColor = foilBase + (holoColor * highlight * 1.5) + (core * 0.8);
|
||||
|
||||
// --- BORDERS AND DEPTH ---
|
||||
// Crisp borders between the squares
|
||||
// Tile borders and edge darkening add perceived depth.
|
||||
float edgeX = 0.5 - abs(f.x);
|
||||
float edgeY = 0.5 - abs(f.y);
|
||||
|
||||
// Tightened the border width to compensate for the larger tile size
|
||||
float border = smoothstep(0.0, 0.015, min(edgeX, edgeY));
|
||||
|
||||
// Add a subtle darkening toward the edges of the tiles
|
||||
float dist = length(f);
|
||||
finalColor *= mix(0.7, 1.0, 1.0 - smoothstep(0.0, 0.5, dist) * 0.3);
|
||||
|
||||
@@ -265,9 +257,7 @@ vec3 styleSuperGoldVinyl(vec2 uv, vec2 tilt, float time) {
|
||||
vec2 staggered = vec2(g.x + 0.5 * mod(floor(g.y), 2.0), g.y);
|
||||
vec2 f = fract(staggered) - 0.5;
|
||||
|
||||
// Optimized: Use dot product for squared distance to avoid expensive sqrt/length.
|
||||
// Original smoothstep values (0.42, 0.10) squared become (0.1764, 0.0100)
|
||||
// Original smoothstep values (0.45, 0.18) squared become (0.2025, 0.0324)
|
||||
// Distance-squared masks avoid square roots and preserve circular dots.
|
||||
float d2 = dot(f, f);
|
||||
float dotMask = smoothstep(0.1764, 0.01, d2);
|
||||
float emboss = smoothstep(0.2025, 0.0324, d2);
|
||||
@@ -281,13 +271,12 @@ vec3 styleSuperGoldVinyl(vec2 uv, vec2 tilt, float time) {
|
||||
}
|
||||
|
||||
void main() {
|
||||
// Standard normalized UVs [0.0 to 1.0] for macro lighting, tilt, and glare
|
||||
// UVs in [0, 1] for global lighting and center-based effects.
|
||||
vec2 uv = FlutterFragCoord().xy / uSize;
|
||||
vec2 center = vec2(0.5, 0.5);
|
||||
vec2 fromCenter = uv - center;
|
||||
|
||||
// Aspect-corrected UVs for physical patterns (grid, sparkles, mosaics)
|
||||
// Dividing by the maximum dimension ensures squares remain squares.
|
||||
// Aspect-corrected UVs keep grids and sparkles physically proportional.
|
||||
float maxDim = max(uSize.x, uSize.y);
|
||||
vec2 aspectUV = FlutterFragCoord().xy / maxDim;
|
||||
|
||||
@@ -295,7 +284,7 @@ void main() {
|
||||
float safeTiltMag = max(tiltMag, 0.001);
|
||||
vec2 tiltDir = uTilt / safeTiltMag;
|
||||
|
||||
// The highlight uses the stretched UV so the glare covers the whole widget nicely
|
||||
// Highlight uses stretched UVs so glare spans the full widget area.
|
||||
float highlightDistance = length(uv - (center + (uTilt * 0.35)));
|
||||
float specularMask = pow(max(0.0, 1.0 - highlightDistance * 2.6), 3.2);
|
||||
vec3 specular = vec3(specularMask) * uSpecular * (0.4 + 0.6 * tiltMag);
|
||||
@@ -303,7 +292,7 @@ void main() {
|
||||
bool isCrackedIce = uStyle >= 0.5 && uStyle < 1.5;
|
||||
float sparkleGridScale = isCrackedIce ? 7.0 : 18.0;
|
||||
|
||||
// ---> USE aspectUV HERE so sparkles are drawn perfectly proportionally
|
||||
// Sparkle grid uses aspect-corrected coordinates to avoid stretching.
|
||||
vec2 sparkleGrid = aspectUV * sparkleGridScale;
|
||||
vec2 grid = floor(sparkleGrid);
|
||||
|
||||
@@ -339,7 +328,7 @@ void main() {
|
||||
}
|
||||
|
||||
vec3 styleBase;
|
||||
// ---> USE aspectUV HERE so the foil patterns tile properly
|
||||
// Style bases sample aspect-corrected UVs for stable pattern geometry.
|
||||
if (uStyle < 0.5) {
|
||||
styleBase = styleHolographicSilver(aspectUV, uTilt, uTime);
|
||||
} else if (uStyle < 1.5) {
|
||||
@@ -353,7 +342,7 @@ void main() {
|
||||
float styleLuma = dot(styleBase, vec3(0.299, 0.587, 0.114));
|
||||
vec3 chromaAdjusted = mix(vec3(styleLuma), styleBase, 0.2 + 0.8 * uPrismatic);
|
||||
|
||||
// General 3D lighting normal uses the stretched UV
|
||||
// Directional lighting uses center-relative UVs for broad foil gradients.
|
||||
vec2 normal2d = normalize(fromCenter + vec2(0.001));
|
||||
float directional = 0.5 + 0.5 * dot(normal2d, tiltDir);
|
||||
float tiltLighting = mix(0.86, 1.20, pow(directional, 1.2));
|
||||
@@ -361,7 +350,7 @@ void main() {
|
||||
|
||||
float microStrength = isCrackedIce ? 0.08 : 0.18;
|
||||
|
||||
// ---> USE aspectUV HERE so the microscopic diffraction doesn't stretch
|
||||
// Micro diffraction uses aspect-corrected UVs to keep grain isotropic.
|
||||
float microMask = 0.5 + 0.5 * sin(dot(aspectUV, vec2(137.0, 57.0)) + noise(aspectUV * 14.0) * 4.0 + dot(uTilt, vec2(9.0, 4.0)) * 2.0);
|
||||
vec3 microShimmer = rainbow(noise(aspectUV * 10.0) + dot(uTilt, vec2(0.4, -0.3)) * 0.3, 0.65, 1.0) * microMask * uDiffraction * microStrength;
|
||||
|
||||
@@ -371,7 +360,7 @@ void main() {
|
||||
vec3 vibrantColor = mix(vec3(luma), mappedColor, 1.4);
|
||||
|
||||
float foilBrightness = dot(vibrantColor, vec3(0.333));
|
||||
float alpha = clamp(foilBrightness * 0.5, 0.0, 0.35);
|
||||
float alpha = clamp(foilBrightness * 0.5, 0.0, 0.35) * clamp(uOpacity, 0.0, 1.0);
|
||||
vec3 finalColor = clamp(vibrantColor, 0.0, 1.0);
|
||||
|
||||
fragColor = vec4(finalColor * alpha, alpha);
|
||||
|
||||
@@ -16,14 +16,14 @@ void main() {
|
||||
expect(shinyCard.style, HolographStyle.crackedIce);
|
||||
});
|
||||
|
||||
test('default sparkle shape is star', () {
|
||||
test('default sparkle shape is none', () {
|
||||
const Shiny shiny = Shiny(child: SizedBox.shrink());
|
||||
const ShinyCard shinyCard = ShinyCard();
|
||||
|
||||
expect(shiny.sparkleShape.kind, SparkleShapeKind.star);
|
||||
expect(shiny.sparkleShape.primary, 8.0);
|
||||
expect(shinyCard.sparkleShape.kind, SparkleShapeKind.star);
|
||||
expect(shinyCard.sparkleShape.primary, 8.0);
|
||||
expect(shiny.sparkleShape.kind, SparkleShapeKind.none);
|
||||
expect(shiny.sparkleShape.primary, 0.0);
|
||||
expect(shinyCard.sparkleShape.kind, SparkleShapeKind.none);
|
||||
expect(shinyCard.sparkleShape.primary, 0.0);
|
||||
});
|
||||
|
||||
test('custom sparkle factories produce expected specs', () {
|
||||
|
||||
Reference in New Issue
Block a user