Initial commit
This commit is contained in:
commit
d0915bc19c
5 changed files with 675 additions and 0 deletions
1
README.md
Normal file
1
README.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
x.com userscript that makes the experience silly because that site is so unserious, nothing is real.
|
||||||
109
xilly-custom-background.user.js
Normal file
109
xilly-custom-background.user.js
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
// ==UserScript==
|
||||||
|
// @name Xilly - Custom Background
|
||||||
|
// @namespace http://experimenting.website/
|
||||||
|
// @version 0.1
|
||||||
|
// @description Change X background to image
|
||||||
|
// @author EXPERIMENTING
|
||||||
|
// @match https://x.com/*
|
||||||
|
// @grant GM_addStyle
|
||||||
|
// @grant GM_setValue
|
||||||
|
// @grant GM_getValue
|
||||||
|
// @grant GM_registerMenuCommand
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function setBackground(imageUrl) {
|
||||||
|
const css = `
|
||||||
|
body {
|
||||||
|
background-image: url('${imageUrl}') !important;
|
||||||
|
background-size: cover !important;
|
||||||
|
background-repeat: no-repeat !important;
|
||||||
|
background-attachment: fixed !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
GM_addStyle(css);
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedImageUrl = GM_getValue('backgroundImageUrl', '');
|
||||||
|
if (savedImageUrl) {
|
||||||
|
setBackground(savedImageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDialog() {
|
||||||
|
let existingDialog = document.getElementById('background-dialog');
|
||||||
|
if (existingDialog) {
|
||||||
|
existingDialog.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialog = document.createElement('div');
|
||||||
|
dialog.id = 'background-dialog';
|
||||||
|
dialog.style.position = 'fixed';
|
||||||
|
dialog.style.top = '10px';
|
||||||
|
dialog.style.right = '10px';
|
||||||
|
dialog.style.zIndex = '10000';
|
||||||
|
dialog.style.backgroundColor = 'black';
|
||||||
|
dialog.style.padding = '10px';
|
||||||
|
dialog.style.border = '1px solid black';
|
||||||
|
dialog.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
|
||||||
|
|
||||||
|
const fromLocalBtn = document.createElement('button');
|
||||||
|
fromLocalBtn.textContent = 'From Local';
|
||||||
|
fromLocalBtn.style.marginRight = '10px';
|
||||||
|
fromLocalBtn.addEventListener('click', fromLocal);
|
||||||
|
|
||||||
|
const fromUrlBtn = document.createElement('button');
|
||||||
|
fromUrlBtn.textContent = 'From URL';
|
||||||
|
fromUrlBtn.addEventListener('click', fromUrl);
|
||||||
|
|
||||||
|
const closeBtn = document.createElement('button');
|
||||||
|
closeBtn.textContent = 'X';
|
||||||
|
closeBtn.style.position = 'absolute';
|
||||||
|
closeBtn.style.top = '5px';
|
||||||
|
closeBtn.style.right = '5px';
|
||||||
|
closeBtn.style.background = 'none';
|
||||||
|
closeBtn.style.border = 'none';
|
||||||
|
closeBtn.style.fontSize = '16px';
|
||||||
|
closeBtn.style.cursor = 'pointer';
|
||||||
|
closeBtn.addEventListener('click', () => dialog.remove());
|
||||||
|
|
||||||
|
dialog.appendChild(closeBtn);
|
||||||
|
dialog.appendChild(fromLocalBtn);
|
||||||
|
dialog.appendChild(fromUrlBtn);
|
||||||
|
document.body.appendChild(dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromLocal() {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = 'image/*';
|
||||||
|
input.onchange = (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const imageUrl = e.target.result;
|
||||||
|
setBackground(imageUrl);
|
||||||
|
GM_setValue('backgroundImageUrl', imageUrl);
|
||||||
|
const dialog = document.getElementById('background-dialog');
|
||||||
|
if (dialog) dialog.remove();
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromUrl() {
|
||||||
|
const url = prompt('Enter image URL:');
|
||||||
|
if (url) {
|
||||||
|
setBackground(url);
|
||||||
|
GM_setValue('backgroundImageUrl', url);
|
||||||
|
const dialog = document.getElementById('background-dialog');
|
||||||
|
if (dialog) dialog.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GM_registerMenuCommand('Set Background', showDialog);
|
||||||
|
})();
|
||||||
185
xilly-custom-timeline.user.js
Normal file
185
xilly-custom-timeline.user.js
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
// ==UserScript==
|
||||||
|
// @name Xilly - Custom Timeline
|
||||||
|
// @namespace http://experimenting.website/
|
||||||
|
// @version 0.1
|
||||||
|
// @description Customize X timeline color
|
||||||
|
// @author EXPERIMENTING
|
||||||
|
// @match https://x.com/*
|
||||||
|
// @grant GM_addStyle
|
||||||
|
// @grant GM_getValue
|
||||||
|
// @grant GM_setValue
|
||||||
|
// @grant GM_registerMenuCommand
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function getSetting(key, defaultValue) {
|
||||||
|
const value = GM_getValue(key);
|
||||||
|
return value !== undefined ? value : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let OPACITY_LEVEL = getSetting('opacityLevel', 0.8);
|
||||||
|
let BACKGROUND_COLOR = getSetting('backgroundColor', '30,44,59'); // RGB
|
||||||
|
let BLUR_LEVEL = getSetting('blurLevel', 5);
|
||||||
|
|
||||||
|
function updateCSS() {
|
||||||
|
const rgb = BACKGROUND_COLOR.split(',');
|
||||||
|
const css = `
|
||||||
|
/* Mengatur transparansi dan warna untuk elemen utama timeline */
|
||||||
|
[data-testid="primaryColumn"] {
|
||||||
|
background-color: rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${OPACITY_LEVEL}) !important;
|
||||||
|
backdrop-filter: blur(${BLUR_LEVEL}px) !important;
|
||||||
|
-webkit-backdrop-filter: blur(${BLUR_LEVEL}px) !important; /* Untuk kompatibilitas Safari */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mengatur transparansi dan warna untuk elemen tweet individual */
|
||||||
|
article[role="article"] {
|
||||||
|
background-color: rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${OPACITY_LEVEL}) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.css-1dbjc4n.r-1habvwh {
|
||||||
|
background-color: rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${OPACITY_LEVEL}) !important;
|
||||||
|
backdrop-filter: blur(${BLUR_LEVEL}px) !important;
|
||||||
|
-webkit-backdrop-filter: blur(${BLUR_LEVEL}px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.r-kritb0.r-1h8ys4a.css-175oi2r {
|
||||||
|
background-color: rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${OPACITY_LEVEL}) !important;
|
||||||
|
backdrop-filter: blur(${BLUR_LEVEL}px) !important;
|
||||||
|
-webkit-backdrop-filter: blur(${BLUR_LEVEL}px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.r-1awozwy.r-yfoy6g.r-18u37iz.r-1wtj0ep.r-13qz1uu.r-184en5c {
|
||||||
|
background-color: rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${OPACITY_LEVEL}) !important;
|
||||||
|
backdrop-filter: blur(${BLUR_LEVEL}px) !important;
|
||||||
|
-webkit-backdrop-filter: blur(${BLUR_LEVEL}px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.r-8oi148 {
|
||||||
|
background-color: rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0) !important;
|
||||||
|
backdrop-filter: blur(${BLUR_LEVEL}px) !important;
|
||||||
|
-webkit-backdrop-filter: blur(${BLUR_LEVEL}px) !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.r-ii8lfi {
|
||||||
|
background-color: rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${OPACITY_LEVEL}) !important;
|
||||||
|
backdrop-filter: blur(${BLUR_LEVEL}px) !important;
|
||||||
|
-webkit-backdrop-filter: blur(${BLUR_LEVEL}px) !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.r-yfoy6g {
|
||||||
|
background-color: rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${OPACITY_LEVEL}) !important;
|
||||||
|
backdrop-filter: blur(${BLUR_LEVEL}px) !important;
|
||||||
|
-webkit-backdrop-filter: blur(${BLUR_LEVEL}px) !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Drawer */
|
||||||
|
.r-105ug2t.r-yfoy6g.r-1867qdf.r-xnswec.r-13awgt0.r-1ce3o0f.r-1udh08x.r-u8s1d.r-13qz1uu {
|
||||||
|
background-color: rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${OPACITY_LEVEL}) !important;
|
||||||
|
backdrop-filter: blur(${BLUR_LEVEL}px) !important;
|
||||||
|
-webkit-backdrop-filter: blur(${BLUR_LEVEL}px) !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.r-g6ijar {
|
||||||
|
background-color: rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${OPACITY_LEVEL}) !important;
|
||||||
|
backdrop-filter: blur(${BLUR_LEVEL}px) !important;
|
||||||
|
-webkit-backdrop-filter: blur(${BLUR_LEVEL}px) !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make post */
|
||||||
|
.r-1h8ys4a.r-dq6lxq.r-hucgq0 {
|
||||||
|
background-color: rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 1) !important;
|
||||||
|
backdrop-filter: blur(${BLUR_LEVEL}px) !important;
|
||||||
|
-webkit-backdrop-filter: blur(${BLUR_LEVEL}px) !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.r-1iud8zs.r-1e5uvyk.r-ii8lfi.r-ne48ov.r-1nna3df {
|
||||||
|
background-color: rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 1) !important;
|
||||||
|
backdrop-filter: blur(${BLUR_LEVEL}px) !important;
|
||||||
|
-webkit-backdrop-filter: blur(${BLUR_LEVEL}px) !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.r-yfoy6g.r-jumn1c.r-xd6kpl.r-gtdqiz.r-ipm5af.r-184en5c {
|
||||||
|
background-color: rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 1) !important;
|
||||||
|
backdrop-filter: blur(${BLUR_LEVEL}px) !important;
|
||||||
|
-webkit-backdrop-filter: blur(${BLUR_LEVEL}px) !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Menjaga agar teks tetap terlihat jelas */
|
||||||
|
.css-901oao, .css-16my406, .css-1hf3ou5 {
|
||||||
|
background-color: transparent !important;
|
||||||
|
backdrop-filter: none !important;
|
||||||
|
-webkit-backdrop-filter: none !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
GM_addStyle(css);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCSS();
|
||||||
|
|
||||||
|
const settingsPanel = document.createElement('div');
|
||||||
|
settingsPanel.style.position = 'fixed';
|
||||||
|
settingsPanel.style.top = '50px';
|
||||||
|
settingsPanel.style.right = '10px';
|
||||||
|
settingsPanel.style.zIndex = '1000';
|
||||||
|
settingsPanel.style.backgroundColor = 'black';
|
||||||
|
settingsPanel.style.border = '1px solid #ccc';
|
||||||
|
settingsPanel.style.padding = '10px';
|
||||||
|
settingsPanel.style.display = 'none';
|
||||||
|
settingsPanel.innerHTML = `
|
||||||
|
<h3>Timeline Settings</h3>
|
||||||
|
<label for="opacitySlider">Opacity: <span id="opacityValue">${OPACITY_LEVEL}</span></label><br>
|
||||||
|
<input type="range" id="opacitySlider" min="0" max="1" step="0.1" value="${OPACITY_LEVEL}"><br>
|
||||||
|
<label for="blurSlider">Timeline Background Blur (px): <span id="blurValue">${BLUR_LEVEL}</span></label><br>
|
||||||
|
<input type="range" id="blurSlider" min="0" max="20" step="1" value="${BLUR_LEVEL}"><br>
|
||||||
|
<label for="colorPicker">Background Color:</label><br>
|
||||||
|
<input type="color" id="colorPicker" value="#${parseInt(BACKGROUND_COLOR.split(',')[0]).toString(16).padStart(2, '0')}${parseInt(BACKGROUND_COLOR.split(',')[1]).toString(16).padStart(2, '0')}${parseInt(BACKGROUND_COLOR.split(',')[2]).toString(16).padStart(2, '0')}"><br>
|
||||||
|
<button id="saveSettings">Save</button>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(settingsPanel);
|
||||||
|
|
||||||
|
const opacitySlider = settingsPanel.querySelector('#opacitySlider');
|
||||||
|
const opacityValue = settingsPanel.querySelector('#opacityValue');
|
||||||
|
opacitySlider.addEventListener('input', () => {
|
||||||
|
OPACITY_LEVEL = opacitySlider.value;
|
||||||
|
opacityValue.innerText = OPACITY_LEVEL;
|
||||||
|
});
|
||||||
|
|
||||||
|
const blurSlider = settingsPanel.querySelector('#blurSlider');
|
||||||
|
const blurValue = settingsPanel.querySelector('#blurValue');
|
||||||
|
blurSlider.addEventListener('input', () => {
|
||||||
|
BLUR_LEVEL = blurSlider.value;
|
||||||
|
blurValue.innerText = BLUR_LEVEL;
|
||||||
|
});
|
||||||
|
|
||||||
|
const colorPicker = settingsPanel.querySelector('#colorPicker');
|
||||||
|
colorPicker.addEventListener('input', () => {
|
||||||
|
const hex = colorPicker.value;
|
||||||
|
const r = parseInt(hex.substr(1,2),16);
|
||||||
|
const g = parseInt(hex.substr(3,2),16);
|
||||||
|
const b = parseInt(hex.substr(5,2),16);
|
||||||
|
BACKGROUND_COLOR = `${r},${g},${b}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveSettingsButton = settingsPanel.querySelector('#saveSettings');
|
||||||
|
saveSettingsButton.addEventListener('click', () => {
|
||||||
|
GM_setValue('opacityLevel', OPACITY_LEVEL);
|
||||||
|
GM_setValue('backgroundColor', BACKGROUND_COLOR);
|
||||||
|
GM_setValue('blurLevel', BLUR_LEVEL);
|
||||||
|
updateCSS();
|
||||||
|
settingsPanel.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
GM_registerMenuCommand('Setting Timeline', () => {
|
||||||
|
settingsPanel.style.display = settingsPanel.style.display === 'none' ? 'block' : 'none';
|
||||||
|
});
|
||||||
|
})();
|
||||||
72
xilly-embed-banner.user.js
Normal file
72
xilly-embed-banner.user.js
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
// ==UserScript==
|
||||||
|
// @name Xilly - Embed Banner
|
||||||
|
// @namespace http://experimenting.website/
|
||||||
|
// @description Replaces X banner with any url
|
||||||
|
// @version 0.1
|
||||||
|
// @author EXPERIMENTING
|
||||||
|
// @match https://x.com/main_experiment
|
||||||
|
// @grant GM_addElement
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function replaceBanner() {
|
||||||
|
const bannerLink = document.querySelector('a[href$="/header_photo"]');
|
||||||
|
|
||||||
|
if (bannerLink) {
|
||||||
|
if (bannerLink.dataset.embedBannerReplaced) return;
|
||||||
|
|
||||||
|
console.log('Twitter Embed Banner: Banner found, replacing...');
|
||||||
|
|
||||||
|
const container = bannerLink.parentElement;
|
||||||
|
|
||||||
|
if (container) {
|
||||||
|
container.style.paddingBottom = '0';
|
||||||
|
container.style.height = 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
|
bannerLink.innerHTML = '';
|
||||||
|
bannerLink.dataset.embedBannerReplaced = 'true';
|
||||||
|
|
||||||
|
bannerLink.removeAttribute('href');
|
||||||
|
bannerLink.style.cursor = 'default';
|
||||||
|
bannerLink.style.display = 'block';
|
||||||
|
bannerLink.style.height = 'auto';
|
||||||
|
bannerLink.onclick = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
GM_addElement(bannerLink, 'iframe', {
|
||||||
|
src: 'https://webosu.online/', /*CHANGE WHATEVER YOU WANT :3*/
|
||||||
|
scrolling: 'yes',
|
||||||
|
style: `
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
transform: scale(0.5);
|
||||||
|
transform-origin: top left;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
aspect-ratio: 1500 / 300;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
bannerLink.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
replaceBanner();
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
|
||||||
|
replaceBanner();
|
||||||
|
|
||||||
|
})();
|
||||||
308
xilly-mouse-trail.user.js
Normal file
308
xilly-mouse-trail.user.js
Normal file
|
|
@ -0,0 +1,308 @@
|
||||||
|
// ==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();
|
||||||
|
|
||||||
|
})();
|
||||||
Loading…
Add table
Add a link
Reference in a new issue