🎮ArcadeLab

摇一摇赛龙舟·端午争渡

by DriftMeteor50
671 lines25.0 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <!-- viewport 设置,禁止用户缩放,保证在大屏上正常显示 -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>摇一摇赛龙舟·端午争渡</title>
    <style>
        /* 全局样式:深色背景 + 水墨氛围 */
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            user-select: none; /* 避免选中文字,提升操作手感 */
        }

        body {
            width: 100%;
            min-height: 100vh;
            background: linear-gradient(145deg, #0a3e2c 0%, #0a2f1f 100%);
            font-family: 'Segoe UI', 'PingFang SC', Roboto, 'Noto Sans CJK SC', 'Microsoft YaHei', sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 20px;
        }

        /* 主游戏卡片 */
        .game-container {
            width: 100%;
            max-width: 800px;
            background: rgba(255,245,220,0.12);
            backdrop-filter: blur(3px);
            border-radius: 72px 72px 48px 48px;
            box-shadow: 0 25px 45px rgba(0,0,0,0.4), inset 0 1px 2px rgba(255,255,200,0.2);
            padding: 18px 20px 30px;
            transition: all 0.2s;
        }

        /* 标题区 + 端午装饰 */
        .title-area {
            text-align: center;
            margin-bottom: 15px;
            position: relative;
        }
        h1 {
            font-size: clamp(32px, 8vw, 56px);
            background: linear-gradient(135deg, #FFE6A7, #FFB347, #FF8C42);
            background-clip: text;
            -webkit-background-clip: text;
            color: transparent;
            text-shadow: 0 2px 5px rgba(0,0,0,0.3);
            letter-spacing: 0.08em;
        }
        .sub {
            font-size: 14px;
            color: #FFDEAD;
            background: rgba(0,0,0,0.4);
            display: inline-block;
            padding: 4px 16px;
            border-radius: 60px;
            margin-top: 6px;
            backdrop-filter: blur(4px);
        }
        /* 龙舟竞渡主视觉 */
        .race-track {
            background: #1A5940;
            border-radius: 48px;
            padding: 20px 12px 12px;
            margin: 10px 0 20px;
            box-shadow: inset 0 0 0 2px rgba(255,220,150,0.6), inset 0 0 0 6px #2F6B4A, 0 12px 20px rgba(0,0,0,0.3);
        }
        .river {
            position: relative;
            background: linear-gradient(180deg, #3B9C6E, #1C6B3F);
            border-radius: 60px;
            min-height: 220px;
            overflow: hidden;
            box-shadow: inset 0 5px 12px rgba(0,0,0,0.3), 0 1px 4px rgba(255,255,200,0.3);
        }
        /* 波浪装饰 */
        .wave {
            position: absolute;
            bottom: 0;
            left: 0;
            width: 100%;
            height: 48px;
            background: repeating-linear-gradient( transparent 0px, transparent 28px, rgba(220,240,255,0.4) 28px, rgba(200,225,250,0.5) 34px);
            pointer-events: none;
            z-index: 1;
        }
        /* 龙舟本体 */
        .dragon-boat {
            position: absolute;
            bottom: 20px;
            left: 5%;
            width: 95px;
            transition: left 0.08s linear;
            filter: drop-shadow(2px 6px 8px rgba(0,0,0,0.5));
            z-index: 10;
            pointer-events: none;
        }
        .dragon-boat img {
            width: 100%;
            display: block;
        }

        /* 排名面板 */
        .ranking-panel {
            background: rgba(0, 0, 0, 0.65);
            backdrop-filter: blur(12px);
            border-radius: 32px;
            padding: 14px 18px;
            margin: 12px 0 0;
            border: 1px solid #FFC285;
            color: #FEF2DF;
        }
        .panel-title {
            display: flex;
            justify-content: space-between;
            border-bottom: 1px solid #E6B87E;
            padding-bottom: 8px;
            margin-bottom: 10px;
            font-weight: bold;
            font-size: 18px;
        }
        .rank-list {
            max-height: 188px;
            overflow-y: auto;
        }
        .rank-item {
            display: flex;
            align-items: center;
            justify-content: space-between;
            background: rgba(255,245,200,0.1);
            margin: 6px 0;
            padding: 6px 12px;
            border-radius: 60px;
            font-size: 15px;
        }
        .rank-number {
            width: 32px;
            font-weight: bold;
            color: #FFC489;
        }
        .player-name {
            flex: 1;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            padding: 0 12px;
        }
        .score {
            background: #FFA559;
            padding: 2px 12px;
            border-radius: 30px;
            font-weight: bold;
            color: #2C2B2B;
        }
        /* 按钮组 */
        .action-buttons {
            display: flex;
            justify-content: center;
            gap: 18px;
            margin: 22px 0 12px;
            flex-wrap: wrap;
        }
        .btn {
            border: none;
            background: #FFCF9A;
            color: #422b1a;
            font-weight: bold;
            padding: 12px 26px;
            border-radius: 48px;
            font-size: 17px;
            cursor: pointer;
            transition: 0.15s;
            box-shadow: 0 4px 6px rgba(0,0,0,0.2);
            display: inline-flex;
            align-items: center;
            gap: 8px;
        }
        .btn-primary {
            background: #FFB347;
            color: #2F2E2E;
            box-shadow: 0 5px 0 #B4621A;
            transform: translateY(-2px);
        }
        .btn-primary:active {
            transform: translateY(3px);
            box-shadow: 0 1px 0 #B4621A;
        }
        .btn-secondary {
            background: #4F9A6F;
            color: white;
            box-shadow: none;
        }
        .shake-hint {
            text-align: center;
            color: #FFE1A0;
            font-size: 14px;
            background: rgba(0,0,0,0.5);
            border-radius: 40px;
            width: fit-content;
            margin: 12px auto 0;
            padding: 6px 24px;
            display: flex;
            align-items: center;
            gap: 8px;
        }
        /* 参赛者输入浮层 */
        .name-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.85);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 1000;
            backdrop-filter: blur(8px);
        }
        .name-card {
            background: #fef3db;
            max-width: 300px;
            width: 80%;
            border-radius: 56px;
            padding: 28px 18px;
            text-align: center;
            box-shadow: 0 20px 35px black;
        }
        .name-card input {
            width: 80%;
            padding: 12px 16px;
            font-size: 18px;
            margin: 20px 0;
            border: 2px solid #CA8E4A;
            border-radius: 56px;
            text-align: center;
        }
        .confirm-btn {
            background: #E07C2C;
            color: white;
            border: none;
            padding: 10px 30px;
            border-radius: 40px;
            font-weight: bold;
        }
        .permission-tip {
            background: #f9e2b7;
            border-left: 7px solid #ff9f4a;
            color: #32261a;
            padding: 8px 12px;
            border-radius: 20px;
            margin: 10px 0;
            font-size: 14px;
        }
        footer {
            text-align: center;
            font-size: 11px;
            color: #B7C9B3;
            margin-top: 18px;
        }

        /* 小屏适配 */
        @media (max-width: 550px) {
            .game-container { padding: 12px; }
            .dragon-boat { width: 70px; }
            .rank-item { font-size: 12px; }
            .btn { padding: 8px 18px; font-size: 14px;}
        }
    </style>
</head>
<body>
<div class="game-container" id="gameApp">
    <div class="title-area">
        <h1>🐉 摇一摇·龙舟竞渡 🥁</h1>
        <div class="sub">端午安康 · 赛出风采</div>
    </div>

    <div class="race-track">
        <div class="river" id="riverTrack">
            <div class="wave"></div>
            <div class="dragon-boat" id="dragonBoat">
                <!-- 使用 Emoji + SVG 风格龙舟,无需额外图片资源,零物料 -->
                <div style="font-size: 68px; line-height: 1; text-align: center; filter: drop-shadow(2px 6px 6px #00000055);">🚣‍♂️🐉</div>
            </div>
        </div>
    </div>

    <!-- 排名面板 -->
    <div class="ranking-panel">
        <div class="panel-title">
            <span>🏆 赛况龙舟榜 🏆</span>
            <span>🏁 目标: 500桨 🏁</span>
        </div>
        <div class="rank-list" id="rankList">
            <div style="text-align:center; padding: 10px;">等待选手上场...</div>
        </div>
    </div>

    <div class="action-buttons">
        <button class="btn btn-primary" id="joinRaceBtn">✨ 我要参赛 ✨</button>
        <button class="btn btn-secondary" id="resetRaceBtn">🔄 重置擂台</button>
    </div>
    <div class="shake-hint" id="shakeHint">
        📱 疯狂摇动手机 → 龙舟向前冲!🎋
    </div>
    <footer>🎋 摇得越快,龙舟越快,先达500桨者夺冠 🎋</footer>
</div>

<script>
    // 简易状态管理,纯前端排名(本次活动数据不依赖后端, 但满足300人氛围)
    // 为了「各团队自行组织 + 大屏展示」,所有参赛玩家存储在当前浏览器localStorage,但单一会场大屏下完全够用
    // 如果是大屏投射(一台电脑/ipad投屏, 观众用自己的手机扫码访问同一个网址摇动,是独立排名)
    // 注意: 摇一摇仅手机生效, 大屏主持人可手动重置或展示最终英雄榜

    // ---------- 游戏全局参数 -------------
    const GOAL_SCORE = 500;          // 目标桨数
    const SHAKE_COUNT_PER_THRESHOLD = 1;  // 每次有效“摇动”增加1桨 (真实竞速感)
    let raceActive = true;            // 比赛是否进行中(有人达到500后暂停摇一摇增量)
    let currentPlayerId = null;       // 当前参与者ID
    let currentPlayerName = null;
    
    // 排行榜数组 { id, name, score, finishTime }
    let leaderboard = [];

    // 摇一摇防连击 & 节流相关变量
    let lastShakeTime = 0;
    let isProcessingShake = false;
    let lastAcceleration = { x: 0, y: 0, z: 0 };
    let shakeCooldown = 300;          // 毫秒内多次摇动只算一次,保证公平性
    let motionListenerActive = false;  // 监听器是否已绑定
    let devicemotionHandler = null;
    
    // DOM 元素
    const dragonBoatEl = document.getElementById('dragonBoat');
    const rankListDiv = document.getElementById('rankList');
    const joinBtn = document.getElementById('joinRaceBtn');
    const resetBtn = document.getElementById('resetRaceBtn');
    const shakeHintDiv = document.getElementById('shakeHint');

    // 获取存储的排名数据 (本地存储)
    function loadLeaderboard() {
        const stored = localStorage.getItem('dragonBoat_rank');
        if(stored) {
            try{
                leaderboard = JSON.parse(stored);
                if(!Array.isArray(leaderboard)) leaderboard = [];
                // 清理未完成的玩家(仅保留分数<GOAL_SCORE的排名中的非冠军,如果某玩家原本分数>=GOAL且未在当前活跃时,保持但视作已经完赛)
                leaderboard = leaderboard.filter(p => p && typeof p.score === 'number');
                leaderboard.sort((a,b) => {
                    if(a.score >= GOAL_SCORE && b.score >= GOAL_SCORE) return (a.finishTime||Infinity) - (b.finishTime||Infinity);
                    if(a.score >= GOAL_SCORE) return -1;
                    if(b.score >= GOAL_SCORE) return 1;
                    return b.score - a.score;
                });
            } catch(e){ leaderboard = []; }
        } else {
            leaderboard = [];
        }
        renderRanking();
    }
    
    function saveLeaderboard() {
        localStorage.setItem('dragonBoat_rank', JSON.stringify(leaderboard));
        renderRanking();
    }
    
    function renderRanking() {
        if(!rankListDiv) return;
        const activePlayers = leaderboard.filter(p => p.score < GOAL_SCORE);
        const finishedPlayers = leaderboard.filter(p => p.score >= GOAL_SCORE).sort((a,b)=> (a.finishTime||0) - (b.finishTime||0));
        const combined = [...finishedPlayers, ...activePlayers.sort((a,b)=>b.score - a.score)];
        if(combined.length === 0) {
            rankListDiv.innerHTML = '<div style="text-align:center; padding: 12px;">🏮 摇动手机,抢占龙舟榜首 🏮</div>';
            return;
        }
        let html = '';
        combined.forEach((player, idx) => {
            const isChampion = (player.score >= GOAL_SCORE);
            let statusIcon = isChampion ? '🏆 ' : (player.score > 0 ? '🐉 ' : '⛵ ');
            let scoreDisplay = `${player.score} / ${GOAL_SCORE}`;
            if(isChampion) scoreDisplay = `🏁 夺冠 ${player.score}`;
            html += `<div class="rank-item">
                        <span class="rank-number">${idx+1}</span>
                        <span class="player-name">${statusIcon} ${escapeHtml(player.name)}</span>
                        <span class="score">${scoreDisplay}</span>
                     </div>`;
        });
        rankListDiv.innerHTML = html;
        // 如果游戏已结束且所有玩家都达到终点,自动锁定摇一摇增量直到重置
        if(activePlayers.length === 0 && combined.length>0 && combined.every(p=>p.score>=GOAL_SCORE)) {
            raceActive = false;
            shakeHintDiv.innerHTML = '🏮 竞渡终章!点击「重置擂台」再启征程 🏮';
        } else {
            if(!raceActive && combined.some(p=>p.score < GOAL_SCORE)) raceActive = true;
            if(raceActive) shakeHintDiv.innerHTML = '📱 疯狂摇动手机 → 龙舟向前冲!🎋';
        }
    }
    
    // 简单防XSS
    function escapeHtml(str) {
        if(!str) return '无名桨手';
        return str.replace(/[&<>]/g, function(m) {
            if(m === '&') return '&amp;';
            if(m === '<') return '&lt;';
            if(m === '>') return '&gt;';
            return m;
        }).substring(0, 16);
    }
    
    // 增加桨数
    function addScore(playerId, increment = 1) {
        if(!raceActive) return false;
        const player = leaderboard.find(p => p.id === playerId);
        if(!player) return false;
        if(player.score >= GOAL_SCORE) return false; // 已完赛不再加分
        
        let newScore = Math.min(player.score + increment, GOAL_SCORE);
        player.score = newScore;
        if(newScore >= GOAL_SCORE && !player.finishTime) {
            player.finishTime = Date.now();
            raceActive = false;   // 有人夺冠立即冻结摇一摇,不再接受新桨
            shakeHintDiv.innerHTML = `🎉 恭喜「${escapeHtml(player.name)}」率先破浪夺魁!🎉 重置后再次争渡~ 🎉`;
            // 通知大屏效果
            setTimeout(() => {
                if(leaderboard.some(p=>p.score < GOAL_SCORE)) raceActive = true;
                else raceActive = false;
                renderRanking();
            },100);
        }
        saveLeaderboard();
        // 更新龙舟动画: 根据当前玩家的进度展示偏移比例
        if(currentPlayerId === playerId) {
            updateBoatProgress(player.score);
        }
        return true;
    }
    
    // 更新UI龙舟位置 (按分数比例,不超过河道宽度的85%)
    function updateBoatProgress(score) {
        if(!dragonBoatEl) return;
        const river = document.getElementById('riverTrack');
        if(!river) return;
        const riverWidth = river.clientWidth;
        const maxLeftPercent = Math.min(0.85, (score / GOAL_SCORE) * 0.92);
        const leftPercent = Math.min(0.85, Math.max(0.05, maxLeftPercent));
        dragonBoatEl.style.left = `calc(${leftPercent * 100}% - 25px)`;
        if(score >= GOAL_SCORE) {
            dragonBoatEl.style.filter = "drop-shadow(0 0 6px gold)";
        } else {
            dragonBoatEl.style.filter = "drop-shadow(2px 6px 8px rgba(0,0,0,0.5))";
        }
    }
    
    // 创建玩家或者切换玩家
    function registerAndStart(playerName) {
        if(!playerName || playerName.trim() === '') playerName = "粽情桨手";
        playerName = playerName.trim().substring(0, 16);
        // 查找是否已存在同名玩家(这里简单起见,允许同名不同ID,但榜单展示好)
        let existing = leaderboard.find(p => p.name === playerName && p.score < GOAL_SCORE);
        if(existing) {
            currentPlayerId = existing.id;
            currentPlayerName = existing.name;
            updateBoatProgress(existing.score);
            shakeHintDiv.innerHTML = `🐉 继续为「${escapeHtml(currentPlayerName)}」摇桨! 当前${existing.score}/${GOAL_SCORE}`;
            if(existing.score >= GOAL_SCORE) {
                shakeHintDiv.innerHTML = `🏁 ${escapeHtml(playerName)} 已斩获冠军,无法划桨,请重置擂台 🏁`;
                raceActive = false;
            } else {
                raceActive = true;
            }
            renderRanking();
            return;
        }
        // 新玩家
        const newId = 'pid_' + Date.now() + '_' + Math.floor(Math.random()*10000);
        const newPlayer = {
            id: newId,
            name: playerName,
            score: 0,
            finishTime: null
        };
        leaderboard.push(newPlayer);
        currentPlayerId = newId;
        currentPlayerName = playerName;
        raceActive = true;
        updateBoatProgress(0);
        saveLeaderboard();
        shakeHintDiv.innerHTML = `🌟 ${escapeHtml(playerName)} 登船!快摇动手机冲线 🌟`;
    }
    
    // 重置所有数据(保留荣耀? 为了趣味完全重置。)
    function resetFullGame() {
        if(confirm('🏮 重置龙舟赛会清空所有玩家记录,是否继续? 🏮')) {
            leaderboard = [];
            currentPlayerId = null;
            currentPlayerName = null;
            raceActive = true;
            localStorage.removeItem('dragonBoat_rank');
            updateBoatProgress(0);
            renderRanking();
            shakeHintDiv.innerHTML = '📱 疯狂摇动手机 → 龙舟向前冲!🎋';
            if(devicemotionHandler) {
                // 保留监听器,避免重新绑定
            }
            // 若当前没玩家,提示加入
            if(!currentPlayerName) {
                showNamePrompt();
            }
        }
    }
    
    // 显示输入昵称层
    function showNamePrompt() {
        const overlay = document.createElement('div');
        overlay.className = 'name-overlay';
        overlay.id = 'nameOverlay';
        overlay.innerHTML = `
            <div class="name-card">
                <h3 style="color:#7A4316;">🐉 端午竞渡 🐉</h3>
                <p style="font-size:14px;">请留下你的大名,摇桨赢龙舟</p>
                <input type="text" id="playerNameInput" placeholder="如: 屈子桨手" maxlength="12">
                <br>
                <button class="confirm-btn" id="confirmNameBtn">启 程 争 渡</button>
            </div>
        `;
        document.body.appendChild(overlay);
        const input = document.getElementById('playerNameInput');
        const confirmBtn = document.getElementById('confirmNameBtn');
        const closeOverlay = (name) => {
            if(name && name.trim()) registerAndStart(name.trim());
            else registerAndStart('龙舟勇士');
            overlay.remove();
            requestMotionPermissionIfNeeded(); // 绑定摇一摇权限
        };
        confirmBtn.onclick = () => { closeOverlay(input.value); };
        input.addEventListener('keypress', (e) => { if(e.key === 'Enter') closeOverlay(input.value); });
        if(!window.DeviceMotionEvent) {
            // fallback
        }
    }
    
    // 摇一摇核心函数 (基于 DeviceMotionEvent)
    function initShakeDetection() {
        if(devicemotionHandler) {
            window.removeEventListener('devicemotion', devicemotionHandler);
        }
        let lastTimestamp = 0;
        const SHAKE_SENSITIVITY = 23; // 摇动强度阈值
        devicemotionHandler = function(event) {
            if(!raceActive) return;
            if(!currentPlayerId) {
                // 没选手时提醒
                if(Date.now() % 4000 < 500) shakeHintDiv.innerHTML = '🥁 先点击「我要参赛」再摇龙舟 🥁';
                return;
            }
            const acc = event.accelerationIncludingGravity;
            if(!acc) return;
            let x = acc.x || 0;
            let y = acc.y || 0;
            let z = acc.z || 0;
            const nowTime = Date.now();
            if(nowTime - lastTimestamp < 120) return; //节流提升性能
            lastTimestamp = nowTime;
            
            // 计算加速度变化强度(差值模长)
            let diff = Math.sqrt(
                Math.pow(x - lastAcceleration.x, 2) +
                Math.pow(y - lastAcceleration.y, 2) +
                Math.pow(z - lastAcceleration.z, 2)
            );
            // 更新上次加速度
            lastAcceleration = { x, y, z };
            if(diff > SHAKE_SENSITIVITY && nowTime - lastShakeTime > shakeCooldown) {
                lastShakeTime = nowTime;
                if(currentPlayerId && raceActive) {
                    addScore(currentPlayerId, SHAKE_COUNT_PER_THRESHOLD);
                    // 增加摇动视觉反馈
                    dragonBoatEl.style.transform = 'scale(1.04)';
                    setTimeout(()=>{ if(dragonBoatEl) dragonBoatEl.style.transform = ''; }, 100);
                    if(currentPlayerId) {
                        const p = leaderboard.find(i=>i.id===currentPlayerId);
                        if(p) shakeHintDiv.innerHTML = `🚣‍♂️ ${escapeHtml(p.name)} 摇桨 +1 ! 总桨数: ${p.score}/${GOAL_SCORE}`;
                    }
                }
            }
        };
        window.addEventListener('devicemotion', devicemotionHandler);
        motionListenerActive = true;
    }
    
    // iOS 13+ 权限请求 (必须由用户手势触发)
    function requestMotionPermissionIfNeeded() {
        if (typeof DeviceMotionEvent !== 'undefined' && typeof DeviceMotionEvent.requestPermission === 'function') {
            // iOS 设备需要权限
            DeviceMotionEvent.requestPermission()
                .then(permissionState => {
                    if (permissionState === 'granted') {
                        initShakeDetection();
                    } else {
                        shakeHintDiv.innerHTML = '⚠️ 未授权运动传感器,无法摇龙舟 ⚠️';
                    }
                })
                .catch(err => {
                    shakeHintDiv.innerHTML = '⚠️ 请点击页面允许传感器权限 ⚠️';
                });
        } else {
            // Android 或者其他直接初始化
            initShakeDetection();
        }
    }
    
    // 结合参赛按钮触发权限请求
    function startWithPermissionAndJoin() {
        if (!currentPlayerName) {
            showNamePrompt();
        } else {
            requestMotionPermissionIfNeeded();
        }
    }
    
    // 绑定界面按钮事件
    joinBtn.onclick = () => {
        showNamePrompt();
    };
    resetBtn.onclick = () => {
        resetFullGame();
    };
    
    // 初始化加载排名并显示注册
    loadLeaderboard();
    // 如果已存在玩家则自动设定当前显示龙舟位置
    if(leaderboard.length > 0) {
        // 可以选取目前榜上第一未完成者作为默认显示
        let unfinished = leaderboard.find(p => p.score < GOAL_SCORE);
        if(unfinished) {
            currentPlayerId = unfinished.id;
            currentPlayerName = unfinished.name;
            updateBoatProgress(unfinished.score);
            raceActive = true;
        } else {
            if(leaderboard[0]) updateBoatProgress(leaderboard[0].score);
        }
    } else {
        updateBoatProgress(0);
    }
    // 尺寸变化防抖刷新龙舟位置
    window.addEventListener('resize', () => {
        if(currentPlayerId){
            const p = leaderboard.find(i=>i.id===currentPlayerId);
            if(p) updateBoatProgress(p.score);
        } else updateBoatProgress(0);
    });
    // 自动请求权限(如果以前已经授权但未触发)
    setTimeout(() => {
        if(currentPlayerName && !motionListenerActive){
            if (typeof DeviceMotionEvent !== 'undefined' && !DeviceMotionEvent.requestPermission) {
                initShakeDetection();
            }
        }
    }, 800);
    
    // 补充说明: 该游戏支持大屏 + 手机用户一同竞技。所有数据基于本地存储,会议主持人可投屏展示排位
</script>
</body>
</html>

Game Source: 摇一摇赛龙舟·端午争渡

Creator: DriftMeteor50

Libraries: none

Complexity: complex (671 lines, 25.0 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-driftmeteor50" to link back to the original. Then publish at arcadelab.ai/publish.