From 5df4871eccee8f79954886d14a9d4b6ab4655a80 Mon Sep 17 00:00:00 2001 From: Zaine Date: Mon, 11 May 2026 15:44:30 +0100 Subject: [PATCH] avatars plus other changes --- assets/scripts/avatar-registry.js | 150 ++++++++++++++++++++++++++++++ assets/scripts/hidden-details.js | 21 ++++- assets/scripts/play.js | 74 +++++++++++++-- assets/styles/hidden-details.css | 36 ++++++- assets/styles/play.css | 127 ++++++++++++++++++++++++- authoring_server.py | 141 +++++++++++++++++++++++++++- build-site.el | 1 + 7 files changed, 532 insertions(+), 18 deletions(-) create mode 100644 assets/scripts/avatar-registry.js diff --git a/assets/scripts/avatar-registry.js b/assets/scripts/avatar-registry.js new file mode 100644 index 0000000..ae69e7f --- /dev/null +++ b/assets/scripts/avatar-registry.js @@ -0,0 +1,150 @@ +(function () { + "use strict"; + + const fallback = "z"; + const sharedSizes = { + marker: 18, + tiny: 22, + small: 32, + medium: 44, + large: 64, + focus: 88 + }; + + const characters = { + "young z": { + id: "young z", + displayLabel: "young z", + avatar: "/assets/avatars/young-z.jpeg", + glow: "#d8a95a", + accent: "#f4bf5f", + fallback, + expressions: { + calm: "/assets/avatars/young-z.jpeg", + happy: "/assets/avatars/young-z.jpeg", + surprised: "/assets/avatars/young-z.jpeg" + }, + sizes: sharedSizes, + observatory: { + territory: "nostalgic-playful", + presenceKeywords: ["childhood", "play", "crayon", "sun", "blanket", "game", "desk", "drawing", "nostalgia"], + atmosphere: "warm childhood light", + focusScale: 1.08 + } + }, + z: { + id: "z", + displayLabel: "z", + avatar: "/assets/avatars/z.jpeg", + glow: "#d6c38a", + accent: "#d8c891", + fallback, + expressions: { + calm: "/assets/avatars/z.jpeg", + receptive: "/assets/avatars/z.jpeg" + }, + sizes: sharedSizes, + observatory: { + territory: "archive-return", + presenceKeywords: ["website", "archive", "observer", "traveler", "making", "return", "home"], + atmosphere: "steady archive lantern", + focusScale: 1 + } + }, + aphy: { + id: "aphy", + displayLabel: "aphy", + avatar: "/assets/avatars/aphy.jpeg", + glow: "#7fb089", + accent: "#83d4df", + fallback, + expressions: { + calm: "/assets/avatars/aphy.jpeg", + curious: "/assets/avatars/aphy.jpeg", + thinking: "/assets/avatars/aphy.jpeg" + }, + sizes: sharedSizes, + observatory: { + territory: "system-symbolic", + presenceKeywords: ["system", "terminal", "logic", "error", "debug", "console", "css", "search", "backup", "symbol"], + atmosphere: "soft diagnostic glow", + focusScale: 1.04 + } + }, + lima: { + id: "lima", + displayLabel: "lima", + avatar: "/assets/avatars/lima.jpg", + glow: "#d06b78", + accent: "#e7a176", + fallback, + expressions: { + calm: "/assets/avatars/lima.jpg", + happy: "/assets/avatars/lima.jpg", + protective: "/assets/avatars/lima.jpg" + }, + sizes: sharedSizes, + observatory: { + territory: "warm-protective", + presenceKeywords: ["warm", "home", "safe", "protect", "kitchen", "ring", "love", "rest", "care", "light"], + atmosphere: "protective kitchen warmth", + focusScale: 1.12 + } + }, + "sensei chi": { + id: "sensei chi", + displayLabel: "sensei chi", + avatar: "/assets/avatars/sensei-chi.jpeg", + glow: "#75a9bd", + accent: "#9ec4be", + fallback, + expressions: { + calm: "/assets/avatars/sensei-chi.jpeg", + reflective: "/assets/avatars/sensei-chi.jpeg" + }, + sizes: sharedSizes, + observatory: { + territory: "reflective-patient", + presenceKeywords: ["quiet", "reflect", "patience", "tea", "garden", "lesson", "wisdom", "still"], + atmosphere: "slow reflective tea light", + focusScale: 1.06 + } + }, + "future z": { + id: "future z", + displayLabel: "future z", + avatar: "/assets/avatars/future-z.jpeg", + glow: "#a58ac9", + accent: "#b7a6cf", + fallback, + expressions: { + calm: "/assets/avatars/future-z.jpeg", + nostalgic: "/assets/avatars/future-z.jpeg", + warning: "/assets/avatars/future-z.jpeg" + }, + sizes: sharedSizes, + observatory: { + territory: "temporal", + presenceKeywords: ["future", "time", "age 40", "later", "return", "warning", "ordinary", "clock"], + atmosphere: "distant temporal shimmer", + focusScale: 1.1 + } + } + }; + + function get(character) { + return characters[character] || characters[fallback]; + } + + function source(character, expression) { + const config = get(character); + return (config.expressions && expression && config.expressions[expression]) || config.avatar || characters[fallback].avatar; + } + + window.MemoryAvatarRegistry = { + fallback, + characters, + get, + source + }; +}()); diff --git a/assets/scripts/hidden-details.js b/assets/scripts/hidden-details.js index b3f41bd..a9c54bf 100644 --- a/assets/scripts/hidden-details.js +++ b/assets/scripts/hidden-details.js @@ -103,6 +103,17 @@ } }; + const AVATAR_REGISTRY = window.MemoryAvatarRegistry || { + fallback: "z", + characters: CHARACTER_REGISTRY, + get(character) { + return this.characters[character] || this.characters[this.fallback] || {}; + }, + source(character) { + return this.get(character).avatar || ""; + } + }; + const familyLayers = [ "Layer 0: surface website - notes, pages, tools, normal navigation.", "Layer 1: casual hidden jokes - aphy notices clicks, typos, tabs, and old-web habits.", @@ -642,9 +653,15 @@ return ``; } - function avatarHtml(character, text) { + function avatarHtml(character, text, size = "small", extraClass = "") { const config = CHARACTER_REGISTRY[character] || CHARACTER_REGISTRY.z; - return `${avatarSvg(character, avatarExpression(character, text))}`; + const portrait = AVATAR_REGISTRY.get(character); + const expression = avatarExpression(character, text); + const src = AVATAR_REGISTRY.source(character, expression); + const color = portrait.glow || portrait.accent || config.territoryColor || "#d3a64d"; + const label = config.displayLabel || portrait.displayLabel || character; + const fallback = escapeHtml(config.symbol || "?"); + return ``; } function toast(message, duration, character) { diff --git a/assets/scripts/play.js b/assets/scripts/play.js index a4da29c..966b926 100644 --- a/assets/scripts/play.js +++ b/assets/scripts/play.js @@ -9,6 +9,53 @@ "A tiny bell rings in a room you have not built yet.", "The page remembers that you came here." ]; + const avatarRegistry = window.MemoryAvatarRegistry || { fallback: "z", characters: {} }; + + function avatarConfig(character) { + return avatarRegistry.characters?.[character] || avatarRegistry.characters?.[avatarRegistry.fallback || "z"] || {}; + } + + function avatarSource(character, expression = "calm") { + const config = avatarConfig(character); + return config.expressions?.[expression] || config.avatar || avatarConfig("z").avatar || ""; + } + + function avatarExpression(entry, character) { + const text = [entry?.title, entry?.content, entry?.triggerConditions, entry?.emotionalRole, entry?.mysteryLevel, ...(entry?.symbols || []), ...(entry?.tags || []), ...(entry?.narrativeArcs || [])].join(" ").toLowerCase(); + if (character === "aphy" && /error|terminal|system|diagnostic|debug|console|search|symbol/.test(text)) return "thinking"; + if (character === "lima" && /safe|home|warm|protect|care|eat|rest|light/.test(text)) return "protective"; + if (character === "young z" && /draw|crayon|game|play|sun|blanket|child/.test(text)) return "surprised"; + if (character === "future z" && /time|future|return|ordinary|warning|later/.test(text)) return "nostalgic"; + if (character === "sensei chi" && /quiet|tea|patience|lesson|garden|wisdom/.test(text)) return "reflective"; + return "calm"; + } + + function primaryCharacter(entry) { + return characterPresence(entry)[0]?.character || (entry?.characters || [])[0] || "z"; + } + + function characterPresence(entry) { + const text = [entry?.title, entry?.content, entry?.triggerConditions, entry?.pageLocation, entry?.emotionalRole, entry?.mysteryLevel, ...(entry?.tags || []), ...(entry?.symbols || []), ...(entry?.narrativeArcs || [])].join(" ").toLowerCase(); + const scores = Object.keys(avatarRegistry.characters || {}).map((character) => { + const config = avatarConfig(character); + let score = (entry?.characters || []).includes(character) ? 10 : 0; + (config.observatory?.presenceKeywords || []).forEach((keyword) => { + if (text.includes(String(keyword).toLowerCase())) score += 3; + }); + if (character === "future z" && entryDepth(entry || {}) === 4) score += 3; + return { character, score }; + }).filter((item) => item.score > 0).sort((a, b) => b.score - a.score); + return scores.length ? scores : [{ character: "z", score: 1 }]; + } + + function avatarHtml(character, entry, size = "small", extraClass = "") { + const config = avatarConfig(character); + const expression = avatarExpression(entry, character); + const src = avatarSource(character, expression); + const label = config.displayLabel || character; + const color = config.glow || config.accent || "#d3a64d"; + return ``; + } document.addEventListener("DOMContentLoaded", () => { const root = $(".play-root[data-play-page]"); @@ -945,8 +992,14 @@ const angle = (index / entries.length) * Math.PI * 2 - Math.PI / 2; const radius = 34 + entryDepth(entry) * 8; const active = selected.some((item) => item.id === entry.id); + const character = primaryCharacter(entry); + const presence = characterPresence(entry)[0]?.score || 1; + const config = avatarConfig(character); + const atmosphere = config.observatory?.atmosphere || "observatory light"; + const focus = active ? " focus" : presence >= 10 ? " close" : presence >= 6 ? " medium" : " distant"; return ``; }).join(""); $$("[data-id]", sky).forEach((button) => { @@ -961,6 +1014,7 @@ selected.push(entry); title.textContent = entry.title; reading.textContent = characterLine(entry); + root.style.setProperty("--active-avatar-glow", avatarConfig(primaryCharacter(entry)).glow || "#d6b36a"); if (selected.length === 2) saveThread(selected[0], selected[1]); render(); } @@ -968,7 +1022,11 @@ function renderLog() { const threads = state.restoredThreads || []; log.innerHTML = threads.length - ? threads.slice(-6).reverse().map((thread) => `

${escapeHtml(thread.relation.replace("Links", ""))}: ${escapeHtml(thread.title)}

`).join("") + ? threads.slice(-6).reverse().map((thread) => { + const entry = entries.find((item) => item.id === thread.source || item.id === thread.target); + const character = primaryCharacter(entry || { characters: ["z"] }); + return `

${avatarHtml(character, entry, "tiny")}${escapeHtml(thread.relation.replace("Links", ""))}: ${escapeHtml(thread.title)}

`; + }).join("") : "

No thread has been restored here yet.

"; } @@ -1024,8 +1082,9 @@ : patient ? "sensei chi notices the quiet you brought from another room." : "aphy has no elapsed-time anomaly to report yet."; list.innerHTML = entries.map((entry, index) => { const seen = visits[entry.id] || {}; + const character = primaryCharacter(entry); return ``; }).join(""); @@ -1079,9 +1138,12 @@ const symbols = [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 16); function render() { - cloud.innerHTML = symbols.map(([symbol, count]) => ``).join(""); + cloud.innerHTML = symbols.map(([symbol, count]) => { + const character = primaryCharacter({ title: symbol, content: symbol, symbols: [symbol] }); + return ``; + }).join(""); $$("[data-symbol]", cloud).forEach((button) => button.addEventListener("click", () => toggleSymbol(button.dataset.symbol))); const known = state.discoveredSymbols || {}; routes.innerHTML = Object.keys(known).length diff --git a/assets/styles/hidden-details.css b/assets/styles/hidden-details.css index 74f3a9c..d454b45 100644 --- a/assets/styles/hidden-details.css +++ b/assets/styles/hidden-details.css @@ -49,8 +49,9 @@ } .hidden-avatar { - width: 2rem; - height: 2rem; + --hidden-avatar-size: 2rem; + width: var(--hidden-avatar-size); + height: var(--hidden-avatar-size); display: inline-grid; place-items: center; flex: 0 0 auto; @@ -60,13 +61,40 @@ 0 0 18px color-mix(in oklab, var(--hidden-avatar-color, var(--accent)) 18%, transparent); overflow: hidden; animation: hidden-avatar-breathe 6s ease-in-out infinite; + transition: box-shadow 180ms ease, filter 180ms ease, transform 180ms ease; } -.hidden-avatar svg { +.hidden-avatar--tiny { --hidden-avatar-size: 1.35rem; } +.hidden-avatar--small { --hidden-avatar-size: 2rem; } +.hidden-avatar--medium { --hidden-avatar-size: 2.75rem; } +.hidden-avatar--large { --hidden-avatar-size: 4rem; } + +.hidden-avatar img, +.hidden-avatar__fallback { width: 100%; height: 100%; display: block; - shape-rendering: geometricPrecision; +} + +.hidden-avatar img { + object-fit: cover; + border-radius: inherit; +} + +.hidden-avatar__fallback { + place-items: center; + color: color-mix(in oklab, var(--hidden-avatar-color, var(--accent)) 38%, var(--fg)); + font-weight: 850; +} + +.hidden-avatar__fallback:not([hidden]) { + display: grid; +} + +.hidden-avatar:hover { + box-shadow: 0 0 0 1px color-mix(in oklab, var(--hidden-avatar-color, var(--accent)) 54%, color-mix(in oklab, #fff 20%, transparent)), + 0 0 24px color-mix(in oklab, var(--hidden-avatar-color, var(--accent)) 28%, transparent); + filter: saturate(1.08); } .hidden-toast.is-visible { diff --git a/assets/styles/play.css b/assets/styles/play.css index 62d3928..72af778 100644 --- a/assets/styles/play.css +++ b/assets/styles/play.css @@ -502,6 +502,7 @@ --ritual-red: #d06b78; --ritual-green: #7fb089; --ritual-violet: #a58ac9; + --active-avatar-glow: var(--ritual-gold); } .observatory-panel { @@ -519,6 +520,7 @@ radial-gradient(circle at 50% 50%, rgba(214, 179, 106, 0.16), transparent 12%), radial-gradient(circle at 28% 34%, rgba(117, 169, 189, 0.14), transparent 18%), radial-gradient(circle at 72% 70%, rgba(208, 107, 120, 0.12), transparent 16%), + radial-gradient(circle at 50% 48%, color-mix(in oklab, var(--active-avatar-glow) 14%, transparent), transparent 26%), repeating-radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.07) 0 1px, transparent 1px 58px), var(--ritual-sky); } @@ -547,6 +549,8 @@ font-weight: 900; transform: translate(-50%, -50%); box-shadow: 0 0 18px rgba(214, 179, 106, 0.28); + transition: width 180ms ease, height 180ms ease, box-shadow 180ms ease, transform 180ms ease, filter 180ms ease; + animation: observatory-breathe 7s ease-in-out infinite; } .observatory-star:hover, @@ -554,9 +558,10 @@ background: radial-gradient(circle, #fff8e8 0 18%, var(--ritual-red) 19% 56%, rgba(208, 107, 120, 0.22) 57% 100%); color: #fff8e8; box-shadow: 0 0 28px rgba(208, 107, 120, 0.46); + filter: saturate(1.08); } -.observatory-star span { +.observatory-star > span:not(.observatory-avatar) { display: grid; place-items: center; width: 100%; @@ -565,6 +570,110 @@ text-transform: uppercase; } +.observatory-avatar { + --avatar-size: 2rem; + display: inline-grid; + place-items: center; + width: var(--avatar-size); + height: var(--avatar-size); + flex: 0 0 auto; + border-radius: 50%; + overflow: hidden; + background: color-mix(in oklab, var(--avatar-glow, var(--ritual-gold)) 20%, #17130f); + box-shadow: 0 0 0 1px color-mix(in oklab, var(--avatar-glow, var(--ritual-gold)) 54%, rgba(255, 248, 232, 0.28)), + 0 0 20px color-mix(in oklab, var(--avatar-glow, var(--ritual-gold)) 28%, transparent); +} + +.observatory-avatar--marker { --avatar-size: 100%; } +.observatory-avatar--tiny { --avatar-size: 1.35rem; } +.observatory-avatar--small { --avatar-size: 2rem; } +.observatory-avatar--medium { --avatar-size: 2.75rem; } +.observatory-avatar--large { --avatar-size: 4rem; } + +.observatory-avatar img, +.observatory-avatar__fallback { + display: block; + width: 100%; + height: 100%; +} + +.observatory-avatar img { + object-fit: cover; + border-radius: inherit; +} + +.observatory-avatar__fallback:not([hidden]) { + display: grid; + place-items: center; + color: #fff8e8; + font-weight: 900; +} + +.observatory-star-avatar { + position: absolute; + inset: 0; + opacity: 0.76; +} + +.observatory-star-avatar.distant { + opacity: 0.42; + transform: scale(0.58); +} + +.observatory-star-avatar.medium { + opacity: 0.82; + transform: scale(0.9); +} + +.observatory-star-avatar.close, +.observatory-star-avatar.focus { + opacity: 1; + transform: scale(1.08); +} + +.observatory-star-avatar.focus { + transform: scale(1.22); + box-shadow: 0 0 0 2px rgba(255, 248, 232, 0.42), 0 0 34px color-mix(in oklab, var(--avatar-glow, var(--ritual-gold)) 48%, transparent); +} + +.observatory-star-label { + position: absolute; + left: 50%; + top: calc(100% + 0.38rem); + width: max-content; + max-width: 13rem; + height: auto !important; + padding: 0.18rem 0.42rem; + border-radius: 999px; + background: rgba(23, 19, 15, 0.72); + color: #fff8e8; + font-size: 0.68rem !important; + line-height: 1.25; + opacity: 0; + pointer-events: none; + text-transform: none !important; + transform: translateX(-50%) translateY(-2px); + transition: opacity 160ms ease, transform 160ms ease; +} + +.observatory-star:hover .observatory-star-label, +.observatory-star.is-active .observatory-star-label { + opacity: 1; + transform: translateX(-50%) translateY(0); +} + +.observatory-thread-row, +.return-memory-head, +.symbol-chip-head { + display: flex; + align-items: center; + gap: 0.55rem; +} + +.observatory-thread-row { + margin: 0.45rem 0; +} + .observatory-drawer, .dream-sentence { padding: 1rem; @@ -591,6 +700,16 @@ gap: 0.35rem; min-height: 100px; text-align: left; + transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease; +} + +.return-memory:hover, +.return-memory:focus-visible, +.symbol-chip:hover, +.symbol-chip:focus-visible { + border-color: color-mix(in oklab, var(--active-avatar-glow, var(--ritual-gold)) 38%, var(--border)); + box-shadow: 0 0 18px color-mix(in oklab, var(--active-avatar-glow, var(--ritual-gold)) 18%, transparent); + transform: translateY(-1px); } .return-memory span, @@ -610,6 +729,7 @@ text-align: left; background: linear-gradient(135deg, color-mix(in oklab, var(--surface) 88%, var(--ritual-blue) 12%), var(--surface)); + transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease; } .symbol-chip.is-active { @@ -617,6 +737,11 @@ color: #fff8e8; } +@keyframes observatory-breathe { + 0%, 100% { box-shadow: 0 0 18px rgba(214, 179, 106, 0.24); } + 50% { box-shadow: 0 0 26px rgba(214, 179, 106, 0.4); } +} + .symbol-chip.is-active span { color: rgba(255, 248, 232, 0.72); } diff --git a/authoring_server.py b/authoring_server.py index 25d106a..8d3bef9 100755 --- a/authoring_server.py +++ b/authoring_server.py @@ -66,6 +66,7 @@ IMAGE_ASSETS_DIR = ROOT / "assets" / "images" HZONE_ASSETS_DIR = IMAGE_ASSETS_DIR / "hzone" HIDDEN_DETAILS_JS = ROOT / "assets" / "scripts" / "hidden-details.js" HIDDEN_CONTENT_JSON = ROOT / "assets" / "content" / "hidden-details.json" +AVATAR_REGISTRY_JS = ROOT / "assets" / "scripts" / "avatar-registry.js" HIDDEN_BACKUP_DIR = ROOT / "backups" / "hidden-details" EXCLUDED_CONTENT_DIR_NAMES = { ".agents", @@ -270,6 +271,115 @@ CHARACTER_REGISTRY = { "affinities": ["z", "young z", "lima", "sensei chi"], }, } + +AVATAR_REGISTRY_META = { + "young z": { + "avatar": "/assets/avatars/young-z.jpeg", + "glow": "#d8a95a", + "accent": "#f4bf5f", + "fallback": "z", + "expressions": { + "calm": "/assets/avatars/young-z.jpeg", + "happy": "/assets/avatars/young-z.jpeg", + "surprised": "/assets/avatars/young-z.jpeg", + }, + "sizes": {"marker": 18, "tiny": 22, "small": 32, "medium": 44, "large": 64, "focus": 88}, + "observatory": { + "territory": "nostalgic-playful", + "presenceKeywords": ["childhood", "play", "crayon", "sun", "blanket", "game", "desk", "drawing", "nostalgia"], + "atmosphere": "warm childhood light", + "focusScale": 1.08, + }, + }, + "z": { + "avatar": "/assets/avatars/z.jpeg", + "glow": "#d6c38a", + "accent": "#d8c891", + "fallback": "z", + "expressions": {"calm": "/assets/avatars/z.jpeg", "receptive": "/assets/avatars/z.jpeg"}, + "sizes": {"marker": 18, "tiny": 22, "small": 32, "medium": 44, "large": 64, "focus": 88}, + "observatory": { + "territory": "archive-return", + "presenceKeywords": ["website", "archive", "observer", "traveler", "making", "return", "home"], + "atmosphere": "steady archive lantern", + "focusScale": 1, + }, + }, + "aphy": { + "avatar": "/assets/avatars/aphy.jpeg", + "glow": "#7fb089", + "accent": "#83d4df", + "fallback": "z", + "expressions": { + "calm": "/assets/avatars/aphy.jpeg", + "curious": "/assets/avatars/aphy.jpeg", + "thinking": "/assets/avatars/aphy.jpeg", + }, + "sizes": {"marker": 18, "tiny": 22, "small": 32, "medium": 44, "large": 64, "focus": 88}, + "observatory": { + "territory": "system-symbolic", + "presenceKeywords": ["system", "terminal", "logic", "error", "debug", "console", "css", "search", "backup", "symbol"], + "atmosphere": "soft diagnostic glow", + "focusScale": 1.04, + }, + }, + "lima": { + "avatar": "/assets/avatars/lima.jpg", + "glow": "#d06b78", + "accent": "#e7a176", + "fallback": "z", + "expressions": { + "calm": "/assets/avatars/lima.jpg", + "happy": "/assets/avatars/lima.jpg", + "protective": "/assets/avatars/lima.jpg", + }, + "sizes": {"marker": 18, "tiny": 22, "small": 32, "medium": 44, "large": 64, "focus": 88}, + "observatory": { + "territory": "warm-protective", + "presenceKeywords": ["warm", "home", "safe", "protect", "kitchen", "ring", "love", "rest", "care", "light"], + "atmosphere": "protective kitchen warmth", + "focusScale": 1.12, + }, + }, + "sensei chi": { + "avatar": "/assets/avatars/sensei-chi.jpeg", + "glow": "#75a9bd", + "accent": "#9ec4be", + "fallback": "z", + "expressions": {"calm": "/assets/avatars/sensei-chi.jpeg", "reflective": "/assets/avatars/sensei-chi.jpeg"}, + "sizes": {"marker": 18, "tiny": 22, "small": 32, "medium": 44, "large": 64, "focus": 88}, + "observatory": { + "territory": "reflective-patient", + "presenceKeywords": ["quiet", "reflect", "patience", "tea", "garden", "lesson", "wisdom", "still"], + "atmosphere": "slow reflective tea light", + "focusScale": 1.06, + }, + }, + "future z": { + "avatar": "/assets/avatars/future-z.jpeg", + "glow": "#a58ac9", + "accent": "#b7a6cf", + "fallback": "z", + "expressions": { + "calm": "/assets/avatars/future-z.jpeg", + "nostalgic": "/assets/avatars/future-z.jpeg", + "warning": "/assets/avatars/future-z.jpeg", + }, + "sizes": {"marker": 18, "tiny": 22, "small": 32, "medium": 44, "large": 64, "focus": 88}, + "observatory": { + "territory": "temporal", + "presenceKeywords": ["future", "time", "age 40", "later", "return", "warning", "ordinary", "clock"], + "atmosphere": "distant temporal shimmer", + "focusScale": 1.1, + }, + }, +} + +for character_id, avatar_meta in AVATAR_REGISTRY_META.items(): + if character_id in CHARACTER_REGISTRY: + CHARACTER_REGISTRY[character_id]["avatar"] = {**CHARACTER_REGISTRY[character_id].get("avatar", {}), **avatar_meta} + CHARACTER_REGISTRY[character_id]["glow"] = avatar_meta["glow"] + CHARACTER_REGISTRY[character_id]["accent"] = avatar_meta["accent"] HIDDEN_CHARACTERS = list(CHARACTER_REGISTRY) HIDDEN_TONES = ["warm", "funny", "nostalgic", "wise", "strange", "soft", "hopeful", "protective", "melancholy"] HIDDEN_RARITIES = ["common", "uncommon", "rare", "very rare", "seasonal", "timed"] @@ -2604,7 +2714,9 @@ HIDDEN_APP_HTML = r""" overflow: hidden; vertical-align: middle; } - .avatar svg { width: 100%; height: 100%; display: block; shape-rendering: geometricPrecision; } + .avatar img, .avatar-fallback { width: 100%; height: 100%; display: block; } + .avatar img { object-fit: cover; border-radius: inherit; } + .avatar-fallback:not([hidden]) { display: grid; place-items: center; color: #fff4d3; font-size: 11px; font-weight: 850; } .avatar--xs { --avatar-size: 18px; } .avatar--sm { --avatar-size: 24px; } .avatar--md { --avatar-size: 34px; } @@ -2693,8 +2805,12 @@ HIDDEN_APP_HTML = r""" .memory-node text, .node-label text, .cluster-label text { fill: #f7ead3; paint-order: stroke; stroke: rgba(14, 11, 8, 0.82); stroke-width: 3px; stroke-linejoin: round; } .memory-node circle { stroke: rgba(255, 246, 223, 0.82); stroke-width: 1.4; filter: drop-shadow(0 0 7px rgba(255, 232, 177, 0.18)); transition: r 160ms ease, stroke 160ms ease, opacity 160ms ease; } .memory-node .character-halo { fill: color-mix(in oklab, var(--node-character-color, rgba(211, 166, 77, 0.46)) 8%, transparent); stroke: var(--node-character-color, rgba(211, 166, 77, 0.46)); stroke-width: 1.6; opacity: 0.34; stroke-dasharray: 2 7; animation: spirit-shimmer 8s ease-in-out infinite; } - .memory-node .node-avatar { opacity: 0; transform: scale(0.72); transition: opacity 220ms ease, transform 220ms ease; } - .zoom-2 .memory-node .node-avatar, .zoom-3 .memory-node .node-avatar, .memory-node.selected .node-avatar { opacity: 0.96; transform: scale(1); } + .memory-node .node-avatar { opacity: 0; transform: scale(0.58); transition: opacity 220ms ease, transform 220ms ease; } + .zoom-1 .memory-node .node-avatar { opacity: 0.5; transform: scale(0.7); } + .zoom-2 .memory-node .node-avatar { opacity: 0.9; transform: scale(0.95); } + .zoom-3 .memory-node .node-avatar, .memory-node.selected .node-avatar { opacity: 1; transform: scale(1.12); } + .zoom-0 .memory-node text { opacity: 1; } + .zoom-3 .memory-node text, .memory-node.selected text { transform: translateY(27px); } .memory-node.selected circle { stroke: #fff3c7; stroke-width: 3; } .memory-node.dragging circle { stroke: #f3cd7a; stroke-width: 3; opacity: 0.72; } .memory-node.compatible circle { stroke: rgba(255, 243, 199, 0.92); stroke-width: 2.4; filter: drop-shadow(0 0 16px rgba(255, 232, 177, 0.46)); } @@ -3041,6 +3157,7 @@ HIDDEN_APP_HTML = r""" const fields = ["title","type","familyLayer","characters","tone","rarity","discoveryDifficulty","mysteryLevel","resonanceScore","pageLocation","triggerConditions","content","symbols","narrativeArcs","tags","emotionalRole","continuationLinks","echoes","thematicLinks","symbolicLinks","triggerLinks","parentLinks","childLinks","mirroredEntries","enabled","cssClassHooks","audioSettings","animationTrigger","notes"]; const $ = (id) => document.getElementById(id); const toneColors = { warm: "#d3a64d", funny: "#7fb089", nostalgic: "#d06b78", wise: "#75a9bd", strange: "#a58ac9", soft: "#e6bd8c", hopeful: "#9fcf9f", protective: "#d98f71", melancholy: "#8fa7c6" }; + let avatarClipSeq = 0; function html(value) { return String(value || "").replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[char])); @@ -3060,6 +3177,18 @@ HIDDEN_APP_HTML = r""" function avatarSvg(character, expression = "soft") { const config = characterConfig(character); const avatar = config.avatar || {}; + const srcSvg = avatar.expressions?.[expression] || avatar.avatar || config.avatarPath || config.portrait || state.meta.characterRegistry?.z?.avatar?.avatar || "/assets/avatars/z.jpeg"; + const colorSvg = config.glow || avatar.glow || config.territoryColor || avatar.accent || "#d3a64d"; + const labelSvg = html(config.displayLabel || character || "z"); + const symbolSvg = html(config.symbol || "?"); + const clipSvg = `avatarClip-${String(character || "z").replace(/[^a-z0-9]+/gi, "-")}-${String(expression || "calm").replace(/[^a-z0-9]+/gi, "-")}-${avatarClipSeq += 1}`; + return ` + + + + + ${symbolSvg} + `; const accent = avatar.accent || config.territoryColor || "#d3a64d"; const secondary = avatar.secondary || "#f6dfb4"; const hair = avatar.hair || "#5d4632"; @@ -3108,8 +3237,10 @@ HIDDEN_APP_HTML = r""" } function avatarHtml(character, size = "sm", expression = "soft", extraClass = "") { const config = characterConfig(character); - const color = config.territoryColor || config.avatar?.accent || "#d3a64d"; - return `${avatarSvg(character, expression)}`; + const avatar = config.avatar || {}; + const color = config.glow || avatar.glow || config.territoryColor || avatar.accent || "#d3a64d"; + const src = avatar.expressions?.[expression] || avatar.avatar || state.meta.characterRegistry?.z?.avatar?.avatar || "/assets/avatars/z.jpeg"; + return ``; } function avatarChip(character, entry = null) { const config = characterConfig(character); diff --git a/build-site.el b/build-site.el index ed59f65..3ec6964 100755 --- a/build-site.el +++ b/build-site.el @@ -215,6 +215,7 @@ FMT / ARGS are passed to `format'." (format "" (z/asset-url (format "assets/scripts/%s" f)))) '("script.js" + "avatar-registry.js" "lunr.js" "competency-status-board.js" "notes.js"