Initial commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
export 'src/sensor_tilt_controller.dart';
|
||||
export 'src/shiny_controller.dart';
|
||||
export 'src/shiny_widget.dart';
|
||||
@@ -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!'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user