Dynamic Cursor

A dynamic cursor thingy

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Dynamic Cursor
// @namespace    http://tampermonkey.net/
// @version      2026-01-20
// @description  A dynamic cursor thingy
// @author       pooiod7
// @include      *
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==

// const s1332=document.createElement("style");
// s1332.textContent="button{transition:filter .5s}button:hover{filter:invert(1)}";
// document.head.appendChild(s1332);

(function() {
    if (window.DCurLoaded == true) return;
    window.DCurLoaded = true;

    var SETTINGS = {
        BASE_SIZE: { key: "size", def: "5", type: "number" },
        BASE_OPACITY: { key: "PointerOpacity", def: "0.9", type: "number" },
        OUTLINE_OPACITY: { key: "OutlineOpacity", def: "1", type: "number" },
        SELECT_OPACITY: { key: "SelectOpacity", def: "0.2", type: "number" },
        MOSTLY_OUTLINE: { key: "ForceOutline", def: false, type: "bool" },
        TEXT_CURSOR_WIDTH: { key: "TextPointerWidth", def: "2", type: "number" },
        OUTLINE_THICKNESS: { key: "OutlineThickness", def: "2", type: "number" },
        IGNORE_SITES: { key: "IgnoreSites", def: "example.com, studio.penguinmod.com, turbowarp.org", type: "text" }
    };

    var VALUES = {};
    Object.keys(SETTINGS).forEach(k => {
        VALUES[k] = GM_getValue(SETTINGS[k].key, SETTINGS[k].def);
    });

    var {
        BASE_SIZE,
        BASE_OPACITY,
        OUTLINE_OPACITY,
        SELECT_OPACITY,
        MOSTLY_OUTLINE,
        TEXT_CURSOR_WIDTH,
        OUTLINE_THICKNESS,
        IGNORE_SITES
    } = VALUES;

    Object.keys(SETTINGS).forEach(k => {
        let s = SETTINGS[k];
        let v = VALUES[k];
        GM_registerMenuCommand(`${k}: ${v}`, () => {
            let nv;
            if (s.type === "bool") {
                nv = confirm(`${k}: OK=true, Cancel=false`);
            } else {
                nv = prompt(`Set ${k}:`, v);
                if (nv === null) return;
                if (s.type === "number") nv = String(nv);
            }
            GM_setValue(s.key, nv);
            window.location.reload();
        });
    });

    var IGNORE_JSON = IGNORE_SITES.split(",").map(s => s.trim()).filter(Boolean);

    var IS_IGNORED = IGNORE_JSON.some(d => location.hostname === d || location.hostname.endsWith("." + d));
    if (IS_IGNORED) return;

    const Z_INDEX = '2147483647';
    var SMOOTH_DURATION = 200;

    const CLICKABLE_SELECTORS = 'a, button, input, textarea, select, details, [role="button"], [role="link"], g[id^="part-"]';
    const TEXT_INPUT_TYPES = ['text', 'password', 'email', 'number', 'search', 'tel', 'url', 'date', 'datetime-local', 'month', 'week', 'time'];

    const globalStyle = document.createElement('style');
    globalStyle.textContent = `*, html, body { cursor: none !important; }`;
    document.head.appendChild(globalStyle);

    const host = document.createElement('div');
    Object.assign(host.style, {
        position: 'fixed', top: '0', left: '0', width: '0', height: '0',
        zIndex: Z_INDEX, pointerEvents: 'none'
    });
    document.body.appendChild(host);

    const shadow = host.attachShadow({ mode: 'open' });

    const shadowStyle = document.createElement('style');
    shadowStyle.textContent = `
        .cursor {
            position: fixed; top: 0; left: 0;
            width: ${BASE_SIZE}px; height: ${BASE_SIZE}px;
            border-radius: 50%;
            background-color: rgba(255, 255, 255, 0.1);
            backdrop-filter: invert(1);
            -webkit-backdrop-filter: invert(1);
            opacity: ${BASE_OPACITY};
            pointer-events: none;
            transform: translate3d(-50%, -50%, 0);
            box-sizing: border-box;
            will-change: transform, width, height, border-radius, padding;
            transition:
                width 0.2s cubic-bezier(0.25, 0.8, 0.25, 1),
                height 0.2s cubic-bezier(0.25, 0.8, 0.25, 1),
                border-radius 0.2s cubic-bezier(0.25, 0.8, 0.25, 1),
                opacity 0.2s ease,
                padding 0.2s ease,
                background-color 0.2s ease;
            -webkit-mask: none;
            mask: none;
        }
        .cursor.smooth-pos {
            transition:
                width 0.2s cubic-bezier(0.25, 0.8, 0.25, 1),
                height 0.2s cubic-bezier(0.25, 0.8, 0.25, 1),
                border-radius 0.2s cubic-bezier(0.25, 0.8, 0.25, 1),
                transform 0.2s cubic-bezier(0.25, 0.8, 0.25, 1),
                opacity 0.2s ease,
                padding 0.2s ease,
                background-color 0.2s ease;
        }
        .cursor.outline-mode {
            background-color: rgba(255, 255, 255, 0.5);
            padding: ${OUTLINE_THICKNESS}px;
            -webkit-mask: linear-gradient(#fff, #fff) content-box, linear-gradient(#fff, #fff);
            -webkit-mask-composite: xor;
            mask-composite: exclude;
        }
        .cursor.text-mode {
            border-radius: 0 !important;
            background-color: rgba(255, 255, 255, 0.5);
        }
        .cursor.hidden { opacity: 0 !important; }
    `;
    shadow.appendChild(shadowStyle);

    const cursor = document.createElement('div');
    cursor.classList.add('cursor');
    shadow.appendChild(cursor);

    let mouseX = -100, mouseY = -100;
    let currentMode = 'idle';
    let smoothRemoveTimer = null;
    let freezeCursor = false;
    let freezeTarget = null;

    const isSVGWithImage = (el) => {
        if (el.tagName.toLowerCase() === 'svg') return !!el.querySelector('image');
        return false;
    };

    const hasImage = (el) => {
        if (el.tagName.toLowerCase() === 'img') return true;
        if (isSVGWithImage(el)) return true;
        if (el.querySelector('svg') && isSVGWithImage(el.querySelector('svg'))) return true;
        if (el.querySelector('img')) return true;
        const style = window.getComputedStyle(el);
        if (style.backgroundImage !== 'none' && style.backgroundImage.includes('url')) return true;
        return false;
    };

    const getTextLineHeight = (el) => {
        const style = window.getComputedStyle(el);
        const lh = parseFloat(style.lineHeight);
        const fs = parseFloat(style.fontSize);
        return isNaN(lh) ? fs * 1.2 : lh;
    };

    const getClickable = (target) => {
        const el = target.closest(CLICKABLE_SELECTORS);
        if (el) return el;
        let traverse = target;
        while (traverse && traverse !== document.body) {
            const style = window.getComputedStyle(traverse);
            if (style.cursor === 'pointer') return traverse;
            traverse = traverse.parentElement;
        }
        return null;
    };

    const isTextBox = (el) => {
        const tag = el.tagName;
        if (tag === 'TEXTAREA') return true;
        if (tag === 'INPUT') {
            const type = el.getAttribute('type') || 'text';
            return TEXT_INPUT_TYPES.includes(type.toLowerCase());
        }
        return false;
    };

    document.addEventListener('mousemove', (e) => {
        mouseX = e.clientX;
        mouseY = e.clientY;
    });

    document.addEventListener('mousedown', (e) => {
        const target = e.target.closest(CLICKABLE_SELECTORS);
        if (target) {
            freezeCursor = target.classList.contains('freeze-cursor');
            freezeTarget = freezeCursor ? target : null;
        }
    });

    document.addEventListener('mouseup', () => {
        freezeCursor = false;
        freezeTarget = null;
    });

    document.addEventListener('mouseleave', () => cursor.classList.add('hidden'));
    document.addEventListener('mouseenter', () => cursor.classList.remove('hidden'));

    function update() {
        let currentX = mouseX;
        let currentY = mouseY;

        if (freezeCursor && freezeTarget) {
            const rect = freezeTarget.getBoundingClientRect();
            currentX = rect.left + rect.width / 2;
            currentY = rect.top + rect.height / 2;
        }

        const elUnderPoint = document.elementFromPoint(currentX, currentY);
        const target = elUnderPoint ? getClickable(elUnderPoint) : null;

        let targetX = currentX;
        let targetY = currentY;
        let targetW = BASE_SIZE;
        let targetH = BASE_SIZE;
        let targetR = '50%';
        let mode = 'idle';
        let isOutline = false;

        if (target) {
            cursor.style.opacity = SELECT_OPACITY;
            const rect = target.getBoundingClientRect();
            const style = window.getComputedStyle(target);

            if (isTextBox(target)) {
                cursor.style.opacity = BASE_OPACITY;
                mode = 'text';
                targetW = TEXT_CURSOR_WIDTH;
                targetH = getTextLineHeight(target);
                targetR = '0px';
                targetX = currentX;
                targetY = currentY;
            } else {
                mode = 'snap';
                targetW = rect.width;
                targetH = rect.height;
                const cssR = parseFloat(style.borderRadius) || 0;
                targetR = Math.max(cssR, 2) + 'px';
                targetX = rect.left + (rect.width / 2);
                targetY = rect.top + (rect.height / 2);
                if (hasImage(target)) isOutline = true;
                if (MOSTLY_OUTLINE && !target.classList.contains('freeze-cursor')) isOutline = true;
                if (isOutline) cursor.style.opacity = OUTLINE_OPACITY;
            }
        } else {
            cursor.style.opacity = BASE_OPACITY;
        }

        if (mode === 'snap') {
            cursor.classList.add('smooth-pos');
            clearTimeout(smoothRemoveTimer);
            smoothRemoveTimer = null;
        } else if (currentMode === 'snap' && mode !== 'snap') {
            cursor.classList.add('smooth-pos');
            if (!smoothRemoveTimer) {
                smoothRemoveTimer = setTimeout(() => {
                    cursor.classList.remove('smooth-pos');
                    smoothRemoveTimer = null;
                }, SMOOTH_DURATION);
            }
        } else if (mode === 'text') {
            cursor.classList.remove('smooth-pos');
            clearTimeout(smoothRemoveTimer);
            smoothRemoveTimer = null;
        }

        if (mode === 'text') {
            cursor.classList.add('text-mode');
            cursor.classList.remove('outline-mode');
        } else {
            cursor.classList.remove('text-mode');
            if (isOutline) cursor.classList.add('outline-mode');
            else cursor.classList.remove('outline-mode');
        }

        cursor.style.width = `${targetW}px`;
        cursor.style.height = `${targetH}px`;
        cursor.style.borderRadius = targetR;
        cursor.style.transform = `translate3d(${targetX}px, ${targetY}px, 0) translate(-50%, -50%)`;

        currentMode = mode;
        requestAnimationFrame(update);
    }

    requestAnimationFrame(update);
})();