avatars plus other changes
All checks were successful
Build Org Website / build (push) Successful in 51s
All checks were successful
Build Org Website / build (push) Successful in 51s
This commit is contained in:
150
assets/scripts/avatar-registry.js
Normal file
150
assets/scripts/avatar-registry.js
Normal 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
|
||||
};
|
||||
}());
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user