|
<div id="star-tooltip" class="star-tooltip"></div>
|
|
<div id="star-popup" class="star-popup"></div>
|
|
<script type="module">
|
|
import { app } from "/app.js";
|
|
|
|
class StarField {
|
|
constructor({ count = 200, container = document.body } = {}) {
|
|
this.container = container;
|
|
this.starCount = count;
|
|
this.stars = [];
|
|
this.positionMap = {};
|
|
this.originalColor = getComputedStyle(document.documentElement).getPropertyValue("--star-color").trim();
|
|
this._createStars();
|
|
window.stars = this.positionMap;
|
|
this.patchConsole()
|
|
this.starSignal = (() => {
|
|
const positionMap = this.positionMap;
|
|
|
|
const areaMap = {
|
|
Center: 'Center',
|
|
Corner: 'Corner',
|
|
Edge: 'Edge',
|
|
North: 'North',
|
|
South: 'South',
|
|
East: 'East',
|
|
West: 'West',
|
|
All: Object.keys(positionMap),
|
|
};
|
|
|
|
const applyEffect = (stars, effectFn, duration = 2000) => {
|
|
const active = new Set();
|
|
stars.forEach((star, i) => {
|
|
const { cleanup } = effectFn(star, i);
|
|
active.add(cleanup);
|
|
});
|
|
setTimeout(() => {
|
|
active.forEach(fn => fn && fn());
|
|
}, duration);
|
|
};
|
|
|
|
const effects = {
|
|
pulseColor: (color, size = 5) => (star) => {
|
|
const orig = {
|
|
color: star.style.backgroundColor,
|
|
width: star.style.width,
|
|
height: star.style.height,
|
|
shadow: star.style.boxShadow
|
|
};
|
|
star.style.backgroundColor = color;
|
|
star.style.width = `${size}px`;
|
|
star.style.height = `${size}px`;
|
|
star.style.boxShadow = `0 0 ${size * 2}px ${color}`;
|
|
return {
|
|
cleanup: () => {
|
|
star.style.backgroundColor = orig.color;
|
|
star.style.width = orig.width;
|
|
star.style.height = orig.height;
|
|
star.style.boxShadow = orig.shadow;
|
|
}
|
|
};
|
|
},
|
|
|
|
flicker: (color) => (star) => {
|
|
let visible = true;
|
|
const orig = {
|
|
color: star.style.backgroundColor,
|
|
shadow: star.style.boxShadow
|
|
};
|
|
const flick = setInterval(() => {
|
|
star.style.backgroundColor = visible ? color : '';
|
|
star.style.boxShadow = visible ? `0 0 4px ${color}` : '';
|
|
visible = !visible;
|
|
}, 200);
|
|
return {
|
|
cleanup: () => {
|
|
clearInterval(flick);
|
|
star.style.backgroundColor = orig.color;
|
|
star.style.boxShadow = orig.shadow;
|
|
}
|
|
};
|
|
},
|
|
|
|
shimmer: (colors) => (star, i) => {
|
|
const orig = star.style.backgroundColor;
|
|
let idx = 0;
|
|
const interval = setInterval(() => {
|
|
star.style.backgroundColor = colors[idx % colors.length];
|
|
idx++;
|
|
}, 100 + (i % 5) * 10);
|
|
return {
|
|
cleanup: () => {
|
|
clearInterval(interval);
|
|
star.style.backgroundColor = orig;
|
|
}
|
|
};
|
|
}
|
|
};
|
|
|
|
const trigger = (signalName) => {
|
|
switch (signalName) {
|
|
// --- Notifications ---
|
|
case 'notif.newDM':
|
|
applyEffect(positionMap[areaMap.Corner], effects.pulseColor('white'));
|
|
break;
|
|
case 'notif.groupMsg':
|
|
applyEffect(positionMap[areaMap.Edge], effects.flicker('blue'), 1000);
|
|
break;
|
|
case 'notif.mention':
|
|
applyEffect(positionMap[areaMap.Center], effects.pulseColor('magenta'));
|
|
break;
|
|
case 'notif.react':
|
|
applyEffect(positionMap[areaMap.South], effects.pulseColor('gold'), 1200);
|
|
break;
|
|
case 'notif.typing':
|
|
applyEffect(positionMap[areaMap.West], effects.pulseColor('teal'), 1500);
|
|
break;
|
|
case 'notif.msgSent':
|
|
applyEffect(positionMap[areaMap.East], effects.pulseColor('lightgreen'), 800);
|
|
break;
|
|
case 'notif.msgRead':
|
|
applyEffect(positionMap[areaMap.North], effects.pulseColor('lightblue'), 1000);
|
|
break;
|
|
|
|
// --- User Status ---
|
|
case 'status.online':
|
|
applyEffect(positionMap[areaMap.Center], effects.pulseColor('green'), 3000);
|
|
break;
|
|
case 'status.offline':
|
|
applyEffect(positionMap[areaMap.Center], effects.pulseColor('gray'), 3000);
|
|
break;
|
|
case 'status.idle':
|
|
applyEffect(positionMap[areaMap.Center], effects.pulseColor('orange'), 3000);
|
|
break;
|
|
case 'status.typing':
|
|
applyEffect(positionMap[areaMap.Center], effects.flicker('lightblue'), 2000);
|
|
break;
|
|
case 'status.join':
|
|
applyEffect(positionMap[areaMap.Edge], effects.pulseColor('cyan'), 1500);
|
|
break;
|
|
case 'status.leave':
|
|
applyEffect(positionMap[areaMap.Edge], effects.pulseColor('black'), 1500);
|
|
break;
|
|
|
|
// --- System ---
|
|
case 'sys.broadcast':
|
|
areaMap.All.forEach(area => {
|
|
applyEffect(positionMap[area], effects.pulseColor('gold'), 1000);
|
|
});
|
|
break;
|
|
case 'sys.warning':
|
|
areaMap.All.forEach(area => {
|
|
applyEffect(positionMap[area], effects.flicker('red'), 2000);
|
|
});
|
|
break;
|
|
case 'sys.down':
|
|
areaMap.All.forEach(area => {
|
|
applyEffect(positionMap[area], effects.flicker('darkred'), 3000);
|
|
});
|
|
break;
|
|
case 'sys.update':
|
|
applyEffect(positionMap[areaMap.North], effects.shimmer(['cyan', 'violet', 'lime']), 2500);
|
|
break;
|
|
case 'sys.ping':
|
|
areaMap.All.forEach(area => {
|
|
applyEffect(positionMap[area], effects.pulseColor('white'), 500);
|
|
});
|
|
break;
|
|
|
|
// --- Activity & Reactions ---
|
|
case 'activity.surge':
|
|
applyEffect(positionMap[areaMap.Center], effects.shimmer(['white', 'blue', 'purple']), 1500);
|
|
break;
|
|
case 'reaction.burst':
|
|
applyEffect(positionMap[areaMap.South], effects.pulseColor('hotpink'), 1000);
|
|
break;
|
|
case 'reaction.lol':
|
|
applyEffect(positionMap[areaMap.South], effects.flicker('yellow'), 800);
|
|
break;
|
|
case 'reaction.likes':
|
|
applyEffect(positionMap[areaMap.Center], effects.pulseColor('deeppink'), 1000);
|
|
break;
|
|
|
|
// --- Focus & Movement ---
|
|
case 'focus.zone':
|
|
applyEffect(positionMap[areaMap.Center], effects.pulseColor('white'), 1500);
|
|
break;
|
|
case 'focus.enter':
|
|
applyEffect(positionMap[areaMap.West], effects.pulseColor('cyan'), 1000);
|
|
break;
|
|
case 'focus.exit':
|
|
applyEffect(positionMap[areaMap.East], effects.pulseColor('gray'), 1000);
|
|
break;
|
|
case 'focus.idle':
|
|
applyEffect(positionMap[areaMap.North], effects.flicker('gray'), 2000);
|
|
break;
|
|
|
|
default:
|
|
console.warn('Unknown star signal:', signalName);
|
|
}
|
|
this.trigger = this.starSignal.trigger;
|
|
};
|
|
|
|
return { trigger };
|
|
})();
|
|
}
|
|
showLogEvent(...args) {
|
|
//this.showNotify(...args)
|
|
}
|
|
|
|
mapLogLevel(level) {
|
|
switch (level) {
|
|
case 'info':
|
|
return { category: 'Info', color: 'skyblue' };
|
|
case 'warn':
|
|
return { category: 'Warning', color: 'orange' };
|
|
case 'error':
|
|
return { category: 'Error', color: 'crimson' };
|
|
case 'log':
|
|
default:
|
|
return { category: 'Log', color: 'gold' };
|
|
}
|
|
}
|
|
patchConsole(){
|
|
|
|
|
|
|
|
|
|
const originalConsole = {
|
|
info: console.info,
|
|
warn: console.warn,
|
|
error: console.error,
|
|
log: console.log,
|
|
};
|
|
const me = this
|
|
// Override console methods
|
|
console.info = function(...args) {
|
|
me.showLogEvent('info', args);
|
|
me.createConsoleStar('info', args);
|
|
originalConsole.info.apply(console, args);
|
|
};
|
|
|
|
console.warn = function(...args) {
|
|
me.showLogEvent('warn', args);
|
|
me.createConsoleStar('warn', args);
|
|
originalConsole.warn.apply(console, args);
|
|
};
|
|
|
|
console.error = function(...args) {
|
|
me.showLogEvent('error', args);
|
|
me.createConsoleStar('error', args);
|
|
originalConsole.error.apply(console, args);
|
|
};
|
|
|
|
console.log = function(...args) {
|
|
me.showLogEvent('log', args);
|
|
me.createConsoleStar('log', args);
|
|
originalConsole.log.apply(console, args);
|
|
};
|
|
}
|
|
|
|
_getStarPosition(star) {
|
|
const left = parseFloat(star.style.left);
|
|
const top = parseFloat(star.style.top);
|
|
if (top < 40 && left >= 40 && left <= 60) return "North";
|
|
if (top > 60 && left >= 40 && left <= 60) return "South";
|
|
if (left < 40 && top >= 40 && top <= 60) return "West";
|
|
if (left > 60 && top >= 40 && top <= 60) return "East";
|
|
if (top >= 40 && top <= 60 && left >= 40 && left <= 60) return "Center";
|
|
return "Corner or Edge";
|
|
}
|
|
|
|
_createStars() {
|
|
for (let i = 0; i < this.starCount; i++) {
|
|
const star = document.createElement("div");
|
|
star.classList.add("star");
|
|
this._randomizeStar(star);
|
|
this._placeStar(star);
|
|
this.container.appendChild(star);
|
|
this.stars.push(star);
|
|
}
|
|
}
|
|
|
|
_randomizeStar(star) {
|
|
star.style.left = `${Math.random() * 100}%`;
|
|
star.style.top = `${Math.random() * 100}%`;
|
|
star.style.width = `${Math.random() * 2 + 1}px`;
|
|
star.style.height = `${Math.random() * 2 + 1}px`;
|
|
star.style.animationDelay = `${Math.random() * 2}s`;
|
|
star.style.position = "absolute";
|
|
star.style.transition = "top 1s ease, left 1s ease, opacity 1s ease";
|
|
|
|
star.shuffle = () => this._randomizeStar(star);
|
|
star.position = this._getStarPosition(star);
|
|
}
|
|
createConsoleStar(level, args) {
|
|
const { category, color } = this.mapLogLevel(level);
|
|
const message = args.map(a => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ');
|
|
|
|
this.addSpecialStar({
|
|
title: category.toUpperCase(),
|
|
content: message,
|
|
category,
|
|
color,
|
|
onClick: () => {
|
|
this.showNotify(level, [`[${category}]`, message]);
|
|
},
|
|
});
|
|
}
|
|
|
|
|
|
_placeStar(star) {
|
|
const pos = star.position;
|
|
if (!this.positionMap[pos]) this.positionMap[pos] = [];
|
|
this.positionMap[pos].push(star);
|
|
}
|
|
|
|
shuffleAll(duration = 1000) {
|
|
this.stars.forEach(star => {
|
|
star.style.transition = `top ${duration}ms ease, left ${duration}ms ease, opacity ${duration}ms ease`;
|
|
star.style.filter = "drop-shadow(0 0 2px white)";
|
|
const left = Math.random() * 100;
|
|
const top = Math.random() * 100;
|
|
star.style.left = `${left}%`;
|
|
star.style.top = `${top}%`;
|
|
|
|
setTimeout(() => {
|
|
star.style.filter = "";
|
|
star.position = this._getStarPosition(star);
|
|
}, duration);
|
|
});
|
|
}
|
|
|
|
glowColor(tempColor, duration = 2500) {
|
|
const lighten = (hex, percent) => {
|
|
const num = parseInt(hex.replace("#", ""), 16);
|
|
const r = Math.min(255, (num >> 16) + Math.round(255 * percent / 100));
|
|
const g = Math.min(255, ((num >> 8) & 0xff) + Math.round(255 * percent / 100));
|
|
const b = Math.min(255, (num & 0xff) + Math.round(255 * percent / 100));
|
|
return `#${(1 << 24 | r << 16 | g << 8 | b).toString(16).slice(1)}`;
|
|
};
|
|
|
|
const glow = lighten(tempColor, 10);
|
|
document.documentElement.style.setProperty("--star-color", glow);
|
|
setTimeout(() => {
|
|
document.documentElement.style.setProperty("--star-color", this.originalColor);
|
|
}, duration);
|
|
}
|
|
|
|
showNotify(text, { duration = 3000, color = "white", fontSize = "1.2em" } = {}) {
|
|
// Create container if needed
|
|
if (!this._notifyContainer) {
|
|
this._notifyContainer = document.createElement("div");
|
|
this._notifyContainer.className = "star-notify-container";
|
|
document.body.appendChild(this._notifyContainer);
|
|
}
|
|
const messages = document.querySelectorAll('.star-notify');
|
|
|
|
const count = Array.from(messages).filter(el => el.textContent.trim() === text).length;
|
|
|
|
if (count) return;
|
|
const note = document.createElement("div");
|
|
note.className = "star-notify";
|
|
note.textContent = text;
|
|
note.style.color = color;
|
|
note.style.fontSize = fontSize;
|
|
|
|
this._notifyContainer.appendChild(note);
|
|
|
|
// Trigger animation
|
|
setTimeout(() => {
|
|
note.style.opacity = 1;
|
|
note.style.transform = "translateY(0)";
|
|
}, 10);
|
|
|
|
// Remove after duration
|
|
setTimeout(() => {
|
|
note.style.opacity = 0;
|
|
note.style.transform = "translateY(-10px)";
|
|
setTimeout(() => note.remove(), 500);
|
|
}, duration);
|
|
}
|
|
|
|
|
|
renderWord(word, { font = "bold", resolution = 8, duration = 1500, rainbow = false } = {}) {
|
|
const canvas = document.createElement("canvas");
|
|
const ctx = canvas.getContext("2d");
|
|
canvas.width = this.container.clientWidth;
|
|
canvas.height = this.container.clientHeight / 3;
|
|
|
|
const fontSize = Math.floor(canvas.height / 2.5);
|
|
ctx.font = `${fontSize}px sans-serif`;
|
|
ctx.fillStyle = "black";
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText(word, canvas.width / 2, canvas.height / 2);
|
|
|
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
|
|
const targetPositions = [];
|
|
|
|
for (let y = 0; y < canvas.height; y += resolution) {
|
|
for (let x = 0; x < canvas.width; x += resolution) {
|
|
const i = (y * canvas.width + x) * 4;
|
|
if (imageData[i + 3] > 128) {
|
|
targetPositions.push([
|
|
(x / canvas.width) * 100,
|
|
(y / canvas.height) * 100,
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
targetPositions.sort(() => Math.random() - 0.5);
|
|
const used = targetPositions.slice(0, this.stars.length);
|
|
|
|
this.stars.forEach((star, i) => {
|
|
star.style.transition = `top ${duration}ms ease, left ${duration}ms ease, opacity ${duration}ms ease, background-color 1s ease`;
|
|
if (i < used.length) {
|
|
const [left, top] = used[i];
|
|
star.style.left = `${left}%`;
|
|
star.style.top = `${top}%`;
|
|
star.style.opacity = 1;
|
|
if (rainbow) {
|
|
const hue = (i / used.length) * 360;
|
|
star.style.backgroundColor = `hsl(${hue}, 100%, 70%)`;
|
|
}
|
|
} else {
|
|
star.style.opacity = 0;
|
|
}
|
|
});
|
|
|
|
setTimeout(() => {
|
|
this.stars.forEach(star => {
|
|
star.position = this._getStarPosition(star);
|
|
if (rainbow) star.style.backgroundColor = "";
|
|
});
|
|
}, duration);
|
|
}
|
|
|
|
explodeAndReturn(duration = 1000) {
|
|
const originalPositions = this.stars.map(star => ({
|
|
left: star.style.left,
|
|
top: star.style.top
|
|
}));
|
|
|
|
this.stars.forEach(star => {
|
|
const angle = Math.random() * 2 * Math.PI;
|
|
const radius = Math.random() * 200;
|
|
const x = 50 + Math.cos(angle) * radius;
|
|
const y = 50 + Math.sin(angle) * radius;
|
|
star.style.transition = `top ${duration / 2}ms ease-out, left ${duration / 2}ms ease-out`;
|
|
star.style.left = `${x}%`;
|
|
star.style.top = `${y}%`;
|
|
});
|
|
|
|
setTimeout(() => {
|
|
this.stars.forEach((star, i) => {
|
|
star.style.transition = `top ${duration}ms ease-in, left ${duration}ms ease-in`;
|
|
star.style.left = originalPositions[i].left;
|
|
star.style.top = originalPositions[i].top;
|
|
});
|
|
}, duration / 2);
|
|
}
|
|
|
|
startColorCycle() {
|
|
let hue = 0;
|
|
if (this._colorInterval) clearInterval(this._colorInterval);
|
|
this._colorInterval = setInterval(() => {
|
|
hue = (hue + 2) % 360;
|
|
this.stars.forEach((star, i) => {
|
|
star.style.backgroundColor = `hsl(${(hue + i * 3) % 360}, 100%, 75%)`;
|
|
});
|
|
}, 100);
|
|
}
|
|
|
|
stopColorCycle() {
|
|
if (this._colorInterval) {
|
|
clearInterval(this._colorInterval);
|
|
this._colorInterval = null;
|
|
this.stars.forEach(star => star.style.backgroundColor = "");
|
|
}
|
|
}
|
|
|
|
addSpecialStar({ title, content, category = "Info", color = "gold", onClick }) {
|
|
const star = this.stars.find(s => !s._dataAttached);
|
|
if (!star) return;
|
|
|
|
star.classList.add("special");
|
|
star.style.backgroundColor = color;
|
|
star._dataAttached = true;
|
|
star._specialData = { title, content, category, color, onClick };
|
|
|
|
const tooltip = document.getElementById("star-tooltip");
|
|
const showTooltip = (e) => {
|
|
tooltip.innerText = `${title} (${category})`;
|
|
tooltip.style.display = "block";
|
|
tooltip.style.left = `${e.clientX + 10}px`;
|
|
tooltip.style.top = `${e.clientY + 10}px`;
|
|
};
|
|
const hideTooltip = () => tooltip.style.display = "none";
|
|
|
|
star.addEventListener("mouseenter", showTooltip);
|
|
star.addEventListener("mouseleave", hideTooltip);
|
|
star.addEventListener("mousemove", showTooltip);
|
|
|
|
const showPopup = (e) => {
|
|
e.stopPropagation();
|
|
this._showPopup(star, star._specialData);
|
|
if (onClick) onClick(star._specialData);
|
|
};
|
|
star.addEventListener("click", showPopup);
|
|
star.addEventListener("touchend", showPopup);
|
|
|
|
return star;
|
|
}
|
|
|
|
_showPopup(star, data) {
|
|
const popup = document.getElementById("star-popup");
|
|
popup.innerHTML = `<strong>${data.title}</strong><br><small>${data.category}</small><div style="margin-top: 5px;">${data.content}</div>`;
|
|
popup.style.display = "block";
|
|
|
|
const rect = star.getBoundingClientRect();
|
|
popup.style.left = `${rect.left + window.scrollX + 10}px`;
|
|
popup.style.top = `${rect.top + window.scrollY + 10}px`;
|
|
|
|
const closeHandler = () => {
|
|
popup.style.display = "none";
|
|
document.removeEventListener("click", closeHandler);
|
|
};
|
|
setTimeout(() => document.addEventListener("click", closeHandler), 100);
|
|
}
|
|
|
|
removeSpecialStar(star) {
|
|
if (!star._specialData) return;
|
|
|
|
star.classList.remove("special");
|
|
delete star._specialData;
|
|
star._dataAttached = false;
|
|
|
|
const clone = star.cloneNode(true);
|
|
star.replaceWith(clone);
|
|
|
|
const index = this.stars.indexOf(star);
|
|
if (index >= 0) this.stars[index] = clone;
|
|
}
|
|
|
|
getSpecialStars() {
|
|
return this.stars
|
|
.filter(star => star._specialData)
|
|
.map(star => ({
|
|
star,
|
|
data: star._specialData
|
|
}));
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const starField = new StarField({starCount: 200});
|
|
app.starField = starField;
|
|
|
|
class DemoSequence {
|
|
constructor(steps = []) {
|
|
this.steps = steps;
|
|
this.overlay = document.createElement("div");
|
|
this.overlay.className = "demo-overlay";
|
|
document.body.appendChild(this.overlay);
|
|
}
|
|
|
|
start() {
|
|
this._runStep(0);
|
|
}
|
|
|
|
_runStep(index) {
|
|
if (index >= this.steps.length) {
|
|
this._clearOverlay();
|
|
return;
|
|
}
|
|
|
|
const { target, text, duration = 3000 } = this.steps[index];
|
|
this._clearHighlights();
|
|
this._showOverlay(text);
|
|
|
|
if (target) {
|
|
const el = typeof target === 'string' ? document.querySelector(target) : target;
|
|
if (el) {
|
|
el.classList.remove("demo-highlight");
|
|
void el.offsetWidth; // force reflow to reset animation
|
|
el.classList.add("demo-highlight");
|
|
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
}
|
|
}
|
|
|
|
setTimeout(() => {
|
|
this._hideOverlay();
|
|
this._runStep(index + 1);
|
|
}, duration);
|
|
}
|
|
|
|
_showOverlay(text) {
|
|
this.overlay.innerText = text;
|
|
this.overlay.style.animation = "demoFadeIn 0.8s ease forwards";
|
|
}
|
|
|
|
_hideOverlay() {
|
|
this.overlay.style.opacity = 0;
|
|
this._clearHighlights();
|
|
}
|
|
|
|
_clearOverlay() {
|
|
this.overlay.remove();
|
|
this._clearHighlights();
|
|
}
|
|
|
|
_clearHighlights() {
|
|
document.querySelectorAll(".demo-highlight").forEach(el => {
|
|
el.classList.remove("demo-highlight");
|
|
});
|
|
}
|
|
}
|
|
|
|
/*
|
|
const demo = new DemoSequence([
|
|
{
|
|
text: "💬 Welcome to the Snek Developer Community!",
|
|
duration: 3000
|
|
},
|
|
{
|
|
target: ".channels-list",
|
|
text: "🔗 Channels help you organize conversations.",
|
|
duration: 3000
|
|
},
|
|
{
|
|
target: ".user-icon",
|
|
text: "👥 Invite team members here.",
|
|
duration: 3000
|
|
},
|
|
{
|
|
target: ".chat-input",
|
|
text: "⌨️ Type your message and hit Enter!",
|
|
duration: 3000
|
|
}
|
|
]);*/
|
|
|
|
// Start when ready (e.g., after load or user action)
|
|
//demo.start();
|
|
|
|
|
|
</script>
|