scroll-button

scroll-button (Draggable, Auto-Fade, Bright-on-Stop)

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         scroll-button
// @namespace    https://github.com/livinginpurple
// @version      20251217.09
// @description  scroll-button (Draggable, Auto-Fade, Bright-on-Stop)
// @license      WTFPL
// @author       livinginpurple
// @include      *
// @run-at       document-end
// @grant        none
// ==/UserScript==

(() => {
    'use strict';

    const init = () => {
        const btnId = 'gamma-scroll-btn';
        if (document.getElementById(btnId)) return;

        // SVG 圖示
        const icons = {
            up: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="white" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 12a.5.5 0 0 0 .5-.5V5.707l2.146 2.147a.5.5 0 0 0 .708-.708l-3-3a.5.5 0 0 0-.708 0l-3 3a.5.5 0 1 0 .708.708L7.5 5.707V11.5a.5.5 0 0 0 .5.5z"/></svg>`,
            down: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="white" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 4a.5.5 0 0 1 .5.5v5.793l2.146-2.147a.5.5 0 0 1 .708.708l-3 3a.5.5 0 0 1-.708 0l-3-3a.5.5 0 1 1 .708-.708L7.5 10.293V4.5A.5.5 0 0 1 8 4z"/></svg>`
        };

        const btn = document.createElement('button');
        btn.id = btnId;
        btn.type = 'button';
        btn.setAttribute('aria-label', 'Scroll navigation');

        // 定義樣式
        Object.assign(btn.style, {
            position: 'fixed',
            right: '20px',
            bottom: '20px',
            width: '40px',
            height: '40px',
            borderRadius: '50%',
            backgroundColor: '#0d6efd',
            border: 'none',
            boxShadow: '0 0.2rem 0.5rem rgba(0,0,0,0.3)',
            zIndex: '10000',
            cursor: 'grab',
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'center',
            transition: 'opacity 0.5s ease-in-out, transform 0.1s, background-color 0.2s',
            touchAction: 'none',
            padding: '0',
            opacity: '0.3' // 預設 0.3
        });

        // 互動視覺效果 (Hover/Touch)
        btn.onmouseenter = () => { btn.style.transform = 'scale(1.1)'; manualWakeUp(); };
        btn.onmouseleave = () => { btn.style.transform = 'scale(1)'; };
        btn.ontouchstart = () => { manualWakeUp(); };

        document.body.appendChild(btn);

        // --- 狀態管理 ---
        const State = { TOP: 'top', SCROLLED: 'scrolled' };
        
        // Timer 變數
        let scrollStopTimer = null; // 用來偵測是否停止滑動
        let fadeTimer = null;       // 用來計算 3 秒後變暗

        // 更新圖示 (不涉及透明度)
        const updateIconState = () => {
            const scrollTop = window.scrollY || document.documentElement.scrollTop;
            const isAtTop = scrollTop < 50;
            const nextState = isAtTop ? State.TOP : State.SCROLLED;
            
            if (btn.dataset.state === nextState) return;

            if (isAtTop) {
                btn.innerHTML = icons.down;
                btn.dataset.state = State.TOP;
            } else {
                btn.innerHTML = icons.up;
                btn.dataset.state = State.SCROLLED;
            }
        };

        // --- 核心邏輯:滑動停止偵測 ---
        const handleScroll = () => {
            // 1. 滑動中:確保是 0.3
            // 為了避免頻繁操作 DOM,只在需要時設定
            if (btn.style.opacity !== '0.3') {
                btn.style.opacity = '0.3';
            }

            updateIconState();

            // 2. 清除之前的計時器 (因為還在滑)
            if (scrollStopTimer) clearTimeout(scrollStopTimer);
            if (fadeTimer) clearTimeout(fadeTimer);

            // 3. 設定新的偵測計時器
            // 如果 150ms 內沒有新的 scroll 事件,就視為「停止滑動」
            scrollStopTimer = setTimeout(() => {
                // --- 滑動停止時執行 ---
                btn.style.opacity = '1'; // 亮起
                
                // 設定 3 秒後變暗
                fadeTimer = setTimeout(() => {
                    btn.style.opacity = '0.3';
                }, 3000);

            }, 150);
        };

        // 手動操作 (點擊/拖曳) 的喚醒邏輯
        const manualWakeUp = () => {
            btn.style.opacity = '1';
            if (scrollStopTimer) clearTimeout(scrollStopTimer);
            if (fadeTimer) clearTimeout(fadeTimer);
            
            // 操作後也是等 3 秒變暗
            fadeTimer = setTimeout(() => {
                btn.style.opacity = '0.3';
            }, 3000);
        };

        // --- 拖曳邏輯 ---
        let isPressed = false;
        let isDragging = false;
        let startX, startY, dragOffsetX, dragOffsetY;

        const onDragStart = (e) => {
            const clientX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
            const clientY = e.type.includes('touch') ? e.touches[0].clientY : e.clientY;

            isPressed = true;
            isDragging = false;
            startX = clientX;
            startY = clientY;

            const rect = btn.getBoundingClientRect();
            dragOffsetX = clientX - rect.left;
            dragOffsetY = clientY - rect.top;

            btn.style.backgroundColor = '#0b5ed7';
            manualWakeUp(); // 拖曳開始,喚醒
        };

        const onDragMove = (e) => {
            if (!isPressed) return;

            const clientX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
            const clientY = e.type.includes('touch') ? e.touches[0].clientY : e.clientY;

            const moveX = clientX - startX;
            const moveY = clientY - startY;
            const distance = Math.sqrt(moveX * moveX + moveY * moveY);

            if (!isDragging && distance < 10) return;

            if (!isDragging) {
                isDragging = true;
                btn.style.cursor = 'grabbing';
                const rect = btn.getBoundingClientRect();
                btn.style.left = rect.left + 'px';
                btn.style.top = rect.top + 'px';
                btn.style.right = 'auto';
                btn.style.bottom = 'auto';
            }

            let newX = clientX - dragOffsetX;
            let newY = clientY - dragOffsetY;
            const maxX = window.innerWidth - 40;
            const maxY = window.innerHeight - 40;

            btn.style.left = Math.max(0, Math.min(newX, maxX)) + 'px';
            btn.style.top = Math.max(0, Math.min(newY, maxY)) + 'px';
            
            manualWakeUp(); // 拖曳中保持喚醒
        };

        const onDragEnd = () => {
            isPressed = false;
            btn.style.cursor = 'grab';
            btn.style.backgroundColor = '#0d6efd';
        };

        btn.addEventListener('mousedown', onDragStart);
        window.addEventListener('mousemove', onDragMove);
        window.addEventListener('mouseup', onDragEnd);

        btn.addEventListener('touchstart', onDragStart, { passive: false });
        window.addEventListener('touchmove', onDragMove, { passive: false });
        window.addEventListener('touchend', onDragEnd);

        // --- 點擊邏輯 ---
        btn.addEventListener('click', (e) => {
            if (isDragging) {
                e.preventDefault();
                e.stopImmediatePropagation();
                isDragging = false;
                return;
            }
            e.preventDefault();
            manualWakeUp();
            
            if (btn.dataset.state === State.TOP) {
                window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
            } else {
                window.scrollTo({ top: 0, behavior: 'smooth' });
            }
        });

        // 綁定 Scroll 事件
        window.addEventListener('scroll', handleScroll, { passive: true });
        
        // 初始化
        updateIconState();
    };

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();