|
马上注册,结交更多好友,享用更多功能^_^
您需要 登录 才可以下载或查看,没有账号?立即注册
x
浮力模拟器
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[type="range"]{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[key] : 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[materialKey] || 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>
复制代码 |
评分
-
查看全部评分
|