forked from firka/firka
nightmare: add simulation from scratch for collision and movement
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:carousel_slider/carousel_slider.dart';
|
||||
@@ -5,9 +6,13 @@ import 'package:firka/core/firka_bundle.dart';
|
||||
import 'package:firka/app/app_state.dart';
|
||||
import 'package:firka/ui/phone/widgets/login_webview.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:sensors_plus/sensors_plus.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:vibration/vibration.dart';
|
||||
|
||||
import 'package:firka/core/bloc/theme_cubit.dart';
|
||||
import 'package:firka/core/state/firka_state.dart';
|
||||
@@ -15,16 +20,13 @@ import 'package:firka/core/image_preloader.dart';
|
||||
import 'package:firka/ui/theme/style.dart';
|
||||
import 'package:firka/ui/shared/delayed_spinner.dart';
|
||||
|
||||
// TODO: Replace these with actual privacy policy URLs
|
||||
const String _privacyUrlHungarian =
|
||||
'https://github.com/QwIT-Development/privacy-policy/blob/master/README.md';
|
||||
const String _privacyUrlOther = 'https://firka.app/privacy';
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
final AppInitialization data;
|
||||
|
||||
const LoginScreen(this.data, {super.key});
|
||||
|
||||
@override
|
||||
State<LoginScreen> createState() => _LoginScreenState();
|
||||
}
|
||||
@@ -36,22 +38,16 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_loginWebView = LoginWebviewWidget(widget.data);
|
||||
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
|
||||
_preloadImages();
|
||||
}
|
||||
|
||||
// Method to get the appropriate privacy policy URL based on language
|
||||
String _getPrivacyPolicyUrl() {
|
||||
// Check if current language is Hungarian by examining the locale
|
||||
final locale = Localizations.localeOf(context).languageCode;
|
||||
return locale == 'hu' ? _privacyUrlHungarian : _privacyUrlOther;
|
||||
}
|
||||
|
||||
// Method to launch privacy policy URL
|
||||
Future<void> _launchPrivacyPolicy() async {
|
||||
final url = _getPrivacyPolicyUrl();
|
||||
try {
|
||||
@@ -73,10 +69,8 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
|
||||
"assets/images/carousel_dark/slide4.webp",
|
||||
"assets/images/logos/colored_logo.webp",
|
||||
];
|
||||
|
||||
try {
|
||||
await ImagePreloader.preloadMultipleAssets(FirkaBundle(), imagePaths);
|
||||
|
||||
setState(() {
|
||||
_preloadDone = true;
|
||||
});
|
||||
@@ -116,6 +110,7 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
|
||||
final paddingWidthHorizontal =
|
||||
MediaQuery.of(context).size.width -
|
||||
MediaQuery.of(context).size.width * 0.95;
|
||||
|
||||
List<Map<String, Object>> slides = [
|
||||
{
|
||||
'title': widget.data.l10n.title1,
|
||||
@@ -124,7 +119,6 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
|
||||
'background': 'assets/images/carousel/slide1_background.webp',
|
||||
'foreground': '',
|
||||
'rotation': 180.00,
|
||||
// „Mi nekünk két szám típusunk van, int (egy 32 bites szám) meg a double (egy 64 bites tört szám), KURVA ANYÁDAT”
|
||||
'scale': 1.5,
|
||||
'x': 0.00,
|
||||
'y': 150.00,
|
||||
@@ -136,7 +130,6 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
|
||||
'background': 'assets/images/carousel/slide2_background.webp',
|
||||
'foreground': '',
|
||||
'rotation': 180.00,
|
||||
//Mivel radiáns, és nullával nem lehet osztani (remélem tudtad), ezért ha eggyel osztunk akkor egy marad
|
||||
'scale': 1.55,
|
||||
'x': 10.00,
|
||||
'y': 160.00,
|
||||
@@ -155,15 +148,15 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
|
||||
{
|
||||
'title': widget.data.l10n.title4,
|
||||
'subtitle': widget.data.l10n.subtitle4,
|
||||
'picture': 'assets/images/$carousel/slide4.webp',
|
||||
'picture': '',
|
||||
'background': 'assets/images/carousel/slide4_background.webp',
|
||||
'foreground': '',
|
||||
'rotation': 180.00,
|
||||
'scale': 1.35,
|
||||
'x': -5.00,
|
||||
'y': 80.00,
|
||||
'cards': true,
|
||||
},
|
||||
//TODO: implement simulated physics so that the little boxes can move like the phone moves
|
||||
];
|
||||
|
||||
return MaterialApp(
|
||||
@@ -211,7 +204,10 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
|
||||
Expanded(
|
||||
child: CarouselSlider.builder(
|
||||
itemCount: slides.length,
|
||||
itemBuilder: (context, index, realIndex) => Column(
|
||||
itemBuilder: (context, index, realIndex) {
|
||||
final isCards = slides[index]['cards'] == true;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
@@ -243,11 +239,71 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isCards)
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
if ((slides[index]['background'] ?? '')
|
||||
.toString()
|
||||
.isNotEmpty)
|
||||
ClipRect(
|
||||
clipper: ImageClipper(
|
||||
MediaQuery.of(context),
|
||||
),
|
||||
child: Transform.rotate(
|
||||
angle:
|
||||
-math.pi /
|
||||
(slides[index]['rotation']!
|
||||
as double),
|
||||
child: Transform.translate(
|
||||
offset: Offset(
|
||||
slides[index]['x'] as double,
|
||||
slides[index]['y'] as double,
|
||||
),
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(
|
||||
context,
|
||||
).size.width,
|
||||
child: Transform.scale(
|
||||
scale:
|
||||
slides[index]['scale']
|
||||
as double,
|
||||
child: Image.asset(
|
||||
slides[index]['background']!
|
||||
as String,
|
||||
bundle: DefaultAssetBundle.of(
|
||||
context,
|
||||
),
|
||||
fit: BoxFit.contain,
|
||||
width: double.infinity,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8),
|
||||
child: LayoutBuilder(
|
||||
builder: (ctx, constraints) =>
|
||||
_FloatingCardsSlide(
|
||||
width: constraints.maxWidth,
|
||||
height: constraints.maxHeight,
|
||||
topPadding: 30,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
Stack(
|
||||
children: [
|
||||
slides[index]['background']! == ''
|
||||
? SizedBox()
|
||||
: ClipRect(
|
||||
if ((slides[index]['background'] ?? '')
|
||||
.toString()
|
||||
.isNotEmpty)
|
||||
ClipRect(
|
||||
clipper: ImageClipper(
|
||||
MediaQuery.of(context),
|
||||
),
|
||||
@@ -285,29 +341,42 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
SizedBox(height: 73),
|
||||
const SizedBox(height: 73),
|
||||
Padding(
|
||||
padding: EdgeInsetsGeometry.symmetric(
|
||||
horizontal: 18,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
// 10 padding each side
|
||||
// LayoutBuilder receives the inset width so the physics walls match the edges
|
||||
),
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Image(
|
||||
width: MediaQuery.of(
|
||||
context,
|
||||
).size.width,
|
||||
child:
|
||||
(slides[index]['picture'] ?? '')
|
||||
.toString()
|
||||
.isNotEmpty
|
||||
? Image(
|
||||
image: PreloadedImageProvider(
|
||||
DefaultAssetBundle.of(context),
|
||||
slides[index]['picture']! as String,
|
||||
DefaultAssetBundle.of(
|
||||
context,
|
||||
),
|
||||
slides[index]['picture']!
|
||||
as String,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
alignment: Alignment.center,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
slides[index]['foreground']! == ''
|
||||
? SizedBox()
|
||||
: SizedBox(
|
||||
if ((slides[index]['foreground'] ?? '')
|
||||
.toString()
|
||||
.isNotEmpty)
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: ClipRect(
|
||||
clipBehavior: Clip.none,
|
||||
@@ -345,7 +414,8 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
options: CarouselOptions(
|
||||
height: double.infinity,
|
||||
autoPlay: false,
|
||||
@@ -368,8 +438,8 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
|
||||
colors: [
|
||||
appStyle.colors.background.withAlpha(0),
|
||||
appStyle.colors.background,
|
||||
], // customize colors
|
||||
stops: [0.0, 0.5], // percentages (0% → 50% → 100%)
|
||||
],
|
||||
stops: const [0.0, 0.5],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
@@ -418,7 +488,7 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
|
||||
13,
|
||||
),
|
||||
blurRadius: 2,
|
||||
offset: Offset(0, 1),
|
||||
offset: const Offset(0, 1),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
],
|
||||
@@ -429,7 +499,9 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
|
||||
textAlign: TextAlign.center,
|
||||
style: appStyle.fonts.H_16px.copyWith(
|
||||
color: appStyle.colors.textPrimaryLight,
|
||||
fontVariations: [FontVariation("wght", 800)],
|
||||
fontVariations: const [
|
||||
FontVariation("wght", 800),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -459,10 +531,380 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
//card config
|
||||
class _CardConfig {
|
||||
final String asset;
|
||||
final Offset baseOffset;
|
||||
final double size;
|
||||
final double parallax;
|
||||
final double glide;
|
||||
final double aspect;
|
||||
|
||||
const _CardConfig({
|
||||
required this.asset,
|
||||
required this.baseOffset,
|
||||
required this.size,
|
||||
required this.parallax,
|
||||
required this.glide,
|
||||
this.aspect = 0.72,
|
||||
});
|
||||
}
|
||||
|
||||
//floating card part
|
||||
class _FloatingCardsSlide extends StatefulWidget {
|
||||
final double width;
|
||||
final double height;
|
||||
final double topPadding;
|
||||
const _FloatingCardsSlide({
|
||||
required this.width,
|
||||
required this.height,
|
||||
this.topPadding = 0,
|
||||
});
|
||||
@override
|
||||
State<_FloatingCardsSlide> createState() => _FloatingCardsSlideState();
|
||||
}
|
||||
|
||||
class _FloatingCardsSlideState extends State<_FloatingCardsSlide>
|
||||
with SingleTickerProviderStateMixin {
|
||||
static const double _friction = 0.878;
|
||||
static const double _cardHeight =
|
||||
45; //not in pixels, idk what unit but it works :p
|
||||
static const double _maxSpeed = _cardHeight * 1.2;
|
||||
static const double _tiltForce = 0.05;
|
||||
static const double _bounceDamping = 0.45;
|
||||
static const double _collisionRestitution = 1.0;
|
||||
|
||||
//minimum speed it has to go to trigger a vibration, so the phone doesn't turn into a bomb if the cards are touching
|
||||
static const double _vibrateSpeedThreshold = _maxSpeed * 0.2;
|
||||
|
||||
static const List<_CardConfig> _cards = [
|
||||
_CardConfig(
|
||||
asset: 'assets/images/carousel/card1.svg',
|
||||
baseOffset: Offset(-100, -15),
|
||||
size: _cardHeight,
|
||||
parallax: 7.5,
|
||||
glide: 1.05,
|
||||
aspect: 4.48,
|
||||
), // viewBox 215x48
|
||||
_CardConfig(
|
||||
asset: 'assets/images/carousel/card2.svg',
|
||||
baseOffset: Offset(-8, -35),
|
||||
size: _cardHeight,
|
||||
parallax: 9.0,
|
||||
glide: 1.12,
|
||||
aspect: 2.25,
|
||||
), // viewBox 108x48
|
||||
_CardConfig(
|
||||
asset: 'assets/images/carousel/card3.svg',
|
||||
baseOffset: Offset(88, -5),
|
||||
size: _cardHeight,
|
||||
parallax: 7.0,
|
||||
glide: 1.0,
|
||||
aspect: 4.13,
|
||||
), // viewBox 198x48
|
||||
_CardConfig(
|
||||
asset: 'assets/images/carousel/card4.svg',
|
||||
baseOffset: Offset(-60, 55),
|
||||
size: _cardHeight,
|
||||
parallax: 9.5,
|
||||
glide: 1.15,
|
||||
aspect: 2.25,
|
||||
), // viewBox 108x48
|
||||
_CardConfig(
|
||||
asset: 'assets/images/carousel/card5.svg',
|
||||
baseOffset: Offset(52, 80),
|
||||
size: _cardHeight,
|
||||
parallax: 10.5,
|
||||
glide: 1.18,
|
||||
aspect: 3.02,
|
||||
), // viewBox 145x48
|
||||
_CardConfig(
|
||||
asset: 'assets/images/carousel/card6.svg',
|
||||
baseOffset: Offset(128, 18),
|
||||
size: _cardHeight,
|
||||
parallax: 7.5,
|
||||
glide: 0.95,
|
||||
aspect: 5.63,
|
||||
), // viewBox 270x48
|
||||
_CardConfig(
|
||||
asset: 'assets/images/carousel/card7.svg',
|
||||
baseOffset: Offset(-138, 22),
|
||||
size: _cardHeight,
|
||||
parallax: 8.5,
|
||||
glide: 1.05,
|
||||
aspect: 3.94,
|
||||
), // viewBox 189x48
|
||||
];
|
||||
|
||||
late Ticker _ticker;
|
||||
List<Offset> _positions = [];
|
||||
List<Offset> _velocities = [];
|
||||
|
||||
Offset _tilt = Offset.zero;
|
||||
Offset? _baseline;
|
||||
StreamSubscription<AccelerometerEvent>? _accelerometerSub;
|
||||
Duration? _lastTick;
|
||||
|
||||
double _sceneWidth = 0;
|
||||
double _sceneHeight = 0;
|
||||
|
||||
//we needed a cooldown, so that again, the phone doesn't turn into a bomb | EDIT: actually this was fixed by the minimum speed, so we don't need it anymore
|
||||
// DateTime? _lastVibration;
|
||||
|
||||
Offset _clampVel(Offset v) => Offset(
|
||||
v.dx.clamp(-_maxSpeed, _maxSpeed),
|
||||
v.dy.clamp(-_maxSpeed, _maxSpeed),
|
||||
);
|
||||
|
||||
void _maybeVibrate() {
|
||||
// final now = DateTime.now();
|
||||
// if (_lastVibration != null &&
|
||||
// now.difference(_lastVibration!).inMilliseconds < 2) //first used 50 but it wasn't good enough, so now it's 2
|
||||
// return;
|
||||
// _lastVibration = now;
|
||||
Vibration.vibrate(duration: 20);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_positions = _cards
|
||||
.map((c) => c.baseOffset + const Offset(0, 300))
|
||||
.toList();
|
||||
|
||||
final rng = math.Random();
|
||||
_velocities = List.generate(_cards.length, (i) {
|
||||
final jitter = (rng.nextDouble() - 0.5) * 3.0;
|
||||
return Offset(jitter, -9.0 * _cards[i].glide);
|
||||
});
|
||||
|
||||
_ticker = createTicker(_tick)..start();
|
||||
|
||||
_accelerometerSub = accelerometerEventStream(
|
||||
samplingPeriod: SensorInterval.gameInterval,
|
||||
).listen(_handleTilt);
|
||||
}
|
||||
|
||||
void _handleTilt(AccelerometerEvent event) {
|
||||
final raw = Offset(event.x, event.y);
|
||||
_baseline ??= raw;
|
||||
final rel = raw - _baseline!;
|
||||
_tilt = Offset(
|
||||
(_tilt.dx * 0.88 + rel.dx * 0.12).clamp(-6.5, 6.5),
|
||||
(_tilt.dy * 0.88 + rel.dy * 0.12).clamp(-6.5, 6.5),
|
||||
);
|
||||
}
|
||||
|
||||
void _tick(Duration elapsed) {
|
||||
if (_positions.isEmpty || _velocities.isEmpty) return;
|
||||
if (_lastTick == null) {
|
||||
_lastTick = elapsed;
|
||||
return;
|
||||
}
|
||||
|
||||
final dt = ((elapsed - _lastTick!).inMicroseconds / 16667.0).clamp(
|
||||
0.0,
|
||||
4.0,
|
||||
);
|
||||
_lastTick = elapsed;
|
||||
|
||||
if (_sceneWidth == 0 || _sceneHeight == 0) return;
|
||||
|
||||
bool collidedThisTick = false;
|
||||
|
||||
setState(() {
|
||||
final slope = Offset(-_tilt.dx, _tilt.dy);
|
||||
final n = _cards.length;
|
||||
|
||||
//friction
|
||||
for (int i = 0; i < n; i++) {
|
||||
final card = _cards[i];
|
||||
_velocities[i] += slope * card.parallax * _tiltForce * dt;
|
||||
_velocities[i] *= math.pow(_friction, dt).toDouble();
|
||||
_velocities[i] = _clampVel(_velocities[i]);
|
||||
_positions[i] += _velocities[i] * dt;
|
||||
}
|
||||
|
||||
// card to card collison and wall stuff
|
||||
//
|
||||
// running both together in a loop means that when card a is against a
|
||||
// wall and card b pushes into it, the wall clamp on the a card.
|
||||
// it goes back through the collision math wizard on the next loop,
|
||||
// so car b receives the correct reaction instead of having a mating session with card a.
|
||||
// five times is enough i think, more on slower end devices might cause issues idk tho
|
||||
final double halfW = _sceneWidth / 2;
|
||||
final double halfH = _sceneHeight / 2;
|
||||
|
||||
for (int iter = 0; iter < 5; iter++) {
|
||||
//here's the card to card magic, meow i'm going crazy :3
|
||||
for (int i = 0; i < n - 1; i++) {
|
||||
for (int j = i + 1; j < n; j++) {
|
||||
final wi = _cards[i].size * _cards[i].aspect;
|
||||
final wj = _cards[j].size * _cards[j].aspect;
|
||||
final hi = _cards[i].size;
|
||||
final hj = _cards[j].size;
|
||||
|
||||
final pi = _positions[i];
|
||||
final pj = _positions[j];
|
||||
|
||||
final overlapX = (wi + wj) / 2 - (pj.dx - pi.dx).abs();
|
||||
final overlapY = (hi + hj) / 2 - (pj.dy - pi.dy).abs();
|
||||
|
||||
if (overlapX > 0 && overlapY > 0) {
|
||||
if (overlapX < overlapY) {
|
||||
final sign = pj.dx > pi.dx ? 1.0 : -1.0;
|
||||
_positions[i] = Offset(pi.dx - sign * overlapX / 2, pi.dy);
|
||||
_positions[j] = Offset(pj.dx + sign * overlapX / 2, pj.dy);
|
||||
|
||||
final viX = _velocities[i].dx;
|
||||
final vjX = _velocities[j].dx;
|
||||
if ((viX - vjX) * sign > 0) {
|
||||
final impulse = (viX - vjX) * _collisionRestitution;
|
||||
_velocities[i] = _clampVel(
|
||||
Offset(_velocities[i].dx - impulse, _velocities[i].dy),
|
||||
);
|
||||
_velocities[j] = _clampVel(
|
||||
Offset(_velocities[j].dx + impulse, _velocities[j].dy),
|
||||
);
|
||||
if ((viX - vjX).abs() > _vibrateSpeedThreshold) {
|
||||
collidedThisTick = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final sign = pj.dy > pi.dy ? 1.0 : -1.0;
|
||||
_positions[i] = Offset(pi.dx, pi.dy - sign * overlapY / 2);
|
||||
_positions[j] = Offset(pj.dx, pj.dy + sign * overlapY / 2);
|
||||
|
||||
final viY = _velocities[i].dy;
|
||||
final vjY = _velocities[j].dy;
|
||||
if ((viY - vjY) * sign > 0) {
|
||||
final impulse = (viY - vjY) * _collisionRestitution;
|
||||
_velocities[i] = _clampVel(
|
||||
Offset(_velocities[i].dx, _velocities[i].dy - impulse),
|
||||
);
|
||||
_velocities[j] = _clampVel(
|
||||
Offset(_velocities[j].dx, _velocities[j].dy + impulse),
|
||||
);
|
||||
if ((viY - vjY).abs() > _vibrateSpeedThreshold) {
|
||||
collidedThisTick = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// wall collision, runs every loop, explained before
|
||||
// feeds back into the next collision loop.
|
||||
for (int i = 0; i < n; i++) {
|
||||
final card = _cards[i];
|
||||
final double cardW = card.size * card.aspect;
|
||||
final double cardH = card.size;
|
||||
|
||||
final double minX = -halfW + cardW / 2;
|
||||
final double maxX = halfW - cardW / 2;
|
||||
final double minY = -halfH + cardH / 2;
|
||||
final double maxY = halfH - cardH / 2;
|
||||
|
||||
Offset pos = _positions[i];
|
||||
double vx = _velocities[i].dx;
|
||||
double vy = _velocities[i].dy;
|
||||
|
||||
if (pos.dx < minX) {
|
||||
pos = Offset(minX, pos.dy);
|
||||
vx = vx.abs() * _bounceDamping;
|
||||
} else if (pos.dx > maxX) {
|
||||
pos = Offset(maxX, pos.dy);
|
||||
vx = -vx.abs() * _bounceDamping;
|
||||
}
|
||||
|
||||
if (pos.dy < minY) {
|
||||
pos = Offset(pos.dx, minY);
|
||||
vy = vy.abs() * _bounceDamping;
|
||||
} else if (pos.dy > maxY) {
|
||||
pos = Offset(pos.dx, maxY);
|
||||
vy = -vy.abs() * _bounceDamping;
|
||||
}
|
||||
|
||||
_velocities[i] = _clampVel(Offset(vx, vy));
|
||||
_positions[i] = pos;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// phone mating session
|
||||
if (collidedThisTick) _maybeVibrate();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ticker.dispose();
|
||||
_accelerometerSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double totalHeight = widget.height;
|
||||
_sceneWidth = widget.width;
|
||||
_sceneHeight = math.max(0, totalHeight - widget.topPadding);
|
||||
|
||||
final Offset center = Offset(
|
||||
_sceneWidth / 2,
|
||||
widget.topPadding + _sceneHeight / 2,
|
||||
);
|
||||
|
||||
return SizedBox(
|
||||
width: _sceneWidth,
|
||||
height: totalHeight,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
children: [
|
||||
//background rectangle
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: widget.topPadding,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: appStyle.colors.buttonSecondaryFill, //button color xdddd
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(32),
|
||||
topRight: Radius.circular(32),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
...List.generate(_cards.length, (i) {
|
||||
final card = _cards[i];
|
||||
final double cardWidth = card.size * card.aspect;
|
||||
final double cardHeight = card.size;
|
||||
final Offset pos =
|
||||
center + _positions[i] - Offset(cardWidth / 2, cardHeight / 2);
|
||||
return Positioned(
|
||||
left: pos.dx,
|
||||
top: pos.dy,
|
||||
width: cardWidth,
|
||||
height: cardHeight,
|
||||
child: SvgPicture.asset(
|
||||
card.asset,
|
||||
width: cardWidth,
|
||||
height: cardHeight,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// this sucks :3
|
||||
class ImageClipper extends CustomClipper<Rect> {
|
||||
final MediaQueryData _mediaQuery;
|
||||
|
||||
ImageClipper(this._mediaQuery);
|
||||
|
||||
@override
|
||||
@@ -476,7 +918,5 @@ class ImageClipper extends CustomClipper<Rect> {
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(covariant CustomClipper<Rect> oldClipper) {
|
||||
return false;
|
||||
}
|
||||
bool shouldReclip(covariant CustomClipper<Rect> oldClipper) => false;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,8 @@ dependencies:
|
||||
live_activities: ^2.4.1
|
||||
logging: ^1.3.0
|
||||
share_plus: ^12.0.0
|
||||
sensors_plus: ^6.1.1
|
||||
flutter_physics: ^0.2.0
|
||||
url_launcher: ^6.3.2
|
||||
shared_preferences: ^2.3.4
|
||||
flutter_dotenv: ^5.2.1
|
||||
@@ -55,6 +57,7 @@ dependencies:
|
||||
flutter_native_splash: ^2.4.7
|
||||
go_router: ^17.1.0
|
||||
flutter_bloc: ^9.0.0
|
||||
vibration: ^3.1.8
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@@ -88,6 +91,7 @@ flutter:
|
||||
- assets/swears/DirtyWords.xml
|
||||
- assets/images/cactus_error_screen.png
|
||||
- assets/images/logos/dave_error.png
|
||||
- assets/icons/button/colorwheel.png
|
||||
|
||||
fonts:
|
||||
- family: Montserrat
|
||||
|
||||
Reference in New Issue
Block a user