xilly/xilly-mouse-trail.user.js
2025-12-16 12:20:00 +07:00

308 lines
No EOL
8.7 KiB
JavaScript

// ==UserScript==
// @name Xilly - Mouse Trail
// @namespace http://experimenting.website/
// @version 0.1
// @description Add firefly mouse trail
// @author EXPERIMENTING
// @match https://x.com/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
// ----- CONFIG -----
const MAX_PARTICLES = 10;
const SPAWN_RATE = 0.4;
const BASE_SIZE = 1.0;
const SIZE_VARIATION = 2.2;
const LIFE_MIN = 700;
const LIFE_MAX = 1600;
const SPEED_FACTOR = 0.02;
const TAIL_SMOOTH = 0.85;
const FRAME_LIMIT_MS = 16.67;
const IGNORE_INPUTS = true;
const COLORS = [
'255,210,120',
'255,200,90',
'255,170,70',
'255,230,160'
];
// ----- CREATE CANVAS OVERLAY -----
let canvas, ctx;
function createCanvas() {
canvas = document.createElement('canvas');
canvas.style.position = 'fixed';
canvas.style.left = '0';
canvas.style.top = '0';
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.style.pointerEvents = 'none';
canvas.style.zIndex = '2147483646';
canvas.id = 'firefly-trail-canvas';
document.documentElement.appendChild(canvas);
ctx = canvas.getContext('2d', { alpha: true });
resizeCanvas();
window.addEventListener('resize', resizeCanvas, { passive: true });
}
function resizeCanvas() {
if (!canvas) return;
const dpr = Math.max(1, window.devicePixelRatio || 1);
const w = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
const h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
canvas.width = Math.floor(w * dpr);
canvas.height = Math.floor(h * dpr);
canvas.style.width = w + 'px';
canvas.style.height = h + 'px';
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
// ----- PARTICLE SYSTEM -----
class Particle {
constructor() {
this.reset(0, 0);
}
reset(x, y) {
this.x = x;
this.y = y;
this.vx = (Math.random() - 0.5) * 0.3;
this.vy = (Math.random() - 0.5) * 0.3;
this.size = BASE_SIZE + Math.random() * SIZE_VARIATION;
this.life = 0;
this.maxLife = LIFE_MIN + Math.random() * (LIFE_MAX - LIFE_MIN);
this.hue = COLORS[Math.floor(Math.random() * COLORS.length)];
this.alpha = 1;
this.alive = true;
this.age = 0;
}
update(dt) {
this.vx *= TAIL_SMOOTH;
this.vy *= TAIL_SMOOTH;
this.x += this.vx * dt;
this.y += this.vy * dt;
this.age += dt;
this.life = this.age;
this.alpha = Math.max(0, 1 - this.life / this.maxLife);
if (this.life >= this.maxLife) this.alive = false;
}
draw(ctx) {
const g = ctx;
const s = this.size;
g.save();
// additive blending for glow
g.globalCompositeOperation = 'lighter';
// radial gradient for soft glow
const grad = g.createRadialGradient(this.x, this.y, 0, this.x, this.y, s * 8);
const rgba = (a) => `rgba(${this.hue},${a})`;
grad.addColorStop(0, rgba(0.95 * this.alpha));
grad.addColorStop(0.2, rgba(0.6 * this.alpha));
grad.addColorStop(0.5, rgba(0.18 * this.alpha));
grad.addColorStop(1, rgba(0));
g.fillStyle = grad;
g.beginPath();
g.arc(this.x, this.y, s * 8, 0, Math.PI * 2);
g.fill();
// small core
g.globalCompositeOperation = 'source-over';
g.shadowColor = `rgba(${this.hue}, ${0.9 * this.alpha})`;
g.shadowBlur = Math.max(6, s * 6);
g.fillStyle = `rgba(${this.hue}, ${1 * this.alpha})`;
g.beginPath();
g.arc(this.x, this.y, s, 0, Math.PI * 2);
g.fill();
g.restore();
}
}
let particles = [];
let freeList = [];
function createParticle(x, y) {
let p;
if (freeList.length) {
p = freeList.pop();
p.reset(x, y);
} else if (particles.length < MAX_PARTICLES) {
p = new Particle();
p.reset(x, y);
particles.push(p);
} else {
// recycle the oldest or dead one
for (let i = 0; i < particles.length; i++) {
if (!particles[i].alive) {
p = particles[i];
p.reset(x, y);
break;
}
}
if (!p) {
// fallback: overwrite random
p = particles[Math.floor(Math.random() * particles.length)];
p.reset(x, y);
}
}
return p;
}
// ----- MOUSE HANDLING -----
let lastX = 0, lastY = 0, lastTime = performance.now();
let enabled = true;
function onMove(e) {
if (!enabled) return;
if (IGNORE_INPUTS) {
const t = e.target;
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
}
const x = e.clientX;
const y = e.clientY;
const now = performance.now();
const dt = Math.max(1, now - lastTime);
const dx = x - lastX;
const dy = y - lastY;
const dist = Math.sqrt(dx * dx + dy * dy) || 0.001;
let toSpawn = Math.min(6, Math.ceil(dist * SPAWN_RATE));
for (let i = 0; i < toSpawn; i++) {
const t = i / Math.max(1, toSpawn);
const sx = lastX + dx * t + (Math.random() - 0.5) * 2;
const sy = lastY + dy * t + (Math.random() - 0.5) * 2;
const p = createParticle(sx, sy);
p.vx += dx * SPEED_FACTOR * (0.6 + Math.random() * 0.9);
p.vy += dy * SPEED_FACTOR * (0.6 + Math.random() * 0.9) + (Math.random() - 0.5) * 0.8;
}
lastX = x;
lastY = y;
lastTime = now;
}
function onTouchMove(e) {
if (!e.touches || e.touches.length === 0) return;
const t = e.touches[0];
onMove({ clientX: t.clientX, clientY: t.clientY, target: t.target });
}
// ----- RENDER LOOP -----
let rafId = null;
function frame(now) {
if (!ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
const dt = 16;
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
if (!p.alive) continue;
p.update(dt);
p.draw(ctx);
}
for (let i = 0; i < particles.length; i++) {
if (!particles[i].alive) {
freeList.push(particles[i]);
}
}
rafId = requestAnimationFrame(frame);
}
// ----- ENABLE / DISABLE CONTROL -----
function enable() {
if (enabled) return;
enabled = true;
canvas.style.display = '';
window.addEventListener('mousemove', onMove, { passive: true });
window.addEventListener('touchmove', onTouchMove, { passive: true });
if (!rafId) rafId = requestAnimationFrame(frame);
}
function disable() {
if (!enabled) return;
enabled = false;
canvas.style.display = 'none';
window.removeEventListener('mousemove', onMove);
window.removeEventListener('touchmove', onTouchMove);
if (rafId) {
cancelAnimationFrame(rafId);
rafId = null;
}
}
// toggle with Ctrl+Shift+K
window.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'k') {
if (enabled) {
disable();
flashStatus('Mouse trail: OFF');
} else {
enable();
flashStatus('Mouse trail: ON');
}
}
});
// small on-screen hint that auto-hides
let hintEl;
function flashStatus(text) {
if (!hintEl) {
hintEl = document.createElement('div');
hintEl.style.position = 'fixed';
hintEl.style.right = '18px';
hintEl.style.bottom = '18px';
hintEl.style.padding = '8px 12px';
hintEl.style.fontSize = '13px';
hintEl.style.background = 'rgba(20,20,20,0.7)';
hintEl.style.color = '#fff';
hintEl.style.borderRadius = '8px';
hintEl.style.zIndex = '2147483647';
hintEl.style.pointerEvents = 'none';
document.documentElement.appendChild(hintEl);
}
hintEl.textContent = text;
hintEl.style.opacity = '1';
setTimeout(() => {
if (hintEl) hintEl.style.transition = 'opacity 700ms';
if (hintEl) hintEl.style.opacity = '0';
}, 900);
}
// ----- INIT -----
function init() {
// avoid running inside some embedded frames or if canvas already present
try {
if (window.top !== window.self) return;
} catch (e) {}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setup, { once: true });
} else {
setup();
}
}
function setup() {
createCanvas();
// initial mouse pos center
lastX = (window.innerWidth || document.documentElement.clientWidth) / 2;
lastY = (window.innerHeight || document.documentElement.clientHeight) / 2;
lastTime = performance.now();
window.addEventListener('mousemove', onMove, { passive: true });
window.addEventListener('touchmove', onTouchMove, { passive: true });
rafId = requestAnimationFrame(frame);
// initial simple hint
flashStatus('Firefly trail enabled — Ctrl+Shift+K to toggle');
}
init();
})();