CoolFace
Apppublic

jatnikonm/simulations

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
blockchain.html415 linesDownload Raw Back to root
1<!DOCTYPE html>2<html lang="id">3<head>4    <meta charset="UTF-8">5    <meta name="viewport" content="width=device-width, initial-scale=1.0">6    <title>Simulasi 3D Penyimpanan Arsip Blockchain</title>7    <script src="https://cdn.tailwindcss.com"></script>8    <style>9        /* CSS Dasar */10        body { margin: 0; overflow: hidden; font-family: 'Inter', sans-serif; background-color: #1f2937; }11        #info { /* Info box di tengah atas */12            position: absolute; top: 10px; width: 90%; max-width: 600px; left: 50%;13            transform: translateX(-50%); text-align: center; z-index: 100; color: #e5e7eb;14            padding: 12px; background-color: rgba(55, 65, 81, 0.85); border-radius: 8px;15            box-shadow: 0 2px 4px rgba(0,0,0,0.2); font-size: 0.9rem;16             min-height: 4em; display: flex; align-items: center; justify-content: center;17             pointer-events: none; /* Agar tidak mengganggu hover di bawahnya */18        }19        #buttonContainer { /* Kontainer untuk tombol Start/Reset di bawah */20            position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);21            z-index: 100; display: flex; gap: 1rem;22        }23        .simButton { /* Style umum untuk tombol simulasi */24            padding: 10px 20px; border-radius: 8px; font-weight: 600; color: white;25            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); transition: background-color 0.3s ease; cursor: pointer;26            text-align: center; flex-grow: 1;27        }28        .simButton:disabled { background-color: #6b7280; cursor: not-allowed; opacity: 0.7; }29        #startButton { background-color: #3b82f6; } #startButton:hover:not(:disabled) { background-color: #2563eb; }30        #resetButton { background-color: #ef4444; } #resetButton:hover:not(:disabled) { background-color: #dc2626; }31 32        #logHistoryPanel { /* Panel Log Histori di kiri */33            position: absolute; top: 10px; left: 10px; width: 256px;34            max-height: calc(100vh - 40px); /* Sesuaikan tinggi maks */35            z-index: 50; display: flex; flex-direction: column;36            background-color: rgba(31, 41, 55, 0.85); /* Samakan dengan info box */37            padding: 0.75rem; border-radius: 8px;38            box-shadow: 0 2px 4px rgba(0,0,0,0.2);39             pointer-events: auto; /* Pastikan bisa di-scroll */40        }41        #logHistoryContent { flex-grow: 1; overflow-y: auto; padding-right: 8px; margin-top: 8px; }42        #logHistoryContent::-webkit-scrollbar { width: 6px; } #logHistoryContent::-webkit-scrollbar-track { background: rgba(75, 85, 99, 0.5); border-radius: 3px; } #logHistoryContent::-webkit-scrollbar-thumb { background: #9ca3af; border-radius: 3px; } #logHistoryContent::-webkit-scrollbar-thumb:hover { background: #6b7280; }43        .logEntry { font-size: 0.8rem; padding-bottom: 4px; border-bottom: 1px solid rgba(107, 114, 128, 0.3); margin-bottom: 4px; color: #d1d5db;}44        .logTimestamp { color: #9ca3af; margin-right: 5px; } .logMessage {}45 46        /* Panel Info Blockchain di Kanan */47        #blockchainInfoPanel {48             position: absolute; top: 10px; right: 10px; width: 256px; /* Samakan lebar dgn log */49             max-height: calc(100vh - 40px);50             z-index: 50; display: flex; flex-direction: column;51             background-color: rgba(31, 41, 55, 0.85);52             padding: 0.75rem; border-radius: 8px;53             box-shadow: 0 2px 4px rgba(0,0,0,0.2);54             color: #d1d5db; /* Warna teks default */55             pointer-events: auto; /* Pastikan bisa di-scroll */56        }57         #blockchainInfoPanel h3 { /* Style judul panel */58             font-weight: 600; /* Bold */ font-size: 1rem; /* Base */ margin-bottom: 0.5rem;59             border-bottom: 1px solid #4b5563; /* Gray 600 */ padding-bottom: 0.25rem;60             color: #e5e7eb; /* Gray 200 */ flex-shrink: 0;61         }62         #blockchainInfoContent { /* Konten yg bisa scroll */63             flex-grow: 1; overflow-y: auto; space-y-2; /* Jarak antar paragraf */64             padding-right: 8px; margin-top: 8px; font-size: 0.8rem; /* Ukuran font konten */65         }66         #blockchainInfoContent p strong { color: #9ca3af; } /* Warna label */67         #blockchainInfoContent span { color: #e5e7eb; word-break: break-all; } /* Warna nilai & wrap */68         /* Scrollbar untuk panel kanan */69         #blockchainInfoContent::-webkit-scrollbar { width: 6px; }70         #blockchainInfoContent::-webkit-scrollbar-track { background: rgba(75, 85, 99, 0.5); border-radius: 3px; }71         #blockchainInfoContent::-webkit-scrollbar-thumb { background: #9ca3af; border-radius: 3px; }72         #blockchainInfoContent::-webkit-scrollbar-thumb:hover { background: #6b7280; }73 74 75        canvas { display: block; /* Hapus cursor: pointer; karena hover ditangani JS */ }76    </style>77</head>78<body>79    <div id="mainUI">80        <div id="info">Arahkan kursor ke komponen (kotak berwarna) untuk info, atau mulai simulasi penyimpanan.</div>81        <div id="buttonContainer">82            <button id="startButton" class="simButton">Mulai Simulasi</button>83            <button id="resetButton" class="simButton">Reset Simulasi</button>84        </div>85        <div id="logHistoryPanel">86            <h3 class="font-bold text-base mb-1 border-b border-gray-600 pb-1 text-gray-100 flex-shrink-0">Log Histori</h3>87            <div id="logHistoryContent"></div>88        </div>89 90         <div id="blockchainInfoPanel" class="hidden">91             <h3>Detail Blok Terbaru</h3>92             <div id="blockchainInfoContent">93                 <p><strong>Blok #:</strong> <span id="bcPanel-blockNum">-</span></p>94                 <p><strong>Timestamp:</strong> <span id="bcPanel-timestamp">-</span></p>95                 <p><strong>Tx ID (Sim):</strong> <span id="bcPanel-txId">-</span></p>96                 <p><strong>Metadata (Sim):</strong> <span id="bcPanel-metadata">-</span></p>97                 <p><strong>File Hash (Sim):</strong> <span id="bcPanel-fileHash">-</span></p>98                 <p><strong>IPFS CID (Sim):</strong> <span id="bcPanel-ipfsCid">-</span></p>99             </div>100         </div>101 102    </div> <div id="container"></div>103 104    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>105    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>106    <script src="https://cdnjs.cloudflare.com/ajax/libs/tween.js/18.6.4/tween.umd.js"></script>107 108    <script type="module">109        // === Variabel Global ===110        let scene, camera, renderer, controls;111        let clientNode, appServerNode, ipfsNode;112        let blockchainNodes = [];113        const mainUI = document.getElementById('mainUI');114        const infoElement = document.getElementById('info');115        const startButton = document.getElementById('startButton');116        const resetButton = document.getElementById('resetButton');117        const container = document.getElementById('container');118        const logHistoryContent = document.getElementById('logHistoryContent');119        const blockchainInfoPanel = document.getElementById('blockchainInfoPanel');120        const bcPanelBlockNum = document.getElementById('bcPanel-blockNum');121        const bcPanelTimestamp = document.getElementById('bcPanel-timestamp');122        const bcPanelTxId = document.getElementById('bcPanel-txId');123        const bcPanelMetadata = document.getElementById('bcPanel-metadata');124        const bcPanelFileHash = document.getElementById('bcPanel-fileHash');125        const bcPanelIpfsCid = document.getElementById('bcPanel-ipfsCid');126 127        // Variabel untuk Interaksi Hover128        const raycaster = new THREE.Raycaster();129        const mouse = new THREE.Vector2();130        let clickableObjects = []; // Tetap gunakan nama ini, isinya node yg bisa di-hover131        let currentlyHoveredObject = null; // Lacak objek yg sedang di-hover132        const defaultInfoText = "Arahkan kursor ke komponen (kotak berwarna) untuk info, atau mulai simulasi penyimpanan.";133 134        const blockGeometry = new THREE.BoxGeometry(0.6, 0.6, 0.6);135        const maxVisibleBlocks = 4;136        const blockSpacing = 0.7;137        const nodePositions = { client: new THREE.Vector3(-15, 0, 0), appServer: new THREE.Vector3(0, 5, 0), ipfs: new THREE.Vector3(15, 0, 0), blockchainCenter: new THREE.Vector3(0, -10, 0), blockchainRadius: 10, numBlockchainNodes: 5 };138        const dataColors = { file: 0x00ff00, cid: 0x00ffff, transaction: 0xff00ff, newBlock: 0x3b82f6, request: 0xffaa00, error: 0xff0000 };139        let archiveCount = 0;140 141 142        // === Inisialisasi ===143        function init() {144            scene = new THREE.Scene(); scene.background = new THREE.Color(0x1f2937);145            camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);146            camera.position.z = 35; camera.position.y = 10;147            renderer = new THREE.WebGLRenderer({ antialias: true });148            renderer.setSize(window.innerWidth, window.innerHeight);149            container.appendChild(renderer.domElement);150            const ambientLight = new THREE.AmbientLight(0xffffff, 0.7); scene.add(ambientLight);151            const directionalLight = new THREE.DirectionalLight(0xffffff, 0.9);152            directionalLight.position.set(5, 15, 10); scene.add(directionalLight);153            controls = new THREE.OrbitControls(camera, renderer.domElement);154            controls.enableDamping = true; controls.dampingFactor = 0.05;155            controls.screenSpacePanning = false; controls.minDistance = 5; controls.maxDistance = 100;156            controls.target.set(0, 0, 0);157            createNodesAndSeparators();158            resetLogHistory();159            archiveCount = 0;160            if (blockchainInfoPanel) blockchainInfoPanel.classList.add('hidden');161            window.addEventListener('resize', onWindowResize, false);162            startButton.addEventListener('click', startSimulation);163            resetButton.addEventListener('click', resetSimulation);164            // *** Ganti listener mousedown dengan mousemove ***165            // renderer.domElement.removeEventListener('mousedown', onDocumentMouseDown, false); // Hapus jika ada166            renderer.domElement.addEventListener('mousemove', onDocumentMouseMove, false);167            animate();168        }169 170        // === Pembuatan Objek 3D ===171        function createNodesAndSeparators() {172              const planeYOffset = -1.6; const titleYOffset = 1; const appZonePlaneGeo = new THREE.PlaneGeometry(25, 15); const appZoneMaterial = new THREE.MeshStandardMaterial({ color: 0x2a3a59, side: THREE.DoubleSide, roughness: 1.0 }); const appZonePlane = new THREE.Mesh(appZonePlaneGeo, appZoneMaterial); appZonePlane.rotation.x = -Math.PI / 2; const appZoneCenterX = (nodePositions.client.x + nodePositions.appServer.x) / 2; const appZoneCenterZ = (nodePositions.client.z + nodePositions.appServer.z) / 2; appZonePlane.position.set(appZoneCenterX, planeYOffset + 2, appZoneCenterZ); scene.add(appZonePlane); addZoneTitle("Zona Aplikasi", appZoneCenterX, appZonePlane.position.y + titleYOffset, appZoneCenterZ + 9); const bcZonePlaneGeo = new THREE.PlaneGeometry(nodePositions.blockchainRadius * 2.5, nodePositions.blockchainRadius * 2.5); const bcZoneMaterial = new THREE.MeshStandardMaterial({ color: 0x444444, side: THREE.DoubleSide, roughness: 1.0 }); const bcZonePlane = new THREE.Mesh(bcZonePlaneGeo, bcZoneMaterial); bcZonePlane.rotation.x = -Math.PI / 2; bcZonePlane.position.copy(nodePositions.blockchainCenter).y = nodePositions.blockchainCenter.y + planeYOffset; scene.add(bcZonePlane); addZoneTitle("Jaringan Blockchain", nodePositions.blockchainCenter.x, bcZonePlane.position.y + titleYOffset, nodePositions.blockchainCenter.z + nodePositions.blockchainRadius + 4); const ipfsZonePlaneGeo = new THREE.PlaneGeometry(10, 10); const ipfsZoneMaterial = new THREE.MeshStandardMaterial({ color: 0x4a235a, side: THREE.DoubleSide, roughness: 1.0 }); const ipfsZonePlane = new THREE.Mesh(ipfsZonePlaneGeo, ipfsZoneMaterial); ipfsZonePlane.rotation.x = -Math.PI / 2; ipfsZonePlane.position.copy(nodePositions.ipfs).y = nodePositions.ipfs.y + planeYOffset; scene.add(ipfsZonePlane); addZoneTitle("Penyimpanan Off-Chain", nodePositions.ipfs.x, ipfsZonePlane.position.y + titleYOffset, nodePositions.ipfs.z + 6);173              clickableObjects = [];174              // *** Tambahkan emissive: 0x000000 pada material node ***175              const nodeGeometry = new THREE.BoxGeometry(3, 3, 3);176              const clientMaterial = new THREE.MeshStandardMaterial({ color: 0x4287f5, emissive: 0x000000 });177              const appServerMaterial = new THREE.MeshStandardMaterial({ color: 0xf5a642, emissive: 0x000000 });178              const ipfsMaterial = new THREE.MeshStandardMaterial({ color: 0x9e42f5, emissive: 0x000000 });179              const blockchainMaterialBase = new THREE.MeshStandardMaterial({ color: 0xcccccc, transparent: true, opacity: 0.9, emissive: 0x000000 });180 181              clientNode = new THREE.Mesh(nodeGeometry, clientMaterial); clientNode.position.copy(nodePositions.client); clientNode.userData = { description: "Perangkat Pengguna (Client): Titik interaksi awal pengguna, tempat arsip diunggah atau diakses.", objectType: "node" }; scene.add(clientNode); addLabel(clientNode, "Pengguna"); clickableObjects.push(clientNode);182              appServerNode = new THREE.Mesh(nodeGeometry, appServerMaterial); appServerNode.position.copy(nodePositions.appServer); appServerNode.userData = { description: "Server Aplikasi: Memproses permintaan, mengelola metadata & hash, berinteraksi dengan IPFS dan Blockchain.", objectType: "node" }; scene.add(appServerNode); addLabel(appServerNode, "Server App"); clickableObjects.push(appServerNode);183              ipfsNode = new THREE.Mesh(nodeGeometry, ipfsMaterial); ipfsNode.position.copy(nodePositions.ipfs); ipfsNode.userData = { description: "IPFS (InterPlanetary File System): Sistem penyimpanan terdistribusi untuk menyimpan file arsip besar secara off-chain.", objectType: "node" }; scene.add(ipfsNode); addLabel(ipfsNode, "IPFS"); clickableObjects.push(ipfsNode);184              const angleStep = (Math.PI * 2) / nodePositions.numBlockchainNodes;185              for (let i = 0; i < nodePositions.numBlockchainNodes; i++) {186                  const angle = i * angleStep; const x = nodePositions.blockchainCenter.x + nodePositions.blockchainRadius * Math.cos(angle); const z = nodePositions.blockchainCenter.z + nodePositions.blockchainRadius * Math.sin(angle);187                  // *** Clone material base agar setiap node punya material instance sendiri ***188                  const nodeMaterialInstance = blockchainMaterialBase.clone();189                  const node = new THREE.Mesh(nodeGeometry.clone(), nodeMaterialInstance);190                  node.position.set(x, nodePositions.blockchainCenter.y, z); node.userData = { chainBlocks: [], description: `Node Blockchain ${i + 1}: Menyimpan salinan ledger, memvalidasi transaksi, dan mencapai konsensus.`, objectType: "node" }; scene.add(node); blockchainNodes.push(node); addLabel(node, `Node BC ${i + 1}`); clickableObjects.push(node);191               }192         }193 194        // --- Fungsi Label ---195        function createTextSprite(message, parameters) { /* ... kode sama ... */196              const fontface = parameters.fontface || 'Arial'; const fontsize = parameters.fontsize || 18; const borderThickness = parameters.borderThickness || 4; const borderColor = parameters.borderColor || { r:0, g:0, b:0, a:1.0 }; const backgroundColor = parameters.backgroundColor || { r:255, g:255, b:255, a:1.0 }; const textColor = parameters.textColor || { r:0, g:0, b:0, a:1.0 }; const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); context.font = `Bold ${fontsize}px ${fontface}`; const metrics = context.measureText(message); const textWidth = metrics.width; context.fillStyle = `rgba(${backgroundColor.r}, ${backgroundColor.g}, ${backgroundColor.b}, ${backgroundColor.a})`; context.strokeStyle = `rgba(${borderColor.r}, ${borderColor.g}, ${borderColor.b}, ${borderColor.a})`; context.lineWidth = borderThickness; roundRect(context, borderThickness/2, borderThickness/2, textWidth + borderThickness, fontsize * 1.4 + borderThickness, 6); context.fillStyle = `rgba(${textColor.r}, ${textColor.g}, ${textColor.b}, 1.0)`; context.fillText( message, borderThickness, fontsize + borderThickness); const texture = new THREE.Texture(canvas); texture.needsUpdate = true; const spriteMaterial = new THREE.SpriteMaterial({ map: texture }); const sprite = new THREE.Sprite(spriteMaterial); sprite.scale.set(0.5 * fontsize, 0.25 * fontsize, 0.75 * fontsize); return sprite;197         }198        function roundRect(ctx, x, y, w, h, r) { /* ... kode sama ... */199               ctx.beginPath(); ctx.moveTo(x+r, y); ctx.lineTo(x+w-r, y); ctx.quadraticCurveTo(x+w, y, x+w, y+r); ctx.lineTo(x+w, y+h-r); ctx.quadraticCurveTo(x+w, y+h, x+w-r, y+h); ctx.lineTo(x+r, y+h); ctx.quadraticCurveTo(x, y+h, x, y+h-r); ctx.lineTo(x, y+r); ctx.quadraticCurveTo(x, y, x+r, y); ctx.closePath(); ctx.fill(); ctx.stroke();200         }201         function addLabel(node, text) { /* ... kode sama ... */202              const sprite = createTextSprite(text, { fontsize: 20, fontface: 'Arial', textColor: { r:255, g:255, b:255, a:1.0 }, backgroundColor: { r:0, g:0, b:0, a:0.6 }, borderColor: { r:255, g:255, b:255, a:0.8 } }); sprite.position.set(node.position.x, node.position.y + 2.0, node.position.z); scene.add(sprite);203         }204         function addZoneTitle(text, x, y, z) { /* ... kode sama ... */205              const titleSprite = createTextSprite(text, { fontsize: 28, fontface: 'Arial', textColor: { r:210, g:210, b:210, a:1.0 }, backgroundColor: { r:0, g:0, b:0, a:0.0 }, borderColor: { r:0, g:0, b:0, a:0.0 } }); titleSprite.position.set(x, y + 0.5, z); scene.add(titleSprite);206         }207        // --- Akhir Fungsi Label ---208 209 210        // === Logika Animasi (tidak berubah) ===211        function animateFlowLine(startNode, endNode, duration, color) { /* ... kode sama, return Promise ... */212               return new Promise(resolve => { const positions = new Float32Array(2 * 3); positions[0] = startNode.position.x; positions[1] = startNode.position.y; positions[2] = startNode.position.z; positions[3] = startNode.position.x; positions[4] = startNode.position.y; positions[5] = startNode.position.z; const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); const material = new THREE.LineBasicMaterial({ color: color, linewidth: 3, transparent: true, opacity: 1.0 }); const line = new THREE.Line(geometry, material); scene.add(line); const targetPosition = { x: startNode.position.x, y: startNode.position.y, z: startNode.position.z }; new TWEEN.Tween(targetPosition) .to({ x: endNode.position.x, y: endNode.position.y, z: endNode.position.z }, duration) .easing(TWEEN.Easing.Linear.None) .onUpdate(() => { const currentPositions = line.geometry.attributes.position.array; currentPositions[3] = targetPosition.x; currentPositions[4] = targetPosition.y; currentPositions[5] = targetPosition.z; line.geometry.attributes.position.needsUpdate = true; }) .onComplete(() => { new TWEEN.Tween(line.material) .to({ opacity: 0 }, 200) .easing(TWEEN.Easing.Linear.None) .onComplete(() => { scene.remove(line); resolve(); }) .start(); }) .start(); });213         }214        function flashNode(node, duration = 500, color = 0xffffff) { /* ... kode sama ... */215               if (!node.material || !node.material.color) return; const originalColor = node.material.color.getHex(); new TWEEN.Tween(node.material.color) .to({ r: new THREE.Color(color).r, g: new THREE.Color(color).g, b: new THREE.Color(color).b }, duration / 2) .easing(TWEEN.Easing.Quadratic.Out) .yoyo(true).repeat(1) .onComplete(() => { node.material.color.setHex(originalColor); }) .start();216         }217        function addBlockToNodeChain(node, blockColor) { /* ... kode sama, return Promise ... */218               return new Promise(resolve => { if (!node || !node.geometry || !node.userData || !Array.isArray(node.userData.chainBlocks)) { console.error("Invalid node or node data for addBlockToNodeChain:", node); resolve(); return; } if (!node.geometry.boundingBox) { node.geometry.computeBoundingBox(); } if (!blockGeometry.boundingBox) { blockGeometry.computeBoundingBox(); } if (!node.geometry.boundingBox || !blockGeometry.boundingBox) { console.error("Failed to compute bounding box for node or block geometry."); resolve(); return; } const nodeSize = new THREE.Vector3(); node.geometry.boundingBox.getSize(nodeSize); const nodeHeight = nodeSize.y; const blockSize = new THREE.Vector3(); blockGeometry.boundingBox.getSize(blockSize); const blockHeight = blockSize.y; const newBlockMaterial = new THREE.MeshStandardMaterial({ color: blockColor }); const newBlock = new THREE.Mesh(blockGeometry.clone(), newBlockMaterial); const chain = node.userData.chainBlocks; const baseY = node.position.y - (nodeHeight / 2) + (blockHeight / 2); const targetY = baseY + chain.length * blockSpacing; newBlock.position.set(node.position.x, targetY + 3, node.position.z); scene.add(newBlock); chain.push(newBlock); new TWEEN.Tween(newBlock.position) .to({ y: targetY }, 800) .easing(TWEEN.Easing.Bounce.Out) .onComplete(() => { let repositionPromises = []; if (chain.length > maxVisibleBlocks) { const oldestBlock = chain.shift(); if (oldestBlock) { scene.remove(oldestBlock); } chain.forEach((currentBlock, i) => { const currentBaseY = node.position.y - (nodeHeight / 2) + (blockHeight / 2); const newTargetY = currentBaseY + i * blockSpacing; const p = new Promise(res => { new TWEEN.Tween(currentBlock.position) .to({ y: newTargetY }, 300) .easing(TWEEN.Easing.Quadratic.Out) .onComplete(res) .start(); }); repositionPromises.push(p); }); } Promise.all(repositionPromises).then(resolve); }) .start(); });219         }220        // --- Akhir Logika Animasi ---221 222 223        // === Logika Log Histori (tidak berubah) ===224        function addLogEntry(message) { /* ... kode sama ... */225              if (!logHistoryContent) return; const timestamp = new Date().toLocaleTimeString('id-ID', { hour12: false }); const logElement = document.createElement('p'); logElement.classList.add('logEntry'); logElement.innerHTML = `<span class="logTimestamp">[${timestamp}]</span> <span class="logMessage">${message}</span>`; logHistoryContent.prepend(logElement); const maxLogEntries = 50; while (logHistoryContent.children.length > maxLogEntries) { logHistoryContent.removeChild(logHistoryContent.lastChild); }226         }227        function resetLogHistory() { /* ... kode sama ... */228              if (logHistoryContent) { logHistoryContent.innerHTML = ''; addLogEntry("Log histori dimulai."); }229         }230        // --- Akhir Logika Log Histori ---231 232        // === Fungsi Generate Mock Data Blockchain ===233        function generateMockData() { /* ... kode sama ... */234             const txId = '0x' + [...Array(16)].map(() => Math.floor(Math.random() * 16).toString(16)).join(''); const fileHash = 'sha256-' + [...Array(32)].map(() => Math.floor(Math.random() * 16).toString(16)).join(''); const ipfsCid = 'Qm' + [...Array(44)].map(() => (Math.random() < 0.5 ? String.fromCharCode(Math.floor(Math.random() * 26) + 97) : Math.floor(Math.random() * 10))).join(''); return { txId, fileHash, ipfsCid };235         }236 237        // === Fungsi Update Panel Info Blockchain ===238        function updateBlockchainInfoPanel(blockNumber, mockData) { /* ... kode sama ... */239             if (!bcPanelBlockNum || !bcPanelTimestamp || !bcPanelTxId || !bcPanelMetadata || !bcPanelFileHash || !bcPanelIpfsCid) { console.error("One or more blockchain info panel elements not found!"); return; } console.log(`Updating blockchain info panel for Block #${blockNumber} with data:`, mockData); bcPanelBlockNum.innerText = blockNumber; bcPanelTimestamp.innerText = new Date().toLocaleString('id-ID', { dateStyle: 'medium', timeStyle: 'medium'}); bcPanelTxId.innerText = mockData.txId; bcPanelMetadata.innerText = `Data simulasi untuk arsip di blok #${blockNumber}.`; bcPanelFileHash.innerText = mockData.fileHash; bcPanelIpfsCid.innerText = mockData.ipfsCid;240         }241 242        // === Fungsi Increment Archive Count ===243        function incrementArchiveCount() { /* ... kode sama ... */244             archiveCount++; console.log("Archive count incremented to:", archiveCount);245         }246 247 248        // === Fungsi Reset State Visual ===249        function resetVisualState() {250               blockchainNodes.forEach(node => {251                   if(node.material.color) node.material.color.setHex(0xcccccc);252                   // *** Reset warna emissive juga ***253                   if(node.material.emissive) node.material.emissive.setHex(0x000000);254                   if(node.userData && node.userData.chainBlocks){ node.userData.chainBlocks.forEach(block => scene.remove(block)); node.userData.chainBlocks = []; }255                });256                // Reset node lain juga257                if(clientNode && clientNode.material.emissive) clientNode.material.emissive.setHex(0x000000);258                if(appServerNode && appServerNode.material.emissive) appServerNode.material.emissive.setHex(0x000000);259                if(ipfsNode && ipfsNode.material.emissive) ipfsNode.material.emissive.setHex(0x000000);260 261               const linesToRemove = []; scene.traverse((object) => { if (object.isLine) { linesToRemove.push(object); } }); linesToRemove.forEach(line => scene.remove(line));262               infoElement.innerText = defaultInfoText; // Gunakan teks default263               resetLogHistory();264               if (blockchainInfoPanel) blockchainInfoPanel.classList.add('hidden');265               currentlyHoveredObject = null; // Reset objek yang di-hover266         }267 268        // === Fungsi Reset Simulasi ===269        function resetSimulation() { /* ... kode sama ... */270                console.log("Resetting simulation..."); TWEEN.removeAll();271                resetVisualState();272                archiveCount = 0;273                console.log("Archive count reset to:", archiveCount);274                startButton.disabled = false; resetButton.disabled = false;275                addLogEntry("Simulasi direset.");276         }277 278        // === Alur Simulasi Penyimpanan ===279        async function startSimulation() {280            startButton.disabled = true; resetButton.disabled = true;281            if (blockchainInfoPanel) blockchainInfoPanel.classList.add('hidden');282            resetVisualState();283            updateInfo("Memulai simulasi penyimpanan...");284            addLogEntry("Memulai simulasi penyimpanan...");285 286            try { // Alur utama tidak berubah287                await delay(500);288                updateInfo("1. Pengguna memulai proses upload arsip..."); addLogEntry("Pengguna memulai upload arsip.");289                await delay(1500);290                updateInfo("2. Mengirim file arsip ke Server Aplikasi..."); addLogEntry("Mengirim file ke Server Aplikasi.");291                await animateFlowLine(clientNode, appServerNode, 2000, dataColors.file);292                flashNode(appServerNode);293                updateInfo("3. Server memproses: Ekstrak metadata, hitung hash..."); addLogEntry("Server memproses file (metadata, hash).");294                await delay(1500);295                updateInfo("4. Server mengirim file ke IPFS..."); addLogEntry("Server mengirim file ke IPFS.");296                await animateFlowLine(appServerNode, ipfsNode, 2000, dataColors.file);297                flashNode(ipfsNode);298                updateInfo("5. IPFS menyimpan file..."); addLogEntry("IPFS menyimpan file.");299                await delay(1000);300                updateInfo("6. Server menerima CID dari IPFS..."); addLogEntry("Server menerima CID dari IPFS.");301                await animateFlowLine(ipfsNode, appServerNode, 1000, dataColors.cid);302                flashNode(appServerNode);303                updateInfo("7. Server membuat transaksi..."); addLogEntry("Server membuat transaksi blockchain.");304                await delay(1500);305                const targetBCNode = blockchainNodes[0];306                updateInfo("8. Mengirim transaksi ke Node BC 1..."); addLogEntry("Mengirim transaksi ke Node BC 1.");307                await animateFlowLine(appServerNode, targetBCNode, 2000, dataColors.transaction);308                flashNode(targetBCNode, 500, dataColors.transaction);309                updateInfo("9. Node 1 menyebarkan transaksi..."); addLogEntry("Node BC 1 melakukan broadcast transaksi.");310                await delay(1000);311                const broadcastPromises = blockchainNodes.slice(1).map(node =>312                    animateFlowLine(targetBCNode, node, 1500, dataColors.transaction).then(() => {313                        flashNode(node, 500, dataColors.transaction);314                    })315                );316                await Promise.all(broadcastPromises);317                addLogEntry("Broadcast transaksi selesai."); await delay(500);318                updateInfo("10. Node melakukan proses konsensus..."); addLogEntry("Node BC memulai proses konsensus.");319                const consensusPromises = blockchainNodes.map(node => {320                    return new Promise(resolve => { flashNode(node, 1000, 0x00ff00); setTimeout(resolve, 1100); });321                });322                await Promise.all(consensusPromises);323                addLogEntry("Konsensus tercapai.");324                updateInfo("11. Transaksi dicatat, blok baru ditambahkan..."); addLogEntry("Mencatat transaksi & menambahkan blok baru.");325                const blockAddingPromises = blockchainNodes.map(node => addBlockToNodeChain(node, dataColors.newBlock));326                console.log("Waiting for all blocks to be added...");327                await Promise.all(blockAddingPromises);328                console.log("All blocks added visually. Proceeding to show panel.");329                addLogEntry("Blok baru ditambahkan ke semua node."); await delay(1000);330 331                // --- Sukses ---332                updateInfo("12. Sukses! Arsip tersimpan aman, data tercatat di blockchain.");333                addLogEntry("Penyimpanan arsip berhasil.");334                incrementArchiveCount();335                console.log("Generating mock data and updating panel...");336                const mockData = generateMockData();337                updateBlockchainInfoPanel(archiveCount, mockData);338                console.log("Attempting to show blockchain info panel. Classes before:", blockchainInfoPanel ? blockchainInfoPanel.className : 'null');339                if (blockchainInfoPanel) blockchainInfoPanel.classList.remove('hidden');340                console.log("Attempting to show blockchain info panel. Classes after:", blockchainInfoPanel ? blockchainInfoPanel.className : 'null');341                console.log("Panel offsetHeight:", blockchainInfoPanel ? blockchainInfoPanel.offsetHeight : 'null');342                startButton.disabled = false;343                resetButton.disabled = false;344 345            } catch (error) {346                console.error("Error during startSimulation:", error);347                updateInfo("Terjadi error selama simulasi penyimpanan.");348                addLogEntry(`Error: ${error.message}`);349                resetButton.disabled = false;350            }351        }352 353         // === Alur Simulasi Penelusuran (Dihapus) ===354         // === Fungsi Simulasi Perubahan Format (Dihapus) ===355 356        // === Fungsi Utilitas ===357        function updateInfo(message) { console.log(message); infoElement.innerText = message; }358        function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }359 360        // === Fungsi Baru: Penanganan Hover Mouse ===361        function onDocumentMouseMove(event) {362            event.preventDefault();363            mouse.x = (event.clientX / window.innerWidth) * 2 - 1;364            mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;365            raycaster.setFromCamera(mouse, camera);366            const intersects = raycaster.intersectObjects(clickableObjects);367 368            if (intersects.length > 0) {369                const intersectedObject = intersects[0].object;370                // Cek jika objek berbeda dari yg dihover sebelumnya371                if (currentlyHoveredObject !== intersectedObject) {372                    // Reset objek yg dihover sebelumnya (jika ada)373                    if (currentlyHoveredObject && currentlyHoveredObject.material.emissive) {374                        currentlyHoveredObject.material.emissive.setHex(0x000000);375                    }376                    // Set objek baru yg dihover377                    currentlyHoveredObject = intersectedObject;378                    if (currentlyHoveredObject.material.emissive) {379                         currentlyHoveredObject.material.emissive.setHex(0x555555); // Warna hover380                    }381                    // Tampilkan deskripsi382                    if (currentlyHoveredObject.userData && currentlyHoveredObject.userData.description) {383                         infoElement.innerText = currentlyHoveredObject.userData.description;384                    }385                }386                 // Jika sama, tidak perlu lakukan apa2387            } else {388                // Jika tidak ada objek yg dihover389                if (currentlyHoveredObject) {390                    // Reset objek yg sebelumnya dihover391                     if (currentlyHoveredObject.material.emissive) {392                        currentlyHoveredObject.material.emissive.setHex(0x000000);393                    }394                    currentlyHoveredObject = null;395                    // Kembalikan info box ke default396                    infoElement.innerText = defaultInfoText;397                }398            }399        }400 401        // === Fungsi Penanganan Klik Mouse (Dihapus) ===402        // function onDocumentMouseDown(event) { ... }403 404 405        // === Loop Animasi & Penanganan Ukuran Jendela ===406        function animate() { requestAnimationFrame(animate); TWEEN.update(); controls.update(); renderer.render(scene, camera); }407        function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }408 409        // === Mulai Aplikasi ===410        init();411 412    </script>413</body>414</html>415