Files
Learn_System/frontend/js/labs/diffusion.js
Maxim Dolgolyov 6afe928c0d feat(labs): visual polish wave — LabFX foundation + 33 sims juiced up
ФУНДАМЕНТ (4 новых файла):
- _fx_core.js: LabFX namespace, glow.drawGlow, glow.pulse, haptic, shake
- _fx_particles.js: пул 1500 объектов, 6 shapes (dot/spark/ring/smoke/splash/dust)
- _fx_motion.js: tween + 12 easings + critically-damped spring
- _fx_sound.js: 9 procedural synth-звуков (click/tick/whoosh/chime/fizz/spark/bounce/pour/drone), Web Audio API
- Sound toggle в шапке lab.html с localStorage-persist

UX МИКРО (CSS + JS):
- Button states: hover scale+brightness, active scale-down, disabled grayscale
- Slider polish: custom thumb с тенью, filled-track gradient, hover/active
- Focus rings через :focus-visible
- Tooltip system .tt-host data-tt= с 400ms hover, fade-in
- Marching ants для selection
- Loading skeleton с shimmer
- Empty state .sim-empty-* паттерн
- Toast: progress bar внизу, icons по типу
- Cursor states utility classes
- View Transitions API для smooth sim-switch, fallback на CSS fade

PHASE 2 — визуальные эффекты для 33 симуляций:

Physics motion: projectile (launch whoosh + landing splash/shake/haptic + target chime), pendulum (max-extension tick + bob glow), collision (bounce + sparks + shake), angrybirds (whoosh/bounce/fizz/chime + confetti), newton (rocket flame trail + scene transitions), forcesandbox (spring glow + impact sparks)

Physics fields: emfield (field-lines glow + particle trail + lightning при high field + Gauss-drag haptic + rod motion sparks), circuit (energized-wire glow ∝ current + LED bloom + short-circuit shake/spark + heat shimmer smoke + place/erase/switch sounds), opticsbench (beam glow на всех режимах + caustics dust у фокуса + TIR/Brewster one-shot sounds)

Thermo+waves: waves (mode-switch whoosh + Mach-cone particles + spectrum harmonic chime + waveform glow), hydrostatics (pour sound + splash при погружении + valve click), isoprocess (PV-trail dust), heatengine (drone цикла + hot/cold reservoir smoke/dust + phase-change ticks), radioactive (Geiger tick throttle + decay sparks + half-life chime + α/β/γ glow)

Chemistry: chemsandbox (pour splash + fizz bubbles/dust/spark по типу реакции + shake при горении), equilibrium/electrolysis/reactions/titration/flask/ionexchange/redox (pour/fizz/spark/chime по событиям, частицы при ключевых моментах), stoichiometry (fizz bubbles + recipe-change click)

Math+geom: geometry (tool-click + object-create tick + locus glow + challenge confetti via LabFX + haptic), triangle (vertex-drop tick + special-point glow), stereo (figure-change whoosh + cross-section chime, no particles — Three.js), trigcircle (drag-haptic + pitch∝angle tick + sin/cos glow), graph/graphtransform/quadratic (slider-tick throttled + curve glow + discriminant-cross chime), probability (bounce + finish-chime burst), normaldist (pulsing shade + bell glow)

Bio+misc: celldivision (phase-change whoosh + prophase dust + anaphase poles spark + cytokinesis chime), photosynthesis (photon tick + ATP chime + glucose sparkle), bohratom (electron-jump chime ∝ levels + photon spark), orbitals/crystal (mode-change whoosh — Three.js, sound only), logic (gate-place + wire-connect + LED bloom + HIGH wire flowing dots + preset chime), gas/brownian/diffusion/states (preset whoosh, throttled tick по событиям)

Все LabFX вызовы обёрнуты в if (window.LabFX) guard — graceful degradation если фундамент не загружен. 47 JS файлов прошли syntax check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 13:58:49 +03:00

485 lines
18 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* DiffusionSim v2 — Diffusion simulation (two gases mixing).
* v2: entropy timeline on history chart, pore mode (gap in partition), density heatmap.
*/
class DiffusionSim {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.W = 0; this.H = 0;
this.particles = [];
this.N = 60;
this.T = 1.0;
this.partitionOn = true;
this._history = []; // {step, fracA_left, entropy}
this._steps = 0;
this._raf = null;
this.onUpdate = null;
this._dpr = 1;
// v2
this._poreMode = false; // partition has a gap in the center
this._poreH = 40; // gap height in pixels
this._heatmap = null; // cached density heatmap
this._hmTick = 0;
// LabFX
this._fxLastT = 0;
this._fxEquilDone = false; // equilibrium chime played once
}
// ── public API ──────────────────────────────────────────────────────────────
fit() {
const dpr = window.devicePixelRatio || 1;
this._dpr = dpr;
const w = this.canvas.offsetWidth, h = this.canvas.offsetHeight;
this.canvas.width = w * dpr; this.canvas.height = h * dpr;
this.W = w; this.H = h;
this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
this.reset();
}
reset() {
const { W, H } = this;
this.partitionOn = true;
this._poreMode = false;
this._steps = 0;
this._history = [{ step: 0, fracA_left: 1.0, entropy: 0 }];
this._heatmap = null;
const particles = [];
const r = 5;
let attA = 0;
while (particles.filter(p => p.type === 'A').length < this.N && attA < this.N * 30) {
attA++;
const x = r + Math.random() * (W / 2 - 2 * r);
const y = r + Math.random() * (H - 2 * r);
const a = Math.random() * Math.PI * 2, s = this.T * 3.5;
particles.push({ x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s, r, type: 'A' });
}
let attB = 0;
while (particles.filter(p => p.type === 'B').length < this.N && attB < this.N * 30) {
attB++;
const x = W / 2 + r + Math.random() * (W / 2 - 2 * r);
const y = r + Math.random() * (H - 2 * r);
const a = Math.random() * Math.PI * 2, s = this.T * 3.5;
particles.push({ x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s, r, type: 'B' });
}
this.particles = particles;
}
togglePartition() {
if (this._poreMode) {
// If pore is on, full toggle removes pore first
this._poreMode = false;
this.partitionOn = true;
} else {
this.partitionOn = !this.partitionOn;
}
if (window.LabFX) LabFX.sound.play('whoosh', { pitch: 0.7, volume: 0.3 });
this._fxEquilDone = false; // allow equilibrium chime again after partition change
}
togglePore() {
if (!this.partitionOn && !this._poreMode) {
// Partition is fully off — re-enable with pore
this.partitionOn = true;
this._poreMode = true;
} else if (this.partitionOn && !this._poreMode) {
this._poreMode = true; // add pore to full partition
} else if (this._poreMode) {
this._poreMode = false; // remove pore, keep partition
}
}
setN(n) { this.N = Math.max(10, Math.min(200, n)); this.reset(); }
setT(t) {
const f = Math.sqrt(t / this.T);
for (const p of this.particles) { p.vx *= f; p.vy *= f; }
this.T = t;
}
start() { if (!this._raf) this._raf = requestAnimationFrame(this._loop.bind(this)); }
stop() { if (this._raf) { cancelAnimationFrame(this._raf); this._raf = null; } }
// ── simulation ──────────────────────────────────────────────────────────────
_loop(now) {
const dt = this._fxLastT ? Math.min(now - this._fxLastT, 80) : 16;
this._fxLastT = now;
this._step(); this._step();
if (window.LabFX) LabFX.particles.update(dt);
this.draw();
this._raf = requestAnimationFrame(this._loop.bind(this));
}
_step() {
const { W, H, particles } = this;
for (const p of particles) {
p.x += p.vx; p.y += p.vy;
if (p.x < p.r) { p.x = p.r; p.vx = Math.abs(p.vx); }
if (p.x > W - p.r) { p.x = W - p.r; p.vx = -Math.abs(p.vx); }
if (p.y < p.r) { p.y = p.r; p.vy = Math.abs(p.vy); }
if (p.y > H - p.r) { p.y = H - p.r; p.vy = -Math.abs(p.vy); }
// Partition logic
if (this.partitionOn) {
const mid = W / 2, hw = 3;
const inPore = this._poreMode
&& p.y > H / 2 - this._poreH / 2
&& p.y < H / 2 + this._poreH / 2;
if (!inPore) {
if (p.vx > 0 && p.x + p.r > mid - hw && p.x < mid) {
p.x = mid - hw - p.r; p.vx = -Math.abs(p.vx);
} else if (p.vx < 0 && p.x - p.r < mid + hw && p.x > mid) {
p.x = mid + hw + p.r; p.vx = Math.abs(p.vx);
}
}
}
}
// Spatial grid collisions
const cs = 14, cols = Math.ceil(W / cs) + 1;
const grid = new Map();
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
const k = Math.floor(p.x / cs) + Math.floor(p.y / cs) * cols;
if (!grid.has(k)) grid.set(k, []);
grid.get(k).push(i);
}
for (let i = 0; i < particles.length; i++) {
const p1 = particles[i];
const cx = Math.floor(p1.x / cs), cy = Math.floor(p1.y / cs);
for (let dcx = -1; dcx <= 1; dcx++) for (let dcy = -1; dcy <= 1; dcy++) {
const cell = grid.get((cx + dcx) + (cy + dcy) * cols);
if (!cell) continue;
for (const j of cell) {
if (j <= i) continue;
const p2 = particles[j];
const dx = p2.x - p1.x, dy = p2.y - p1.y;
const d = Math.hypot(dx, dy), md = p1.r + p2.r;
if (d < md && d > 0.001) {
const nx = dx / d, ny = dy / d;
const dvn = (p1.vx - p2.vx) * nx + (p1.vy - p2.vy) * ny;
if (dvn < 0) continue;
p1.vx -= dvn * nx; p1.vy -= dvn * ny;
p2.vx += dvn * nx; p2.vy += dvn * ny;
const ov = (md - d) / 2;
p1.x -= nx * ov; p1.y -= ny * ov;
p2.x += nx * ov; p2.y += ny * ov;
}
}
}
}
// History (with entropy)
if (this._steps % 60 === 0) {
const left = particles.filter(p => p.x < W / 2);
const fracA_left = left.length > 0
? left.filter(p => p.type === 'A').length / left.length
: 0;
const f = fracA_left;
const entropy = -(f * Math.log(f + 1e-9) + (1 - f) * Math.log(1 - f + 1e-9));
this._history.push({ step: this._steps, fracA_left, entropy });
if (this._history.length > 200) this._history.shift();
}
// Heatmap update (every 30 steps)
if (this._steps % 30 === 0) this._updateHeatmap();
this._steps++;
if (this._steps % 30 === 0 && this.onUpdate) this.onUpdate(this.info());
// Equilibrium detection: mixed >= 45% and partition is off
if (!this.partitionOn && !this._fxEquilDone && this._steps % 60 === 0 && window.LabFX) {
const info = this.info();
if (+info.mixed >= 45) {
this._fxEquilDone = true;
LabFX.sound.play('chime', { pitch: 1.0, volume: 0.3 });
}
}
}
_updateHeatmap() {
const { W, H, particles } = this;
const cols = 20, rows = 14;
const cw = W / cols, ch = H / rows;
const grid = [];
for (let r = 0; r < rows; r++) {
grid[r] = [];
for (let c = 0; c < cols; c++) grid[r][c] = { A: 0, B: 0 };
}
for (const p of particles) {
const c = Math.min(cols - 1, Math.floor(p.x / cw));
const r = Math.min(rows - 1, Math.floor(p.y / ch));
grid[r][c][p.type]++;
}
const maxCount = Math.max(...grid.flat().map(c => c.A + c.B), 1);
this._heatmap = { grid, cols, rows, cw, ch, maxCount };
}
info() {
const { particles, W, N } = this;
const leftA = particles.filter(p => p.x < W / 2 && p.type === 'A').length;
const leftB = particles.filter(p => p.x < W / 2 && p.type === 'B').length;
const rightA = particles.filter(p => p.x >= W / 2 && p.type === 'A').length;
const rightB = particles.filter(p => p.x >= W / 2 && p.type === 'B').length;
const mixed = (leftB + rightA) / (2 * N);
const fracAL = leftA / ((leftA + leftB) || 1);
const entropy = -(fracAL * Math.log(fracAL + 1e-9) + (1 - fracAL) * Math.log(1 - fracAL + 1e-9));
return {
leftA, leftB, rightA, rightB,
mixed: (mixed * 100).toFixed(0),
entropy: entropy.toFixed(3),
partitionOn: this.partitionOn,
poreMode: this._poreMode,
steps: this._steps,
};
}
// ── drawing ─────────────────────────────────────────────────────────────────
draw() {
const { ctx, W, H } = this;
const TAU = Math.PI * 2;
ctx.fillStyle = '#080818'; ctx.fillRect(0, 0, W, H);
// Background tints
ctx.fillStyle = 'rgba(6,214,224,0.04)'; ctx.fillRect(0, 0, W / 2, H);
ctx.fillStyle = 'rgba(241,91,181,0.04)'; ctx.fillRect(W / 2, 0, W / 2, H);
// Density heatmap (subtle)
this._drawHeatmap(ctx);
// Partition
if (this.partitionOn) this._drawPartition(ctx, W, H);
// Particles
ctx.save();
for (const p of this.particles) {
const color = p.type === 'A' ? '#06D6E0' : '#F15BB5';
ctx.shadowColor = color; ctx.shadowBlur = 6;
ctx.fillStyle = color;
ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, TAU); ctx.fill();
}
ctx.restore();
// Concentration bar (right side)
this._drawConcBar(ctx, W, H);
// History chart with entropy (bottom)
this._drawHistoryChart(ctx, W, H);
// Stats overlay (top-left)
this._drawStats(ctx);
if (window.LabFX) LabFX.particles.draw(ctx);
}
_drawHeatmap(ctx) {
const hm = this._heatmap;
if (!hm) return;
for (let r = 0; r < hm.rows; r++) for (let c = 0; c < hm.cols; c++) {
const cell = hm.grid[r][c];
const total = cell.A + cell.B;
if (total === 0) continue;
const frac = total / hm.maxCount;
// Color based on A vs B ratio
const fracA = cell.A / total;
// Mix cyan and pink by composition
const rr = Math.round(6 + (241 - 6) * (1 - fracA));
const rg = Math.round(214 + (91 - 214) * (1 - fracA));
const rb = Math.round(224 + (181 - 224) * (1 - fracA));
ctx.fillStyle = `rgba(${rr},${rg},${rb},${frac * 0.08})`;
ctx.fillRect(c * hm.cw, r * hm.ch, hm.cw, hm.ch);
}
}
_drawPartition(ctx, W, H) {
const mid = W / 2, pw = 6;
const poreOn = this._poreMode;
const poreY1 = H / 2 - this._poreH / 2;
const poreY2 = H / 2 + this._poreH / 2;
ctx.save();
ctx.shadowBlur = 10; ctx.shadowColor = 'rgba(255,255,255,0.5)';
const grad = ctx.createLinearGradient(mid - pw / 2, 0, mid + pw / 2, 0);
grad.addColorStop(0, 'rgba(255,255,255,0.15)');
grad.addColorStop(1, 'rgba(255,255,255,0.05)');
ctx.fillStyle = grad;
if (!poreOn) {
ctx.fillRect(mid - pw / 2, 0, pw, H);
} else {
// Two segments (above and below pore)
ctx.fillRect(mid - pw / 2, 0, pw, poreY1);
ctx.fillRect(mid - pw / 2, poreY2, pw, H - poreY2);
// Pore opening highlight
ctx.shadowBlur = 0;
ctx.strokeStyle = 'rgba(255,255,255,0.35)'; ctx.lineWidth = 1;
ctx.setLineDash([3, 3]);
ctx.beginPath(); ctx.moveTo(mid - pw / 2, poreY1); ctx.lineTo(mid + pw / 2, poreY1); ctx.stroke();
ctx.beginPath(); ctx.moveTo(mid - pw / 2, poreY2); ctx.lineTo(mid + pw / 2, poreY2); ctx.stroke();
ctx.setLineDash([]);
// Pore gap arrows (showing flow direction)
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.font = "bold 10px monospace"; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('⇌', mid, H / 2);
}
if (!poreOn) {
// Door handle
const hx = mid - 10, hy = H / 2 - 14, hw = 20, hh = 28;
ctx.shadowBlur = 0;
ctx.fillStyle = 'rgba(255,255,255,0.12)';
ctx.strokeStyle = 'rgba(255,255,255,0.3)'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.roundRect(hx, hy, hw, hh, 4); ctx.fill(); ctx.stroke();
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.font = "bold 10px monospace"; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('||', mid, H / 2);
}
ctx.restore();
}
_drawConcBar(ctx, W, H) {
const barX = W - 20, barHalf = H / 2;
const { particles } = this;
const lA = particles.filter(p => p.x < W / 2 && p.type === 'A').length;
const lT = particles.filter(p => p.x < W / 2).length || 1;
const rA = particles.filter(p => p.x >= W / 2 && p.type === 'A').length;
const rT = particles.filter(p => p.x >= W / 2).length || 1;
const fAL = lA / lT, fAR = rA / rT;
ctx.fillStyle = '#06D6E0'; ctx.fillRect(barX, 0, 20, barHalf * fAL);
ctx.fillStyle = '#F15BB5'; ctx.fillRect(barX, barHalf * fAL, 20, barHalf * (1 - fAL));
ctx.fillStyle = '#06D6E0'; ctx.fillRect(barX, barHalf, 20, barHalf * fAR);
ctx.fillStyle = '#F15BB5'; ctx.fillRect(barX, barHalf + barHalf * fAR, 20, barHalf * (1 - fAR));
ctx.fillStyle = 'rgba(255,255,255,0.2)'; ctx.fillRect(barX, barHalf - 1, 20, 2);
ctx.strokeStyle = 'rgba(255,255,255,0.15)'; ctx.lineWidth = 1;
ctx.strokeRect(barX, 0, 20, H);
ctx.save();
ctx.fillStyle = 'rgba(255,255,255,0.35)'; ctx.font = "9px 'Manrope', sans-serif";
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.translate(barX + 10, H / 2); ctx.rotate(-Math.PI / 2);
ctx.fillText('Концентрация', 0, 0);
ctx.restore();
}
_drawHistoryChart(ctx, W, H) {
const graphH = 100, graphY = H - graphH, graphW = W - 24;
ctx.save();
ctx.fillStyle = 'rgba(0,0,10,0.76)';
ctx.beginPath(); ctx.roundRect(0, graphY, graphW, graphH, 8); ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.06)'; ctx.lineWidth = 1; ctx.stroke();
ctx.fillStyle = 'rgba(255,255,255,0.5)'; ctx.font = "10px 'Manrope', sans-serif";
ctx.textAlign = 'left'; ctx.textBaseline = 'top';
ctx.fillText('Доля A в левой половине', 10, graphY + 6);
// Y-axis labels
ctx.fillStyle = 'rgba(255,255,255,0.3)'; ctx.font = "9px 'Manrope', sans-serif";
ctx.fillText('1.0', 4, graphY + 18);
ctx.fillText('0.0', 4, graphY + graphH - 10);
const refY = graphY + graphH * 0.5 - 2;
ctx.setLineDash([4, 4]); ctx.strokeStyle = 'rgba(255,255,255,0.2)'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(24, refY); ctx.lineTo(graphW - 10, refY); ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = 'rgba(255,255,255,0.25)'; ctx.font = "9px 'Manrope', sans-serif";
ctx.textAlign = 'left'; ctx.fillText('равновесие', 28, refY - 10);
const hist = this._history;
if (hist.length > 1) {
const plotX0 = 28, plotW = graphW - 38;
const plotY0 = graphY + 18, plotH2 = graphH - 28;
// Concentration line (cyan)
ctx.strokeStyle = '#06D6E0'; ctx.lineWidth = 1.5;
ctx.beginPath();
for (let i = 0; i < hist.length; i++) {
const hx = plotX0 + (i / (hist.length - 1)) * plotW;
const hy = plotY0 + plotH2 * (1 - hist[i].fracA_left);
if (i === 0) ctx.moveTo(hx, hy); else ctx.lineTo(hx, hy);
}
ctx.stroke();
// Entropy line (orange, dashed, scaled to 0..ln(2) ≈ 0.693)
const maxEnt = Math.log(2);
ctx.strokeStyle = '#FFB347'; ctx.lineWidth = 1.2;
ctx.setLineDash([4, 3]);
ctx.beginPath();
for (let i = 0; i < hist.length; i++) {
const hx = plotX0 + (i / (hist.length - 1)) * plotW;
const hy = plotY0 + plotH2 * (1 - hist[i].entropy / maxEnt);
if (i === 0) ctx.moveTo(hx, hy); else ctx.lineTo(hx, hy);
}
ctx.stroke();
ctx.setLineDash([]);
// Legend
ctx.fillStyle = '#06D6E0'; ctx.beginPath(); ctx.arc(plotX0 + plotW - 50, graphY + 8, 3, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.5)'; ctx.font = "8px sans-serif"; ctx.textBaseline = 'middle';
ctx.textAlign = 'left'; ctx.fillText('X(A)', plotX0 + plotW - 44, graphY + 8);
ctx.fillStyle = '#FFB347'; ctx.beginPath(); ctx.arc(plotX0 + plotW - 22, graphY + 8, 3, 0, Math.PI * 2); ctx.fill();
ctx.fillText('S', plotX0 + plotW - 16, graphY + 8);
// Current value
const last = hist[hist.length - 1];
const endX = plotX0 + plotW;
const endY = plotY0 + plotH2 * (1 - last.fracA_left);
ctx.fillStyle = '#06D6E0'; ctx.font = "bold 10px 'Manrope', sans-serif";
ctx.textAlign = 'right'; ctx.textBaseline = 'middle';
ctx.fillText((last.fracA_left * 100).toFixed(0) + '%', endX - 2, endY);
}
ctx.restore();
}
_drawStats(ctx) {
const info = this.info();
const pad = 10, panelW = 180, panelH = 90, px = 14, py = 14;
ctx.save();
ctx.fillStyle = 'rgba(0,0,10,0.72)';
ctx.beginPath(); ctx.roundRect(px, py, panelW, panelH, 8); ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.07)'; ctx.lineWidth = 1; ctx.stroke();
const lineH = 18;
ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.font = "11px 'Manrope', sans-serif";
ctx.fillStyle = '#06D6E0';
ctx.fillText(`Лево: A=${info.leftA} B=${info.leftB}`, px + pad, py + pad);
ctx.fillStyle = '#F15BB5';
ctx.fillText(`Право: A=${info.rightA} B=${info.rightB}`, px + pad, py + pad + lineH);
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.fillText(`Смешивание: ${info.mixed}%`, px + pad, py + pad + lineH * 2);
const stateLabel = !info.partitionOn ? 'Снята' : info.poreMode ? 'С порой' : 'Вкл';
const stateColor = !info.partitionOn ? '#F15BB5' : info.poreMode ? '#FFB347' : '#06D6E0';
ctx.fillStyle = stateColor;
ctx.fillText(`Раздел: ${stateLabel}`, px + pad, py + pad + lineH * 3);
ctx.restore();
}
}
if (typeof module !== 'undefined') module.exports = DiffusionSim;