Wanikani: Burn Celebration

Plays a celebration sound and shows confetti when you burn an item

// ==UserScript==
// @name         Wanikani: Burn Celebration
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Plays a celebration sound and shows confetti when you burn an item
// @author       Alterux
// @match        https://www.wanikani.com/*
// @match        https://preview.wanikani.com/*
// @license      MIT; http://opensource.org/licenses/MIT
// @grant        none
// ==/UserScript==

(function ($, wkof) {
    // Constants
    const DEG_TO_RAD = Math.PI / 180;
    const FRAME_RATE = 60;
    const DT = 1.0 / FRAME_RATE;
    const COLORS = [
        ["#df0049", "#660671"],
        ["#00e857", "#005291"],
        ["#2bebbc", "#05798a"],
        ["#ffd200", "#b06c00"]
    ];

    let lastBurnedKanji = '';
    let interval;
    let confettiPapers = [];
    let settings = {};

    class Vector2 {
        constructor(x, y) {
            this.x = x;
            this.y = y;
        }
    }

    class ConfettiPaper {
        constructor(_x, _y) {
            this.pos = new Vector2(_x, _y);
            this.rotationSpeed = Math.random() * 600 + 800;
            this.angle = DEG_TO_RAD * Math.random() * 360;
            this.rotation = DEG_TO_RAD * Math.random() * 360;
            this.cosA = 1.0;
            this.size = 5.0;
            this.oscillationSpeed = Math.random() * 1.5 + 0.5;
            this.xSpeed = 40.0;
            this.ySpeed = Math.random() * 60 + 50.0;
            this.time = Math.random();
            this.corners = Array.from({ length: 4 }, (_, i) => {
                let angle = this.angle + DEG_TO_RAD * (i * 90 + 45);
                return new Vector2(Math.cos(angle), Math.sin(angle));
            });
            let ci = Math.floor(Math.random() * COLORS.length);
            [this.frontColor, this.backColor] = COLORS[ci];
        }

        update() {
            this.time += DT;
            this.rotation += this.rotationSpeed * DT;
            this.cosA = Math.cos(DEG_TO_RAD * this.rotation);
            this.pos.x += Math.cos(this.time * this.oscillationSpeed) * this.xSpeed * DT;
            this.pos.y += this.ySpeed * DT;
            if (this.pos.y > window.innerHeight) {
                this.dead = true;
            }
        }

        draw(ctx) {
            ctx.fillStyle = this.cosA > 0 ? this.frontColor : this.backColor;
            ctx.beginPath();
            ctx.moveTo(
                this.pos.x + this.corners[0].x * this.size,
                this.pos.y + this.corners[0].y * this.size * this.cosA
            );
            for (let i = 1; i < 4; i++) {
                ctx.lineTo(
                    this.pos.x + this.corners[i].x * this.size,
                    this.pos.y + this.corners[i].y * this.size * this.cosA
                );
            }
            ctx.closePath();
            ctx.fill();
        }
    }

    async function initializeWKOF() {
        if (!wkof) {
            if (confirm(`${script_name} requires WaniKani Open Framework.\nClick "OK" to be forwarded to installation instructions.`)) {
                window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549';
            }
            return;
        }

        await wkof.include('Menu,Settings');
        await wkof.ready('Settings,Menu');
        const defaults = {
            celebrationSound: 'enabled',
            celebrationConfetti: 'enabled'
        };
        settings = await wkof.Settings.load('burn_celebration', defaults);
        wkof.Menu.insert_script_link({
            name: 'burn_celebration',
            submenu: 'Settings',
            title: 'Burn Celebration',
            on_click: open_settings
        });
    }

    function open_settings() {
        const config = {
            script_id: 'burn_celebration',
            title: 'Burn Celebration',
            on_save: updateSettings,
            content: {
                celebrationSound: {
                    type: 'dropdown',
                    default: 'enabled',
                    label: 'Celebration Sound',
                    hover_tip: 'Play a celebration sound when you burn an item',
                    content: { disabled: 'Disabled', enabled: 'Enabled' },
                },
                celebrationConfetti: {
                    type: 'dropdown',
                    default: 'enabled',
                    label: 'Confetti Animation',
                    hover_tip: 'Show confetti when you burn an item',
                    content: { disabled: 'Disabled', enabled: 'Enabled' },
                },
            },
        };
        new wkof.Settings(config).open();
    }

    function updateSettings() {
        if (settings.celebrationSound !== 'disabled') {
            new Audio("https://toemat.com/wanikani/burnit.ogg").play();
        }
        if (settings.celebrationConfetti !== 'disabled') {
            startConfetti();
        }
    }

    function startConfetti() {
        if (interval) {
            clearInterval(interval);
            confettiPapers = [];
        }
        let confettiPaperCount = 100;
        for (let i = 0; i < confettiPaperCount; i++) {
            confettiPapers.push(new ConfettiPaper(Math.random() * window.innerWidth, Math.random() * (-50)));
        }
        interval = setInterval(updateConfetti, 1000.0 / FRAME_RATE);
    }

    function updateConfetti() {
        let canvas = document.getElementById("confettiCanvas") || createConfettiCanvas();
        let ctx = canvas.getContext('2d');
        ctx.clearRect(0, 0, canvas.width, canvas.height);

        for (const paper of confettiPapers) {
            paper.update();
            paper.draw(ctx);
        }

        confettiPapers = confettiPapers.filter(paper => !paper.dead);
        if (!confettiPapers.length) {
            clearInterval(interval);
            canvas.remove();
        }
    }

    function createConfettiCanvas() {
        let canvas = document.createElement('canvas');
        canvas.id = "confettiCanvas";
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;
        canvas.style.zIndex = "9999";
        canvas.style.position = "fixed";
        canvas.style.top = "0";
        canvas.style.left = "0";
        canvas.style.pointerEvents = "none"; // This allows click events to pass through the canvas.
        document.body.appendChild(canvas);
        return canvas;
    }

    window.addEventListener('didChangeSRS', (e) => {
        const srs = e.detail.newLevelText;
        const currentKanji = document.querySelector('.character-header__characters').textContent;

        if (/burn/i.test(srs) && currentKanji !== lastBurnedKanji) {
            lastBurnedKanji = currentKanji;
            if (settings.celebrationSound !== 'disabled') {
                new Audio("https://toemat.com/wanikani/burnit.ogg").play();
            }
            if (settings.celebrationConfetti !== 'disabled') {
                startConfetti();
            }
        }
    });

    initializeWKOF();

})(window.jQuery, window.wkof);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址