simulations / blockchain.html
jnm-itb
Implement structural updates and optimizations across multiple modules
e07e0a7
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simulasi 3D Penyimpanan Arsip Blockchain</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* CSS Dasar */
body { margin: 0; overflow: hidden; font-family: 'Inter', sans-serif; background-color: #1f2937; }
#info { /* Info box di tengah atas */
position: absolute; top: 10px; width: 90%; max-width: 600px; left: 50%;
transform: translateX(-50%); text-align: center; z-index: 100; color: #e5e7eb;
padding: 12px; background-color: rgba(55, 65, 81, 0.85); border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.2); font-size: 0.9rem;
min-height: 4em; display: flex; align-items: center; justify-content: center;
pointer-events: none; /* Agar tidak mengganggu hover di bawahnya */
}
#buttonContainer { /* Kontainer untuk tombol Start/Reset di bawah */
position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
z-index: 100; display: flex; gap: 1rem;
}
.simButton { /* Style umum untuk tombol simulasi */
padding: 10px 20px; border-radius: 8px; font-weight: 600; color: white;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); transition: background-color 0.3s ease; cursor: pointer;
text-align: center; flex-grow: 1;
}
.simButton:disabled { background-color: #6b7280; cursor: not-allowed; opacity: 0.7; }
#startButton { background-color: #3b82f6; } #startButton:hover:not(:disabled) { background-color: #2563eb; }
#resetButton { background-color: #ef4444; } #resetButton:hover:not(:disabled) { background-color: #dc2626; }
#logHistoryPanel { /* Panel Log Histori di kiri */
position: absolute; top: 10px; left: 10px; width: 256px;
max-height: calc(100vh - 40px); /* Sesuaikan tinggi maks */
z-index: 50; display: flex; flex-direction: column;
background-color: rgba(31, 41, 55, 0.85); /* Samakan dengan info box */
padding: 0.75rem; border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
pointer-events: auto; /* Pastikan bisa di-scroll */
}
#logHistoryContent { flex-grow: 1; overflow-y: auto; padding-right: 8px; margin-top: 8px; }
#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; }
.logEntry { font-size: 0.8rem; padding-bottom: 4px; border-bottom: 1px solid rgba(107, 114, 128, 0.3); margin-bottom: 4px; color: #d1d5db;}
.logTimestamp { color: #9ca3af; margin-right: 5px; } .logMessage {}
/* Panel Info Blockchain di Kanan */
#blockchainInfoPanel {
position: absolute; top: 10px; right: 10px; width: 256px; /* Samakan lebar dgn log */
max-height: calc(100vh - 40px);
z-index: 50; display: flex; flex-direction: column;
background-color: rgba(31, 41, 55, 0.85);
padding: 0.75rem; border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
color: #d1d5db; /* Warna teks default */
pointer-events: auto; /* Pastikan bisa di-scroll */
}
#blockchainInfoPanel h3 { /* Style judul panel */
font-weight: 600; /* Bold */ font-size: 1rem; /* Base */ margin-bottom: 0.5rem;
border-bottom: 1px solid #4b5563; /* Gray 600 */ padding-bottom: 0.25rem;
color: #e5e7eb; /* Gray 200 */ flex-shrink: 0;
}
#blockchainInfoContent { /* Konten yg bisa scroll */
flex-grow: 1; overflow-y: auto; space-y-2; /* Jarak antar paragraf */
padding-right: 8px; margin-top: 8px; font-size: 0.8rem; /* Ukuran font konten */
}
#blockchainInfoContent p strong { color: #9ca3af; } /* Warna label */
#blockchainInfoContent span { color: #e5e7eb; word-break: break-all; } /* Warna nilai & wrap */
/* Scrollbar untuk panel kanan */
#blockchainInfoContent::-webkit-scrollbar { width: 6px; }
#blockchainInfoContent::-webkit-scrollbar-track { background: rgba(75, 85, 99, 0.5); border-radius: 3px; }
#blockchainInfoContent::-webkit-scrollbar-thumb { background: #9ca3af; border-radius: 3px; }
#blockchainInfoContent::-webkit-scrollbar-thumb:hover { background: #6b7280; }
canvas { display: block; /* Hapus cursor: pointer; karena hover ditangani JS */ }
</style>
</head>
<body>
<div id="mainUI">
<div id="info">Arahkan kursor ke komponen (kotak berwarna) untuk info, atau mulai simulasi penyimpanan.</div>
<div id="buttonContainer">
<button id="startButton" class="simButton">Mulai Simulasi</button>
<button id="resetButton" class="simButton">Reset Simulasi</button>
</div>
<div id="logHistoryPanel">
<h3 class="font-bold text-base mb-1 border-b border-gray-600 pb-1 text-gray-100 flex-shrink-0">Log Histori</h3>
<div id="logHistoryContent"></div>
</div>
<div id="blockchainInfoPanel" class="hidden">
<h3>Detail Blok Terbaru</h3>
<div id="blockchainInfoContent">
<p><strong>Blok #:</strong> <span id="bcPanel-blockNum">-</span></p>
<p><strong>Timestamp:</strong> <span id="bcPanel-timestamp">-</span></p>
<p><strong>Tx ID (Sim):</strong> <span id="bcPanel-txId">-</span></p>
<p><strong>Metadata (Sim):</strong> <span id="bcPanel-metadata">-</span></p>
<p><strong>File Hash (Sim):</strong> <span id="bcPanel-fileHash">-</span></p>
<p><strong>IPFS CID (Sim):</strong> <span id="bcPanel-ipfsCid">-</span></p>
</div>
</div>
</div> <div id="container"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tween.js/18.6.4/tween.umd.js"></script>
<script type="module">
// === Variabel Global ===
let scene, camera, renderer, controls;
let clientNode, appServerNode, ipfsNode;
let blockchainNodes = [];
const mainUI = document.getElementById('mainUI');
const infoElement = document.getElementById('info');
const startButton = document.getElementById('startButton');
const resetButton = document.getElementById('resetButton');
const container = document.getElementById('container');
const logHistoryContent = document.getElementById('logHistoryContent');
const blockchainInfoPanel = document.getElementById('blockchainInfoPanel');
const bcPanelBlockNum = document.getElementById('bcPanel-blockNum');
const bcPanelTimestamp = document.getElementById('bcPanel-timestamp');
const bcPanelTxId = document.getElementById('bcPanel-txId');
const bcPanelMetadata = document.getElementById('bcPanel-metadata');
const bcPanelFileHash = document.getElementById('bcPanel-fileHash');
const bcPanelIpfsCid = document.getElementById('bcPanel-ipfsCid');
// Variabel untuk Interaksi Hover
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let clickableObjects = []; // Tetap gunakan nama ini, isinya node yg bisa di-hover
let currentlyHoveredObject = null; // Lacak objek yg sedang di-hover
const defaultInfoText = "Arahkan kursor ke komponen (kotak berwarna) untuk info, atau mulai simulasi penyimpanan.";
const blockGeometry = new THREE.BoxGeometry(0.6, 0.6, 0.6);
const maxVisibleBlocks = 4;
const blockSpacing = 0.7;
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 };
const dataColors = { file: 0x00ff00, cid: 0x00ffff, transaction: 0xff00ff, newBlock: 0x3b82f6, request: 0xffaa00, error: 0xff0000 };
let archiveCount = 0;
// === Inisialisasi ===
function init() {
scene = new THREE.Scene(); scene.background = new THREE.Color(0x1f2937);
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 35; camera.position.y = 10;
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7); scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.9);
directionalLight.position.set(5, 15, 10); scene.add(directionalLight);
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; controls.dampingFactor = 0.05;
controls.screenSpacePanning = false; controls.minDistance = 5; controls.maxDistance = 100;
controls.target.set(0, 0, 0);
createNodesAndSeparators();
resetLogHistory();
archiveCount = 0;
if (blockchainInfoPanel) blockchainInfoPanel.classList.add('hidden');
window.addEventListener('resize', onWindowResize, false);
startButton.addEventListener('click', startSimulation);
resetButton.addEventListener('click', resetSimulation);
// *** Ganti listener mousedown dengan mousemove ***
// renderer.domElement.removeEventListener('mousedown', onDocumentMouseDown, false); // Hapus jika ada
renderer.domElement.addEventListener('mousemove', onDocumentMouseMove, false);
animate();
}
// === Pembuatan Objek 3D ===
function createNodesAndSeparators() {
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);
clickableObjects = [];
// *** Tambahkan emissive: 0x000000 pada material node ***
const nodeGeometry = new THREE.BoxGeometry(3, 3, 3);
const clientMaterial = new THREE.MeshStandardMaterial({ color: 0x4287f5, emissive: 0x000000 });
const appServerMaterial = new THREE.MeshStandardMaterial({ color: 0xf5a642, emissive: 0x000000 });
const ipfsMaterial = new THREE.MeshStandardMaterial({ color: 0x9e42f5, emissive: 0x000000 });
const blockchainMaterialBase = new THREE.MeshStandardMaterial({ color: 0xcccccc, transparent: true, opacity: 0.9, emissive: 0x000000 });
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);
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);
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);
const angleStep = (Math.PI * 2) / nodePositions.numBlockchainNodes;
for (let i = 0; i < nodePositions.numBlockchainNodes; i++) {
const angle = i * angleStep; const x = nodePositions.blockchainCenter.x + nodePositions.blockchainRadius * Math.cos(angle); const z = nodePositions.blockchainCenter.z + nodePositions.blockchainRadius * Math.sin(angle);
// *** Clone material base agar setiap node punya material instance sendiri ***
const nodeMaterialInstance = blockchainMaterialBase.clone();
const node = new THREE.Mesh(nodeGeometry.clone(), nodeMaterialInstance);
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);
}
}
// --- Fungsi Label ---
function createTextSprite(message, parameters) { /* ... kode sama ... */
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;
}
function roundRect(ctx, x, y, w, h, r) { /* ... kode sama ... */
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();
}
function addLabel(node, text) { /* ... kode sama ... */
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);
}
function addZoneTitle(text, x, y, z) { /* ... kode sama ... */
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);
}
// --- Akhir Fungsi Label ---
// === Logika Animasi (tidak berubah) ===
function animateFlowLine(startNode, endNode, duration, color) { /* ... kode sama, return Promise ... */
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(); });
}
function flashNode(node, duration = 500, color = 0xffffff) { /* ... kode sama ... */
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();
}
function addBlockToNodeChain(node, blockColor) { /* ... kode sama, return Promise ... */
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(); });
}
// --- Akhir Logika Animasi ---
// === Logika Log Histori (tidak berubah) ===
function addLogEntry(message) { /* ... kode sama ... */
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); }
}
function resetLogHistory() { /* ... kode sama ... */
if (logHistoryContent) { logHistoryContent.innerHTML = ''; addLogEntry("Log histori dimulai."); }
}
// --- Akhir Logika Log Histori ---
// === Fungsi Generate Mock Data Blockchain ===
function generateMockData() { /* ... kode sama ... */
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 };
}
// === Fungsi Update Panel Info Blockchain ===
function updateBlockchainInfoPanel(blockNumber, mockData) { /* ... kode sama ... */
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;
}
// === Fungsi Increment Archive Count ===
function incrementArchiveCount() { /* ... kode sama ... */
archiveCount++; console.log("Archive count incremented to:", archiveCount);
}
// === Fungsi Reset State Visual ===
function resetVisualState() {
blockchainNodes.forEach(node => {
if(node.material.color) node.material.color.setHex(0xcccccc);
// *** Reset warna emissive juga ***
if(node.material.emissive) node.material.emissive.setHex(0x000000);
if(node.userData && node.userData.chainBlocks){ node.userData.chainBlocks.forEach(block => scene.remove(block)); node.userData.chainBlocks = []; }
});
// Reset node lain juga
if(clientNode && clientNode.material.emissive) clientNode.material.emissive.setHex(0x000000);
if(appServerNode && appServerNode.material.emissive) appServerNode.material.emissive.setHex(0x000000);
if(ipfsNode && ipfsNode.material.emissive) ipfsNode.material.emissive.setHex(0x000000);
const linesToRemove = []; scene.traverse((object) => { if (object.isLine) { linesToRemove.push(object); } }); linesToRemove.forEach(line => scene.remove(line));
infoElement.innerText = defaultInfoText; // Gunakan teks default
resetLogHistory();
if (blockchainInfoPanel) blockchainInfoPanel.classList.add('hidden');
currentlyHoveredObject = null; // Reset objek yang di-hover
}
// === Fungsi Reset Simulasi ===
function resetSimulation() { /* ... kode sama ... */
console.log("Resetting simulation..."); TWEEN.removeAll();
resetVisualState();
archiveCount = 0;
console.log("Archive count reset to:", archiveCount);
startButton.disabled = false; resetButton.disabled = false;
addLogEntry("Simulasi direset.");
}
// === Alur Simulasi Penyimpanan ===
async function startSimulation() {
startButton.disabled = true; resetButton.disabled = true;
if (blockchainInfoPanel) blockchainInfoPanel.classList.add('hidden');
resetVisualState();
updateInfo("Memulai simulasi penyimpanan...");
addLogEntry("Memulai simulasi penyimpanan...");
try { // Alur utama tidak berubah
await delay(500);
updateInfo("1. Pengguna memulai proses upload arsip..."); addLogEntry("Pengguna memulai upload arsip.");
await delay(1500);
updateInfo("2. Mengirim file arsip ke Server Aplikasi..."); addLogEntry("Mengirim file ke Server Aplikasi.");
await animateFlowLine(clientNode, appServerNode, 2000, dataColors.file);
flashNode(appServerNode);
updateInfo("3. Server memproses: Ekstrak metadata, hitung hash..."); addLogEntry("Server memproses file (metadata, hash).");
await delay(1500);
updateInfo("4. Server mengirim file ke IPFS..."); addLogEntry("Server mengirim file ke IPFS.");
await animateFlowLine(appServerNode, ipfsNode, 2000, dataColors.file);
flashNode(ipfsNode);
updateInfo("5. IPFS menyimpan file..."); addLogEntry("IPFS menyimpan file.");
await delay(1000);
updateInfo("6. Server menerima CID dari IPFS..."); addLogEntry("Server menerima CID dari IPFS.");
await animateFlowLine(ipfsNode, appServerNode, 1000, dataColors.cid);
flashNode(appServerNode);
updateInfo("7. Server membuat transaksi..."); addLogEntry("Server membuat transaksi blockchain.");
await delay(1500);
const targetBCNode = blockchainNodes[0];
updateInfo("8. Mengirim transaksi ke Node BC 1..."); addLogEntry("Mengirim transaksi ke Node BC 1.");
await animateFlowLine(appServerNode, targetBCNode, 2000, dataColors.transaction);
flashNode(targetBCNode, 500, dataColors.transaction);
updateInfo("9. Node 1 menyebarkan transaksi..."); addLogEntry("Node BC 1 melakukan broadcast transaksi.");
await delay(1000);
const broadcastPromises = blockchainNodes.slice(1).map(node =>
animateFlowLine(targetBCNode, node, 1500, dataColors.transaction).then(() => {
flashNode(node, 500, dataColors.transaction);
})
);
await Promise.all(broadcastPromises);
addLogEntry("Broadcast transaksi selesai."); await delay(500);
updateInfo("10. Node melakukan proses konsensus..."); addLogEntry("Node BC memulai proses konsensus.");
const consensusPromises = blockchainNodes.map(node => {
return new Promise(resolve => { flashNode(node, 1000, 0x00ff00); setTimeout(resolve, 1100); });
});
await Promise.all(consensusPromises);
addLogEntry("Konsensus tercapai.");
updateInfo("11. Transaksi dicatat, blok baru ditambahkan..."); addLogEntry("Mencatat transaksi & menambahkan blok baru.");
const blockAddingPromises = blockchainNodes.map(node => addBlockToNodeChain(node, dataColors.newBlock));
console.log("Waiting for all blocks to be added...");
await Promise.all(blockAddingPromises);
console.log("All blocks added visually. Proceeding to show panel.");
addLogEntry("Blok baru ditambahkan ke semua node."); await delay(1000);
// --- Sukses ---
updateInfo("12. Sukses! Arsip tersimpan aman, data tercatat di blockchain.");
addLogEntry("Penyimpanan arsip berhasil.");
incrementArchiveCount();
console.log("Generating mock data and updating panel...");
const mockData = generateMockData();
updateBlockchainInfoPanel(archiveCount, mockData);
console.log("Attempting to show blockchain info panel. Classes before:", blockchainInfoPanel ? blockchainInfoPanel.className : 'null');
if (blockchainInfoPanel) blockchainInfoPanel.classList.remove('hidden');
console.log("Attempting to show blockchain info panel. Classes after:", blockchainInfoPanel ? blockchainInfoPanel.className : 'null');
console.log("Panel offsetHeight:", blockchainInfoPanel ? blockchainInfoPanel.offsetHeight : 'null');
startButton.disabled = false;
resetButton.disabled = false;
} catch (error) {
console.error("Error during startSimulation:", error);
updateInfo("Terjadi error selama simulasi penyimpanan.");
addLogEntry(`Error: ${error.message}`);
resetButton.disabled = false;
}
}
// === Alur Simulasi Penelusuran (Dihapus) ===
// === Fungsi Simulasi Perubahan Format (Dihapus) ===
// === Fungsi Utilitas ===
function updateInfo(message) { console.log(message); infoElement.innerText = message; }
function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
// === Fungsi Baru: Penanganan Hover Mouse ===
function onDocumentMouseMove(event) {
event.preventDefault();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(clickableObjects);
if (intersects.length > 0) {
const intersectedObject = intersects[0].object;
// Cek jika objek berbeda dari yg dihover sebelumnya
if (currentlyHoveredObject !== intersectedObject) {
// Reset objek yg dihover sebelumnya (jika ada)
if (currentlyHoveredObject && currentlyHoveredObject.material.emissive) {
currentlyHoveredObject.material.emissive.setHex(0x000000);
}
// Set objek baru yg dihover
currentlyHoveredObject = intersectedObject;
if (currentlyHoveredObject.material.emissive) {
currentlyHoveredObject.material.emissive.setHex(0x555555); // Warna hover
}
// Tampilkan deskripsi
if (currentlyHoveredObject.userData && currentlyHoveredObject.userData.description) {
infoElement.innerText = currentlyHoveredObject.userData.description;
}
}
// Jika sama, tidak perlu lakukan apa2
} else {
// Jika tidak ada objek yg dihover
if (currentlyHoveredObject) {
// Reset objek yg sebelumnya dihover
if (currentlyHoveredObject.material.emissive) {
currentlyHoveredObject.material.emissive.setHex(0x000000);
}
currentlyHoveredObject = null;
// Kembalikan info box ke default
infoElement.innerText = defaultInfoText;
}
}
}
// === Fungsi Penanganan Klik Mouse (Dihapus) ===
// function onDocumentMouseDown(event) { ... }
// === Loop Animasi & Penanganan Ukuran Jendela ===
function animate() { requestAnimationFrame(animate); TWEEN.update(); controls.update(); renderer.render(scene, camera); }
function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }
// === Mulai Aplikasi ===
init();
</script>
</body>
</html>