feat: add vehicle customization, Arabic search normalization, and smooth movement interpolation
This commit is contained in:
@@ -0,0 +1,383 @@
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class VehicleStyleOption {
|
||||
final String id;
|
||||
final String labelAr;
|
||||
final IconData icon;
|
||||
|
||||
const VehicleStyleOption({
|
||||
required this.id,
|
||||
required this.labelAr,
|
||||
required this.icon,
|
||||
});
|
||||
}
|
||||
|
||||
class VehicleColorOption {
|
||||
final int colorValue;
|
||||
final String nameAr;
|
||||
|
||||
const VehicleColorOption({
|
||||
required this.colorValue,
|
||||
required this.nameAr,
|
||||
});
|
||||
|
||||
Color get color => Color(colorValue);
|
||||
}
|
||||
|
||||
class VehicleIconGenerator {
|
||||
VehicleIconGenerator._();
|
||||
|
||||
static const List<VehicleStyleOption> availableStyles = [
|
||||
VehicleStyleOption(
|
||||
id: 'car',
|
||||
labelAr: 'سيارة عصرية 3D',
|
||||
icon: Icons.directions_car_filled_rounded,
|
||||
),
|
||||
VehicleStyleOption(
|
||||
id: 'suv',
|
||||
labelAr: 'دفع رباعي تكتيكي',
|
||||
icon: Icons.airport_shuttle_rounded,
|
||||
),
|
||||
VehicleStyleOption(
|
||||
id: 'arrow',
|
||||
labelAr: 'سهم الملاحة المجسم',
|
||||
icon: Icons.navigation_rounded,
|
||||
),
|
||||
];
|
||||
|
||||
static const List<VehicleColorOption> availableColors = [
|
||||
VehicleColorOption(colorValue: 0xFF007AFF, nameAr: 'أزرق سيرو'),
|
||||
VehicleColorOption(colorValue: 0xFF00C853, nameAr: 'أخضر تكتيكي'),
|
||||
VehicleColorOption(colorValue: 0xFFFF3B30, nameAr: 'أحمر رياضي'),
|
||||
VehicleColorOption(colorValue: 0xFFFF9500, nameAr: 'برتقالي شمسي'),
|
||||
VehicleColorOption(colorValue: 0xFF212529, nameAr: 'أسود كربوني'),
|
||||
VehicleColorOption(colorValue: 0xFF00E5FF, nameAr: 'سماوي كهربائي'),
|
||||
];
|
||||
|
||||
/// Generates crisp PNG image bytes for a vehicle icon based on style and color.
|
||||
/// Standard canvas size is 192x192 px to ensure crystal clear presentation on high-DPI displays.
|
||||
static Future<Uint8List> generateVehicleIconBytes({
|
||||
required String styleId,
|
||||
required Color primaryColor,
|
||||
double size = 192.0,
|
||||
}) async {
|
||||
final recorder = ui.PictureRecorder();
|
||||
final canvas = Canvas(recorder, Rect.fromLTWH(0, 0, size, size));
|
||||
|
||||
switch (styleId) {
|
||||
case 'suv':
|
||||
_drawSuv(canvas, size, primaryColor);
|
||||
break;
|
||||
case 'arrow':
|
||||
_drawNavigationArrow(canvas, size, primaryColor);
|
||||
break;
|
||||
case 'car':
|
||||
default:
|
||||
_drawSedanCar(canvas, size, primaryColor);
|
||||
break;
|
||||
}
|
||||
|
||||
final picture = recorder.endRecording();
|
||||
final img = await picture.toImage(size.toInt(), size.toInt());
|
||||
final byteData = await img.toByteData(format: ui.ImageByteFormat.png);
|
||||
return byteData!.buffer.asUint8List();
|
||||
}
|
||||
|
||||
// ── 1. MODERN 3D SEDAN CAR ────────────────────────────────────
|
||||
static void _drawSedanCar(Canvas canvas, double size, Color color) {
|
||||
final center = Offset(size / 2, size / 2);
|
||||
final width = size * 0.46;
|
||||
final height = size * 0.82;
|
||||
|
||||
// 1. Soft Realistic Drop Shadow underneath
|
||||
final shadowPaint = Paint()
|
||||
..color = const Color(0x55000000)
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 12.0);
|
||||
final shadowRect = RRect.fromRectAndRadius(
|
||||
Rect.fromCenter(center: center.translate(0, 6), width: width * 1.05, height: height * 1.02),
|
||||
const Radius.circular(24),
|
||||
);
|
||||
canvas.drawRRect(shadowRect, shadowPaint);
|
||||
|
||||
// 2. Main Car Chassis Body
|
||||
final chassisRect = RRect.fromRectAndRadius(
|
||||
Rect.fromCenter(center: center, width: width, height: height),
|
||||
const Radius.circular(22),
|
||||
);
|
||||
|
||||
// Glossy metallic gradient across body
|
||||
final hsl = HSLColor.fromColor(color);
|
||||
final lightColor = hsl.withLightness((hsl.lightness + 0.18).clamp(0.0, 1.0)).toColor();
|
||||
final darkColor = hsl.withLightness((hsl.lightness - 0.15).clamp(0.0, 1.0)).toColor();
|
||||
|
||||
final bodyPaint = Paint()
|
||||
..shader = ui.Gradient.linear(
|
||||
Offset(center.dx - width / 2, center.dy),
|
||||
Offset(center.dx + width / 2, center.dy),
|
||||
[darkColor, lightColor, color, darkColor],
|
||||
[0.0, 0.35, 0.7, 1.0],
|
||||
);
|
||||
canvas.drawRRect(chassisRect, bodyPaint);
|
||||
|
||||
// Subtle 3D white chassis outline
|
||||
final outlinePaint = Paint()
|
||||
..color = Colors.white.withValues(alpha: 0.45)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2.2;
|
||||
canvas.drawRRect(chassisRect, outlinePaint);
|
||||
|
||||
// 3. Front Windshield (curved dark glass)
|
||||
final windshieldPaint = Paint()
|
||||
..shader = ui.Gradient.linear(
|
||||
Offset(center.dx, center.dy - height * 0.28),
|
||||
Offset(center.dx, center.dy - height * 0.08),
|
||||
[const Color(0xDD1A2332), const Color(0x992C3E50)],
|
||||
);
|
||||
final windshieldPath = Path()
|
||||
..moveTo(center.dx - width * 0.36, center.dy - height * 0.12)
|
||||
..lineTo(center.dx - width * 0.28, center.dy - height * 0.26)
|
||||
..quadraticBezierTo(
|
||||
center.dx, center.dy - height * 0.30,
|
||||
center.dx + width * 0.28, center.dy - height * 0.26,
|
||||
)
|
||||
..lineTo(center.dx + width * 0.36, center.dy - height * 0.12)
|
||||
..close();
|
||||
canvas.drawPath(windshieldPath, windshieldPaint);
|
||||
|
||||
// 4. Car Roof / Sunroof
|
||||
final roofRect = RRect.fromRectAndRadius(
|
||||
Rect.fromCenter(
|
||||
center: Offset(center.dx, center.dy + height * 0.02),
|
||||
width: width * 0.65,
|
||||
height: height * 0.28,
|
||||
),
|
||||
const Radius.circular(8),
|
||||
);
|
||||
final roofPaint = Paint()
|
||||
..color = hsl.withLightness((hsl.lightness - 0.08).clamp(0.0, 1.0)).toColor();
|
||||
canvas.drawRRect(roofRect, roofPaint);
|
||||
|
||||
// Panoramic Sunroof glass
|
||||
final sunroofRect = RRect.fromRectAndRadius(
|
||||
Rect.fromCenter(
|
||||
center: Offset(center.dx, center.dy + height * 0.02),
|
||||
width: width * 0.52,
|
||||
height: height * 0.20,
|
||||
),
|
||||
const Radius.circular(6),
|
||||
);
|
||||
canvas.drawRRect(
|
||||
sunroofRect,
|
||||
Paint()..color = const Color(0xBB1E293B),
|
||||
);
|
||||
|
||||
// 5. Rear Windshield
|
||||
final rearGlassPath = Path()
|
||||
..moveTo(center.dx - width * 0.35, center.dy + height * 0.18)
|
||||
..lineTo(center.dx - width * 0.27, center.dy + height * 0.30)
|
||||
..quadraticBezierTo(
|
||||
center.dx, center.dy + height * 0.32,
|
||||
center.dx + width * 0.27, center.dy + height * 0.30,
|
||||
)
|
||||
..lineTo(center.dx + width * 0.35, center.dy + height * 0.18)
|
||||
..close();
|
||||
canvas.drawPath(
|
||||
rearGlassPath,
|
||||
Paint()..color = const Color(0xDD1A2332),
|
||||
);
|
||||
|
||||
// 6. LED Headlights (Glowing Cyan-White at front)
|
||||
final headlightPaint = Paint()..color = const Color(0xFFF8FAFC);
|
||||
final headlightGlow = Paint()
|
||||
..color = const Color(0x8838BDF8)
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0);
|
||||
|
||||
// Left & right headlights
|
||||
final leftLight = Rect.fromLTWH(center.dx - width * 0.42, center.dy - height * 0.46, width * 0.22, height * 0.08);
|
||||
final rightLight = Rect.fromLTWH(center.dx + width * 0.20, center.dy - height * 0.46, width * 0.22, height * 0.08);
|
||||
canvas.drawRRect(RRect.fromRectAndRadius(leftLight, const Radius.circular(4)), headlightGlow);
|
||||
canvas.drawRRect(RRect.fromRectAndRadius(leftLight, const Radius.circular(4)), headlightPaint);
|
||||
canvas.drawRRect(RRect.fromRectAndRadius(rightLight, const Radius.circular(4)), headlightGlow);
|
||||
canvas.drawRRect(RRect.fromRectAndRadius(rightLight, const Radius.circular(4)), headlightPaint);
|
||||
|
||||
// 7. LED Taillights (Red accent bars at rear)
|
||||
final taillightPaint = Paint()..color = const Color(0xFFFF2D55);
|
||||
final leftTail = Rect.fromLTWH(center.dx - width * 0.42, center.dy + height * 0.42, width * 0.22, height * 0.06);
|
||||
final rightTail = Rect.fromLTWH(center.dx + width * 0.20, center.dy + height * 0.42, width * 0.22, height * 0.06);
|
||||
canvas.drawRRect(RRect.fromRectAndRadius(leftTail, const Radius.circular(3)), taillightPaint);
|
||||
canvas.drawRRect(RRect.fromRectAndRadius(rightTail, const Radius.circular(3)), taillightPaint);
|
||||
|
||||
// 8. Side Mirrors
|
||||
final mirrorPaint = Paint()..color = color;
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(center.dx - width * 0.58, center.dy - height * 0.18, width * 0.12, height * 0.08),
|
||||
const Radius.circular(4),
|
||||
),
|
||||
mirrorPaint,
|
||||
);
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(center.dx + width * 0.46, center.dy - height * 0.18, width * 0.12, height * 0.08),
|
||||
const Radius.circular(4),
|
||||
),
|
||||
mirrorPaint,
|
||||
);
|
||||
}
|
||||
|
||||
// ── 2. TACTICAL 4x4 SUV ───────────────────────────────────────
|
||||
static void _drawSuv(Canvas canvas, double size, Color color) {
|
||||
final center = Offset(size / 2, size / 2);
|
||||
final width = size * 0.52;
|
||||
final height = size * 0.84;
|
||||
|
||||
// Drop Shadow
|
||||
final shadowPaint = Paint()
|
||||
..color = const Color(0x66000000)
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 14.0);
|
||||
final shadowRect = RRect.fromRectAndRadius(
|
||||
Rect.fromCenter(center: center.translate(0, 6), width: width * 1.06, height: height * 1.02),
|
||||
const Radius.circular(20),
|
||||
);
|
||||
canvas.drawRRect(shadowRect, shadowPaint);
|
||||
|
||||
// SUV Boxy Rugged Body
|
||||
final chassisRect = RRect.fromRectAndRadius(
|
||||
Rect.fromCenter(center: center, width: width, height: height),
|
||||
const Radius.circular(18),
|
||||
);
|
||||
final hsl = HSLColor.fromColor(color);
|
||||
final darkColor = hsl.withLightness((hsl.lightness - 0.18).clamp(0.0, 1.0)).toColor();
|
||||
final lightColor = hsl.withLightness((hsl.lightness + 0.15).clamp(0.0, 1.0)).toColor();
|
||||
|
||||
final bodyPaint = Paint()
|
||||
..shader = ui.Gradient.linear(
|
||||
Offset(center.dx - width / 2, center.dy),
|
||||
Offset(center.dx + width / 2, center.dy),
|
||||
[darkColor, lightColor, color, darkColor],
|
||||
[0.0, 0.35, 0.7, 1.0],
|
||||
);
|
||||
canvas.drawRRect(chassisRect, bodyPaint);
|
||||
|
||||
// Heavy outline
|
||||
canvas.drawRRect(
|
||||
chassisRect,
|
||||
Paint()
|
||||
..color = Colors.white.withValues(alpha: 0.5)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2.4,
|
||||
);
|
||||
|
||||
// Roof Rack Crossbars
|
||||
final rackPaint = Paint()
|
||||
..color = const Color(0xFF1E293B)
|
||||
..strokeWidth = 3.5
|
||||
..style = PaintingStyle.stroke;
|
||||
canvas.drawLine(
|
||||
Offset(center.dx - width * 0.36, center.dy - height * 0.12),
|
||||
Offset(center.dx + width * 0.36, center.dy - height * 0.12),
|
||||
rackPaint,
|
||||
);
|
||||
canvas.drawLine(
|
||||
Offset(center.dx - width * 0.36, center.dy + height * 0.12),
|
||||
Offset(center.dx + width * 0.36, center.dy + height * 0.12),
|
||||
rackPaint,
|
||||
);
|
||||
|
||||
// Front Windshield
|
||||
final windshieldRect = RRect.fromRectAndRadius(
|
||||
Rect.fromCenter(
|
||||
center: Offset(center.dx, center.dy - height * 0.22),
|
||||
width: width * 0.74,
|
||||
height: height * 0.18,
|
||||
),
|
||||
const Radius.circular(8),
|
||||
);
|
||||
canvas.drawRRect(
|
||||
windshieldRect,
|
||||
Paint()..color = const Color(0xDD0F172A),
|
||||
);
|
||||
|
||||
// Aggressive LED Front Light Bar
|
||||
final lightBar = RRect.fromRectAndRadius(
|
||||
Rect.fromCenter(
|
||||
center: Offset(center.dx, center.dy - height * 0.44),
|
||||
width: width * 0.85,
|
||||
height: height * 0.07,
|
||||
),
|
||||
const Radius.circular(4),
|
||||
);
|
||||
canvas.drawRRect(
|
||||
lightBar,
|
||||
Paint()
|
||||
..color = const Color(0xAA38BDF8)
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 5),
|
||||
);
|
||||
canvas.drawRRect(lightBar, Paint()..color = Colors.white);
|
||||
|
||||
// Rear Brake Light Bar
|
||||
final rearLightBar = RRect.fromRectAndRadius(
|
||||
Rect.fromCenter(
|
||||
center: Offset(center.dx, center.dy + height * 0.44),
|
||||
width: width * 0.80,
|
||||
height: height * 0.06,
|
||||
),
|
||||
const Radius.circular(3),
|
||||
);
|
||||
canvas.drawRRect(rearLightBar, Paint()..color = const Color(0xFFFF2D55));
|
||||
}
|
||||
|
||||
// ── 3. 3D DIRECTIONAL NAVIGATION ARROW (Google / Apple Style) ──
|
||||
static void _drawNavigationArrow(Canvas canvas, double size, Color color) {
|
||||
final center = Offset(size / 2, size / 2);
|
||||
final radius = size * 0.44;
|
||||
|
||||
// Glowing outer circular halo
|
||||
final haloPaint = Paint()
|
||||
..color = color.withValues(alpha: 0.22)
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 14.0);
|
||||
canvas.drawCircle(center, radius, haloPaint);
|
||||
|
||||
// White disc background with elevation shadow
|
||||
final shadowPaint = Paint()
|
||||
..color = const Color(0x44000000)
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 8.0);
|
||||
canvas.drawCircle(center.translate(0, 4), radius * 0.78, shadowPaint);
|
||||
|
||||
final discPaint = Paint()..color = Colors.white;
|
||||
canvas.drawCircle(center, radius * 0.78, discPaint);
|
||||
|
||||
// Inner directional 3D chevron arrow
|
||||
final arrowPath = Path()
|
||||
..moveTo(center.dx, center.dy - radius * 0.65) // Top sharp point
|
||||
..lineTo(center.dx + radius * 0.52, center.dy + radius * 0.42) // Bottom right wing
|
||||
..lineTo(center.dx, center.dy + radius * 0.18) // Inner center notch
|
||||
..lineTo(center.dx - radius * 0.52, center.dy + radius * 0.42) // Bottom left wing
|
||||
..close();
|
||||
|
||||
// 3D Split Lighting on Chevron
|
||||
final hsl = HSLColor.fromColor(color);
|
||||
final lightHalf = hsl.withLightness((hsl.lightness + 0.12).clamp(0.0, 1.0)).toColor();
|
||||
final darkHalf = hsl.withLightness((hsl.lightness - 0.14).clamp(0.0, 1.0)).toColor();
|
||||
|
||||
final arrowPaint = Paint()
|
||||
..shader = ui.Gradient.linear(
|
||||
Offset(center.dx - radius * 0.5, center.dy),
|
||||
Offset(center.dx + radius * 0.5, center.dy),
|
||||
[darkHalf, lightHalf],
|
||||
);
|
||||
canvas.drawPath(arrowPath, arrowPaint);
|
||||
|
||||
// Center divider line
|
||||
final linePaint = Paint()
|
||||
..color = Colors.white.withValues(alpha: 0.55)
|
||||
..strokeWidth = 2.0;
|
||||
canvas.drawLine(
|
||||
Offset(center.dx, center.dy - radius * 0.62),
|
||||
Offset(center.dx, center.dy + radius * 0.18),
|
||||
linePaint,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
class ArabicSearchNormalizer {
|
||||
ArabicSearchNormalizer._();
|
||||
|
||||
static final RegExp _diacriticsRegExp = RegExp(
|
||||
r'[\u064B-\u065F\u0670\u0640]', // Harakat, Tanween, Shadda, Sukoon, Tatweel
|
||||
);
|
||||
|
||||
/// Normalizes Arabic text for flexible, fuzzy searching:
|
||||
/// - Strips diacritics / tashkeel
|
||||
/// - Normalizes all Alef forms (أ, إ, آ, ٱ) -> ا
|
||||
/// - Normalizes Alef Maksura (ى) -> ي
|
||||
/// - Normalizes Teh Marbuta (ة) -> ه
|
||||
/// - Normalizes Hamza on Waw (ؤ) -> و and Hamza on Yaa (ئ) -> ي
|
||||
/// - Lowercases English characters and trims spaces
|
||||
static String normalize(String text) {
|
||||
if (text.isEmpty) return '';
|
||||
|
||||
String cleaned = text
|
||||
.replaceAll(_diacriticsRegExp, '')
|
||||
.replaceAll(RegExp(r'[أإآٱ]'), 'ا')
|
||||
.replaceAll('ى', 'ي')
|
||||
.replaceAll('ة', 'ه')
|
||||
.replaceAll('ؤ', 'و')
|
||||
.replaceAll('ئ', 'ي')
|
||||
.toLowerCase()
|
||||
.trim();
|
||||
|
||||
// Collapse multiple whitespaces into a single space
|
||||
cleaned = cleaned.replaceAll(RegExp(r'\s+'), ' ');
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
/// Checks if [target] contains [query], normalizing both in Arabic and English.
|
||||
static bool matches(String target, String query) {
|
||||
final normTarget = normalize(target);
|
||||
final normQuery = normalize(query);
|
||||
|
||||
if (normQuery.isEmpty) return true;
|
||||
if (normTarget.isEmpty) return false;
|
||||
|
||||
if (normTarget.contains(normQuery)) return true;
|
||||
|
||||
// Split words in query and verify each token is present
|
||||
final queryWords = normQuery.split(' ').where((w) => w.isNotEmpty);
|
||||
if (queryWords.length > 1) {
|
||||
return queryWords.every((word) => normTarget.contains(word));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user