From e3f0d8ffafea155e308419f0d8cd9c10ff8fb70f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 19 May 2026 14:08:05 +0100 Subject: [PATCH] Drive Huginn stardust from packed art channels --- README.md | 11 ++- inspector/src/HuginnFieldCanvas.tsx | 40 ++++++---- inspector/src/main.tsx | 114 +++------------------------- package-lock.json | 4 +- package.json | 2 +- 5 files changed, 47 insertions(+), 124 deletions(-) diff --git a/README.md b/README.md index 344ac83..f9aed6e 100644 --- a/README.md +++ b/README.md @@ -72,10 +72,13 @@ intersects the current screen bounds with the artwork bounds, and assigns only visible quadtree cells to those slots. Each visible cell is seeded with the Aetheria Stardust `RandomFirst` hash and xorshift follow-up sequence, then each particle pair trades opacity with sine/cosine phase while tracing a short -segment through the packed flow texture. Zooming raises the live quadtree level; -coarse cells fade down while finer cells seeded from the same spatial lattice -fade in around them, so detail density rises without changing the field's -identity. +segment through the packed flow texture. The packed map uses `RG` for flow, `B` +for structure/curvature/detail pressure, and `A` for emission/field strength; +particle color comes from the source Huginn albedo at the starting UV so the +flow carries brushstrokes instead of repainting them at the final sample. +Zooming raises the live quadtree level; coarse cells fade down while finer cells +seeded from the same spatial lattice fade in around them, so detail density +rises without changing the field's identity. ## Example diff --git a/inspector/src/HuginnFieldCanvas.tsx b/inspector/src/HuginnFieldCanvas.tsx index d1fa62b..2052af4 100644 --- a/inspector/src/HuginnFieldCanvas.tsx +++ b/inspector/src/HuginnFieldCanvas.tsx @@ -314,14 +314,18 @@ fn updateParticles(@builtin(global_invocation_id) id: vec3u) { ); let sourceAlbedo = sampleAlbedo(base); - var packed = samplePacked(base); + let startPacked = samplePacked(base); + var packed = startPacked; let levelDetail = f32(level) / f32(${STARDUST_MAX_LEVEL}u); let detail = clamp(uniforms.detail, 0.75, 2.0); - let fieldVector = packed.rg * 2.0 - vec2f(1.0); + let fieldVector = startPacked.rg * 2.0 - vec2f(1.0); let fieldDirection = normalize(fieldVector + vec2f(0.0001, 0.0)); let curvatureGradient = channelGradient(base, 2u); let tangent = normalize(vec2f(-curvatureGradient.y, curvatureGradient.x) + fieldDirection * 0.28 + vec2f(0.0001, 0.0)); - let fieldBlend = normalize(fieldDirection * 0.62 + tangent * (0.32 + packed.b * 0.22) + triFlow(base * 18.0 - vec2f(0.0, uniforms.time * 0.035), triSeed) * 0.22); + let sourceStructure = startPacked.b; + let sourceEmission = startPacked.a; + let domainNoise = triFlow(base * (14.0 + sourceStructure * 18.0) - vec2f(0.0, uniforms.time * (0.026 + sourceEmission * 0.018)), triSeed); + let fieldBlend = normalize(fieldDirection * (0.56 + sourceEmission * 0.18) + tangent * (0.26 + sourceStructure * 0.36) + domainNoise * (0.13 + sourceStructure * 0.27)); var nodePush = vec2f(0.0); var nodeHeat = 0.0; for (var envelopeIndex = 0u; envelopeIndex < ${MAX_NODE_ENVELOPES}u; envelopeIndex = envelopeIndex + 1u) { @@ -342,30 +346,36 @@ fn updateParticles(@builtin(global_invocation_id) id: vec3u) { } let flowLine = normalize(fieldBlend + nodePush); - var uv = clamp(base - flowLine * lifetime * (uniforms.flowGain / 24.0) * (0.009 + packed.a * 0.028 + packed.b * 0.012) * spacing, vec2f(0.001), vec2f(0.999)); + var uv = clamp(base - flowLine * lifetime * (uniforms.flowGain / 24.0) * (0.009 + sourceEmission * 0.030 + sourceStructure * 0.016) * spacing, vec2f(0.001), vec2f(0.999)); packed = samplePacked(uv); particle.position = uv; - particle.velocity = flowLine * (4.0 + packed.a * 9.0 + packed.b * 4.0); + let advectedFlow = packed.rg * 2.0 - vec2f(1.0); + let advectedShear = length(advectedFlow - fieldVector); + let emission = clamp(sourceEmission * 0.42 + packed.a * 0.78, 0.0, 1.0); + let structure = clamp(sourceStructure * 0.52 + packed.b * 0.62, 0.0, 1.0); + particle.velocity = flowLine * (3.2 + emission * 10.5 + structure * 5.4 + advectedShear * 2.2); particle.life = lifetime; let flowAxis = abs(fieldVector.x - fieldVector.y); let lifeFade = parabola(lifetime, 2.0); let phase = lifetime * 6.28318530718 + f32(companion) * 1.57079632679; let pairFade = pow(clamp(sin(phase) * 0.5 + 0.5, 0.0, 1.0), 1.35); - let heat = clamp(packed.a * 0.78 + packed.b * 0.62 + nodeHeat * 0.38 + lifeFade * 0.18, 0.0, 1.0); - let brass = clamp((packed.b - packed.a * 0.32) * 1.35 + flowAxis * 0.18, 0.0, 1.0); - let cyan = vec3f(0.08 + heat * 0.28, 0.44 + heat * 0.46, 0.52 + heat * 0.62); - let gold = vec3f(0.86, 0.58, 0.22); - let deep = vec3f(0.02, 0.10, 0.12); - let albedoLift = mix(deep, sourceAlbedo.rgb, clamp(sourceAlbedo.a * (0.72 + heat * 0.24), 0.0, 1.0)); - let fieldTint = mix(cyan, gold, brass * 0.26); - let rgb = mix(albedoLift, fieldTint, 0.18 + heat * 0.16); + let heat = clamp(emission * 0.82 + structure * 0.48 + advectedShear * 0.22 + nodeHeat * 0.38 + lifeFade * 0.16, 0.0, 1.0); + let brass = clamp((structure - emission * 0.22) * 1.22 + flowAxis * 0.22, 0.0, 1.0); + let cyan = vec3f(0.07 + emission * 0.24, 0.40 + heat * 0.50, 0.52 + emission * 0.72); + let gold = vec3f(0.96, 0.65 + structure * 0.14, 0.24); + let violet = vec3f(0.40 + sourceAlbedo.r * 0.18, 0.14 + sourceAlbedo.g * 0.08, 0.54 + sourceAlbedo.b * 0.24); + let deep = vec3f(0.012, 0.052, 0.065); + let albedoLift = mix(deep, sourceAlbedo.rgb, clamp(sourceAlbedo.a * (0.64 + emission * 0.30 + structure * 0.16), 0.0, 1.0)); + let fieldTint = mix(mix(cyan, gold, brass * 0.34), violet, clamp(sourceStructure * (1.0 - sourceEmission) * 0.28, 0.0, 0.28)); + let emissiveInk = sourceAlbedo.rgb * (0.25 + emission * 0.72) + fieldTint * (0.20 + structure * 0.22); + let rgb = mix(albedoLift, emissiveInk, clamp(0.16 + heat * 0.28 + sourceAlbedo.a * 0.18, 0.0, 0.74)); particle.color = vec4f( rgb, - uniforms.alpha * (0.003 + heat * 0.010 + packed.a * 0.005 + nodeHeat * 0.0025) * lifeFade * pairFade * band.weight * (0.72 + levelDetail * 0.28) + uniforms.alpha * (0.0022 + emission * 0.013 + structure * 0.006 + advectedShear * 0.003 + nodeHeat * 0.0025) * lifeFade * pairFade * band.weight * (0.72 + levelDetail * 0.28) ); - particle.size = mix(0.24, 0.92, sizeRandom) * (0.7 + packed.a * 0.7 + packed.b * 0.28 + nodeHeat * 0.14) * (0.92 + detail * 0.035) * (0.82 + levelDetail * 0.22) * lifeFade * (0.35 + pairFade * 0.65) * (0.82 + band.weight * 0.18); + particle.size = mix(0.20, 0.98, sizeRandom) * (0.62 + emission * 0.82 + structure * 0.36 + advectedShear * 0.18 + nodeHeat * 0.14) * (0.92 + detail * 0.035) * (0.82 + levelDetail * 0.22) * lifeFade * (0.35 + pairFade * 0.65) * (0.82 + band.weight * 0.18); particles[index] = particle; } `; diff --git a/inspector/src/main.tsx b/inspector/src/main.tsx index 5ad73ea..48a98f2 100644 --- a/inspector/src/main.tsx +++ b/inspector/src/main.tsx @@ -1,6 +1,6 @@ -import { EpiphanyGraphViewer, type EpiphanyGraphEdge, type EpiphanyGraphNode, type EpiphanyGraphsState, type TerrainForceContext, type TerrainForceSample, type ViewerSelection } from "@epiphanygraph/epiphany-graph-viewer"; +import { EpiphanyGraphViewer, type EpiphanyGraphEdge, type EpiphanyGraphNode, type EpiphanyGraphsState, type ViewerSelection } from "@epiphanygraph/epiphany-graph-viewer"; import { createRoot } from "react-dom/client"; -import { Component, useEffect, useMemo, useRef, useState, type DragEvent, type ReactNode } from "react"; +import { Component, useMemo, useState, type DragEvent, type ReactNode } from "react"; import { inspectCultCacheBytes, type CultCacheInspection, type InspectedCatalogEntry, type InspectedRecord } from "../../src/cult-cache-inspector"; import { HuginnFieldCanvas } from "./HuginnFieldCanvas"; @@ -22,12 +22,6 @@ type GraphProjection = { truncatedValueNodes: number; }; -type TerrainTexture = { - width: number; - height: number; - rgba: Uint8ClampedArray; -}; - const MAX_EXPANDED_VALUE_NODES = 960; const assetPath = (name: string) => `${import.meta.env.BASE_URL}${name}`; const HUGINN_ART = { @@ -279,7 +273,6 @@ function InspectionView({ setSelection: (selection: Selection) => void; onInspectFile: (file: File) => Promise; }) { - const terrainForces = useHuginnTerrainForces(HUGINN_ART.field); const expandedRawSelection = graphSelection?.kind === "node" ? graphProjection.nodeSelections.get(graphSelection.nodeId) : graphSelection?.kind === "edge" @@ -358,7 +351,16 @@ function InspectionView({ title="CultCache Structure" viewportBackdrop={} viewportBackground="#03070a" - terrainForces={terrainForces} + layoutMode={{ architecture: "layered", dataflow: "combined-force" }} + motion={{ + strength: 1.24, + damping: 0.88, + flow: 1.2, + orbit: 0.84, + lift: 0.9, + pulse: 1, + emitNodeEnvelopes: true, + }} overlayPanels showSidebar={false} focusSelection @@ -401,98 +403,6 @@ function InspectionView({ ); } -function useHuginnTerrainForces(fieldUrl: string) { - const textureRef = useRef(null); - - useEffect(() => { - let cancelled = false; - loadTerrainTexture(fieldUrl) - .then((texture) => { - if (!cancelled) { - textureRef.current = texture; - } - }) - .catch(() => { - textureRef.current = null; - }); - return () => { - cancelled = true; - }; - }, [fieldUrl]); - - return useMemo(() => ({ - strength: 1.35, - damping: 0.9, - envelopeStrength: 0.03, - emitNodeEnvelopes: true, - sample: (x: number, y: number, context: TerrainForceContext): TerrainForceSample => { - const texture = textureRef.current; - const bounds = context.bounds ?? { x: 0, y: 0, width: 1, height: 1 }; - const worldX = (x * context.viewportWidth - context.viewX) / Math.max(0.001, context.scale); - const worldY = (y * context.viewportHeight - context.viewY) / Math.max(0.001, context.scale); - const side = Math.max(1, Math.max(bounds.width, bounds.height)); - const uvX = (worldX - (bounds.x + bounds.width * 0.5 - side * 0.5)) / side; - const uvY = (worldY - (bounds.y + bounds.height * 0.5 - side * 0.5)) / side; - if (!texture) { - const dx = uvX - 0.5; - const dy = uvY - 0.5; - const radius = Math.max(0.001, Math.hypot(dx, dy)); - return { - flowX: -dy / radius * 0.35, - flowY: dx / radius * 0.35, - strength: Math.max(0, 1 - radius * 2), - curvature: 0.25, - }; - } - return sampleTerrainTexture(texture, uvX, uvY); - }, - }), []); -} - -function loadTerrainTexture(url: string): Promise { - const image = new Image(); - image.decoding = "async"; - image.src = url; - return image.decode() - .catch(() => new Promise((resolve, reject) => { - if (image.complete && image.naturalWidth > 0) { - resolve(); - return; - } - image.onload = () => resolve(); - image.onerror = () => reject(new Error(`Could not load ${url}`)); - })) - .then(() => { - const canvas = document.createElement("canvas"); - canvas.width = 256; - canvas.height = 256; - const context = canvas.getContext("2d", { willReadFrequently: true }); - if (!context) { - throw new Error("Canvas 2D context unavailable."); - } - context.imageSmoothingEnabled = true; - context.imageSmoothingQuality = "high"; - context.drawImage(image, 0, 0, canvas.width, canvas.height); - return { - width: canvas.width, - height: canvas.height, - rgba: context.getImageData(0, 0, canvas.width, canvas.height).data, - }; - }); -} - -function sampleTerrainTexture(texture: TerrainTexture, x: number, y: number): TerrainForceSample { - const sx = Math.max(0, Math.min(texture.width - 1, Math.floor(x * texture.width))); - const sy = Math.max(0, Math.min(texture.height - 1, Math.floor(y * texture.height))); - const index = (sy * texture.width + sx) * 4; - return { - flowX: texture.rgba[index] / 255 * 2 - 1, - flowY: texture.rgba[index + 1] / 255 * 2 - 1, - curvature: texture.rgba[index + 2] / 255, - strength: texture.rgba[index + 3] / 255, - }; -} - function PanelHeader({ title, count }: { title: string; count: string }) { return (
diff --git a/package-lock.json b/package-lock.json index 360723d..4a44c43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cultcache-ts", - "version": "0.13.0", + "version": "0.13.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cultcache-ts", - "version": "0.13.0", + "version": "0.13.1", "license": "MIT", "dependencies": { "@msgpack/msgpack": "^3.1.3", diff --git a/package.json b/package.json index ea2a594..73ea216 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cultcache-ts", - "version": "0.13.0", + "version": "0.13.1", "description": "CultCache-style typed persistence for TypeScript with schema-backed polymorphic MessagePack storage.", "main": "dist/index.js", "types": "dist/index.d.ts",