0% found this document useful (0 votes)
4 views73 pages

Code HTML

The document is an HTML template for a physics laboratory application, featuring a sidebar with tabs for navigation and various interactive elements. It includes styles for buttons, panels, and tooltips, designed for a user-friendly interface. The layout is responsive, adapting to different screen sizes for optimal usability.

Uploaded by

tphuc6310
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
4 views73 pages

Code HTML

The document is an HTML template for a physics laboratory application, featuring a sidebar with tabs for navigation and various interactive elements. It includes styles for buttons, panels, and tooltips, designed for a user-friendly interface. The layout is responsive, adapting to different screen sizes for optimal usability.

Uploaded by

tphuc6310
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd

<!

DOCTYPE html>

<html lang="vi">

<head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>Phòng Thí Nghiệm Từ Trường - Sư Phạm Vật Lý 12</title>

<script src="[Link]

<link href="[Link]
family=Segoe+UI:wght@400;600;700&display=swap" rel="stylesheet">

<style>

* { box-sizing: border-box; user-select: none; }

body { margin: 0; display: flex; height: 100vh; font-family: 'Segoe UI', sans-serif; background-
color: #0b0f19; color: #c9d1d9; overflow: hidden; }

#sidebar { width: 360px; flex-shrink: 0; background: rgba(13, 17, 23, 0.98); border-right: 1px
solid #30363d; display: flex; flex-direction: column; z-index: 10; box-shadow: 5px 0 20px
rgba(0,0,0,0.9); overflow-y: auto; }

.header { padding: 20px; border-bottom: 1px solid #30363d; text-align: center; }

h1 { font-size: 20px; color: #58a6ff; margin: 0 0 5px 0; text-transform: uppercase; }

.subtitle { font-size: 13px; color: #8b949e; margin: 0; }

.tabs { display: flex; flex-direction: column; padding: 10px; gap: 5px; border-bottom: 1px solid
#30363d; }

.tab-btn { background: transparent; color: #8b949e; border: 1px solid transparent; padding:
12px; border-radius: 8px; text-align: left; font-weight: 600; cursor: pointer; transition: 0.2s; font-size:
14px;}

.tab-btn:hover { background: #1f2937; color: #c9d1d9; }

.[Link] { background: rgba(88, 166, 255, 0.1); border: 1px solid #58a6ff; color: #58a6ff; }

.panel { padding: 15px; display: none; flex-direction: column; gap: 10px;}

.[Link] { display: flex; }


[Link]-btn { background: #21262d; color: #c9d1d9; border: 1px solid #30363d; padding:
10px; border-radius: 6px; cursor: pointer; font-size: 13.5px; font-weight: 600; transition: 0.2s;
display: flex; align-items: center; gap: 8px; }

[Link]-btn:hover { border-color: #58a6ff; background: #1f2937; color: #fff; }

.btn-special { background: linear-gradient(145deg, #238636, #2ea043); color: white; border:


none; font-weight: bold; justify-content: center;}

.btn-special:hover { background: #2ea043; transform: translateY(-2px); }

.btn-oersted { background: linear-gradient(145deg, #8e44ad, #d35400); color: white; border:


none; font-weight: bold;}

.btn-tap { background: linear-gradient(145deg, #d97706, #b45309); color: white; border: none;


font-weight: bold; justify-content: center; }

.toggle-box { display: flex; align-items: center; gap: 10px; font-size: 13px; color: #58a6ff;
padding: 10px 15px; border-bottom: 1px solid #30363d; background: rgba(88, 166, 255, 0.05); font-
weight: 600;}

input[type="checkbox"] { width: 16px; height: 16px; accent-color: #58a6ff; cursor: pointer; }

/* BẢNG ĐIỀU KHIỂN TÙY CHỈNH VẬT THỂ */

#prop-panel {

margin-top: 10px;

padding: 12px;

background: rgba(88, 166, 255, 0.08);

border: 1px solid #58a6ff;

border-radius: 8px;

display: none;

/* PC: Nằm theo luồng tự nhiên của Sidebar */

position: relative;

width: 100%;

z-index: 1;

#prop-panel h3 { margin: 0 0 10px 0; color: #58a6ff; font-size: 14px;}

.slider-box { margin-bottom: 10px; }


.slider-box label { display: flex; justify-content: space-between; font-size: 13px; margin-bottom:
5px; color: #c9d1d9; }

.slider-box input[type="range"] { width: 100%; accent-color: #58a6ff; }

.prop-row { display: flex; align-items: center; justify-content: space-between; font-size: 13px;


margin-bottom: 5px;}

.instruction { font-size: 13px; color: #8b949e; background: rgba(0,0,0,0.3); padding: 12px;


border-radius: 8px; border: 1px dashed #30363d; line-height: 1.5; margin-top: auto;}

#canvas-container { flex: 1; position: relative; min-width: 0; overflow: hidden; }

#oersted-tooltip { position: absolute; top: 85px; left: 20px; right: auto; background: rgba(13, 17,
23, 0.95); border: 1px solid #d35400; padding: 15px; border-radius: 8px; width: 360px; color:
#c9d1d9; font-size: 14px; line-height: 1.6; display: none; box-shadow: 0 5px 15px rgba(0,0,0,0.5); z-
index: 50; backdrop-filter: blur(4px); }

#oersted-tooltip h3 { color: #f39c12; margin: 0 0 10px 0; font-size: 16px; border-bottom: 1px


solid #30363d; padding-bottom: 5px;}

/* OVERLAYS CHO TAB 3 */

.overlay-screen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background:
rgba(11, 15, 25, 0.95); display: none; flex-direction: column; align-items: center; justify-content:
center; z-index: 50; padding: 20px;}

.theory-box { background: #161b22; border: 2px solid #30363d; padding: 30px; border-radius:
12px; max-width: 900px; color: #c9d1d9; font-size: 15px; line-height: 1.6;}

.theory-box h2 { color: #58a6ff; margin-top: 0; text-align: center; border-bottom: 1px solid


#30363d; padding-bottom: 10px;}

.theory-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 20px; margin-top: 20px;}

.theory-card { background: #0d1117; padding: 15px; border-radius: 8px; border: 1px solid
#21262d;}

.theory-card h3 { color: #f1c40f; margin-top: 0; font-size: 15px;}

#game-ui { position: absolute; bottom: 40px; left: 50%; transform: translateX(-50%);


background: rgba(13, 17, 23, 0.95); padding: 25px; border-radius: 12px; border: 2px solid #58a6ff;
display: none; text-align: center; box-shadow: 0 10px 40px rgba(0,0,0,0.8); width: 650px;}

#game-progress { color: #8b949e; font-size: 14px; margin-bottom: 10px; font-weight: bold;}

#game-question { font-size: 17px; font-weight: bold; margin-bottom: 20px; color: #fff; line-
height: 1.5;}
.game-btn-group { display: flex; flex-direction: column; gap: 10px; }

.game-btn { padding: 12px; font-size: 15px; border: none; border-radius: 6px; cursor: pointer;
font-weight: bold; background: #21262d; color: white; border: 1px solid #30363d; transition: 0.2s;}

.game-btn:hover { background: #3b82f6; border-color: #58a6ff; }

#game-feedback { margin-top: 15px; font-size: 16px; font-weight: bold; display: none; padding:
10px; border-radius: 6px;}

/* BẢNG TỔNG KẾT */

.summary-box {

background: #161b22;

border: 2px solid #2ea043;

padding: 30px; /* Giảm padding một chút */

border-radius: 12px;

max-width: 800px;

text-align: center;

max-height: 85vh; /* Giới hạn chiều cao bảng */

overflow-y: auto; /* Cho phép cuộn bên trong bảng */

.summary-box h2 { color: #2ea043; font-size: 28px; margin-top: 0;}

.summary-content { text-align: left; background: #0d1117; padding: 20px; border-radius: 8px;


margin: 20px 0; font-size: 15.5px; line-height: 1.8;}

.summary-content span { color: #f1c40f; font-weight: bold;}

.summary-content ul { padding-left: 20px; margin: 0; }

.summary-content li { margin-bottom: 10px; }

/* NÚT (i) HƯỚNG DẪN */

#btn-info {

position: absolute;

top: 20px;

right: 20px;

width: 42px;

height: 42px;

border-radius: 50%;
background: #21262d;

color: #58a6ff;

border: 2px solid #30363d;

font-size: 22px;

font-weight: bold;

font-family: Georgia, serif;

cursor: pointer;

z-index: 60;

display: flex;

align-items: center;

justify-content: center;

transition: all 0.2s;

box-shadow: 0 4px 15px rgba(0,0,0,0.6);

#btn-info:hover {

background: #30363d;

border-color: #58a6ff;

transform: scale(1.1);

/* NÚT CÀI ĐẶT (BÁNH RĂNG) */

#btn-settings { position: absolute; top: 20px; right: 75px; width: 42px; height: 42px; border-
radius: 50%; background: #21262d; color: #c9d1d9; border: 2px solid #30363d; font-size: 20px;
cursor: pointer; z-index: 60; display: flex; align-items: center; justify-content: center; transition: all
0.2s; box-shadow: 0 4px 15px rgba(0,0,0,0.6); }

#btn-settings:hover { background: #30363d; border-color: #58a6ff; transform: scale(1.1); }

/* NÚT HƯỚNG DẪN OERSTED (CUỘN THƯ) */

#btn-oersted-guide { position: absolute; top: 20px; right: 130px; width: 42px; height: 42px;
border-radius: 50%; background: #21262d; color: #c9d1d9; border: 2px solid #30363d; font-size:
20px; cursor: pointer; z-index: 60; display: flex; align-items: center; justify-content: center;
transition: all 0.2s; box-shadow: 0 4px 15px rgba(0,0,0,0.6); }

#btn-oersted-guide:hover { background: #30363d; border-color: #58a6ff; transform: scale(1.1); }

/* MENU DROP-DOWN CHỨA 2 CÔNG TẮC */


#settings-menu { position: absolute; top: 70px; right: 75px; background: rgba(13, 17, 23, 0.95);
border: 1px solid #30363d; border-radius: 8px; padding: 10px; display: none; flex-direction: column;
gap: 5px; z-index: 60; box-shadow: 0 5px 20px rgba(0,0,0,0.8); width: 270px; }

#settings-menu .toggle-box { border-bottom: 1px dashed #30363d; background: transparent; }

#settings-menu .toggle-box:last-child { border-bottom: none; }

/* NÚT ĐIỀU KHIỂN GIAO DIỆN ĐIỆN THOẠI */

#btn-mobile-tools { display: none; position: absolute; bottom: 25px; left: 50%; transform:
translateX(-50%); background: linear-gradient(145deg, #1f6feb, #3b82f6); color: white; border:
none; padding: 12px 25px; border-radius: 30px; font-size: 15px; font-weight: bold; cursor: pointer; z-
index: 90; box-shadow: 0 10px 25px rgba(0,0,0,0.8); transition: 0.2s; }

#btn-mobile-tools:active { transform: translateX(-50%) scale(0.95); }

#btn-close-tools { display: none; margin: 12px auto 0; background: #21262d; border: 1px solid
#30363d; color: #8b949e; padding: 6px 20px; border-radius: 20px; font-size: 12px; font-weight: 600;
cursor: pointer; transition: 0.2s; }

/* ===== 1. TỰ ĐỘNG THÍCH ỨNG KHI MÀN HÌNH NHỎ ===== */

@media (max-width: 768px) {

#canvas-container { width: 100%; height: 100vh; }

#sidebar { position: fixed; bottom: -80vh; left: 0; width: 100%; height: 65vh; border-right:
none; border-top: 1px solid #30363d; border-radius: 20px 20px 0 0; box-shadow: 0 -15px 40px
rgba(0,0,0,0.9); transition: bottom 0.35s cubic-bezier(0.2, 0.8, 0.2, 1); z-index: 100; }

[Link]-tools #sidebar { bottom: 0; }

#btn-mobile-tools { display: block; }

#btn-close-tools { display: inline-block; }

#btn-info, #btn-settings, #btn-oersted-guide { top: 10px; width: 36px; height: 36px; font-size:
16px; }

#btn-info { right: 10px; }

#btn-settings { right: 55px; }

#btn-oersted-guide { right: 100px; }

#settings-menu { top: 55px; right: 10px; width: 220px; }

.theory-box { width: 95%; padding: 20px; max-height: 90vh; overflow-y: auto; }

.theory-grid { grid-template-columns: 1fr; gap: 10px; }

#game-ui { width: 95%; bottom: 85px; padding: 15px; }


/* Dạt bảng Oersted sang trái, làm gọn lại và thêm thanh cuộn */

#oersted-tooltip { width: 45%; top: 60px; left: 2.5%; right: auto; max-height: 50vh; overflow-y:
auto; padding: 12px; font-size: 12.5px; background: rgba(13, 17, 23, 0.85); backdrop-filter: blur(4px);
border-radius: 8px; }

/* ===== 2. ÉP BUỘC KÍCH HOẠT KHI BẬT CÔNG TẮC TRÊN MÁY TÍNH ===== */

[Link]-mode #canvas-container { width: 100%; height: 100vh; }

[Link]-mode #sidebar { position: fixed; bottom: -80vh; left: 0; width: 100%; height: 65vh;
border-right: none; border-top: 1px solid #30363d; border-radius: 20px 20px 0 0; box-shadow: 0 -
15px 40px rgba(0,0,0,0.9); transition: bottom 0.35s cubic-bezier(0.2, 0.8, 0.2, 1); z-index: 100; }

[Link]-tools #sidebar { bottom: 0; }

[Link]-mode #btn-mobile-tools { display: block; }

[Link]-mode #btn-close-tools { display: inline-block; }

[Link]-mode #btn-info, [Link]-mode #btn-settings { top: 10px; width: 36px; height:


36px; font-size: 16px; }

[Link]-mode #btn-info { right: 10px; }

[Link]-mode #btn-settings { right: 55px; }

[Link]-mode #settings-menu { top: 55px; right: 10px; width: 220px; }

[Link]-mode .theory-box { width: 95%; padding: 20px; max-height: 90vh; overflow-y:


auto; }

[Link]-mode .theory-grid { grid-template-columns: 1fr; gap: 10px; }

[Link]-mode #game-ui { width: 95%; bottom: 85px; padding: 15px; }

[Link]-mode #oersted-tooltip { width: 45%; top: 60px; left: 2.5%; right: auto; max-height:
50vh; overflow-y: auto; padding: 12px; font-size: 12.5px; background: rgba(13, 17, 23, 0.85);
backdrop-filter: blur(4px); border-radius: 8px; }

/* BIẾN BẢNG TÙY CHỈNH THÀNH CỬA SỔ NỔI ĐỘC LẬP TRÊN ĐIỆN THOẠI */

@media (max-width: 768px) {

#prop-panel { position: fixed !important; top: 20px !important; left: 2.5% !important; width:
95% !important; z-index: 200 !important; background: rgba(13, 17, 23, 0.98) !important; border: 1px
solid #58a6ff !important; box-shadow: 0 15px 50px rgba(0,0,0,0.9) !important; max-height: 85vh !
important; overflow-y: auto !important; }

}
[Link]-mode #prop-panel { position: fixed !important; top: 20px !important; left: 2.5% !
important; width: 95% !important; z-index: 200 !important; background: rgba(13, 17, 23, 0.98) !
important; border: 1px solid #58a6ff !important; box-shadow: 0 15px 50px rgba(0,0,0,0.9) !
important; max-height: 85vh !important; overflow-y: auto !important; }

</style>

</head>

<body>

<div id="sidebar">

<div class="header">

<h1>Bài: Từ trường - Vật lí lớp 12</h1>

<p class="subtitle">Phòng Thí nghiệm ảo - Sản phẩm của Nhóm 7</p>

<button id="btn-close-tools" onclick="toggleMobileTools()">⬇️Thu gọn thanh công


cụ</button>

</div>

<div class="tabs">

<div style="font-size: 11.5px; color: #8b949e; text-transform: uppercase; font-weight: 700;


margin: 5px 0 8px 5px; letter-spacing: 0.5px;">📚 Tiến trình</div>

<button class="tab-btn active" onclick="switchTab(1)"> 1. Tương tác từ & Thí nghiệm


Oersted</button>

<button class="tab-btn" onclick="switchTab(2)">✨ 2. Thí nghiệm quan sát Từ Phổ</button>

<button class="tab-btn" onclick="switchTab(3)">✋ 3. Luyện tập quy tắc bàn tay phải</button>

</div>

<div id="panel-1" class="panel active">

<div style="font-size: 11.5px; color: #8b949e; text-transform: uppercase; font-weight: 700;


margin: 5px 0 5px 0; letter-spacing: 0.5px;">🧪 Được thiết lập sẵn</div>

<button class="action-btn btn-oersted" onclick="setupOerstedExp()">🔍 Thí nghiệm Oersted


(Trang 56 SGK)</button>
<div style="font-size: 11.5px; color: #8b949e; text-transform: uppercase; font-weight: 700;
margin: 15px 0 5px 0; letter-spacing: 0.5px;">🧰 Dụng cụ</div>

<button class="action-btn" onclick="spawnMagnet()"><span style="display: inline-block;


width: 35px; text-align: center; white-space: nowrap; letter-spacing: -2px;">🟥🟦</span> Nam châm
thẳng</button>

<button class="action-btn" onclick="spawnMagneticNeedle()"><span style="display: inline-


block; width: 35px; text-align: center;">🧭</span> Kim nam châm (La bàn)</button>

<div style="background: transparent; border: 1px solid #30363d; border-radius: 8px; padding:
10px; display: flex; flex-direction: column; gap: 8px;">

<span style="color: #c9d1d9; font-size: 13.5px; font-weight: 600; padding-left: 2px;">⚡ Dây
dẫn thẳng:</span>

<button class="action-btn" onclick="spawnPointWire()"><span style="display: inline-block;


width: 35px; text-align: center;">⊙/⊗</span> Đặt vuông góc mặt phẳng</button>

<button class="action-btn" onclick="spawnFlexibleWire()"><span style="display: inline-


block; width: 35px; text-align: center;">➖</span> Đặt nằm trên mặt phẳng</button>

</div>

<div id="prop-panel">

<div id="prop-header" style="display: flex; justify-content: space-between; align-items:


center; margin-bottom: 10px; border-bottom: 1px dashed #30363d; padding-bottom: 10px; cursor:
grab; touch-action: none;">

<h3 id="prop-title" style="margin: 0; color: #58a6ff; font-size: 15px;">⚙️Tùy chỉnh Vật


thể</h3>

<button class="action-btn" onclick="closePropPanel()" style="background: transparent;


border: none; font-size: 16px; padding: 0; margin: 0; color: #f85149; box-shadow:
none;">❌</button>

</div>

<div class="slider-box" id="scale-control">

<label>Kích thước: <span id="scaleVal">100%</span></label>

<input type="range" id="scaleSlider" min="50" max="250" value="100">

</div>

<div id="group-magnet" style="display: none;">

<div class="slider-box">

<label>Độ mạnh từ trường: <span id="magStrengthVal">100%</span></label>


<input type="range" id="magStrengthSlider" min="20" max="300" value="100">

</div>

</div>

<div id="group-wire" style="display: none;">

<div class="slider-box">

<label>Cường độ dòng điện (I): <span id="wireCurrentVal">100%</span></label>

<input type="range" id="wireCurrentSlider" min="10" max="300" value="100">

</div>

<button class="action-btn" id="btnToggleOnOff" style="width: 100%; justify-content:


center; background: #1f6feb; margin-bottom: 10px;">TẮT DÒNG ĐIỆN</button>

</div>

<div class="slider-box" id="angle-control">

<label>Góc xoay: <span id="angleVal">0°</span></label>

<input type="range" id="angleSlider" min="0" max="360" value="0">

</div>

<div class="prop-row">

<div style="display: flex; align-items: center; gap: 5px;">

<input type="checkbox" id="chkPin">

<label for="chkPin" style="color: #f1c40f; font-weight: bold; cursor: pointer; margin:


0;">📌 Cố định</label>

</div>

<button class="action-btn" id="btnFlipMagnet" style="padding: 5px 15px; background:


#d35400;">🔄 Đảo 180°</button>

</div>

</div>

<button class="action-btn" id="btnDeleteEntity" style="width: 100%; justify-content: center;


background: #f85149; margin-top: 10px;"> XÓA VẬT THỂ NÀY</button>

<hr style="border: 0; border-top: 1px dashed #30363d; width: 100%; margin: 15px 0 5px 0;">

<button class="action-btn" onclick="clearAll()" style="color: #f85149; justify-content:center;">


Dọn dẹp bàn thí nghiệm</button>
</div>

<div id="panel-2" class="panel">

<div style="font-size: 11.5px; color: #8b949e; text-transform: uppercase; font-weight: 700;


margin: 5px 0 5px 0; letter-spacing: 0.5px;">🧰 DỤNG CỤ</div>

<button class="action-btn" onclick="setupTab2('magnet_straight')"><span style="display:


inline-block; width: 35px; text-align: center; white-space: nowrap; letter-spacing: -2px;">🟥🟦</span>
Nam châm thẳng</button>

<button class="action-btn" onclick="setupTab2('magnet_u')"><span style="display: inline-


block; width: 35px; text-align: center;">🧲</span> Nam châm chữ U</button>

<button class="action-btn" onclick="setupTab2('wire_straight')"><span style="display: inline-


block; width: 35px; text-align: center;">⚡</span> Dây thẳng mang dòng điện</button>

<button class="action-btn" onclick="setupTab2('solenoid')"><span style="display: inline-block;


width: 35px; text-align: center;">🔂</span> Ống dây mang dòng điện</button>

<button class="action-btn" onclick="spawnMagneticNeedle()"><span style="display: inline-block;


width: 35px; text-align: center;">🧭</span> Kim nam châm (La bàn)</button>

<div style="margin-top: 15px; border-top: 1px dashed #30363d; padding-top: 15px;


display:flex; flex-direction:column; gap:10px;">

<div style="font-size: 11.5px; color: #8b949e; text-transform: uppercase; font-weight: 700;


margin-bottom: 2px; letter-spacing: 0.5px;">✨ THAO TÁC</div>

<button class="action-btn btn-special" onclick="sprinkleIronFilings()">1. Rắc mạt sắt (Rắc


càng nhiều càng dày)</button>

<button class="action-btn btn-tap" onclick="tapLightly()">2. Gõ nhẹ</button>

<button class="action-btn" onclick="clearFilings()" style="justify-content:center;">🧹 Quét


sạch mạt sắt</button>

</div>

</div>

<div id="panel-3" class="panel">

<div style="font-size: 11.5px; color: #8b949e; text-transform: uppercase; font-weight: 700;


margin: 5px 0 8px 0; letter-spacing: 0.5px;">📝 KIỂM TRA NHANH</div>

<p style="font-size: 13.5px; color:#c9d1d9; line-height: 1.6; margin-bottom: 15px;">


Cùng thử sức với các câu hỏi trắc nghiệm để củng cố kiến thức nhé! Bài tập sẽ được chọn
ngẫu nhiên nhưng đảm bảo bao quát đủ 3 trường hợp cơ bản: <b>Dòng điện thẳng</b>, <b>Dòng
điện tròn</b> và <b>Ống dây</b>.

</p>

<button class="action-btn btn-special" onclick="showTheory()">📖 Xem Lý thuyết & Bắt


đầu</button>

</div>

<div style="margin-top: auto; padding: 15px; border-top: 1px solid #30363d; text-align: center;
font-size: 11.5px; color: #8b949e; line-height: 1.6; background: rgba(0,0,0,0.2);">

<b>Đồ án kết thúc học phần:</b><br>

Trí tuệ nhân tạo trong giáo dục<br>

Khoa CNTT - Trường ĐH Sư phạm [Link]

</div>

</div>

<div id="canvas-container">

<button id="btn-info" onclick="openInfoModal()" title="Hướng dẫn sử dụng">i</button>

<button id="btn-settings" onclick="toggleSettingsMenu()" title="Cài đặt hiển thị">⚙️</button>

<button id="btn-oersted-guide" class="icon-btn" onclick="toggleOerstedGuide()" style="display:


none;" title="Hướng dẫn thí nghiệm Oersted">📜</button>

<button id="btn-open-prop" onclick="showPropPanel()" style="display: none; position: absolute;


bottom: 85px; right: 15px; background: #58a6ff; color: white; border: none; padding: 10px 18px;
border-radius: 20px; font-weight: bold; z-index: 85; box-shadow: 0 4px 15px rgba(0,0,0,0.5);">⚙️Tùy
chỉnh vật</button>

<button id="btn-mobile-tools" onclick="toggleMobileTools()"> Thanh công cụ</button>

<div id="settings-menu">

<div class="toggle-box">

<input type="checkbox" id="chkFieldLines">

<label for="chkFieldLines" style="cursor: pointer;">Hiển thị đường sức từ (Vector)</label>

</div>

<div class="toggle-box">
<input type="checkbox" id="chkEarthField">

<label for="chkEarthField" style="color: #2ea043; cursor: pointer;">🌍 Bật Từ trường Trái


Đất</label>

</div>

<div class="toggle-box">

<input type="checkbox" id="chkMobileMode">

<label for="chkMobileMode" style="cursor: pointer;">📱 Chế độ Điện thoại (Dành cho máy
tính để bàn)</label>

</div>

</div>

<div id="info-modal" class="overlay-screen" style="z-index: 100;">

<div class="theory-box" style="max-width: 650px; text-align: left; padding: 30px; box-shadow:


0 10px 50px rgba(0,0,0,0.9);">

<h2 style="color: #f1c40f; margin-top: 0; text-align: center; border-bottom: 1px solid


#30363d; padding-bottom: 15px;">

💡 HƯỚNG DẪN SỬ DỤNG VÀ THAO TÁC

</h2>

<div style="font-size: 15.5px; line-height: 1.8; color: #c9d1d9; margin: 20px 0;">

<p>🔹 <b>Kéo thả & Xóa:</b> Dùng chuột di chuyển vật thể để xem tương tác lực từ.
<b>Nhấn đúp chuột (Double-click)</b> lên vật thể bất kỳ để xóa nó khỏi bàn.</p>

<p>🔹 <b>Bảng Tùy chỉnh:</b> Click chuột 1 lần vào vật thể để mở bảng thông số bên trái
(thay đổi kích thước, cường độ I, độ mạnh từ trường, góc xoay...).</p>

<p>🔹 <b>Cài đặt hiển thị (⚙️):</b> Nhấn biểu tượng Bánh răng ở góc phải phía trên để
bật/tắt Vector đường sức từ và Từ trường Trái Đất.</p>

<p>🔹 <b>Thí nghiệm quan sát Từ phổ (Tab 2):</b> Dùng để trực quan hóa từ trường. Hãy
nhớ nhấn rắc mạt sắt và <i>"Gõ nhẹ"</i> để quan sát mạt sắt sắp xếp thành từ phổ.</p>

<p>🔹 <b>Luyện tập (Tab 3):</b> Hệ thống tự động kiểm tra kỹ năng vận dụng <i>Quy tắc
nắm bàn tay phải</i>, giúp củng cố kiến thức ngay sau khi thực hành.</p>

</div>

<div style="text-align: center; margin-top: 25px;">

<button class="action-btn" style="margin: 0 auto; padding: 10px 35px; font-size: 15px;


justify-content: center;" onclick="closeInfoModal()">Đã hiểu</button>

</div>

</div>
</div>

<div id="welcome-modal" class="overlay-screen" style="display: flex; z-index: 100;">

<div class="theory-box" style="max-width: 650px; text-align: left; padding: 35px; box-shadow:


0 10px 50px rgba(0,0,0,0.9);">

<h2 style="color: #58a6ff; margin-top: 0; margin-bottom: 20px; text-align: center; border-


bottom: 1px solid #30363d; padding-bottom: 15px;">

🔬 PHÒNG THÍ NGHIỆM ẢO: TỪ TRƯỜNG

</h2>

<div style="font-size: 15px; line-height: 1.7; color: #c9d1d9;">

<p>Chào mừng các em đến với hệ thống mô phỏng tương tác trực quan, bám sát nội
dung <b>Bài 14: Từ trường (Sách giáo khoa Vật lí 12 - Kết Nối Tri Thức)</b>.</p>

<p style="margin-top: 15px; color: #f1c40f; font-weight: bold;">✨ Thông tin dự án:</p>

<ul style="padding-left: 20px; margin-top: 5px; margin-bottom: 20px;">

<li style="margin-bottom: 8px;"><b>Thực hiện:</b> Nguyễn Trương Thiên Phúc -


Thành viên nhóm 7.</li>

<li style="margin-bottom: 8px;"><b>Công cụ:</b> Google AI Studio.</li>

<li style="margin-bottom: 8px;"><b>Mục đích:</b> Số hóa học liệu, tổ chức hoạt động
khám phá và thực hành trực quan nhằm phát huy phẩm chất, năng lực học sinh theo tinh thần
<b>Công văn 5512/BGDĐT-GDTrH</b>.</li>

<li style="margin-bottom: 8px;"><b>Tính năng:</b> Tương tác trực tiếp với nam châm,
quan sát từ phổ mạt sắt, thực hiện thí nghiệm Oersted và rèn luyện kỹ năng qua hệ thống câu hỏi
Quy tắc nắm bàn tay phải.</li>

</ul>

</div>

<div style="text-align: center; margin-top: 25px;">

<button class="btn-special" style="padding: 12px 40px; font-size: 16px; border-radius:


8px;" onclick="closeWelcomeModal()">Tiến hành thí nghiệm 🚀</button>

</div>

</div>

</div>

<!-- BẢNG HƯỚNG DẪN OERSTED (CÓ THỂ ĐÓNG MỞ) -->

<div id="oersted-tooltip" style="display: none;">


<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom:
8px; border-bottom: 1px dashed #30363d; padding-bottom: 8px;">

<h3 style="margin: 0; color: #58a6ff; font-size: 14px;">💡 Hướng dẫn Thí nghiệm
Oersted</h3>

<button onclick="toggleOerstedGuide()" style="background: transparent; border: none;


font-size: 14px; cursor: pointer; color: #f85149; padding: 0;">❌</button>

</div>

<p style="margin-bottom: 8px;"><b>Bước 1:</b> Khởi tạo thí nghiệm với Từ trường Trái Đất
<b>đang tắt</b>. Dây dẫn được bố trí nằm dọc (theo trục Bắc - Nam). Kim nam châm định hướng tự
do.</p>

<p style="margin-bottom: 8px;"><b>Bước 2:</b> Click vào dây điện đang hiển thị trên màn
hình rồi nhấn <b style="color:#1f6feb;">BẬT DÒNG ĐIỆN</b>. Quan sát thấy kim nam châm xoay
lệch đi và nằm <b>vuông góc (90°)</b> so với dây dẫn. Nguyên nhân của hiện tượng này sẽ được giải
đáp khi em học đến <b>Tab 3. Luyện tập quy tắc bàn tay phải</b>.</p>

<p style="margin-bottom: 8px;"><b>Bước 3:</b> Click chọn ô biểu tượng hình bánh răng rồi
chọn ô <b style="color:#2ea043;">Bật Từ trường Trái Đất</b>. Lúc này, kim nam châm không còn
nằm vuông góc mà lệch chếch đi một góc. Hiện tượng này xảy ra do kim đang chịu ảnh hưởng đồng
thời của hai nguồn từ trường: từ trường do dòng điện sinh ra và từ trường tự nhiên của Trái
Đất.</p>

<p style="margin-top: 15px; font-style: italic; color: #f1c40f; border-top: 1px dashed #30363d;
padding-top: 10px;"><b>📌 Nhiệm vụ khám phá:</b> Các em hãy thử đảo chiều dòng điện và rút ra
nhận xét về sự thay đổi hướng lệch của kim nam châm nhé.</p>

</div>

<div id="theory-screen" class="overlay-screen">

<div class="theory-box">

<h2>📖 QUY TẮC NẮM BÀN TAY PHẢI</h2>

<div class="theory-grid">

<div class="theory-card">

<h3>1. Dòng điện thẳng</h3>

<p><b>Ngón cái</b> chỉ chiều dòng điện (I). Chiều khum của 4 ngón tay là chiều của
các <b>đường tròn đồng tâm</b>.</p>

</div>
<div class="theory-card">

<h3>2. Dòng điện tròn</h3>

<p>Khum <b>4 ngón tay</b> theo chiều dòng điện (I). <b>Ngón cái</b> chỉ chiều của
đường sức từ đi qua tâm (chiều ra cực Bắc).</p>

</div>

<div class="theory-card">

<h3>3. Ống dây mang dòng điện (Solenoid)</h3>

<p>Khum <b>4 ngón tay</b> theo chiều dòng điện (I) quấn trên ống. <b>Ngón cái</b>
chỉ chiều đường sức từ bên trong ống (hướng về đầu Cực Bắc).</p>

</div>

</div>

<div style="text-align: center; margin-top: 25px;">

<button class="btn-special" style="padding: 12px 30px; margin-bottom: 60px; font-size: 16px;


border-radius: 8px; margin-bottom: 60px; position: relative; bottom: 20px;"
onclick="startRightHandGame()">🚀 Đã hiểu! Bắt đầu</button>

</div>

</div>

</div>

<div id="game-ui">

<div id="game-progress">Câu hỏi: 1 / 5</div>

<div id="game-question">Câu hỏi sẽ hiện ở đây</div>

<div class="game-btn-group" id="game-answers"></div>

<div id="game-feedback"></div>

<button id="game-next-btn" class="action-btn" style="margin-top: 15px; margin-left: auto;


margin-right: auto; display: none;" onclick="processNextQuestion()">Tiếp tục </button>

</div>

<div id="end-screen" class="overlay-screen">

<div class="summary-box">

<h2>🎉 XUẤT SẮC! HOÀN THÀNH BÀI HỌC</h2>

<div class="summary-content">
<p style="font-size: 18px; color:#58a6ff; text-align:center; font-weight:bold; margin-
bottom: 15px;">Em đã học:</p>

<ul>

<li><span>Từ trường</span> là trường lực gây ra bởi dòng điện hoặc nam châm, là
một dạng vật chất tồn tại xung quanh dòng điện hoặc nam châm mà biểu hiện cụ thể là sự xuất hiện
của lực từ tác dụng lên một dòng điện hay một nam châm khác đặt trong nó.</li>

<li><span>Đường sức từ</span> là những đường vẽ ở trong không gian có từ trường


sao cho tiếp tuyến với nó tại mỗi điểm trùng với phương của vectơ cảm ứng từ tại điểm đó. Chiều
của đường sức từ là chiều của vectơ cảm ứng từ.</li>

<li><span>Quy tắc nắm bàn tay phải</span> dùng để xác định chiều của đường sức từ
hoặc chiều dòng điện một cách chính xác.</li>

</ul>

</div>

<button class="action-btn" style="margin: 0 auto; padding: 10px 20px;"


onclick="switchTab(1)">🔄 Trở lại Phòng Thí Nghiệm</button>

</div>

</div>

</div>

<script>

// ================= STATE & GLOBALS =================

let currentTab = 1;

let entities = [];

let ironFilings = [];

let isFilingsTapped = false;

let draggedEntity = null;

let selectedEntity = null;

let dragOffsetX = 0, dragOffsetY = 0;

let mouseWasDragged = false;

let drawFieldLines = false;

let earthFieldActive = false;

// Game State
let gameActive = false;

let currentQ = 1; const MAX_Q = 5;

let currentQData = {};

let particles = [];

let questionPool = [];

// UI Elements

const propPanel = [Link]('prop-panel');

const groupMagnet = [Link]('group-magnet');

const groupWire = [Link]('group-wire');

const scaleSlider = [Link]('scaleSlider');

const scaleVal = [Link]('scaleVal');

const magStrengthSlider = [Link]('magStrengthSlider');

const magStrengthVal = [Link]('magStrengthVal');

const wireCurrentSlider = [Link]('wireCurrentSlider');

const wireCurrentVal = [Link]('wireCurrentVal');

const angleSlider = [Link]('angleSlider');

const angleVal = [Link]('angleVal');

const chkPin = [Link]('chkPin');

const btnToggleOnOff = [Link]('btnToggleOnOff');

const btnFlipMagnet = [Link]('btnFlipMagnet');

function setup() {

let container = [Link]('canvas-container');

let canvas = createCanvas([Link], [Link]);

[Link]('canvas-container');

textAlign(CENTER, CENTER); rectMode(CENTER);

[Link]('chkFieldLines').addEventListener('change', e => drawFieldLines =


[Link]);

[Link]('chkEarthField').addEventListener('change', e => earthFieldActive =


[Link]);
// Lệnh bật/tắt giao diện điện thoại

// Lệnh bật/tắt giao diện điện thoại (đã sửa lỗi mất thanh công cụ)

[Link]('chkMobileMode').addEventListener('change', e => {

if ([Link]) {

[Link]('mobile-mode');

} else {

// Khi tắt chế độ điện thoại, phải dọn dẹp sạch các class liên quan

[Link]('mobile-mode');

[Link]('show-tools');

// Ép hệ thống tính toán lại kích thước bàn thí nghiệm sau 100ms

setTimeout(() => {

windowResized();

}, 100);

});

[Link]('input', (e) => {

if (selectedEntity) { [Link] = [Link] / 100; [Link] =


[Link] + '%'; }

});

[Link]('input', (e) => {

// THÊM ĐIỀU KIỆN CHẤP NHẬN CẢ NAM CHÂM CHỮ U (u_magnet)

if (selectedEntity && ([Link] === 'magnet' || [Link] === 'u_magnet')) {

[Link] = [Link] / 100;

[Link] = [Link] + '%';

isFilingsTapped = false; // Bắt buộc mạt sắt phải được gõ lại nếu thay đổi lực

});

[Link]('input', (e) => {

if (selectedEntity && typeof [Link] !== 'undefined') {


[Link] = [Link] / 100; [Link] =
[Link] + '%';

});

[Link]('input', (e) => {

if (selectedEntity && typeof [Link] !== 'undefined') {

[Link] = radians([Link]); [Link] = [Link] + '°';


isFilingsTapped = false;

});

[Link]('change', (e) => {

if (selectedEntity) { [Link] = [Link]; if ([Link])


{ [Link] = 0; [Link] = 0; } }

});

[Link]('click', () => {

if (selectedEntity && typeof [Link] !== 'undefined') {

[Link] = ![Link];

[Link] = [Link] ? "TẮT DÒNG ĐIỆN" : "BẬT DÒNG


ĐIỆN";

[Link] = [Link] ? "#1f6feb" : "#f85149";

isFilingsTapped = false;

});

[Link]('click', () => {

if (selectedEntity) {

[Link] += PI;

let deg = (degrees([Link]) % 360 + 360) % 360;

[Link] = deg; [Link] = [Link](deg) + '°'; isFilingsTapped = false;


}

});

// Dán đoạn này ngay trên switchTab(1);

[Link]('btnDeleteEntity').addEventListener('click', () => {

if (selectedEntity) {

entities = [Link](e => e !== selectedEntity);

clearFilings(); // Xóa mạt sắt nếu có để tránh lỗi hiển thị

[Link] = 'none';

selectedEntity = null;

});

switchTab(1);

function draw() {

background(11, 15, 25);

drawGrid();

if (currentTab === 1 && earthFieldActive) drawCompassRose();

if (drawFieldLines && (currentTab === 1 || currentTab === 2)) renderPerfectFieldLines();

if (currentTab === 1) {

updatePhysics();

// 1. Cập nhật vị trí và trạng thái của tất cả vật thể trước

for (let e of entities) { [Link](); }

// 2. Ép buộc vẽ LA BÀN ra trước (Luôn nằm ở lớp dưới cùng)

for (let e of entities) {

if ([Link] === 'magnetic_needle') [Link]();

}
// 3. Sau đó mới vẽ DÂY ĐIỆN và các vật thể khác đè lên trên

for (let e of entities) {

if ([Link] !== 'magnetic_needle') [Link]();

else if (currentTab === 2) {

// Tab 2: Vẽ vật thể ở giữa màn hình

for (let e of entities) {

[Link]();

[Link]();

// Gọi hàm vẽ mạt sắt ra màn hình

updateAndDrawIronFilings();

else if (currentTab === 3 && gameActive) {

drawGameScene();

if ([Link]('end-screen').[Link] === 'flex') drawConfetti();

function drawGrid() {

stroke(25, 30, 40, 150); strokeWeight(1);

for (let i = 0; i < width; i += 40) line(i, 0, i, height);

for (let j = 0; j < height; j += 40) line(0, j, width, j);

function drawCompassRose() {

push(); translate(70, 70); stroke(80); strokeWeight(2);

line(0, -35, 0, 35); line(-35, 0, 35, 0);


fill(232, 17, 35); noStroke(); triangle(-6, -12, 6, -12, 0, -40);

fill(200); textSize(14); textStyle(BOLD); text("B", 0, -52); text("N", 0, 50); text("Đ", 50, 0); text("T", -
50, 0);

fill(88, 166, 255); textSize(11); textStyle(NORMAL); text("Địa lý", 0, 65); pop();

function switchTab(tabId) {

currentTab = tabId;

clearAll();

closeAllOverlays();

gameActive = false;

[Link]('.tab-btn').forEach((btn, i) => [Link]('active', i + 1 ===


tabId));

[Link]('.panel').forEach((panel, i) => [Link]('active', i + 1 ===


tabId));

// Quản lý nút Cuộn thư 📜 (Hướng dẫn Oersted)

let btnGuide = [Link]('btn-oersted-guide');

if (btnGuide) [Link] = (tabId === 1) ? 'flex' : 'none';

// --- BỔ SUNG CHO BƯỚC C ---

// Ẩn tất cả các bảng và nút tùy chỉnh khi đổi Tab

let oerstedTooltip = [Link]('oersted-tooltip');

if (oerstedTooltip) [Link] = 'none';

let btnOpen = [Link]('btn-open-prop');

if (btnOpen) [Link] = 'none';

let propPanel = [Link]('prop-panel');

if (propPanel) [Link] = 'none';

}
function clearAll() {

entities = []; ironFilings = []; isFilingsTapped = false;

selectedEntity = null; [Link] = 'none';

[Link]('oersted-tooltip').[Link] = 'none';

function clearFilings() { ironFilings = []; isFilingsTapped = false; }

function closeAllOverlays() {

[Link]('theory-screen').[Link] = 'none';

[Link]('game-ui').[Link] = 'none';

[Link]('end-screen').[Link] = 'none';

// ================= CÁC HÀM SPAWN VẬT THỂ (ĐÃ TỐI ƯU VỊ TRÍ MOBILE) =================

function getSafeSpawnY() {

let isMobile = [Link] <= 768 || [Link]('mobile-mode');

// Nếu là mobile, sinh ra ở 20% chiều cao màn hình, nếu máy tính thì ở giữa (50%)

return isMobile ? height * 0.2 : height * 0.5;

function spawnMagnet() {

[Link](new Magnet(width/2 + random(-15,15), getSafeSpawnY() + random(-15,15)));

function spawnMagneticNeedle() {

[Link](new MagneticNeedle(width/2 + random(-15,15), getSafeSpawnY() + 80 + random(-


15,15)));

function spawnPointWire() {

[Link](new PointWire(width/2 + random(-15,15), getSafeSpawnY() + random(-15,15)));


}

function spawnFlexibleWire() {

// Dây nằm ngang nên cần căn chỉnh Y đặc biệt hơn một chút

[Link](new FlexibleWire(getSafeSpawnY() + [Link] * 30));

// ================= VẬT LÝ VA CHẠM (NO JITTER, ĐẨY TRƯỢT TỰ NHIÊN) =================

function updatePhysics() {

for (let e of entities) { [Link] = 0; [Link] = 0; }

for (let i = 0; i < [Link]; i++) {

for (let j = i + 1; j < [Link]; j++) {

let A = entities[i]; let B = entities[j];

// Tương tác Nam châm

if ([Link] === 'magnet' && [Link] === 'magnet') {

let d = max(dist([Link].x, [Link].y, [Link].x, [Link].y), 1);

let targetDist = 145 * max([Link], [Link]);

let pA = [Link](); let pB = [Link]();

let dNN = dist(pA.N.x, pA.N.y, pB.N.x, pB.N.y); let dSS = dist(pA.S.x, pA.S.y, pB.S.x, pB.S.y);

let dNS = dist(pA.N.x, pA.N.y, pB.S.x, pB.S.y); let dSN = dist(pA.S.x, pA.S.y, pB.N.x, pB.N.y);

let minDist = min(dNN, dSS, dNS, dSN);

if (d < targetDist) {

let overlap = targetDist - d;

let pDir = createVector([Link].x - [Link].x, [Link].y - [Link].y).normalize();

if (minDist === dNS || minDist === dSN) {

// NẾU HÚT -> Đứng im, Snap cứng


if (![Link] && ![Link]) { [Link]([Link]().mult(overlap / 2));
[Link]([Link]().mult(overlap / 2)); }

else if (![Link]) { [Link]([Link]().mult(overlap)); } else if (![Link])


{ [Link]([Link]().mult(overlap)); }

[Link] *= 0; [Link] *= 0; [Link] *= 0; [Link] *= 0;

} else {

// NẾU ĐẨY -> Không Snap, đẩy văng ra theo hướng vector nối tâm

[Link](pDir.x*40, pDir.y*40); [Link](-pDir.x*40, -pDir.y*40);

continue;

else if (d < 350) {

let fMag = min(20, 25000 / (d*d));

let dirX = ([Link].x - [Link].x)/d; let dirY = ([Link].y - [Link].y)/d;

if (minDist === dNN || minDist === dSS) { [Link](dirX*fMag, dirY*fMag);


[Link](-dirX*fMag, -dirY*fMag); }

else { [Link](-dirX*fMag, -dirY*fMag); [Link](dirX*fMag, dirY*fMag); }

// Lorentz (Tương tác Nam châm và Dây)

if (([Link] === 'magnet' && [Link] === 'point_wire') || ([Link] === 'point_wire' && [Link] ===
'magnet')) {

let mag = [Link] === 'magnet' ? A : B; let wire = [Link] === 'magnet' ? B : A;

if (![Link]) continue;

let d = max(dist([Link].x, [Link].y, [Link].x, [Link].y), 1);

if (d < 300 && d > 30) {

let B_field = getNetBField([Link].x, [Link].y, wire);

let forceMag = [Link] * 0.5; let Fx = -[Link] * forceMag *


B_field.y; let Fy = [Link] * forceMag * B_field.x;

[Link](Fx, Fy); [Link](-Fx, -Fy);

}
// === TÍNH NĂNG MỚI ĐÃ ĐƯỢC "THUẦN HÓA" (LỰC AMPÈRE) ===

if ([Link] === 'point_wire' && [Link] === 'point_wire') {

if ([Link] && [Link]) {

let d = max(dist([Link].x, [Link].y, [Link].x, [Link].y), 1);

let targetDist = (A.r * [Link] + B.r * [Link]) + 4;

let isAttracting = ([Link] === [Link]);

// "Vùng dính" (Snap Zone) được nới rộng ra +3 pixel để bắt dính từ xa một cách mượt

if (isAttracting && d < targetDist + 3) {

let overlap = targetDist - d;

let pDir = createVector([Link].x - [Link].x, [Link].y - [Link].y).normalize();

if (![Link] && ![Link]) { [Link]([Link]().mult(overlap / 2));


[Link]([Link]().mult(overlap / 2)); }

else if (![Link]) { [Link]([Link]().mult(overlap)); } else if (![Link])


{ [Link]([Link]().mult(overlap)); }

// Đóng băng toàn bộ vận tốc để 2 dây ngoan ngoãn nằm im

[Link] = 0; [Link] = 0; [Link] = 0; [Link] = 0;

else if (!isAttracting && d < targetDist) {

// Đẩy văng ra dứt khoát nếu vô tình đè lên nhau

let pDir = createVector([Link].x - [Link].x, [Link].y - [Link].y).normalize();

[Link](pDir.x * 20, pDir.y * 20); [Link](-pDir.x * 20, -pDir.y * 20);

else if (d < 400) {

// Kìm hãm gia tốc: Giới hạn lực hút/đẩy tối đa là 5 (min(5, ...)) để trượt đến nhau
thanh lịch

let forceMag = min(5, (1000 / d) * [Link] * [Link]);

let dirX = ([Link].x - [Link].x) / d;


let dirY = ([Link].y - [Link].y) / d;

if (isAttracting) {

[Link](-dirX * forceMag, -dirY * forceMag);

[Link](dirX * forceMag, dirY * forceMag);

} else {

[Link](dirX * forceMag, dirY * forceMag);

[Link](-dirX * forceMag, -dirY * forceMag);

function getNetBField(x, y, excludeEntity = null) {

let netB = createVector(0, 0);

if (currentTab === 1 && earthFieldActive) [Link](createVector(0, -30));

let multiplier = (currentTab === 2) ? 4 : 1;

for (let e of entities) {

if (e === excludeEntity) continue;

if ([Link] === 'magnet') {

let p = [Link](); let bB = 4000 * multiplier * [Link];

let dN = max(dist(x,y, p.N.x,p.N.y), 5); [Link](createVector(x-p.N.x, y-


p.N.y).normalize().mult(bB/(dN*dN)));

let dS = max(dist(x,y, p.S.x,p.S.y), 5); [Link](createVector(p.S.x-x, p.S.y-


y).normalize().mult(bB/(dS*dS)));

} else if ([Link] === 'u_magnet') {

let p = [Link](); let bB = 5000 * multiplier * [Link];


let dN = max(dist(x,y, p.N.x,p.N.y), 5); [Link](createVector(x-p.N.x, y-
p.N.y).normalize().mult(bB/(dN*dN)));

let dS = max(dist(x,y, p.S.x,p.S.y), 5); [Link](createVector(p.S.x-x, p.S.y-


y).normalize().mult(bB/(dS*dS)));

} else if ([Link] === 'point_wire' && [Link]) {

let dx = [Link].x; let dy = [Link].y; let d = max(sqrt(dx*dx+dy*dy), 5);

let bMag = (1800/d) * multiplier * [Link];

[Link](createVector(dy/d * bMag * [Link], -dx/d * bMag * [Link]));

} else if ([Link] === 'solenoid' && [Link]) {

// VẬT LÝ CHUẨN: Tích phân từ trường của 15 vòng dây dẫn rời rạc

let v = createVector(x - [Link].x, y - [Link].y);

[Link](-[Link]);

let halfL = (e.w * [Link]) / 2;

let halfR = (e.h * [Link]) / 2;

let numLoops = 15;

let solenoidB = createVector(0, 0);

for (let i = 0; i < numLoops; i++) {

let lx = map(i, 0, numLoops - 1, -halfL + 5, halfL - 5);

let dxTop = v.x - lx; let dyTop = v.y - (-halfR);

let dTop = max(sqrt(dxTop*dxTop + dyTop*dyTop), 5);

let bTop = (300 / dTop) * multiplier * [Link] * [Link];

[Link](createVector(dyTop/dTop * bTop, -dxTop/dTop * bTop));

let dxBot = v.x - lx; let dyBot = v.y - halfR;

let dBot = max(sqrt(dxBot*dxBot + dyBot*dyBot), 5);

let bBot = (300 / dBot) * multiplier * [Link] * [Link];

[Link](createVector(-dyBot/dBot * bBot, dxBot/dBot * bBot));

[Link]([Link]);

[Link](solenoidB);

} else if ([Link] === 'flexible_wire' && [Link]) {


let v = createVector(x - [Link].x, y - [Link].y); [Link](-[Link]);

let d = max(abs(v.y), 5); let bMag = (1500 / d) * multiplier * [Link];

let localB = createVector(0, -[Link] * bMag); [Link]([Link]); [Link](localB);

} else if ([Link] === 'oersted_wire' && [Link]) {

let v = createVector(x - [Link].x, y - [Link].y); [Link](-[Link]);

let d = max(abs(v.x), 5); let bMag = (2000 / d) * multiplier * [Link];

let localB = createVector([Link] * bMag, 0); [Link]([Link]); [Link](localB);

return netB;

// ================= VẼ ĐƯỜNG TRÒN HOÀN HẢO =================

function renderPerfectFieldLines() {

stroke(88, 166, 255, 120); strokeWeight(1.2); noFill();

let magnets = [Link](e => [Link] === 'magnet' || [Link] === 'u_magnet' || [Link] ===
'solenoid');

let pointWires = [Link](e => [Link] === 'point_wire' && [Link]);

if ([Link] === 0) {

for (let w of pointWires) {

let numCircles = [Link](2, [Link](6 * ([Link] || 1)));

for (let r = 1; r <= numCircles; r++) {

let radius = 20 + r * 25 * [Link]; circle([Link].x, [Link].y, radius * 2);

let p5RotationDir = -[Link]; let aAng = (frameCount * 0.02 * p5RotationDir) %


TWO_PI;

let pDir = aAng + ([Link] === 1 ? -PI/2 : PI/2);

push(); translate([Link].x + cos(aAng)*radius, [Link].y + sin(aAng)*radius); rotate(pDir);

fill(255, 200, 50, 255); noStroke(); triangle(-6, -4, 6, 0, -6, 4); pop();

} else {
for (let m of magnets) {

let intensity = [Link] || [Link] || 1;

if ([Link] === 'solenoid') {

// ÉP ĐỐI XỨNG: Vẽ 1 đường ruột, 1 đường Ra Bắc, 1 đường Vào Nam

let numLines = [Link](3, [Link](9 * intensity));

if (numLines % 2 === 0) numLines++; // Luôn lẻ để có nét vẽ giữa tâm

let halfH = (m.h / 2) * [Link];

let halfW = (m.w / 2) * [Link];

for (let i = 1; i <= numLines; i++) {

let offsetY = map(i, 1, numLines, -halfH + 6, halfH - 6);

let pN = {

x: [Link].x + [Link] * halfW * cos([Link]) - offsetY * sin([Link]),

y: [Link].y + [Link] * halfW * sin([Link]) + offsetY * cos([Link])

};

let pS = {

x: [Link].x - [Link] * halfW * cos([Link]) - offsetY * sin([Link]),

y: [Link].y - [Link] * halfW * sin([Link]) + offsetY * cos([Link])

};

// 1. Vẽ ruột (thẳng tắp)

stroke(88, 166, 255, 120); strokeWeight(1.2);

line(pS.x, pS.y, pN.x, pN.y);

// 2. Mũi tên trong ruột (tỉa xen kẽ)

if (i % 2 !== 0) {

let midX = (pS.x + pN.x) / 2; let midY = (pS.y + pN.y) / 2;

let dirAng = [Link] + ([Link] === 1 ? 0 : PI);


push(); translate(midX, midY); rotate(dirAng);

fill(255, 200, 50, 255); noStroke(); triangle(-5, -3.5, 5, 0, -5, 3.5); pop();

// 3. VẼ ĐỐI XỨNG: Bắt buộc RA BẮC và VÀO NAM

let dirAng = [Link] + ([Link] === 1 ? 0 : PI);

// Lệnh 1: Đi ra cực Bắc (traceDir = 1)

traceFieldLine(pN.x + 2*cos(dirAng), pN.y + 2*sin(dirAng), magnets, pointWires, 1,


'solenoid');

// Lệnh 2: Đi vào cực Nam (traceDir = -1)

traceFieldLine(pS.x - 2*cos(dirAng), pS.y - 2*sin(dirAng), magnets, pointWires, -1,


'solenoid');

} else {

let poles = [Link]();

if(poles) {

let numLines = [Link](8, [Link](14 * intensity));

for (let i = 0; i < numLines; i++) {

let angle = (i * TWO_PI / numLines) + ([Link] || 0);

traceFieldLine(poles.N.x + cos(angle)*15*[Link], poles.N.y +


sin(angle)*15*[Link], magnets, pointWires, 1);

traceFieldLine(poles.S.x + cos(angle)*15*[Link], poles.S.y +


sin(angle)*15*[Link], magnets, pointWires, -1);

for (let w of pointWires) {

let numCircles = [Link](2, [Link](6 * ([Link] || 1)));

for (let r = 1; r <= numCircles; r++) {

traceFieldLine([Link].x + 20 + r*25*[Link], [Link].y, magnets, pointWires,


[Link]);

}
}

function traceFieldLine(startX, startY, magnets, pointWires, traceDir, source = null) {

let path = []; let px = startX, py = startY;

// Tăng giới hạn lên 2000 bước để nét vẽ đủ thời gian quay về đích

for (let step = 0; step < 2000; step++) {

[Link]({x: px, y: py});

let B = getNetBField(px, py);

if (currentTab === 1 && earthFieldActive) [Link](createVector(0, -30));

if ([Link]() < 0.01) break;

[Link]().mult(2 * traceDir); px += B.x; py += B.y;

let hit = false;

if (step > 15) {

for (let m of magnets) {

if ([Link] === 'solenoid') {

// Dừng ngay khi chạm bề mặt vỏ ống dây để khép kín vòng lặp

if ([Link](px, py)) hit = true;

} else if ([Link]) {

let p = [Link]();

if (traceDir === 1 && dist(px, py, p.S.x, p.S.y) < 15*[Link]) hit = true;

if (traceDir === -1 && dist(px, py, p.N.x, p.N.y) < 15*[Link]) hit = true;

for (let w of pointWires) { if (dist(px, py, [Link].x, [Link].y) < w.r*[Link] - 2) hit = true; }

}
if (step > 60 && dist(px, py, startX, startY) < 10) { [Link]({x: startX, y: startY}); break; }

if (hit || px < -200 || px > width+200 || py < -200 || py > height+200) { [Link]({x: px, y: py});
break; }

beginShape(); for (let pt of path) vertex(pt.x, pt.y); endShape();

// Rải mũi tên cân xứng

if ([Link] > 50) {

let ratios = [0.5]; // Ống dây chỉ cần 1 mũi tên ở đỉnh vòng cung

if (source !== 'solenoid' && [Link] > 600) ratios = [0.25, 0.5, 0.75];

for (let ratio of ratios) {

let idx = [Link]([Link] * ratio);

let pt1 = path[idx - 1]; let pt2 = path[idx];

let isAcne = false;

if (source !== 'solenoid') {

for (let m of magnets) {

if ([Link] && [Link] !== 'solenoid') {

let p = [Link]();

if (dist(pt2.x, pt2.y, p.N.x, p.N.y) < 45*[Link] || dist(pt2.x, pt2.y, p.S.x, p.S.y) <
45*[Link]) {

isAcne = true;

if (pt1 && pt2 && !isAcne) {

let angle = atan2(pt2.y - pt1.y, pt2.x - pt1.x);

if (traceDir === -1) angle += PI;


push(); translate(pt2.x, pt2.y); rotate(angle);

fill(255, 200, 50, 255); noStroke(); triangle(-6, -4, 6, 0, -6, 4); pop();

function setupTab2(type) {

clearAll();

if (type === 'magnet_straight') [Link](new Magnet(width/2, height/2));

if (type === 'magnet_u') [Link](new UMagnet(width/2, height/2));

if (type === 'wire_straight') [Link](new PointWire(width/2, height/2));

if (type === 'solenoid') [Link](new Solenoid(width/2, height/2));

function sprinkleIronFilings() {

// Ép [Link] cập nhật lại chiều rộng chuẩn của container trước khi rắc

let container = [Link]('canvas-container');

if (container) {

resizeCanvas([Link], [Link]);

isFilingsTapped = false;

let numNew = 2000;

for (let i = 0; i < numNew; i++) {

let x = random(width);

let y = random(height);

let B = getNetBField(x, y);

if (random() < map([Link](), 0, 1, 0.2, 1)) {

[Link]({
x: x, y: y,

angle: random(TWO_PI),

targetAngle: random(TWO_PI),

l: random(4, 9),

o: random(120, 255)

});

function tapLightly() { isFilingsTapped = true; }

function updateAndDrawIronFilings() {

if ([Link] === 0) return;

strokeWeight(1.2);

for (let f of ironFilings) {

if (isFilingsTapped) {

let bField = getNetBField(f.x, f.y);

if ([Link]() > 0.01) {

[Link] = [Link](); let diff = [Link] - [Link];

while (diff < -PI) diff += TWO_PI; while (diff > PI) diff -= TWO_PI; [Link] += diff * 0.15;

stroke(200, 220, 255, f.o);

push(); translate(f.x, f.y); rotate([Link]); line(-f.l/2, 0, f.l/2, 0); pop();

// ================= CÁC LỚP VẬT THỂ (CLASSES) =================

class Entity {

constructor(x, y, type) {

[Link] = createVector(x, y); [Link]=0; [Link]=0; [Link]=0; [Link]=0;

[Link]=type; [Link]=1; [Link]=0.75; [Link]=0;


[Link] = false; [Link] = 1.0;

applyForce(fx, fy) { [Link] += fx; [Link] += fy; }

update() {

if ([Link]) { [Link] = 0; [Link] = 0; }

else if (draggedEntity !== this && [Link] !== 'flexible_wire') {

[Link] += [Link]; [Link] += [Link]; [Link] *= [Link]; [Link] *= [Link];

[Link]([Link], [Link]); [Link].x = constrain([Link].x, 50, width-50); [Link].y =


constrain([Link].y, 50, height-50);

isMouseOver(mx, my) { return false; }

// OERSTED CHUẨN XÁC DÂY DỌC

class OerstedWire extends Entity {

constructor(x, y) {

super(x, y, 'oersted_wire');

this.w = 20; this.h = height - 100;

[Link] = -1;

[Link] = false; [Link] = 1.0;

[Link] = true;

// Cập nhật vùng chọn chuột để nó xoay theo dây

isMouseOver(mx, my) {

let localM = createVector(mx - [Link].x, my - [Link].y);

[Link](-[Link]);

return localM.x > -(this.w*[Link])/2 && localM.x < (this.w*[Link])/2 && localM.y
> -(this.h*[Link])/2 && localM.y < (this.h*[Link])/2;

toggleCurrent() { [Link] *= -1; }


display() {

push();

translate([Link].x, [Link].y);

rotate([Link]); // <-- Lệnh này giúp dây xoay đồ họa

scale([Link]);

fill(70); noStroke();

rect(0, -this.h/2, 26, 16, 3);

rect(0, this.h/2, 26, 16, 3);

let wC = ![Link] ? '#555' : ([Link] === -1 ? '#f1c40f' : '#e67e22');

stroke(selectedEntity === this ? '#ffffff' : wC); strokeWeight(6);

line(0, -this.h/2, 0, this.h/2);

if ([Link]) {

for (let i = 0; i < 15; i++) {

let t = ((frameCount * 0.005) + i * 0.066) % 1;

if ([Link] === -1) t = 1 - t;

let py = lerp(-this.h/2, this.h/2, t);

fill(255); noStroke(); circle(0, py, 14);

fill(255); textSize(16); textStyle(BOLD);

text([Link] === -1 ? "I ⬆" : "I ⬇", 40, 0);

pop();

function setupOerstedExp() {

clearAll(); earthFieldActive = false; [Link]('chkEarthField').checked = false;


let needle = new MagneticNeedle(width/2, height/2);

[Link] = PI/6;

[Link] = false; // Đã đổi thành KHÔNG cố định (mặc định)

[Link] = 2.5; // Thiết lập kích thước mặc định to lên 250%

[Link](needle);

let wire = new OerstedWire(width/2, height/2);

[Link](wire);

[Link]('oersted-tooltip').[Link] = 'block';

class Magnet extends Entity {

constructor(x, y) { super(x, y, 'magnet'); this.w=160; this.h=40; [Link] = 1.0; }

getPoles() { let hW=(this.w*[Link])/2; let c=cos([Link]); let s=sin([Link]); return { N:


{x:[Link].x+hW*c, y:[Link].y+hW*s}, S: {x:[Link].x-hW*c, y:[Link].y-hW*s} }; }

isMouseOver(mx, my) { return dist(mx, my, [Link].x, [Link].y) < (this.w*[Link])/2 + 10; }

update() { [Link](); } // Đã xóa Torque tự lật

display() {

push(); translate([Link].x, [Link].y); rotate([Link]); scale([Link]);

fill(0, 110, 200); stroke(15); strokeWeight(2); if(selectedEntity===this) stroke(255);


rect(-this.w/4, 0, this.w/2, this.h, 4,0,0,4); fill(255); noStroke(); textSize(20); textStyle(BOLD); text('S',
-this.w/4, 1);

fill(220, 20, 40); stroke(15); strokeWeight(2); if(selectedEntity===this) stroke(255); rect(this.w/4,


0, this.w/2, this.h, 0,4,4,0); fill(255); noStroke(); textSize(20); textStyle(BOLD); text('N', this.w/4, 1);

if([Link]) { fill('#f1c40f'); noStroke(); circle(0,0,14); fill('#000'); circle(0,0,5); }

pop();

}
class UMagnet extends Entity {

constructor(x, y) { super(x, y, 'u_magnet'); this.w=100; this.h=120; [Link]=0;


[Link]=1.0; }

getPoles() {

let lxN = (this.w/2) * [Link]; let lyN = (-this.h/2+20) * [Link];

let lxS = (this.w/2) * [Link]; let lyS = (this.h/2-20) * [Link];

let c = cos([Link]); let s = sin([Link]);

return {

N: {x: [Link].x + lxN*c - lyN*s, y: [Link].y + lxN*s + lyN*c},

S: {x: [Link].x + lxS*c - lyS*s, y: [Link].y + lxS*s + lyS*c}

};

isMouseOver(mx, my) {

let v = createVector(mx - [Link].x, my - [Link].y); [Link](-[Link]);

return v.x > -(this.w*[Link])/2 && v.x < (this.w*[Link])/2 && v.y > -
(this.h*[Link])/2 && v.y < (this.h*[Link])/2;

display() {

push(); translate([Link].x, [Link].y); rotate([Link]); scale([Link]);

noFill(); stroke(150); strokeWeight(40); strokeCap(SQUARE); arc(-10, 0, this.w, this.h-40,


HALF_PI, -HALF_PI, OPEN);

line(-10, -this.h/2+20, this.w/2-20, -this.h/2+20); line(-10, this.h/2-20, this.w/2-20, this.h/2-20);

noStroke(); fill(220, 20, 40); rect(this.w/2-10, -this.h/2+20, 40, 40, 0,8,8,0); fill(0, 110, 200);
rect(this.w/2-10, this.h/2-20, 40, 40, 0,8,8,0);

fill(255); textSize(18); textStyle(BOLD); text('N', this.w/2-5, -this.h/2+20); text('S', this.w/2-5,


this.h/2-20); pop();

class Solenoid extends Entity {


constructor(x, y) { super(x, y, 'solenoid'); this.w=200; this.h=60; [Link]=1;
[Link]=true; [Link]=1.0; [Link]=0; }

isMouseOver(mx, my) {

let v = createVector(mx - [Link].x, my - [Link].y); [Link](-[Link]);

return v.x > -(this.w*[Link])/2 && v.x < (this.w*[Link])/2 && v.y > -
(this.h*[Link])/2 && v.y < (this.h*[Link])/2;

toggleCurrent() { [Link]*=-1; isFilingsTapped=false; }

getPoles() {

let lxN = ((this.w*[Link])/2)*[Link]; let lyN = 0;

let lxS = -((this.w*[Link])/2)*[Link]; let lyS = 0;

let c = cos([Link]); let s = sin([Link]);

return {

N: {x: [Link].x + lxN*c - lyN*s, y: [Link].y + lxN*s + lyN*c},

S: {x: [Link].x + lxS*c - lyS*s, y: [Link].y + lxS*s + lyS*c}

};

display() {

push(); translate([Link].x, [Link].y); rotate([Link]); scale([Link]); // Thêm lệnh


rotate

let col = [Link] ? '#f1c40f' : '#555';

if(selectedEntity===this) { stroke(255); strokeWeight(2); noFill(); rect(0,0,this.w+10, this.h+10,


10); }

fill(30, 40, 50, 200); stroke(100); strokeWeight(2); rect(0, 0, this.w, this.h, 10);

stroke(col); strokeWeight(4); noFill(); for(let i=-this.w/2+10; i<this.w/2; i+=15) ellipse(i, 0, 20,


this.h+10);

if([Link]) { fill(255); noStroke(); textSize(16); textStyle(BOLD);


text([Link]===1 ? "I ➔" : "I ⬅", 0, -this.h/2-20); }

if([Link]) { fill('#f1c40f'); noStroke(); circle(0,0,14); fill('#000'); circle(0,0,5); }


pop();

class PointWire extends Entity {

constructor(x, y) { super(x, y, 'point_wire'); this.r=25; [Link]=1; [Link]=true;


[Link]=1.0; }

isMouseOver(mx, my) { return dist(mx, my, [Link].x, [Link].y) < (this.r*[Link])+5; }

toggleCurrent() { [Link]*=-1; isFilingsTapped=false; }

display() {

push(); translate([Link].x, [Link].y); scale([Link]);

let wC = ![Link] ? '#555' : ([Link]===1 ? '#f1c40f' : '#e67e22');

fill(20); stroke(wC); strokeWeight(3); if(selectedEntity===this) stroke(255); circle(0,0,this.r*2);

strokeWeight(4); if([Link]===1){fill(wC); circle(0,0,10);}else{line(-10,-10,10,10); line(10,-


10,-10,10);}

if([Link]) { fill('#f1c40f'); noStroke(); circle(-15,-15,14); fill('#000'); circle(-15,-15,5); }

pop();

class MagneticNeedle extends Entity {

constructor(x, y) {

super(x, y, 'magnetic_needle');

this.r=20;

[Link]= 0;

[Link] = 2.5; // Thiết lập mặc định 250% cho tất cả la bàn

isMouseOver(mx, my) { return dist(mx, my, [Link].x, [Link].y) < (this.r*[Link])+5; }

update() {

[Link](); let b = getNetBField([Link].x, [Link].y, this);

if([Link]()>0.02){ let tA=[Link](); let diff=[Link]; while(diff<-PI) diff+=TWO_PI;


while(diff>PI) diff-=TWO_PI; [Link]+=diff*0.15; }
}

display() {

push(); translate([Link].x, [Link].y); rotate([Link]); scale([Link]);

fill(25, 30, 40); stroke(80); strokeWeight(2); circle(0,0,this.r*2);

fill(232, 17, 35); noStroke(); triangle(0,-4,0,4,this.r,0);

fill(0, 120, 215); triangle(0,-4,0,4,-this.r,0);

fill(255); textSize(12); textStyle(BOLD); text('N', this.r-8, 0); text('S', -this.r+8, 0);

fill(200); circle(0,0,5);

if([Link]) { fill('#f1c40f'); noStroke(); circle(0,0,10); }

pop();

class FlexibleWire extends Entity {

constructor(y) { super(width/2, y, 'flexible_wire'); this.l=width-100; [Link]=1;


[Link]=0; [Link]=true; [Link]=1.0; }

isMouseOver(mx, my) {

let localM = createVector(mx - [Link].x, my - [Link].y); [Link](-[Link]);

return localM.x > (-this.l*[Link])/2 && localM.x < (this.l*[Link])/2 &&


abs(localM.y) < 20*[Link];

toggleCurrent() { [Link]*=-1; }

update() {

if([Link]) { [Link]=0; [Link]=0; }

else if (draggedEntity !== this) {

[Link] += [Link]; [Link] += [Link]; [Link] *= [Link]; [Link] *= [Link];

[Link]([Link], [Link]); [Link].x = constrain([Link].x, 100, width-100); [Link].y =


constrain([Link].y, 100, height-100);

let fY=0; let allW = [Link](e=>[Link]==='flexible_wire');

let minDist = 999;


for(let o of allW){

if(o!==this && [Link] && [Link]){

let d = max(dist([Link].x, [Link].y, [Link].x, [Link].y), 1);

minDist = min(minDist, d);

let v = createVector([Link].x - [Link].x, [Link].y - [Link].y); [Link](-[Link]);

let myDir = [Link]([Link]).mult([Link]);

let oDir = [Link]([Link]).mult([Link]);

let parallel = [Link](oDir);

if(abs(v.y)>20 && abs(v.x) < this.l/2) { fY += (parallel * 20000 * [Link] *


[Link]) / v.y; }

let maxBend = min(60, (minDist / 2.2)); // KHÔNG BAO GIỜ ĐÂM CHÉO

[Link]=lerp([Link], constrain(fY, -maxBend, maxBend), 0.1);

if(![Link]) [Link]=lerp([Link], 0, 0.2);

display() {

push(); translate([Link].x, [Link].y); rotate([Link]); scale([Link]);

fill(70); noStroke(); rect(-this.l/2, 0, 16, 26, 3); rect(this.l/2, 0, 16, 26, 3);

let wC = ![Link] ? '#555' : ([Link]===1 ? '#f1c40f' : '#e67e22');

stroke(selectedEntity === this ? '#ffffff' : wC); strokeWeight(6); noFill();

bezier(-this.l/2, 0, 0, [Link], 0, [Link], this.l/2, 0);

if([Link]) {

for(let i=0; i<15; i++) {

let t=((frameCount*0.005) + i*0.066) % 1; if([Link]===-1) t=1-t;

let px=bezierPoint(-this.l/2, 0, 0, this.l/2, t); let py=bezierPoint(0, [Link], [Link], 0,


t);

fill(255); noStroke(); circle(px, py, 14);

fill(255); textSize(16); textStyle(BOLD); text([Link]===1?"I ➔":"I ⬅", -this.l/2+50, -20);


}

if([Link]) { fill('#f1c40f'); noStroke(); circle(-this.l/2,-15,14); fill('#000'); circle(-this.l/2,-


15,5); }

pop();

// ================= BỘ CÂU HỎI SMART POOL (BẢO ĐẢM TỈ LỆ 100%) =================

function showTheory() { [Link]('theory-screen').[Link] = 'flex'; }

let allQuestionsData = [

{ id: 's_b_1', type: 'straight_findB', dir: 1 }, { id: 's_b_-1', type: 'straight_findB', dir: -1 },

{ id: 's_i_1', type: 'straight_findI', dir: 1 }, { id: 's_i_-1', type: 'straight_findI', dir: -1 },

{ id: 'c_b_1', type: 'circle_findB', dir: 1 }, { id: 'c_b_-1', type: 'circle_findB', dir: -1 },

{ id: 'c_i_1', type: 'circle_findI', dir: 1 }, { id: 'c_i_-1', type: 'circle_findI', dir: -1 },

{ id: 'so_p_1', type: 'solenoid_findPoles', dir: 1 }, { id: 'so_p_-1', type: 'solenoid_findPoles', dir: -1 },

{ id: 'so_i_L', type: 'solenoid_findI', isLeftNorth: true }, { id: 'so_i_R', type: 'solenoid_findI',
isLeftNorth: false }

];

function initQuestionPool() {

// SỬA LỖI: Reset lại "trí nhớ" của tất cả câu hỏi trước khi bắt đầu vòng mới

for (let q of allQuestionsData) {

[Link] = false;

[Link] = false;

let straights = [Link](0, 4); let circles = [Link](4, 8); let solenoids =
[Link](8, 12);

let q1 = [Link](floor(random([Link])), 1)[0];

let q2 = [Link](floor(random([Link])), 1)[0];

let q3 = [Link](floor(random([Link])), 1)[0];


let remaining = [...straights, ...circles, ...solenoids];

let q4 = [Link](floor(random([Link])), 1)[0];

let q5 = [Link](floor(random([Link])), 1)[0];

questionPool = [q1, q2, q3, q4, q5];

for (let i = [Link] - 1; i > 0; i--) {

let j = floor(random() * (i + 1)); [questionPool[i], questionPool[j]] = [questionPool[j],


questionPool[i]];

function startRightHandGame() {

closeAllOverlays(); clearAll(); gameActive = true; currentQ = 1;

initQuestionPool(); [Link]('game-ui').[Link] = 'block';

generateQuestion();

function generateQuestion() {

[Link]('game-progress').innerText = `Câu hỏi: ${currentQ} / ${MAX_Q}`;

[Link]('game-feedback').[Link] = 'none';

[Link]('game-next-btn').[Link] = 'none';

currentQData = questionPool[currentQ - 1];

let type = [Link]; let dir = [Link];

let qText = ""; let btns = "";

let prefix = "";

// 1. Xác định chú thích dạng bài dựa vào ID câu hỏi

if ([Link]('straight')) {

prefix = "Dòng điện thẳng";

} else if ([Link]('circle')) {

prefix = "Dòng điện tròn";


} else if ([Link]('solenoid')) {

prefix = "Dòng điện trong ống dây / Nam châm điện";

// 2. Nội dung câu hỏi (Lưu ý: Mình đã đổi \n thành <br> để xuống dòng chuẩn HTML)

if (type === 'straight_findB') {

qText = `Dòng điện thẳng đang hướng ${dir === 1 ? "ĐI RA (⊙)" : "ĐI VÀO (⊗)"} mặt
phẳng.<br>Các đường sức từ là những đường tròn đồng tâm có chiều như thế nào?`;

btns = `<button class="game-btn" onclick="checkAnswer(${dir === 1 ? 0 : 1})">↺ Ngược chiều


kim đồng hồ</button>

<button class="game-btn" onclick="checkAnswer(${dir === 1 ? 1 : 0})">↻ Cùng chiều kim


đồng hồ</button>`;

else if (type === 'straight_findI') {

kim đồng hồ ↺" : "cùng chiều kim đồng hồ ↻"}.<br>Dòng điện đang hướng về phía nào?`;
qText = `Các đường sức từ là những đường tròn đồng tâm có chiều ${dir === 1 ? "ngược chiều

btns = `<button class="game-btn" onclick="checkAnswer(${dir === 1 ? 0 : 1})">⊙ Đi ra mặt


phẳng</button>

<button class="game-btn" onclick="checkAnswer(${dir === 1 ? 1 : 0})">⊗ Đi vào mặt


phẳng</button>`;

else if (type === 'circle_findB') {

qText = `Dòng điện tròn đang chạy ${dir === 1 ? "ngược chiều kim đồng hồ ↺" : "cùng chiều kim
đồng hồ ↻"}.<br>Đường sức từ đi qua tâm vòng dây có chiều hướng về đâu?`;

btns = `<button class="game-btn" onclick="checkAnswer(${dir === 1 ? 0 : 1})">⊙ Đi ra mặt


phẳng</button>

<button class="game-btn" onclick="checkAnswer(${dir === 1 ? 1 : 0})">⊗ Đi vào mặt


phẳng</button>`;

else if (type === 'circle_findI') {

qText = `Từ trường tại tâm vòng dây đang hướng ${dir === 1 ? "ĐI RA (⊙)" : "ĐI VÀO (⊗)"} mặt
phẳng.<br>Dòng điện đang chạy trên vòng dây theo chiều nào?`;

btns = `<button class="game-btn" onclick="checkAnswer(${dir === 1 ? 0 : 1})">↺ Ngược chiều


kim đồng hồ</button>
<button class="game-btn" onclick="checkAnswer(${dir === 1 ? 1 : 0})">↻ Cùng chiều kim
đồng hồ</button>`;

else if (type === 'solenoid_findPoles') {

qText = `Dòng điện chạy qua các vòng dây mặt trước của ống dây đang hướng ${dir === 1 ? "ĐI
LÊN" : "ĐI XUỐNG"}.<br>Đầu bên TRÁI của ống dây là Cực gì?`;

btns = `<button class="game-btn" onclick="checkAnswer(${dir === 1 ? 0 : 1})">Cực Bắc


(N)</button>

<button class="game-btn" onclick="checkAnswer(${dir === 1 ? 1 : 0})">Cực Nam


(S)</button>`;

else if (type === 'solenoid_findI') {

let isLeftNorth = [Link];

qText = `Đầu bên ${isLeftNorth ? 'TRÁI' : 'PHẢI'} của ống dây đang là Cực Bắc (N).<br>Chiều dòng
điện chạy qua mặt trước các vòng dây hướng thế nào?`;

btns = `<button class="game-btn" onclick="checkAnswer(${isLeftNorth ? 0 : 1})">⬆️Đi


Lên</button>

<button class="game-btn" onclick="checkAnswer(${isLeftNorth ? 1 : 0})">⬇️Đi


Xuống</button>`;

// 3. Kết hợp chú thích và câu hỏi với CSS trang trí đẹp mắt

let finalHTML = `

<span style="color: #f1c40f; font-size: 14.5px; text-transform: uppercase; letter-spacing: 0.5px;


border-bottom: 2px solid #f1c40f; padding-bottom: 2px; display: inline-block; margin-bottom:
12px;">

CÂU HỎI: ${prefix}

</span><br>

${qText}

`;

[Link]('game-question').innerHTML = finalHTML;

[Link]('game-answers').innerHTML = btns;

}
function checkAnswer(isCorrect) {

let fb = [Link]('game-feedback');

[Link] = 'block'; [Link]('game-answers').innerHTML = "";

// Ghi nhận kết quả đúng/sai vào dữ liệu của câu hỏi hiện tại

if (isCorrect === 0) {

[Link] = "✅ XUẤT SẮC! Bạn đã áp dụng chính xác quy tắc bàn tay phải."; [Link] =
"#2ea043";

[Link] = true;

} else {

[Link] = "❌ CẦN CỐ GẮNG HƠN! Hãy xem lại cách thực hiện quy tắc nắm bàn tay phải
đúng cách nhé."; [Link] = "#f85149";

[Link] = false;

[Link] = true; [Link]('game-next-btn').[Link] = 'block';

function processNextQuestion() {

if (currentQ >= MAX_Q) {

[Link]('game-ui').[Link] = 'none';

showEndScreen(); // Gọi hàm tạo bảng thống kê điểm

gameActive = false;

} else { currentQ++; generateQuestion(); }

// Hàm mới: Xử lý và hiển thị bảng thống kê động

function showEndScreen() {

let totalCorrect = 0;

let stats = {

straight: { total: 0, correct: 0, name: "Dòng điện thẳng" },


circle: { total: 0, correct: 0, name: "Dòng điện tròn" },

solenoid: { total: 0, correct: 0, name: "Ống dây / Nam châm điện" }

};

for (let q of questionPool) {

let cat = [Link]('straight') ? 'straight' : ([Link]('circle') ? 'circle' : 'solenoid');

stats[cat].total++;

if ([Link]) {

stats[cat].correct++;

totalCorrect++;

let endScreen = [Link]('end-screen');

let summaryBox = [Link]('.summary-box');

let title = ""; let color = "";

// KIỂM TRA ĐIỂM SỐ ĐỂ BẮN PHÁO HOA

if (totalCorrect === MAX_Q) { // ĐÚNG 5/5

title = "🏆 XUẤT SẮC TUYỆT ĐỐI (5/5)!";

color = "#f1c40f"; // Màu vàng Gold

[Link] = color;

createConfetti(); // KÍCH HOẠT PHÁO HOA!

else if (totalCorrect >= 3) { // Đạt nhưng chưa tối đa

title = "🎉 TỐT LẮM! HOÀN THÀNH BÀI HỌC";

color = "#2ea043"; // Màu xanh lá

[Link] = color;

particles = []; // Tắt pháo hoa

}
else { // Chưa đạt

title = "💪 CỐ GẮNG LÊN! CHƯA ĐẠT YÊU CẦU";

color = "#f85149"; // Màu đỏ

[Link] = color;

particles = []; // Tắt pháo hoa

// Giao diện bảng thống kê điểm

let statsHTML = `

<div style="background: #0d1117; padding: 15px; border-radius: 8px; margin-bottom: 20px;


text-align: left; border: 1px solid #30363d;">

<h3 style="color: #58a6ff; margin-top: 0; text-align: center; border-bottom: 1px dashed


#30363d; padding-bottom: 10px;">📊 Thống kê: Đúng ${totalCorrect}/${MAX_Q} câu</h3>

<ul style="list-style: none; padding: 0; margin: 0; font-size: 15.5px; color: #c9d1d9;">

<li style="margin-bottom: 8px;">🔹 <b>${[Link]}:</b> $


{[Link]}/${[Link]} câu</li>

<li style="margin-bottom: 8px;">🔹 <b>${[Link]}:</b> ${[Link]}/$


{[Link]} câu</li>

<li>🔹 <b>${[Link]}:</b> ${[Link]}/${[Link]}


câu</li>

</ul>

</div>

`;

// Thay đổi nội dung hiển thị dựa vào Đạt hay Không Đạt

let contentHTML = "";

if (totalCorrect >= 3) {

contentHTML = `

<div class="summary-content">

<p style="font-size: 18px; color:#58a6ff; text-align:center; font-weight:bold; margin-


bottom: 15px;">Em đã nắm vững kiến thức:</p>

<ul>
<li><span>Từ trường</span> là trường lực gây ra bởi dòng điện hoặc nam châm, là một
dạng của vật chất tồn tại xung quanh dòng điện hoặc nam châm mà biểu hiện cụ thể là sự xuất hiện
của lực từ tác dụng lên một dòng điện hay một nam châm khác đặt trong nó.</li>

<li style="margin-top: 10px;"><span>Đường sức từ</span> là những đường vẽ ở trong


không gian có từ trường sao cho tiếp tuyến với nó tại mỗi điểm trùng với phương của vectơ cảm ứng
từ tại điểm đó. Chiều của đường sức từ là chiều của vectơ cảm ứng từ.</li>

<li style="margin-top: 10px;"><span>Quy tắc nắm bàn tay phải:</span><br>

- <i>Với dòng điện thẳng:</i> Giơ ngón cái hướng theo chiều dòng điện, khum bốn
ngón tay kia xung quanh dây dẫn chỉ chiều của đường sức từ.<br>

- <i>Với dòng điện tròn và ống dây:</i> Khum bàn tay phải sao cho chiều từ cổ tay đến
các ngón tay trùng với chiều dòng điện chạy qua các vòng dây, ngón cái choãi ra chỉ chiều của đường
sức từ.

</li>

</ul>

</div>

`;

} else {

contentHTML = `

<div class="summary-content" style="text-align: center; border-color: #f85149;">

<p style="font-size: 16px; color:#c9d1d9; line-height: 1.6;">Em mới trả lời đúng <b>$
{totalCorrect}</b> câu. Đừng nản chí nhé!<br>Hãy bấm xem lại phần Lý thuyết (hoặc lật lại SGK Vật lí
12) và thử sức một lần nữa để rèn luyện thật nhuần nhuyễn Quy tắc bàn tay phải.</p>

<button class="action-btn btn-special" style="margin: 20px auto 0; padding: 12px 25px;


font-size: 15px;" onclick="startRightHandGame()">🔄 Làm lại bài tập</button>

</div>

`;

// Chèn toàn bộ vào khung hiển thị

[Link] = `

<h2 style="color: ${color}; font-size: 26px; margin-top: 0;">${title}</h2>

${statsHTML}

${contentHTML}

<button class="action-btn" style="margin: 20px auto 40px; padding: 10px 20px;"


onclick="switchTab(1)">🏠 Trở lại Phòng Thí Nghiệm</button>
`;

[Link] = 'flex';

function drawGameScene() {

let isMobile = [Link] <= 768 || [Link]('mobile-mode');

// Tùy chỉnh vị trí và tỉ lệ thu nhỏ cho điện thoại

let vOffset = isMobile ? height * 0.25 : height/2 - 70;

let qScale = isMobile ? 0.7 : 1.0;

push();

translate(width/2, vOffset);

scale(qScale); // Thu nhỏ toàn bộ hình minh họa để vừa khung hình ĐT

let type = [Link]; let dir = [Link]; let show = [Link];

if ([Link]('straight')) {

fill(20); stroke('#58a6ff'); strokeWeight(4); circle(0, 0, 70);

if (show || type === 'straight_findB') {

strokeWeight(5); if(dir === 1) { fill('#58a6ff'); circle(0, 0, 16); } else { line(-15,-15,15,15);


line(15,-15,-15,15); }

} else { fill(255); noStroke(); textSize(30); text("?", 0, -5); }

if (show || type === 'straight_findI') {

stroke('#f1c40f'); noFill(); strokeWeight(3);

for(let r = 70; r <= 150; r += 40) {

circle(0, 0, r*2);

let p5RotationDir = -dir;

let aAng = (frameCount * 0.02 * p5RotationDir) % TWO_PI;

let pDir = aAng + (dir === 1 ? -PI/2 : PI/2);


push(); translate(cos(aAng)*r, sin(aAng)*r); rotate(pDir);

fill('#f1c40f'); noStroke(); triangle(-8,-6, 8,0, -8,6); pop();

else if ([Link]('circle')) {

noFill(); stroke('#f1c40f'); strokeWeight(6); circle(0, 0, 200);

if (show || type === 'circle_findB') {

let p5RotationDir = -dir;

let aAng = (frameCount * 0.02 * p5RotationDir) % TWO_PI;

let pDir = aAng + (dir === 1 ? -PI/2 : PI/2);

push(); translate(cos(aAng)*100, sin(aAng)*100); rotate(pDir);

fill('#f1c40f'); noStroke(); triangle(-12,-8, 12,0, -12,8); pop();

if (show || type === 'circle_findI') {

fill(20); stroke('#58a6ff'); strokeWeight(4); circle(0, 0, 60);

strokeWeight(5); if(dir === 1) { fill('#58a6ff'); circle(0, 0, 16); } else { line(-15,-15,15,15);


line(15,-15,-15,15); }

} else { fill(255); noStroke(); textSize(30); text("?", 0, -5); }

else if ([Link]('solenoid')) {

let w = 240; let h = 80;

fill(30, 40, 50, 200); stroke(100); strokeWeight(2); rect(0, 0, w, h, 10);

stroke('#f1c40f'); strokeWeight(4); noFill();

for(let i=-w/2+20; i<w/2-10; i+= 25) { ellipse(i, 0, 30, h+15); }

let iDir = (type === 'solenoid_findPoles') ? dir : ([Link] ? 1 : -1);

let nLeft = (iDir === 1);

if (show || type === 'solenoid_findPoles') {


fill(255); noStroke(); textSize(20); textStyle(BOLD);

for(let i=-w/2+20; i<w/2-10; i+= 25) { text(iDir === 1 ? "⬆" : "⬇", i+15, 0); }

} else { fill(255); noStroke(); textSize(40); text("I = ?", 0, 0); }

if (show || type === 'solenoid_findI') {

fill(nLeft ? '#e81123' : '#0078d7'); noStroke(); rect(-w/2-25, 0, 50, h, 4);

fill(255); textSize(20); textStyle(BOLD); text(nLeft ? 'N' : 'S', -w/2-25, 0);

fill(!nLeft ? '#e81123' : '#0078d7'); noStroke(); rect(w/2+25, 0, 50, h, 4);

fill(255); textSize(20); textStyle(BOLD); text(!nLeft ? 'N' : 'S', w/2+25, 0);

} else { fill(255); noStroke(); textSize(30); text("Cực gì?", -w/2-25, -60); }

pop();

function createConfetti() {

particles = [];

// Bắn 3 chùm pháo hoa khổng lồ từ dưới lên

let burstPoints = [width * 0.2, width * 0.5, width * 0.8];

for (let bp of burstPoints) {

for (let i = 0; i < 120; i++) { // Tăng số lượng hạt lên 120

[Link]({

x: bp,

y: height,

vx: random(-9, 9), // Lực bung tỏa ngang mạnh hơn

vy: random(-18, -35), // Lực bắn lên cao hơn

r: random([255, 50, 255, 0, 200]), // Bảng màu RGB siêu sáng

g: random([255, 200, 50, 255, 0]),

b: random([255, 50, 255, 50, 0]),

size: random(8, 20), // Hạt to rõ ràng hơn

angle: random(TWO_PI),
spin: random(-0.4, 0.4),

life: random(200, 350) // Thời gian sống lâu hơn

});

function drawConfetti() {

noStroke();

for (let i = [Link] - 1; i >= 0; i--) {

let p = particles[i];

[Link] += 0.45; // Trọng lực rơi

[Link] *= 0.97; [Link] *= 0.97; // Lực cản không khí

p.x += [Link]; p.y += [Link];

[Link] += [Link];

[Link] -= 2;

// GIỮ MÀU RỰC RỠ 100%: Chỉ mờ đi khi life < 100 (chuẩn bị biến mất)

let alpha = [Link] > 100 ? 255 : map([Link], 0, 100, 0, 255);

push(); translate(p.x, p.y); rotate([Link]);

fill(p.r, p.g, p.b, alpha);

if (i % 2 === 0) rect(-[Link]/2, -[Link]/2, [Link], [Link]);

else circle(0, 0, [Link]);

pop();

if ([Link] <= 0 || p.y > height + 50) [Link](i, 1);

// Bắn thêm liên tục các chùm nhỏ để màn hình luôn sôi động
if ([Link] < 60 && random() < 0.05 && [Link]('end-
screen').[Link] === 'flex') {

let bx = random(width * 0.1, width * 0.9);

for (let i = 0; i < 50; i++) {

[Link]({

x: bx, y: height, vx: random(-6, 6), vy: random(-15, -28),

r: random(100,255), g: random(100,255), b: random(100,255),

size: random(7, 16), angle: random(TWO_PI), spin: random(-0.3, 0.3), life: random(150,
300)

});

// ================= UI ĐIỀU KHIỂN & CHUỘT =================

// Hàm đóng bảng tùy chỉnh độc lập

// Hàm đóng bảng tùy chỉnh độc lập

function closePropPanel() {

let panel = [Link]('prop-panel');

[Link] = 'none';

selectedEntity = null;

// Dọn dẹp tọa độ kéo thả (đã làm ở bước trước)

[Link]('top');

[Link]('left');

// --- BỔ SUNG CHO BƯỚC C ---

let btnOpen = [Link]('btn-open-prop');

if (btnOpen) [Link] = 'none';

function mousePressed(event) {
if (event && [Link] && [Link] !== 'CANVAS') return;

mouseWasDragged = false;

let hit = false;

let prevSelected = selectedEntity; // LƯU TRẠNG THÁI: Nhớ xem vật nào đang được chọn trước khi
bấm

if (currentTab !== 3) {

for (let i = [Link] - 1; i >= 0; i--) {

if (entities[i].isMouseOver(mouseX, mouseY)) {

draggedEntity = entities[i];

// LOGIC MỚI: Nếu bấm trúng vật ĐÃ ĐƯỢC CHỌN từ trước -> Bật cờ cho phép đảo chiều

if (prevSelected === draggedEntity) {

[Link] = true;

} else {

[Link] = false;

selectedEntity = draggedEntity;

// --- PHỤC HỒI GIAO DIỆN BẢNG TÙY CHỈNH ---

[Link]('group-magnet').[Link] = 'none';

[Link]('group-wire').[Link] = 'none';

[Link] = [Link];

[Link] = [Link] * 100;

[Link]('scaleVal').innerText = [Link]([Link] *
100) + '%';

if (typeof [Link] !== 'undefined') {

[Link]('angle-control').[Link] = 'block';
[Link] = degrees([Link]);

[Link]('angleVal').innerText =
[Link](degrees([Link])) + '°';

} else {

[Link]('angle-control').[Link] = 'none';

if (['magnet', 'u_magnet'].includes([Link])) {

[Link]('group-magnet').[Link] = 'block';

if (typeof [Link] !== 'undefined') {

[Link] = [Link] * 100;

[Link]('magStrengthVal').innerText =
[Link]([Link] * 100) + '%';

else if (['point_wire', 'solenoid', 'flexible_wire',


'oersted_wire'].includes([Link])) {

[Link]('group-wire').[Link] = 'block';

[Link] = [Link] * 100;

[Link]('wireCurrentVal').innerText =
[Link]([Link] * 100) + '%';

let btnToggle = [Link]('btnToggleOnOff');

[Link] = [Link] ? "TẮT DÒNG ĐIỆN" : "BẬT DÒNG


ĐIỆN";

[Link] = [Link] ? "#1f6feb" : "#f85149";

[Link]([Link](i, 1)[0]);

hit = true;

break;

}
}

if (!hit) {

[Link]('show-tools');

selectedEntity = null;

let panel = [Link]('prop-panel');

if (panel) {

[Link] = 'none';

[Link]('top');

[Link]('left');

let btnOpen = [Link]('btn-open-prop');

if (btnOpen) [Link] = 'none';

function mouseDragged() {

if (draggedEntity && ![Link]) {

if ([Link] === 'flexible_wire' || [Link] === 'oersted_wire') {

let offset = createVector(dragOffsetX, dragOffsetY); [Link]([Link] || 0);

[Link].x = mouseX - offset.x; [Link].y = mouseY - offset.y;

} else {

[Link].x = mouseX - dragOffsetX; [Link].y = mouseY - dragOffsetY;

mouseWasDragged = true; isFilingsTapped = false;

function mouseReleased(event) {

if (event && [Link] && [Link] !== 'CANVAS') return;


if (selectedEntity) {

// TÍNH NĂNG MỚI: Chỉ đổi chiều dòng điện nếu thả chuột không kéo, VÀ vật thể này ĐÃ được
chọn từ trước

if (!mouseWasDragged && [Link] && typeof


[Link] === 'function') {

[Link]();

// Reset lại cờ sau khi xử lý xong

[Link] = false;

// Kỹ thuật "Teleport" bảo vệ máy tính, fix lỗi điện thoại

let isMobile = [Link] <= 768 || [Link]('mobile-mode');

let panel = [Link]('prop-panel');

if (isMobile) {

[Link](panel);

[Link]('btn-open-prop').[Link] = 'block';

[Link] = 'none';

} else {

let activePanel = [Link]('panel-' + currentTab);

if (activePanel && [Link] !== activePanel) {

[Link](panel);

[Link] = 'block';

[Link]('btn-open-prop').[Link] = 'none';

draggedEntity = null;

// Hàm mới để bấm nút thì mới mở bảng


function showPropPanel() {

let panel = [Link]('prop-panel');

[Link](panel); // Chắc chắn bảng ở ngoài cùng trên điện thoại

[Link] = 'block';

[Link]('btn-open-prop').[Link] = 'none';

// ================= HỖ TRỢ CẢM ỨNG NATIVE CHO IOS =================

let lastTouchTime = 0; // Biến ghi nhớ thời gian chạm

function touchStarted(event) {

let currentTime = new Date().getTime();

let timeDiff = currentTime - lastTouchTime;

// Nếu gõ 2 lần liên tiếp trong vòng 0.3 giây (300ms) -> Kích hoạt xóa vật

if (timeDiff < 300 && timeDiff > 0) {

doubleClicked();

lastTouchTime = 0; // Reset

} else {

lastTouchTime = currentTime;

mousePressed(event);

// Chặn cuộn trang của iOS

if (event && [Link] && [Link] === 'CANVAS') {

return false;

function touchMoved(event) {

mouseDragged(event);

if (event && [Link] && [Link] === 'CANVAS') {


return false;

function touchEnded(event) {

mouseReleased(event);

function doubleClicked() {

for (let i = [Link] - 1; i >= 0; i--) {

if (entities[i].isMouseOver(mouseX, mouseY)) { [Link](i, 1); [Link] =


'none'; break; }

function windowResized() {

let container = [Link]('canvas-container');

if (container) {

// Sử dụng clientWidth/Height để lấy kích thước thực tế vùng hiển thị

let w = [Link];

let h = [Link];

resizeCanvas(w, h);

// Đảm bảo tọa độ vật thể không bị văng ra ngoài khi xoay màn hình

for (let e of entities) {

[Link].x = constrain([Link].x, 50, width - 50);

[Link].y = constrain([Link].y, 50, height - 50);

function closeWelcomeModal() {

[Link]('welcome-modal').[Link] = 'none';
}

function openInfoModal() {

[Link]('info-modal').[Link] = 'flex';

function closeInfoModal() {

[Link]('info-modal').[Link] = 'none';

function toggleSettingsMenu() {

let menu = [Link]('settings-menu');

[Link] = ([Link] === 'flex') ? 'none' : 'flex';

function toggleMobileTools() {

[Link]('show-tools');

// Ép hệ thống tính lại kích thước ngay khi Sidebar vừa di chuyển xong

setTimeout(() => {

windowResized();

}, 400); // Đợi 400ms cho hiệu ứng trượt Sidebar hoàn tất

// Hàm bật/tắt bảng hướng dẫn Oersted

function toggleOerstedGuide() {

let tooltip = [Link]('oersted-tooltip');

if (tooltip) {

if ([Link] === 'none' || [Link] === '') {

[Link] = 'block';

} else {

[Link] = 'none';

// ================= TÍNH NĂNG KÉO THẢ BẢNG TÙY CHỈNH =================


let propHeader = [Link]('prop-header');

let isDraggingPanel = false;

let dragStartX = 0, dragStartY = 0;

let initialLeft = 0, initialTop = 0;

function startDragPanel(e) {

let isMobile = [Link] <= 768 || [Link]('mobile-mode');

// Chỉ cho phép kéo ở ĐT, và KHÔNG kéo nếu vô tình chạm trúng nút ❌

if (!isMobile || [Link] === 'BUTTON') return;

isDraggingPanel = true;

let clientX = [Link] ? [Link][0].clientX : [Link];

let clientY = [Link] ? [Link][0].clientY : [Link];

dragStartX = clientX;

dragStartY = clientY;

// Lấy tọa độ hiện tại

let style = [Link](propPanel);

initialLeft = parseFloat([Link]) || 0;

initialTop = parseFloat([Link]) || 0;

[Link] = 'grabbing';

function onDragPanel(e) {

if (!isDraggingPanel) return;

[Link](); // Ngăn trình duyệt cuộn trang màn hình khi đang vuốt kéo bảng

let clientX = [Link] ? [Link][0].clientX : [Link];

let clientY = [Link] ? [Link][0].clientY : [Link];


let dx = clientX - dragStartX;

let dy = clientY - dragStartY;

// Ép vị trí mới (Dùng setProperty để vượt qua lớp bảo vệ !important của CSS)

[Link]('left', (initialLeft + dx) + 'px', 'important');

[Link]('top', (initialTop + dy) + 'px', 'important');

function endDragPanel() {

isDraggingPanel = false;

if(propHeader) [Link] = 'grab';

// Bật bộ lắng nghe cho cả Chuột và Cảm ứng

if (propHeader) {

[Link]('mousedown', startDragPanel);

[Link]('mousemove', onDragPanel);

[Link]('mouseup', endDragPanel);

[Link]('touchstart', startDragPanel, {passive: false});

[Link]('touchmove', onDragPanel, {passive: false});

[Link]('touchend', endDragPanel);

function touchEnded(event) {

mouseReleased(event); // Truyền tham số event vào đây

</script>

<!-- ======================================================= -->

<!-- HỆ THỐNG CHATBOT GIA SƯ VẬT LÍ (TÍCH HỢP GEMINI API) -->

<!-- ======================================================= -->

<style>
#chatbot-toggle {

position: fixed; bottom: 20px; right: 20px; width: 60px; height: 60px;

background: linear-gradient(145deg, #1f6feb, #58a6ff); color: white;

border: none; border-radius: 50%; font-size: 28px; cursor: pointer;

box-shadow: 0 5px 20px rgba(0,0,0,0.6); z-index: 1000;

display: flex; align-items: center; justify-content: center; transition: 0.3s;

#chatbot-toggle:hover { transform: scale(1.1); box-shadow: 0 5px 25px rgba(88, 166, 255, 0.5); }

#chatbot-container {

position: fixed; bottom: 90px; right: 20px; width: 350px; height: 450px;

background: #0d1117; border: 2px solid #30363d; border-radius: 12px;

box-shadow: 0 10px 40px rgba(0,0,0,0.8); z-index: 1000;

display: none; flex-direction: column; overflow: hidden;

#chatbot-header {

background: #161b22; padding: 15px; border-bottom: 1px solid #30363d;

display: flex; justify-content: space-between; align-items: center;

#chatbot-header h3 { margin: 0; color: #58a6ff; font-size: 16px; display: flex; align-items: center;
gap: 8px;}

#chatbot-close { background: transparent; border: none; color: #8b949e; font-size: 20px; cursor:
pointer; }

#chatbot-close:hover { color: #f85149; }

#chatbot-messages { flex: 1; padding: 15px; overflow-y: auto; display: flex; flex-direction:


column; gap: 12px; }

.msg { max-width: 85%; padding: 10px 14px; border-radius: 10px; font-size: 14px; line-height:
1.5; word-wrap: break-word; }

.[Link] { align-self: flex-end; background: #1f6feb; color: white; border-bottom-right-radius:


2px; }

.[Link] { align-self: flex-start; background: #21262d; color: #c9d1d9; border: 1px solid
#30363d; border-bottom-left-radius: 2px; }
#chatbot-input-area { display: flex; padding: 10px; background: #161b22; border-top: 1px solid
#30363d; gap: 10px; }

#chatbot-input { flex: 1; background: #0b0f19; border: 1px solid #30363d; color: #c9d1d9;
padding: 10px; border-radius: 8px; font-size: 14px; outline: none; }

#chatbot-input:focus { border-color: #58a6ff; }

#chatbot-send { background: #2ea043; color: white; border: none; padding: 0 15px; border-
radius: 8px; font-weight: bold; cursor: pointer; transition: 0.2s; }

#chatbot-send:hover { background: #238636; }

.typing-indicator { align-self: flex-start; background: transparent; color: #8b949e; font-size: 13px;


font-style: italic; display: none; margin-bottom: 10px; }

@media (max-width: 768px) {

#chatbot-container { width: 90%; left: 5%; right: auto; bottom: 85px; height: 60vh; }

#chatbot-toggle { bottom: 85px; right: 15px; left: auto; width: 50px; height: 50px; font-size:
24px; z-index: 95;}

/* --- CHÈN THÊM: CSS cho trạng thái mở rộng và nút bấm --- */

#[Link] { width: 500px; height: 75vh; }

.chat-btn-group { display: flex; gap: 12px; align-items: center; }

.chat-header-btn { background: transparent; border: none; color: #8b949e; font-size: 18px;


cursor: pointer; padding: 0; transition: 0.2s; }

.chat-header-btn:hover { color: #58a6ff; }

#chatbot-close { padding: 0; } /* Ghi đè lại padding nút đóng cũ */

@media (max-width: 768px) {

#[Link] {

width: 100%; left: 0; right: auto; bottom: 0; height: 100vh;

border-radius: 0; z-index: 1001;

</style>
<button id="chatbot-toggle" onclick="toggleChat()">💬</button>

<div id="chatbot-container">

<div id="chatbot-header">

<h3>🤖 Gia sư AI Vật Lí 12</h3>

<div class="chat-btn-group">

<button id="chatbot-expand" class="chat-header-btn" onclick="toggleExpandChat()"


title="Phóng to">⛶</button>

<button id="chatbot-close" class="chat-header-btn" onclick="toggleChat()"


title="Đóng">✖</button>

</div>

</div>

<div id="chatbot-messages">

<div class="msg bot">Chào em! Thầy là Trợ lí AI hỗ trợ bài Từ trường. Em có thắc mắc gì về
mô phỏng hay lý thuyết không?</div>

</div>

<div class="typing-indicator" id="typing-indicator">Gia sư đang suy nghĩ...</div>

<div id="chatbot-input-area">

<input type="text" id="chatbot-input" placeholder="Hỏi gia sư tại đây..."


onkeypress="handleKeyPress(event)">

<button id="chatbot-send" onclick="sendMessage()">Gửi</button>

</div>

</div>

<script>

// === EM HÃY DÁN API KEY VÀO GIỮA 2 DẤU NGOẶC KÉP Ở DÒNG DƯỚI NÀY ===

const GEMINI_API_KEY = "AIzaSyCSY3AHADImWhyt6mutyUEAfkTrRulQFNw";

const chatContainer = [Link]('chatbot-container');

const chatMessages = [Link]('chatbot-messages');

const chatInput = [Link]('chatbot-input');

const typingIndicator = [Link]('typing-indicator');


function toggleChat() {

[Link] = ([Link] === 'flex') ? 'none' : 'flex';

if ([Link] === 'flex') [Link]();

function toggleExpandChat() {

const expandBtn = [Link]('chatbot-expand');

[Link]('expanded');

if ([Link]('expanded')) {

[Link] = '🗕'; // Đổi thành biểu tượng thu nhỏ

[Link] = 'Thu nhỏ';

} else {

[Link] = '⛶'; // Đổi lại thành biểu tượng phóng to

[Link] = 'Phóng to';

// Tự động cuộn xuống tin nhắn mới nhất để không bị che khuất

[Link] = [Link];

function handleKeyPress(e) {

if ([Link] === 'Enter') sendMessage();

function appendMessage(sender, text) {

const msgDiv = [Link]('div');

[Link]('msg', sender);

[Link] = [Link](/\n/g, '<br>');

[Link](msgDiv);

[Link] = [Link];
}

// 1. Hàm đọc tri thức từ file bên ngoài

async function getWebKnowledge() {

try {

// Gọi file [Link] cùng thư mục

const response = await fetch('[Link]');

if (![Link]) return "Không tìm thấy tài liệu hướng dẫn.";

return await [Link]();

} catch (error) {

[Link]("Lỗi đọc file tri thức:", error);

return "Dữ liệu hướng dẫn hiện đang được cập nhật.";

// 2. Hàm gửi tin nhắn đã được nâng cấp

async function sendMessage() {

const userText = [Link]();

if (!userText) return;

appendMessage('user', userText);

[Link] = '';

[Link] = 'block';

[Link] = [Link];

// ĐỌC "SỔ TAY" TRƯỚC KHI GỬI CHO AI

const webKnowledge = await getWebKnowledge();

const systemPrompt = `Bạn là Gia sư AI Vật lí 12 tại trang web này.

Dưới đây là SỔ TAY HƯỚNG DẪN SỬ DỤNG của trang web:

---

${webKnowledge}
---

Dựa vào thông tin trên, hãy trả lời câu hỏi sau của học sinh một cách gợi mở,

không nhắc đến PhET hay các nguồn bên ngoài.

Câu hỏi: "${userText}"`;

try {

const response = await


fetch(`[Link]
preview:generateContent?key=${GEMINI_API_KEY}`, {

method: 'POST',

headers: { 'Content-Type': 'application/json' },

body: [Link]({

contents: [{ parts: [{ text: systemPrompt }] }]

})

});

const data = await [Link]();

if (![Link]) {

throw new Error(`[Mã lỗi ${[Link]}] ${[Link] ? [Link] : 'Google


API từ chối'}`);

const botReply = [Link][0].[Link][0].text;

[Link] = 'none';

appendMessage('bot', botReply);

} catch (error) {

[Link]("Chi tiết lỗi:", error);

[Link] = 'none';

appendMessage('bot', "⚠️Hệ thống báo lỗi: " + [Link]);


}

</script>

<!-- ======================================================= -->

</body>

</html>

You might also like