🎮ArcadeLab

iPhone XS — анатомия без экрана

by PrismBolt10
472 lines20.1 KB
▶ Play
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no">
    <title>iPhone XS — анатомия без экрана</title>
    <style>
        * {
            user-select: none;
            -webkit-tap-highlight-color: transparent;
            touch-action: manipulation;
        }

        body {
            background: #1a1e2c;
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            font-family: 'SF Pro Text', 'Segoe UI', system-ui, -apple-system, BlinkMacSystemFont, Roboto, sans-serif;
            padding: 16px;
            margin: 0;
        }

        .container {
            background: rgba(0, 0, 0, 0.4);
            backdrop-filter: blur(3px);
            border-radius: 64px;
            padding: 20px 16px 24px;
            box-shadow: 0 20px 35px rgba(0, 0, 0, 0.5);
        }

        .phone-card {
            background: #10121e;
            border-radius: 52px;
            padding: 20px;
            border: 1px solid #3a405b;
            box-shadow: inset 0 0 0 1px rgba(255,255,255,0.05), 0 12px 28px black;
        }

        .title {
            text-align: center;
            font-size: 1.7rem;
            font-weight: 700;
            letter-spacing: -0.3px;
            background: linear-gradient(135deg, #e0e7ff, #a5b4fc);
            -webkit-background-clip: text;
            background-clip: text;
            color: transparent;
            margin-bottom: 6px;
            display: flex;
            justify-content: center;
            align-items: baseline;
            gap: 12px;
            flex-wrap: wrap;
        }

        .badge {
            font-size: 0.8rem;
            background: #2a2f3f;
            padding: 4px 12px;
            border-radius: 60px;
            font-weight: normal;
            color: #b9c7ff;
        }

        .sub {
            text-align: center;
            font-size: 0.75rem;
            color: #7c83a7;
            margin-bottom: 18px;
            border-bottom: 1px dashed #2f354a;
            display: inline-block;
            width: auto;
            margin-left: auto;
            margin-right: auto;
            padding-bottom: 6px;
        }

        /* рамка телефона */
        .iphone-frame {
            background: #0b0d15;
            border-radius: 36px;
            padding: 12px 10px 10px 10px;
            box-shadow: inset 0 0 0 3px #3e435c, 0 10px 20px rgba(0,0,0,0.4);
            margin: 10px 0 16px;
        }

        canvas {
            display: block;
            width: 100%;
            height: auto;
            background: #07090f;
            border-radius: 28px;
            box-shadow: 0 0 0 2px #2c3146, inset 0 0 0 2px #1e212e;
            cursor: pointer;
            touch-action: manipulation;
        }

        .info-panel {
            display: flex;
            justify-content: space-between;
            align-items: center;
            gap: 12px;
            flex-wrap: wrap;
            background: #141824b3;
            backdrop-filter: blur(12px);
            padding: 10px 18px;
            border-radius: 60px;
            margin: 16px 0 12px;
        }

        .counter {
            background: #00000066;
            padding: 6px 16px;
            border-radius: 40px;
            font-weight: bold;
            font-size: 1.2rem;
            color: #FFD966;
        }

        .counter span {
            font-size: 1.8rem;
            font-weight: 800;
            color: #ffb347;
            margin-right: 6px;
        }

        .reset-btn {
            background: #3a405b;
            border: none;
            padding: 8px 18px;
            border-radius: 40px;
            font-weight: 600;
            color: white;
            font-size: 0.9rem;
            box-shadow: 0 2px 6px black;
            transition: 0.08s linear;
            cursor: pointer;
            font-family: inherit;
            display: inline-flex;
            align-items: center;
            gap: 8px;
        }

        .reset-btn:active {
            transform: scale(0.96);
        }

        .log-area {
            background: #0b0d16cc;
            border-radius: 28px;
            padding: 10px 16px;
            font-size: 0.75rem;
            font-family: monospace;
            color: #bfc9ff;
            max-height: 80px;
            overflow-y: auto;
            border: 1px solid #2c314a;
            margin-top: 8px;
        }

        .log-entry {
            border-bottom: 1px solid #2f354a;
            padding: 5px 0;
            font-size: 0.7rem;
        }

        .footer-tip {
            text-align: center;
            font-size: 0.7rem;
            color: #6f769b;
            margin-top: 14px;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="phone-card">
        <div class="title">
            📱 iPhone XS <span class="badge">Внутренности (экран снят)</span>
        </div>
        <div style="text-align: center">
            <span class="sub">🔬 Тапни по детали — вытащи и удали</span>
        </div>

        <div class="iphone-frame">
            <canvas id="phoneCanvas" width="750" height="1400" style="width:100%; height:auto; max-width:750px; aspect-ratio:750/1400"></canvas>
        </div>

        <div class="info-panel">
            <div class="counter">
                <span id="removedCount">0</span> / <span id="totalCount">0</span> деталей удалено
            </div>
            <button class="reset-btn" id="resetBtn">⟳ ВОССТАНОВИТЬ</button>
        </div>
        <div class="log-area" id="logArea">
            <div class="log-entry">🔧 Нажми на любой компонент — он выпадет и исчезнет.</div>
        </div>
        <div class="footer-tip">
            💡 Реалистичная компоновка iPhone XS: L-батарея, логическая плата, Taptic Engine, двойная камера, разъемы. Экран отсутствует.
        </div>
    </div>
</div>

<script>
    (function(){
        // ---------- ДАННЫЕ О КОМПОНЕНТАХ ----------
        // Все координаты подобраны под canvas 750x1400 (соотношение iPhone XS ~ 750x1334 + служебная зона)
        // Каждый объект: id, название, x, y, width, height, цвет/стиль, активен ли
        const components = [
            { id: "battery_left", name: "🔋 Левый аккумулятор (L-батарея)", x: 45, y: 640, w: 320, h: 180, color: "#3c3a45", stroke: "#a0a5c0", active: true },
            { id: "battery_right", name: "🔋 Правый аккумулятор (нижняя часть)", x: 385, y: 880, w: 320, h: 140, color: "#40404b", stroke: "#a0a5c0", active: true },
            { id: "mainboard", name: "💚 Системная плата (A12 Bionic)", x: 400, y: 560, w: 305, h: 300, color: "#2c5a2e", stroke: "#6fbf6f", active: true },
            { id: "taptic", name: "📳 Taptic Engine (тактильный мотор)", x: 45, y: 1040, w: 310, h: 120, color: "#4a3e5c", stroke: "#b298dc", active: true },
            { id: "speaker_grill", name: "🔊 Нижний динамик / разъем", x: 420, y: 1040, w: 280, h: 100, color: "#2f2a3e", stroke: "#8f8bbf", active: true },
            { id: "wireless_charge", name: "⚡ Беспроводная зарядка (катушка)", x: 280, y: 820, w: 200, h: 200, color: "#5a4c2e", stroke: "#d4b87a", active: true, shape: "circle" },
            { id: "rear_camera1", name: "📷 Широкоугольная камера 12 Мп", x: 540, y: 440, w: 68, h: 68, color: "#1e1f2c", stroke: "#d4af37", active: true, shape: "circle" },
            { id: "rear_camera2", name: "📷 Телеобъектив 12 Мп", x: 625, y: 440, w: 68, h: 68, color: "#1e1f2c", stroke: "#d4af37", active: true, shape: "circle" },
            { id: "front_camera", name: "🤳 Фронтальная камера / Face ID", x: 280, y: 118, w: 72, h: 52, color: "#342e3f", stroke: "#9d90c7", active: true },
            { id: "prox_sensor", name: "🌡️ Датчик приближения / Ambient light", x: 380, y: 122, w: 58, h: 42, color: "#242236", stroke: "#b8a9ff", active: true },
            { id: "lightning_port", name: "🔌 Lightning разъем", x: 330, y: 1200, w: 110, h: 48, color: "#5d5350", stroke: "#cfb284", active: true },
            { id: "antenna_flex", name: "📡 Антенный кабель (нижний)", x: 45, y: 1190, w: 240, h: 35, color: "#4a4540", stroke: "#b3a68f", active: true },
            { id: "power_ic", name: "⚙️ Power Management IC", x: 480, y: 730, w: 65, h: 45, color: "#3f6844", stroke: "#92c47c", active: true },
            { id: "nand_chip", name: "💾 NAND Flash 64/256/512GB", x: 550, y: 680, w: 90, h: 70, color: "#3f6850", stroke: "#79b89a", active: true }
        ];

        // Общее количество компонентов
        const totalCount = components.length;
        let removedIds = [];      // массив удалённых id
        let ctx, canvas;

        // DOM элементы
        const canvasElem = document.getElementById('phoneCanvas');
        const removedSpan = document.getElementById('removedCount');
        const totalSpan = document.getElementById('totalCount');
        const resetBtn = document.getElementById('resetBtn');
        const logDiv = document.getElementById('logArea');

        totalSpan.innerText = totalCount;

        // Функция отрисовки всего (активные компоненты)
        function drawPhone() {
            if (!ctx) return;
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            
            // Фон "плата" — текстура
            ctx.fillStyle = "#10121c";
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            
            // сетка для инженерного вида
            ctx.strokeStyle = "#2a2f3e";
            ctx.lineWidth = 0.5;
            for (let i = 0; i < canvas.width; i += 40) {
                ctx.beginPath();
                ctx.moveTo(i, 0);
                ctx.lineTo(i, canvas.height);
                ctx.stroke();
                ctx.beginPath();
                ctx.moveTo(0, i);
                ctx.lineTo(canvas.width, i);
                ctx.stroke();
            }
            
            // контур самого телефона (рамка)
            ctx.strokeStyle = "#69779c";
            ctx.lineWidth = 4;
            ctx.strokeRect(12, 12, canvas.width-24, canvas.height-24);
            
            // рисуем активные компоненты
            for (let comp of components) {
                if (removedIds.includes(comp.id)) continue; // пропускаем удалённые
                
                ctx.save();
                if (comp.shape === "circle") {
                    ctx.beginPath();
                    ctx.arc(comp.x + comp.w/2, comp.y + comp.h/2, comp.w/2, 0, Math.PI*2);
                    ctx.fillStyle = comp.color;
                    ctx.fill();
                    ctx.strokeStyle = comp.stroke;
                    ctx.lineWidth = 2.5;
                    ctx.stroke();
                    // внутренние блики
                    ctx.beginPath();
                    ctx.arc(comp.x + comp.w/2 - 6, comp.y + comp.h/2 - 6, 6, 0, Math.PI*2);
                    ctx.fillStyle = "#ffffff30";
                    ctx.fill();
                } else {
                    // прямоугольник со скруглением
                    const radius = 12;
                    ctx.beginPath();
                    ctx.moveTo(comp.x + radius, comp.y);
                    ctx.lineTo(comp.x + comp.w - radius, comp.y);
                    ctx.quadraticCurveTo(comp.x + comp.w, comp.y, comp.x + comp.w, comp.y + radius);
                    ctx.lineTo(comp.x + comp.w, comp.y + comp.h - radius);
                    ctx.quadraticCurveTo(comp.x + comp.w, comp.y + comp.h, comp.x + comp.w - radius, comp.y + comp.h);
                    ctx.lineTo(comp.x + radius, comp.y + comp.h);
                    ctx.quadraticCurveTo(comp.x, comp.y + comp.h, comp.x, comp.y + comp.h - radius);
                    ctx.lineTo(comp.x, comp.y + radius);
                    ctx.quadraticCurveTo(comp.x, comp.y, comp.x + radius, comp.y);
                    ctx.closePath();
                    ctx.fillStyle = comp.color;
                    ctx.fill();
                    ctx.strokeStyle = comp.stroke;
                    ctx.lineWidth = 2;
                    ctx.stroke();
                    
                    // маленькие детали на компонентах (микросхемы)
                    ctx.fillStyle = "#e0e4ff30";
                    ctx.fillRect(comp.x + 10, comp.y + 8, comp.w-20, 6);
                }
                
                // Подпись компонента (если позволяет место)
                ctx.font = "bold 14px 'SF Mono', monospace";
                ctx.fillStyle = "#f8f9ff";
                ctx.shadowBlur = 0;
                if (comp.w > 100 && comp.h > 40) {
                    ctx.fillText(comp.name.substring(0, 20), comp.x + 8, comp.y + 22);
                } else {
                    ctx.font = "9px monospace";
                    ctx.fillStyle = "#cbd5ff";
                    ctx.fillText(comp.name.substring(0,12), comp.x + 4, comp.y + 15);
                }
                ctx.restore();
            }
            
            // специальный текст "iPhone XS" и "экран отсутствует"
            ctx.font = "italic 14px 'SF Pro Text'";
            ctx.fillStyle = "#8f96bd";
            ctx.shadowBlur = 0;
            ctx.fillText("⚙️ ДИСПЛЕЙ СНЯТ — внутренний слой", 260, 50);
            ctx.font = "bold 11px monospace";
            ctx.fillStyle = "#aaafff";
            ctx.fillText("iPhone XS LOGIC BOARD REV", 30, 1350);
        }
        
        // Обновление счетчика
        function updateCounterUI() {
            const removedCount = removedIds.length;
            removedSpan.innerText = removedCount;
        }
        
        // Добавить запись в лог (с временем)
        function addLog(componentName, action = "удалена") {
            const logEntry = document.createElement('div');
            logEntry.className = 'log-entry';
            const now = new Date();
            const timeStr = `${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}:${now.getSeconds().toString().padStart(2,'0')}`;
            logEntry.innerHTML = `[${timeStr}] 🧩 ${componentName} — ${action}`;
            logDiv.prepend(logEntry);
            if (logDiv.children.length > 12) {
                logDiv.removeChild(logDiv.lastChild);
            }
            // легкая вибрация, если поддерживается (iOS сработает в webapp, но ограничено)
            if (navigator.vibrate) navigator.vibrate(40);
        }
        
        // Удалить компонент по id (вытащить)
        function removeComponent(componentId) {
            const comp = components.find(c => c.id === componentId);
            if (!comp) return false;
            if (removedIds.includes(componentId)) return false; // уже удалён
            
            removedIds.push(componentId);
            updateCounterUI();
            addLog(comp.name, "🔩 вытащена и удалена");
            drawPhone(); // перерисовываем
            return true;
        }
        
        // Полностью сбросить все удалённые
        function resetAll() {
            if (removedIds.length === 0) {
                addLog("Сброс", "⚠️ все компоненты уже на месте");
                return;
            }
            removedIds = [];
            updateCounterUI();
            addLog("✨ Система", "все детали восстановлены! Снова можно вытаскивать.");
            drawPhone();
            if (navigator.vibrate) navigator.vibrate(100);
        }
        
        // Обработчик нажатия / тапа на canvas — определяем, по какому компоненту кликнули
        function handleCanvasTap(e) {
            // получаем координаты относительно canvas (учитывая масштаб)
            const rect = canvas.getBoundingClientRect();
            const scaleX = canvas.width / rect.width;   // canvas native width / отображаемый css width
            const scaleY = canvas.height / rect.height;
            
            let clientX, clientY;
            if (e.touches) {
                // touch event
                clientX = e.touches[0].clientX;
                clientY = e.touches[0].clientY;
                e.preventDefault();
            } else {
                clientX = e.clientX;
                clientY = e.clientY;
            }
            
            let canvasX = (clientX - rect.left) * scaleX;
            let canvasY = (clientY - rect.top) * scaleY;
            
            if (canvasX < 0 || canvasY < 0 || canvasX > canvas.width || canvasY > canvas.height) return;
            
            // идём с конца (чтоб верхние детали перекрывали? но важен порядок обратный)
            // Для корректного выбора - идём от последних в массиве компонентов (чтобы мелкие сверху не мешали)
            // Но у нас нет иерархии, будем проверять все активные и выбирать тот, чья область содержит точку.
            let hitComponent = null;
            for (let i = components.length-1; i >= 0; i--) {
                const comp = components[i];
                if (removedIds.includes(comp.id)) continue;
                
                let isHit = false;
                if (comp.shape === "circle") {
                    const centerX = comp.x + comp.w/2;
                    const centerY = comp.y + comp.h/2;
                    const radius = comp.w/2;
                    const dx = canvasX - centerX;
                    const dy = canvasY - centerY;
                    if (dx*dx + dy*dy <= radius*radius) isHit = true;
                } else {
                    // прямоугольник
                    if (canvasX >= comp.x && canvasX <= comp.x + comp.w && canvasY >= comp.y && canvasY <= comp.y + comp.h) {
                        isHit = true;
                    }
                }
                if (isHit) {
                    hitComponent = comp;
                    break;
                }
            }
            
            if (hitComponent) {
                removeComponent(hitComponent.id);
            } else {
                addLog("Пустое место", "👻 тут ничего нет (клик по подложке)");
                if (navigator.vibrate) navigator.vibrate(20);
            }
        }
        
        // Инициализация canvas и обработчиков
        function init() {
            canvas = canvasElem;
            canvas.width = 750;
            canvas.height = 1400;
            ctx = canvas.getContext('2d');
            
            drawPhone();
            
            // слушатели для мыши и тач-событий
            canvas.addEventListener('click', (e) => {
                handleCanvasTap(e);
            });
            canvas.addEventListener('touchstart', (e) => {
                handleCanvasTap(e);
                e.preventDefault();
            }, { passive: false });
            
            resetBtn.addEventListener('click', () => {
                resetAll();
            });
            
            updateCounterUI();
        }
        
        init();
    })();
</script>
</body>
</html>

Game Source: iPhone XS — анатомия без экрана

Creator: PrismBolt10

Libraries: none

Complexity: complex (472 lines, 20.1 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: iphone-xs-prismbolt10" to link back to the original. Then publish at arcadelab.ai/publish.