lthn.io/resources/js/app.js
Claude 77cc45dd83
feat: lthn.io CorePHP app — TLD website + blockchain services
Modules:
- Chain: daemon RPC client (DaemonRpc singleton, cached queries)
- Explorer: block browser, tx viewer, alias directory, search, stats API
- Names: .lthn TLD registrar portal (availability check, lookup, directory)
- Trade: scaffold (DEX frontend + API)
- Pool: scaffold (mining pool dashboard)

Replaces 5 Node.js containers (5.9GB) with one FrankenPHP app.
Built on CorePHP framework pattern from host.uk.com.

Co-Authored-By: Charon <charon@lethean.io>
2026-04-03 16:13:55 +01:00

169 lines
5.9 KiB
JavaScript

import './bootstrap';
import '@tailwindplus/elements';
// Particle animation - floating whispy dots that respond to mouse
class ParticleAnimation {
constructor(el, { quantity = 30, staticity = 50, ease = 50 } = {}) {
this.canvas = el;
if (!this.canvas) return;
this.canvasContainer = this.canvas.parentElement;
this.context = this.canvas.getContext('2d');
this.dpr = window.devicePixelRatio || 1;
this.settings = {
quantity: quantity,
staticity: staticity,
ease: ease,
};
this.circles = [];
this.mouse = {
x: 0,
y: 0,
};
this.canvasSize = {
w: 0,
h: 0,
};
this.onMouseMove = this.onMouseMove.bind(this);
this.initCanvas = this.initCanvas.bind(this);
this.resizeCanvas = this.resizeCanvas.bind(this);
this.drawCircle = this.drawCircle.bind(this);
this.drawParticles = this.drawParticles.bind(this);
this.remapValue = this.remapValue.bind(this);
this.animate = this.animate.bind(this);
this.init();
}
init() {
this.initCanvas();
this.animate();
window.addEventListener('resize', this.initCanvas);
window.addEventListener('mousemove', this.onMouseMove);
}
initCanvas() {
this.resizeCanvas();
this.drawParticles();
}
onMouseMove(event) {
const { clientX, clientY } = event;
const rect = this.canvas.getBoundingClientRect();
const { w, h } = this.canvasSize;
const x = clientX - rect.left - (w / 2);
const y = clientY - rect.top - (h / 2);
const inside = x < (w / 2) && x > -(w / 2) && y < (h / 2) && y > -(h / 2);
if(inside) {
this.mouse.x = x;
this.mouse.y = y;
}
}
resizeCanvas() {
this.circles.length = 0;
this.canvasSize.w = this.canvasContainer.offsetWidth;
this.canvasSize.h = this.canvasContainer.offsetHeight;
this.canvas.width = this.canvasSize.w * this.dpr;
this.canvas.height = this.canvasSize.h * this.dpr;
this.canvas.style.width = this.canvasSize.w + 'px';
this.canvas.style.height = this.canvasSize.h + 'px';
this.context.scale(this.dpr, this.dpr);
}
circleParams() {
const x = Math.floor(Math.random() * this.canvasSize.w);
const y = Math.floor(Math.random() * this.canvasSize.h);
const translateX = 0;
const translateY = 0;
const size = Math.floor(Math.random() * 2) + 1;
const alpha = 0;
const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1));
const dx = (Math.random() - 0.5) * 0.2;
const dy = (Math.random() - 0.5) * 0.2;
const magnetism = 0.1 + Math.random() * 4;
const colorIndex = Math.floor(Math.random() * 4);
return { x, y, translateX, translateY, size, alpha, targetAlpha, dx, dy, magnetism, colorIndex };
}
drawCircle(circle, update = false) {
const { x, y, translateX, translateY, size, alpha } = circle;
this.context.translate(translateX, translateY);
this.context.beginPath();
this.context.arc(x, y, size, 0, 2 * Math.PI);
// Use Raven logo colors - mix of #66c (102, 102, 204) and #336 (51, 51, 102)
const colors = [
`rgba(102, 102, 204, ${alpha})`, // #66c - lighter indigo
`rgba(147, 112, 219, ${alpha})`, // purple
`rgba(138, 43, 226, ${alpha})`, // blueviolet
`rgba(102, 153, 204, ${alpha})`, // soft blue
];
this.context.fillStyle = colors[circle.colorIndex || 0];
this.context.fill();
this.context.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
if (!update) {
this.circles.push(circle);
}
}
clearContext() {
this.context.clearRect(0, 0, this.canvasSize.w, this.canvasSize.h);
}
drawParticles() {
this.clearContext();
const particleCount = this.settings.quantity;
for (let i = 0; i < particleCount; i++) {
const circle = this.circleParams();
this.drawCircle(circle);
}
}
remapValue(value, start1, end1, start2, end2) {
const remapped = (value - start1) * (end2 - start2) / (end1 - start1) + start2;
return remapped > 0 ? remapped : 0;
}
animate() {
this.clearContext();
this.circles.forEach((circle, i) => {
const edge = [
circle.x + circle.translateX - circle.size,
this.canvasSize.w - circle.x - circle.translateX - circle.size,
circle.y + circle.translateY - circle.size,
this.canvasSize.h - circle.y - circle.translateY - circle.size,
];
const closestEdge = edge.reduce((a, b) => Math.min(a, b));
const remapClosestEdge = this.remapValue(closestEdge, 0, 20, 0, 1).toFixed(2);
if(remapClosestEdge > 1) {
circle.alpha += 0.02;
if(circle.alpha > circle.targetAlpha) circle.alpha = circle.targetAlpha;
} else {
circle.alpha = circle.targetAlpha * remapClosestEdge;
}
circle.x += circle.dx;
circle.y += circle.dy;
circle.translateX += ((this.mouse.x / (this.settings.staticity / circle.magnetism)) - circle.translateX) / this.settings.ease;
circle.translateY += ((this.mouse.y / (this.settings.staticity / circle.magnetism)) - circle.translateY) / this.settings.ease;
if (circle.x < -circle.size || circle.x > this.canvasSize.w + circle.size || circle.y < -circle.size || circle.y > this.canvasSize.h + circle.size) {
this.circles.splice(i, 1);
const newCircle = this.circleParams();
this.drawCircle(newCircle);
} else {
this.drawCircle({ ...circle, x: circle.x, y: circle.y, translateX: circle.translateX, translateY: circle.translateY, alpha: circle.alpha }, true);
}
});
window.requestAnimationFrame(this.animate);
}
}
// Initialize particle animations when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
const canvasElements = document.querySelectorAll('[data-particle-animation]');
canvasElements.forEach(canvas => {
const options = {
quantity: canvas.dataset.particleQuantity || 30,
staticity: canvas.dataset.particleStaticity || 50,
ease: canvas.dataset.particleEase || 50,
};
new ParticleAnimation(canvas, options);
});
});