動畫瘋评分显示增强

自用, 动画疯评分获取, 支持按需获取、排序、导出、暂停/继续/停止,并拥有独特的“动态声纳”高亮特效。

// ==UserScript==
// @name         動畫瘋评分显示增强
// @namespace    http://tampermonkey.net/
// @version      3.1.1
// @description  自用, 动画疯评分获取, 支持按需获取、排序、导出、暂停/继续/停止,并拥有独特的“动态声纳”高亮特效。
// @author       pixelpulse
// @match        https://ani.gamer.com.tw/*
// @connect      ani.gamer.com.tw
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @run-at       document-end
// @license      MIT
// ==/UserScript==
// 本脚本仅供个人学习和技术交流使用,请勿用于商业目的。所有数据的版权归原作者和知乎所有。因使用本脚本产生的一切后果由使用者自行承担。
/*
 * =================================================================================================
 * 【重要声明及使用条款 / Important Disclaimer and Terms of Use】
 *
 * 1. **脚本目的 (Purpose of the Script):**
 * 本脚本(動畫瘋评分显示增强)仅为个人学习和技术交流目的而开发。其核心功能是为用户提供评分聚合显示、
 * 排序及索引导出等辅助功能,旨在改善个人在浏览动画列表时的信息筛选效率。本脚本非官方出品,与
 * 動畫瘋(ani.gamer.com.tw)没有任何关联。
 *
 * 2. **数据与版权 (Data and Copyright):**
 * 本脚本处理及显示的所有内容(包括但不限于评分、标题、链接、图片)的版权、数据权及其他所有权,均归属于
 * 動畫瘋平台及其内容提供方所有。本脚本尊重并承认上述所有权。
 *
 * 3. **关于网络请求的说明 (Note on Network Requests):**
 * 为获取单部动画的评分,本脚本会模拟用户浏览行为,向動畫瘋服务器发送必要的网络请求。作者已采取
 * 【请求延迟、随机间隔、数据缓存】等技术手段,旨在最大程度上减少请求频率、降低服务器负载,以负责任的
 * 方式实现功能。请勿修改相关参数以进行不当的、高频率的请求。
 *
 * 4. **【特别条款】关于“導出為表格”功能 (SPECIAL CLAUSE regarding "Export to CSV" feature):**
 * a. **性质界定 (Definition of Nature):** 本功能导出的数据仅包含【排名、评分、名称、链接】等核心元数据。
 * 其性质是为用户个人备份或分析创建内容的“索引”或“目录”,并非内容本身的完整复制。
 * b. **使用限制 (Usage Restrictions):** 用户承诺,通过本功能导出的文件将严格用于【个人、非商业性】
 * 用途(如个人资料整理、观影记录)。
 * c. **严禁行为 (Prohibited Actions):** 【严禁】将导出的数据用于任何商业目的、进行二次分发、公开展示,
 * 或用于构建与動畫瘋存在竞争关系的产品或服务。
 *
 * 5. **风险与责任 (Risk and Liability):**
 * 本脚本按“原样”提供,作者不对其功能的完整性、准确性、稳定性或永久可用性作任何保证。用户因使用本脚本
 * 而产生的任何直接或间接后果,包括但不限于数据错误、账户风险或与動畫瘋《用户协议》可能产生的冲突,
 * 【均由用户本人承担全部责任】。作者对此不承担任何法律或经济责任。
 *
 * 6. **接受条款 (Acceptance of Terms):**
 * 一旦您选择安装并使用本脚本,即表示您已完整阅读、充分理解并同意遵守以上所有条款。如果您不同意
 * 任何条款,请立即停止使用并卸载本脚本。
 *
 * 请在遵守相关法律法规及動畫瘋平台规定的前提下,合理、负责地使用本脚本。
 * =================================================================================================
 */
(function () {
    'use strict';

    // --- 全局CSS样式注入 ---
    // 使用 GM_addStyle 在页面加载时一次性注入所有需要的CSS,包括UI、弹窗和动画效果。
    GM_addStyle(`
        /* 排序结果弹窗的半透明黑色背景遮罩层 */
        .anime-sorter-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.6); z-index: 9998; display: flex; align-items: center; justify-content: center; }
        /* 弹窗主体内容框 */
        .anime-sorter-content { background-color: #fff; color: #121212; border-radius: 8px; width: 80%; max-width: 800px; height: 80%; max-height: 80vh; display: flex; flex-direction: column; box-shadow: 0 5px 15px rgba(0,0,0,0.3); }
        /* 弹窗头部 */
        .anime-sorter-header { padding: 12px 16px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; gap: 10px; }
        .anime-sorter-title { font-size: 16px; font-weight: 600; }
        /* 弹窗关闭按钮 */
        .anime-sorter-close { font-size: 24px; font-weight: bold; cursor: pointer; border: none; background: none; padding: 0 8px; margin-left: auto; } /* margin-left: auto 确保关闭按钮在最右侧 */
        /* 弹窗可滚动的内容区域 */
        .anime-sorter-body { overflow-y: auto; padding: 8px 16px; text-align: center; }
        /* 排序结果的每一行 */
        .sorted-item { display: flex; align-items: center; padding: 10px 0; border-bottom: 1px solid #f0f0f0; text-align: left;}
        /* 新增:排名列样式 */
        .sorted-item-rank { font-size: 14px; font-weight: bold; color: #6c757d; flex-shrink: 0; width: 60px; text-align: center; }
        .sorted-item-score { font-size: 14px; font-weight: bold; color: #1772F6; flex-shrink: 0; width: 90px; }
        .sorted-item-details { flex-grow: 1; min-width: 0; }
        .sorted-item-title { font-size: 15px; color: #121212; text-decoration: none; display: block; margin-bottom: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
        .sorted-item-title:hover { color: #0084ff; }
        .sorted-item-actions { margin-top: 5px; }
        .sorted-item-button { font-size: 12px; padding: 3px 8px; margin-right: 8px; border: 1px solid #ccc; border-radius: 3px; background: #f9f9f9; cursor: pointer; text-decoration: none; color: #333; }
        .sorted-item-button:hover { background: #eee; border-color: #bbb; }
        /* 新增:导出按钮特殊样式 */
        #export-csv-btn { background-color: #28a745; color: white; border-color: #28a745; }

        /* “数字色彩闪烁”动画的关键帧 */
        @keyframes digital-flicker { 0% { border-color: #00FFFF; } 25% { border-color: #FF00FF; } 50% { border-color: #FFFF00; } 75% { border-color: #00FF00; } 100% { border-color: #FF4500; } }
        /* “声纳扩散”动画的关键帧 */
        @keyframes sonar-pulse { 0% { transform: translate(-50%, -50%) scale(0.1); opacity: 0.85; } 100% { transform: translate(-50%, -50%) scale(1.2); opacity: 0; } }
        /* 持久锁定的蓝色边框类 */
        .persistent-highlight { outline: 3px solid #007aff !important; box-shadow: 0 0 10px #007aff !important; z-index: 9999 !important; }
        /* 动态声纳/定位标线 的容器样式 */
        .highlight-reticle { position: absolute; top: 50%; left: 50%; width: 1px; height: 1px; transform: translate(-50%, -50%); pointer-events: none; z-index: 5; }
        /* 使用伪元素::before和::after创建两个同心圆,并应用动画 */
        .highlight-reticle::before, .highlight-reticle::after { content: ''; position: absolute; left: 0; top: 0; width: 200px; height: 200px; margin-left: -100px; margin-top: -100px; border-radius: 50%; border: 12px solid; opacity: 0.5; animation-name: digital-flicker, sonar-pulse; animation-duration: 1.5s, 2s; animation-iteration-count: infinite, infinite; animation-timing-function: linear, ease-out; }
        /* 让第二个圆延迟开始,形成扩散的视觉差 */
        .highlight-reticle::after { animation-delay: 1s; }
    `);

    // --- 全局配置与状态管理 ---
    let RATING_THRESHOLD = 4.5;
    const BASE_DELAY_MS = 500;
    const RANDOM_DELAY_MS = 1000;
    const CACHE_PREFIX = 'anime_rating_'; // 用于【评分性能缓存】
    const CACHE_EXPIRATION_MS = 24 * 60 * 60 * 1000;
    const SORT_DATA_SESSION_KEY = 'anime_sort_data'; // 用于【排序功能数据】

    let processingQueue = [], isPaused = false, isStopped = false, totalQueueCount = 0;
    let currentlyHighlightedElement = null, highlightedOverlayElement = null;
    let controlContainer, startButton, pauseResumeButton, stopButton, progressIndicator, sortButton, buttonGroup;

    // --- 核心处理逻辑 ---

    /**
     * @description 任务队列的“引擎”,负责从队列中取出一个任务并处理。
     */
    function processQueue() {
        if (isPaused || isStopped) return;
        if (processingQueue.length === 0) { resetUI(); return; }
        updateProgress();
        const card = processingQueue.shift();
        processAnimeCard(card);
    }

    /**
     * @description 处理单个动漫卡片的函数:获取评分、注入DOM、存入缓存。
     * @param {HTMLElement} card - 需要处理的动漫卡片<a>元素。
     */
    function processAnimeCard(card) {
        if (card.classList.contains('rating-processed')) { setTimeout(processQueue, 50); return; }
        card.classList.add('rating-processed');
        const animeLink = card.href;
        if (!animeLink) { setTimeout(processQueue, 50); return; }
        const snMatch = animeLink.match(/sn=(\d+)/);
        if (!snMatch) { setTimeout(processQueue, 50); return; }
        const animeSN = snMatch[1];
        const cachedData = getFromCache(animeSN);
        if (cachedData) {
            injectRating(card, cachedData.value);
            const delay = BASE_DELAY_MS / 2 + Math.random() * RANDOM_DELAY_MS / 2;
            setTimeout(processQueue, delay);
            return;
        }
        GM_xmlhttpRequest({
            method: "GET", url: animeLink, headers: { "User-Agent": navigator.userAgent, "Referer": window.location.href },
            onload: function (response) {
                let rating = 'Error';
                if (response.status >= 200 && response.status < 400) {
                    const parser = new DOMParser();
                    const doc = parser.parseFromString(response.responseText, 'text/html');
                    const ratingElement = doc.querySelector('.score-overall-number');
                    rating = ratingElement ? parseFloat(ratingElement.textContent).toFixed(1) : 'N/A';
                    saveToCache(animeSN, rating);
                }
                injectRating(card, rating);
                const delay = BASE_DELAY_MS + Math.random() * RANDOM_DELAY_MS;
                setTimeout(processQueue, delay);
            },
            onerror: function () {
                injectRating(card, 'Error');
                const delay = BASE_DELAY_MS + Math.random() * RANDOM_DELAY_MS;
                setTimeout(processQueue, delay);
            }
        });
    }

    // --- UI 控制与事件处理 ---

    /**
     * @description 创建并初始化所有控制按钮和面板。
     */
    function createControls() {
        controlContainer = document.createElement('div');
        controlContainer.style.position = 'fixed'; controlContainer.style.bottom = '20px'; controlContainer.style.left = '20px';
        controlContainer.style.zIndex = '9999'; controlContainer.style.display = 'flex'; controlContainer.style.flexDirection = 'column';
        controlContainer.style.alignItems = 'flex-start'; controlContainer.style.gap = '10px';
        buttonGroup = document.createElement('div');
        buttonGroup.style.display = 'flex'; buttonGroup.style.flexDirection = 'column'; buttonGroup.style.gap = '8px';
        const createButton = (id, text, onClick) => {
            const button = document.createElement('button');
            button.id = id; button.textContent = text;
            button.style.padding = '8px 12px'; button.style.fontSize = '14px'; button.style.color = 'white';
            button.style.border = 'none'; button.style.borderRadius = '5px'; button.style.cursor = 'pointer';
            button.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)'; button.style.width = '100px';
            button.addEventListener('click', onClick);
            return button;
        };
        sortButton = createButton('sortBtn', '排序', handleSort);
        sortButton.style.backgroundColor = '#6f42c1';
        startButton = createButton('startBtn', '获取评分', promptAndFetch);
        startButton.style.backgroundColor = '#00a0d8';
        pauseResumeButton = createButton('pauseResumeBtn', '暂停', handlePauseResume);
        pauseResumeButton.style.backgroundColor = '#ffc107';
        stopButton = createButton('stopBtn', '停止', handleStop);
        stopButton.style.backgroundColor = '#dc3545';
        const processControls = document.createElement('div');
        processControls.style.display = 'flex'; processControls.style.gap = '8px';
        processControls.append(pauseResumeButton, stopButton);
        progressIndicator = document.createElement('span');
        progressIndicator.style.color = 'black'; progressIndicator.style.backgroundColor = 'rgba(255, 255, 255, 0.8)';
        progressIndicator.style.padding = '5px 10px'; progressIndicator.style.borderRadius = '5px'; progressIndicator.style.fontSize = '14px';
        buttonGroup.append(sortButton, startButton);
        controlContainer.append(buttonGroup, processControls, progressIndicator);
        document.body.appendChild(controlContainer);
        resetUI();
    }

    /**
     * @description 重置UI到初始状态(“待命”状态)。
     */
    function resetUI() {
        buttonGroup.style.display = 'flex';
        startButton.style.display = 'inline-block';
        sortButton.disabled = false;
        sortButton.style.opacity = '1';
        pauseResumeButton.style.display = 'none';
        stopButton.style.display = 'none';
        progressIndicator.style.display = 'none';
        pauseResumeButton.textContent = '暂停';
        pauseResumeButton.style.backgroundColor = '#ffc107';
    }

    /**
     * @description 设置UI到“处理中”状态。
     */
    function setProcessingUI() {
        buttonGroup.style.display = 'flex';
        startButton.style.display = 'none';
        sortButton.disabled = true;
        sortButton.style.opacity = '0.5';
        pauseResumeButton.style.display = 'inline-block';
        stopButton.style.display = 'inline-block';
        progressIndicator.style.display = 'inline-block';
    }

    /**
     * @description 更新进度指示器的文本。
     */
    function updateProgress() {
        const processedCount = totalQueueCount - processingQueue.length;
        progressIndicator.textContent = `处理中: ${processedCount} / ${totalQueueCount}`;
    }

    /**
     * @description “获取评分”按钮的点击事件处理函数,负责弹出询问框。
     */
    function promptAndFetch() {
        clearCurrentHighlight();
        const userInput = prompt('需要高亮≥?分的动漫?', RATING_THRESHOLD);
        if (userInput === null) return;
        const newThreshold = parseFloat(userInput);
        if (!isNaN(newThreshold)) { RATING_THRESHOLD = newThreshold; }
        startProcessing();
    }

    /**
     * @description 初始化任务队列并开始处理。
     */
    function startProcessing() {
        if (getSortData().length > 0) {
            if (confirm('是否要清除之前的排序数据并重新开始?')) {
                setSortData([]);
            }
        }
        isStopped = false; isPaused = false;
        const animeCards = document.querySelectorAll('a.anime-card-block:not(.rating-processed), a.theme-list-main:not(.rating-processed)');
        processingQueue = Array.from(animeCards);
        totalQueueCount = processingQueue.length;
        if (totalQueueCount === 0) {
            alert('当前页面已无未获取评分的动漫。');
            resetUI();
            return;
        }
        setProcessingUI();
        processQueue();
    }

    /**
     * @description “暂停/继续”按钮的点击事件处理函数。
     */
    function handlePauseResume() {
        clearCurrentHighlight();
        isPaused = !isPaused;
        if (isPaused) {
            pauseResumeButton.textContent = '继续';
            pauseResumeButton.style.backgroundColor = '#28a745';
            sortButton.disabled = false;
            sortButton.style.opacity = '1';
        } else {
            pauseResumeButton.textContent = '暂停';
            pauseResumeButton.style.backgroundColor = '#ffc107';
            sortButton.disabled = true;
            sortButton.style.opacity = '0.5';
            processQueue();
        }
    }

    /**
     * @description “停止”按钮的点击事件处理函数。
     */
    function handleStop() {
        clearCurrentHighlight();
        isStopped = true;
        processingQueue = [];
        resetUI();
    }

    // --- 排序、弹窗与导出模块 ---

    /**
     * @description “排序”按钮的点击事件处理函数。
     */
    function handleSort() {
        clearCurrentHighlight();
        const storedData = getSortData().filter(item => !isNaN(item.score));
        if (storedData.length === 0) {
            displaySortModal([]);
            return;
        }
        const dataWithElements = storedData.map(item => {
            const urlQuery = item.url.split('?')[1];
            const element = urlQuery ? document.querySelector(`a[href$="?${urlQuery}"]`) : null;
            return { ...item, element };
        }).filter(item => item.element);
        if (dataWithElements.length === 0) {
            alert('排序数据中的项目均未在当前页面找到。可能需要重新获取评分。');
            return;
        }
        const sorted = dataWithElements.sort((a, b) => b.score - a.score);
        displaySortModal(sorted);
    }

    /**
     * @description 关闭并移除排序结果弹窗。
     */
    function closeSortModal() {
        const modal = document.getElementById('anime-sorter-modal');
        if (modal) { document.body.removeChild(modal); }
    }

    /**
     * @description 在弹窗中动态生成并显示排序结果。
     * @param {Array} sortedItems - 已排序的、且附加了DOM元素引用的项目数据。
     */
    function displaySortModal(sortedItems) {
        closeSortModal();
        const overlay = document.createElement('div');
        overlay.id = 'anime-sorter-modal';
        overlay.className = 'anime-sorter-overlay';

        let modalHtml = `<div class="anime-sorter-content"><div class="anime-sorter-header"><span class="anime-sorter-title">评分排序结果</span><button id="export-csv-btn" class="sorted-item-button">導出為表格</button><button class="anime-sorter-close">&times;</button></div><div class="anime-sorter-body">`;
        if (sortedItems.length === 0) {
            modalHtml += `<p style="padding: 20px;">暂无数据。请先点击【获取评分】按钮来收集动漫评分。</p>`;
        } else {
            sortedItems.forEach((item, index) => {
                modalHtml += `<div class="sorted-item"><div class="sorted-item-rank">#${index + 1}</div><div class="sorted-item-score">★ ${item.scoreText}</div><div class="sorted-item-details"><a class="sorted-item-title" href="${item.url}" target="_blank" title="${item.name.replace(/"/g, '&quot;')}">${item.name}</a><div class="sorted-item-actions"><a href="${item.url}" target="_blank" class="sorted-item-button">新窗口打开</a><button class="sorted-item-button locate-btn" data-item-index="${index}">定位</button></div></div></div>`;
            });
        }
        modalHtml += `</div></div>`;
        overlay.innerHTML = modalHtml;
        document.body.appendChild(overlay);

        if (sortedItems.length > 0) {
            overlay.querySelector('#export-csv-btn').addEventListener('click', () => {
                exportToCsv(sortedItems);
            });
        }

        overlay.querySelectorAll('.locate-btn').forEach(btn => {
            btn.addEventListener('click', () => {
                const itemIndex = parseInt(btn.dataset.itemIndex, 10);
                const item = sortedItems[itemIndex];
                if (!item || !item.element) { alert('定位失败,无法找到对应的DOM元素。'); return; }
                closeSortModal();
                performVisualLock(item.element);
            });
        });

        overlay.querySelector('.anime-sorter-close').addEventListener('click', closeSortModal);
        overlay.addEventListener('click', (e) => { if (e.target === overlay) closeSortModal(); });
    }

    /**
     * @description 将排序结果导出为CSV文件并触发下载。
     * @param {Array} data - 已排序的动漫数据数组。
     */
    function exportToCsv(data) {
        let csvContent = '"排名","评分","名称","链接"\n';
        data.forEach((item, index) => {
            const rank = index + 1;
            const score = item.scoreText;
            const name = `"${item.name.replace(/"/g, '""')}"`;
            const url = item.url;
            csvContent += `${rank},${score},${name},${url}\n`;
        });
        const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' });
        const link = document.createElement("a");
        const url = URL.createObjectURL(blob);
        link.setAttribute("href", url);
        link.setAttribute("download", "動畫瘋評分排序.csv");
        link.style.visibility = 'hidden';
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
    }

    // --- 视觉锁定与高亮清除功能 ---

    /**
     * @description 清除当前所有的高亮效果(蓝框和声纳)。
     */
    function clearCurrentHighlight() {
        if (currentlyHighlightedElement) {
            currentlyHighlightedElement.classList.remove('persistent-highlight');
        }
        if (highlightedOverlayElement) {
            highlightedOverlayElement.remove();
        }
        currentlyHighlightedElement = null;
        highlightedOverlayElement = null;
    }

    /**
     * @description 对指定元素执行“蓝框 + 动态声纳”的视觉锁定效果。
     * @param {HTMLElement} element - 要高亮的目标卡片元素。
     */
    function performVisualLock(element) {
        clearCurrentHighlight();
        currentlyHighlightedElement = element;
        element.classList.add('persistent-highlight');
        element.scrollIntoView({ behavior: 'smooth', block: 'center' });
        const reticle = document.createElement('div');
        reticle.className = 'highlight-reticle';
        const injectionTarget = element.classList.contains('theme-list-main') ? element.querySelector('.theme-img-block') : element;
        if (injectionTarget) {
            injectionTarget.style.position = 'relative';
            injectionTarget.appendChild(reticle);
            highlightedOverlayElement = reticle;
        }
    }

    // --- 辅助函数 ---

    /**
     * @description 将评分标签注入DOM,并将数据存入排序库。
     */
    function injectRating(card, rating) {
        const nameElement = card.querySelector('.anime-name_for-marquee') || card.querySelector('.theme-name') || card.querySelector('.anime-name');
        const animeName = nameElement ? nameElement.textContent.trim() : '未知名称';
        const numericRating = parseFloat(rating);
        if (!isNaN(numericRating)) {
            let storedData = getSortData();
            if (!storedData.some(item => item.url === card.href)) {
                storedData.push({ name: animeName, score: numericRating, scoreText: rating, url: card.href });
                setSortData(storedData);
            }
        }
        const injectionTarget = card.classList.contains('theme-list-main') ? card.querySelector('.theme-img-block') : card;
        if (!injectionTarget) return;
        const ratingDiv = document.createElement('div');
        ratingDiv.style.position = 'absolute'; ratingDiv.style.top = '5px'; ratingDiv.style.right = '5px';
        ratingDiv.style.padding = '2px 6px'; ratingDiv.style.backgroundColor = 'rgba(0, 0, 0, 0.75)';
        ratingDiv.style.color = 'white'; ratingDiv.style.fontSize = '14px'; ratingDiv.style.fontWeight = 'bold';
        ratingDiv.style.borderRadius = '4px'; ratingDiv.style.zIndex = '10';
        ratingDiv.textContent = `★ ${rating}`;
        if (!isNaN(numericRating) && numericRating >= RATING_THRESHOLD) {
            const highlightTarget = card.classList.contains('theme-list-main') ? card.parentElement : card;
            highlightTarget.style.outline = '3px solid #FFD700';
            ratingDiv.style.color = '#FFD700';
        }
        injectionTarget.style.position = 'relative';
        injectionTarget.appendChild(ratingDiv);
    }

    /**
     * @description 将【单个评分】存入localStorage,用于加速下次加载显示,有有效期。
     */
    function saveToCache(key, value) {
        const item = { value, timestamp: new Date().getTime() };
        localStorage.setItem(CACHE_PREFIX + key, JSON.stringify(item));
    }

    /**
     * @description 从localStorage读取【单个评分】的有效缓存。
     */
    function getFromCache(key) {
        const itemStr = localStorage.getItem(CACHE_PREFIX + key);
        if (!itemStr) return null;
        const item = JSON.parse(itemStr);
        if (new Date().getTime() - item.timestamp > CACHE_EXPIRATION_MS) {
            localStorage.removeItem(CACHE_PREFIX + key);
            return null;
        }
        return item;
    }

    /**
     * @description 从sessionStorage获取【用于排序】的所有动漫数据。
     */
    function getSortData() { return JSON.parse(sessionStorage.getItem(SORT_DATA_SESSION_KEY) || '[]'); }

    /**
     * @description 将【用于排序】的所有动漫数据写入sessionStorage。
     */
    function setSortData(data) { sessionStorage.setItem(SORT_DATA_SESSION_KEY, JSON.stringify(data)); }

    // --- 脚本入口 ---

    // 当页面完全加载后,执行createControls函数来创建UI。
    window.addEventListener('load', createControls);

})();