| Current Path : /var/www/html/onapp/simulator-interaktif-mikropengawal/ |
| Current File : /var/www/html/onapp/simulator-interaktif-mikropengawal/index.html |
<!DOCTYPE html>
<html lang="ms" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>SIMuC v1.3.0 - Simulator Interaktif Mikropengawal</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-icons/1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800;900&display=swap" rel="stylesheet">
<style>
:root{--primary:#F5A623;--primary-light:#FFEACC;--primary-dark:#D4891A;--primary-glow:rgba(245,166,35,0.3);--bg:#FFF9F0;--surface:#FFFFFF;--text:#3D3D3D;--text-secondary:#7A7A7A;--accent:#FF8C42;--success:#4CAF50;--border:#F0E0CC;--shadow:rgba(200,160,100,0.15);--shadow-strong:rgba(200,160,100,0.3);--overlay:rgba(255,249,240,0.95)}
[data-theme="dark"]{--primary:#F5A623;--primary-light:#4A3A20;--primary-dark:#FFB84D;--primary-glow:rgba(245,166,35,0.2);--bg:#1A1612;--surface:#2A2420;--text:#F0E6D6;--text-secondary:#A09080;--accent:#FF8C42;--success:#66BB6A;--border:#3A3028;--shadow:rgba(0,0,0,0.3);--shadow-strong:rgba(0,0,0,0.5);--overlay:rgba(26,22,18,0.95)}
*{box-sizing:border-box;margin:0;padding:0}
html,body{width:100%;height:100%;overflow:hidden;font-family:'Nunito',sans-serif;background:var(--bg);color:var(--text);-webkit-tap-highlight-color:transparent;user-select:none}
.screen{position:fixed;inset:0;display:none;flex-direction:column;align-items:center;justify-content:center;z-index:10;transition:opacity .5s ease}
.screen.active{display:flex}.screen.fade-out{opacity:0;pointer-events:none}
.pattern-bg{position:fixed;inset:0;overflow:hidden;z-index:0}
.pattern-bg .bg-image{position:absolute;inset:0;background:url('./images/bg.jpg') center/cover no-repeat}
.pattern-bg .bg-overlay{position:absolute;inset:0;background:linear-gradient(180deg,rgba(245,180,70,0.75) 0%,rgba(245,166,35,0.45) 35%,rgba(255,170,60,0.15) 65%,transparent 100%)}
.pattern-bg .circuit-lines{position:absolute;inset:0;opacity:.04;background-image:linear-gradient(90deg,rgba(255,255,255,.5) 1px,transparent 1px),linear-gradient(rgba(255,255,255,.5) 1px,transparent 1px);background-size:60px 60px}
.pattern-bg canvas.symbols-canvas{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}
@keyframes bgFloat{0%,100%{transform:translate(0,0)}33%{transform:translate(-2%,1%)}66%{transform:translate(1%,-1%)}}
#titleScreen{z-index:20}.title-content{text-align:center;padding:2rem;position:relative;z-index:2}
#titleScreen .title-abbr{color:#2D2A25;text-shadow:none}
#titleScreen .title-subject{color:#4A4540;text-shadow:none}
#titleScreen .title-target{color:#5A5550;text-shadow:none}
#titleScreen .title-version{background:rgba(0,0,0,0.08);color:#2D2A25;border-color:rgba(0,0,0,0.15);backdrop-filter:blur(4px)}
#titleScreen .btn-simuc{background:linear-gradient(135deg,var(--primary),var(--accent));color:#fff;box-shadow:0 6px 24px rgba(0,0,0,0.2)}
#titleScreen .btn-simuc:hover{background:linear-gradient(135deg,var(--primary-dark),var(--primary));transform:translateY(-2px) scale(1.02);box-shadow:0 10px 36px rgba(0,0,0,0.25);color:#fff}
.app-logo{width:120px;height:120px;background:linear-gradient(135deg,var(--primary),var(--accent));border-radius:28px;display:flex;align-items:center;justify-content:center;margin:0 auto 1.5rem;box-shadow:0 12px 40px var(--shadow-strong);animation:logoFloat 3s ease-in-out infinite;position:relative}
.app-logo::after{content:'';position:absolute;inset:-4px;border-radius:32px;background:linear-gradient(135deg,var(--primary),var(--accent));z-index:-1;opacity:.3;filter:blur(12px)}
.app-logo i{font-size:3.2rem;color:white}
.app-logo-img{max-width:min(480px,80vw);height:auto;margin:0 auto 1rem;display:block;animation:logoFloat 3s ease-in-out infinite;filter:drop-shadow(0 12px 32px rgba(0,0,0,0.15))}
@keyframes logoFloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-10px)}}
.title-name{font-size:clamp(2rem,6vw,3.5rem);font-weight:900;line-height:1.15;margin-bottom:.3rem}
.title-name span{background:linear-gradient(135deg,var(--primary),var(--accent));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.title-abbr{font-size:clamp(1.1rem,3vw,1.6rem);font-weight:700;color:var(--primary);letter-spacing:3px;margin-bottom:.5rem}
.title-subject{font-size:clamp(.85rem,2vw,1.05rem);color:var(--text-secondary);font-weight:600;margin-bottom:2.5rem}
.title-version{display:inline-block;padding:4px 16px;border-radius:20px;font-size:.75rem;font-weight:800;letter-spacing:1px;background:var(--primary-light);color:var(--primary-dark);border:1.5px solid var(--primary);margin-bottom:1rem}
.title-target{font-size:.8rem;color:var(--text-secondary);margin-top:1.5rem;font-weight:600}
.btn-simuc{background:linear-gradient(135deg,var(--primary),var(--accent));color:#fff;border:none;padding:.85rem 2.8rem;border-radius:50px;font-family:'Nunito',sans-serif;font-size:1.15rem;font-weight:800;cursor:pointer;transition:all .25s cubic-bezier(.4,0,.2,1);box-shadow:0 6px 24px var(--shadow-strong);letter-spacing:.5px}
.btn-simuc:hover{transform:translateY(-2px) scale(1.02);box-shadow:0 10px 36px var(--shadow-strong);color:#fff}.btn-simuc:active{transform:scale(.98)}
.btn-simuc-outline{background:var(--surface);color:var(--primary);border:2px solid var(--primary);padding:.7rem 1.6rem;border-radius:50px;font-family:'Nunito',sans-serif;font-size:.95rem;font-weight:700;cursor:pointer;transition:all .25s ease;box-shadow:0 4px 16px var(--shadow)}
.btn-simuc-outline:hover{background:var(--primary-light);color:var(--primary-dark)}
.btn-nav{width:52px;height:52px;border-radius:50%;background:var(--surface);border:2px solid var(--border);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .25s ease;box-shadow:0 4px 16px var(--shadow);color:var(--text);font-size:1.3rem}
.btn-nav:hover{background:var(--primary);border-color:var(--primary);color:#fff;transform:scale(1.08)}.btn-nav:active{transform:scale(.95)}
.btn-nav:disabled{opacity:.3;cursor:not-allowed}.btn-nav:disabled:hover{background:var(--surface);border-color:var(--border);color:var(--text);transform:none}
.settings-toggle{position:fixed;top:16px;right:16px;z-index:100;width:44px;height:44px;border-radius:50%;background:var(--surface);border:2px solid var(--border);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .3s ease;box-shadow:0 4px 16px var(--shadow);color:var(--text);font-size:1.2rem}
.settings-toggle:hover{background:var(--primary);border-color:var(--primary);color:#fff;transform:rotate(60deg)}
.settings-toggle.open{transform:rotate(90deg);background:var(--primary);border-color:var(--primary);color:#fff}
.settings-panel{position:fixed;top:0;right:-340px;width:320px;max-width:85vw;height:100%;background:var(--surface);border-left:1px solid var(--border);z-index:99;transition:right .35s cubic-bezier(.4,0,.2,1);box-shadow:-8px 0 30px var(--shadow);overflow-y:auto;padding:80px 24px 24px}
.settings-panel.open{right:0}
.comp-video-btn{position:fixed;top:70px;right:16px;z-index:100;width:44px;height:44px;border-radius:50%;background:var(--surface);border:2px solid var(--border);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .3s ease;box-shadow:0 4px 16px var(--shadow);color:var(--primary);font-size:1.2rem}
.comp-video-btn:hover{background:var(--primary);border-color:var(--primary);color:#fff}
.comp-video-panel{position:fixed;top:120px;right:16px;width:340px;max-width:85vw;z-index:98;border-radius:16px;overflow:hidden;box-shadow:0 12px 48px rgba(0,0,0,0.2);background:#000;transform:scale(0.8) translateY(-20px);opacity:0;pointer-events:none;transition:all .3s cubic-bezier(.4,0,.2,1)}
.comp-video-panel.open{transform:scale(1) translateY(0);opacity:1;pointer-events:auto}
.comp-video-panel .cvp-thumb{width:100%;aspect-ratio:16/9;object-fit:cover;display:block;cursor:pointer}
.comp-video-panel.playing .cvp-thumb{display:none}
.comp-video-panel video{width:100%;aspect-ratio:16/9;object-fit:cover;display:none}
.comp-video-panel.playing video{display:block}
.comp-video-panel .cvp-play{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:56px;height:56px;border-radius:50%;background:rgba(255,255,255,0.92);border:none;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 6px 24px rgba(0,0,0,0.3);transition:all .2s}
.comp-video-panel .cvp-play i{font-size:1.5rem;color:var(--primary);margin-left:3px}
.comp-video-panel.playing .cvp-play{opacity:0;pointer-events:none}
.comp-video-panel .cvp-badge{position:absolute;top:8px;left:8px;background:rgba(40,40,40,0.8);backdrop-filter:blur(4px);color:#fff;padding:3px 10px;border-radius:14px;font-size:.65rem;font-weight:800;letter-spacing:.5px;display:flex;align-items:center;gap:5px}
.comp-video-panel .cvp-badge::before{content:'';width:7px;height:7px;border-radius:50%;background:#EF4444;animation:vidPulse 1.5s ease-in-out infinite}
.comp-video-panel .cvp-close{position:absolute;top:8px;right:8px;width:28px;height:28px;border-radius:50%;background:rgba(0,0,0,0.5);border:none;color:#fff;font-size:.8rem;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s;z-index:2}
.comp-video-panel .cvp-close:hover{background:rgba(0,0,0,0.8)}
.comp-video-panel .cvp-caption{padding:8px 12px;background:rgba(0,0,0,0.85);color:#fff;font-size:.72rem;font-weight:600;text-align:center}
.comp-info-btn{position:fixed;top:70px;right:68px;z-index:100;width:44px;height:44px;border-radius:50%;background:var(--surface);border:2px solid var(--border);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .3s ease;box-shadow:0 4px 16px var(--shadow);color:var(--primary);font-size:1.2rem}
.comp-info-btn:hover{background:var(--primary);border-color:var(--primary);color:#fff}
.comp-info-btn.active{background:var(--primary);border-color:var(--primary);color:#fff}
.comp-info-panel{position:fixed;top:120px;right:16px;width:360px;max-width:85vw;max-height:calc(100vh - 280px);z-index:98;border-radius:16px;overflow:hidden;box-shadow:0 12px 48px rgba(0,0,0,0.18);background:var(--surface);transform:scale(0.8) translateY(-20px);opacity:0;pointer-events:none;transition:all .3s cubic-bezier(.4,0,.2,1);display:flex;flex-direction:column}
.comp-info-panel.open{transform:scale(1) translateY(0);opacity:1;pointer-events:auto}
.cip-header{display:flex;align-items:center;gap:8px;padding:12px 14px;background:linear-gradient(135deg,var(--primary),var(--accent));color:#fff;flex-shrink:0}
.cip-header .cip-icon{font-size:1.1rem;flex-shrink:0}
.cip-header .cip-title{flex:1;font-size:.92rem;font-weight:800;line-height:1.2}
.cip-header .cip-cat{font-size:.65rem;font-weight:700;background:rgba(255,255,255,0.25);padding:2px 8px;border-radius:10px;flex-shrink:0}
.cip-header .cip-close{width:28px;height:28px;border-radius:50%;background:rgba(255,255,255,.25);border:none;display:flex;align-items:center;justify-content:center;cursor:pointer;color:#fff;font-size:.8rem;transition:all .2s;flex-shrink:0}
.cip-header .cip-close:hover{background:rgba(255,255,255,.5)}
.cip-body{padding:14px 16px;overflow-y:auto;flex:1;font-size:.84rem;line-height:1.75;font-weight:600;color:var(--text)}
.cip-body p{margin-bottom:.5rem}
.cip-body .bul{padding-left:1.2rem;position:relative}.cip-body .bul::before{content:'●';position:absolute;left:0;color:var(--primary);font-size:.55rem;top:5px}
.cip-footer{padding:8px 14px;border-top:1px solid var(--border);flex-shrink:0;display:flex;gap:8px}
.cip-footer button{padding:6px 14px;border-radius:20px;border:2px solid var(--primary);background:var(--surface);color:var(--primary);font-family:'Nunito',sans-serif;font-size:.78rem;font-weight:700;cursor:pointer;display:flex;align-items:center;gap:5px;transition:all .2s}
.cip-footer button:hover{background:var(--primary);color:#fff}
@media(max-width:576px){.comp-info-btn{right:60px;top:70px}.comp-info-panel{width:280px;right:8px;top:110px;max-height:calc(100vh - 260px)}}
.settings-panel h5{font-weight:800;font-size:1.3rem;margin-bottom:1.5rem}
.setting-group{margin-bottom:1.5rem;padding-bottom:1.5rem;border-bottom:1px solid var(--border)}.setting-group:last-child{border-bottom:none}
.setting-label{font-size:.85rem;font-weight:700;color:var(--text-secondary);text-transform:uppercase;letter-spacing:1px;margin-bottom:.6rem}
.theme-switch-wrap{display:flex;align-items:center;gap:12px}
.theme-switch{width:56px;height:30px;border-radius:30px;background:var(--border);position:relative;cursor:pointer;transition:background .3s;flex-shrink:0}
.theme-switch.dark{background:var(--primary)}
.theme-switch .toggle-knob{width:24px;height:24px;border-radius:50%;background:white;position:absolute;top:3px;left:3px;transition:transform .3s cubic-bezier(.4,0,.2,1);box-shadow:0 2px 6px rgba(0,0,0,.2)}
.theme-switch.dark .toggle-knob{transform:translateX(26px)}
.theme-switch-label{font-weight:600;font-size:.95rem}
.volume-control{display:flex;align-items:center;gap:10px}
.volume-control i{font-size:1.2rem;color:var(--primary);flex-shrink:0}
.volume-slider{flex:1;-webkit-appearance:none;height:6px;border-radius:3px;background:var(--border);outline:none}
.volume-slider::-webkit-slider-thumb{-webkit-appearance:none;width:20px;height:20px;border-radius:50%;background:var(--primary);cursor:pointer;box-shadow:0 2px 8px var(--shadow-strong)}
.volume-val{font-size:.85rem;font-weight:700;color:var(--text-secondary);min-width:35px;text-align:right}
.save-config-btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:12px;border-radius:14px;background:linear-gradient(135deg,var(--primary),var(--accent));border:none;color:#fff;font-family:'Nunito',sans-serif;font-size:.95rem;font-weight:800;cursor:pointer;transition:all .25s;box-shadow:0 4px 16px var(--shadow-strong)}
.save-config-btn:hover{transform:translateY(-1px);box-shadow:0 6px 24px var(--shadow-strong)}.save-config-btn:active{transform:scale(.97)}
.save-config-btn.saved{background:var(--success)!important}
#menuScreen{background:var(--bg);position:relative}
#menuScreen::before{content:'';position:absolute;inset:0;background:linear-gradient(180deg,rgba(245,180,70,0.75) 0%,rgba(245,166,35,0.45) 35%,rgba(255,170,60,0.15) 65%,transparent 100%);pointer-events:none;z-index:0}
#menuScreen .menu-content{width:100%;max-width:1100px;height:100vh;padding:2rem 2rem 1.5rem;position:relative;z-index:2;display:flex;gap:2.5rem;align-items:center}
.menu-left{flex:1;min-width:280px;display:flex;flex-direction:column;align-items:center}
.menu-right{flex:1.1;min-width:300px;display:flex;flex-direction:column;align-items:center;justify-content:center}
.menu-logo{width:auto;height:48px;margin-bottom:.8rem;filter:drop-shadow(0 4px 12px rgba(0,0,0,0.1))}
.menu-cards-wrap{width:100%;max-width:480px}
.menu-header{text-align:center;margin-bottom:1.5rem}.menu-header h2{font-size:clamp(1.3rem,4vw,1.8rem);font-weight:800}.menu-header p{font-size:.9rem;color:var(--text-secondary);font-weight:600}
.video-player{position:relative;border-radius:18px;overflow:hidden;box-shadow: rgb(38, 57, 77) 0px 20px 30px -10px;cursor:pointer;background:#000;aspect-ratio:16/9}
.video-player .video-thumb{width:100%;height:100%;object-fit:cover;display:block;transition:opacity .3s}
.video-player.playing .video-thumb{opacity:0;pointer-events:none;position:absolute;inset:0}
.video-player video{width:100%;height:100%;object-fit:cover;display:none}
.video-player.playing video{display:block}
.video-badge{position:absolute;top:12px;left:12px;background:rgba(50,50,50,0.8);backdrop-filter:blur(6px);color:#fff;padding:4px 14px;border-radius:20px;font-size:.7rem;font-weight:800;letter-spacing:1px;display:flex;align-items:center;gap:6px;z-index:3}
.video-badge::before{content:'';width:8px;height:8px;border-radius:50%;background:#EF4444;animation:vidPulse 1.5s ease-in-out infinite}
@keyframes vidPulse{0%,100%{opacity:1}50%{opacity:.4}}
.video-duration{position:absolute;bottom:12px;right:12px;background:rgba(0,0,0,0.7);color:#fff;padding:3px 10px;border-radius:8px;font-size:.72rem;font-weight:700;z-index:3;font-family:'Nunito',sans-serif}
.video-play-btn{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:68px;height:68px;border-radius:50%;background:rgba(255,255,255,0.95);display:flex;align-items:center;justify-content:center;box-shadow:0 8px 32px rgba(0,0,0,0.25);z-index:3;transition:all .25s ease;border:none;cursor:pointer}
.video-play-btn i{font-size:1.8rem;color:var(--primary);margin-left:4px}
.video-player:hover .video-play-btn{transform:translate(-50%,-50%) scale(1.08);box-shadow:0 12px 40px rgba(0,0,0,0.3)}
.video-player.playing .video-play-btn{opacity:0;pointer-events:none}
.video-player.playing .video-badge::before{background:#EF4444}
.video-frame{position:absolute;inset:8%;border:2px solid rgba(255,255,255,0.25);border-radius:6px;z-index:2;pointer-events:none}
.video-caption{text-align:center;margin-top:.8rem;font-size:.82rem;font-weight:700;color:var(--text-secondary);line-height:1.4}
.menu-back-row{margin-top:1.2rem;text-align:center}
@media(max-width:768px){#menuScreen .menu-content{flex-direction:column;max-width:500px;gap:1.5rem}.menu-left,.menu-right{min-width:auto;width:100%}}
.menu-card{background:var(--surface);border:2px solid var(--border);border-radius:20px;padding:1.2rem 1.4rem;margin-bottom:1rem;cursor:pointer;transition:all .3s cubic-bezier(.4,0,.2,1);display:flex;align-items:center;gap:1rem;box-shadow:0 4px 16px var(--shadow);position:relative;overflow:hidden}
.menu-card::before{content:'';position:absolute;left:0;top:0;bottom:0;width:5px;background:var(--primary);border-radius:0 3px 3px 0;transform:scaleY(0);transition:transform .3s ease}
.menu-card:hover::before{transform:scaleY(1)}.menu-card:hover{transform:translateY(-3px);box-shadow:0 8px 32px var(--shadow-strong);border-color:var(--primary)}
.menu-card-num{width:50px;height:50px;border-radius:16px;background:linear-gradient(135deg,var(--primary),var(--accent));display:flex;align-items:center;justify-content:center;color:#fff;font-size:1.3rem;font-weight:900;flex-shrink:0}
.menu-card-body h4{font-size:1rem;font-weight:800;margin-bottom:.2rem}.menu-card-body p{font-size:.8rem;color:var(--text-secondary);font-weight:600;margin:0}
.menu-card-arrow{margin-left:auto;color:var(--primary);font-size:1.2rem;opacity:0;transform:translateX(-8px);transition:all .3s ease}.menu-card:hover .menu-card-arrow{opacity:1;transform:translateX(0)}
#viewerScreen{flex-direction:column;background:var(--bg);position:relative;z-index:25}
#viewerScreen::before{content:'';position:absolute;inset:0;background:linear-gradient(180deg,rgba(245,180,70,0.55) 0%,rgba(245,166,35,0.25) 30%,rgba(255,170,60,0.08) 55%,transparent 100%);pointer-events:none;z-index:0}
.viewer-topbar{position:fixed;top:0;left:0;right:0;height:60px;background:var(--overlay);backdrop-filter:blur(12px);border-bottom:1px solid var(--border);display:flex;align-items:center;padding:0 16px;z-index:30}
.topbar-back{width:40px;height:40px;border-radius:50%;background:var(--surface);border:2px solid var(--border);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .25s ease;color:var(--text);font-size:1.1rem;flex-shrink:0}
.topbar-back:hover{background:var(--primary);border-color:var(--primary);color:#fff}
.topbar-title{flex:1;text-align:center;font-weight:800;font-size:clamp(.8rem,2.5vw,1rem);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:0 8px}
.category-tabs{position:fixed;bottom:100px;left:0;right:0;background:var(--overlay);backdrop-filter:blur(12px);border-top:1px solid var(--border);display:flex;overflow-x:auto;scrollbar-width:none;padding:8px 12px;gap:6px;z-index:29}
.category-tabs::-webkit-scrollbar{display:none}
.cat-tab{flex-shrink:0;padding:6px 16px;border-radius:30px;border:2px solid var(--border);background:var(--surface);font-family:'Nunito',sans-serif;font-size:.8rem;font-weight:700;color:var(--text-secondary);cursor:pointer;transition:all .25s ease;white-space:nowrap;display:flex;align-items:center;gap:6px}
.cat-tab:hover{border-color:var(--primary);color:var(--primary)}.cat-tab.active{background:linear-gradient(135deg,var(--primary),var(--accent));border-color:var(--primary);color:#fff}
#viewer3d{position:fixed;top:60px;left:0;right:0;bottom:145px;z-index:5}
#viewer3d canvas{width:100%!important;height:100%!important;display:block;cursor:grab;touch-action:none}#viewer3d canvas:active{cursor:grabbing}
.component-name-overlay{position:fixed;top:68px;left:50%;transform:translateX(-50%);z-index:20;text-align:center;pointer-events:none}
.component-name-overlay h3{font-size:clamp(1.1rem,3vw,1.5rem);font-weight:900;margin-bottom:4px}
.component-cat-badge{display:inline-block;padding:3px 14px;border-radius:20px;font-size:.72rem;font-weight:700;color:#fff;letter-spacing:.5px}
.component-counter{position:fixed;bottom:115px;left:50%;transform:translateX(-50%);z-index:20;font-size:.85rem;font-weight:700;color:var(--text-secondary);background:var(--overlay);backdrop-filter:blur(8px);padding:4px 16px;border-radius:20px;border:1px solid var(--border)}
.viewer-bottombar{position:fixed;bottom:0;left:0;right:0;height:100px;background:var(--overlay);backdrop-filter:blur(12px);border-top:1px solid var(--border);display:flex;align-items:center;justify-content:center;gap:20px;padding:0 20px;z-index:30}
.label-toggle{width:44px;height:44px;border-radius:50%;background:var(--surface);border:2px solid var(--primary);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .25s ease;box-shadow:0 4px 16px var(--shadow);color:var(--primary);font-size:1.1rem;position:relative}
.label-toggle:hover{transform:scale(1.08)}.label-toggle:active{transform:scale(.95)}
.label-toggle.off{opacity:.45;border-color:var(--border);color:var(--text-secondary)}
.info-btn{width:52px;height:52px;border-radius:50%;background:linear-gradient(135deg,var(--primary),var(--accent));border:none;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .25s ease;box-shadow:0 6px 20px var(--shadow-strong);color:#fff;font-size:1.4rem}
.info-btn:hover{transform:scale(1.1)}.info-btn:active{transform:scale(.95)}
.info-btn.active-info{box-shadow:0 0 0 4px var(--primary-light),0 6px 20px var(--shadow-strong);animation:infoPulse 1.5s ease-in-out infinite}
@keyframes infoPulse{0%,100%{box-shadow:0 0 0 4px var(--primary-light),0 6px 20px var(--shadow-strong)}50%{box-shadow:0 0 0 8px transparent,0 6px 20px var(--shadow-strong)}}
.speech-float-btn{position:fixed;bottom:112px;right:20px;z-index:35;width:44px;height:44px;border-radius:50%;background:linear-gradient(135deg,var(--primary),var(--accent));border:none;color:#fff;font-size:1.2rem;cursor:pointer;display:none;align-items:center;justify-content:center;box-shadow:0 4px 16px var(--shadow-strong);transition:all .25s ease}
.speech-float-btn.visible{display:flex}.speech-float-btn:hover{transform:scale(1.1)}
.speech-float-btn.playing{animation:speakPulse 1s ease-in-out infinite}
@keyframes speakPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.12)}}
.orbit-hint{position:fixed;bottom:112px;left:50%;transform:translateX(-50%);z-index:25;background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:8px 18px;font-size:.75rem;font-weight:700;color:var(--text-secondary);display:flex;align-items:center;gap:8px;box-shadow:0 4px 12px var(--shadow);animation:hintPulse 2s ease-in-out infinite;pointer-events:none}
@keyframes hintPulse{0%,100%{opacity:.8}50%{opacity:1}}.orbit-hint.hidden{display:none}
.dev-coord-bar{position:fixed;bottom:102px;left:12px;z-index:40;background:rgba(0,0,0,.85);color:#0f0;font-family:'Courier New',monospace;font-size:.72rem;padding:6px 14px;border-radius:10px;display:none;line-height:1.5;max-width:92vw;white-space:pre;border:1px solid rgba(0,255,0,.2)}
.dev-coord-bar.visible{display:block}
.info-popup-overlay{position:fixed;inset:0;z-index:50;display:none;pointer-events:none}
.info-popup-overlay.open{display:block}
.info-popup{position:absolute;width:370px;max-width:90vw;max-height:70vh;background:var(--surface);border:2px solid var(--primary);border-radius:20px;box-shadow:0 12px 48px var(--shadow-strong),0 0 0 1px var(--border);overflow:hidden;pointer-events:auto;display:flex;flex-direction:column;left:50%;top:50%;transform:translate(-50%,-50%)}
.info-popup-header{display:flex;align-items:center;gap:8px;padding:12px 14px 10px;cursor:move;background:linear-gradient(135deg,var(--primary),var(--accent));color:#fff;flex-shrink:0}
.info-popup-header .popup-icon{font-size:1rem;flex-shrink:0}
.info-popup-header .popup-title{flex:1;font-size:.95rem;font-weight:800;line-height:1.3}
.info-popup-header .popup-close{width:28px;height:28px;border-radius:50%;background:rgba(255,255,255,.25);border:none;display:flex;align-items:center;justify-content:center;cursor:pointer;color:#fff;font-size:.8rem;transition:all .2s;flex-shrink:0}
.info-popup-header .popup-close:hover{background:rgba(255,255,255,.5);transform:scale(1.1)}
.info-popup-body{padding:14px 16px;overflow-y:auto;flex:1;font-size:.86rem;line-height:1.7;font-weight:600;color:var(--text)}
.info-popup-body p{margin-bottom:.4rem}
.info-popup-body .bul{padding-left:1.2rem;position:relative}.info-popup-body .bul::before{content:'●';position:absolute;left:0;color:var(--primary);font-size:.55rem;top:5px}
.info-popup-footer{padding:8px 14px 12px;border-top:1px solid var(--border);flex-shrink:0;display:flex;gap:8px}
.popup-speech-btn{display:flex;align-items:center;gap:6px;padding:7px 14px;border-radius:30px;background:linear-gradient(135deg,var(--primary),var(--accent));border:none;color:#fff;font-family:'Nunito',sans-serif;font-size:.8rem;font-weight:700;cursor:pointer;transition:all .25s}
.popup-speech-btn:hover{transform:scale(1.03)}.popup-speech-btn.playing{animation:speakPulse 1s ease-in-out infinite}
.loading-overlay{position:fixed;inset:0;background:var(--bg);z-index:300;display:flex;flex-direction:column;align-items:center;justify-content:center;transition:opacity .5s ease}
.loading-overlay.hidden{opacity:0;pointer-events:none}
.loader-ring{width:56px;height:56px;border:5px solid var(--border);border-top-color:var(--primary);border-radius:50%;animation:spin .8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}.loader-text{margin-top:1rem;font-weight:700;color:var(--text-secondary);font-size:.9rem}
@media(max-width:576px){.app-logo-img{max-width:280px}.app-logo{width:90px;height:90px;border-radius:22px}.app-logo i{font-size:2.5rem}.btn-simuc{padding:.75rem 2.2rem;font-size:1rem}.viewer-bottombar{height:85px}.btn-nav{width:46px;height:46px;font-size:1.1rem}.info-btn{width:46px;height:46px;font-size:1.2rem}#viewer3d{bottom:130px}.component-counter{bottom:145px}.speech-float-btn{bottom:145px}.dev-coord-bar{bottom:132px}.comp-video-panel{width:280px;right:8px;top:110px}.info-popup{width:92vw;max-height:60vh}}
/* ============ TUTORIAL OVERLAY ============ */
.tut-overlay{position:fixed;inset:0;z-index:200;display:none;pointer-events:auto}
.tut-overlay.active{display:block}
.tut-backdrop{position:absolute;inset:0;background:rgba(0,0,0,0.65);transition:all .4s ease}
.tut-spotlight{position:absolute;border-radius:12px;box-shadow:0 0 0 9999px rgba(0,0,0,0.65);transition:all .5s cubic-bezier(.4,0,.2,1);z-index:201;pointer-events:none}
.tut-spotlight::after{content:'';position:absolute;inset:-4px;border:3px solid var(--primary);border-radius:14px;animation:tutPulse 2s ease-in-out infinite}
@keyframes tutPulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(1.03)}}
.tut-dialog{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);z-index:210;display:flex;align-items:flex-end;gap:0;max-width:700px;width:92vw;pointer-events:auto;animation:tutSlideUp .4s ease}
@keyframes tutSlideUp{from{opacity:0;transform:translateX(-50%) translateY(30px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}
.tut-char{width:140px;flex-shrink:0;position:relative;bottom:0}
.tut-char img{width:100%;height:auto;display:block;filter:drop-shadow(0 4px 16px rgba(0,0,0,0.2));animation:tutCharBounce 3s ease-in-out infinite}
@keyframes tutCharBounce{0%,100%{transform:translateY(0)}50%{transform:translateY(-6px)}}
.tut-bubble{flex:1;background:var(--surface);border:2px solid var(--primary);border-radius:20px;box-shadow:0 12px 48px rgba(0,0,0,0.25);overflow:hidden;position:relative;margin-bottom:20px}
.tut-bubble::before{content:'';position:absolute;bottom:-10px;left:40px;width:20px;height:20px;background:var(--surface);border-bottom:2px solid var(--primary);border-left:2px solid var(--primary);transform:rotate(-45deg);z-index:-1}
.tut-bubble-header{padding:10px 16px 6px;display:flex;align-items:center;gap:8px}
.tut-bubble-header .tut-name{font-weight:800;font-size:.9rem;color:var(--primary)}
.tut-bubble-header .tut-step{font-size:.65rem;font-weight:700;color:var(--text-secondary);background:var(--primary-light);padding:2px 10px;border-radius:12px;margin-left:auto}
.tut-bubble-body{padding:4px 16px 12px;font-size:.88rem;line-height:1.65;font-weight:600;color:var(--text);min-height:60px}
.tut-bubble-footer{padding:8px 16px 12px;display:flex;gap:8px;align-items:center;border-top:1px solid var(--border)}
.tut-btn{padding:7px 18px;border-radius:24px;border:2px solid var(--primary);font-family:'Nunito',sans-serif;font-size:.8rem;font-weight:700;cursor:pointer;transition:all .2s}
.tut-btn.primary{background:var(--primary);color:#fff;border-color:var(--primary)}
.tut-btn.primary:hover{opacity:.9;transform:translateY(-1px)}
.tut-btn.outline{background:transparent;color:var(--primary)}
.tut-btn.outline:hover{background:var(--primary-light)}
.tut-btn.skip{background:transparent;color:var(--text-secondary);border-color:var(--border);margin-left:auto;font-size:.72rem}
.tut-btn.skip:hover{color:var(--text);border-color:var(--text-secondary)}
.tut-typing{display:inline}
.tut-typing .cursor{display:inline-block;width:2px;height:1em;background:var(--primary);animation:tutBlink .6s infinite;vertical-align:text-bottom;margin-left:2px}
@keyframes tutBlink{0%,100%{opacity:1}50%{opacity:0}}
.tut-vo-icon{font-size:1rem;color:var(--primary);animation:tutVoWave 1s ease-in-out infinite;margin-right:4px}
@keyframes tutVoWave{0%,100%{transform:scale(1)}50%{transform:scale(1.15)}}
@media(max-width:576px){.tut-dialog{bottom:12px;gap:0}.tut-char{width:90px}.tut-bubble-body{font-size:.8rem;min-height:48px}.tut-bubble{border-radius:16px;margin-bottom:12px}}
/* VIEWER LOADING SCREEN */
.viewer-loader{position:fixed;inset:0;z-index:200;display:flex;flex-direction:column;align-items:center;justify-content:center;background:var(--bg);transition:opacity .5s ease}
.viewer-loader.hidden{opacity:0;pointer-events:none}
.viewer-loader .loader-logo{width:80px;height:80px;margin-bottom:1.5rem;animation:loaderPulse 1.5s ease-in-out infinite}
@keyframes loaderPulse{0%,100%{transform:scale(1);opacity:1}50%{transform:scale(1.08);opacity:.7}}
.viewer-loader .loader-spinner{width:40px;height:40px;border:4px solid var(--border);border-top-color:var(--primary);border-radius:50%;animation:loaderSpin .8s linear infinite;margin-bottom:1rem}
@keyframes loaderSpin{to{transform:rotate(360deg)}}
.viewer-loader .loader-text{font-size:.9rem;font-weight:700;color:var(--text-secondary);text-align:center}
.viewer-loader .loader-text span{color:var(--primary)}
/* TUTORIAL REPLAY BUTTON - distinctive */
#tutReplayBtn{background:linear-gradient(135deg,#1E88E5,#1565C0);border-color:#1E88E5;color:#fff}
#tutReplayBtn:hover{transform:scale(1.1);box-shadow:0 4px 16px rgba(30,136,229,0.4)}
</style>
</head>
<body>
<div class="pattern-bg"><div class="bg-image"></div><div class="bg-overlay"></div><div class="circuit-lines"></div><canvas class="symbols-canvas" id="symbolsCanvas"></canvas></div>
<div class="loading-overlay" id="loadingOverlay"><div class="loader-ring"></div><div class="loader-text">Memuatkan SIMuC...</div></div>
<button class="settings-toggle" id="settingsToggle" aria-label="Tetapan"><i class="bi bi-gear-fill"></i></button>
<div class="settings-panel" id="settingsPanel">
<h5><i class="bi bi-gear-fill" style="color:var(--primary)"></i> Tetapan</h5>
<div class="setting-group"><div class="setting-label">Tema Paparan</div><div class="theme-switch-wrap"><i class="bi bi-sun-fill" style="color:var(--primary);font-size:1.1rem"></i><div class="theme-switch" id="themeSwitch" role="switch" tabindex="0"><div class="toggle-knob"></div></div><i class="bi bi-moon-fill" style="font-size:1rem"></i><span class="theme-switch-label" id="themeSwitchLabel">Cerah</span></div></div>
<div class="setting-group"><div class="setting-label">Muzik Latar Belakang</div><div class="volume-control"><i class="bi bi-music-note-beamed"></i><input type="range" class="volume-slider" id="bgMusicVolume" min="0" max="100" value="30"><span class="volume-val" id="bgMusicVolVal">30%</span></div></div>
<div class="setting-group"><div class="setting-label">Kesan Bunyi</div><div class="volume-control"><i class="bi bi-volume-up-fill"></i><input type="range" class="volume-slider" id="sfxVolume" min="0" max="100" value="50"><span class="volume-val" id="sfxVolVal">50%</span></div></div>
<div class="setting-group"><div class="setting-label">Putar Automatik Model 3D</div><div class="theme-switch-wrap"><div class="theme-switch dark" id="autoRotateSwitch" role="switch" tabindex="0"><div class="toggle-knob"></div></div><span class="theme-switch-label" id="autoRotateLabel">Hidup</span></div></div>
<div class="setting-group" id="scaleGroup" style="display:none"><div class="setting-label">Skala Model 3D</div><div class="volume-control"><i class="bi bi-arrows-angle-expand"></i><input type="range" class="volume-slider" id="modelScaleSlider" min="1" max="200" value="100"><span class="volume-val" id="modelScaleVal">100%</span></div></div>
<div class="setting-group"><div class="setting-label">Mod Pembangun (Dev)</div><div class="theme-switch-wrap"><div class="theme-switch" id="devModeSwitch" role="switch" tabindex="0"><div class="toggle-knob"></div></div><span class="theme-switch-label" id="devModeLabel">Mati</span></div></div>
<div class="setting-group"><button class="save-config-btn" id="saveConfigBtn" onclick="saveConfig()"><i class="bi bi-download"></i> Simpan Config</button></div>
<div class="setting-group"><button class="save-config-btn" style="background:linear-gradient(135deg,#1E88E5,#1565C0)" onclick="playClick();sessionStorage.removeItem('simuc_tut_shown');if(screen==='viewer'){startTutorial()}else{goToViewer()}"><i class="bi bi-mortarboard-fill"></i> Mulakan Tutorial</button></div>
</div>
<div class="screen active" id="titleScreen"><div class="title-content">
<img src="./images/logo.png" alt="SIMuC Logo" class="app-logo-img">
<!--div class="title-abbr">SIMuC</div-->
<div class="title-subject">Reka Bentuk dan Teknologi (RBT)<br><small>Tahun 4 – Tahun 6 · 10 – 12 Tahun</small></div>
<!--div class="title-target">Tahun 4 – Tahun 6 · 10 – 12 Tahun</div-->
<button class="btn-simuc" onclick="playClick();goToMenu()"><i class="bi bi-play-fill"></i> Mula</button><br><br>
<div class="title-version" id="versionBadge">v1.3.0</div><br>
</div></div>
<div class="screen" id="menuScreen"><div class="menu-content">
<div class="menu-left">
<!--img src="./images/logo.png" alt="SIMuC" class="menu-logo"-->
<div class="menu-header"><h2>Menu Utama</h2><p>Pilih modul untuk mula belajar</p></div>
<div class="menu-cards-wrap">
<div class="menu-card" onclick="playClick();goToViewer()"><div class="menu-card-num">1</div><div class="menu-card-body"><h4>Pengenalan Kepada Perkakasan</h4><p>Kenali komponen elektronik secara 3D</p></div><i class="bi bi-chevron-right menu-card-arrow"></i></div>
<div class="menu-card" onclick="playClick();window.open('circuit-v1.2.0.html','_self');"><div class="menu-card-num">2</div><div class="menu-card-body"><h4>Pembangunan Litar</h4><p>Belajar membina litar langkah demi langkah</p></div><i class="bi bi-chevron-right menu-card-arrow"></i></div>
<div class="menu-card" onclick="playClick();window.open('program-v1.1.0.html','_self');"><div class="menu-card-num">3</div><div class="menu-card-body"><h4>Pengekodan Mikropengawal</h4><p>Tulis kod Arduino dan uji secara maya</p></div><i class="bi bi-chevron-right menu-card-arrow"></i></div>
<div class="menu-card" onclick="playClick();window.open('https://pdisk-ext.onrender.com/');"><div class="menu-card-num">4</div><div class="menu-card-body"><h4>Aplikasi Sistem</h4><p>Aplikasikan sistem mikropengawal di lokasi</p></div><i class="bi bi-chevron-right menu-card-arrow"></i></div>
</div>
<div class="menu-back-row"><button class="btn-simuc-outline" onclick="playClick();goToTitle()"><i class="bi bi-arrow-left"></i> Kembali</button></div>
</div>
<div class="menu-right">
<div class="video-player" id="videoPlayer" onclick="toggleVideo()">
<img src="./images/video.png" alt="Video Intro" class="video-thumb" id="videoThumb">
<video id="introVideo" src="./videos/intro.mp4" preload="metadata" playsinline></video>
<div class="video-frame"></div>
<span class="video-badge">VIDEO</span>
<button class="video-play-btn" id="videoPlayBtn"><i class="bi bi-play-fill"></i></button>
<span class="video-duration" id="videoDuration">0:00</span>
</div>
<div class="video-caption">Pengenalan Kepada Simulator Interaktif Mikropengawal (SIMuC)</div>
</div>
</div></div>
<div class="screen" id="viewerScreen">
<div class="viewer-topbar"><div class="topbar-back" onclick="playClick();goToMenu()"><i class="bi bi-arrow-left"></i></div><div class="topbar-title">Pengenalan Kepada Perkakasan</div><div style="width:40px"></div></div>
<button class="comp-video-btn" id="compVideoBtn" onclick="toggleCompVideo()" title="Video Komponen"><i class="bi bi-play-circle-fill"></i></button>
<button class="comp-info-btn" id="compInfoBtn" onclick="toggleCompInfo()" title="Maklumat Komponen"><i class="bi bi-info-circle-fill"></i></button>
<div class="comp-info-panel" id="compInfoPanel">
<div class="cip-header">
<i class="bi bi-cpu cip-icon"></i>
<div class="cip-title" id="cipTitle">-</div>
<span class="cip-cat" id="cipCat">-</span>
<button class="cip-close" onclick="closeCompInfo()"><i class="bi bi-x-lg"></i></button>
</div>
<div class="cip-body" id="cipBody"><p style="color:var(--text-secondary)">Pilih komponen untuk lihat maklumat.</p></div>
<div class="cip-footer" id="cipFooter" style="display:none">
<button onclick="playClick();speakCompInfo()"><i class="bi bi-volume-up-fill"></i> Dengar</button>
</div>
</div>
<div class="comp-video-panel" id="compVideoPanel">
<img src="./images/components.png" alt="Video" class="cvp-thumb" id="cvpThumb" onclick="playCompVideo()">
<video id="compVideo" src="./videos/components.mp4" preload="metadata" playsinline></video>
<span class="cvp-badge">VIDEO</span>
<button class="cvp-play" onclick="playCompVideo()"><i class="bi bi-play-fill"></i></button>
<button class="cvp-close" onclick="closeCompVideo()"><i class="bi bi-x-lg"></i></button>
<div class="cvp-caption">Video Pengenalan Komponen</div>
</div>
<div class="category-tabs" id="categoryTabs"></div>
<div class="component-name-overlay"><h3 id="componentName">-</h3><span class="component-cat-badge" id="componentCatBadge">-</span></div>
<div id="viewer3d"></div>
<div class="orbit-hint" id="orbitHint"><i class="bi bi-arrows-move"></i> Seret untuk putar · Cubit untuk zum</div>
<div class="component-counter" id="componentCounter">1 / 10</div>
<div class="dev-coord-bar" id="devCoordBar">DEV MODE</div>
<button class="speech-float-btn" id="speechFloatBtn" onclick="playClick();toggleSpeech()" title="Dengar Penerangan"><i class="bi bi-volume-up-fill"></i></button>
<div class="info-popup-overlay" id="infoPopupOverlay">
<div class="info-popup" id="infoPopup">
<div class="info-popup-header" id="infoPopupHeader">
<i class="bi bi-eye-fill popup-icon"></i>
<div class="popup-title" id="popupTitle">-</div>
<button class="popup-close" id="popupCloseBtn"><i class="bi bi-x-lg"></i></button>
</div>
<div class="info-popup-body" id="popupBody"></div>
<div class="info-popup-footer" id="popupFooter">
<button class="popup-speech-btn" id="popupSpeechBtn" onclick="playClick();playPopupSpeech()"><i class="bi bi-volume-up-fill"></i> Dengar</button>
</div>
</div>
</div>
<div class="viewer-bottombar">
<button class="btn-nav" id="prevBtn" onclick="playClick();navigateComponent(-1)"><i class="bi bi-chevron-left"></i></button>
<button class="label-toggle" id="labelToggleBtn" onclick="playClick();toggleLabelsVis()" title="Togol Label"><i class="bi bi-tag-fill"></i></button>
<button class="info-btn" id="infoBtn" onclick="playClick();toggleInfo3D()"><i class="bi bi-info-circle-fill"></i></button>
<button class="label-toggle" id="tutReplayBtn" onclick="playClick();sessionStorage.removeItem('simuc_tut_shown');startTutorial()" title="Tutorial"><i class="bi bi-mortarboard-fill"></i></button>
<button class="btn-nav" id="nextBtn" onclick="playClick();navigateComponent(1)"><i class="bi bi-chevron-right"></i></button>
</div>
<!-- VIEWER LOADING SCREEN -->
<div class="viewer-loader" id="viewerLoader">
<img src="./images/logo.png" alt="SIMuC" class="loader-logo" style="width:120px;height:auto">
<div class="loader-spinner"></div>
<div class="loader-text">Memuatkan <span>Komponen 3D</span>...</div>
</div>
<!-- TUTORIAL OVERLAY -->
<div class="tut-overlay" id="tutOverlay">
<div class="tut-backdrop" id="tutBackdrop"></div>
<div class="tut-spotlight" id="tutSpotlight"></div>
<div class="tut-dialog" id="tutDialog">
<div class="tut-char"><img src="./images/mascot.png" alt="Maskot" id="tutCharImg" onerror="this.onerror=null;this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 140 200%22%3E%3Crect width=%22140%22 height=%22200%22 rx=%2220%22 fill=%22%23F5A623%22/%3E%3Ctext x=%2270%22 y=%22110%22 text-anchor=%22middle%22 fill=%22white%22 font-size=%2260%22 font-family=%22sans-serif%22%3E🤖%3C/text%3E%3C/svg%3E'"></div>
<div class="tut-bubble">
<div class="tut-bubble-header">
<span class="tut-name" id="tutName">Cikgu Mikro</span>
<span class="tut-step" id="tutStepBadge">1 / 8</span>
</div>
<div class="tut-bubble-body" id="tutText"></div>
<div class="tut-bubble-footer">
<button class="tut-btn outline" id="tutPrevBtn" onclick="playClick();tutPrev()"><i class="bi bi-chevron-left"></i> Kembali</button>
<button class="tut-btn primary" id="tutNextBtn" onclick="playClick();tutNext()">Seterusnya <i class="bi bi-chevron-right"></i></button>
<button class="tut-btn skip" id="tutSkipBtn" onclick="playClick();tutEnd()">Langkau <i class="bi bi-x"></i></button>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.bundle.min.js"></script>
<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://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js"></script>
<script>
const D2R=Math.PI/180,deg=d=>d*D2R,setRot=(o,r)=>o.rotation.set(deg(r[0]),deg(r[1]),deg(r[2]));
const isMobile='ontouchstart' in window||navigator.maxTouchPoints>0;
let CFG=null,theme='light',screen='title',catFilter='all',compIdx=0,comps=[];
let scene,cam,ren,ctrl,model=null,labels=[],infoIcon=null,info3D=null,infoVis=false,anim=null;
let bgm=null,sfxVol=.5,bgmVol=.3,spAudio=null,speaking=false;
let devMode=false,dragging=null,dragType=null,rc=new THREE.Raycaster(),mv=new THREE.Vector2();
let popupInfo=null,ptrT=0,ptrX=0,ptrY=0;
let modelBaseScale=[1,1,1],modelScaleMult=1;
let info3DDragging=false,labelsVisible=true;
/* AUDIO */
function playClick(){try{const c=new(window.AudioContext||window.webkitAudioContext)(),o=c.createOscillator(),g=c.createGain();o.connect(g);g.connect(c.destination);o.frequency.setValueAtTime(800,c.currentTime);o.frequency.exponentialRampToValueAtTime(600,c.currentTime+.05);g.gain.setValueAtTime(sfxVol*.3,c.currentTime);g.gain.exponentialRampToValueAtTime(.001,c.currentTime+.08);o.start(c.currentTime);o.stop(c.currentTime+.08)}catch(e){}}
function startBgm(){if(bgm||!CFG?.audio?.backgroundMusic?.src)return;const a=new Audio(CFG.audio.backgroundMusic.src);a.loop=true;a.volume=bgmVol;a.play().catch(()=>{});bgm=a}
function stopSpeech(){speaking=false;document.querySelectorAll('.playing').forEach(b=>b.classList.remove('playing'));if(spAudio){spAudio.pause();spAudio=null}if('speechSynthesis' in window)speechSynthesis.cancel()}
function doSpeak(src,text,btn){stopSpeech();speaking=true;if(btn)btn.classList.add('playing');if(src){spAudio=new Audio(src);spAudio.volume=sfxVol;spAudio.onended=stopSpeech;spAudio.onerror=()=>tts(text,btn);spAudio.play().catch(()=>tts(text,btn))}else tts(text,btn)}
function tts(text,btn){if('speechSynthesis' in window){speechSynthesis.cancel();const u=new SpeechSynthesisUtterance(text);u.lang='ms-MY';u.rate=.9;u.onend=stopSpeech;speechSynthesis.speak(u)}else stopSpeech()}
function toggleSpeech(){const c=comps[compIdx];if(!c)return;if(speaking){stopSpeech();return}doSpeak(c.info?.speech,c.info?.body||c.info?.title||c.name,document.getElementById('speechFloatBtn'))}
function playPopupSpeech(){if(!popupInfo)return;if(speaking){stopSpeech();return}doSpeak(popupInfo.speech,popupInfo.body||popupInfo.title,document.getElementById('popupSpeechBtn'))}
/* THEME */
function setTheme(t){theme=t;document.documentElement.setAttribute('data-theme',t);document.getElementById('themeSwitch').classList.toggle('dark',t==='dark');document.getElementById('themeSwitchLabel').textContent=t==='dark'?'Gelap':'Cerah';if(scene){scene.background=new THREE.Color(t==='dark'?0x151210:0xF2C97D);rebuildGrid();const c=comps[compIdx];if(c){clearLabels();addLabels(c);addInfoIcon(c);if(infoVis)showInfo3D(c)}}}
function rebuildGrid(){scene.children.filter(c=>c.type==='GridHelper'||c.userData.gnd).forEach(c=>scene.remove(c));const gc=theme==='dark'?0x66563E:0xD4A854;const grid=new THREE.GridHelper(20,20,gc,gc);grid.position.y=-1;scene.add(grid);const gm=new THREE.Mesh(new THREE.PlaneGeometry(30,30),new THREE.ShadowMaterial({opacity:.08}));gm.rotation.x=-Math.PI/2;gm.position.y=-1;gm.receiveShadow=true;gm.userData.gnd=true;scene.add(gm)}
/* SETTINGS */
function initSettings(){
const tog=document.getElementById('settingsToggle'),pan=document.getElementById('settingsPanel');
tog.addEventListener('click',()=>{playClick();pan.classList.toggle('open');tog.classList.toggle('open')});
document.addEventListener('click',e=>{if(!pan.contains(e.target)&&!tog.contains(e.target)){pan.classList.remove('open');tog.classList.remove('open')}});
document.getElementById('themeSwitch').addEventListener('click',()=>{playClick();setTheme(theme==='light'?'dark':'light')});
document.getElementById('bgMusicVolume').addEventListener('input',e=>{const v=+e.target.value;document.getElementById('bgMusicVolVal').textContent=v+'%';bgmVol=v/100;if(bgm)bgm.volume=bgmVol});
document.getElementById('sfxVolume').addEventListener('input',e=>{const v=+e.target.value;document.getElementById('sfxVolVal').textContent=v+'%';sfxVol=v/100});
document.getElementById('autoRotateSwitch').addEventListener('click',()=>{playClick();const s=document.getElementById('autoRotateSwitch');s.classList.toggle('dark');const on=s.classList.contains('dark');document.getElementById('autoRotateLabel').textContent=on?'Hidup':'Mati';if(ctrl)ctrl.autoRotate=on});
document.getElementById('devModeSwitch').addEventListener('click',()=>{playClick();const s=document.getElementById('devModeSwitch');s.classList.toggle('dark');devMode=s.classList.contains('dark');document.getElementById('devModeLabel').textContent=devMode?'Hidup':'Mati';document.getElementById('devCoordBar').style.display=devMode?'':'none';if(comps[compIdx])loadComp(compIdx)});
document.getElementById('modelScaleSlider').addEventListener('input',e=>{
const v=+e.target.value;document.getElementById('modelScaleVal').textContent=v+'%';
modelScaleMult=v/100;
if(model){model.scale.set(modelBaseScale[0]*modelScaleMult,modelBaseScale[1]*modelScaleMult,modelBaseScale[2]*modelScaleMult)}
// Update config in memory
const c=comps[compIdx];if(c)c.model.scale=[modelBaseScale[0]*modelScaleMult,modelBaseScale[1]*modelScaleMult,modelBaseScale[2]*modelScaleMult];
});
}
/* SAVE CONFIG */
function saveConfig(){
const json=JSON.stringify(CFG,null,2);
const blob=new Blob([json],{type:'application/json'});
const url=URL.createObjectURL(blob);
const a=document.createElement('a');a.href=url;a.download='config.json';a.click();
URL.revokeObjectURL(url);
const btn=document.getElementById('saveConfigBtn');
btn.classList.add('saved');btn.innerHTML='<i class="bi bi-check-lg"></i> Disimpan!';
setTimeout(()=>{btn.classList.remove('saved');btn.innerHTML='<i class="bi bi-download"></i> Simpan Config'},2000);
}
/* COMPONENT VIDEO PLAYER */
function toggleCompVideo(){
const panel=document.getElementById('compVideoPanel');
if(panel.classList.contains('open')){closeCompVideo()}
else{panel.classList.add('open')}
}
function playCompVideo(){
const panel=document.getElementById('compVideoPanel');
const video=document.getElementById('compVideo');
if(panel.classList.contains('playing')){
video.pause();panel.classList.remove('playing');
} else {
video.play();panel.classList.add('playing');
}
}
function closeCompVideo(){
const panel=document.getElementById('compVideoPanel');
const video=document.getElementById('compVideo');
video.pause();video.currentTime=0;
panel.classList.remove('open','playing');
}
/* COMPONENT INFO PANEL */
function toggleCompInfo(){
const panel=document.getElementById('compInfoPanel');
const btn=document.getElementById('compInfoBtn');
if(panel.classList.contains('open')){
closeCompInfo();
} else {
updateCompInfoPanel();
panel.classList.add('open');
btn.classList.add('active');
}
}
function updateCompInfoPanel(){
const c=comps[compIdx];if(!c)return;
const cat=CFG.categories.find(x=>x.id===c.category);
document.getElementById('cipTitle').textContent=c.info?.title||c.name;
document.getElementById('cipCat').textContent=cat?.name||c.category||'-';
const body=c.info?.body||'';
document.getElementById('cipBody').innerHTML=body?fmtBodyCip(body):'<p style="color:var(--text-secondary)">Maklumat belum tersedia untuk komponen ini.</p>';
document.getElementById('cipFooter').style.display=(c.info?.speech||c.info?.body)?'':'none';
}
function fmtBodyCip(t){return t.split('\n').map(l=>{l=l.trim();if(!l)return'';if(l.startsWith('•'))return'<p class="bul">'+l.substring(1).trim()+'</p>';return'<p>'+l+'</p>'}).join('')}
function closeCompInfo(){
document.getElementById('compInfoPanel').classList.remove('open');
document.getElementById('compInfoBtn').classList.remove('active');
}
function speakCompInfo(){
const c=comps[compIdx];if(!c)return;
if(speaking){stopSpeech();return}
doSpeak(c.info?.speech,c.info?.body||c.info?.title||c.name,document.getElementById('speechFloatBtn'));
}
/* NAV */
function sw(f,t){const fe=document.getElementById(f+'Screen'),te=document.getElementById(t+'Screen');if(fe){fe.classList.add('fade-out');setTimeout(()=>fe.classList.remove('active','fade-out'),400)}setTimeout(()=>{if(te)te.classList.add('active')},200);screen=t}
function goToTitle(){sw(screen,'title');disposeViewer();pauseIntroVideo()}
function goToMenu(){startBgm();
// Hide viewer, ensure menu is visible
if(tutActive)tutEnd();
document.getElementById('viewerScreen').classList.remove('active');
document.getElementById('menuScreen').classList.add('active');
document.getElementById('titleScreen').classList.remove('active');
screen='menu';
disposeViewer();closeCompVideo();closeCompInfo();
}
function goToViewer(){pauseIntroVideo();
// Show viewer with loading screen
document.getElementById('viewerScreen').classList.add('active');
const loader=document.getElementById('viewerLoader');
loader.classList.remove('hidden');
screen='viewer';
setTimeout(()=>{
initViewer();buildTabs();filterComps('all');
// Hide loader after 3D is ready
setTimeout(()=>{
loader.classList.add('hidden');
// Launch tutorial on first visit this session
if(!sessionStorage.getItem('simuc_tut_shown')){
setTimeout(startTutorial,600);
}
},800);
},300)
}
function buildTabs(){const el=document.getElementById('categoryTabs');let h='<button class="cat-tab active" data-cat="all" onclick="playClick();filterComps(\'all\')"><span>📋</span> Semua</button>';CFG.categories.forEach(c=>{h+='<button class="cat-tab" data-cat="'+c.id+'" onclick="playClick();filterComps(\''+c.id+'\')"><span>'+c.icon+'</span> '+c.name+'</button>'});el.innerHTML=h}
function filterComps(id){catFilter=id;comps=id==='all'?[...CFG.components]:CFG.components.filter(c=>c.category===id);compIdx=0;document.querySelectorAll('.cat-tab').forEach(t=>t.classList.toggle('active',t.dataset.cat===id));loadComp(0)}
function navigateComponent(d){const n=compIdx+d;if(n<0||n>=comps.length)return;loadComp(n)}
document.addEventListener('keydown',e=>{if(screen!=='viewer')return;if(tutActive){if(e.key==='Escape')tutEnd();if(e.key==='ArrowRight'||e.key===' ')tutNext();if(e.key==='ArrowLeft')tutPrev();return}if(e.key==='ArrowLeft'){playClick();navigateComponent(-1)}if(e.key==='ArrowRight'){playClick();navigateComponent(1)}if(e.key==='i'||e.key==='I'){playClick();toggleInfo3D()}if(e.key==='Escape')closeAllPanels()});
function closeAllPanels(){hideInfo3D();closePopup();document.getElementById('infoBtn').classList.remove('active-info')}
function toggleLabelsVis(){
labelsVisible=!labelsVisible;
document.getElementById('labelToggleBtn').classList.toggle('off',!labelsVisible);
labels.forEach(lo=>{
if(lo.sprite)lo.sprite.visible=labelsVisible;
if(lo.lineMesh)lo.lineMesh.visible=labelsVisible;
if(lo.dot)lo.dot.visible=labelsVisible;
if(lo.hitbox)lo.hitbox.visible=labelsVisible;
});
}
/* ISO CAMERA */
function setIsoView(){const iso=CFG.viewer3D.isometricView||{distance:10,angleX:30,angleY:45};const d=iso.distance,ax=deg(iso.angleX),ay=deg(iso.angleY);cam.position.set(d*Math.cos(ax)*Math.sin(ay),d*Math.sin(ax),d*Math.cos(ax)*Math.cos(ay));ctrl.target.set(0,.5,0);ctrl.update()}
/* THREE.JS */
function initViewer(){
const el=document.getElementById('viewer3d');
scene=new THREE.Scene();scene.background=new THREE.Color(theme==='dark'?0x151210:0xF2C97D);
const c=CFG.viewer3D.camera;
cam=new THREE.PerspectiveCamera(c.fov,el.clientWidth/el.clientHeight,c.near,c.far);
ren=new THREE.WebGLRenderer({antialias:true});ren.setSize(el.clientWidth,el.clientHeight);ren.setPixelRatio(Math.min(devicePixelRatio,2));ren.shadowMap.enabled=true;ren.shadowMap.type=THREE.PCFSoftShadowMap;ren.outputEncoding=THREE.sRGBEncoding;ren.toneMapping=THREE.ACESFilmicToneMapping;ren.toneMappingExposure=1.2;el.appendChild(ren.domElement);
const cc=CFG.viewer3D.controls;
ctrl=new THREE.OrbitControls(cam,ren.domElement);ctrl.enableDamping=cc.enableDamping;ctrl.dampingFactor=cc.dampingFactor;ctrl.minDistance=cc.minDistance;ctrl.maxDistance=cc.maxDistance;ctrl.enablePan=cc.enablePan;
ctrl.autoRotate=true;ctrl.autoRotateSpeed=cc.autoRotateSpeed||1;
ctrl.target.set(0,.5,0);
// Touch gestures are handled natively by OrbitControls
ctrl.touches={ONE:THREE.TOUCH.ROTATE,TWO:THREE.TOUCH.DOLLY_PAN};
const l=CFG.viewer3D.lighting;scene.add(new THREE.AmbientLight(0xffffff,l.ambientIntensity));const dl=new THREE.DirectionalLight(0xffffff,l.directionalIntensity);dl.position.set(...l.directionalPosition);dl.castShadow=true;dl.shadow.mapSize.set(1024,1024);scene.add(dl);scene.add(new THREE.DirectionalLight(0xFFE4C4,.3));scene.add(new THREE.HemisphereLight(0xFFF8EE,0xD4A37A,.4));
rebuildGrid();
document.getElementById('scaleGroup').style.display='block';
window.addEventListener('resize',onResize);
ctrl.addEventListener('start',()=>document.getElementById('orbitHint').classList.add('hidden'));
ren.domElement.addEventListener('pointerdown',onPtrDown);ren.domElement.addEventListener('pointermove',onPtrMove);ren.domElement.addEventListener('pointerup',onPtrUp);
tick();
}
function onResize(){const el=document.getElementById('viewer3d');if(!el||!cam||!ren)return;cam.aspect=el.clientWidth/el.clientHeight;cam.updateProjectionMatrix();ren.setSize(el.clientWidth,el.clientHeight)}
function tick(){anim=requestAnimationFrame(tick);if(ctrl)ctrl.update();if(infoIcon)infoIcon.rotation.y+=.02;if(info3D&&infoVis)info3D.lookAt(cam.position);if(ren&&scene&&cam)ren.render(scene,cam)}
function disposeViewer(){stopSpeech();closePopup();if(anim)cancelAnimationFrame(anim);window.removeEventListener('resize',onResize);if(ren){ren.domElement.removeEventListener('pointerdown',onPtrDown);ren.domElement.removeEventListener('pointermove',onPtrMove);ren.domElement.removeEventListener('pointerup',onPtrUp);ren.dispose();document.getElementById('viewer3d').innerHTML=''}scene=null;cam=null;ren=null;ctrl=null;model=null;labels=[];infoIcon=null;info3D=null;infoVis=false;document.getElementById('infoBtn').classList.remove('active-info');document.getElementById('speechFloatBtn').classList.remove('visible');document.getElementById('scaleGroup').style.display='none'}
/* POINTER EVENTS */
function mVec(e){const r=ren.domElement.getBoundingClientRect();return new THREE.Vector2(((e.clientX-r.left)/r.width)*2-1,-((e.clientY-r.top)/r.height)*2+1)}
function onPtrDown(e){
ptrT=Date.now();ptrX=e.clientX;ptrY=e.clientY;
mv.copy(mVec(e));rc.setFromCamera(mv,cam);
// Check 3D info panel drag
if(info3D&&infoVis){const hI=rc.intersectObject(info3D,true);if(hI.length){info3DDragging=true;ctrl.enabled=false;e.preventDefault();return}}
if(!devMode)return;
const dots=labels.filter(l=>l.dot).map(l=>l.dot);
const hD=rc.intersectObjects(dots);
if(hD.length){dragging=hD[0].object;dragType='anchor';ctrl.enabled=false;e.preventDefault();return}
const hbs=labels.filter(l=>l.hitbox).map(l=>l.hitbox);
const hL=rc.intersectObjects(hbs);
if(hL.length){dragging=hL[0].object;dragType='label';ctrl.enabled=false;e.preventDefault();return}
}
function onPtrMove(e){
// 3D info panel drag
if(info3DDragging&&info3D){
mv.copy(mVec(e));rc.setFromCamera(mv,cam);
const pl=new THREE.Plane(new THREE.Vector3(0,0,1).applyQuaternion(cam.quaternion),0);
pl.constant=-pl.normal.dot(info3D.position);
const pt=new THREE.Vector3();
if(rc.ray.intersectPlane(pl,pt)){info3D.position.copy(pt)}
return;
}
if(!devMode||!dragging)return;
mv.copy(mVec(e));rc.setFromCamera(mv,cam);
const pl=new THREE.Plane(new THREE.Vector3(0,0,1).applyQuaternion(cam.quaternion),0);
pl.constant=-pl.normal.dot(dragging.position);
const pt=new THREE.Vector3();
if(rc.ray.intersectPlane(pl,pt)){
dragging.position.copy(pt);
const lo=dragType==='anchor'?labels.find(l=>l.dot===dragging):labels.find(l=>l.hitbox===dragging);
if(!lo)return;
if(dragType==='label'&&lo.sprite)lo.sprite.position.copy(pt);
if(lo.lineMesh){scene.remove(lo.lineMesh);const col=theme==='dark'?0xD4A060:0x7A5A1A;lo.lineMesh=makeLineMesh(lo.dot.position,lo.sprite.position,col);scene.add(lo.lineMesh)}
const idx=labels.indexOf(lo);const ld=comps[compIdx]?.labels?.[idx];
}
}
function onPtrUp(e){
const dt=Date.now()-ptrT,dx=Math.abs(e.clientX-ptrX),dy=Math.abs(e.clientY-ptrY);
const isClick=dt<300&&dx<10&&dy<10;
// End 3D info panel drag
if(info3DDragging){info3DDragging=false;ctrl.enabled=true;if(!isClick)return}
if(dragging&&devMode){
const p=dragging.position;const lo=dragType==='anchor'?labels.find(l=>l.dot===dragging):labels.find(l=>l.hitbox===dragging);
const idx=labels.indexOf(lo);const ld=comps[compIdx]?.labels?.[idx];const ts=dragType==='anchor'?'anchor':'position';
if(!isClick){
console.log('"'+(ld?.text)+'" '+ts+': ['+p.x.toFixed(3)+', '+p.y.toFixed(3)+', '+p.z.toFixed(3)+']');
// Update config in memory
if(ld){if(ts==='anchor')ld.anchor=[+p.x.toFixed(3),+p.y.toFixed(3),+p.z.toFixed(3)];else ld.position=[+p.x.toFixed(3),+p.y.toFixed(3),+p.z.toFixed(3)]}
dragging=null;dragType=null;ctrl.enabled=true;return;
}
dragging=null;dragType=null;ctrl.enabled=true;
}
if(!isClick)return;
mv.copy(mVec(e));rc.setFromCamera(mv,cam);
// Info icon click
if(infoIcon){const h=rc.intersectObject(infoIcon,true);if(h.length){playClick();closePopup();toggleInfo3D();return}}
// 3D info panel click - open as popup (the eye-popup icon area or on mobile any click)
if(info3D&&infoVis){const hP=rc.intersectObject(info3D,true);if(hP.length){playClick();openComponentPopup();return}}
// Anchor dots
const dots=labels.filter(l=>l.dot).map(l=>l.dot);
const hD=rc.intersectObjects(dots);
if(hD.length){const lo=labels.find(l=>l.dot===hD[0].object);const idx=labels.indexOf(lo);const ld=comps[compIdx]?.labels?.[idx];if(ld){playClick();closeAllPanels();openPopup(ld)}return}
// Label hitboxes
const hbs=labels.filter(l=>l.hitbox).map(l=>l.hitbox);
const hL=rc.intersectObjects(hbs);
if(hL.length){const lo=labels.find(l=>l.hitbox===hL[0].object);const idx=labels.indexOf(lo);const ld=comps[compIdx]?.labels?.[idx];if(ld){playClick();closeAllPanels();openPopup(ld)}return}
}
/* INFO POPUP */
function openPopup(ld){
stopSpeech();const info=ld.info;const ov=document.getElementById('infoPopupOverlay'),pp=document.getElementById('infoPopup');
if(!info){document.getElementById('popupTitle').textContent=ld.text;document.getElementById('popupBody').innerHTML='<p style="color:var(--text-secondary)">Maklumat terperinci belum tersedia.</p>';document.getElementById('popupFooter').style.display='none';popupInfo=null}
else{document.getElementById('popupTitle').textContent=info.title||ld.text;document.getElementById('popupBody').innerHTML=fmtBody(info.body||'');document.getElementById('popupFooter').style.display=(info.speech||info.body)?'flex':'none';popupInfo=info}
pp.style.left='50%';pp.style.top='50%';pp.style.transform='translate(-50%,-50%)';ov.classList.add('open');
}
function openComponentPopup(){
// Open main component info as UI popup
const c=comps[compIdx];if(!c||!c.info)return;
stopSpeech();
document.getElementById('popupTitle').textContent=c.info.title||c.name;
document.getElementById('popupBody').innerHTML=fmtBody(c.info.body||'');
document.getElementById('popupFooter').style.display=(c.info.speech||c.info.body)?'flex':'none';
popupInfo=c.info;
const pp=document.getElementById('infoPopup');
pp.style.left='50%';pp.style.top='50%';pp.style.transform='translate(-50%,-50%)';
document.getElementById('infoPopupOverlay').classList.add('open');
}
function closePopup(){document.getElementById('infoPopupOverlay').classList.remove('open');stopSpeech();popupInfo=null}
function fmtBody(t){return t.split('\n').map(l=>{l=l.trim();if(!l)return'';if(l.startsWith('•'))return'<p class="bul">'+l.substring(1).trim()+'</p>';return'<p>'+l+'</p>'}).join('')}
document.getElementById('popupCloseBtn').addEventListener('click',()=>{playClick();closePopup()});
// Draggable popup (pointer + touch)
(function(){let d=false,ox=0,oy=0;const h=document.getElementById('infoPopupHeader'),p=document.getElementById('infoPopup');
h.addEventListener('pointerdown',e=>{if(e.target.closest('.popup-close'))return;d=true;const r=p.getBoundingClientRect();ox=e.clientX-r.left;oy=e.clientY-r.top;p.style.transform='none';p.style.left=r.left+'px';p.style.top=r.top+'px';h.setPointerCapture(e.pointerId)});
h.addEventListener('pointermove',e=>{if(!d)return;let nx=e.clientX-ox,ny=e.clientY-oy;nx=Math.max(0,Math.min(window.innerWidth-50,nx));ny=Math.max(0,Math.min(window.innerHeight-50,ny));p.style.left=nx+'px';p.style.top=ny+'px'});
h.addEventListener('pointerup',()=>{d=false});h.addEventListener('pointercancel',()=>{d=false})})();
/* LOAD COMPONENT */
function loadComp(idx){
if(!comps.length)return;compIdx=idx;const c=comps[idx];
document.getElementById('componentName').textContent=c.name;
const cat=CFG.categories.find(x=>x.id===c.category);
const b=document.getElementById('componentCatBadge');b.textContent=cat?cat.name:c.category;b.style.background=cat?.color||'var(--primary)';
document.getElementById('componentCounter').textContent=(idx+1)+' / '+comps.length;
document.getElementById('prevBtn').disabled=idx===0;document.getElementById('nextBtn').disabled=idx===comps.length-1;
// Update floating info panel if open
if(document.getElementById('compInfoPanel').classList.contains('open'))updateCompInfoPanel();
document.getElementById('orbitHint').classList.remove('hidden');
document.getElementById('speechFloatBtn').classList.add('visible');
closeAllPanels();stopSpeech();clearModel();clearLabels();
labelsVisible=true;document.getElementById('labelToggleBtn').classList.remove('off');
// Reset scale slider
modelBaseScale=[...c.model.scale];modelScaleMult=1;
document.getElementById('modelScaleSlider').value=100;document.getElementById('modelScaleVal').textContent='100%';
setIsoView();
const loader=new THREE.GLTFLoader();
loader.load(c.model.src,gltf=>{const m=gltf.scene;m.position.set(...c.model.position);setRot(m,c.model.rotation);m.scale.set(...c.model.scale);m.traverse(ch=>{if(ch.isMesh){ch.castShadow=true;ch.receiveShadow=true}});scene.add(m);model=m;addLabels(c);addInfoIcon(c)},undefined,()=>{const p=makePlaceholder(c);scene.add(p);model=p;addLabels(c);addInfoIcon(c)});
}
function clearModel(){if(model&&scene)scene.remove(model);model=null}
function clearLabels(){labels.forEach(lo=>{['sprite','lineMesh','dot','hitbox'].forEach(k=>{if(lo[k])scene.remove(lo[k])})});labels=[];if(infoIcon&&scene){scene.remove(infoIcon);infoIcon=null}if(info3D&&scene){scene.remove(info3D);info3D=null;infoVis=false}}
/* LABELS - depthTest:true so they go behind model */
function addLabels(c){
if(!c.labels)return;const lineCol=theme==='dark'?0xD4A060:0x7A5A1A;
c.labels.forEach(ld=>{
const fs=ld.fontSize||8;
const lPos=new THREE.Vector3(...ld.position);
const aPos=new THREE.Vector3(...(ld.anchor||ld.position));
const hasInfo=!!ld.info;
const sprite=makeLabel(ld.text,fs,hasInfo);sprite.position.copy(lPos);scene.add(sprite);
const hbW=sprite.scale.x*1.2,hbH=sprite.scale.y*1.2;
const hitbox=new THREE.Mesh(new THREE.PlaneGeometry(hbW,hbH),new THREE.MeshBasicMaterial({visible:false,side:THREE.DoubleSide}));
hitbox.position.copy(lPos);scene.add(hitbox);
const lineMesh=makeLineMesh(aPos,lPos,lineCol);scene.add(lineMesh);
const dotR=devMode?.12:.08;
const dot=new THREE.Mesh(new THREE.SphereGeometry(dotR,12,12),new THREE.MeshBasicMaterial({color:hasInfo?0xF5A623:0x999999}));
dot.position.copy(aPos);
if(hasInfo){const ring=new THREE.Mesh(new THREE.TorusGeometry(dotR*2,.02,8,24),new THREE.MeshBasicMaterial({color:0xF5A623,transparent:true,opacity:.5}));ring.rotation.x=Math.PI/2;dot.add(ring)}
scene.add(dot);
labels.push({sprite,lineMesh,dot,hitbox});
});
}
function makeLineMesh(a,b,col){const dir=new THREE.Vector3().subVectors(b,a);const len=dir.length();if(len<.001)return new THREE.Group();const mid=new THREE.Vector3().addVectors(a,b).multiplyScalar(.5);const geo=new THREE.CylinderGeometry(.025,.025,len,6);const mat=new THREE.MeshBasicMaterial({color:col,transparent:true,opacity:.9});const mesh=new THREE.Mesh(geo,mat);mesh.position.copy(mid);mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0,1,0),dir.clone().normalize());return mesh}
function makeLabel(text,fs,hasInfo){
const cv=document.createElement('canvas'),cx=cv.getContext('2d'),sc=4,sz=fs*sc;
cx.font='bold '+sz+'px Nunito,sans-serif';
const eyeW=hasInfo?sz*1.2:0;const tw=cx.measureText(text).width;const pad=sz*.6;
cv.width=tw+pad*2+eyeW;cv.height=sz*2;
const dk=theme==='dark';
cx.fillStyle=dk?'rgba(42,36,32,.92)':'rgba(255,255,255,.92)';rr(cx,0,0,cv.width,cv.height,cv.height/2);cx.fill();
cx.strokeStyle=dk?'rgba(85,72,53,1)':'rgba(180,150,110,1)';cx.lineWidth=3*sc;rr(cx,0,0,cv.width,cv.height,cv.height/2);cx.stroke();
cx.fillStyle=hasInfo?'#F5A623':'#999';cx.beginPath();cx.arc(sz*.7,cv.height/2,sz*.18,0,Math.PI*2);cx.fill();
cx.font='bold '+sz+'px Nunito,sans-serif';cx.fillStyle=dk?'#F0E6D6':'#3D3D3D';cx.textAlign='left';cx.textBaseline='middle';cx.fillText(text,sz*1.05,cv.height/2);
if(hasInfo){const ex=tw+sz*1.25+pad*.1,ey=cv.height/2,es=sz*.35;cx.fillStyle='#F5A623';cx.beginPath();cx.ellipse(ex,ey,es,es*.6,0,0,Math.PI*2);cx.fill();cx.fillStyle=dk?'#2A2420':'#fff';cx.beginPath();cx.arc(ex,ey,es*.3,0,Math.PI*2);cx.fill();cx.fillStyle='#F5A623';cx.beginPath();cx.arc(ex,ey,es*.13,0,Math.PI*2);cx.fill()}
const tex=new THREE.CanvasTexture(cv);tex.minFilter=THREE.LinearFilter;
const aspect=cv.width/cv.height,sprH=.45+(fs-8)*.025;
// depthTest TRUE so labels properly occlude behind model
const sp=new THREE.Sprite(new THREE.SpriteMaterial({map:tex,transparent:true,depthTest:true,depthWrite:false,sizeAttenuation:true}));
sp.scale.set(aspect*sprH,sprH,1);return sp;
}
function rr(c,x,y,w,h,r){c.beginPath();c.moveTo(x+r,y);c.lineTo(x+w-r,y);c.quadraticCurveTo(x+w,y,x+w,y+r);c.lineTo(x+w,y+h-r);c.quadraticCurveTo(x+w,y+h,x+w-r,y+h);c.lineTo(x+r,y+h);c.quadraticCurveTo(x,y+h,x,y+h-r);c.lineTo(x,y+r);c.quadraticCurveTo(x,y,x+r,y);c.closePath()}
/* INFO ICON */
function addInfoIcon(c){const ic=c.infoIcon||CFG.infoIcon;const g=new THREE.Group();g.add(new THREE.Mesh(new THREE.TorusGeometry(.3,.05,12,24),new THREE.MeshStandardMaterial({color:0xF5A623,emissive:0xF5A623,emissiveIntensity:.3,metalness:.6,roughness:.2})));const cv=document.createElement('canvas');cv.width=cv.height=128;const cx=cv.getContext('2d');cx.fillStyle='#F5A623';cx.beginPath();cx.arc(64,64,50,0,Math.PI*2);cx.fill();cx.fillStyle='#fff';cx.font='bold 60px Nunito';cx.textAlign='center';cx.textBaseline='middle';cx.fillText('i',64,68);g.add(new THREE.Mesh(new THREE.PlaneGeometry(.45,.45),new THREE.MeshBasicMaterial({map:new THREE.CanvasTexture(cv),transparent:true,side:THREE.DoubleSide,depthTest:false})));const p=ic.position||CFG.infoIcon.defaultPosition,s=ic.scale||CFG.infoIcon.defaultScale;g.position.set(...p);g.scale.set(s[0]*3,s[1]*3,s[2]*3);scene.add(g);infoIcon=g}
/* 3D INFO PANEL */
function toggleInfo3D(){
closePopup();const c=comps[compIdx];if(!c)return;
// On mobile, always open as popup instead of 3D panel
if(isMobile){openComponentPopup();return}
if(infoVis){hideInfo3D();document.getElementById('infoBtn').classList.remove('active-info')}
else{showInfo3D(c);document.getElementById('infoBtn').classList.add('active-info')}
}
function hideInfo3D(){if(info3D&&scene)scene.remove(info3D);info3D=null;infoVis=false}
function showInfo3D(c){
hideInfo3D();infoVis=true;const cfg=CFG.info3DPanel,pw=cfg.width||3.2;
const title=c.info?.title||c.name,body=c.info?.body||'',cat=CFG.categories.find(x=>x.id===c.category),catN=cat?.name||'';
const cv=document.createElement('canvas'),pxW=600;cv.width=pxW;cv.height=2000;const cx=cv.getContext('2d'),dk=theme==='dark';
cx.font='bold 36px Nunito';const tL=wrap(cx,title,pxW-80);cx.font='600 24px Nunito';const bL=wrap(cx,body,pxW-80);
const lh=30,tot=50+tL.length*42+48+bL.length*lh+80;cv.height=Math.min(tot,1200);
cx.fillStyle=dk?'rgba(42,36,32,.96)':'rgba(255,255,255,.96)';rr(cx,0,0,cv.width,cv.height,24);cx.fill();
cx.strokeStyle='#F5A623';cx.lineWidth=4;rr(cx,0,0,cv.width,cv.height,24);cx.stroke();
cx.fillStyle='#F5A623';cx.fillRect(20,0,cv.width-40,5);
let y=45;cx.fillStyle='#F5A623';cx.font='bold 36px Nunito';cx.textBaseline='top';tL.forEach(l=>{cx.fillText(l,40,y);y+=42});y+=4;
cx.fillStyle=cat?.color||'#F5A623';const bw=cx.measureText(catN).width+28;rr(cx,40,y,bw,28,14);cx.fill();cx.fillStyle='#fff';cx.font='bold 18px Nunito';cx.fillText(catN,54,y+5);y+=48;
cx.strokeStyle=dk?'#3A3028':'#F0E0CC';cx.lineWidth=2;cx.beginPath();cx.moveTo(40,y);cx.lineTo(cv.width-40,y);cx.stroke();y+=16;
cx.fillStyle=dk?'#F0E6D6':'#3D3D3D';cx.font='600 24px Nunito';bL.forEach(l=>{if(l.startsWith('• ')){cx.fillStyle='#F5A623';cx.beginPath();cx.arc(50,y+12,5,0,Math.PI*2);cx.fill();cx.fillStyle=dk?'#F0E6D6':'#3D3D3D';cx.fillText(l.substring(2),64,y)}else cx.fillText(l,40,y);y+=lh});
// Eye icon at bottom - "Click to open as popup"
y+=10;cx.fillStyle='#F5A623';cx.globalAlpha=.7;cx.beginPath();cx.ellipse(cv.width/2,y+12,18,11,0,0,Math.PI*2);cx.fill();cx.globalAlpha=1;cx.fillStyle=dk?'#2A2420':'#fff';cx.beginPath();cx.arc(cv.width/2,y+12,7,0,Math.PI*2);cx.fill();cx.fillStyle='#F5A623';cx.beginPath();cx.arc(cv.width/2,y+12,3,0,Math.PI*2);cx.fill();
cx.font='600 16px Nunito';cx.fillStyle=dk?'#A09080':'#7A7A7A';cx.textAlign='center';cx.fillText('Klik untuk buka popup / seret untuk alih',cv.width/2,y+35);cx.textAlign='left';
const ph=pw*(cv.height/cv.width),tex=new THREE.CanvasTexture(cv);tex.minFilter=THREE.LinearFilter;
info3D=new THREE.Group();
info3D.add(new THREE.Mesh(new THREE.PlaneGeometry(pw,ph),new THREE.MeshBasicMaterial({map:tex,transparent:true,side:THREE.DoubleSide,depthTest:false})));
const ox=cfg.offsetX||3.2;info3D.position.set(ox,cfg.offsetY||1,cfg.offsetZ||0);
const sP=new THREE.Vector3(0,1,0),eP=new THREE.Vector3(ox-pw/2-.1,cfg.offsetY||1,cfg.offsetZ||0);
const cl=new THREE.Line(new THREE.BufferGeometry().setFromPoints([sP,eP]),new THREE.LineBasicMaterial({color:0xF5A623,transparent:true,opacity:.5}));
cl.position.set(-ox,-(cfg.offsetY||1),-(cfg.offsetZ||0));info3D.add(cl);scene.add(info3D);
}
function wrap(cx,t,mw){const ls=[];t.split('\n').forEach(p=>{if(!p.trim()){ls.push('');return}const ws=p.split(' ');let l='';ws.forEach(w=>{const t2=l+(l?' ':'')+w;if(cx.measureText(t2).width>mw&&l){ls.push(l);l=w}else l=t2});if(l)ls.push(l)});return ls}
/* PLACEHOLDERS */
function makePlaceholder(c){
const g=new THREE.Group(),cc={mikropengawal:0x4A90D9,input:0x4CAF50,output:0xFF6B6B,litar:0x9B59B6,suis:0xF39C12}[c.category]||0xF5A623;
switch(c.id){
case 'arduino':{const bm=new THREE.MeshStandardMaterial({color:0x0066AA,roughness:.4,metalness:.1});const b=new THREE.Mesh(new THREE.BoxGeometry(3.5,.15,2.2),bm);b.castShadow=true;g.add(b);const usb=new THREE.Mesh(new THREE.BoxGeometry(.5,.3,.6),new THREE.MeshStandardMaterial({color:0xC0C0C0,metalness:.8,roughness:.2}));usb.position.set(-1.7,.2,0);g.add(usb);const ch=new THREE.Mesh(new THREE.BoxGeometry(1,.12,.4),new THREE.MeshStandardMaterial({color:0x1A1A1A}));ch.position.set(.3,.14,0);g.add(ch);const pm=new THREE.MeshStandardMaterial({color:0xFFD700,metalness:.9});for(let i=0;i<14;i++){const p=new THREE.Mesh(new THREE.BoxGeometry(.08,.3,.08),pm);p.position.set(-1.5+i*.22,.2,1);g.add(p)}for(let i=0;i<6;i++){const p=new THREE.Mesh(new THREE.BoxGeometry(.08,.3,.08),pm);p.position.set(-.5+i*.22,.2,-1);g.add(p)}break}
case 'breadboard':{g.add(Object.assign(new THREE.Mesh(new THREE.BoxGeometry(4,.15,2),new THREE.MeshStandardMaterial({color:0xF5F5F0})),{castShadow:true}));break}
case 'resistor':{const bd=new THREE.Mesh(new THREE.CylinderGeometry(.15,.15,1,12),new THREE.MeshStandardMaterial({color:0xD4A574}));bd.rotation.z=Math.PI/2;bd.castShadow=true;g.add(bd);[0x8B4513,0,0xFF0000,0xFFD700].forEach((c2,i)=>{const bnd=new THREE.Mesh(new THREE.CylinderGeometry(.16,.16,.06,12),new THREE.MeshStandardMaterial({color:c2}));bnd.rotation.z=Math.PI/2;bnd.position.set(-.3+i*.18,0,0);g.add(bnd)});const lm=new THREE.MeshStandardMaterial({color:0xC0C0C0,metalness:.9});[-1,1].forEach(s=>{const l=new THREE.Mesh(new THREE.CylinderGeometry(.02,.02,1,6),lm);l.rotation.z=Math.PI/2;l.position.set(s,0,0);g.add(l)});break}
case 'potentiometer':{g.add(Object.assign(new THREE.Mesh(new THREE.CylinderGeometry(.6,.6,.3,20),new THREE.MeshStandardMaterial({color:0x3366AA})),{castShadow:true}));const k=new THREE.Mesh(new THREE.CylinderGeometry(.25,.3,.4,16),new THREE.MeshStandardMaterial({color:0xC0C0C0,metalness:.8}));k.position.y=.35;g.add(k);break}
case 'led':{const d=new THREE.Mesh(new THREE.SphereGeometry(.25,16,12,0,Math.PI*2,0,Math.PI/2),new THREE.MeshStandardMaterial({color:0xFF0000,emissive:0xFF0000,emissiveIntensity:.3,transparent:true,opacity:.85}));d.position.y=.4;g.add(d);const bs=new THREE.Mesh(new THREE.CylinderGeometry(.25,.28,.3,16),new THREE.MeshStandardMaterial({color:0xFF0000,transparent:true,opacity:.6}));bs.position.y=.25;g.add(bs);break}
case 'buzzer':{g.add(Object.assign(new THREE.Mesh(new THREE.CylinderGeometry(.5,.5,.3,20),new THREE.MeshStandardMaterial({color:0x1A1A1A})),{castShadow:true}));break}
case 'pushbutton':{g.add(Object.assign(new THREE.Mesh(new THREE.BoxGeometry(.5,.2,.5),new THREE.MeshStandardMaterial({color:0x1A1A1A})),{castShadow:true}));const cp=new THREE.Mesh(new THREE.CylinderGeometry(.15,.15,.15,12),new THREE.MeshStandardMaterial({color:0xFF4444}));cp.position.y=.17;g.add(cp);break}
case 'servo':{g.add(Object.assign(new THREE.Mesh(new THREE.BoxGeometry(1.2,.8,.6),new THREE.MeshStandardMaterial({color:0x2255AA})),{castShadow:true}));break}
case 'ultrasonic':{g.add(Object.assign(new THREE.Mesh(new THREE.BoxGeometry(2,.1,1),new THREE.MeshStandardMaterial({color:0x0066AA})),{castShadow:true}));[-.5,.5].forEach(x=>{const cy=new THREE.Mesh(new THREE.CylinderGeometry(.35,.35,.3,20),new THREE.MeshStandardMaterial({color:0xC0C0C0,metalness:.7}));cy.rotation.x=Math.PI/2;cy.position.set(x,.2,.5);g.add(cy)});break}
case 'ldr':{g.add(Object.assign(new THREE.Mesh(new THREE.CylinderGeometry(.35,.35,.1,20),new THREE.MeshStandardMaterial({color:0x8B4513})),{castShadow:true}));break}
default:{g.add(Object.assign(new THREE.Mesh(new THREE.BoxGeometry(1.5,.8,1),new THREE.MeshStandardMaterial({color:cc})),{castShadow:true}))}
}
g.position.set(...c.model.position);setRot(g,c.model.rotation);g.scale.set(...c.model.scale);return g;
}
/* ============================================================
TUTORIAL SYSTEM
============================================================ */
let tutStep=0,tutActive=false,tutSpeaking=false,tutAudio=null;
const TUT_STEPS=[
{
target:null,
text:'Selamat datang ke <strong>Pengenalan Kepada Perkakasan</strong>! 🎉 Di sini, anda akan meneroka komponen elektronik secara 3D interaktif. Mari saya tunjukkan cara menggunakan aplikasi ini.',
audio:'./audio/tutorial/step1.mp3'
},
{
target:'viewer3d',
text:'Ini adalah <strong>Paparan 3D</strong> — model komponen elektronik akamn dipaparkan di sini. Anda boleh <strong>seret untuk putar</strong> model dan <strong>cubit untuk zum</strong> masuk atau keluar.',
audio:'./audio/tutorial/step2.mp3'
},
{
target:'componentName',
text:'Nama dan <strong>kategori komponen</strong> yang sedang dipaparkan akan tertera di sini. Setiap komponen mempunyai kategori tersendiri seperti Mikropengawal, Peranti Input, atau Output.',
audio:'./audio/tutorial/step3.mp3'
},
{
target:'prevBtn',
text:'Gunakan butang <strong>anak panah kiri</strong> dan <strong>kanan</strong> untuk melayari antara komponen. Anda juga boleh menggunakan kekunci anak panah pada papan kekunci.',
audio:'./audio/tutorial/step4.mp3'
},
{
target:'infoBtn',
text:'Ketik butang <strong>Info</strong> ini untuk memaparkan panel maklumat 3D bersebelahan model. Panel ini mengandungi penerangan terperinci tentang komponen.',
audio:'./audio/tutorial/step5.mp3'
},
{
target:'labelToggleBtn',
text:'Butang <strong>Label</strong> ini menghidupkan atau mematikan label pada bahagian penting komponen dalam paparan 3D.',
audio:'./audio/tutorial/step6.mp3'
},
{
target:'categoryTabs',
text:'Gunakan <strong>tab kategori</strong> ini untuk menapis komponen mengikut jenis — Mikropengawal, Peranti Input, Peranti Output, Komponen Litar, atau Suis dan Kawalan.',
audio:'./audio/tutorial/step7.mp3'
},
{
target:'compVideoBtn',
text:'Ketik butang <strong>Video</strong> untuk menonton video penerangan mengenai komponen elektronik.',
audio:'./audio/tutorial/step8.mp3'
},
{
target:'compInfoBtn',
text:'Ketik butang <strong>Maklumat</strong> untuk membaca penerangan terperinci tentang komponen yang sedang dipaparkan.',
audio:'./audio/tutorial/step9.mp3'
},
{
target:'speechFloatBtn',
text:'Butang <strong>Audio</strong> ini akan membacakan penerangan komponen secara lisan untuk anda. Sesuai untuk pembelajaran pendengaran!',
audio:'./audio/tutorial/step10.mp3'
},
{
target:null,
text:'Tahniah! 🎊 Anda sudah bersedia untuk meneroka komponen elektronik. Selamat belajar! Anda boleh mulakan tutorial semula dari butang <strong>⚙ Tetapan</strong>.',
audio:'./audio/tutorial/step11.mp3'
}
];
function startTutorial(){
tutStep=0;tutActive=true;
document.getElementById('tutOverlay').classList.add('active');
showTutStep();
}
function tutNext(){
tutStopVO();
tutStep++;
if(tutStep>=TUT_STEPS.length){tutEnd();return}
showTutStep();
}
function tutPrev(){
tutStopVO();
if(tutStep>0)tutStep--;
showTutStep();
}
function tutEnd(){
tutStopVO();
tutActive=false;tutStep=0;
document.getElementById('tutOverlay').classList.remove('active');
sessionStorage.setItem('simuc_tut_shown','1');
}
function showTutStep(){
const step=TUT_STEPS[tutStep];
if(!step)return;
// Update badge
document.getElementById('tutStepBadge').textContent=(tutStep+1)+' / '+TUT_STEPS.length;
// Prev button visibility
document.getElementById('tutPrevBtn').style.display=tutStep===0?'none':'';
// Next button text
const nb=document.getElementById('tutNextBtn');
if(tutStep===TUT_STEPS.length-1){nb.innerHTML='Selesai <i class="bi bi-check-lg"></i>'}
else{nb.innerHTML='Seterusnya <i class="bi bi-chevron-right"></i>'}
// Spotlight
const spot=document.getElementById('tutSpotlight');
const backdrop=document.getElementById('tutBackdrop');
if(step.target){
const el=document.getElementById(step.target);
if(el){
// Temporarily show hidden elements for spotlight
if(step.target==='speechFloatBtn'){el.style.display='flex'}
const r=el.getBoundingClientRect();
const pad=8;
spot.style.display='block';
spot.style.left=(r.left-pad)+'px';
spot.style.top=(r.top-pad)+'px';
spot.style.width=(r.width+pad*2)+'px';
spot.style.height=(r.height+pad*2)+'px';
backdrop.style.display='none';// spotlight handles dimming via box-shadow
}
} else {
spot.style.display='none';
backdrop.style.display='block';
}
// Type text with animation
typeText(step.text);
// Voice over (MP3 audio)
setTimeout(()=>tutPlayVO(step.audio),400);
}
function typeText(html){
const el=document.getElementById('tutText');
el.innerHTML='';
// Parse HTML to insert character by character while preserving tags
const div=document.createElement('div');div.innerHTML=html;
const fullText=div.textContent||div.innerText;
let idx=0;
el.innerHTML='<span class="tut-typing"><span class="cursor"></span></span>';
// For simplicity, show full HTML after brief animation
const typing=el.querySelector('.tut-typing');
function tick(){
if(idx>=fullText.length||!tutActive){
el.innerHTML=html;return;
}
typing.textContent=fullText.substring(0,++idx);
typing.innerHTML+=('<span class="cursor"></span>');
setTimeout(tick,18);
}
tick();
// Failsafe: show full text after max time
setTimeout(()=>{if(tutActive)el.innerHTML=html},fullText.length*20+200);
}
function tutPlayVO(src){
if(!src)return;
tutStopVO();
try{
tutAudio=new Audio(src);
tutAudio.volume=0.8;
tutAudio.onended=()=>{tutSpeaking=false};
tutAudio.onerror=()=>{tutSpeaking=false;tutAudio=null};
tutAudio.play().then(()=>{tutSpeaking=true}).catch(()=>{tutSpeaking=false});
}catch(e){tutSpeaking=false}
}
function tutStopVO(){
if(tutAudio){try{tutAudio.pause();tutAudio.currentTime=0}catch(e){}tutAudio=null}
tutSpeaking=false;
}
// Reposition spotlight on resize
window.addEventListener('resize',()=>{if(tutActive)showTutStep()});
/* INIT */
async function init(){
try{const r=await fetch('./config.json');CFG=await r.json()}catch(e){CFG={app:{version:'v1.3.0',devMode:false},audio:{},infoIcon:{defaultScale:[.3,.3,.3],defaultPosition:[0,2.5,0],defaultRotation:[0,0,0]},info3DPanel:{width:3.2,offsetX:3.2,offsetY:1,offsetZ:0},categories:[],components:[],viewer3D:{camera:{fov:50,near:.1,far:1000,defaultPosition:[0,3,8]},isometricView:{distance:10,angleX:30,angleY:45},controls:{enableDamping:true,dampingFactor:.08,minDistance:2,maxDistance:20,enablePan:true,autoRotate:true,autoRotateSpeed:1},lighting:{ambientIntensity:.6,directionalIntensity:.8,directionalPosition:[5,10,7]},grid:{size:20,divisions:20,visible:true}}}}
devMode=false;// User mode by default - enable via settings
if(!CFG.viewer3D.isometricView)CFG.viewer3D.isometricView={distance:10,angleX:30,angleY:45};
CFG.app.version='v1.3.0';
document.getElementById('versionBadge').textContent='v1.3.0';
initSettings();await document.fonts.ready;
initSymbols();
initVideoPlayer();
setTimeout(()=>document.getElementById('loadingOverlay').classList.add('hidden'),600);
}
/* ============================================================
FLOATING ELECTRONIC SYMBOLS ANIMATION
============================================================ */
function initSymbols(){
const canvas=document.getElementById('symbolsCanvas');
if(!canvas)return;
const ctx=canvas.getContext('2d');
let W,H;
function resize(){W=canvas.width=window.innerWidth;H=canvas.height=window.innerHeight}
resize();window.addEventListener('resize',resize);
// Electronic symbol shapes as path drawing functions
const shapes=[
// Resistor zigzag
(ctx,s)=>{ctx.beginPath();ctx.moveTo(-s,0);ctx.lineTo(-s*.7,-s*.5);ctx.lineTo(-s*.35,s*.5);ctx.lineTo(0,-s*.5);ctx.lineTo(s*.35,s*.5);ctx.lineTo(s*.7,-s*.5);ctx.lineTo(s,0);ctx.stroke()},
// Capacitor
(ctx,s)=>{ctx.beginPath();ctx.moveTo(-s,0);ctx.lineTo(-s*.15,0);ctx.moveTo(-s*.15,-s*.6);ctx.lineTo(-s*.15,s*.6);ctx.moveTo(s*.15,-s*.6);ctx.lineTo(s*.15,s*.6);ctx.moveTo(s*.15,0);ctx.lineTo(s,0);ctx.stroke()},
// LED triangle
(ctx,s)=>{ctx.beginPath();ctx.moveTo(-s*.4,-s*.5);ctx.lineTo(-s*.4,s*.5);ctx.lineTo(s*.4,0);ctx.closePath();ctx.stroke();ctx.beginPath();ctx.moveTo(s*.4,-s*.5);ctx.lineTo(s*.4,s*.5);ctx.stroke();ctx.beginPath();ctx.moveTo(s*.5,-s*.6);ctx.lineTo(s*.8,-s*.9);ctx.moveTo(s*.5,-s*.3);ctx.lineTo(s*.8,-s*.6);ctx.stroke()},
// Chip/IC rectangle
(ctx,s)=>{ctx.strokeRect(-s*.5,-s*.4,s,s*.8);for(let i=-1;i<=1;i++){ctx.beginPath();ctx.moveTo(i*s*.3,-s*.4);ctx.lineTo(i*s*.3,-s*.7);ctx.stroke();ctx.beginPath();ctx.moveTo(i*s*.3,s*.4);ctx.lineTo(i*s*.3,s*.7);ctx.stroke()}},
// Ground symbol
(ctx,s)=>{ctx.beginPath();ctx.moveTo(0,-s*.6);ctx.lineTo(0,-s*.1);ctx.stroke();ctx.beginPath();ctx.moveTo(-s*.5,-s*.1);ctx.lineTo(s*.5,-s*.1);ctx.stroke();ctx.beginPath();ctx.moveTo(-s*.3,s*.15);ctx.lineTo(s*.3,s*.15);ctx.stroke();ctx.beginPath();ctx.moveTo(-s*.1,s*.4);ctx.lineTo(s*.1,s*.4);ctx.stroke()},
// Inductor coils
(ctx,s)=>{ctx.beginPath();ctx.moveTo(-s,0);for(let i=0;i<4;i++){const cx=-s+s*.25+i*s*.5;ctx.arc(cx,-s*.05,s*.22,Math.PI,0,false)}ctx.lineTo(s,0);ctx.stroke()},
// Diode
(ctx,s)=>{ctx.beginPath();ctx.moveTo(-s,0);ctx.lineTo(-s*.2,0);ctx.moveTo(-s*.2,-s*.45);ctx.lineTo(-s*.2,s*.45);ctx.lineTo(s*.3,0);ctx.closePath();ctx.stroke();ctx.beginPath();ctx.moveTo(s*.3,-s*.45);ctx.lineTo(s*.3,s*.45);ctx.stroke();ctx.beginPath();ctx.moveTo(s*.3,0);ctx.lineTo(s,0);ctx.stroke()},
// Voltage source circle
(ctx,s)=>{ctx.beginPath();ctx.arc(0,0,s*.45,0,Math.PI*2);ctx.stroke();ctx.beginPath();ctx.moveTo(-s*.15,-s*.15);ctx.lineTo(s*.15,-s*.15);ctx.moveTo(0,-s*.3);ctx.lineTo(0,0);ctx.moveTo(-s*.15,s*.2);ctx.lineTo(s*.15,s*.2);ctx.stroke()},
];
// Create particles
const particles=[];
const COUNT=25;
for(let i=0;i<COUNT;i++){
particles.push({
x:Math.random()*W,
y:Math.random()*H,
vx:(Math.random()-0.5)*0.4,
vy:(Math.random()-0.5)*0.3,
size:14+Math.random()*22,
targetSize:14+Math.random()*22,
sizeSpeed:0.01+Math.random()*0.02,
rot:Math.random()*Math.PI*2,
rotSpeed:(Math.random()-0.5)*0.008,
shape:shapes[Math.floor(Math.random()*shapes.length)],
opacity:0.06+Math.random()*0.1,
targetOpacity:0.06+Math.random()*0.1,
changeTick:Math.floor(Math.random()*300),
});
}
function tick(){
ctx.clearRect(0,0,W,H);
particles.forEach(p=>{
// Move
p.x+=p.vx;p.y+=p.vy;
p.rot+=p.rotSpeed;
// Wrap edges
if(p.x<-50)p.x=W+50;if(p.x>W+50)p.x=-50;
if(p.y<-50)p.y=H+50;if(p.y>H+50)p.y=-50;
// Size pulsing
p.size+=(p.targetSize-p.size)*p.sizeSpeed;
p.opacity+=(p.targetOpacity-p.opacity)*0.01;
// Periodically change target size/opacity
p.changeTick--;
if(p.changeTick<=0){
p.targetSize=14+Math.random()*26;
p.targetOpacity=0.04+Math.random()*0.12;
p.changeTick=200+Math.floor(Math.random()*400);
// Occasionally change direction slightly
p.vx+=(Math.random()-0.5)*0.15;
p.vy+=(Math.random()-0.5)*0.1;
p.vx=Math.max(-0.6,Math.min(0.6,p.vx));
p.vy=Math.max(-0.4,Math.min(0.4,p.vy));
}
// Draw
ctx.save();
ctx.translate(p.x,p.y);
ctx.rotate(p.rot);
ctx.strokeStyle='rgba(255,255,255,'+p.opacity+')';
ctx.lineWidth=1.5;
ctx.lineCap='round';ctx.lineJoin='round';
p.shape(ctx,p.size);
ctx.restore();
});
requestAnimationFrame(tick);
}
tick();
}
/* ============================================================
VIDEO PLAYER
============================================================ */
function pauseIntroVideo(){
const video=document.getElementById('introVideo');
const player=document.getElementById('videoPlayer');
if(video&&!video.paused){video.pause()}
if(player)player.classList.remove('playing');
const btn=document.getElementById('videoPlayBtn');
if(btn)btn.innerHTML='<i class="bi bi-play-fill"></i>';
}
function toggleVideo(){
const player=document.getElementById('videoPlayer');
const video=document.getElementById('introVideo');
const playBtn=document.getElementById('videoPlayBtn');
if(player.classList.contains('playing')){
video.pause();
player.classList.remove('playing');
playBtn.innerHTML='<i class="bi bi-play-fill"></i>';
} else {
video.play();
player.classList.add('playing');
playBtn.innerHTML='<i class="bi bi-pause-fill"></i>';
}
}
function initVideoPlayer(){
const video=document.getElementById('introVideo');
if(!video)return;
video.addEventListener('loadedmetadata',()=>{
const dur=video.duration;
const m=Math.floor(dur/60),s=Math.floor(dur%60);
document.getElementById('videoDuration').textContent=m+':'+String(s).padStart(2,'0');
});
video.addEventListener('timeupdate',()=>{
if(!video.paused){
const cur=video.currentTime,dur=video.duration;
const m=Math.floor((dur-cur)/60),s=Math.floor((dur-cur)%60);
document.getElementById('videoDuration').textContent=m+':'+String(s).padStart(2,'0');
}
});
video.addEventListener('ended',()=>{
const player=document.getElementById('videoPlayer');
player.classList.remove('playing');
document.getElementById('videoPlayBtn').innerHTML='<i class="bi bi-play-fill"></i>';
const dur=video.duration;
const m=Math.floor(dur/60),s=Math.floor(dur%60);
document.getElementById('videoDuration').textContent=m+':'+String(s).padStart(2,'0');
});
}
init();
</script>
</body>
</html>