From a3510939d5163ab546cec4b5064fdc18fd4daf79 Mon Sep 17 00:00:00 2001 From: digua Date: Tue, 30 Jun 2026 23:03:26 +0800 Subject: [PATCH] feat(relationships): enrich 3d galaxy colors --- .../RelationshipGalaxyThreeCanvas.vue | 41 ++++++++++---- ...relationship-galaxy-3d-edge-colors.test.ts | 54 +++++++++++++++++++ .../relationship-galaxy-3d-edge-colors.ts | 36 +++++++++++++ .../relationship-galaxy-3d-scene.test.ts | 48 +++++++++++++---- .../relationship-galaxy-3d-scene.ts | 10 ++-- 5 files changed, 166 insertions(+), 23 deletions(-) create mode 100644 src/pages/people/relationships/relationship-galaxy-3d-edge-colors.test.ts create mode 100644 src/pages/people/relationships/relationship-galaxy-3d-edge-colors.ts diff --git a/src/pages/people/relationships/components/RelationshipGalaxyThreeCanvas.vue b/src/pages/people/relationships/components/RelationshipGalaxyThreeCanvas.vue index e8fcf075..2165d16f 100644 --- a/src/pages/people/relationships/components/RelationshipGalaxyThreeCanvas.vue +++ b/src/pages/people/relationships/components/RelationshipGalaxyThreeCanvas.vue @@ -10,6 +10,10 @@ import { type RelationshipGalaxy3DScene, } from '../relationship-galaxy-3d-scene' import { buildRelationshipGalaxy3DEdgeCurvePoints } from '../relationship-galaxy-3d-edge-path' +import { + setRelationshipGalaxy3DEdgeGradientColor, + type RelationshipGalaxy3DEdgeRenderBucket, +} from '../relationship-galaxy-3d-edge-colors' import { buildRelationshipVisibleLabelKeys } from '../relationship-galaxy-connections' import { applyRelationshipGalaxy3DSafeArea, @@ -189,8 +193,9 @@ function addThinEdgePaths( depthWrite: false, }) - const color = new THREE.Color() - const edgeTint = new THREE.Color(0xc4a16a) + const sourceColor = new THREE.Color() + const targetColor = new THREE.Color() + const vertexColor = new THREE.Color() const positions: number[] = [] const colors: number[] = [] @@ -198,15 +203,16 @@ function addThinEdgePaths( const source = scenePosition(edge.source, model) const target = scenePosition(edge.target, model) const points = buildRelationshipGalaxy3DEdgeCurvePoints(source, target, edge.source.seed + edge.target.seed) - color.setHex(edge.color) - color.lerp(edgeTint, bucket === 'highlight' ? 0.18 : bucket === 'normal' ? 0.38 : 0.5) - color.multiplyScalar(bucket === 'highlight' ? 0.95 : 0.48) + const stepCount = Math.max(1, points.length - 1) + sourceColor.setHex(edge.source.color) + targetColor.setHex(edge.target.color) for (let index = 0; index < points.length - 1; index += 1) { const current = points[index] const next = points[index + 1] positions.push(current.x, current.y, current.z, next.x, next.y, next.z) - colors.push(color.r, color.g, color.b, color.r, color.g, color.b) + pushEdgeGradientColor(colors, vertexColor, sourceColor, targetColor, bucket, index / stepCount) + pushEdgeGradientColor(colors, vertexColor, sourceColor, targetColor, bucket, (index + 1) / stepCount) } } @@ -224,6 +230,18 @@ function addThinEdgePaths( edgeGroup.add(line) } +function pushEdgeGradientColor( + colors: number[], + vertexColor: THREE.Color, + sourceColor: THREE.Color, + targetColor: THREE.Color, + bucket: RelationshipGalaxy3DEdgeRenderBucket, + progress: number +) { + setRelationshipGalaxy3DEdgeGradientColor(vertexColor, sourceColor, targetColor, bucket, progress) + colors.push(vertexColor.r, vertexColor.g, vertexColor.b) +} + function groupEdgesByWidth(edges: RelationshipGalaxy3DEdge[], bucket: 'dim' | 'normal' | 'highlight') { const groups = new Map() for (const edge of edges) { @@ -690,9 +708,9 @@ function getStarTexture(colorValue: number, variant: number): THREE.CanvasTextur const rayAlpha = 0.18 + pseudoRandom(variant + 21.7) * 0.12 drawEllipticDisc(context, xScale, yScale, rotation, 38, [ - [0, 'rgba(255,255,255,0.16)'], - [0.26, toRgb(edgeColor, 0.2)], - [0.58, toRgb(edgeColor, 0.06)], + [0, 'rgba(255,255,255,0.1)'], + [0.26, toRgb(edgeColor, 0.16)], + [0.58, toRgb(edgeColor, 0.04)], [1, 'rgba(255,255,255,0)'], ]) @@ -702,8 +720,9 @@ function getStarTexture(colorValue: number, variant: number): THREE.CanvasTextur drawEllipticDisc(context, xScale, yScale, rotation, 18, [ [0, 'rgba(255,255,255,1)'], - [0.36, 'rgba(255,255,248,0.98)'], - [0.68, toRgb(edgeColor, 0.74)], + [0.28, 'rgba(255,255,255,0.94)'], + [0.54, toRgb(edgeColor, 0.95)], + [0.78, toRgb(edgeColor, 0.38)], [1, 'rgba(255,255,255,0)'], ]) diff --git a/src/pages/people/relationships/relationship-galaxy-3d-edge-colors.test.ts b/src/pages/people/relationships/relationship-galaxy-3d-edge-colors.test.ts new file mode 100644 index 00000000..5916a140 --- /dev/null +++ b/src/pages/people/relationships/relationship-galaxy-3d-edge-colors.test.ts @@ -0,0 +1,54 @@ +/** + * Run: pnpm test -- src/pages/people/relationships/relationship-galaxy-3d-edge-colors.test.ts + */ + +import assert from 'node:assert/strict' +import test from 'node:test' +import * as THREE from 'three' +import { setRelationshipGalaxy3DEdgeGradientColor } from './relationship-galaxy-3d-edge-colors' + +test('builds edge vertex colors as a gradient anchored to both endpoint node colors', () => { + const source = new THREE.Color(0xffcc00) + const target = new THREE.Color(0x00aaff) + const start = new THREE.Color() + const middle = new THREE.Color() + const end = new THREE.Color() + + setRelationshipGalaxy3DEdgeGradientColor(start, source, target, 'highlight', 0) + setRelationshipGalaxy3DEdgeGradientColor(middle, source, target, 'highlight', 0.5) + setRelationshipGalaxy3DEdgeGradientColor(end, source, target, 'highlight', 1) + + assert.ok(start.r > end.r, 'the source endpoint should keep more of the source red/yellow tone') + assert.ok(end.b > start.b, 'the target endpoint should keep more of the target blue tone') + assert.ok(Math.abs(middle.r - (start.r + end.r) / 2) < 0.001) + assert.ok(Math.abs(middle.g - (start.g + end.g) / 2) < 0.001) + assert.ok(Math.abs(middle.b - (start.b + end.b) / 2) < 0.001) +}) + +test('keeps normal and dim edge gradients visually quieter than highlighted edges', () => { + const source = new THREE.Color(0xffcc00) + const target = new THREE.Color(0x00aaff) + const highlighted = new THREE.Color() + const normal = new THREE.Color() + const dim = new THREE.Color() + + setRelationshipGalaxy3DEdgeGradientColor(highlighted, source, target, 'highlight', 0.25) + setRelationshipGalaxy3DEdgeGradientColor(normal, source, target, 'normal', 0.25) + setRelationshipGalaxy3DEdgeGradientColor(dim, source, target, 'dim', 0.25) + + assert.ok(normal.getHSL({ h: 0, s: 0, l: 0 }).l < highlighted.getHSL({ h: 0, s: 0, l: 0 }).l) + assert.ok(dim.getHSL({ h: 0, s: 0, l: 0 }).l < normal.getHSL({ h: 0, s: 0, l: 0 }).l) +}) + +test('keeps normal edge endpoint colors distinct instead of collapsing into warm yellow', () => { + const source = new THREE.Color(0x38bdf8) + const target = new THREE.Color(0xf472b6) + const start = new THREE.Color() + const end = new THREE.Color() + + setRelationshipGalaxy3DEdgeGradientColor(start, source, target, 'normal', 0) + setRelationshipGalaxy3DEdgeGradientColor(end, source, target, 'normal', 1) + + assert.ok(start.b - start.r > 0.2, 'the blue endpoint should still read as blue after quieting') + assert.ok(end.r - end.g > 0.08, 'the pink endpoint should still read as pink after quieting') +}) diff --git a/src/pages/people/relationships/relationship-galaxy-3d-edge-colors.ts b/src/pages/people/relationships/relationship-galaxy-3d-edge-colors.ts new file mode 100644 index 00000000..37cb6744 --- /dev/null +++ b/src/pages/people/relationships/relationship-galaxy-3d-edge-colors.ts @@ -0,0 +1,36 @@ +import * as THREE from 'three' + +export type RelationshipGalaxy3DEdgeRenderBucket = 'dim' | 'normal' | 'highlight' + +const EDGE_TINT = new THREE.Color(0xffffff) + +export function setRelationshipGalaxy3DEdgeGradientColor( + output: THREE.Color, + source: THREE.Color, + target: THREE.Color, + bucket: RelationshipGalaxy3DEdgeRenderBucket, + progress: number +): THREE.Color { + output.copy(source).lerp(target, clampUnit(progress)) + output.lerp(EDGE_TINT, getEdgeTintMix(bucket)) + output.multiplyScalar(getEdgeBrightness(bucket)) + return output +} + +function clampUnit(value: number): number { + if (value <= 0) return 0 + if (value >= 1) return 1 + return value +} + +function getEdgeTintMix(bucket: RelationshipGalaxy3DEdgeRenderBucket): number { + if (bucket === 'highlight') return 0.06 + if (bucket === 'normal') return 0.16 + return 0.26 +} + +function getEdgeBrightness(bucket: RelationshipGalaxy3DEdgeRenderBucket): number { + if (bucket === 'highlight') return 0.95 + if (bucket === 'normal') return 0.5 + return 0.34 +} diff --git a/src/pages/people/relationships/relationship-galaxy-3d-scene.test.ts b/src/pages/people/relationships/relationship-galaxy-3d-scene.test.ts index 47c2dd87..085e093f 100644 --- a/src/pages/people/relationships/relationship-galaxy-3d-scene.test.ts +++ b/src/pages/people/relationships/relationship-galaxy-3d-scene.test.ts @@ -375,7 +375,7 @@ test('uses varied node colors, larger important nodes, and no glow field', () => const friend = scene.nodes.find((item) => item.key === 'weixin:friend') const groupmate = scene.nodes.find((item) => item.key === 'weixin:groupmate') - assert.equal(owner?.color, 0xfff2a8) + assert.equal(owner?.color, 0xf8fbff) assert.ok((friend?.color ?? 0) !== 0x2563eb) assert.ok((groupmate?.color ?? 0) !== 0x22d3ee) assert.ok(new Set(scene.nodes.map((item) => item.color)).size >= 3) @@ -384,22 +384,52 @@ test('uses varied node colors, larger important nodes, and no glow field', () => assert.equal(Object.hasOwn(groupmate ?? {}, 'glow'), false) }) -test('uses stellar temperature colors instead of saturated rainbow node colors', () => { +test('uses a broad deterministic multi-color palette for contact stars', () => { const graph: PeopleRelationshipsGraphData = { nodes: [ node({ key: 'weixin:owner', kind: 'owner', rank: 1, pool: 'friend', score: 1 }), - node({ key: 'weixin:warm', rank: 2, pool: 'friend', score: 0.9 }), - node({ key: 'weixin:cool', rank: 30, pool: 'non_friend', score: 0.45 }), + ...Array.from({ length: 120 }, (_, index) => + node({ + key: `weixin:colorful-${index}`, + rank: index + 2, + pool: index % 3 === 0 ? 'friend' : 'non_friend', + score: Math.max(0.2, 1 - index / 80), + }) + ), ], edges: [], communities: [], } const scene = buildRelationshipGalaxy3DScene(graph) - const oldRainbowColors = new Set([0xb7ff72, 0xff9bd8, 0x67f4a8, 0xb9b4ff, 0xd7ff8a]) - - assert.equal( - scene.nodes.some((item) => oldRainbowColors.has(item.color)), - false + const contactColorFamilies = new Set( + scene.nodes + .filter((item) => item.node.kind !== 'owner') + .map((item) => colorFamily(item.color)) + .filter(Boolean) ) + + assert.ok(contactColorFamilies.size >= 6) }) + +function colorFamily(color: number): string | null { + const red = ((color >> 16) & 255) / 255 + const green = ((color >> 8) & 255) / 255 + const blue = (color & 255) / 255 + const max = Math.max(red, green, blue) + const min = Math.min(red, green, blue) + const delta = max - min + if (delta < 0.18) return null + + let hue = 0 + if (max === red) hue = ((green - blue) / delta + (green < blue ? 6 : 0)) * 60 + else if (max === green) hue = ((blue - red) / delta + 2) * 60 + else hue = ((red - green) / delta + 4) * 60 + + if (hue < 30 || hue >= 330) return 'rose' + if (hue < 90) return 'gold' + if (hue < 150) return 'green' + if (hue < 210) return 'cyan' + if (hue < 270) return 'blue' + return 'violet' +} diff --git a/src/pages/people/relationships/relationship-galaxy-3d-scene.ts b/src/pages/people/relationships/relationship-galaxy-3d-scene.ts index 44ec7bb7..24f250d9 100644 --- a/src/pages/people/relationships/relationship-galaxy-3d-scene.ts +++ b/src/pages/people/relationships/relationship-galaxy-3d-scene.ts @@ -55,9 +55,13 @@ export interface RelationshipGalaxy3DSceneOptions { selectedKey?: string | null } -const OWNER_COLOR = 0xfff2a8 -const FRIEND_NODE_COLORS = [0xfff6da, 0xffdf9a, 0xdcecff, 0xffffff, 0xffb47a] -const GROUPMATE_NODE_COLORS = [0xdbe9ff, 0xf8fbff, 0xffedc7, 0xcfe6ff, 0xffc49a] +const OWNER_COLOR = 0xf8fbff +const FRIEND_NODE_COLORS = [ + 0xff8fb3, 0xffc857, 0x7dd3fc, 0xa78bfa, 0x86efac, 0xfb7185, 0x38bdf8, 0xf0abfc, 0xfde68a, 0x5eead4, +] +const GROUPMATE_NODE_COLORS = [ + 0xffb86b, 0x60a5fa, 0x34d399, 0xe879f9, 0xfacc15, 0x5eead4, 0xc084fc, 0xf472b6, 0xa3e635, 0x93c5fd, +] const MAX_3D_SCENE_RADIUS = 1700 interface RelationshipGalaxy3DVector {