Update.
This commit is contained in:
parent
a55d15b635
commit
3bf09f9083
@ -3,6 +3,7 @@ import logging
|
|||||||
import pathlib
|
import pathlib
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
from snek import snode
|
from snek import snode
|
||||||
from snek.view.threads import ThreadsView
|
from snek.view.threads import ThreadsView
|
||||||
import json
|
import json
|
||||||
@ -96,6 +97,7 @@ class Application(BaseApplication):
|
|||||||
self.jinja2_env.add_extension(LinkifyExtension)
|
self.jinja2_env.add_extension(LinkifyExtension)
|
||||||
self.jinja2_env.add_extension(PythonExtension)
|
self.jinja2_env.add_extension(PythonExtension)
|
||||||
self.jinja2_env.add_extension(EmojiExtension)
|
self.jinja2_env.add_extension(EmojiExtension)
|
||||||
|
self.time_start = datetime.now()
|
||||||
self.ssh_host = "0.0.0.0"
|
self.ssh_host = "0.0.0.0"
|
||||||
self.ssh_port = 2242
|
self.ssh_port = 2242
|
||||||
self.setup_router()
|
self.setup_router()
|
||||||
@ -112,7 +114,34 @@ class Application(BaseApplication):
|
|||||||
self.on_startup.append(self.start_user_availability_service)
|
self.on_startup.append(self.start_user_availability_service)
|
||||||
self.on_startup.append(self.start_ssh_server)
|
self.on_startup.append(self.start_ssh_server)
|
||||||
self.on_startup.append(self.prepare_database)
|
self.on_startup.append(self.prepare_database)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uptime_seconds(self):
|
||||||
|
return (datetime.now() - self.time_start).total_seconds()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uptime(self):
|
||||||
|
return self._format_uptime(self.uptime_seconds)
|
||||||
|
|
||||||
|
def _format_uptime(self,seconds):
|
||||||
|
seconds = int(seconds)
|
||||||
|
days, seconds = divmod(seconds, 86400)
|
||||||
|
hours, seconds = divmod(seconds, 3600)
|
||||||
|
minutes, seconds = divmod(seconds, 60)
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
if days > 0:
|
||||||
|
parts.append(f"{days} day{'s' if days != 1 else ''}")
|
||||||
|
if hours > 0:
|
||||||
|
parts.append(f"{hours} hour{'s' if hours != 1 else ''}")
|
||||||
|
if minutes > 0:
|
||||||
|
parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}")
|
||||||
|
if seconds > 0 or not parts:
|
||||||
|
parts.append(f"{seconds} second{'s' if seconds != 1 else ''}")
|
||||||
|
|
||||||
|
return ", ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
async def start_user_availability_service(self, app):
|
async def start_user_availability_service(self, app):
|
||||||
app.user_availability_service_task = asyncio.create_task(app.services.socket.user_availability_service())
|
app.user_availability_service_task = asyncio.create_task(app.services.socket.user_availability_service())
|
||||||
async def snode_sync(self, app):
|
async def snode_sync(self, app):
|
||||||
|
@ -66,7 +66,7 @@ export class Chat extends EventHandler {
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this._waitConnect = resolve;
|
this._waitConnect = resolve;
|
||||||
console.debug("Connecting..");
|
console.debug("Connecting..");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this._socket = new WebSocket(this._url);
|
this._socket = new WebSocket(this._url);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -196,7 +196,9 @@ export class App extends EventHandler {
|
|||||||
this.ws.addEventListener("connected", (data) => {
|
this.ws.addEventListener("connected", (data) => {
|
||||||
this.ping("online");
|
this.ping("online");
|
||||||
});
|
});
|
||||||
|
this.ws.addEventListener("reconnecting", (data) => {
|
||||||
|
this.starField?.showNotify("Connecting..","#CC0000")
|
||||||
|
})
|
||||||
this.ws.addEventListener("channel-message", (data) => {
|
this.ws.addEventListener("channel-message", (data) => {
|
||||||
me.emit("channel-message", data);
|
me.emit("channel-message", data);
|
||||||
});
|
});
|
||||||
|
@ -1,42 +1,178 @@
|
|||||||
|
:root {
|
||||||
|
--star-color: white;
|
||||||
|
--background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
.star {
|
body.day {
|
||||||
|
--star-color: #444;
|
||||||
|
--background-color: #e6f0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.night {
|
||||||
|
--star-color: white;
|
||||||
|
--background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
transition: background-color 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--star-color);
|
||||||
|
animation: twinkle 2s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes twinkle {
|
||||||
|
0%, 100% { opacity: 0.8; transform: scale(1); }
|
||||||
|
50% { opacity: 1; transform: scale(1.2); }
|
||||||
|
}
|
||||||
|
|
||||||
|
#themeToggle {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star.special {
|
||||||
|
box-shadow: 0 0 10px 3px gold;
|
||||||
|
transform: scale(1.4);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-tooltip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 2px;
|
font-size: 12px;
|
||||||
height: 2px;
|
color: white;
|
||||||
background: var(--star-color, #fff);
|
|
||||||
border-radius: 50%;
|
|
||||||
opacity: 0;
|
|
||||||
transition: background 0.5s ease;
|
|
||||||
animation: twinkle ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes twinkle {
|
|
||||||
0%, 100% { opacity: 0; }
|
|
||||||
50% { opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes star-glow-frames {
|
|
||||||
0% {
|
|
||||||
box-shadow: 0 0 5px --star-color;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
box-shadow: 0 0 20px --star-color, 0 0 30px --star-color;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
box-shadow: 0 0 5px --star-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.star-glow {
|
|
||||||
animation: star-glow-frames 1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
color: var(--star-content-color, #eee);
|
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
text-align: center;
|
pointer-events: none;
|
||||||
top: 40%;
|
z-index: 9999;
|
||||||
transform: translateY(-40%);
|
white-space: nowrap;
|
||||||
|
text-shadow: 1px 1px 2px black;
|
||||||
|
display: none;
|
||||||
|
padding: 2px 6px;
|
||||||
}
|
}
|
||||||
|
.star-popup {
|
||||||
|
position: absolute;
|
||||||
|
max-width: 300px;
|
||||||
|
color: #fff;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
z-index: 10000;
|
||||||
|
text-shadow: 1px 1px 3px black;
|
||||||
|
display: none;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.star:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-popup {
|
||||||
|
position: absolute;
|
||||||
|
max-width: 300px;
|
||||||
|
background: white;
|
||||||
|
color: black;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 20px rgba(0,0,0,0.3);
|
||||||
|
z-index: 10000;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-popup h3 {
|
||||||
|
margin: 0 0 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-popup button {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: 3em;
|
||||||
|
color: white;
|
||||||
|
font-family: 'Segoe UI', sans-serif;
|
||||||
|
font-weight: 300;
|
||||||
|
text-align: center;
|
||||||
|
text-shadow: 0 0 20px rgba(0,0,0,0.8);
|
||||||
|
z-index: 9999;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.6s ease;
|
||||||
|
max-width: 80vw;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes demoFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -60%) scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes demoPulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 rgba(255, 255, 150, 0);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
box-shadow: 0 0 30px 15px rgba(255, 255, 150, 0.9);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 rgba(255, 255, 150, 0);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-highlight {
|
||||||
|
animation: demoPulse 1.5s ease-out;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-notify-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 50px;
|
||||||
|
right: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
z-index: 9999;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-notify {
|
||||||
|
opacity: 0;
|
||||||
|
background: transparent;
|
||||||
|
padding: 5px 10px;
|
||||||
|
color: white;
|
||||||
|
font-weight: 300;
|
||||||
|
text-shadow: 0 0 10px rgba(0,0,0,0.7);
|
||||||
|
transition: opacity 0.5s ease, transform 0.5s ease;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
font-family: 'Segoe UI', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -95,7 +95,8 @@ export class Socket extends EventHandler {
|
|||||||
if (this.shouldReconnect)
|
if (this.shouldReconnect)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.log("Reconnecting");
|
console.log("Reconnecting");
|
||||||
return this.connect();
|
this.emit("reconnecting");
|
||||||
|
return this.connect();
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,504 +1,392 @@
|
|||||||
|
<div id="star-tooltip" class="star-tooltip"></div>
|
||||||
|
<div id="star-popup" class="star-popup"></div>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { app } from "/app.js";
|
import { app } from "/app.js";
|
||||||
|
|
||||||
const STAR_COUNT = 200;
|
|
||||||
const body = document.body;
|
|
||||||
|
|
||||||
function getStarPosition(star) {
|
|
||||||
const leftPercent = parseFloat(star.style.left);
|
|
||||||
const topPercent = parseFloat(star.style.top);
|
|
||||||
|
|
||||||
let position;
|
|
||||||
|
|
||||||
if (topPercent < 40 && leftPercent >= 40 && leftPercent <= 60) {
|
|
||||||
position = 'North';
|
|
||||||
} else if (topPercent > 60 && leftPercent >= 40 && leftPercent <= 60) {
|
|
||||||
position = 'South';
|
|
||||||
} else if (leftPercent < 40 && topPercent >= 40 && topPercent <= 60) {
|
|
||||||
position = 'West';
|
|
||||||
} else if (leftPercent > 60 && topPercent >= 40 && topPercent <= 60) {
|
|
||||||
position = 'East';
|
|
||||||
} else if (topPercent >= 40 && topPercent <= 60 && leftPercent >= 40 && leftPercent <= 60) {
|
|
||||||
position = 'Center';
|
|
||||||
} else {
|
|
||||||
position = 'Corner or Edge';
|
|
||||||
}
|
|
||||||
return position
|
|
||||||
}
|
|
||||||
let stars = {}
|
|
||||||
window.stars = stars
|
|
||||||
|
|
||||||
|
|
||||||
function createStar() {
|
|
||||||
const star = document.createElement('div');
|
|
||||||
star.classList.add('star');
|
|
||||||
star.style.left = `${Math.random() * 100}%`;
|
|
||||||
star.style.top = `${Math.random() * 100}%`;
|
|
||||||
star.shuffle = () => {
|
|
||||||
star.style.left = `${Math.random() * 100}%`;
|
|
||||||
star.style.top = `${Math.random() * 100}%`;
|
|
||||||
|
|
||||||
star.position = getStarPosition(star)
|
|
||||||
}
|
|
||||||
star.position = getStarPosition(star)
|
|
||||||
|
|
||||||
function moveStarToPosition(star, position) {
|
|
||||||
let top, left;
|
|
||||||
|
|
||||||
switch (position) {
|
|
||||||
case 'North':
|
|
||||||
top = `${Math.random() * 20}%`;
|
|
||||||
left = `${40 + Math.random() * 20}%`;
|
|
||||||
break;
|
|
||||||
case 'South':
|
|
||||||
top = `${80 + Math.random() * 10}%`;
|
|
||||||
left = `${40 + Math.random() * 20}%`;
|
|
||||||
break;
|
|
||||||
case 'West':
|
|
||||||
top = `${40 + Math.random() * 20}%`;
|
|
||||||
left = `${Math.random() * 20}%`;
|
|
||||||
break;
|
|
||||||
case 'East':
|
|
||||||
top = `${40 + Math.random() * 20}%`;
|
|
||||||
left = `${80 + Math.random() * 10}%`;
|
|
||||||
break;
|
|
||||||
case 'Center':
|
|
||||||
top = `${45 + Math.random() * 10}%`;
|
|
||||||
left = `${45 + Math.random() * 10}%`;
|
|
||||||
break;
|
|
||||||
default: // 'Corner or Edge' fallback
|
|
||||||
top = `${Math.random() * 100}%`;
|
|
||||||
left = `${Math.random() * 100}%`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
star.style.top = top;
|
|
||||||
star.style.left = left;
|
|
||||||
|
|
||||||
star.position = getStarPosition(star)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if(!stars[star.position])
|
|
||||||
stars[star.position] = []
|
|
||||||
stars[star.position].push(star)
|
|
||||||
const size = Math.random() * 2 + 1;
|
|
||||||
star.style.width = `${size}px`;
|
|
||||||
star.style.height = `${size}px`;
|
|
||||||
const duration = Math.random() * 3 + 2;
|
|
||||||
const delay = Math.random() * 5;
|
|
||||||
star.style.animationDuration = `${duration}s`;
|
|
||||||
star.style.animationDelay = `${delay}s`;
|
|
||||||
body.appendChild(star);
|
|
||||||
}
|
|
||||||
|
|
||||||
Array.from({ length: STAR_COUNT }, createStar);
|
|
||||||
|
|
||||||
function lightenColor(hex, percent) {
|
|
||||||
const num = parseInt(hex.replace("#", ""), 16);
|
|
||||||
let r = (num >> 16) + Math.round(255 * percent / 100);
|
|
||||||
let g = ((num >> 8) & 0x00FF) + Math.round(255 * percent / 100);
|
|
||||||
let b = (num & 0x0000FF) + Math.round(255 * percent / 100);
|
|
||||||
r = Math.min(255, r);
|
|
||||||
g = Math.min(255, g);
|
|
||||||
b = Math.min(255, b);
|
|
||||||
return `#${(1 << 24 | r << 16 | g << 8 | b).toString(16).slice(1)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalColor = document.documentElement.style.getPropertyValue("--star-color").trim();
|
|
||||||
|
|
||||||
function glowCSSVariable(varName, glowColor, duration = 500) {
|
|
||||||
const root = document.documentElement;
|
|
||||||
|
|
||||||
//igetComputedStyle(root).getPropertyValue(varName).trim();
|
|
||||||
glowColor = lightenColor(glowColor, 10);
|
|
||||||
root.style.setProperty(varName, glowColor);
|
|
||||||
setTimeout(() => {
|
|
||||||
root.style.setProperty(varName, originalColor);
|
|
||||||
}, duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateStarColorDelayed(color) {
|
|
||||||
glowCSSVariable('--star-color', color, 2500);
|
|
||||||
}
|
|
||||||
app.updateStarColor = updateStarColorDelayed;
|
|
||||||
app.ws.addEventListener("set_typing", (data) => {
|
|
||||||
updateStarColorDelayed(data.color);
|
|
||||||
});
|
|
||||||
window.createAvatar = () => {
|
|
||||||
let avatar = document.createElement("avatar-face")
|
|
||||||
document.querySelector("main").appendChild(avatar)
|
|
||||||
return avatar
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarFace extends HTMLElement {
|
|
||||||
static get observedAttributes(){
|
|
||||||
return ['emotion','face-color','eye-color','text','balloon-color','text-color'];
|
|
||||||
}
|
|
||||||
constructor(){
|
|
||||||
super();
|
|
||||||
this._shadow = this.attachShadow({mode:'open'});
|
|
||||||
this._shadow.innerHTML = `
|
|
||||||
<style>
|
|
||||||
:host { display:block; position:relative; }
|
|
||||||
canvas { width:100%; height:100%; display:block; }
|
|
||||||
</style>
|
|
||||||
<canvas></canvas>
|
|
||||||
`;
|
|
||||||
this._c = this._shadow.querySelector('canvas');
|
|
||||||
this._ctx = this._c.getContext('2d');
|
|
||||||
|
|
||||||
// state
|
|
||||||
this._mouse = {x:0,y:0};
|
|
||||||
this._blinkTimer = 0;
|
|
||||||
this._blinking = false;
|
|
||||||
this._lastTime = 0;
|
|
||||||
|
|
||||||
// defaults
|
|
||||||
this._emotion = 'neutral';
|
|
||||||
this._faceColor = '#ffdfba';
|
|
||||||
this._eyeColor = '#000';
|
|
||||||
this._text = '';
|
|
||||||
this._balloonColor = '#fff';
|
|
||||||
this._textColor = '#000';
|
|
||||||
}
|
|
||||||
|
|
||||||
attributeChangedCallback(name,_old,newV){
|
|
||||||
if (name==='emotion') this._emotion = newV||'neutral';
|
|
||||||
else if (name==='face-color') this._faceColor = newV||'#ffdfba';
|
|
||||||
else if (name==='eye-color') this._eyeColor = newV||'#000';
|
|
||||||
else if (name==='text') this._text = newV||'';
|
|
||||||
else if (name==='balloon-color')this._balloonColor = newV||'#fff';
|
|
||||||
else if (name==='text-color') this._textColor = newV||'#000';
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback(){
|
|
||||||
// watch size so canvas buffer matches display
|
|
||||||
this._ro = new ResizeObserver(entries=>{
|
|
||||||
for(const ent of entries){
|
|
||||||
const w = ent.contentRect.width;
|
|
||||||
const h = ent.contentRect.height;
|
|
||||||
const dpr = devicePixelRatio||1;
|
|
||||||
this._c.width = w*dpr;
|
|
||||||
this._c.height = h*dpr;
|
|
||||||
this._ctx.scale(dpr,dpr);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this._ro.observe(this);
|
|
||||||
|
|
||||||
// track mouse so eyes follow
|
|
||||||
this._shadow.addEventListener('mousemove', e=>{
|
|
||||||
const r = this._c.getBoundingClientRect();
|
|
||||||
this._mouse.x = e.clientX - r.left;
|
|
||||||
this._mouse.y = e.clientY - r.top;
|
|
||||||
});
|
|
||||||
|
|
||||||
this._lastTime = performance.now();
|
|
||||||
this._raf = requestAnimationFrame(t=>this._loop(t));
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback(){
|
|
||||||
cancelAnimationFrame(this._raf);
|
|
||||||
this._ro.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateBlink(dt){
|
|
||||||
this._blinkTimer -= dt;
|
|
||||||
if (this._blinkTimer<=0){
|
|
||||||
this._blinking = !this._blinking;
|
|
||||||
this._blinkTimer = this._blinking
|
|
||||||
? 0.1
|
|
||||||
: 2 + Math.random()*3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_roundRect(x,y,w,h,r){
|
|
||||||
const ctx = this._ctx;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(x+r,y);
|
|
||||||
ctx.lineTo(x+w-r,y);
|
|
||||||
ctx.quadraticCurveTo(x+w,y, x+w,y+r);
|
|
||||||
ctx.lineTo(x+w,y+h-r);
|
|
||||||
ctx.quadraticCurveTo(x+w,y+h, x+w-r,y+h);
|
|
||||||
ctx.lineTo(x+r,y+h);
|
|
||||||
ctx.quadraticCurveTo(x,y+h, x,y+h-r);
|
|
||||||
ctx.lineTo(x,y+r);
|
|
||||||
ctx.quadraticCurveTo(x,y, x+r,y);
|
|
||||||
ctx.closePath();
|
|
||||||
}
|
|
||||||
|
|
||||||
_draw(ts){
|
|
||||||
const ctx = this._ctx;
|
|
||||||
const W = this._c.clientWidth;
|
|
||||||
const H = this._c.clientHeight;
|
|
||||||
ctx.clearRect(0,0,W,H);
|
|
||||||
|
|
||||||
// HEAD + BOB
|
|
||||||
const cx = W/2;
|
|
||||||
const cy = H/2 + Math.sin(ts*0.002)*8;
|
|
||||||
const R = Math.min(W,H)*0.25;
|
|
||||||
|
|
||||||
// SPEECH BALLOON
|
|
||||||
if (this._text){
|
|
||||||
const pad = 6;
|
|
||||||
ctx.font = `${R*0.15}px sans-serif`;
|
|
||||||
const m = ctx.measureText(this._text);
|
|
||||||
const tw = m.width, th = R*0.18;
|
|
||||||
const bw = tw + pad*2, bh = th + pad*2;
|
|
||||||
const bx = cx - bw/2, by = cy - R - bh - 10;
|
|
||||||
// bubble
|
|
||||||
ctx.fillStyle = this._balloonColor;
|
|
||||||
this._roundRect(bx,by,bw,bh,6);
|
|
||||||
ctx.fill();
|
|
||||||
ctx.strokeStyle = '#888';
|
|
||||||
ctx.lineWidth = 1.2;
|
|
||||||
ctx.stroke();
|
|
||||||
// tail
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(cx-6, by+bh);
|
|
||||||
ctx.lineTo(cx+6, by+bh);
|
|
||||||
ctx.lineTo(cx, cy-R+4);
|
|
||||||
ctx.closePath();
|
|
||||||
ctx.fill();
|
|
||||||
ctx.stroke();
|
|
||||||
// text
|
|
||||||
ctx.fillStyle = this._textColor;
|
|
||||||
ctx.textBaseline = 'top';
|
|
||||||
ctx.fillText(this._text, bx+pad, by+pad);
|
|
||||||
}
|
|
||||||
|
|
||||||
// FACE
|
|
||||||
ctx.fillStyle = this._faceColor;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(cx,cy,R,0,2*Math.PI);
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
// EYES
|
|
||||||
const eyeY = cy - R*0.2;
|
|
||||||
const eyeX = R*0.4;
|
|
||||||
const eyeR= R*0.12;
|
|
||||||
const pupR= eyeR*0.5;
|
|
||||||
|
|
||||||
for(let i=0;i<2;i++){
|
|
||||||
const ex = cx + (i? eyeX:-eyeX);
|
|
||||||
const ey = eyeY;
|
|
||||||
// eyeball
|
|
||||||
ctx.fillStyle = '#fff';
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(ex,ey,eyeR,0,2*Math.PI);
|
|
||||||
ctx.fill();
|
|
||||||
// pupil follows
|
|
||||||
let dx = this._mouse.x - ex;
|
|
||||||
let dy = this._mouse.y - ey;
|
|
||||||
const d = Math.hypot(dx,dy);
|
|
||||||
const max = eyeR - pupR - 2;
|
|
||||||
if (d>max){ dx=dx/d*max; dy=dy/d*max; }
|
|
||||||
if (this._blinking){
|
|
||||||
ctx.strokeStyle='#000';
|
|
||||||
ctx.lineWidth=3;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(ex-eyeR,ey);
|
|
||||||
ctx.lineTo(ex+eyeR,ey);
|
|
||||||
ctx.stroke();
|
|
||||||
} else {
|
|
||||||
ctx.fillStyle = this._eyeColor;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(ex+dx,ey+dy,pupR,0,2*Math.PI);
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ANGRY BROWS
|
|
||||||
if (this._emotion==='angry'){
|
|
||||||
ctx.strokeStyle='#000';
|
|
||||||
ctx.lineWidth=4;
|
|
||||||
[[-eyeX,1],[ eyeX,-1]].forEach(([off,dir])=>{
|
|
||||||
const sx = cx+off - eyeR;
|
|
||||||
const sy = eyeY - eyeR*1.3;
|
|
||||||
const ex = cx+off + eyeR;
|
|
||||||
const ey2= sy + dir*6;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(sx,sy);
|
|
||||||
ctx.lineTo(ex,ey2);
|
|
||||||
ctx.stroke();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// MOUTH by emotion
|
|
||||||
const mw = R*0.6;
|
|
||||||
const my = cy + R*0.25;
|
|
||||||
ctx.strokeStyle='#a33';
|
|
||||||
ctx.lineWidth=4;
|
|
||||||
|
|
||||||
if (this._emotion==='surprised'){
|
|
||||||
ctx.fillStyle='#a33';
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(cx,my,mw*0.3,0,2*Math.PI);
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
else if (this._emotion==='sad'){
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(cx,my,mw/2,1.15*Math.PI,1.85*Math.PI,true);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
else if (this._emotion==='angry'){
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(cx-mw/2,my+2);
|
|
||||||
ctx.lineTo(cx+mw/2,my-2);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const s = this._emotion==='happy'? 0.15*Math.PI:0.2*Math.PI;
|
|
||||||
const e = this._emotion==='happy'? 0.85*Math.PI:0.8*Math.PI;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(cx,my,mw/2,s,e);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_loop(ts){
|
|
||||||
const dt = (ts - this._lastTime)/1000;
|
|
||||||
this._lastTime = ts;
|
|
||||||
this._updateBlink(dt);
|
|
||||||
this._draw(ts);
|
|
||||||
this._raf = requestAnimationFrame(t=>this._loop(t));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customElements.define('avatar-face', AvatarFace);
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarReplacer {
|
|
||||||
constructor(target, opts={}){
|
|
||||||
this.target = target;
|
|
||||||
// record original inline styles so we can restore
|
|
||||||
this._oldVis = target.style.visibility || '';
|
|
||||||
this._oldPos = target.style.position || '';
|
|
||||||
// hide the target
|
|
||||||
target.style.visibility = 'hidden';
|
|
||||||
// measure
|
|
||||||
const rect = target.getBoundingClientRect();
|
|
||||||
// create avatar
|
|
||||||
this.avatar = document.createElement('avatar-face');
|
|
||||||
// copy all supported opts into attributes
|
|
||||||
['emotion','faceColor','eyeColor','text','balloonColor','textColor']
|
|
||||||
.forEach(k => {
|
|
||||||
const attr = k.replace(/[A-Z]/g,m=>'-'+m.toLowerCase());
|
|
||||||
if (opts[k] != null) this.avatar.setAttribute(attr, opts[k]);
|
|
||||||
});
|
|
||||||
// position absolutely
|
|
||||||
const scrollX = window.pageXOffset;
|
|
||||||
const scrollY = window.pageYOffset;
|
|
||||||
Object.assign(this.avatar.style, {
|
|
||||||
position: 'absolute',
|
|
||||||
left: (rect.left + scrollX) + 'px',
|
|
||||||
top: (rect.top + scrollY) + 'px',
|
|
||||||
width: rect.width + 'px',
|
|
||||||
height: rect.height + 'px',
|
|
||||||
zIndex: 9999
|
|
||||||
});
|
|
||||||
document.body.appendChild(this.avatar);
|
|
||||||
}
|
|
||||||
|
|
||||||
detach(){
|
|
||||||
// remove avatar and restore target
|
|
||||||
if (this.avatar && this.avatar.parentNode) {
|
|
||||||
this.avatar.parentNode.removeChild(this.avatar);
|
|
||||||
this.avatar = null;
|
|
||||||
}
|
|
||||||
this.target.style.visibility = this._oldVis;
|
|
||||||
this.target.style.position = this._oldPos;
|
|
||||||
}
|
|
||||||
|
|
||||||
// static convenience method
|
|
||||||
static attach(target, opts){
|
|
||||||
return new AvatarReplacer(target, opts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
// DEMO wiring
|
|
||||||
const btnGo = document.getElementById('go');
|
|
||||||
const btnReset = document.getElementById('reset');
|
|
||||||
let repl1, repl2;
|
|
||||||
|
|
||||||
btnGo.addEventListener('click', ()=>{
|
|
||||||
// replace #one with a happy avatar saying "Hi!"
|
|
||||||
repl1 = AvatarReplacer.attach(
|
|
||||||
document.getElementById('one'),
|
|
||||||
{emotion:'happy', text:'Hi!', balloonColor:'#fffbdd', textColor:'#333'}
|
|
||||||
);
|
|
||||||
// replace #two with a surprised avatar
|
|
||||||
repl2 = AvatarReplacer.attach(
|
|
||||||
document.getElementById('two'),
|
|
||||||
{emotion:'surprised', faceColor:'#eeffcc', text:'Wow!', balloonColor:'#ddffdd'}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
btnReset.addEventListener('click', ()=>{
|
|
||||||
if (repl1) repl1.detach();
|
|
||||||
if (repl2) repl2.detach();
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
class StarField {
|
class StarField {
|
||||||
constructor(container = document.body, options = {}) {
|
constructor({ count = 200, container = document.body } = {}) {
|
||||||
this.container = container;
|
this.container = container;
|
||||||
|
this.starCount = count;
|
||||||
this.stars = [];
|
this.stars = [];
|
||||||
this.setOptions(options);
|
this.positionMap = {};
|
||||||
|
this.originalColor = getComputedStyle(document.documentElement).getPropertyValue("--star-color").trim();
|
||||||
|
this._createStars();
|
||||||
|
window.stars = this.positionMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
setOptions({
|
_getStarPosition(star) {
|
||||||
starCount = 200,
|
const left = parseFloat(star.style.left);
|
||||||
minSize = 1,
|
const top = parseFloat(star.style.top);
|
||||||
maxSize = 3,
|
if (top < 40 && left >= 40 && left <= 60) return "North";
|
||||||
speed = 5,
|
if (top > 60 && left >= 40 && left <= 60) return "South";
|
||||||
color = "white"
|
if (left < 40 && top >= 40 && top <= 60) return "West";
|
||||||
}) {
|
if (left > 60 && top >= 40 && top <= 60) return "East";
|
||||||
this.options = { starCount, minSize, maxSize, speed, color };
|
if (top >= 40 && top <= 60 && left >= 40 && left <= 60) return "Center";
|
||||||
|
return "Corner or Edge";
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
_createStars() {
|
||||||
this.stars.forEach(star => star.remove());
|
for (let i = 0; i < this.starCount; i++) {
|
||||||
this.stars = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
generate() {
|
|
||||||
this.clear();
|
|
||||||
const { starCount, minSize, maxSize, speed, color } = this.options;
|
|
||||||
|
|
||||||
for (let i = 0; i < starCount; i++) {
|
|
||||||
const star = document.createElement("div");
|
const star = document.createElement("div");
|
||||||
star.classList.add("star");
|
star.classList.add("star");
|
||||||
const size = Math.random() * (maxSize - minSize) + minSize;
|
this._randomizeStar(star);
|
||||||
|
this._placeStar(star);
|
||||||
Object.assign(star.style, {
|
|
||||||
left: `${Math.random() * 100}%`,
|
|
||||||
top: `${Math.random() * 100}%`,
|
|
||||||
width: `${size}px`,
|
|
||||||
height: `${size}px`,
|
|
||||||
backgroundColor: color,
|
|
||||||
position: "absolute",
|
|
||||||
borderRadius: "50%",
|
|
||||||
opacity: "0.8",
|
|
||||||
animation: `twinkle ${speed}s ease-in-out infinite`,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.container.appendChild(star);
|
this.container.appendChild(star);
|
||||||
this.stars.push(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
_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);
|
||||||
}
|
}
|
||||||
|
|
||||||
const starField = new StarField(document.body, {
|
|
||||||
starCount: 200,
|
renderWord(word, { font = "bold", resolution = 8, duration = 1500, rainbow = false } = {}) {
|
||||||
minSize: 1,
|
const canvas = document.createElement("canvas");
|
||||||
maxSize: 3,
|
const ctx = canvas.getContext("2d");
|
||||||
speed: 5,
|
canvas.width = this.container.clientWidth;
|
||||||
color: "white"
|
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>
|
</script>
|
||||||
|
@ -37,7 +37,21 @@
|
|||||||
showHelp();
|
showHelp();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
app.ws.addEventListener("refresh", (data) => {
|
||||||
|
app.starField.showNotify(data.message);
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
},4000)
|
||||||
|
})
|
||||||
|
app.ws.addEventListener("deployed", (data) => {
|
||||||
|
app.starField.renderWord("Deployed",{"rainbow":true,"resolution":8});
|
||||||
|
setTimeout(() => {
|
||||||
|
app.starField.shuffleAll(5000);
|
||||||
|
},10000)
|
||||||
|
})
|
||||||
|
app.ws.addEventListener("starfield.render_word", (data) => {
|
||||||
|
app.starField.renderWord(data.word,data);
|
||||||
|
})
|
||||||
const textBox = document.querySelector("chat-input").textarea
|
const textBox = document.querySelector("chat-input").textarea
|
||||||
textBox.addEventListener("paste", async (e) => {
|
textBox.addEventListener("paste", async (e) => {
|
||||||
try {
|
try {
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import traceback
|
import traceback
|
||||||
|
import asyncio
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from snek.system.model import now
|
from snek.system.model import now
|
||||||
@ -21,13 +21,29 @@ import logging
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class RPCView(BaseView):
|
class RPCView(BaseView):
|
||||||
|
|
||||||
class RPCApi:
|
class RPCApi:
|
||||||
def __init__(self, view, ws):
|
def __init__(self, view, ws):
|
||||||
self.view = view
|
self.view = view
|
||||||
self.app = self.view.app
|
self.app = self.view.app
|
||||||
self.services = self.app.services
|
self.services = self.app.services
|
||||||
self.ws = ws
|
self.ws = ws
|
||||||
|
self.user_session = {}
|
||||||
|
|
||||||
|
async def _session_ensure(self):
|
||||||
|
uid = await self.view.session_get("uid")
|
||||||
|
if not uid in self.user_session:
|
||||||
|
self.user_session[uid] = {
|
||||||
|
"said_hello": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def session_get(self, key, default):
|
||||||
|
await self._session_ensure()
|
||||||
|
return self.user_session[self.user_uid].get(key, default)
|
||||||
|
|
||||||
|
async def session_set(self, key, value):
|
||||||
|
await self._session_ensure()
|
||||||
|
self.user_session[self.user_uid][key] = value
|
||||||
|
return True
|
||||||
|
|
||||||
async def db_insert(self, table_name, record):
|
async def db_insert(self, table_name, record):
|
||||||
self._require_login()
|
self._require_login()
|
||||||
@ -323,6 +339,11 @@ class RPCView(BaseView):
|
|||||||
async for record in self.services.channel.get_users(channel_uid)
|
async for record in self.services.channel.get_users(channel_uid)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
async def _schedule(self, uid, seconds, call):
|
||||||
|
await asyncio.sleep(seconds)
|
||||||
|
await self.services.socket.send_to_user(uid, call)
|
||||||
|
|
||||||
|
|
||||||
async def ping(self, callId, *args):
|
async def ping(self, callId, *args):
|
||||||
if self.user_uid:
|
if self.user_uid:
|
||||||
user = await self.services.user.get(uid=self.user_uid)
|
user = await self.services.user.get(uid=self.user_uid)
|
||||||
@ -330,7 +351,14 @@ class RPCView(BaseView):
|
|||||||
await self.services.user.save(user)
|
await self.services.user.save(user)
|
||||||
return {"pong": args}
|
return {"pong": args}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def get(self):
|
async def get(self):
|
||||||
|
async def schedule(uid, seconds, call):
|
||||||
|
await asyncio.sleep(seconds)
|
||||||
|
await self.services.socket.send_to_user(uid, call)
|
||||||
|
|
||||||
ws = web.WebSocketResponse()
|
ws = web.WebSocketResponse()
|
||||||
await ws.prepare(self.request)
|
await ws.prepare(self.request)
|
||||||
if self.request.session.get("logged_in"):
|
if self.request.session.get("logged_in"):
|
||||||
@ -343,6 +371,16 @@ class RPCView(BaseView):
|
|||||||
await self.services.socket.subscribe(
|
await self.services.socket.subscribe(
|
||||||
ws, subscription["channel_uid"], self.request.session.get("uid")
|
ws, subscription["channel_uid"], self.request.session.get("uid")
|
||||||
)
|
)
|
||||||
|
if self.request.app.uptime_seconds < 10:
|
||||||
|
await schedule(self.request.session.get("uid"),1,{"event":"refresh", "data": {
|
||||||
|
"message": "Finishing deployment"}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await schedule(self.request.session.get("uid"),10,{"event": "deployed", "data": {
|
||||||
|
"uptime": self.request.app.uptime}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
rpc = RPCView.RPCApi(self, ws)
|
rpc = RPCView.RPCApi(self, ws)
|
||||||
async for msg in ws:
|
async for msg in ws:
|
||||||
if msg.type == web.WSMsgType.TEXT:
|
if msg.type == web.WSMsgType.TEXT:
|
||||||
|
Loading…
Reference in New Issue
Block a user