浮力模拟器
浮力模拟器1.可以通过调节质量和体积,查看方块在水中的状态(浮起或沉入水底)
2.可以切换不同的材质,观察不同材质不同体积时,在水中的受力
3.初始化为100L水,可实时观察水位高度
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>浮力模拟</title>
<style>
:root{
/* 页面浅绿色背景 */
--bg-page: linear-gradient(#eaffea 0%, #e3ffe3 50%, #dbffdb 100%);
--panel-bg: rgba(255,255,255,0.92);
--panel-border: #d5d7de;
--text:#1f2937;
--muted:#6b7280;
/* 水体为蓝色 */
--water-top:#5aa6ff;
--water-bottom:#386bcb;
/* 默认方块颜色(未选材质时) */
--cube:#b97a42;
--cube-edge:#7b4f24;
}
*{box-sizing:border-box}
html,body{height:100%}
body{
margin:0;
font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;
color:var(--text);
background: var(--bg-page);
display:flex;
flex-direction:column;
gap:12px;
}
header{
padding:10px 16px;
display:flex;
align-items:center;
justify-content:space-between;
gap:12px;
}
header h1{margin:0; font-size:18px}
.mini{font-size:12px;color:var(--muted)}
.app{
display:grid;
grid-template-columns: 1.25fr 0.85fr;
gap:16px;
width:100%;
max-width:1200px;
padding:0 12px 16px;
margin:0 auto;
}
@media (max-width: 900px){
.app{grid-template-columns: 1fr}
}
/* 池子画布区域:池子居中 */
.stage-wrap{
position:relative;
width:100%;
aspect-ratio: 16 / 9;
border-radius:12px;
overflow:hidden;
box-shadow:0 10px 28px rgba(0,0,0,.14);
background: radial-gradient(900px 280px at 50% 30%, rgba(255,255,255,.4), transparent 60%), var(--bg-page);
display:flex;
align-items:center; justify-content:center;
}
canvas{width:100%; height:100%; display:block}
/* 控制面板 */
.panel{
background:var(--panel-bg);
border:1px solid var(--panel-border);
border-radius:12px;
padding:12px;
box-shadow:0 4px 18px rgba(0,0,0,.10);
}
.panel h2{margin:6px 0 12px; font-size:16px}
.row{display:flex; align-items:center; gap:10px; margin:8px 0}
.row label{min-width:64px; font-size:14px}
.row .value{width:110px; text-align:right; font-variant-numeric: tabular-nums}
.row input{flex:1}
.inline{display:flex; gap:8px; align-items:center; flex-wrap:wrap}
.btn-line{display:flex; gap:8px; margin-top:8px; flex-wrap:wrap}
button{
appearance:none; border:1px solid var(--panel-border); background:white;
color:var(--text); border-radius:8px; padding:8px 12px; cursor:pointer
}
button:hover{border-color:#c0c4cf}
.card{
margin-top:12px; padding:10px; border:1px dashed #cfd3dd; border-radius:10px; background:white
}
.mono{font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;}
.good{color:#16a34a}
.bad{color:#ef4444}
</style>
</head>
<body>
<header>
<h1>浮力模拟</h1>
<div class="mini">规则:m=ρ×V;体积≤10 L,质量上限=ρ×10 L;</div>
</header>
<main class="app">
<section class="stage-wrap">
<canvas id="stage" aria-label="交互画布"></canvas>
</section>
<aside class="panel" id="panel">
<h2>控制面板</h2>
<div class="row">
<label for="volume">体积</label>
<input id="volume" type="range" step="0.01">
<div class="value"><span id="volumeOut">—</span> L</div>
</div>
<div class="row">
<label for="mass">质量</label>
<input id="mass" type="range" step="0.01">
<div class="value"><span id="massOut">—</span> kg</div>
</div>
<div class="row inline">
<label>密度</label>
<div class="value" style="width:auto"><span id="rhoOut">—</span> kg/m³</div>
<span id="floatStatus" class="mini">—</span>
</div>
<div class="row inline">
<label for="preset">材质</label>
<select id="preset">
<option value="">无(自由模式)</option>
<option value="wood" selected>木头 ~ 600</option>
<option value="ice">冰 ~ 920</option>
<option value="glass">玻璃 ~ 2500</option>
<option value="al">铝 ~ 2700</option>
<option value="fe">钢铁 ~ 7850</option>
<option value="cu">铜 ~ 8900</option>
</select>
<label class="mini inline" style="gap:6px">
<input id="lockRho" type="checkbox" checked>
锁定密度
</label>
</div>
<div class="btn-line">
<button id="reset">重置</button>
</div>
<div class="card mono" id="readout">计算中…</div>
</aside>
</main>
<script>
// ================= 常量与单位(SI) =================
const RHO_WATER = 1000.0; // kg/m³
const G = 9.81; // m/s²
const W =0.8, D = 0.5, H = 1.0; // 容器长宽高 m
const A = W * D; // 底面积 m²
const V_WATER0 = 0.1; // 初始水量 0.1 m³ = 100 L
const EPS = 1e-9;
const MASS_EPS = 1e-9; // “无方块”质量阈值
const VOLUME_ZERO_EPS_L = 1e-6; // 升,体积≈0 的阈值
// 控制水位线标签的垂直偏移(像素,数值越大越高)
const HUD_LABEL_OFFSET_PX = 16;
// 方块几何限制
const S_MIN = 0.05;
const S_MAX = Math.min(W, D) * 0.95;
const V_MIN_L = 0.00; // 体积最小值改为 0 L
const V_MAX_L_HARD = 10.0; // 体积上限:10 L
const V_MAX_M3 = V_MAX_L_HARD / 1000.0;
// 材质密度与颜色(铜色为 RGB(184,115,51))
const PRESET_RHO = {
wood: 600, ice: 920, glass: 2500, al: 2700, fe: 7850, cu: 8900
};
const PRESET_COLOR = {
wood: { fill:'#8d6e63', edge:'#5d4037' }, // 木
ice:{ fill:'rgba(188,235,255,0.75)', edge:'#90cdf4' }, // 冰
glass:{ fill:'rgba(200,220,240,0.45)', edge:'#93b4d6' }, // 玻璃
al: { fill:'#c7c9cc', edge:'#8e8f93' }, // 铝
fe: { fill:'#5f6672', edge:'#3f444c' }, // 钢铁
cu: { fill:'rgb(184,115,51)', edge:'rgb(124,78,35)' }, // 铜
default: { fill:getCss('--cube'), edge:getCss('--cube-edge') }
};
// ================ DOM ================
const canvas = document.getElementById('stage');
const ctx = canvas.getContext('2d');
const elVolume = document.getElementById('volume');
const elMass = document.getElementById('mass');
const outVolume = document.getElementById('volumeOut');
const outMass = document.getElementById('massOut');
const outRho = document.getElementById('rhoOut');
const outFloatStatus = document.getElementById('floatStatus');
const readout = document.getElementById('readout');
const presetSel = document.getElementById('preset');
const lockRho = document.getElementById('lockRho');
const btnReset = document.getElementById('reset');
// 初始化滑块范围
elVolume.min = V_MIN_L.toFixed(2);
elVolume.max = V_MAX_L_HARD.toFixed(2);
elVolume.value = "5.00"; // 默认 5 L
// ================ 工具 ================
function getCss(name){ return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); }
function clamp(v,a,b){ return Math.max(a, Math.min(b, v)); }
const L_to_m3 = L => L / 1000.0;
const m3_to_L = m3 => m3 * 1000.0;
const fmt = (x,n=2) => (+x).toFixed(n);
function activeRho(){
const key = presetSel.value;
return key ? PRESET_RHO : null;
}
function ensureMassRange(){
const rho = activeRho();
if (rho){
elMass.min = "0.00";
elMass.max = (rho * V_MAX_M3).toFixed(2); // ρ × 10 L
}else{
elMass.min = "0.00";
elMass.max = "200.00"; // 自由模式下给一个宽松上限
}
}
function isCubeActive(){
return state.mass > MASS_EPS && state.volumeL > VOLUME_ZERO_EPS_L;
}
// ================ 物理求解(静态平衡) ================
function solveEquilibrium(mass, side){
const s = side, m = mass, V_cube = s**3;
const canFloatByDensity = m <= RHO_WATER * V_cube + EPS;
if (canFloatByDensity){
const V_sub = m / RHO_WATER;
const s2 = s*s || EPS; // 防护
const h_sub = V_sub / s2;
const h_pre = (V_WATER0 + V_sub)/A;
const h = Math.min(h_pre, H);
const y_bottom_est = h - h_sub;
if (y_bottom_est >= -EPS){
const y_bottom = Math.max(0, y_bottom_est);
const V_water_actual = Math.max(0, A*h - V_sub);
const overflow = Math.max(0, V_WATER0 - V_water_actual);
return {mode:'浮起', h, V_cube, V_sub, h_sub, y_bottom,
overflow, F_b:RHO_WATER*G*V_sub, W_force:m*G, N:0};
}
}
let A_minus = A - s*s; if (A_minus<=0) A_minus = EPS;
const h_partial_pre = V_WATER0 / A_minus;
let V_sub, h, overflow=0, mode;
if (h_partial_pre < s - EPS){
h = Math.min(h_partial_pre, H);
if (h_partial_pre > H + EPS) overflow = Math.max(0, V_WATER0 - H*A_minus);
V_sub = s*s*h; mode='触底-部分浸没';
}else{
V_sub = s**3;
const h_pre = (V_WATER0 + V_sub)/A;
h = Math.min(h_pre, H);
if (h_pre > H + EPS) overflow = Math.max(0, V_WATER0 - (A*H - V_sub));
mode='触底-完全浸没';
}
const F_b = RHO_WATER*G*V_sub, W_force=m*G, N=Math.max(0, W_force-F_b);
const h_sub = Math.min(s>EPS ? V_sub/(s*s) : 0, s);
return {mode, h, V_cube, V_sub, h_sub, y_bottom:0, overflow, F_b, W_force, N};
}
// 仅水(无方块)解:用于质量=0 或体积=0
function solveWaterOnly(){
const h_pre = V_WATER0 / A;
const h = Math.min(h_pre, H);
return {
mode: '无方块',
h,
V_cube: 0, V_sub: 0, h_sub: 0,
y_bottom: 0,
overflow: Math.max(0, V_WATER0 - A*H),
F_b: 0, W_force: 0, N: 0
};
}
// ================ 拖拽中的即时置换 =================
function solveDuringDrag(y_bottom, side, mass){
const s = side, V_cube = s**3, s2 = s*s;
const h0 = V_WATER0 / A; // 未浸没解
const hFull = (V_WATER0 + V_cube)/A; // 完全浸没解
let h_pre, d; // 浸没高度(m)
if (h0 <= y_bottom + EPS){
d = 0; h_pre = h0; // 未浸没
}else{
const A_minus = A - s2;
if (A_minus <= EPS){
if (hFull >= y_bottom + s - EPS){ d = s; h_pre = hFull; }
else { d = Math.max(0, h0 - y_bottom); h_pre = h0; }
}else{
const hPart = (V_WATER0 - s2*y_bottom) / A_minus;
if (hPart > y_bottom + EPS && hPart < y_bottom + s - EPS){
d = hPart - y_bottom; h_pre = hPart;
}else if (hPart <= y_bottom + EPS){
d = 0; h_pre = h0;
}else{
d = s; h_pre = hFull;
}
}
}
const h = Math.min(h_pre, H);
const V_sub = s2 * d;
const overflow = Math.max(0, V_WATER0 + V_sub - A*H);
return {
mode: '拖拽中',
h, V_cube, V_sub,
h_sub: d,
y_bottom: clamp(y_bottom, 0, H - s),
overflow,
F_b: RHO_WATER * G * V_sub,
W_force: mass * G,
N: 0
};
}
// ================ 池子居中 ================
const view = { w:0, h:0, tank:{left:0, top:0, width:0, height:0, scale:1} };
function resizeCanvas(){
const rect = canvas.getBoundingClientRect();
const dpr = Math.max(1, window.devicePixelRatio || 1);
canvas.width = Math.round(rect.width * dpr);
canvas.height = Math.round(rect.height * dpr);
ctx.setTransform(dpr,0,0,dpr,0,0);
view.w = rect.width; view.h = rect.height;
const margin = 18;
const S = Math.min((view.w - margin*2)/W, (view.h - margin*2)/H);
const tankW = W*S, tankH = H*S;
view.tank.left = (view.w - tankW)/2;
view.tank.top = (view.h - tankH)/2;
view.tank.width = tankW; view.tank.height = tankH; view.tank.scale = S;
draw();
}
function yFromBottom(y_m){
const t = view.tank.top, S = view.tank.scale;
return t + (H - y_m) * S;
}
// ================ 交互状态(含拖拽) ================
const state = {
mass: 0,
volumeL: parseFloat(elVolume.value),
s: Math.cbrt(L_to_m3(parseFloat(elVolume.value))),
sol: null, // 平衡解
solDrag: null, // 拖拽即时解
cubePx: {x:0,y:0,w:0,h:0},
cubeTargetPx: {x:0,y:0},
dragging:false, dragOffset:{x:0,y:0}, animId:null,
lastInput:'volume'
};
// ================ 绘制 ================
function drawTank(){
const {left, top, width, height} = view.tank;
ctx.fillStyle = "#9aa0a6";
ctx.fillRect(left-8, top-8, width+16, height+16);
ctx.fillStyle = "#cfd3da";
ctx.fillRect(left, top, width, height);
ctx.lineWidth = 2;
ctx.strokeStyle = "#6a6a6a";
ctx.strokeRect(left, top, width, height);
}
function drawWater(sol){
const {left} = view.tank;
const S = view.tank.scale;
const h = sol.h, yTop = yFromBottom(h), hPx = h*S;
const grd = ctx.createLinearGradient(0, yTop + hPx, 0, yTop);
grd.addColorStop(0, getCss('--water-bottom'));
grd.addColorStop(1, getCss('--water-top'));
ctx.fillStyle = grd;
ctx.fillRect(view.tank.left, yTop, view.tank.width, hPx);
ctx.fillStyle = "rgba(255,255,255,0.35)";
ctx.fillRect(left, yTop-1.5, view.tank.width, 1.5);
}
function equilibriumCubeRect(sol){
const S = view.tank.scale;
const s = Math.cbrt(sol.V_cube);
const wPx = s*S, hPx = s*S;
const xLeft = view.tank.left + (view.tank.width - wPx)/2; // 目标水平居中
const yTop = yFromBottom(sol.y_bottom + s);
return {x:xLeft, y:yTop, w:wPx, h:hPx};
}
function drawCube(rect, materialKey){
const style = PRESET_COLOR || PRESET_COLOR.default;
ctx.fillStyle = style.fill;
ctx.strokeStyle = style.edge;
ctx.lineWidth = 2;
ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
ctx.strokeRect(rect.x, rect.y, rect.w, rect.h);
ctx.fillStyle = "rgba(255,255,255,0.14)";
ctx.fillRect(rect.x, rect.y, rect.w, 6);
ctx.fillStyle = "rgba(0,0,0,0.06)";
for(let i=0;i<6;i++){ ctx.fillRect(rect.x + i*(rect.w/6), rect.y, 2, rect.h); }
}
// 水位线标签:只显示“总水量(升)”,位置可通过 HUD_LABEL_OFFSET_PX 调高
function drawHUD(sol){
const VsL = m3_to_L(sol.V_sub);
const totalL = 100 + VsL;
const text = `${fmt(totalL,2)} 升(L)`;
ctx.font = "12px ui-monospace, monospace";
const tw = ctx.measureText(text).width + 16, th = 18;
// 将标签位置比水面更高 HUD_LABEL_OFFSET_PX 像素,并做夹取避免越界
let y = yFromBottom(sol.h) - th/2 - HUD_LABEL_OFFSET_PX;
const yMin = view.tank.top + 4;
const yMax = view.tank.top + view.tank.height - th - 4;
y = Math.max(yMin, Math.min(y, yMax));
const x = view.tank.left - tw - 8;
ctx.fillStyle = "rgba(0,0,0,0.25)"; ctx.fillRect(x, y, tw, th);
ctx.strokeStyle = "rgba(255,255,255,0.6)"; ctx.strokeRect(x, y, tw, th);
ctx.fillStyle = "#e5f0ff"; ctx.fillText(text, x+8, y+12);
}
function draw(){
const solActive = state.dragging && state.solDrag ? state.solDrag : state.sol;
ctx.clearRect(0,0,canvas.width, canvas.height);
drawTank();
if (solActive){
drawWater(solActive);
drawHUD(solActive);
if (isCubeActive()){
drawCube(state.cubePx, presetSel.value);
}
}
}
// ================ 状态设置 ================
function setFloatStatusFromSol(sol){
if (!isCubeActive()){
outFloatStatus.textContent = "无方块(质量=0 或体积=0)";
outFloatStatus.className = "mini";
return;
}
const floats = sol.mode === '浮起';
outFloatStatus.textContent = floats ? "密度低于水 → 漂浮" : "密度高/受限 → 下沉或触底";
outFloatStatus.className = "mini " + (floats ? "good" : "bad");
}
function updateFromControls(source=state.lastInput){
state.lastInput = source;
// 读取滑块
let volumeL = clamp(parseFloat(elVolume.value), V_MIN_L, V_MAX_L_HARD);
let mass = parseFloat(elMass.value);
const rhoAct = activeRho();
// 若体积为 0:强制质量=0(不论什么模式/来源)
if (volumeL <= VOLUME_ZERO_EPS_L){
volumeL = 0;
mass = 0;
elVolume.value = "0.00";
elMass.value = "0.00";
}else{
// 材质模式:强制锁定密度,体积/质量双向联动
if (rhoAct){
lockRho.checked = true;
lockRho.disabled = true;
ensureMassRange();
if (source === 'mass'){
mass = clamp(mass, parseFloat(elMass.min), parseFloat(elMass.max));
const V = mass / rhoAct; // m³
volumeL = clamp(m3_to_L(V), V_MIN_L, V_MAX_L_HARD);
elVolume.value = volumeL.toFixed(2);
}else{
volumeL = clamp(volumeL, V_MIN_L, V_MAX_L_HARD);
const V = L_to_m3(volumeL);
mass = clamp(rhoAct * V, parseFloat(elMass.min), parseFloat(elMass.max));
elMass.value = mass.toFixed(2);
}
}else{
// 自由模式
lockRho.disabled = false;
ensureMassRange();
if (lockRho.checked){
const rhoLock = getLockedRhoForFreeMode();
elMass.max = (rhoLock * V_MAX_M3).toFixed(2);
if (source === 'mass'){
mass = clamp(mass, 0, parseFloat(elMass.max));
volumeL = clamp(m3_to_L(mass / rhoLock), V_MIN_L, V_MAX_L_HARD);
elVolume.value = volumeL.toFixed(2);
}else{
const V = L_to_m3(volumeL);
mass = clamp(rhoLock * V, 0, parseFloat(elMass.max));
elMass.value = mass.toFixed(2);
}
}
}
}
// 写回状态
state.mass = mass;
state.volumeL = volumeL;
state.s = Math.cbrt(L_to_m3(volumeL));
// 平衡求解与读数(无方块 -> 水体独立解)
state.sol = isCubeActive()
? solveEquilibrium(state.mass, state.s)
: solveWaterOnly();
const rhoDisplay = rhoAct ? rhoAct : (state.mass / L_to_m3(state.volumeL));
outVolume.textContent = fmt(state.volumeL, 2);
outMass.textContent = fmt(state.mass, 2);
outRho.textContent = isFinite(rhoDisplay) ? fmt(rhoDisplay, 1) : "—";
setFloatStatusFromSol(state.sol);
renderInfoAndPositions(state.sol, /*snapWhenIdle*/ true);
}
function getLockedRhoForFreeMode(){
const shown = parseFloat(outRho.textContent);
return isFinite(shown) ? shown : 600;
}
function renderInfoAndPositions(sol, snapWhenIdle){
// 文本读数
if (!isCubeActive()){
readout.innerHTML =
`状态: 无方块(质量=0 或体积=0)<br>` +
`总水量: ${fmt(100.00,2)} L | 水位 h=${fmt(sol.h,3)} m`;
}else{
const VsL = m3_to_L(sol.V_sub);
const totalL = 100 + VsL;
const VcubeL = m3_to_L(sol.V_cube);
const frac = (sol.V_sub / sol.V_cube) * 100;
readout.innerHTML =
`状态: ${sol.mode}<br>` +
`总水量: ${fmt(totalL,2)} L | 水位 h=${fmt(sol.h,3)} m<br>` +
`方块: 体积 ${fmt(VcubeL,1)} L, 边长 s=${fmt(Math.cbrt(sol.V_cube),3)} m, 质量 ${fmt(state.mass,2)} kg<br>` +
`密度: ${fmt(state.mass / L_to_m3(state.volumeL),1)} kg/m³(水=1000)<br>` +
`浸没体积: ${fmt(sol.V_sub,4)} m³(${fmt(frac,1)}%)<br>` +
`受力: 浮力 ${fmt(sol.F_b,1)} N | 重力 ${fmt(sol.W_force,1)} N | 支持力 ${fmt(sol.N,1)} N`;
}
// 平衡位置(作为目标)
if (isCubeActive()){
const eq = equilibriumCubeRect(state.sol);
state.cubePx.w = eq.w; state.cubePx.h = eq.h;
if (snapWhenIdle && !state.dragging && !state.animId){
state.cubePx.x = eq.x;
state.cubePx.y = eq.y;
}
state.cubeTargetPx.x = eq.x;
state.cubeTargetPx.y = eq.y;
}
draw();
}
// ================ 拖拽(即时体积置换 + 松手回归平衡) ================
function pointInRect(px,py,r){ return px>=r.x && px<=r.x+r.w && py>=r.y && py<=r.y+r.h; }
function clampCubeInsideTank(rect){
const Lp = view.tank.left, Tp = view.tank.top, Wp = view.tank.width, Hp = view.tank.height;
rect.x = clamp(rect.x, Lp, Lp + Wp - rect.w);
rect.y = clamp(rect.y, Tp, Tp + Hp - rect.h);
}
function getCanvasPoint(e){
const r = canvas.getBoundingClientRect();
return { x: e.clientX - r.left, y: e.clientY - r.top };
}
// 将当前像素y(方块顶边)转换为自底向上的 y_bottom(m)
function currentYBottomMeters(){
const S = view.tank.scale, topPx = view.tank.top;
const yTopPx = state.cubePx.y;
const s = state.s;
const yBottom = H - (yTopPx - topPx)/S - s;
return clamp(yBottom, 0, H - s);
}
canvas.addEventListener('pointerdown', (e) => {
if (!isCubeActive()) return; // 无方块:不可拖拽
const p = getCanvasPoint(e);
if (pointInRect(p.x, p.y, state.cubePx)){
state.dragging = true;
canvas.setPointerCapture(e.pointerId);
state.dragOffset.x = p.x - state.cubePx.x;
state.dragOffset.y = p.y - state.cubePx.y;
if (state.animId){ cancelAnimationFrame(state.animId); state.animId = null; }
}
});
canvas.addEventListener('pointermove', (e) => {
if (!state.dragging || !isCubeActive()) return;
const p = getCanvasPoint(e);
state.cubePx.x = p.x - state.dragOffset.x;
state.cubePx.y = p.y - state.dragOffset.y;
clampCubeInsideTank(state.cubePx);
// 拖拽时:根据当前 y_bottom 计算水位与浸没体积
const yb = currentYBottomMeters();
state.solDrag = solveDuringDrag(yb, state.s, state.mass);
renderInfoAndPositions(state.solDrag, /*snapWhenIdle*/ false);
});
function updateStatusToEquilibrium(){
// 松开或取消:切回平衡态(无方块 -> 水体独立解)
state.sol = isCubeActive()
? solveEquilibrium(state.mass, state.s)
: solveWaterOnly();
state.solDrag = null;
setFloatStatusFromSol(state.sol);
renderInfoAndPositions(state.sol, /*snapWhenIdle*/ false);
}
canvas.addEventListener('pointerup', (e) => {
if (!state.dragging) return;
state.dragging = false;
canvas.releasePointerCapture(e.pointerId);
updateStatusToEquilibrium();
if (isCubeActive()) startReturnAnimation(); // 仅有方块时回弹
});
canvas.addEventListener('pointercancel', () => {
if (state.dragging){
state.dragging = false;
updateStatusToEquilibrium();
if (isCubeActive()) startReturnAnimation();
}
});
function startReturnAnimation(){
const duration = 450;
const start = performance.now();
const y0 = state.cubePx.y;
const xKeep = state.cubePx.x;
const yTarget = state.cubeTargetPx.y;
function step(t){
const k = clamp((t - start)/duration, 0, 1);
const e = 1 - Math.pow(1 - k, 3); // easeOutCubic
state.cubePx.y = y0 + (yTarget - y0) * e;
state.cubePx.x = xKeep;
// 动画过程中显示平衡解
state.solDrag = null;
draw();
if (k < 1) state.animId = requestAnimationFrame(step);
else state.animId = null;
}
if (state.animId) cancelAnimationFrame(state.animId);
state.animId = requestAnimationFrame(step);
}
// ================ 记录输入来源 ================
elVolume.addEventListener('input', () => updateFromControls('volume'));
elMass.addEventListener('input', () => updateFromControls('mass'));
presetSel.addEventListener('change', () => {
const key = presetSel.value;
if (key){
lockRho.checked = true;
lockRho.disabled = true;
}else{
lockRho.disabled = false;
}
ensureMassRange();
updateFromControls('volume');
});
lockRho.addEventListener('change', () => {
if (activeRho()){ lockRho.checked = true; lockRho.disabled = true; }
updateFromControls('volume');
});
btnReset.addEventListener('click', () => {
presetSel.value = "wood"; // 默认木头
lockRho.checked = true; lockRho.disabled = true;
elVolume.value = "5.00";
ensureMassRange();
updateFromControls('volume');
});
window.addEventListener('resize', resizeCanvas);
ensureMassRange();
resizeCanvas();
// 初始化质量与状态(按默认材质密度)
(function init(){
const rho = activeRho();
const V = L_to_m3(parseFloat(elVolume.value));
elMass.value = (rho * V).toFixed(2);
updateFromControls('volume');
})();
</script>
</body>
</html>
以下是浮力模拟器的实现要点解析和关键代码示例:
### 核心实现思路
1. **物理模型**:
- 浮力计算:`F_b = ρ_water * g * V_sub`
- 重力计算:`W = m * g`
- 物体状态判断:
- 漂浮:`ρ_object < ρ_water`
- 悬浮:`ρ_object = ρ_water`
- 沉底:`ρ_object > ρ_water`
2. **关键交互功能**:
// 材质选择事件
presetSel.addEventListener('change', () => {
const material = presetSel.value;
if(material) {
// 自动设置密度并锁定
const density = PRESET_RHO;
lockRho.checked = true;
updateCubeProperties(density);
}
});
// 滑块联动逻辑
function updateCubeProperties(targetDensity) {
const volume = parseFloat(elVolume.value);
const mass = targetDensity * (volume/1000);
elMass.value = mass.toFixed(2);
redrawScene();
}
### 物理计算核心代码
function calculateBuoyancy() {
// 获取输入值
const volume_m3 = parseFloat(elVolume.value) / 1000;
const mass = parseFloat(elMass.value);
// 计算密度
const density = mass / volume_m3;
// 计算浮力状态
if(density < RHO_WATER) {
// 漂浮状态
const submergedRatio = density / RHO_WATER;
const submergedVolume = volume_m3 * submergedRatio;
return {
status: "漂浮",
submergedVolume: submergedVolume,
buoyancyForce: RHO_WATER * G * submergedVolume
};
} else {
// 沉底状态
return {
status: "沉底",
submergedVolume: volume_m3,
buoyancyForce: RHO_WATER * G * volume_m3
};
}
}
### 水位计算实现
function calculateWaterLevel() {
const baseArea = W * D; // 容器底面积
const initialVolume = 0.1; // 100L水
// 获取方块浸没体积
const { submergedVolume } = calculateBuoyancy();
// 计算新水位高度
const newWaterLevel = (initialVolume + submergedVolume) / baseArea;
// 限制水位不超过容器高度
return Math.min(newWaterLevel, H);
}
### 使用说明
1. **调节参数**:
- 通过滑块调整体积(0-10L)和质量(0-200kg)
- 选择材质自动设置密度
- 勾选"锁定密度"保持密度恒定
2. **观察效果**:
- 方块颜色变化表示不同材质
- 实时水位线显示
- 控制面板显示浮力/重力数据
3. **交互操作**:
- 拖拽方块改变位置
- 点击"重置"恢复初始状态
### 效果展示关键代码
function drawWater() {
const waterLevel = calculateWaterLevel();
const canvasHeight = canvas.height;
// 计算水面Y坐标(画布坐标系)
const waterY = canvasHeight - (waterLevel / H) * canvasHeight;
// 绘制水体渐变
const gradient = ctx.createLinearGradient(0, waterY, 0, canvasHeight);
gradient.addColorStop(0, getCss('--water-top'));
gradient.addColorStop(1, getCss('--water-bottom'));
ctx.fillStyle = gradient;
ctx.fillRect(0, waterY, canvas.width, canvas.height - waterY);
}
[查看完整示例](https://codepen.io/your-demo-link)(实际开发时需替换为真实演示链接)
这个模拟器完整实现了:
1. 阿基米德原理可视化
2. 材质密度实时计算
3. 水位动态变化
4. 物体状态(漂浮/沉底)判断
5. 交互式拖拽体验
通过调节参数,可以直观理解:
- 为什么钢铁轮船能漂浮
- 冰山的水下体积比例
- 密度与浮力的关系等物理概念
以上回复来自 FishC AI 小助理,如未能正确解答您的问题,请继续追问。 @不二如是 2# 与 FishC 有啥区别 sfqxx 发表于 2025-8-13 19:46
@不二如是 2# 与 FishC 有啥区别
JS祖师爷,专门服务Web板块
FishC是全局 不二如是 发表于 2025-8-13 19:55
JS祖师爷,专门服务Web板块
FishC是全局
{:10_323:}3Q 赞啊!! 不错不错{:10_256:} 这个匿名鱼油是怎么回事??{:10_257:} sfqxx 发表于 2025-8-13 19:46
@不二如是 2# 与 FishC 有啥区别
你好,他显示我的用户组还没有权限添加好友 匿名鱼油 发表于 2025-8-14 18:47
你好,他显示我的用户组还没有权限添加好友
好的,加油升级吧 sfqxx 发表于 2025-8-14 18:49
好的,加油升级吧
{:5_109:} 匿名鱼油 发表于 2025-8-14 18:50
哦对了你发的html代码该怎么下载在自己这里运行啊,我不太会{:10_282:} sfqxx 发表于 2025-8-14 18:51
哦对了你发的html代码该怎么下载在自己这里运行啊,我不太会
我使用的是vs code,你可以参考这个视频下载插件
https://www.bilibili.com/video/BV1PL411t7Fy/?spm_id_from=333.337.search-card.all.click&vd_source=451f1e8f04d8c94807752b187ccd4c7e
也可以直接将代码保存为一个html文件,然后拖入浏览器打开就可以了
sfqxx 发表于 2025-8-14 18:51
哦对了你发的html代码该怎么下载在自己这里运行啊,我不太会
不过直接保存打开网页会有一些功能缺陷
页:
[1]