308 lines
No EOL
8.7 KiB
JavaScript
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();
|
|
|
|
})(); |