Files
999/lib/screens/matrix_geometry_screen.dart

968 lines
34 KiB
Dart

import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/houses_session_model.dart';
import 'symbol_detail_screen.dart';
enum GeometryViewMode {
constellation, // Sacred polygon / constellation of chosen houses
allChains, // All chains overlaid
singleChain, // Focus on one specific chain
}
class MatrixGeometryScreen extends StatefulWidget {
final HousesSession session;
final int? initialChainIndex; // Optional focus on chain 1..N
const MatrixGeometryScreen({
super.key,
required this.session,
this.initialChainIndex,
});
@override
State<MatrixGeometryScreen> createState() => _MatrixGeometryScreenState();
}
class _MatrixGeometryScreenState extends State<MatrixGeometryScreen>
with SingleTickerProviderStateMixin {
final TransformationController _transformController = TransformationController();
late GeometryViewMode _viewMode;
int _selectedChainIndex = 1; // 1-based index
late AnimationController _animController;
static const double kCellSize = 64.0;
static const double kCellMargin = 3.0;
static const double kHeaderHeight = 72.0;
static const List<Color> kChainColors = [
Color(0xFF00E5FF), // Cyan
Color(0xFFFF4081), // Pink / Magenta
Color(0xFFFFD54F), // Gold
Color(0xFFB388FF), // Purple
Color(0xFF00E676), // Emerald
Color(0xFFFF6E40), // Coral
Color(0xFF448AFF), // Electric Blue
Color(0xFFEEFF41), // Lime
Color(0xFFFF80AB), // Rose Gold
];
@override
void initState() {
super.initState();
_animController = AnimationController(
vsync: this,
duration: const Duration(seconds: 4),
)..repeat();
if (widget.initialChainIndex != null &&
widget.initialChainIndex! > 0 &&
widget.initialChainIndex! <= widget.session.chains.length) {
_viewMode = GeometryViewMode.singleChain;
_selectedChainIndex = widget.initialChainIndex!;
} else {
_viewMode = GeometryViewMode.constellation;
}
_transformController.value = Matrix4.diagonal3Values(0.52, 0.52, 1.0);
}
@override
void dispose() {
_animController.dispose();
_transformController.dispose();
super.dispose();
}
void _resetZoom() {
HapticFeedback.lightImpact();
setState(() {
_transformController.value = Matrix4.diagonal3Values(0.52, 0.52, 1.0);
});
}
String _getFigureName(int pointsCount) {
switch (pointsCount) {
case 1:
return 'Монада (Точка Первоистока)';
case 2:
return 'Диада (Вектор Полярности)';
case 3:
return 'Триада (Дельта Творения)';
case 4:
return 'Тетрада (Земной Четверик)';
case 5:
return 'Пентаграмма (Узел Пяти Стихий)';
case 6:
return 'Гексаграмма (Печать Равновесия)';
case 7:
return 'Гептада (Семизвездный Венец)';
case 8:
return 'Октада (Роза Пространства)';
case 9:
return 'Эннеаграмма (Девятикратное Полотно Судьбы)';
default:
return 'Сакральная Геометрия ($pointsCount узлов)';
}
}
@override
Widget build(BuildContext context) {
const totalWidth = 30 * (kCellSize + kCellMargin * 2);
const totalHeight = 33 * (kCellSize + kCellMargin * 2) + kHeaderHeight;
final selectedChain = widget.session.chains.isNotEmpty &&
_selectedChainIndex <= widget.session.chains.length
? widget.session.chains[_selectedChainIndex - 1]
: null;
return Scaffold(
backgroundColor: const Color(0xFF0B0D18),
appBar: AppBar(
backgroundColor: const Color(0xFF101323),
elevation: 0,
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Геометрия Матрицы 33×30',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
Text(
_viewMode == GeometryViewMode.constellation
? _getFigureName(widget.session.selectedHouses.length)
: (_viewMode == GeometryViewMode.allChains
? 'Все Цепочки Домов (${widget.session.chains.length})'
: 'Цепочка #$_selectedChainIndex: ${selectedChain?.endTypeTitle ?? ""}'),
style: const TextStyle(
fontSize: 11,
color: Color(0xFFFFD54F),
letterSpacing: 0.3,
),
),
],
),
actions: [
IconButton(
tooltip: 'Центрировать масштаб',
icon: const Icon(Icons.center_focus_strong_rounded, color: Color(0xFF90CAF9)),
onPressed: _resetZoom,
),
IconButton(
tooltip: 'Справка по геометрии',
icon: const Icon(Icons.info_outline_rounded, color: Color(0xFFCFD8DC)),
onPressed: () => _showHelpDialog(context),
),
],
),
body: Stack(
children: [
// 1. Zoomable & Pannable Matrix Canvas
InteractiveViewer(
transformationController: _transformController,
constrained: false,
minScale: 0.15,
maxScale: 2.5,
boundaryMargin: const EdgeInsets.all(500),
child: SizedBox(
width: totalWidth,
height: totalHeight,
child: Stack(
children: [
_buildBaseGrid(totalWidth, totalHeight),
AnimatedBuilder(
animation: _animController,
builder: (context, child) {
return CustomPaint(
size: Size(totalWidth, totalHeight),
painter: _MatrixGeometryPainter(
session: widget.session,
viewMode: _viewMode,
selectedChainIndex: _selectedChainIndex,
pulseValue: _animController.value,
chainColors: kChainColors,
),
);
},
),
],
),
),
),
// 2. Top Control Filter Bar (Modes & Chains selector)
Positioned(
top: 12,
left: 16,
right: 16,
child: _buildModeSelectorBar(),
),
// 3. Bottom Information Card
Positioned(
bottom: 16,
left: 16,
right: 16,
child: _buildBottomInfoCard(selectedChain),
),
],
),
);
}
Widget _buildModeSelectorBar() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xE613162A),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF2C3258), width: 0.8),
boxShadow: const [
BoxShadow(
color: Color(0x66000000),
blurRadius: 16,
offset: Offset(0, 6),
),
],
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
_buildModeChip(
icon: Icons.auto_awesome_rounded,
label: 'Фигура (${widget.session.selectedHouses.length})',
isSelected: _viewMode == GeometryViewMode.constellation,
activeColor: const Color(0xFFFFD54F),
onTap: () {
HapticFeedback.selectionClick();
setState(() => _viewMode = GeometryViewMode.constellation);
},
),
const SizedBox(width: 6),
_buildModeChip(
icon: Icons.hub_rounded,
label: 'Все Цепочки',
isSelected: _viewMode == GeometryViewMode.allChains,
activeColor: const Color(0xFF00E5FF),
onTap: () {
HapticFeedback.selectionClick();
setState(() => _viewMode = GeometryViewMode.allChains);
},
),
const SizedBox(width: 8),
Container(width: 1, height: 24, color: const Color(0xFF2C3258)),
const SizedBox(width: 8),
for (int i = 1; i <= widget.session.chains.length; i++) ...[
_buildChainChip(i),
const SizedBox(width: 6),
],
],
),
),
);
}
Widget _buildModeChip({
required IconData icon,
required String label,
required bool isSelected,
required Color activeColor,
required VoidCallback onTap,
}) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(10),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: isSelected ? activeColor.withValues(alpha: 0.22) : Colors.transparent,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isSelected ? activeColor : const Color(0xFF2C3258),
width: isSelected ? 1.2 : 0.6,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: isSelected ? activeColor : const Color(0xFF90A4AE)),
const SizedBox(width: 5),
Text(
label,
style: TextStyle(
color: isSelected ? Colors.white : const Color(0xFF90A4AE),
fontWeight: isSelected ? FontWeight.bold : FontWeight.w500,
fontSize: 12,
),
),
],
),
),
),
);
}
Widget _buildChainChip(int index) {
final isSelected = _viewMode == GeometryViewMode.singleChain && _selectedChainIndex == index;
final color = kChainColors[(index - 1) % kChainColors.length];
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
HapticFeedback.selectionClick();
setState(() {
_viewMode = GeometryViewMode.singleChain;
_selectedChainIndex = index;
});
},
borderRadius: BorderRadius.circular(10),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 6),
decoration: BoxDecoration(
color: isSelected ? color.withValues(alpha: 0.24) : Colors.transparent,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isSelected ? color : const Color(0xFF2C3258),
width: isSelected ? 1.2 : 0.6,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 7,
height: 7,
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
),
const SizedBox(width: 5),
Text(
'#$index',
style: TextStyle(
color: isSelected ? Colors.white : const Color(0xFFB0BEC5),
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
fontSize: 12,
),
),
],
),
),
),
);
}
Widget _buildBottomInfoCard(HouseChain? selectedChain) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xF2121528),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: const Color(0xFF2E355F), width: 0.8),
boxShadow: const [
BoxShadow(
color: Color(0x77000000),
blurRadius: 20,
offset: Offset(0, 8),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.session.querentWord != null) ...[
Row(
children: [
const Icon(Icons.stars_rounded, color: Color(0xFFFFD54F), size: 15),
const SizedBox(width: 5),
Text(
'Кверент: «${widget.session.querentWord!.word.toUpperCase()}» (Дом #${(widget.session.querentHouseIndex! + 1).toString().padLeft(3, "0")})',
style: const TextStyle(
color: Color(0xFFFFD54F),
fontSize: 11.5,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 6),
],
Row(
children: [
if (_viewMode == GeometryViewMode.constellation) ...[
const Icon(Icons.star_rounded, color: Color(0xFFFFD54F), size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
_getFigureName(widget.session.selectedHouses.length),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: const Color(0x33FFD54F),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFFFD54F), width: 0.6),
),
child: Text(
'${widget.session.selectedHouses.length} узлов',
style: const TextStyle(
color: Color(0xFFFFD54F),
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
] else if (_viewMode == GeometryViewMode.allChains) ...[
const Icon(Icons.hub_rounded, color: Color(0xFF00E5FF), size: 20),
const SizedBox(width: 8),
const Expanded(
child: Text(
'Сводная Паутина Судеб (Все Цепочки)',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
),
Text(
'${widget.session.chains.length} лучей',
style: const TextStyle(
color: Color(0xFF00E5FF),
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
] else if (selectedChain != null) ...[
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: kChainColors[(_selectedChainIndex - 1) % kChainColors.length],
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
'Цепочка #$_selectedChainIndex: ${selectedChain.endTypeTitle}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: selectedChain.statusColor.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: selectedChain.statusColor, width: 0.6),
),
child: Text(
'${selectedChain.steps.length} звеньев',
style: TextStyle(
color: selectedChain.statusColor,
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
],
],
),
const SizedBox(height: 8),
Text(
_viewMode == GeometryViewMode.constellation
? 'Выбранные дома образуют сакральный сигил на полотне 33×30. Линии соединяют дома в порядке выпадения жребия, создавая геометрию пространства расклада.'
: (_viewMode == GeometryViewMode.allChains
? 'Трассировка движения импульсов от каждого открытого дома (до 9 звеньев). Узоры переплетаются сквозь матрицу, выявляя пересечения и фокусы судьбы.'
: (selectedChain?.description ?? '')),
style: const TextStyle(
color: Color(0xFFCFD8DC),
fontSize: 12.5,
height: 1.4,
),
),
],
),
);
}
Widget _buildBaseGrid(double totalWidth, double totalHeight) {
final chosenIndices = {
for (final h in widget.session.selectedHouses) h.houseIndex: h
};
return Column(
children: [
SizedBox(
height: kHeaderHeight,
child: Row(
children: List.generate(10, (sectorIdx) {
final sector = widget.session.sectors[sectorIdx];
final triadWidth = 3 * (kCellSize + kCellMargin * 2);
return SizedBox(
width: triadWidth,
height: kHeaderHeight,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 4),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
const Color(0xFF1B1F38),
sector.color.withValues(alpha: 0.18),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: sector.color.withValues(alpha: 0.5), width: 0.8),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'АРХОНТ ${sectorIdx + 1}',
style: TextStyle(
color: sector.color,
fontSize: 9,
fontWeight: FontWeight.bold,
letterSpacing: 0.8,
),
),
const SizedBox(height: 2),
Text(
sector.archonWord.word.toUpperCase(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
);
}),
),
),
for (int r = 0; r < 33; r++)
Row(
children: [
for (int c = 0; c < 30; c++)
_buildCellWidget(r, c, chosenIndices[r * 30 + c]),
],
),
],
);
}
Widget _buildCellWidget(int row, int col, HouseCell? chosenCell) {
final isChosen = chosenCell != null;
final cell = widget.session.grid[row * 30 + col];
final isQuerent = widget.session.querentHouseIndex != null && cell.houseIndex == widget.session.querentHouseIndex;
return Container(
width: kCellSize,
height: kCellSize,
margin: const EdgeInsets.all(kCellMargin),
decoration: BoxDecoration(
color: isQuerent
? const Color(0xFF381E52)
: (isChosen
? const Color(0xFF2A224D)
: const Color(0xFF101323).withValues(alpha: 0.75)),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isQuerent
? const Color(0xFFFFD54F)
: (isChosen
? const Color(0xFFFFD54F)
: const Color(0xFF1E223D).withValues(alpha: 0.8)),
width: isQuerent ? 2.2 : (isChosen ? 1.5 : 0.6),
),
boxShadow: isQuerent
? [
BoxShadow(
color: const Color(0xFFFFD54F).withValues(alpha: 0.6),
blurRadius: 12,
spreadRadius: 1,
),
BoxShadow(
color: const Color(0xFFB388FF).withValues(alpha: 0.4),
blurRadius: 16,
spreadRadius: 2,
),
]
: (isChosen
? [
BoxShadow(
color: const Color(0xFFFFD54F).withValues(alpha: 0.35),
blurRadius: 10,
),
]
: null),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(10),
onTap: () {
HapticFeedback.lightImpact();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SymbolDetailScreen(word: cell.houseWord),
),
);
},
child: Padding(
padding: const EdgeInsets.all(3.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (isQuerent) ...[
Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
decoration: BoxDecoration(
color: const Color(0xFFFFD54F),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'👑 КВЕРЕНТ',
style: TextStyle(
color: Colors.black,
fontSize: 7.5,
fontWeight: FontWeight.w900,
),
),
),
const SizedBox(height: 2),
] else if (isChosen) ...[
Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
decoration: BoxDecoration(
color: const Color(0xFFFFD54F),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'#${chosenCell.selectionOrder}',
style: const TextStyle(
color: Colors.black,
fontSize: 9,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 2),
],
Text(
cell.houseWord.word,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: isChosen ? Colors.white : const Color(0xFF6B7280),
fontSize: 10.5,
fontWeight: isChosen ? FontWeight.bold : FontWeight.normal,
),
),
Text(
'#${(cell.houseIndex + 1).toString().padLeft(3, "0")}',
style: TextStyle(
color: isChosen
? const Color(0xFFFFD54F)
: const Color(0xFF374151),
fontSize: 8.5,
),
),
],
),
),
),
),
);
}
void _showHelpDialog(BuildContext context) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: const Color(0xFF15182D),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
title: const Row(
children: [
Icon(Icons.auto_awesome_mosaic_rounded, color: Color(0xFFFFD54F)),
SizedBox(width: 8),
Text('Сакральная Геометрия', style: TextStyle(color: Colors.white, fontSize: 17)),
],
),
content: const SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Полотно 33×30 образует единое координатное поле оракула из 990 домов и 10 архонтов.',
style: TextStyle(color: Color(0xFFCFD8DC), fontSize: 13, height: 1.4),
),
SizedBox(height: 12),
Text(
'• Фигура Расклада: золотые лучи соединяют открытые дома в порядке жребия, создавая сигил ситуации.',
style: TextStyle(color: Color(0xFFFFD54F), fontSize: 12.5, height: 1.4),
),
SizedBox(height: 8),
Text(
'• Цепочки Домов: неоновые векторы трассируют путь гостя каждого дома (до 9 звеньев). При замыкании возникает «Петля Судьбы», при попадании в имя Архонта — луч восходит к Небесному Престолу.',
style: TextStyle(color: Color(0xFF00E5FF), fontSize: 12.5, height: 1.4),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Понятно', style: TextStyle(color: Color(0xFFFFD54F))),
),
],
),
);
}
}
class _MatrixGeometryPainter extends CustomPainter {
final HousesSession session;
final GeometryViewMode viewMode;
final int selectedChainIndex;
final double pulseValue;
final List<Color> chainColors;
static const double kCellSize = 64.0;
static const double kCellMargin = 3.0;
static const double kHeaderHeight = 72.0;
_MatrixGeometryPainter({
required this.session,
required this.viewMode,
required this.selectedChainIndex,
required this.pulseValue,
required this.chainColors,
});
Offset _getCellCenter(int row, int col) {
final x = col * (kCellSize + kCellMargin * 2) + kCellMargin + kCellSize / 2;
final y = kHeaderHeight + row * (kCellSize + kCellMargin * 2) + kCellMargin + kCellSize / 2;
return Offset(x, y);
}
Offset _getArchonCenter(int sectorIndex) {
final startX = (sectorIndex * 3) * (kCellSize + kCellMargin * 2);
final width = 3 * (kCellSize + kCellMargin * 2);
return Offset(startX + width / 2, kHeaderHeight / 2);
}
@override
void paint(Canvas canvas, Size size) {
if (viewMode == GeometryViewMode.constellation) {
_paintConstellation(canvas);
} else if (viewMode == GeometryViewMode.allChains) {
for (int i = 0; i < session.chains.length; i++) {
_paintChain(canvas, session.chains[i], i + 1, false);
}
} else {
if (selectedChainIndex > 0 && selectedChainIndex <= session.chains.length) {
_paintChain(canvas, session.chains[selectedChainIndex - 1], selectedChainIndex, true);
}
}
_paintQuerentAnchor(canvas);
}
void _paintQuerentAnchor(Canvas canvas) {
if (session.querentHouseIndex == null) return;
final qRow = session.querentHouseIndex! ~/ 30;
final qCol = session.querentHouseIndex! % 30;
final qCenter = _getCellCenter(qRow, qCol);
final wavePaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color(0xFFFFD54F).withValues(alpha: 0.55 + 0.25 * math.sin(pulseValue * math.pi * 2));
canvas.drawCircle(qCenter, 22.0 + 4.0 * math.sin(pulseValue * math.pi * 2), wavePaint);
final auraPaint = Paint()
..style = PaintingStyle.fill
..color = const Color(0xFFB388FF).withValues(alpha: 0.20 + 0.08 * math.cos(pulseValue * math.pi * 2));
canvas.drawCircle(qCenter, 30.0, auraPaint);
if (viewMode == GeometryViewMode.constellation) {
final tetherPaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1.4
..color = const Color(0xFFFFD54F).withValues(alpha: 0.35 + 0.15 * math.sin(pulseValue * math.pi * 2));
for (final h in session.selectedHouses) {
final hCenter = _getCellCenter(h.row, h.col);
canvas.drawLine(qCenter, hCenter, tetherPaint);
}
}
final coreGlow = Paint()
..style = PaintingStyle.fill
..color = const Color(0xFFFFD54F);
canvas.drawCircle(qCenter, 6.5, coreGlow);
canvas.drawCircle(qCenter, 2.5, Paint()..color = Colors.white);
}
void _paintConstellation(Canvas canvas) {
final houses = List<HouseCell>.from(session.selectedHouses)
..sort((a, b) => (a.selectionOrder ?? 0).compareTo(b.selectionOrder ?? 0));
if (houses.isEmpty) return;
final points = houses.map((h) => _getCellCenter(h.row, h.col)).toList();
if (points.length >= 3) {
final polyPath = Path()..moveTo(points.first.dx, points.first.dy);
for (int i = 1; i < points.length; i++) {
polyPath.lineTo(points[i].dx, points[i].dy);
}
polyPath.close();
final fillPaint = Paint()
..style = PaintingStyle.fill
..color = const Color(0xFFFFD54F).withValues(alpha: 0.10 + 0.04 * math.sin(pulseValue * math.pi * 2));
canvas.drawPath(polyPath, fillPaint);
}
final glowPaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 6.0 + 1.5 * math.sin(pulseValue * math.pi * 2)
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..color = const Color(0xFFFFD54F).withValues(alpha: 0.35);
final corePaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.8
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..color = const Color(0xFFFFE082);
final linePath = Path()..moveTo(points.first.dx, points.first.dy);
for (int i = 1; i < points.length; i++) {
linePath.lineTo(points[i].dx, points[i].dy);
}
if (points.length >= 3) {
linePath.close();
}
canvas.drawPath(linePath, glowPaint);
canvas.drawPath(linePath, corePaint);
for (int i = 0; i < points.length; i++) {
final p = points[i];
final nodeGlow = Paint()
..style = PaintingStyle.fill
..color = const Color(0xFFFFD54F).withValues(alpha: 0.4 + 0.2 * math.sin(pulseValue * math.pi * 2 + i));
canvas.drawCircle(p, 12.0, nodeGlow);
final nodeCore = Paint()
..style = PaintingStyle.fill
..color = Colors.white;
canvas.drawCircle(p, 5.0, nodeCore);
}
}
void _paintChain(Canvas canvas, HouseChain chain, int chainIdx, bool isFocused) {
if (chain.steps.isEmpty) return;
final color = chainColors[(chainIdx - 1) % chainColors.length];
final glowWidth = isFocused ? 7.0 : 4.0;
final coreWidth = isFocused ? 3.0 : 2.0;
final glowPaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = glowWidth
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..color = color.withValues(alpha: isFocused ? 0.45 : 0.25);
final corePaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = coreWidth
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..color = color;
final points = <Offset>[];
for (final step in chain.steps) {
points.add(_getCellCenter(step.house.row, step.house.col));
}
final path = Path()..moveTo(points.first.dx, points.first.dy);
for (int i = 1; i < points.length; i++) {
path.lineTo(points[i].dx, points[i].dy);
}
if (chain.endType == ChainEndType.archonThrone && chain.steps.last.destinationArchon != null) {
final archonCenter = _getArchonCenter(chain.steps.last.destinationArchon!.index);
path.lineTo(archonCenter.dx, archonCenter.dy);
points.add(archonCenter);
}
if (chain.endType == ChainEndType.loop && points.length >= 2) {
final lastP = points.last;
final haloPaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 3.0
..color = const Color(0xFFFF5252);
canvas.drawCircle(lastP, 18.0 + 4.0 * math.sin(pulseValue * math.pi * 2), haloPaint);
}
canvas.drawPath(path, glowPaint);
canvas.drawPath(path, corePaint);
for (int i = 0; i < points.length; i++) {
final p = points[i];
final isStart = i == 0;
final isEnd = i == points.length - 1;
final nodePaint = Paint()
..style = PaintingStyle.fill
..color = isStart
? const Color(0xFFFFD54F)
: (isEnd ? const Color(0xFFFF5252) : color);
canvas.drawCircle(p, isStart || isEnd ? 6.5 : 4.0, nodePaint);
final centerDot = Paint()
..style = PaintingStyle.fill
..color = Colors.white;
canvas.drawCircle(p, isStart || isEnd ? 2.5 : 1.5, centerDot);
}
}
@override
bool shouldRepaint(covariant _MatrixGeometryPainter oldDelegate) {
return true;
}
}