From c7382c11a5d2be3bd09d0f639c180faf30d1c039 Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Wed, 15 Apr 2026 11:24:32 +0200 Subject: [PATCH] Update README and code to set default sparkle shape to 'none' and add opacity control Signed-off-by: Hans Kokx --- README.md | 15 ++++- example/lib/main.dart | 126 ++++++++++++++++++++++++++---------- lib/holo_shiny.dart | 3 + lib/main.dart | 2 + lib/src/shiny_widget.dart | 122 +++++++++++++++++++++++++++++++++- shaders/shiny_card.frag | 59 +++++++---------- test/shiny_widget_test.dart | 10 +-- 7 files changed, 257 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index be4cfa7..9adf073 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/example/lib/main.dart b/example/lib/main.dart index 7b3b087..69e10eb 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -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 { + /// Sensor-driven controller used by the first two demo surfaces. late final ShinyController _sensorController; + + /// Manual stream used by tilt preset buttons. late final StreamController _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 _sparkleChoices = { + 'None': SparkleShapeSpec.none, '8-Point Star': SparkleShapeSpec.eightPointStar, '5-Point Star': SparkleShapeSpec.fivePointStar, 'Rectangle': SparkleShapeSpec.rectangle, @@ -90,6 +101,11 @@ class _ExampleHomeState extends State { 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 { sparkle: _sparkle, specular: _specular, diffraction: _diffraction, + opacity: _opacity, ), ), const SizedBox(height: 16), @@ -169,27 +186,18 @@ class _ExampleHomeState extends State { _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: [ - 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 { 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 { } 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: [ - 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: [ 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( + segments: HolographStyle.values + .map((HolographStyle style) => ButtonSegment( + value: style, + label: Text(_styleLabel(style)), + )) + .toList(), + selected: {selectedStyle}, + onSelectionChanged: (Set 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 onChanged; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + 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(); diff --git a/lib/holo_shiny.dart b/lib/holo_shiny.dart index e1cfbd4..aa04a49 100644 --- a/lib/holo_shiny.dart +++ b/lib/holo_shiny.dart @@ -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'; diff --git a/lib/main.dart b/lib/main.dart index a725658..9d3d3d8 100644 --- a/lib/main.dart +++ b/lib/main.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}); diff --git a/lib/src/shiny_widget.dart b/lib/src/shiny_widget.dart index 615f1cf..d6e6670 100644 --- a/lib/src/shiny_widget.dart +++ b/lib/src/shiny_widget.dart @@ -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 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 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 sparkle: widget.sparkle, specular: widget.specular, diffraction: widget.diffraction, + opacity: widget.opacity, sparkleShape: widget.sparkleShape, style: widget.style, child: base, diff --git a/shaders/shiny_card.frag b/shaders/shiny_card.frag index 2c63131..0ef22c7 100644 --- a/shaders/shiny_card.frag +++ b/shaders/shiny_card.frag @@ -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); diff --git a/test/shiny_widget_test.dart b/test/shiny_widget_test.dart index 6f394e2..3813e36 100644 --- a/test/shiny_widget_test.dart +++ b/test/shiny_widget_test.dart @@ -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', () {