cyberhybridhub/lib/guid/guid_glyph_shape.dart
2026-06-03 04:21:42 -05:00

535 lines
16 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:math' as math;
import 'dart:typed_data';
import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
/// Deterministic 3D crystal mesh + palette derived from a question UUID.
///
/// Parses the canonical 16-byte UUID (hyphens optional), builds a perturbed
/// icosahedron with optional stellated facets, and paints it with depth-sorted
/// shaded faces so the same id always produces the same glyph.
class GuidGlyphShape {
GuidGlyphShape._(this.bytes);
final Uint8List bytes;
/// Builds a shape from [guid]. Non-UUID strings fall back to a stable hash.
factory GuidGlyphShape.fromGuid(String guid) {
final Uint8List? parsed = tryParseUuidBytes(guid);
if (parsed != null) {
return GuidGlyphShape._(parsed);
}
return GuidGlyphShape._(_hashTo16Bytes(guid));
}
static const double _phi = 1.618033988749895;
static final List<_Vec3> _icosahedronBase = <_Vec3>[
_Vec3(-1, _phi, 0),
_Vec3(1, _phi, 0),
_Vec3(-1, -_phi, 0),
_Vec3(1, -_phi, 0),
_Vec3(0, -1, _phi),
_Vec3(0, 1, _phi),
_Vec3(0, -1, -_phi),
_Vec3(0, 1, -_phi),
_Vec3(_phi, 0, -1),
_Vec3(_phi, 0, 1),
_Vec3(-_phi, 0, -1),
_Vec3(-_phi, 0, 1),
].map((v) => v.normalized(scale: 0.88)).toList(growable: false);
static const List<List<int>> _icosahedronFaces = <List<int>>[
<int>[0, 11, 5],
<int>[0, 5, 1],
<int>[0, 1, 7],
<int>[0, 7, 10],
<int>[0, 10, 11],
<int>[1, 5, 9],
<int>[5, 11, 4],
<int>[11, 10, 2],
<int>[10, 7, 6],
<int>[7, 1, 8],
<int>[3, 9, 4],
<int>[3, 4, 2],
<int>[3, 2, 6],
<int>[3, 6, 8],
<int>[3, 8, 9],
<int>[4, 9, 5],
<int>[2, 4, 11],
<int>[6, 2, 10],
<int>[8, 6, 7],
<int>[9, 8, 1],
];
double _byte(int index) => bytes[index % 16] / 255;
/// Base orientation baked into every guid (before slider warp).
({double x, double y, double z}) get _baseEuler => (
x: (_byte(1) - 0.5) * math.pi * 0.9,
y: (_byte(2) - 0.5) * math.pi * 2,
z: (_byte(3) - 0.5) * math.pi * 0.55,
);
_Vec3 get _lightDirection => _Vec3(
0.25 + (_byte(10) - 0.5) * 0.5,
-0.45 + (_byte(11) - 0.5) * 0.35,
0.65 + (_byte(12) - 0.5) * 0.4,
).normalized();
/// Isometric projection mix from guid bytes.
({double xz, double lift}) get _projection => (
xz: 0.42 + _byte(13) * 0.22,
lift: 0.28 + _byte(14) * 0.18,
);
List<_Vec3> _unitVerticesRaw() {
return List<_Vec3>.generate(12, (int i) {
final _Vec3 base = _icosahedronBase[i];
final double radial =
0.76 + _byte(i) * 0.42 + (_byte(i + 7) - 0.5) * 0.14;
final _Vec3 wobble = _Vec3(
(_byte(i + 3) - 0.5) * 0.22,
(_byte(i + 5) - 0.5) * 0.22,
(_byte(i + 9) - 0.5) * 0.22,
);
return (base + wobble).normalized(scale: radial);
});
}
_Vec3 _rotateEuler(_Vec3 v, ({double x, double y, double z}) euler) {
var out = v.rotateX(euler.x).rotateY(euler.y).rotateZ(euler.z);
return out;
}
/// Stellated icosahedron: some faces gain a pyramid apex for extra complexity.
({List<_Vec3> vertices, List<_MeshFace> faces}) _buildMesh(
List<_Vec3> vertices,
List<List<int>> triangles,
) {
final List<_Vec3> verts = List<_Vec3>.from(vertices);
final List<_MeshFace> faces = <_MeshFace>[
for (int fi = 0; fi < triangles.length; fi++)
_MeshFace(
fi,
triangles[fi][0],
triangles[fi][1],
triangles[fi][2],
),
];
for (int fi = 0; fi < triangles.length; fi++) {
if ((bytes[(fi + 4) % 16] % 3) == 0) {
continue;
}
final List<int> tri = triangles[fi];
final _Vec3 v0 = verts[tri[0]];
final _Vec3 v1 = verts[tri[1]];
final _Vec3 v2 = verts[tri[2]];
final _Vec3 centroid = (v0 + v1 + v2) * (1 / 3);
final _Vec3 normal = _Vec3.cross(v1 - v0, v2 - v0).normalized();
final double spike = 0.18 + _byte(fi + 6) * 0.38;
final int apex = verts.length;
verts.add(centroid + normal * spike);
faces[fi] = _MeshFace(fi, tri[0], tri[1], apex);
faces.add(_MeshFace(fi + 100, tri[1], tri[2], apex));
faces.add(_MeshFace(fi + 200, tri[2], tri[0], apex));
}
return (vertices: verts, faces: faces);
}
({List<_Vec3> vertices, List<_MeshFace> faces}) displayMesh(num displayValue) {
final double t = displayT(displayValue);
final double spinSign = (bytes[9] & 1) == 0 ? 1.0 : -1.0;
final ({double x, double y, double z}) euler = (
x: _baseEuler.x + t * (0.32 + _byte(4) * 0.28) * spinSign,
y: _baseEuler.y + t * (0.55 + _byte(5) * 0.35) * spinSign,
z: _baseEuler.z + t * (0.2 + _byte(6) * 0.18),
);
final ({List<_Vec3> vertices, List<_MeshFace> faces}) mesh =
_buildMesh(_unitVerticesRaw(), _icosahedronFaces);
final List<_Vec3> rotated = <_Vec3>[
for (final _Vec3 v in mesh.vertices) _rotateEuler(v, euler),
];
return (vertices: rotated, faces: mesh.faces);
}
/// Projects a unit-space vertex to 2D (isometric-style).
Offset _projectUnit(_Vec3 v) {
final ({double xz, double lift}) proj = _projection;
return Offset(
v.x - v.z * proj.xz,
v.y + v.z * proj.lift + v.x * proj.lift * 0.12,
);
}
List<Offset> displayUnitVertices(num displayValue) {
final ({List<_Vec3> vertices, List<_MeshFace> faces}) mesh =
displayMesh(displayValue);
return mesh.vertices.map(_projectUnit).toList(growable: false);
}
/// Fill color: warm accent band so glyphs stay on-brand but distinct.
Color fillColor() {
final double hue = 8 + ((bytes[2] << 8 | bytes[3]) / 65535) * 42;
return HSLColor.fromAHSL(1, hue, 0.72, 0.52).toColor();
}
Color strokeColor() =>
HSLColor.fromColor(fillColor()).withLightness(0.38).toColor();
/// Normalized slider position in [-1, 1] for [displayValue] in [-10, 10].
static double displayT(num displayValue) =>
(displayValue / 10).clamp(-1.0, 1.0).toDouble();
/// True when the slider is at neutral zero (no directional guess).
static bool isNeutralZero(num displayValue) => displayValue == 0;
/// 0 at zero, 1 at ±10.
static double displayIntensity(num displayValue) =>
isNeutralZero(displayValue) ? 0 : (displayValue.abs() / 10).clamp(0.0, 1.0);
static const Color _negativeAccent = Color(0xFFF87171);
static const Color _neutralBlend = Color(0xFF64748B);
/// Progressive green (+) / red () fill; crystal white at zero.
Color displayFillColor(num displayValue) {
if (isNeutralZero(displayValue)) {
return const Color(0xFFF8FCFF);
}
final double intensity = displayIntensity(displayValue);
final Color target =
displayValue > 0 ? AppColors.success : _negativeAccent;
return Color.lerp(fillColor(), target, 0.25 + intensity * 0.75)!;
}
Color displayStrokeColor(num displayValue) {
if (isNeutralZero(displayValue)) {
return const Color(0xFFE2E8F0);
}
final double intensity = displayIntensity(displayValue);
final Color target =
displayValue > 0 ? AppColors.success : _negativeAccent;
return Color.lerp(_neutralBlend, target, 0.35 + intensity * 0.65)!;
}
Color _shadeFaceColor(Color base, double shade, num displayValue) {
if (isNeutralZero(displayValue)) {
return Color.lerp(
const Color(0xFFE8EEF4),
const Color(0xFFF8FCFF),
shade,
)!;
}
final HSLColor hsl = HSLColor.fromColor(base);
return hsl
.withLightness((hsl.lightness * (0.55 + shade * 0.5)).clamp(0.18, 0.82))
.withSaturation((hsl.saturation * (0.75 + shade * 0.35)).clamp(0.2, 1))
.toColor();
}
/// Furthest 2D extent after projection (for glow sizing).
double displayMaxUnitRadius(num displayValue) {
double maxR = 0;
for (final Offset p in displayUnitVertices(displayValue)) {
maxR = math.max(maxR, p.distance);
}
return maxR > 0 ? maxR : 0.5;
}
/// Glow color for shadows (crystal white at zero, green +, red ).
Color displayGlowColor(num displayValue) {
if (isNeutralZero(displayValue)) {
return const Color(0xFFF8FCFF);
}
return displayValue > 0 ? AppColors.success : _negativeAccent;
}
double displayStrokeWidth(num displayValue) {
final double t = displayT(displayValue);
return (1.8 + t.abs() * 0.9 + (bytes[8] / 255) * 0.35).clamp(1.4, 3.2);
}
static Uint8List? tryParseUuidBytes(String guid) {
final String hex = guid.replaceAll('-', '').trim().toLowerCase();
if (hex.length != 32 || !RegExp(r'^[0-9a-f]{32}$').hasMatch(hex)) {
return null;
}
final Uint8List out = Uint8List(16);
for (int i = 0; i < 16; i++) {
out[i] = int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16);
}
return out;
}
static Uint8List _hashTo16Bytes(String input) {
var h1 = 0x811c9dc5;
var h2 = 0x01000193;
for (final int codeUnit in input.codeUnits) {
h1 = (h1 ^ codeUnit) * 0x01000193;
h2 = (h2 + codeUnit) * 0x85ebca6b;
}
final Uint8List out = Uint8List(16);
for (int i = 0; i < 16; i++) {
final int mix = (h1 >> (i % 16)) ^ (h2 >> ((i + 3) % 16)) ^ (i * 31);
out[i] = mix & 0xff;
}
return out;
}
}
class _Vec3 {
const _Vec3(this.x, this.y, this.z);
final double x;
final double y;
final double z;
double get length => math.sqrt(x * x + y * y + z * z);
_Vec3 normalized({double scale = 1}) {
final double len = length;
if (len < 1e-9) {
return _Vec3(0, scale, 0);
}
final double s = scale / len;
return _Vec3(x * s, y * s, z * s);
}
_Vec3 operator +(_Vec3 other) => _Vec3(x + other.x, y + other.y, z + other.z);
_Vec3 operator -(_Vec3 other) => _Vec3(x - other.x, y - other.y, z - other.z);
_Vec3 operator *(double scalar) => _Vec3(x * scalar, y * scalar, z * scalar);
_Vec3 rotateX(double angle) {
final double c = math.cos(angle);
final double s = math.sin(angle);
return _Vec3(x, y * c - z * s, y * s + z * c);
}
_Vec3 rotateY(double angle) {
final double c = math.cos(angle);
final double s = math.sin(angle);
return _Vec3(x * c + z * s, y, -x * s + z * c);
}
_Vec3 rotateZ(double angle) {
final double c = math.cos(angle);
final double s = math.sin(angle);
return _Vec3(x * c - y * s, x * s + y * c, z);
}
static double dot(_Vec3 a, _Vec3 b) => a.x * b.x + a.y * b.y + a.z * b.z;
static _Vec3 cross(_Vec3 a, _Vec3 b) => _Vec3(
a.y * b.z - a.z * b.y,
a.z * b.x - a.x * b.z,
a.x * b.y - a.y * b.x,
);
}
class _MeshFace {
const _MeshFace(this.id, this.i0, this.i1, this.i2);
final int id;
final int i0;
final int i1;
final int i2;
double averageDepth(List<_Vec3> vertices) =>
(vertices[i0].z + vertices[i1].z + vertices[i2].z) / 3;
_Vec3 normal(List<_Vec3> vertices) {
final _Vec3 v0 = vertices[i0];
final _Vec3 v1 = vertices[i1];
final _Vec3 v2 = vertices[i2];
return _Vec3.cross(v1 - v0, v2 - v0).normalized();
}
}
/// Painted glyph for a question id (replaces the fixed red dot).
class QuestionGuidGlyph extends StatelessWidget {
const QuestionGuidGlyph({
super.key,
required this.guid,
this.size = 80,
this.displayValue = 0,
});
final String guid;
final double size;
/// Slider value in [-10, 10]; warps the painted glyph for display only.
final num displayValue;
@override
Widget build(BuildContext context) {
final GuidGlyphShape shape = GuidGlyphShape.fromGuid(guid);
final double intensity = GuidGlyphShape.displayIntensity(displayValue);
final Color glow = shape.displayGlowColor(displayValue);
final double strokeWidth = shape.displayStrokeWidth(displayValue);
final double baseRadius = (size / 2 - strokeWidth - 2).clamp(18.0, size / 2);
final double paintedRadius = baseRadius * (1 + intensity * 0.12);
final double bodyDiameter = paintedRadius * 2;
final double blur = bodyDiameter * (0.2 + intensity * 0.16);
final double spread = bodyDiameter * (0.03 + intensity * 0.06);
final double glowAlpha = 0.38 + intensity * 0.42;
return SizedBox(
width: size,
height: size,
child: Center(
child: Container(
width: bodyDiameter,
height: bodyDiameter,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.transparent,
boxShadow: <BoxShadow>[
BoxShadow(
color: glow.withValues(alpha: glowAlpha),
blurRadius: blur,
spreadRadius: spread * 0.35,
),
BoxShadow(
color: glow.withValues(alpha: glowAlpha * 0.45),
blurRadius: blur * 1.75,
spreadRadius: spread,
),
],
),
child: CustomPaint(
painter: _GuidGlyphPainter(
shape: shape,
displayValue: displayValue,
paintedRadius: paintedRadius,
),
),
),
),
);
}
}
class _GuidGlyphPainter extends CustomPainter {
_GuidGlyphPainter({
required this.shape,
required this.displayValue,
required this.paintedRadius,
});
final GuidGlyphShape shape;
final num displayValue;
final double paintedRadius;
@override
void paint(Canvas canvas, Size size) {
final Offset center = Offset(size.width / 2, size.height / 2);
final double maxUnitR = shape.displayMaxUnitRadius(displayValue);
final double scale = paintedRadius / maxUnitR;
final double strokeWidth = shape.displayStrokeWidth(displayValue);
final ({List<_Vec3> vertices, List<_MeshFace> faces}) mesh =
shape.displayMesh(displayValue);
final List<Offset> projected = mesh.vertices
.map((v) => center + shape._projectUnit(v) * scale)
.toList(growable: false);
final List<_MeshFace> sorted = List<_MeshFace>.from(mesh.faces)
..sort(
(a, b) =>
a.averageDepth(mesh.vertices).compareTo(b.averageDepth(mesh.vertices)),
);
final Color baseFill = shape.displayFillColor(displayValue);
final _Vec3 light = shape._lightDirection;
for (final _MeshFace face in sorted) {
final _Vec3 normal = face.normal(mesh.vertices);
final double ndotl =
_Vec3.dot(normal, light).clamp(-1.0, 1.0) * 0.5 + 0.5;
final double shade = (0.28 + ndotl * 0.72).clamp(0.22, 1.0);
final Path path = Path()
..moveTo(projected[face.i0].dx, projected[face.i0].dy)
..lineTo(projected[face.i1].dx, projected[face.i1].dy)
..lineTo(projected[face.i2].dx, projected[face.i2].dy)
..close();
canvas.drawPath(
path,
Paint()
..color = shape._shadeFaceColor(baseFill, shade, displayValue)
..style = PaintingStyle.fill,
);
}
final Color edgeColor = shape.displayStrokeColor(displayValue);
final Paint edgePaint = Paint()
..color = edgeColor.withValues(alpha: GuidGlyphShape.isNeutralZero(displayValue) ? 0.55 : 0.75)
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeJoin = StrokeJoin.round;
final Set<String> drawnEdges = <String>{};
for (final _MeshFace face in mesh.faces) {
_strokeEdge(canvas, projected, edgePaint, drawnEdges, face.i0, face.i1);
_strokeEdge(canvas, projected, edgePaint, drawnEdges, face.i1, face.i2);
_strokeEdge(canvas, projected, edgePaint, drawnEdges, face.i2, face.i0);
}
// Specular highlight on the front-most face.
if (sorted.isNotEmpty) {
final _MeshFace front = sorted.last;
final Offset highlight = Offset(
(projected[front.i0].dx +
projected[front.i1].dx +
projected[front.i2].dx) /
3,
(projected[front.i0].dy +
projected[front.i1].dy +
projected[front.i2].dy) /
3,
);
canvas.drawCircle(
highlight,
(scale * 0.09).clamp(2.0, 6.0),
Paint()
..color = Colors.white.withValues(
alpha: GuidGlyphShape.isNeutralZero(displayValue) ? 0.35 : 0.28,
),
);
}
}
void _strokeEdge(
Canvas canvas,
List<Offset> projected,
Paint paint,
Set<String> drawnEdges,
int a,
int b,
) {
final int lo = a < b ? a : b;
final int hi = a < b ? b : a;
final String key = '$lo-$hi';
if (!drawnEdges.add(key)) {
return;
}
canvas.drawLine(projected[lo], projected[hi], paint);
}
@override
bool shouldRepaint(covariant _GuidGlyphPainter oldDelegate) =>
oldDelegate.shape.bytes != shape.bytes ||
oldDelegate.displayValue != displayValue ||
oldDelegate.paintedRadius != paintedRadius;
}