mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
feat(relationships): enrich 3d galaxy colors
This commit is contained in:
1 parent
97c5400f9e
commit
a3510939d5
5 files changed
+166
-23
No files matched your search
@@ -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<number, RelationshipGalaxy3DEdge[]>()
|
||||
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)'],
|
||||
])
|
||||
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user