Meta Description" name="description" />
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
<title>Simulasi 3D Tracking Conveyor Belt</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{width:100%;height:100%;overflow:hidden;background:#0d1117;font-family:Arial,Helvetica,sans-serif;color:#e6edf3}
#scene{position:fixed;inset:0;display:block;touch-action:none}
#title{position:fixed;top:0;left:0;right:0;padding:8px 12px;background:rgba(13,17,23,.85);font-size:14px;font-weight:bold;text-align:center;border-bottom:1px solid #30363d;z-index:5}
#hud{position:fixed;z-index:6;background:rgba(22,27,34,.94);border:1px solid #30363d;border-radius:10px;padding:12px;width:300px;top:46px;right:12px}
#hud h3{font-size:13px;color:#ff6b6b;margin-bottom:8px}
.ctl{margin:9px 0}
.ctl label{display:block;font-size:12px;margin-bottom:3px}
.ctl .v{float:right;font-weight:bold}
.ctl input{width:100%}
#t1{color:#ff6b6b}#t2{color:#4dd0e1}#t3{color:#69db7c}
.btn{margin:4px 3px 0 0;padding:6px 10px;border:none;border-radius:6px;background:#e63946;color:#fff;font-size:12px;cursor:pointer}
.btn.alt{background:#30363d}
.btn:active{opacity:.7}
#status{margin-top:10px;padding:8px;background:#0d1117;border-radius:6px;font-size:12px;line-height:1.5;border:1px solid #30363d}
#hint{margin-top:8px;font-size:11px;color:#8b949e;line-height:1.5}
#zoom{position:fixed;left:12px;bottom:12px;z-index:6;display:flex;flex-direction:column;gap:6px}
#zoom button{width:40px;height:40px;font-size:20px;border:1px solid #30363d;background:rgba(22,27,34,.94);color:#e6edf3;border-radius:8px;cursor:pointer}
#loading{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:#0d1117;z-index:20;font-size:14px}
@media(max-width:820px){
#hud{top:auto;right:8px;left:8px;bottom:8px;width:auto;max-height:52vh;overflow-y:auto}
#zoom{left:8px;bottom:auto;top:52px;flex-direction:row}
}
</style>
</head>
<body>
<div id="title">⚙️ Simulasi 3D Tracking Conveyor Belt — Muatan Ore</div>
<canvas id="scene"></canvas>
<div id="zoom">
<button onclick="zoomBy(0.82)">+</button>
<button onclick="zoomBy(1.22)">-</button>
</div>
<div id="hud">
<h3>🔧 Panel Penyetelan</h3>
<div class="ctl"><label id="t1">Upper Roller (idler atas) <span class="v" id="vU">0</span></label>
<input type="range" id="sU" min="-30" max="30" value="0"></div>
<div class="ctl"><label id="t2">Lower Roller (return) <span class="v" id="vL">0</span></label>
<input type="range" id="sL" min="-30" max="30" value="0"></div>
<div class="ctl"><label id="t3">Tail Pulley <span class="v" id="vT">0</span></label>
<input type="range" id="sT" min="-30" max="30" value="0"></div>
<button class="btn" onclick="setMiring()">⚠️ Simulasi Belt Miring</button>
<button class="btn alt" onclick="resetAll()">🔄 Reset</button>
<div id="status">Status: Belt lurus ✅ — ore berjalan normal</div>
<div id="hint">
• Geser slider → hanya bagian itu yang bergerak, lalu berhenti<br>
• Bagian aktif disorot <b style="color:#ff4d4d">merah</b> + garis bantu <b style="color:#ffd43b">kuning</b><br>
• Pinch = zoom · 1 jari = putar · 2 jari = geser
</div>
</div>
<div id="loading">Memuat mesin 3D…</div>
<script src=""></script>
<script src=""></script>
<script>
/* ============ SETUP ============ */
const D2R = Math.PI/180, TROUGH = 35*D2R;
let scene,camera,renderer,controls,clock;
let carryBelt,returnBelt,carryTex,returnTex;
let idlerSets=[],returnRollers=[],tailGroup=null;
let oreRocks=[],spills=[],arrows=[];
let guides={upper:null,lower:null,tail:null};
const S={ tU:0,tL:0,tT:0, cU:0,cL:0,cT:0, active:null, activeUntil:0 };
const MAT={
roller : new THREE.MeshStandardMaterial({color:0x9aa3ad,metalness:.85,roughness:.35}),
hot : new THREE.MeshStandardMaterial({color:0xff3b30,emissive:0x5a0a06,metalness:.5,roughness:.4}),
housing: new THREE.MeshStandardMaterial({color:0x30363d,metalness:.6,roughness:.6}),
steel : new THREE.MeshStandardMaterial({color:0x4a525c,metalness:.7,roughness:.5}),
belt : null, ore:[
new THREE.MeshStandardMaterial({color:0x8b7355,roughness:.95}),
new THREE.MeshStandardMaterial({color:0x6b5b3e,roughness:.95}),
new THREE.MeshStandardMaterial({color:0xa0926b,roughness:.95})
]
};
function beltTexture(){
const c=document.createElement('canvas');c.width=128;c.height=64;
const g=c.getContext('2d');
g.fillStyle='#26262b';g.fillRect(0,0,128,64);
g.strokeStyle='#33333a';g.lineWidth=7;
for(let i=-64;i<160;i+=30){g.beginPath();g.moveTo(i,0);g.lineTo(i+22,64);g.stroke();}
g.fillStyle='#1c1c20';g.fillRect(0,0,128,3);g.fillRect(0,61,128,3);
const t=new THREE.CanvasTexture(c);
t.wrapS=t.wrapT=THREE.RepeatWrapping;
return t;
}
function label(text,color){
const c=document.createElement('canvas');c.width=256;c.height=64;
const g=c.getContext('2d');
g.fillStyle='rgba(13,17,23,.75)';g.fillRect(0,0,256,64);
g.font='bold 26px Arial';g.fillStyle=color;g.textAlign='center';
g.fillText(text,128,42);
const sp=new THREE.Sprite(new THREE.SpriteMaterial({map:new THREE.CanvasTexture(c),transparent:true,depthTest:false}));
sp.scale.set(3.6,0.9,1);
return sp;
}
/* ============ BUILD ============ */
function init(){
scene=new THREE.Scene();
scene.background=new THREE.Color(0x0d1117);
scene.fog=new THREE.Fog(0x0d1117,40,90);
camera=new THREE.PerspectiveCamera(45,innerWidth/innerHeight,0.1,300);
camera.position.set(16,11,17);
renderer=new THREE.WebGLRenderer({canvas:document.getElementById('scene'),antialias:true});
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.setSize(innerWidth,innerHeight);
renderer.shadowMap.enabled=true;
controls=new THREE.OrbitControls(camera,renderer.domElement);
controls.target.set(0,1.4,0);
controls.enableDamping=true;controls.dampingFactor=.08;
controls.minDistance=6;controls.maxDistance=60;
controls.maxPolarAngle=Math.PI/2.05;
scene.add(new THREE.HemisphereLight(0xbfd4ff,0x20242b,0.75));
const dl=new THREE.DirectionalLight(0xffffff,1.0);
dl.position.set(14,20,12);dl.castShadow=true;
dl.shadow.mapSize.set(1024,1024);
dl.shadow.camera.left=-22;dl.shadow.camera.right=22;
dl.shadow.camera.top=22;dl.shadow.camera.bottom=-22;
scene.add(dl);
scene.add(new THREE.DirectionalLight(0x5b7fff,0.25).translateX(-15));
const ground=new THREE.Mesh(new THREE.PlaneGeometry(120,80),
new THREE.MeshStandardMaterial({color:0x1a1f26,roughness:1}));
ground.rotation.x=-Math.PI/2;ground.position.y=-1.2;ground.receiveShadow=true;
scene.add(ground);
const grid=new THREE.GridHelper(120,60,0x30363d,0x21262d);
grid.position.y=-1.19;scene.add(grid);
buildFrame();
buildPulleys();
buildIdlers();
buildReturnRollers();
buildBelts();
buildOre();
buildArrows();
buildLabels();
clock=new THREE.Clock();
document.getElementById('loading').style.display='none';
addEventListener('resize',onResize);
animate();
}
function buildFrame(){
const g=new THREE.Group();
[-1.95,1.95].forEach(z=>{
const beam=new THREE.Mesh(new THREE.BoxGeometry(27,0.3,0.22),MAT.steel);
beam.position.set(0,1.15,z);beam.castShadow=true;g.add(beam);
const beam2=new THREE.Mesh(new THREE.BoxGeometry(27,0.18,0.18),MAT.steel);
beam2.position.set(0,-0.05,z);g.add(beam2);
});
[-11,-5.5,0,5.5,11].forEach(x=>{
[-1.95,1.95].forEach(z=>{
const leg=new THREE.Mesh(new THREE.BoxGeometry(0.22,2.5,0.22),MAT.steel);
leg.position.set(x,-0.05,z);leg.castShadow=true;g.add(leg);
const lo=new THREE.Mesh(new THREE.BoxGeometry(0.2,1.15,0.2),MAT.steel);
lo.position.set(x,-0.75,z*1.35);g.add(lo);
});
const cross=new THREE.Mesh(new THREE.BoxGeometry(0.16,0.16,4.3),MAT.steel);
cross.position.set(x,-0.05,0);g.add(cross);
});
scene.add(g);
}
function pulley(radius,len){
const g=new THREE.Group();
const drum=new THREE.Mesh(new THREE.CylinderGeometry(radius,radius,len,28),MAT.roller);
drum.rotation.x=Math.PI/2;drum.castShadow=true;g.add(drum);
[-1,1].forEach(s=>{
const h=new THREE.Mesh(new THREE.BoxGeometry(radius*1.5,radius*1.9,0.42),MAT.housing);
h.position.z=s*(len/2+0.28);g.add(h);
const b=new THREE.Mesh(new THREE.CylinderGeometry(radius*0.34,radius*0.34,0.5,16),MAT.roller);
b.rotation.x=Math.PI/2;b.position.z=s*(len/2+0.3);g.add(b);
[[-1,-1],[1,-1],[-1,1],[1,1]].forEach(p=>{
const bolt=new THREE.Mesh(new THREE.CylinderGeometry(0.07,0.07,0.1,8),
new THREE.MeshStandardMaterial({color:0xffd43b,metalness:.6,roughness:.4}));
bolt.rotation.x=Math.PI/2;
bolt.position.set(p[0]*radius*0.6,p[1]*radius*0.75,s*(len/2+0.5));
g.add(bolt);
});
});
return g;
}
function buildPulleys(){
tailGroup=new THREE.Group();
tailGroup.position.set(-13,1.32,0);
const p=pulley(0.72,3.3);
tailGroup.add(p);
tailGroup.userData.meshes=p.children.filter(c=>c.material===MAT.roller);
guides.tail=makeGuide(0xffd43b,4.6);
guides.tail.position.y=0.95;
tailGroup.add(guides.tail);
scene.add(tailGroup);
const head=pulley(0.72,3.3);
head.position.set(13,1.32,0);
scene.add(head);
const motor=new THREE.Mesh(new THREE.BoxGeometry(1.5,1.1,1.1),MAT.housing);
motor.position.set(14.4,0.5,1.9);motor.castShadow=true;scene.add(motor);
}
function makeGuide(color,len){
const geo=new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0,0,-len/2),new THREE.Vector3(0,0,len/2)]);
const line=new THREE.Line(geo,new THREE.LineBasicMaterial({color,linewidth:2,transparent:true,opacity:.95}));
line.visible=false;
return line;
}
function buildIdlers(){
[-9.5,-5,0,5,9.5].forEach((x,i)=>{
const root=new THREE.Group();root.position.x=x;
const pivot=new THREE.Group();root.add(pivot);
const meshes=[];
const c=new THREE.Mesh(new THREE.CylinderGeometry(0.28,0.28,1.5,20),MAT.roller);
c.rotation.x=Math.PI/2;c.position.y=1.72;c.castShadow=true;
pivot.add(c);meshes.push(c);
[-1,1].forEach(s=>{
const w=new THREE.Mesh(new THREE.CylinderGeometry(0.26,0.26,1.0,20),MAT.roller);
w.rotation.x=Math.PI/2 - s*TROUGH;
w.position.set(0, 1.72+0.5*Math.sin(TROUGH), s*(0.75+0.5*Math.cos(TROUGH)));
w.castShadow=true;pivot.add(w);meshes.push(w);
const br=new THREE.Mesh(new THREE.BoxGeometry(0.18,0.9,0.18),MAT.housing);
br.position.set(0,1.4,s*1.75);pivot.add(br);
});
const base=new THREE.Mesh(new THREE.BoxGeometry(0.2,0.16,4),MAT.housing);
base.position.y=1.18;root.add(base);
if(i===2){
guides.upper=makeGuide(0xffd43b,5.2);
guides.upper.position.y=2.25;
pivot.add(guides.upper);
}
root.userData={pivot,meshes};
idlerSets.push(root);scene.add(root);
});
}
function buildReturnRollers(){
[-8,-2.5,3,8.5].forEach((x,i)=>{
const root=new THREE.Group();root.position.x=x;
const pivot=new THREE.Group();root.add(pivot);
const r=new THREE.Mesh(new THREE.CylinderGeometry(0.26,0.26,3.3,20),MAT.roller);
r.rotation.x=Math.PI/2;r.position.y=0.36;r.castShadow=true;
pivot.add(r);
if(i===1){
guides.lower=makeGuide(0xffd43b,5.0);
guides.lower.position.y=0.7;
pivot.add(guides.lower);
}
root.userData={pivot,meshes:[r]};
returnRollers.push(root);scene.add(root);
});
}
function buildBelts(){
carryTex=beltTexture();carryTex.repeat.set(14,1);
returnTex=beltTexture();returnTex.repeat.set(14,1);
const mC=new THREE.MeshStandardMaterial({map:carryTex,roughness:.85,metalness:.05,side:THREE.DoubleSide});
const mR=new THREE.MeshStandardMaterial({map:returnTex,roughness:.85,metalness:.05,side:THREE.DoubleSide});
carryBelt=new THREE.Group();
const mid=new THREE.Mesh(new THREE.PlaneGeometry(26,1.5),mC);
mid.rotation.x=-Math.PI/2;mid.position.y=2.01;mid.receiveShadow=true;
carryBelt.add(mid);
[-1,1].forEach(s=>{
const w=new THREE.Mesh(new THREE.PlaneGeometry(26,1.0),mC);
w.rotation.x=-Math.PI/2;
w.rotation.y=0;
w.rotation.z=0;
w.geometry.rotateX(0);
w.rotation.set(-Math.PI/2 + s*TROUGH*0, 0, 0);
// tilt wing belt to match wing rollers
w.rotation.x=-Math.PI/2;
w.rotation.order='ZYX';
w.rotation.z=0;
w.position.set(0, 2.01+0.5*Math.sin(TROUGH), s*(0.75+0.5*Math.cos(TROUGH)));
w.rotateOnAxis(new THREE.Vector3(1,0,0), -s*TROUGH);
w.receiveShadow=true;
carryBelt.add(w);
});
scene.add(carryBelt);
returnBelt=new THREE.Group();
const rb=new THREE.Mesh(new THREE.PlaneGeometry(26,3.2),mR);
rb.rotation.x=-Math.PI/2;rb.position.y=0.63;rb.receiveShadow=true;
returnBelt.add(rb);
scene.add(returnBelt);
}
function buildOre(){
const geos=[
new THREE.DodecahedronGeometry(0.22,0),
new THREE.DodecahedronGeometry(0.17,0),
new THREE.IcosahedronGeometry(0.2,0)
];
for(let i=0;i<190;i++){
const m=new THREE.Mesh(geos[i%3],MAT.ore[i%3]);
const t=Math.random();
m.userData.z=(Math.random()-0.5)*1.5;
m.userData.y=2.12+Math.random()*0.42*(1-Math.abs(m.userData.z)/0.9);
m.position.set(-13+t*26, m.userData.y, m.userData.z);
m.scale.setScalar(0.7+Math.random()*0.7);
m.rotation.set(Math.random()*3,Math.random()*3,Math.random()*3);
m.castShadow=true;
oreRocks.push(m);scene.add(m);
}
for(let i=0;i<70;i++){
const m=new THREE.Mesh(geos[i%3],MAT.ore[i%3]);
m.scale.setScalar(0.55);m.visible=false;
m.userData.vy=0;m.userData.side=1;
spills.push(m);scene.add(m);
}
}
function buildArrows(){
const geo=new THREE.ConeGeometry(0.22,0.6,10);
const mat=new THREE.MeshStandardMaterial({color:0x39d353,emissive:0x0f5c22,transparent:true,opacity:.9});
for(let s of [-1,1]){
for(let i=0;i<6;i++){
const a=new THREE.Mesh(geo,mat.clone());
a.rotation.z=-Math.PI/2;
a.position.set(-11+i*4.4, 1.45, s*2.55);
a.userData.phase=i*0.35;
arrows.push(a);scene.add(a);
}
const bar=new THREE.Mesh(new THREE.BoxGeometry(24,0.06,0.06),
new THREE.MeshStandardMaterial({color:0x1f7a35}));
bar.position.set(-0.2,1.45,s*2.55);scene.add(bar);
const sp=label('➡ ARAH BELT','#39d353');
sp.position.set(0,2.9,s*2.9);scene.add(sp);
}
}
function buildLabels(){
const L=[
['TAIL PULLEY','#69db7c',-13,0.2],
['HEAD PULLEY','#8b949e',13,0.2],
['UPPER ROLLER','#ff6b6b',0,3.35],
['LOWER ROLLER','#4dd0e1',-2.5,-0.35]
];
L.forEach(([t,c,x,y])=>{const s=label(t,c);s.position.set(x,y===0.2?0.15:y,0);
if(t==='LOWER ROLLER'){s.position.set(-2.5,-0.4,2.6);}
if(t==='UPPER ROLLER'){s.position.set(0,3.4,0);}
if(t==='TAIL PULLEY'){s.position.set(-13,2.9,0);}
if(t==='HEAD PULLEY'){s.position.set(13,2.9,0);}
scene.add(s);});
}
/* ============ INTERAKSI ============ */
const eU=document.getElementById('sU'),eL=document.getElementById('sL'),eT=document.getElementById('sT');
function touch(part){S.active=part;S.activeUntil=performance.now()+2600;}
eU.addEventListener('input',()=>{S.tU=+eU.value;touch('upper');});
eL.addEventListener('input',()=>{S.tL=+eL.value;touch('lower');});
eT.addEventListener('input',()=>{S.tT=+eT.value;touch('tail');});
function resetAll(){eU.value=0;eL.value=0;eT.value=0;S.tU=S.tL=S.tT=0;touch('upper');}
function setMiring(){eU.value=-18;eL.value=12;eT.value=22;S.tU=-18;S.tL=12;S.tT=22;touch('tail');}
function zoomBy(f){
const d=camera.position.clone().sub(controls.target);
const len=THREE.MathUtils.clamp(d.length()*f,controls.minDistance,controls.maxDistance);
camera.position.copy(controls.target.clone().add(d.setLength(len)));
}
function onResize(){
camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();
renderer.setSize(innerWidth,innerHeight);
}
/* ============ LOOP ============ */
function animate(){
requestAnimationFrame(animate);
const dt=Math.min(clock.getDelta(),0.05), now=performance.now();
// --- bagian mekanis: HANYA bergerak saat nilai berubah (lerp lalu berhenti) ---
const k=1-Math.pow(0.001,dt);
S.cU+=(S.tU-S.cU)*k;
S.cL+=(S.tL-S.cL)*k;
S.cT+=(S.tT-S.cT)*k;
if(Math.abs(S.tU-S.cU)<0.01)S.cU=S.tU;
if(Math.abs(S.tL-S.cL)<0.01)S.cL=S.tL;
if(Math.abs(S.tT-S.cT)<0.01)S.cT=S.tT;
idlerSets.forEach((r,i)=>{
r.userData.pivot.rotation.y = S.cU*D2R*0.5*(i===2?1:0.45);
});
returnRollers.forEach((r,i)=>{
r.userData.pivot.rotation.y = S.cL*D2R*0.5*(i===1?1:0.45);
});
tailGroup.position.z = S.cT*0.022;
tailGroup.rotation.y = S.cT*D2R*0.35;
// --- highlight bagian aktif ---
const on = now < S.activeUntil;
const pulse = 0.5+0.5*Math.sin(now*0.012);
const setMat=(list,hot)=>list.forEach(m=>m.material = hot?MAT.hot:MAT.roller);
setMat(idlerSets[2].userData.meshes, on&&S.active==='upper');
idlerSets.forEach((r,i)=>{ if(i!==2) setMat(r.userData.meshes,false); });
returnRollers.forEach((r,i)=> setMat(r.userData.meshes, on&&S.active==='lower'&&i===1));
setMat(tailGroup.userData.meshes, on&&S.active==='tail');
guides.upper.visible = on&&S.active==='upper';
guides.lower.visible = on&&S.active==='lower';
guides.tail.visible = on&&S.active==='tail';
[guides.upper,guides.lower,guides.tail].forEach(g=>{if(g&&g.visible)g.material.opacity=0.35+0.65*pulse;});
// --- belt: JALAN TERUS ---
carryTex.offset.x -= dt*0.55;
returnTex.offset.x += dt*0.55;
// --- tracking belt (mengikuti hasil penyetelan) ---
const carryOff = (S.cT*0.5 + S.cU*0.42)/30*1.25;
const retOff = (S.cT*0.3 + S.cL*0.55)/30*1.25;
carryBelt.position.z += (carryOff-carryBelt.position.z)*k;
returnBelt.position.z += (retOff-returnBelt.position.z)*k;
carryBelt.rotation.y = -S.cU*D2R*0.05;
// --- muatan ore: JALAN TERUS ---
const spd = 4.2*dt;
oreRocks.forEach(m=>{
m.position.x += spd;
if(m.position.x>13.2) m.position.x=-13.2;
m.position.z = m.userData.z + carryBelt.position.z;
m.position.y = m.userData.y;
m.rotation.z -= spd*0.9;
});
// --- ceceran ore saat belt sangat miring ---
const mis=Math.abs(S.cU)+Math.abs(S.cL)+Math.abs(S.cT);
const side=Math.sign(carryBelt.position.z)||1;
spills.forEach(p=>{
if(!p.visible){
if(mis>34 && Math.random()<0.05){
p.visible=true;p.userData.vy=0;
p.position.set(-12+Math.random()*24, 2.0, side*(1.45+carryBelt.position.z));
}
return;
}
p.userData.vy-=13*dt;
p.position.y+=p.userData.vy*dt;
p.position.z+=side*1.3*dt;
p.position.x+=spd*0.6;
p.rotation.x+=dt*4;p.rotation.y+=dt*3;
if(p.position.y<-1.05){p.visible=false;}
});
// --- panah arah ---
arrows.forEach(a=>{a.material.opacity=0.35+0.6*(0.5+0.5*Math.sin(now*0.005-a.userData.phase*3));});
// --- status ---
document.getElementById('vU').textContent=S.tU;
document.getElementById('vL').textContent=S.tL;
document.getElementById('vT').textContent=S.tT;
const st=document.getElementById('status');
const dir = carryBelt.position.z>0.06?'↗ belt geser KANAN':carryBelt.position.z<-0.06?'↙ belt geser KIRI':'● belt di TENGAH';
if(mis<5) st.innerHTML='Status: Belt lurus ✅ — ore berjalan normal<br><span style="color:#8b949e">'+dir+'</span>';
else if(mis<25) st.innerHTML='Status: Belt sedikit miring ⚠️ — perlu penyetelan<br><span style="color:#ffd43b">'+dir+'</span>';
else st.innerHTML='Status: Belt sangat miring ❌ — segera koreksi!<br><span style="color:#ff6b6b">'+dir+' · ore mulai tumpah</span>';
controls.update();
renderer.render(scene,camera);
}
init();
</script>
</body>
</html>
1
1
20KB
20KB
95.0ms
208.0ms
96.0ms