jatnikonm/simulations
0
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 