avatars plus other changes
All checks were successful
Build Org Website / build (push) Successful in 51s

This commit is contained in:
2026-05-11 15:44:30 +01:00
parent ae591de204
commit 5df4871ecc
7 changed files with 532 additions and 18 deletions

View File

@@ -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
};
}());

View File

@@ -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 `<svg width="36" height="36" viewBox="0 0 36 36" aria-hidden="true" focusable="false"><rect x="2" y="2" width="32" height="32" rx="10" fill="${secondary}" opacity=".34"></rect><circle cx="18" cy="18" r="15" fill="${accent}" opacity=".18"></circle>${motif}<path d="M10 29c1.6-5.1 5-7.4 8.2-7.4S25 23.9 27 29" fill="${clothes}" opacity=".94"></path><path d="M9.5 16.4c.2-6.4 4.1-10.3 8.8-10.3s8.2 3.8 8.4 10.3c-2-2-4.4-3.1-7.2-3.1-3.7 0-6.7 1.1-10 3.1z" fill="${hair}"></path><path d="M10.6 17.8c0-5.3 3.4-8.7 7.7-8.7s7.7 3.4 7.7 8.7c0 5.9-3.3 9.7-7.7 9.7s-7.7-3.8-7.7-9.7z" fill="${skin}"></path><path d="M10.7 16.2c2.4-4.2 6.5-5.3 12.1-4.4 1.1.8 2.2 2.2 3 4.1-3.6-.7-6.5-2.2-8.8-4.1-1.3 2-3.5 3.6-6.3 4.4z" fill="${hair}"></path>${eye}<circle cx="12.4" cy="20" r="1.25" fill="#ef8e8e" opacity=".34"></circle><circle cx="24.2" cy="20" r="1.25" fill="#ef8e8e" opacity=".34"></circle><path d="${mouth}" stroke="#3f2d29" stroke-width="1.45" stroke-linecap="round" fill="none"></path><text x="28.8" y="31.4" text-anchor="middle" font-size="7" fill="#fff7e8" font-family="monospace">${symbol}</text></svg>`;
}
function avatarHtml(character, text) {
function avatarHtml(character, text, size = "small", extraClass = "") {
const config = CHARACTER_REGISTRY[character] || CHARACTER_REGISTRY.z;
return `<span class="hidden-avatar" style="--hidden-avatar-color:${config.territoryColor || "#d3a64d"}" title="${escapeHtml(config.displayLabel || character)}">${avatarSvg(character, avatarExpression(character, text))}</span>`;
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 `<span class="hidden-avatar hidden-avatar--${escapeHtml(size)} ${escapeHtml(extraClass)}" style="--hidden-avatar-color:${escapeHtml(color)}" title="${escapeHtml(label)}" data-character="${escapeHtml(character)}" data-expression="${escapeHtml(expression)}"><img src="${escapeHtml(src)}" alt="" loading="lazy" decoding="async" onerror="this.hidden=true;this.nextElementSibling.hidden=false"><span class="hidden-avatar__fallback" hidden>${fallback}</span></span>`;
}
function toast(message, duration, character) {

View File

@@ -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 `<span class="observatory-avatar observatory-avatar--${escapeAttr(size)} ${escapeAttr(extraClass)}" style="--avatar-glow:${escapeAttr(color)}" title="${escapeAttr(label)}" data-character="${escapeAttr(character)}"><img src="${escapeAttr(src)}" alt="" loading="lazy" decoding="async" onerror="this.hidden=true;this.nextElementSibling.hidden=false"><span class="observatory-avatar__fallback" hidden>${escapeHtml((label || "?").slice(0, 1))}</span></span>`;
}
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 `<button type="button" class="observatory-star${active ? " is-active" : ""}" data-id="${escapeAttr(entry.id)}" style="--x:${50 + Math.cos(angle) * radius}%;--y:${50 + Math.sin(angle) * radius * 0.74}%;--size:${18 + Number(entry.resonanceScore || 3) * 2}px">
<span>${escapeHtml((entry.characters || ["z"])[0].slice(0, 1))}</span>
${avatarHtml(character, entry, "marker", `observatory-star-avatar${focus}`)}
<span class="observatory-star-label">${escapeHtml(entry.title || atmosphere)}</span>
</button>`;
}).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) => `<p><strong>${escapeHtml(thread.relation.replace("Links", ""))}</strong>: ${escapeHtml(thread.title)}</p>`).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 `<p class="observatory-thread-row">${avatarHtml(character, entry, "tiny")}<span><strong>${escapeHtml(thread.relation.replace("Links", ""))}</strong>: ${escapeHtml(thread.title)}</span></p>`;
}).join("")
: "<p>No thread has been restored here yet.</p>";
}
@@ -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 `<button type="button" class="return-memory" data-id="${escapeAttr(entry.id)}">
<strong>${escapeHtml(entry.title)}</strong>
<span class="return-memory-head">${avatarHtml(character, entry, "small")}<strong>${escapeHtml(entry.title)}</strong></span>
<span>${seen.count ? `observed ${seen.count} time${seen.count === 1 ? "" : "s"}` : "not observed here yet"}</span>
</button>`;
}).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]) => `<button type="button" class="symbol-chip${selected.includes(symbol) ? " is-active" : ""}" data-symbol="${escapeAttr(symbol)}">
<strong>${escapeHtml(symbol)}</strong><span>${count}</span>
</button>`).join("");
cloud.innerHTML = symbols.map(([symbol, count]) => {
const character = primaryCharacter({ title: symbol, content: symbol, symbols: [symbol] });
return `<button type="button" class="symbol-chip${selected.includes(symbol) ? " is-active" : ""}" data-symbol="${escapeAttr(symbol)}">
<span class="symbol-chip-head">${avatarHtml(character, { title: symbol, symbols: [symbol] }, "tiny")}<strong>${escapeHtml(symbol)}</strong></span><span>${count}</span>
</button>`;
}).join("");
$$("[data-symbol]", cloud).forEach((button) => button.addEventListener("click", () => toggleSymbol(button.dataset.symbol)));
const known = state.discoveredSymbols || {};
routes.innerHTML = Object.keys(known).length