Initial commit

This commit is contained in:
2026-04-15 00:51:17 +02:00
commit 0fec97163d
72 changed files with 3076 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
export 'src/sensor_tilt_controller.dart';
export 'src/shiny_controller.dart';
export 'src/shiny_widget.dart';
+20
View File
@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: Text('Hello World!'),
),
),
);
}
}
+122
View File
@@ -0,0 +1,122 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:sensors_plus/sensors_plus.dart';
/// Produces a normalized tilt stream from gyroscope and accelerometer data.
class SensorTiltController {
/// Creates and starts the sensor controller.
SensorTiltController({
this.alpha = 0.98,
this.maxTiltRadians = 0.5,
}) : _invAlpha = 1.0 - alpha {
_start();
}
/// Complementary filter blending coefficient.
final double alpha;
/// Cached inverse alpha to avoid repetitive subtractions.
final double _invAlpha;
/// Physical angle that maps to normalized magnitude `1.0`.
final double maxTiltRadians;
final StreamController<Offset> _controller =
StreamController<Offset>.broadcast();
// High-performance timing for tight physics loops
final Stopwatch _stopwatch = Stopwatch();
StreamSubscription<GyroscopeEvent>? _gyroSub;
StreamSubscription<AccelerometerEvent>? _accelSub;
double _roll = 0.0;
double _pitch = 0.0;
double _accelRoll = 0.0;
double _accelPitch = 0.0;
int? _lastMicroseconds;
/// The normalized tilt stream.
Stream<Offset> get stream => _controller.stream;
void _start() {
if (!_supportsSensorStreams) return;
_stopwatch.start();
try {
_gyroSub = gyroscopeEventStream().listen(
_onGyro,
onError: (_) {},
cancelOnError: false,
);
} on MissingPluginException {
_gyroSub = null;
}
try {
_accelSub = accelerometerEventStream().listen(
_onAccel,
onError: (_) {},
cancelOnError: false,
);
} on MissingPluginException {
_accelSub = null;
}
}
bool get _supportsSensorStreams {
if (kIsWeb) return true;
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
return true;
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
return false;
}
}
void _onGyro(GyroscopeEvent event) {
final int nowMicros = _stopwatch.elapsedMicroseconds;
final int? last = _lastMicroseconds;
_lastMicroseconds = nowMicros;
if (last == null) return;
final double dt = (nowMicros - last) / 1000000.0;
if (dt <= 0.0) return;
final double gyroRoll = _roll + (event.y * dt);
final double gyroPitch = _pitch + (event.x * dt);
_roll = (alpha * gyroRoll) + (_invAlpha * _accelRoll);
_pitch = (alpha * gyroPitch) + (_invAlpha * _accelPitch);
_controller.add(_normalizedOffset());
}
void _onAccel(AccelerometerEvent event) {
const double g = 9.81;
_accelRoll = (event.x / g).clamp(-1.0, 1.0);
_accelPitch = (event.y / g).clamp(-1.0, 1.0);
}
Offset _normalizedOffset() {
final double dx = (_roll / maxTiltRadians).clamp(-1.0, 1.0);
final double dy = (_pitch / maxTiltRadians).clamp(-1.0, 1.0);
return Offset(dx, dy);
}
/// Stops sensor streams and releases resources.
Future<void> dispose() async {
_stopwatch.stop();
await _gyroSub?.cancel();
await _accelSub?.cancel();
await _controller.close();
}
}
+47
View File
@@ -0,0 +1,47 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'sensor_tilt_controller.dart';
/// Optional controller for providing tilt input to [Shiny] widgets.
///
/// By default, no stream is emitted. Set [useSensor] to true to use built-in
/// motion sensors, or provide [tiltStream] to fully control tilt externally.
class ShinyController {
/// Creates a [ShinyController].
///
/// If [tiltStream] is provided, it overrides the built-in sensor stream.
ShinyController({
this.useSensor = false,
this.tiltStream,
}) {
if (tiltStream == null && useSensor) {
_sensorController = SensorTiltController();
}
}
/// Enables internal sensor-based tilt when [tiltStream] is null.
final bool useSensor;
/// Optional external tilt stream in normalized `[-1.0, 1.0]` space.
final Stream<Offset>? tiltStream;
SensorTiltController? _sensorController;
/// The effective tilt stream consumed by [Shiny].
Stream<Offset> get stream {
if (tiltStream != null) {
return tiltStream!;
}
if (_sensorController != null) {
return _sensorController!.stream;
}
return const Stream<Offset>.empty();
}
/// Disposes internally-owned resources.
Future<void> dispose() async {
await _sensorController?.dispose();
}
}
+490
View File
@@ -0,0 +1,490 @@
import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'shiny_controller.dart';
enum HolographStyle {
holographicSilver,
crackedIce,
silverMosaic,
superGoldVinyl,
}
enum SparkleShapeKind {
star,
rectangle,
polygon,
randomPolygon,
confetti,
}
class SparkleShapeSpec {
const SparkleShapeSpec._({
required this.kind,
required this.primary,
required this.secondary,
required this.tertiary,
});
static const SparkleShapeSpec eightPointStar = SparkleShapeSpec._(
kind: SparkleShapeKind.star,
primary: 8.0,
secondary: 0.42,
tertiary: 0.0,
);
static const SparkleShapeSpec fivePointStar = SparkleShapeSpec._(
kind: SparkleShapeKind.star,
primary: 5.0,
secondary: 0.42,
tertiary: 0.0,
);
static const SparkleShapeSpec rectangle = SparkleShapeSpec._(
kind: SparkleShapeKind.rectangle,
primary: 0.24,
secondary: 0.055,
tertiary: 1.0,
);
static const SparkleShapeSpec diamond = SparkleShapeSpec._(
kind: SparkleShapeKind.polygon,
primary: 4.0,
secondary: 1.0,
tertiary: 0.78539816339,
);
static const SparkleShapeSpec hexagon = SparkleShapeSpec._(
kind: SparkleShapeKind.polygon,
primary: 6.0,
secondary: 1.0,
tertiary: 0.0,
);
static const SparkleShapeSpec randomPolygon = SparkleShapeSpec._(
kind: SparkleShapeKind.randomPolygon,
primary: 0.0,
secondary: 0.0,
tertiary: 0.0,
);
static const SparkleShapeSpec confetti = SparkleShapeSpec._(
kind: SparkleShapeKind.confetti,
primary: 0.34,
secondary: 0.045,
tertiary: 1.0,
);
factory SparkleShapeSpec.customStar(
{int points = 8, double innerRatio = 0.42}) {
return SparkleShapeSpec._(
kind: SparkleShapeKind.star,
primary: points.toDouble().clamp(4.0, 12.0),
secondary: innerRatio.clamp(0.15, 0.85),
tertiary: 0.0,
);
}
factory SparkleShapeSpec.customPolygon(
{int sides = 6, double aspectRatio = 1.0, double rotation = 0.0}) {
return SparkleShapeSpec._(
kind: SparkleShapeKind.polygon,
primary: sides.toDouble().clamp(3.0, 10.0),
secondary: aspectRatio.clamp(0.4, 2.0),
tertiary: rotation,
);
}
factory SparkleShapeSpec.customRectangle(
{double halfWidth = 0.24,
double halfHeight = 0.055,
double rotationJitter = 1.0}) {
return SparkleShapeSpec._(
kind: SparkleShapeKind.rectangle,
primary: halfWidth.clamp(0.05, 0.45),
secondary: halfHeight.clamp(0.02, 0.30),
tertiary: rotationJitter.clamp(0.0, 1.0),
);
}
factory SparkleShapeSpec.customConfetti(
{double length = 0.34,
double thickness = 0.045,
double rotationJitter = 1.0}) {
return SparkleShapeSpec._(
kind: SparkleShapeKind.confetti,
primary: length.clamp(0.08, 0.48),
secondary: thickness.clamp(0.01, 0.14),
tertiary: rotationJitter.clamp(0.0, 1.0),
);
}
final SparkleShapeKind kind;
final double primary;
final double secondary;
final double tertiary;
}
class Shiny extends StatefulWidget {
const Shiny({
super.key,
required this.child,
this.controller,
this.tilt,
this.prismatic = 0.8,
this.sparkle = 0.8,
this.specular = 0.8,
this.diffraction = 0.8,
this.sparkleShape = SparkleShapeSpec.eightPointStar,
this.style = HolographStyle.crackedIce,
this.blendMode = BlendMode.screen,
this.enableShader = true,
});
final Widget child;
final ShinyController? controller;
final Offset? tilt;
final double prismatic;
final double sparkle;
final double specular;
final double diffraction;
final SparkleShapeSpec sparkleShape;
final HolographStyle style;
final BlendMode blendMode;
final bool enableShader;
@override
State<Shiny> createState() => _ShinyState();
}
class _ShinyState extends State<Shiny> with TickerProviderStateMixin {
static Future<ui.FragmentProgram>? _programFuture;
ui.FragmentShader? _shader;
StreamSubscription<Offset>? _tiltSub;
late final Ticker _ticker;
double _time = 0.0;
Offset _tilt = Offset.zero;
@override
void initState() {
super.initState();
_ticker = createTicker((Duration elapsed) {
if (!mounted || _shader == null) return;
setState(() {
_time = elapsed.inMicroseconds / 1000000.0;
});
});
_attachController();
if (widget.enableShader) {
_loadShader();
_ticker.start();
}
}
@override
void didUpdateWidget(covariant Shiny oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.controller != widget.controller ||
oldWidget.tilt != widget.tilt) {
_tiltSub?.cancel();
_attachController();
}
if (oldWidget.enableShader != widget.enableShader) {
if (widget.enableShader) {
if (_shader == null) _loadShader();
_ticker.start();
} else {
_ticker.stop();
}
}
}
Future<void> _loadShader() async {
try {
_programFuture ??= _loadProgram();
final ui.FragmentProgram program = await _programFuture!;
if (!mounted) return;
setState(() {
_shader = program.fragmentShader();
});
} catch (_) {
// Keep rendering without shader when runtime effects are unavailable.
}
}
Future<ui.FragmentProgram> _loadProgram() async {
try {
return await ui.FragmentProgram.fromAsset('shaders/shiny_card.frag');
} catch (_) {
return await ui.FragmentProgram.fromAsset(
'packages/holo_shiny/shaders/shiny_card.frag');
}
}
void _attachController() {
if (widget.tilt != null) return;
final ShinyController? controller = widget.controller;
if (controller == null) return;
_tiltSub = controller.stream.listen((Offset value) {
if (!mounted || widget.tilt != null) return;
final clamped = _clampOffset(value);
if (_tilt != clamped) {
setState(() {
_tilt = clamped;
});
}
});
}
@override
void dispose() {
_ticker.dispose();
_tiltSub?.cancel();
_shader?.dispose();
super.dispose();
}
ui.Shader _configureShader(Rect bounds) {
final Offset effectiveTilt = _clampOffset(widget.tilt ?? _tilt);
final ui.FragmentShader shader = _shader!;
shader
..setFloat(0, bounds.width)
..setFloat(1, bounds.height)
..setFloat(2, effectiveTilt.dx)
..setFloat(3, effectiveTilt.dy)
..setFloat(4, _time)
..setFloat(5, widget.prismatic)
..setFloat(6, widget.sparkle)
..setFloat(7, widget.specular)
..setFloat(8, widget.diffraction)
..setFloat(9, widget.style.index.toDouble())
..setFloat(10, widget.sparkleShape.kind.index.toDouble())
..setFloat(11, widget.sparkleShape.primary)
..setFloat(12, widget.sparkleShape.secondary)
..setFloat(13, widget.sparkleShape.tertiary);
return shader;
}
@override
Widget build(BuildContext context) {
if (!widget.enableShader || _shader == null) return widget.child;
return ShaderMask(
blendMode: widget.blendMode,
shaderCallback: _configureShader,
child: widget.child,
);
}
Offset _clampOffset(Offset value) {
return Offset(value.dx.clamp(-1.0, 1.0), value.dy.clamp(-1.0, 1.0));
}
}
class ShinyCard extends StatefulWidget {
const ShinyCard({
super.key,
this.background,
this.foreground,
this.shape = const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16))),
this.width = 300,
this.height = 420,
this.controller,
this.prismatic = 0.8,
this.sparkle = 0.8,
this.specular = 0.8,
this.diffraction = 0.8,
this.sparkleShape = SparkleShapeSpec.eightPointStar,
this.style = HolographStyle.crackedIce,
this.enableShader = true,
});
final Widget? background;
final Widget? foreground;
final ShapeBorder shape;
final double width;
final double height;
final ShinyController? controller;
final double prismatic;
final double sparkle;
final double specular;
final double diffraction;
final SparkleShapeSpec sparkleShape;
final HolographStyle style;
final bool enableShader;
@override
State<ShinyCard> createState() => _ShinyCardState();
}
class _ShinyCardState extends State<ShinyCard>
with SingleTickerProviderStateMixin {
static const ValueKey<String> transformKey =
ValueKey<String>('holo_shiny.card.transform');
StreamSubscription<Offset>? _tiltSub;
late final AnimationController _spring;
// State segregation: prevents total widget rebuilds during drag events
late final ValueNotifier<Offset> _tiltNotifier = ValueNotifier(Offset.zero);
late final StreamController<Offset> _internalTiltStream =
StreamController<Offset>.broadcast();
late final ShinyController _internalShinyController =
ShinyController(tiltStream: _internalTiltStream.stream);
Offset _sensorTilt = Offset.zero;
Offset _dragTilt = Offset.zero;
Offset _dragTiltAtRelease = Offset.zero;
bool _isDragging = false;
bool _isReturning = false;
void _updateTilt() {
final tilt = (_isDragging || _isReturning) ? _dragTilt : _sensorTilt;
if (_tiltNotifier.value != tilt) {
_tiltNotifier.value = tilt;
if (!_internalTiltStream.isClosed) {
_internalTiltStream.add(tilt);
}
}
}
@override
void initState() {
super.initState();
_spring = AnimationController(
vsync: this, duration: const Duration(milliseconds: 850))
..addListener(() {
if (!mounted || !_isReturning) return;
final double t = Curves.elasticOut.transform(_spring.value);
_dragTilt =
Offset.lerp(_dragTiltAtRelease, Offset.zero, t) ?? Offset.zero;
_updateTilt();
})
..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed) {
_isReturning = false;
_updateTilt();
}
});
_attachController();
}
@override
void didUpdateWidget(covariant ShinyCard oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.controller != widget.controller) {
_tiltSub?.cancel();
_attachController();
}
}
void _attachController() {
final ShinyController? controller = widget.controller;
if (controller == null) return;
_tiltSub = controller.stream.listen((Offset value) {
if (!mounted || _isDragging || _isReturning) return;
_sensorTilt = _clampOffset(value);
_updateTilt();
});
}
void _onPanUpdate(DragUpdateDetails details) {
_isDragging = true;
_isReturning = false;
_spring.stop();
final double dx = (details.delta.dx / widget.width) * 2.0;
final double dy = (details.delta.dy / widget.height) * 2.0;
_dragTilt = _clampOffset(_dragTilt + Offset(dx, dy));
_updateTilt();
}
void _onPanEnd(DragEndDetails details) {
_isDragging = false;
_isReturning = true;
_dragTiltAtRelease = _dragTilt;
_spring
..reset()
..forward();
}
@override
void dispose() {
_tiltSub?.cancel();
_spring.dispose();
_tiltNotifier.dispose();
_internalTiltStream.close();
_internalShinyController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final Widget base = widget.background ??
const ColoredBox(color: Color(0xFF212121), child: SizedBox.expand());
// Isolate the static tree structure so it never rebuilds when tilted
final Widget staticChild = ClipPath(
clipper: ShapeBorderClipper(shape: widget.shape),
child: SizedBox(
width: widget.width,
height: widget.height,
child: Stack(
fit: StackFit.expand,
children: <Widget>[
Shiny(
enableShader: widget.enableShader,
controller: _internalShinyController,
tilt: null, // Managed natively via the internal stream controller
prismatic: widget.prismatic,
sparkle: widget.sparkle,
specular: widget.specular,
diffraction: widget.diffraction,
sparkleShape: widget.sparkleShape,
style: widget.style,
child: base,
),
if (widget.foreground != null) widget.foreground!,
],
),
),
);
return GestureDetector(
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
child: ValueListenableBuilder<Offset>(
valueListenable: _tiltNotifier,
builder: (context, tilt, child) {
final Matrix4 transform = Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateX(-tilt.dy * 0.3)
..rotateY(tilt.dx * 0.3);
return Transform(
key: transformKey,
alignment: Alignment.center,
transform: transform,
child: child,
);
},
child: staticChild,
),
);
}
Offset _clampOffset(Offset value) {
return Offset(value.dx.clamp(-1.0, 1.0), value.dy.clamp(-1.0, 1.0));
}
}