기체 시뮬레이터 프로젝트
고치기 → 압력과 부피 → 온도와 부피 → 최종본
📊 학생 기록
👤 학번·이름 넣기
0%
🧑🏫
👤 선생님께 기록 보내기
학번과 이름을 넣으면 이 페이지에서 쓴
찾은 잘못
,
돌아보기(극복한 점·느낀 점)
,
내 시뮬레이터 기획서
가 선생님께 전해집니다. 다른 칸은 이 기기에만 남아요.
학번
(1학년 3반 12번 → 10312)
이름
참여 코드
(선생님이 알려 준 숫자)
들어가기
다른 학생으로 바꾸기
닫기
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>기체 압력 상자</title> <style> :root{ --paper:#eef4f9; --ink:#1e2c39; --dim:#41586c; --line:#9db7cc; --soft:#c3d5e2; --hi:#f9fcfe; --accent:#20748f; --c1:#b23a30; --c4:#3a7a3f; } *{box-sizing:border-box} body{margin:0;padding:16px;background:var(--paper);color:var(--ink); font-family:"Pretendard","Malgun Gothic",system-ui,sans-serif;line-height:1.6} .wrap{max-width:560px;margin:0 auto} h1{font-size:20px;margin:0 0 2px} .sub{margin:0 0 12px;color:var(--dim);font-size:14px} canvas{width:100%;height:auto;background:#fff;border:2px solid var(--line); border-radius:12px;display:block;touch-action:none} .panel{margin-top:12px;padding:12px 14px;background:#fff;border:1px solid var(--soft);border-radius:12px} .row{display:flex;align-items:center;gap:10px;margin-bottom:8px} .row:last-child{margin-bottom:0} .row label{min-width:70px;font-weight:700;font-size:14px} input[type=range]{flex:1;accent-color:var(--accent)} .val{min-width:44px;text-align:right;font-family:"JetBrains Mono",ui-monospace,monospace; font-weight:700;font-size:15px;color:var(--accent)} button{font:inherit;font-weight:700;font-size:14px;padding:7px 15px;border:1px solid var(--line); border-radius:9px;background:var(--hi);color:var(--ink);cursor:pointer} button:hover{background:#eaf3fa} .reads{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:10px} .read{padding:9px;background:var(--hi);border:1px solid var(--soft);border-radius:10px;text-align:center} .read span{display:block;font-size:13px;color:var(--dim)} .read b{font-size:25px;font-family:"JetBrains Mono",ui-monospace,monospace;line-height:1.25} .read.p b{color:var(--c1)} .read.h b{color:var(--c4)} .bar{height:12px;background:#e4edf4;border-radius:7px;overflow:hidden;margin-top:6px} .bar i{display:block;height:100%;background:var(--c1);width:0} </style> </head> <body> <div class="wrap"> <h1>기체 압력 상자</h1> <p class="sub">상자 안 기체 입자가 벽에 부딪혀 <b>압력</b>이 생기는 모습을 보여 줍니다.</p> <canvas id="box" width="520" height="360"></canvas> <div class="panel"> <div class="row"> <label for="num">입자 수</label> <input type="range" id="num" min="4" max="60" value="24"> <span class="val" id="numVal">24</span> </div> <div class="row"> <button id="play">⏸ 멈춤</button> <button id="reset">↺ 다시 뿌리기</button> </div> </div> <div class="reads"> <div class="read h"><span>1초 동안 벽에 부딪힌 횟수</span><b id="hitOut">0</b></div> <div class="read p"><span>기체의 압력</span><b id="prsOut">0</b> <div class="bar"><i id="prsBar"></i></div> </div> </div> </div> <script> const cv = document.getElementById('box'); const ctx = cv.getContext('2d'); const W = cv.width, H = cv.height; /* ===== 값 설정 ===== */ const R = 22; // 입자 하나의 반지름(픽셀) const GY = 0.34; // 매 프레임 아래쪽으로 더해 주는 속도 const BOUNCE = 0.74; // 벽에 부딪힌 뒤 남는 속도의 비율 const SPEED = 2.7; // 처음 나눠 주는 빠르기 let ps = []; // 입자 목록 let playing = true; // 움직이는 중인가 let hitCount = 0; // 충돌을 센 값 let hitShow = 0; // 화면에 보여 줄 1초당 충돌 수 let lastSec = performance.now(); /* 입자를 상자 안에 뿌린다 */ function makeParticles(n){ ps = []; for(let i=0;i<n;i++){ const a = Math.random()*Math.PI*2; ps.push({ x: R + Math.random()*(W-2*R), y: R + Math.random()*(H-2*R), vx: Math.cos(a)*SPEED, vy: Math.sin(a)*SPEED }); } } /* 입자를 한 걸음 움직인다 */ function step(){ for(const p of ps){ p.vy += GY; // 아래쪽으로 속도를 더한다 p.x += p.vx; p.y += p.vy; // 벽에 부딪히면 튕겨 나오고, 그 순간에 한 번 센다 if(p.x < R) { p.x = R; p.vx = -p.vx; hit(p.vx); } // 왼쪽 벽 if(p.x > W-R) { p.x = W-R; p.vx = -p.vx; hit(p.vx); } // 오른쪽 벽 if(p.y < R) { p.y = R; p.vy = -p.vy; hit(p.vy); } // 위쪽 벽 if(p.y > H-R) { p.y = H-R; p.vy = -p.vy*BOUNCE; hit(p.vy); } // 아래쪽 벽 } } /* 충돌 한 번을 센다 — 튕겨 나오는 빠르기가 거의 0이면(벽에 붙어 있는 것) 세지 않는다 */ function hit(v){ if(Math.abs(v) > 0.3) hitCount++; } /* 기체의 압력을 구한다 — 1초 동안 벽에 부딪힌 횟수에서 */ function pressure(){ return hitShow * 3; } /* 화살표 하나를 그린다 */ function arrow(x1,y1,x2,y2){ const a = Math.atan2(y2-y1, x2-x1); ctx.strokeStyle = '#b23a30'; ctx.fillStyle = '#b23a30'; ctx.lineWidth = 2.5; ctx.beginPath(); ctx.moveTo(x1,y1); ctx.lineTo(x2,y2); ctx.stroke(); ctx.beginPath(); ctx.moveTo(x2,y2); ctx.lineTo(x2-9*Math.cos(a-0.42), y2-9*Math.sin(a-0.42)); ctx.lineTo(x2-9*Math.cos(a+0.42), y2-9*Math.sin(a+0.42)); ctx.closePath(); ctx.fill(); } /* 화면을 그린다 */ function draw(){ ctx.clearRect(0,0,W,H); // 상자 안쪽 ctx.fillStyle = '#f4f9fd'; ctx.fillRect(0,0,W,H); // 입자 for(const p of ps){ ctx.beginPath(); ctx.arc(p.x,p.y,R,0,Math.PI*2); ctx.fillStyle = '#3a7a3f'; ctx.fill(); ctx.strokeStyle = '#2b5c2f'; ctx.lineWidth = 1.5; ctx.stroke(); } // 기체가 벽을 미는 힘을 화살표로 보여 준다 for(let i=0;i<5;i++){ const x = W*(i+1)/6; arrow(x, H-34, x, H-8); } // 상자 테두리 ctx.strokeStyle = '#1c507e'; ctx.lineWidth = 5; ctx.strokeRect(2.5,2.5,W-5,H-5); } /* 되풀이해서 실행한다 */ function loop(now){ if(playing) step(); draw(); if(now - lastSec >= 1000){ hitShow = hitCount; hitCount = 0; lastSec = now; document.getElementById('hitOut').textContent = hitShow; } const pr = pressure(); document.getElementById('prsOut').textContent = pr.toFixed(0); document.getElementById('prsBar').style.width = Math.min(100, pr/1.2) + '%'; requestAnimationFrame(loop); } /* 조작 단추 연결 */ const numEl = document.getElementById('num'); numEl.addEventListener('input', ()=>{ document.getElementById('numVal').textContent = numEl.value; makeParticles(+numEl.value); }); document.getElementById('play').addEventListener('click', e=>{ playing = !playing; e.target.textContent = playing ? '⏸ 멈춤' : '▶ 움직이기'; }); document.getElementById('reset').addEventListener('click', ()=> makeParticles(+numEl.value)); makeParticles(+numEl.value); requestAnimationFrame(loop); </script> </body> </html>
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>누르는 상자 — 부피와 압력</title> <style> :root{ --paper:#eef4f9; --ink:#1e2c39; --dim:#41586c; --line:#9db7cc; --soft:#c3d5e2; --hi:#f9fcfe; --accent:#20748f; --c1:#b23a30; --c2:#3f64b5; --c4:#3a7a3f; } *{box-sizing:border-box} body{margin:0;padding:16px;background:var(--paper);color:var(--ink); font-family:"Pretendard","Malgun Gothic",system-ui,sans-serif;line-height:1.6} .wrap{max-width:560px;margin:0 auto} h1{font-size:20px;margin:0 0 2px} .sub{margin:0 0 12px;color:var(--dim);font-size:14px} canvas{width:100%;height:auto;background:#fff;border:2px solid var(--line); border-radius:12px;display:block;touch-action:none} .panel{margin-top:12px;padding:12px 14px;background:#fff;border:1px solid var(--soft);border-radius:12px} .row{display:flex;align-items:center;gap:10px;margin-bottom:8px} .row:last-child{margin-bottom:0} .row label{min-width:70px;font-weight:700;font-size:14px} input[type=range]{flex:1;accent-color:var(--accent)} .val{min-width:48px;text-align:right;font-family:"JetBrains Mono",ui-monospace,monospace; font-weight:700;font-size:15px;color:var(--accent)} button{font:inherit;font-weight:700;font-size:14px;padding:7px 15px;border:1px solid var(--line); border-radius:9px;background:var(--hi);color:var(--ink);cursor:pointer} button:hover{background:#eaf3fa} .reads{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin-top:10px} .read{padding:9px;background:var(--hi);border:1px solid var(--soft);border-radius:10px;text-align:center} .read span{display:block;font-size:12.5px;color:var(--dim)} .read b{font-size:23px;font-family:"JetBrains Mono",ui-monospace,monospace;line-height:1.25} .read.v b{color:var(--c2)} .read.h b{color:var(--c4)} .read.s b{color:#7d5ba6} .read.p b{color:var(--c1)} .tip{margin:9px 2px 0;font-size:13px;line-height:1.55;color:var(--dim)} .tip b{color:var(--c1)} </style> </head> <body> <div class="wrap"> <h1>누르는 상자</h1> <p class="sub">온도가 일정할 때, 오른쪽 벽을 밀어 <b>부피</b>를 줄이면 기체의 <b>압력</b>이 어떻게 되는지 보여 줍니다.</p> <canvas id="box" width="520" height="360"></canvas> <div class="panel"> <div class="row"> <label for="vol">부피</label> <input type="range" id="vol" min="35" max="100" value="100"> <span class="val" id="volVal">100%</span> </div> <div class="row"> <label for="num">입자 수</label> <input type="range" id="num" min="0" max="180" value="120"> <span class="val" id="numVal">120</span> </div> <div class="row"> <button id="play">⏸ 멈춤</button> <button id="reset">↺ 다시 뿌리기</button> </div> </div> <div class="reads"> <div class="read v"><span>부피</span><b id="volOut">100</b></div> <div class="read h"><span>1초당 손잡이 충돌 횟수</span><b id="hitOut">0</b></div> <div class="read s"><span>입자의 평균 빠르기</span><b id="spdOut">100</b></div> <div class="read p"><span>기체의 압력</span><b id="prsOut">40</b></div> </div> <p class="tip">위·아래 벽은 상자를 좁히면 함께 짧아집니다. 그래서 길이가 변하지 않는 <b>손잡이(오른쪽 벽)</b>에 부딪히는 횟수만 세어야 공정하게 비교할 수 있습니다.</p> </div> <script> const cv = document.getElementById('box'); const ctx = cv.getContext('2d'); const W = cv.width, H = cv.height; /* ===== 값 설정 ===== */ const R = 5; // 입자 하나의 반지름(픽셀) const SPEED = 2.6; // 처음 나눠 주는 빠르기 let ps = []; let playing = true; let vol = 100; // 부피(%) let wallX = W * vol / 100; // 오른쪽 벽의 위치 let hitCount = 0; let hitShow = 0; let lastSec = performance.now(); /* 입자를 상자 안에 뿌린다 */ function makeParticles(n){ ps = []; for(let i=0;i<n;i++){ const a = Math.random()*Math.PI*2; ps.push({ x: R + Math.random()*(wallX-2*R), y: R + Math.random()*(H-2*R), vx: Math.cos(a)*SPEED, vy: Math.sin(a)*SPEED }); } } /* 입자를 한 걸음 움직인다 */ function step(){ for(const p of ps){ p.x += p.vx; p.y += p.vy; if(p.x < R && p.vx < 0){ p.x = R; p.vx = -p.vx; } // 왼쪽 벽 if(p.y < R && p.vy < 0){ p.y = R; p.vy = -p.vy; } // 위쪽 벽 if(p.y > H-R && p.vy > 0){ p.y = H-R; p.vy = -p.vy; } // 아래쪽 벽 // 오른쪽 움직이는 벽 — 상자 안쪽에서 부딪힐 때 if(p.x < wallX && p.x + R > wallX && p.vx > 0){ p.x = wallX-R; p.vx = -p.vx; hitCount++; } // 오른쪽 움직이는 벽 — 상자 바깥쪽에서 부딪힐 때 if(p.x > wallX && p.x - R < wallX && p.vx < 0){ p.x = wallX+R; p.vx = -p.vx; } // 그림 밖으로 나가지 않게 막는다 if(p.x > W-R && p.vx > 0){ p.x = W-R; p.vx = -p.vx; } } } /* 입자의 평균 빠르기를 구한다 (처음 값이 100) */ function avgSpeed(){ if(ps.length === 0) return 0; let s = 0; for(const p of ps) s += Math.hypot(p.vx, p.vy); return s / ps.length / SPEED * 100; } /* 기체의 압력을 구한다 */ function pressure(){ return 40 / (vol/100); } /* 화살표 하나를 그린다 */ function arrow(x1,y1,x2,y2){ const a = Math.atan2(y2-y1, x2-x1); ctx.strokeStyle = '#b23a30'; ctx.fillStyle = '#b23a30'; ctx.lineWidth = 2.2; ctx.beginPath(); ctx.moveTo(x1,y1); ctx.lineTo(x2,y2); ctx.stroke(); ctx.beginPath(); ctx.moveTo(x2,y2); ctx.lineTo(x2-8*Math.cos(a-0.42), y2-8*Math.sin(a-0.42)); ctx.lineTo(x2-8*Math.cos(a+0.42), y2-8*Math.sin(a+0.42)); ctx.closePath(); ctx.fill(); } /* 화면을 그린다 */ function draw(){ ctx.clearRect(0,0,W,H); ctx.fillStyle = '#e8eef3'; ctx.fillRect(0,0,W,H); // 상자 바깥 ctx.fillStyle = '#f4f9fd'; ctx.fillRect(0,0,wallX,H); // 상자 안 for(const p of ps){ // 입자 ctx.beginPath(); ctx.arc(p.x,p.y,R,0,Math.PI*2); ctx.fillStyle = '#3a7a3f'; ctx.fill(); } // 기체가 벽을 미는 힘 — 네 벽 모두 for(let i=0;i<4;i++){ const y = H*(i+1)/5, x = wallX*(i+1)/5; arrow(26, y, 6, y); // 왼쪽 arrow(wallX-26, y, wallX-6, y); // 오른쪽 arrow(x, 26, x, 6); // 위 arrow(x, H-26, x, H-6); // 아래 } // 움직이는 벽(손잡이) ctx.fillStyle = '#8a5f24'; ctx.fillRect(wallX-6, 0, 12, H); ctx.fillStyle = '#c4711e'; ctx.fillRect(wallX-6, H/2-26, 12, 52); ctx.strokeStyle = '#1c507e'; ctx.lineWidth = 5; ctx.strokeRect(2.5, 2.5, wallX-5, H-5); } /* 되풀이해서 실행한다 */ function loop(now){ if(playing) step(); draw(); if(now - lastSec >= 1000){ hitShow = hitCount; hitCount = 0; lastSec = now; document.getElementById('hitOut').textContent = hitShow; } document.getElementById('volOut').textContent = vol + '%'; document.getElementById('spdOut').textContent = avgSpeed().toFixed(0); document.getElementById('prsOut').textContent = pressure().toFixed(0); requestAnimationFrame(loop); } /* 조작 단추 연결 */ const volEl = document.getElementById('vol'); const numEl = document.getElementById('num'); volEl.addEventListener('input', ()=>{ const before = wallX; vol = +volEl.value; wallX = W * vol / 100; document.getElementById('volVal').textContent = vol + '%'; // 상자가 좁아진 만큼 입자를 빠르게 한다 const ratio = before / wallX; for(const p of ps){ p.vx *= ratio; p.vy *= ratio; } }); numEl.addEventListener('input', ()=>{ document.getElementById('numVal').textContent = numEl.value; makeParticles(+numEl.value); }); document.getElementById('play').addEventListener('click', e=>{ playing = !playing; e.target.textContent = playing ? '⏸ 멈춤' : '▶ 움직이기'; }); document.getElementById('reset').addEventListener('click', ()=> makeParticles(+numEl.value)); makeParticles(+numEl.value); requestAnimationFrame(loop); </script> </body> </html>
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>데우는 상자 — 온도와 부피</title> <style> :root{ --paper:#eef4f9; --ink:#1e2c39; --dim:#41586c; --line:#9db7cc; --soft:#c3d5e2; --hi:#f9fcfe; --accent:#20748f; --c1:#b23a30; --c2:#3f64b5; --c3:#c4711e; --c4:#3a7a3f; } *{box-sizing:border-box} body{margin:0;padding:16px;background:var(--paper);color:var(--ink); font-family:"Pretendard","Malgun Gothic",system-ui,sans-serif;line-height:1.6} .wrap{max-width:560px;margin:0 auto} h1{font-size:20px;margin:0 0 2px} .sub{margin:0 0 12px;color:var(--dim);font-size:14px} canvas{width:100%;height:auto;background:#fff;border:2px solid var(--line); border-radius:12px;display:block;touch-action:none} .panel{margin-top:12px;padding:12px 14px;background:#fff;border:1px solid var(--soft);border-radius:12px} .row{display:flex;align-items:center;gap:10px;margin-bottom:8px} .row:last-child{margin-bottom:0} .row label{min-width:70px;font-weight:700;font-size:14px} input[type=range]{flex:1;accent-color:var(--c3)} .val{min-width:52px;text-align:right;font-family:"JetBrains Mono",ui-monospace,monospace; font-weight:700;font-size:15px;color:var(--c3)} button{font:inherit;font-weight:700;font-size:14px;padding:7px 15px;border:1px solid var(--line); border-radius:9px;background:var(--hi);color:var(--ink);cursor:pointer} button:hover{background:#eaf3fa} .reads{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin-top:10px} .read{padding:9px;background:var(--hi);border:1px solid var(--soft);border-radius:10px;text-align:center} .read span{display:block;font-size:12.5px;color:var(--dim)} .read b{font-size:23px;font-family:"JetBrains Mono",ui-monospace,monospace;line-height:1.25} .read.t b{color:var(--c3)} .read.v b{color:var(--c2)} .read.n b{color:var(--c4)} .read.s b{color:#7d5ba6} </style> </head> <body> <div class="wrap"> <h1>데우는 상자</h1> <p class="sub">기체를 데우면 위에 얹힌 <b>뚜껑</b>이 밀려 올라가 <b>부피</b>가 어떻게 되는지 보여 줍니다.</p> <canvas id="box" width="520" height="380"></canvas> <div class="panel"> <div class="row"> <label for="temp">온도</label> <input type="range" id="temp" min="0" max="100" value="20"> <span class="val" id="tempVal">20℃</span> </div> <div class="row"> <button id="play">⏸ 멈춤</button> <button id="reset">↺ 처음으로</button> </div> </div> <div class="reads"> <div class="read t"><span>온도</span><b id="tOut">20</b></div> <div class="read v"><span>기체의 부피</span><b id="vOut">50</b></div> <div class="read n"><span>입자 수</span><b id="nOut">27</b></div> <div class="read s"><span>입자의 평균 빠르기</span><b id="sOut">100</b></div> </div> </div> <script> const cv = document.getElementById('box'); const ctx = cv.getContext('2d'); const W = cv.width, H = cv.height; /* ===== 값 설정 ===== */ const SPEED = 2.6; // 20℃일 때의 빠르기 const ATM = 0; // 바깥 공기가 뚜껑을 누르는 정도 const PUSH_K = 0.050; // 입자가 뚜껑을 밀어 올리는 정도 const DAMP = 0.90; // 뚜껑이 출렁이는 것을 가라앉히는 값 const TOP = 34; // 뚜껑이 올라갈 수 있는 가장 높은 곳 const BOTTOM = H-40; // 뚜껑이 내려갈 수 있는 가장 낮은 곳 let R = 5; // 입자 하나의 반지름(픽셀) let temp = 20; // 온도(℃) let ps = []; let playing = true; let lidY = H/2; // 뚜껑의 높이 let lidV = 0; // 뚜껑이 움직이는 속도 let push = 0; // 입자가 뚜껑을 민 정도 /* 온도에 맞는 빠르기 */ function targetSpeed(t){ return SPEED * Math.sqrt((t+273)/293); } /* 온도에 맞는 입자 수 */ function targetCount(t){ return 20 + Math.round(t*0.35); } /* 온도에 맞는 입자 반지름 */ function targetRadius(t){ return 4 + t*0.05; } /* 입자를 상자 안에 뿌린다 */ function makeParticles(n){ const v = targetSpeed(temp); ps = []; for(let i=0;i<n;i++){ const a = Math.random()*Math.PI*2; ps.push({ x: R + Math.random()*(W-2*R), y: lidY + R + Math.random()*(H-lidY-2*R), vx: Math.cos(a)*v, vy: Math.sin(a)*v }); } } /* 온도를 바꾼다 */ function setTemp(t){ temp = t; R = targetRadius(t); // 입자 크기를 맞춘다 const want = targetCount(t); // 입자 수를 맞춘다 while(ps.length > want) ps.pop(); while(ps.length < want){ const a = Math.random()*Math.PI*2, v = targetSpeed(t); ps.push({ x: R+Math.random()*(W-2*R), y: lidY+R+Math.random()*(H-lidY-2*R), vx: Math.cos(a)*v, vy: Math.sin(a)*v }); } const want_v = targetSpeed(t); // 빠르기를 맞춘다 const now_v = avgSpeedRaw(); if(now_v > 0){ const k = want_v / now_v; for(const p of ps){ p.vx *= k; p.vy *= k; } } } function avgSpeedRaw(){ if(ps.length === 0) return 0; let s = 0; for(const p of ps) s += Math.hypot(p.vx, p.vy); return s / ps.length; } /* 입자와 뚜껑을 한 걸음 움직인다 */ function step(){ push = 0; for(const p of ps){ p.x += p.vx; p.y += p.vy; if(p.x < R && p.vx < 0){ p.x = R; p.vx = -p.vx; } // 왼쪽 벽 if(p.x > W-R && p.vx > 0){ p.x = W-R; p.vx = -p.vx; } // 오른쪽 벽 if(p.y > H-R && p.vy > 0){ p.y = H-R; p.vy = -p.vy; } // 바닥 // 뚜껑 아래쪽에 부딪힌다 if(p.y < lidY + R && p.vy < 0){ p.y = lidY + R; p.vy = -p.vy; push += 2*Math.abs(p.vy); // 뚜껑을 밀어 올린 정도를 모은다 } } // 뚜껑 : 아래에서 입자가 밀어 올리고, 위에서 바깥 공기가 누른다 lidV += push*PUSH_K - ATM; lidV *= DAMP; lidY -= lidV; if(lidY < TOP) { lidY = TOP; lidV = 0; } if(lidY > BOTTOM){ lidY = BOTTOM; lidV = 0; } for(const p of ps) if(p.y < lidY + R) p.y = lidY + R; } /* 화면을 그린다 */ function draw(){ ctx.clearRect(0,0,W,H); ctx.fillStyle = '#e8eef3'; ctx.fillRect(0,0,W,H); // 뚜껑 위 바깥 ctx.fillStyle = '#f4f9fd'; ctx.fillRect(0,lidY,W,H-lidY); // 기체가 든 곳 for(const p of ps){ ctx.beginPath(); ctx.arc(p.x,p.y,R,0,Math.PI*2); ctx.fillStyle = '#3a7a3f'; ctx.fill(); } ctx.fillStyle = '#8a5f24'; ctx.fillRect(0, lidY-13, W, 13); // 뚜껑 ctx.fillStyle = '#c4711e'; ctx.fillRect(W/2-34, lidY-20, 68, 8); ctx.strokeStyle = '#1c507e'; ctx.lineWidth = 5; // 상자 ctx.beginPath(); ctx.moveTo(2.5, 0); ctx.lineTo(2.5, H-2.5); ctx.lineTo(W-2.5, H-2.5); ctx.lineTo(W-2.5, 0); ctx.stroke(); // 불꽃 — 온도가 높을수록 크게 const f = temp/100; ctx.fillStyle = 'rgba(196,113,30,' + (0.25+0.75*f) + ')'; for(let i=0;i<7;i++){ const x = W*(i+1)/8, h = 6 + 22*f; ctx.beginPath(); ctx.moveTo(x-7, H-3); ctx.quadraticCurveTo(x, H-3-h*1.7, x+7, H-3); ctx.fill(); } } /* 되풀이해서 실행한다 */ function loop(){ if(playing) step(); draw(); document.getElementById('tOut').textContent = temp; document.getElementById('vOut').textContent = Math.round((H-lidY)/H*100) + '%'; document.getElementById('nOut').textContent = ps.length; document.getElementById('sOut').textContent = Math.round(avgSpeedRaw()/SPEED*100); requestAnimationFrame(loop); } /* 조작 단추 연결 */ const tempEl = document.getElementById('temp'); tempEl.addEventListener('input', ()=>{ document.getElementById('tempVal').textContent = tempEl.value + '℃'; setTemp(+tempEl.value); }); document.getElementById('play').addEventListener('click', e=>{ playing = !playing; e.target.textContent = playing ? '⏸ 멈춤' : '▶ 움직이기'; }); document.getElementById('reset').addEventListener('click', ()=>{ lidY = H/2; lidV = 0; tempEl.value = 20; document.getElementById('tempVal').textContent = '20℃'; temp = 20; R = targetRadius(20); makeParticles(targetCount(20)); }); R = targetRadius(temp); makeParticles(targetCount(temp)); requestAnimationFrame(loop); </script> </body> </html>
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>공기 탐험대</title> <style> :root{ --bg:#eef6fc; --card:#ffffff; --ink:#1e2c39; --dim:#4a6072; --line:#c9dbe8; --main:#2f7fc1; --main-soft:#dcecf9; --c1:#b23a30; --c2:#3f64b5; --c3:#c4711e; --c4:#3a7a3f; } body.mint { --bg:#eaf7f2; --main:#1f8a6a; --main-soft:#d6f0e6; --line:#c4e3d6; } body.sunset{ --bg:#fdf1ea; --main:#d0643a; --main-soft:#fbe0d3; --line:#efd2c3; } body.night { --bg:#172230; --card:#213041; --ink:#eaf2f8; --dim:#a8bccd; --line:#35495e; --main:#5fb0f0; --main-soft:#27405a; } *{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--ink);font-family:"Pretendard","Malgun Gothic",system-ui,sans-serif; line-height:1.65;font-size:17px;word-break:keep-all} header{padding:22px 18px 8px;text-align:center} header h1{margin:0;font-size:clamp(26px,5vw,36px);letter-spacing:-.02em} header p{margin:6px 0 0;color:var(--dim);font-size:15px} nav{display:flex;gap:6px;justify-content:center;flex-wrap:wrap;padding:12px 12px 4px} nav button{font:inherit;font-size:15px;font-weight:700;border:1px solid var(--line);background:var(--card);color:var(--ink); border-radius:999px;padding:8px 16px;cursor:pointer} nav button.on{background:var(--main);border-color:var(--main);color:#fff} main{max-width:720px;margin:0 auto;padding:10px 16px 30px} section{display:none} section.on{display:block} .card{background:var(--card);border:1px solid var(--line);border-radius:18px;padding:16px 18px;margin:12px 0} h2{font-size:22px;margin:6px 0 4px} .say{color:var(--dim);font-size:15.5px;margin:0 0 8px} canvas{width:100%;height:auto;display:block;border-radius:14px;background:#fff;border:2px solid var(--line);touch-action:none} .row{display:flex;align-items:center;gap:12px;margin:10px 0} .row label{min-width:78px;font-weight:700;font-size:15px} .row input[type=range]{flex:1;accent-color:var(--main)} .row .v{min-width:56px;text-align:right;font-weight:800;color:var(--main);font-variant-numeric:tabular-nums} .reads{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin-top:10px} .read{background:var(--main-soft);border-radius:12px;padding:8px 10px;text-align:center} .read span{display:block;font-size:12.5px;color:var(--dim)} .read b{font-size:24px;font-variant-numeric:tabular-nums} .btns{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px} .btns button{font:inherit;font-size:14px;font-weight:700;border:1px solid var(--line);background:var(--card);color:var(--ink); border-radius:10px;padding:7px 14px;cursor:pointer} .ex{display:grid;gap:10px} .ex .card{margin:0} .tag{display:inline-block;font-size:12.5px;font-weight:800;color:#fff;background:var(--main);border-radius:999px;padding:2px 10px;margin-bottom:4px} .q{margin:0 0 12px} .q ol{list-style:none;padding:0;margin:8px 0 0;display:grid;gap:6px} .q li button{width:100%;text-align:left;font:inherit;font-size:16px;border:1px solid var(--line);background:var(--card);color:var(--ink); border-radius:12px;padding:9px 12px;cursor:pointer} .q li button.ok{background:#e3f4e8;border-color:#3a7a3f;color:#1e4d24} .q li button.no{background:#fbe7e5;border-color:#b23a30;color:#6b1f19} .q .why{display:none;margin-top:8px;padding:9px 12px;border-radius:10px;background:var(--main-soft);font-size:15.5px} .q.done .why{display:block} .limit{border-left:5px solid var(--c3)} footer{text-align:center;color:var(--dim);font-size:13px;padding:10px 0 24px} </style> </head> <body> <script> /* ▼ 내 앱 설정 — 수업 페이지(만들기 탭 ⓒ 시작 앱)가 이 부분을 채웁니다 ▼ */ const MY = { "title": "공기 탐험대", "about": "기체 입자가 움직이고 부딪히는 모습으로 압력·부피·온도의 관계를 알아보는 시뮬레이터", "maker": "예시", "theme": "sky", "examples": [ { "name": "산 위에서 부푼 과자 봉지", "kind": "압력과 부피", "text": "높은 산에 올라가면 봉지 바깥 공기가 누르는 힘이 약해집니다. 봉지 안 기체 입자들은 그대로 안쪽 벽을 밀기 때문에 봉지가 부풀어 오릅니다." }, { "name": "뜨거운 물에 넣은 찌그러진 탁구공", "kind": "온도와 부피", "text": "탁구공 속 공기가 데워지면 기체 입자의 움직임이 활발해져 공 안쪽을 더 세게 밉니다. 그래서 찌그러진 곳이 펴집니다." } ], "quiz": [ { "q": "기체 입자 수를 늘리면 벽에 부딪히는 횟수는 어떻게 되나요?", "choices": ["줄어든다", "변하지 않는다", "늘어난다", "0이 된다"], "answer": 2, "why": "입자가 많을수록 더 자주 부딪히고, 그래서 압력이 커집니다." }, { "q": "온도가 일정할 때 주사기를 눌러 부피를 줄이면 기체 입자의 빠르기는 어떻게 되나요?", "choices": ["빨라진다", "느려진다", "변하지 않는다", "멈춘다"], "answer": 2, "why": "입자의 빠르기는 온도에 따라 달라지므로, 온도가 그대로면 빠르기도 그대로입니다. 부피가 줄면 벽까지 가는 거리가 짧아져 더 자주 부딪힐 뿐입니다." }, { "q": "공기를 데우면 부피가 커지는 까닭으로 알맞은 것은?", "choices": ["입자 하나하나가 커져서", "입자 수가 늘어나서", "입자의 움직임이 활발해져서", "입자가 가벼워져서"], "answer": 2, "why": "입자의 크기와 수는 그대로입니다. 움직임이 활발해진 입자가 더 세게 밀어 입자 사이가 넓어집니다." } ], "limits": "화면은 2차원이고, 실제 공기보다 입자를 훨씬 적게 그렸습니다. 모든 입자가 같은 빠르기로 움직이고 입자끼리는 부딪히지 않는데, 실제 기체 입자는 빠르기가 제각각이고 서로 부딪힙니다." }; /* ▲ 내 앱 설정 끝 ▲ */ </script> <header> <h1 id="appTitle">공기 탐험대</h1> <p id="appAbout"></p> </header> <nav id="tabs"> <button data-t="t1" class="on">기체의 압력</button> <button data-t="t2">압력과 부피</button> <button data-t="t3">온도와 부피</button> <button data-t="t4">생활 속 공기</button> <button data-t="t5">내가 낸 퀴즈</button> </nav> <main> <!-- 1. 기체의 압력 --> <section id="t1" class="on"> <div class="card"> <h2>기체의 압력은 어디에서 올까?</h2> <p class="say">기체 입자는 쉬지 않고 모든 방향으로 움직이며 벽에 부딪힙니다. <b>기체의 압력은 일정한 면적에 기체 입자가 충돌하여 작용하는 힘</b>이고, 모든 방향으로 작용합니다.</p> <canvas id="c1" width="520" height="340"></canvas> <div class="row"><label for="n1">입자 수</label><input type="range" id="n1" min="4" max="60" value="24"><span class="v" id="n1v">24</span></div> <div class="btns"><button id="p1">⏸ 멈춤</button></div> <div class="reads"> <div class="read"><span>1초 동안 벽에 부딪힌 횟수</span><b id="h1">0</b></div> <div class="read"><span>압력 눈금</span><b id="pr1">0</b></div> </div> <p class="say" style="margin-top:10px">입자 수를 늘리면 더 자주 부딪혀 압력이 커집니다.</p> </div> </section> <!-- 2. 압력과 부피 --> <section id="t2"> <div class="card"> <h2>좁히면 왜 압력이 커질까?</h2> <p class="say">온도가 일정할 때 오른쪽 벽(손잡이)을 밀어 부피를 줄여 보세요. 입자의 빠르기는 그대로인데, 벽까지 가는 거리가 짧아져 <b>더 자주</b> 부딪히므로 압력이 커집니다.</p> <canvas id="c2" width="520" height="340"></canvas> <div class="row"><label for="v2">부피</label><input type="range" id="v2" min="35" max="100" value="100"><span class="v" id="v2v">100%</span></div> <div class="reads"> <div class="read"><span>부피</span><b id="vo2">100%</b></div> <div class="read"><span>1초당 손잡이 충돌 횟수</span><b id="h2">0</b></div> <div class="read"><span>평균 빠르기</span><b id="s2">100</b></div> <div class="read"><span>압력 눈금</span><b id="pr2">0</b></div> </div> <p class="say" style="margin-top:10px">압력은 일정한 면적에 부딪히는 힘이므로, 크기가 변하지 않는 손잡이에 부딪힌 횟수만 셉니다. (위·아래 벽은 상자를 좁히면 함께 짧아집니다)</p> </div> </section> <!-- 3. 온도와 부피 --> <section id="t3"> <div class="card"> <h2>데우면 왜 부풀어 오를까?</h2> <p class="say">온도를 높이면 입자의 움직임이 활발해져 뚜껑을 더 세게 밀어 올립니다. 뚜껑은 안에서 미는 것과 바깥 공기가 누르는 것이 같아지는 곳에서 멈춥니다. 입자의 <b>수와 크기는 변하지 않습니다</b>.</p> <canvas id="c3" width="520" height="360"></canvas> <div class="row"><label for="t3r">온도</label><input type="range" id="t3r" min="0" max="100" value="20"><span class="v" id="t3v">20℃</span></div> <div class="reads"> <div class="read"><span>온도</span><b id="to3">20℃</b></div> <div class="read"><span>부피</span><b id="vo3">0%</b></div> <div class="read"><span>입자 수</span><b id="n3">30</b></div> <div class="read"><span>평균 빠르기</span><b id="s3">100</b></div> </div> </div> </section> <!-- 4. 생활 속 공기 --> <section id="t4"> <div class="card"><h2>생활 속 공기</h2><p class="say">내가 고른 예시와 그 까닭입니다.</p></div> <div class="ex" id="exList"></div> </section> <!-- 5. 내가 낸 퀴즈 --> <section id="t5"> <div class="card"><h2>내가 낸 퀴즈</h2><p class="say">답을 고르면 맞았는지와 풀이가 나옵니다.</p></div> <div id="quizList"></div> </section> <div class="card limit"><h2 style="font-size:18px">이 시뮬레이터의 한계</h2><p class="say" id="limits" style="margin:0"></p></div> </main> <footer id="foot"></footer> <script> /* ── 설정 반영 ── */ const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); document.title = MY.title || '공기 탐험대'; document.getElementById('appTitle').textContent = MY.title || '공기 탐험대'; document.getElementById('appAbout').textContent = MY.about || ''; if (['mint','sunset','night'].includes(MY.theme)) document.body.classList.add(MY.theme); document.getElementById('limits').textContent = MY.limits || ''; document.getElementById('foot').textContent = MY.maker ? '만든 사람: ' + MY.maker : ''; document.getElementById('exList').innerHTML = (MY.examples || []).map(e => `<div class="card"><span class="tag">${esc(e.kind)}</span><h2 style="font-size:19px;margin:2px 0 6px">${esc(e.name)}</h2><p class="say" style="margin:0">${esc(e.text)}</p></div>`).join('') || '<div class="card"><p class="say">아직 예시가 없습니다.</p></div>'; document.getElementById('quizList').innerHTML = (MY.quiz || []).map((q, i) => `<div class="card q" data-a="${Number(q.answer)}"><b>${i + 1}. ${esc(q.q)}</b><ol>${(q.choices || []).map((c, j) => `<li><button data-j="${j}">${'①②③④⑤'[j] || ''} ${esc(c)}</button></li>`).join('')}</ol><div class="why">${esc(q.why)}</div></div>`).join('') || '<div class="card"><p class="say">아직 퀴즈가 없습니다.</p></div>'; document.getElementById('quizList').addEventListener('click', e => { const b = e.target.closest('button[data-j]'); if (!b) return; const box = b.closest('.q'); if (box.classList.contains('done')) return; const ok = Number(b.dataset.j) === Number(box.dataset.a); b.classList.add(ok ? 'ok' : 'no'); if (ok) box.classList.add('done'); else setTimeout(() => b.classList.remove('no'), 700); }); /* ── 탭 ── */ let cur = 't1'; document.getElementById('tabs').addEventListener('click', e => { const b = e.target.closest('button[data-t]'); if (!b) return; cur = b.dataset.t; document.querySelectorAll('#tabs button').forEach(x => x.classList.toggle('on', x === b)); document.querySelectorAll('main section').forEach(s => s.classList.toggle('on', s.id === cur)); }); /* ── 공통: 입자 그리기와 화살표 ── */ const GREEN = '#3a7a3f'; function dot(ctx, x, y, r) { ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fillStyle = GREEN; ctx.fill(); } function arrow(ctx, x1, y1, x2, y2) { const a = Math.atan2(y2 - y1, x2 - x1); ctx.strokeStyle = '#b23a30'; ctx.fillStyle = '#b23a30'; ctx.lineWidth = 2.2; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); ctx.beginPath(); ctx.moveTo(x2, y2); ctx.lineTo(x2 - 8 * Math.cos(a - .42), y2 - 8 * Math.sin(a - .42)); ctx.lineTo(x2 - 8 * Math.cos(a + .42), y2 - 8 * Math.sin(a + .42)); ctx.closePath(); ctx.fill(); } function spray(n, x0, x1, y0, y1, r, v) { const out = []; for (let i = 0; i < n; i++) { const a = Math.random() * Math.PI * 2; out.push({ x: x0 + r + Math.random() * (x1 - x0 - 2 * r), y: y0 + r + Math.random() * (y1 - y0 - 2 * r), vx: Math.cos(a) * v, vy: Math.sin(a) * v }); } return out; } /* ── 1. 기체의 압력 : 네 벽 모두에서 튕기는 순간 한 번씩 센다 ── */ const T1 = (() => { const cv = document.getElementById('c1'), ctx = cv.getContext('2d'), W = cv.width, H = cv.height, R = 5, V = 2.7; let ps = spray(24, 0, W, 0, H, R, V), playing = true, hit = 0, shown = 0, flash = [0, 0, 0, 0]; function step() { for (const p of ps) { p.x += p.vx; p.y += p.vy; if (p.x < R && p.vx < 0) { p.x = R; p.vx = -p.vx; hit++; flash[0] = 6; } if (p.x > W - R && p.vx > 0) { p.x = W - R; p.vx = -p.vx; hit++; flash[1] = 6; } if (p.y < R && p.vy < 0) { p.y = R; p.vy = -p.vy; hit++; flash[2] = 6; } if (p.y > H - R && p.vy > 0) { p.y = H - R; p.vy = -p.vy; hit++; flash[3] = 6; } } } function draw() { ctx.fillStyle = '#f6fbff'; ctx.fillRect(0, 0, W, H); ps.forEach(p => dot(ctx, p.x, p.y, R)); for (let i = 0; i < 4; i++) { const y = H * (i + 1) / 5, x = W * (i + 1) / 5; arrow(ctx, 26, y, 6, y); arrow(ctx, W - 26, y, W - 6, y); arrow(ctx, x, 26, x, 6); arrow(ctx, x, H - 26, x, H - 6); } ctx.lineWidth = 5; ctx.strokeStyle = '#1c507e'; ctx.strokeRect(2.5, 2.5, W - 5, H - 5); ctx.strokeStyle = 'rgba(255,196,0,.9)'; ctx.lineWidth = 6; if (flash[0] > 0) { ctx.beginPath(); ctx.moveTo(3, 0); ctx.lineTo(3, H); ctx.stroke(); flash[0]--; } if (flash[1] > 0) { ctx.beginPath(); ctx.moveTo(W - 3, 0); ctx.lineTo(W - 3, H); ctx.stroke(); flash[1]--; } if (flash[2] > 0) { ctx.beginPath(); ctx.moveTo(0, 3); ctx.lineTo(W, 3); ctx.stroke(); flash[2]--; } if (flash[3] > 0) { ctx.beginPath(); ctx.moveTo(0, H - 3); ctx.lineTo(W, H - 3); ctx.stroke(); flash[3]--; } } const n = document.getElementById('n1'); n.oninput = () => { document.getElementById('n1v').textContent = n.value; ps = spray(+n.value, 0, W, 0, H, R, V); }; document.getElementById('p1').onclick = e => { playing = !playing; e.target.textContent = playing ? '⏸ 멈춤' : '▶ 움직이기'; }; return { step: () => { if (playing) step(); }, draw, second() { shown = hit; hit = 0; document.getElementById('h1').textContent = shown; document.getElementById('pr1').textContent = Math.round(shown * 3); } }; })(); /* ── 2. 압력과 부피 : 손잡이(오른쪽 벽)만 센다, 빠르기는 건드리지 않는다 ── */ const T2 = (() => { const cv = document.getElementById('c2'), ctx = cv.getContext('2d'), W = cv.width, H = cv.height, R = 5, V = 2.6; let wall = W, ps = spray(120, 0, W, 0, H, R, V), hit = 0, shown = 0; function step() { for (const p of ps) { p.x += p.vx; p.y += p.vy; if (p.x < R && p.vx < 0) { p.x = R; p.vx = -p.vx; } if (p.y < R && p.vy < 0) { p.y = R; p.vy = -p.vy; } if (p.y > H - R && p.vy > 0) { p.y = H - R; p.vy = -p.vy; } if (p.x > wall - R && p.vx > 0) { p.x = wall - R; p.vx = -p.vx; hit++; } } } function speed() { let s = 0; ps.forEach(p => s += Math.hypot(p.vx, p.vy)); return ps.length ? s / ps.length / V * 100 : 0; } function draw() { ctx.fillStyle = '#e8eef3'; ctx.fillRect(0, 0, W, H); ctx.fillStyle = '#f6fbff'; ctx.fillRect(0, 0, wall, H); ps.forEach(p => dot(ctx, p.x, p.y, R)); ctx.fillStyle = '#8a5f24'; ctx.fillRect(wall - 6, 0, 12, H); ctx.fillStyle = '#c4711e'; ctx.fillRect(wall - 6, H / 2 - 26, 12, 52); ctx.lineWidth = 5; ctx.strokeStyle = '#1c507e'; ctx.strokeRect(2.5, 2.5, wall - 5, H - 5); } const v = document.getElementById('v2'); v.oninput = () => { wall = W * v.value / 100; for (const p of ps) if (p.x > wall - R) p.x = wall - R; // 벽이 입자를 밀고 간다 document.getElementById('v2v').textContent = v.value + '%'; document.getElementById('vo2').textContent = v.value + '%'; }; return { step, draw, second() { shown = hit; hit = 0; document.getElementById('h2').textContent = shown; document.getElementById('s2').textContent = Math.round(speed()); document.getElementById('pr2').textContent = Math.round(shown * 3); } }; })(); /* ── 3. 온도와 부피 : 뚜껑은 안에서 미는 것 = 바깥 공기가 누르는 것 에서 멈춘다 ── */ const T3 = (() => { const cv = document.getElementById('c3'), ctx = cv.getContext('2d'), W = cv.width, H = cv.height; const R = 5, N = 30, V0 = 2.6, ATM = 0.027, PUSH_K = 0.05, DAMP = 0.9, TOP = 34, BOTTOM = H - 40; let temp = 20, lidY = H / 2, lidV = 0; const vOf = t => V0 * Math.sqrt((t + 273) / 293); let ps = spray(N, 0, W, lidY, H, R, vOf(temp)); function step() { let push = 0; for (const p of ps) { p.x += p.vx; p.y += p.vy; if (p.x < R && p.vx < 0) { p.x = R; p.vx = -p.vx; } if (p.x > W - R && p.vx > 0) { p.x = W - R; p.vx = -p.vx; } if (p.y > H - R && p.vy > 0) { p.y = H - R; p.vy = -p.vy; } if (p.y < lidY + R && p.vy < 0) { p.y = lidY + R; p.vy = -p.vy; push += 2 * Math.abs(p.vy); } } lidV += push * PUSH_K - ATM; lidV *= DAMP; lidY -= lidV; if (lidY < TOP) { lidY = TOP; lidV = 0; } if (lidY > BOTTOM) { lidY = BOTTOM; lidV = 0; } for (const p of ps) if (p.y < lidY + R) p.y = lidY + R; } function draw() { ctx.fillStyle = '#e8eef3'; ctx.fillRect(0, 0, W, H); ctx.fillStyle = '#f6fbff'; ctx.fillRect(0, lidY, W, H - lidY); ps.forEach(p => dot(ctx, p.x, p.y, R)); for (let i = 0; i < 4; i++) { const x = W * (i + 1) / 5; arrow(ctx, x, lidY - 44, x, lidY - 16); } // 바깥 공기가 누른다 ctx.fillStyle = '#8a5f24'; ctx.fillRect(0, lidY - 13, W, 13); ctx.strokeStyle = '#1c507e'; ctx.lineWidth = 5; ctx.beginPath(); ctx.moveTo(2.5, 0); ctx.lineTo(2.5, H - 2.5); ctx.lineTo(W - 2.5, H - 2.5); ctx.lineTo(W - 2.5, 0); ctx.stroke(); const f = temp / 100; ctx.fillStyle = `rgba(196,113,30,${.25 + .75 * f})`; for (let i = 0; i < 7; i++) { const x = W * (i + 1) / 8, h = 6 + 22 * f; ctx.beginPath(); ctx.moveTo(x - 7, H - 3); ctx.quadraticCurveTo(x, H - 3 - h * 1.7, x + 7, H - 3); ctx.fill(); } } const t = document.getElementById('t3r'); t.oninput = () => { const old = vOf(temp); temp = +t.value; const k = vOf(temp) / old; ps.forEach(p => { p.vx *= k; p.vy *= k; }); // 온도가 바꾸는 것은 빠르기뿐 document.getElementById('t3v').textContent = temp + '℃'; document.getElementById('to3').textContent = temp + '℃'; }; return { step, draw, second() { let s = 0; ps.forEach(p => s += Math.hypot(p.vx, p.vy)); document.getElementById('vo3').textContent = Math.round((H - lidY) / H * 100) + '%'; document.getElementById('n3').textContent = ps.length; document.getElementById('s3').textContent = Math.round(s / ps.length / V0 * 100); } }; })(); /* ── 되풀이 : 1초에 60걸음으로 고정 (화면 새로고침 빠르기와 상관없이 같은 숫자) ── */ const SIMS = { t1: T1, t2: T2, t3: T3 }; let last = performance.now(), acc = 0, secAcc = 0; function loop(now) { const dt = Math.min(0.1, (now - last) / 1000); last = now; const sim = SIMS[cur]; if (sim) { acc += dt; secAcc += dt; while (acc >= 1 / 60) { sim.step(); acc -= 1 / 60; } if (secAcc >= 1) { sim.second(); secAcc = 0; } sim.draw(); } requestAnimationFrame(loop); } requestAnimationFrame(loop); </script> </body> </html>