/* math6_anim.js — движок анимированных canvas-демонстраций «Математики 6».
 * Неймспейс window.Math6Anim. Используется билдерами глав (math_6_chN.html).
 *
 * Принципы (как в chem7_anim.js — проверено):
 *  - один RAF-цикл на демо, пауза вне вьюпорта (IntersectionObserver), stop() при уходе;
 *  - prefers-reduced-motion → один статичный кадр вместо анимации;
 *  - HEADLESS (jsdom-тесты / HeadlessChrome): getContext НЕ вызывается (ctx=null),
 *    строится только DOM-каркас → тесты не падают;
 *  - тёмная тема и акценты — через CSS-переменные; ⛔ без эмодзи.
 *
 * Каждое демо: fn(host, opts) → { stop(), set(opts) }. Перерисовка при смене параметра — set().
 */
(function (W) {
'use strict';
if (W.Math6Anim && W.Math6Anim.__installed) return;
var D = W.document;
var M = W.Math6Anim = W.Math6Anim || {};
M.__installed = true;

var HEADLESS = (typeof navigator !== 'undefined' && /jsdom|HeadlessChrome/i.test(navigator.userAgent || ''));
function reduced() { try { return !!(W.matchMedia && W.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) { return false; } }
function now() { try { return W.performance && W.performance.now ? W.performance.now() : Date.now(); } catch (e) { return Date.now(); } }
function cssVar(name, fb) { try { var v = getComputedStyle(D.documentElement).getPropertyValue(name).trim(); return v || fb; } catch (e) { return fb; } }
function kf(x) { var s = (Math.round(x * 100) / 100).toString(); return s.replace('.', ','); }

function observeVisible(host, cb) {
  if (typeof IntersectionObserver === 'undefined') { cb(true); return { disconnect: function () {} }; }
  var io = new IntersectionObserver(function (es) { cb(es[0] && es[0].isIntersecting); }, { threshold: 0.01 });
  io.observe(host); return io;
}

/* canvas в host; ctx=null в HEADLESS (getContext не зовём) */
function sceneCanvas(host, w, h) {
  host.innerHTML = '';
  var cv = D.createElement('canvas');
  var dpr = HEADLESS ? 1 : (W.devicePixelRatio || 1);
  cv.width = w * dpr; cv.height = h * dpr;
  cv.style.width = '100%'; cv.style.maxWidth = w + 'px'; cv.style.height = 'auto';
  cv.style.borderRadius = '12px'; cv.style.display = 'block'; cv.style.margin = '0 auto';
  cv.style.background = 'var(--card,#fff)'; cv.style.border = '1px solid var(--border,#e2e8f0)';
  host.appendChild(cv);
  var ctx = null;
  if (!HEADLESS) { try { ctx = cv.getContext('2d'); if (ctx) ctx.scale(dpr, dpr); } catch (e) { ctx = null; } }
  return { cv: cv, ctx: ctx, w: w, h: h };
}

/* RAF-цикл; step(tSec). В HEADLESS/reduced — один кадр step(0). Возвращает {stop}. */
function loop(host, step) {
  if (HEADLESS || reduced()) { try { step(0); } catch (e) {} return { stop: function () {} }; }
  var raf = 0, running = true, visible = true, t0 = now();
  var io = observeVisible(host, function (v) { visible = v; });
  function frame() {
    if (!running) return;
    if (visible) { try { step((now() - t0) / 1000); } catch (e) { running = false; return; } }
    raf = W.requestAnimationFrame(frame);
  }
  raf = W.requestAnimationFrame(frame);
  return { stop: function () { running = false; try { W.cancelAnimationFrame(raf); } catch (e) {} try { io.disconnect(); } catch (e) {} } };
}

function caption(host, html) {
  var c = D.createElement('div');
  c.style.cssText = 'text-align:center;font-weight:700;color:var(--pri2,#3730a3);margin-top:8px;font-size:1.02rem';
  c.innerHTML = html; host.appendChild(c);
  if (W.renderMathInElement) try { W.renderMathInElement(c, { delimiters: [{ left: '$', right: '$', display: false }], throwOnError: false }); } catch (e) {}
  return c;
}

/* ============================ ДЕМО 1: КОЛЕСО КАТИТСЯ (C = πd = 2πr) ============================ */
M.rollingCircle = function (host, opts) {
  opts = opts || {}; var r = opts.r || 3;
  var W0 = 520, H0 = 200;
  var sc = sceneCanvas(host, W0, H0);
  var cap = caption(host, '');
  var PI = 3.14, period = 4.2;
  function draw(tSec) {
    var ctx = sc.ctx; var pri = cssVar('--pri', '#4f46e5'), acc = cssVar('--pri2', '#3730a3'), mut = cssVar('--muted', '#64748b');
    if (!ctx) return;
    var Rpx = 26 + r * 5; if (Rpx > 60) Rpx = 60;
    var baseY = H0 - 46, startX = 34;
    var p = (tSec % period) / period; /* 0..1 — один оборот */
    var travel = p * 2 * Math.PI * Rpx;
    var cx = startX + Rpx + travel, cy = baseY - Rpx, ang = p * 2 * Math.PI;
    ctx.clearRect(0, 0, W0, H0);
    /* линия земли */
    ctx.strokeStyle = mut; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(20, baseY); ctx.lineTo(W0 - 20, baseY); ctx.stroke();
    /* развёрнутый путь (длина окружности) — толстая лента */
    ctx.strokeStyle = acc; ctx.lineWidth = 5; ctx.lineCap = 'round';
    ctx.beginPath(); ctx.moveTo(startX + Rpx, baseY + 10); ctx.lineTo(startX + Rpx + travel, baseY + 10); ctx.stroke();
    /* колесо */
    ctx.strokeStyle = pri; ctx.lineWidth = 3; ctx.fillStyle = 'rgba(79,70,229,0.08)';
    ctx.beginPath(); ctx.arc(cx, cy, Rpx, 0, 2 * Math.PI); ctx.fill(); ctx.stroke();
    /* спица + метка на ободе (видно вращение) */
    var mx = cx + Rpx * Math.sin(ang), my = cy + Rpx * Math.cos(ang);
    ctx.strokeStyle = pri; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(mx, my); ctx.stroke();
    ctx.fillStyle = '#e11d48'; ctx.beginPath(); ctx.arc(mx, my, 5, 0, 2 * Math.PI); ctx.fill();
    /* центр */
    ctx.fillStyle = acc; ctx.beginPath(); ctx.arc(cx, cy, 3, 0, 2 * Math.PI); ctx.fill();
    /* подпись пройденного пути */
    ctx.fillStyle = mut; ctx.font = '12px JetBrains Mono, monospace'; ctx.textAlign = 'center';
    ctx.fillText('путь за ' + (p < 0.999 ? Math.round(p * 100) + '% оборота' : 'полный оборот'), startX + Rpx + travel / 2, baseY + 30);
  }
  var L = loop(host, draw);
  cap.innerHTML = 'За один полный оборот колесо проходит путь, равный длине окружности: $C = 2\\pi r = \\pi d = ' + kf(2 * PI * r) + '$ (при $r=' + r + '$).';
  if (W.renderMathInElement) try { W.renderMathInElement(cap, { delimiters: [{ left: '$', right: '$', display: false }], throwOnError: false }); } catch (e) {}
  return { stop: L.stop, host: host };
};

/* ============================ ДЕМО 2: ЗАМЕТАНИЕ ПЛОЩАДИ КРУГА (S = πr²) ============================ */
M.sweepArea = function (host, opts) {
  opts = opts || {}; var r = opts.r || 3;
  var W0 = 300, H0 = 300; var sc = sceneCanvas(host, W0, H0);
  var cap = caption(host, '');
  var PI = 3.14, period = 3.6;
  function draw(tSec) {
    var ctx = sc.ctx; if (!ctx) return;
    var pri = cssVar('--pri', '#4f46e5'), acc = cssVar('--pri2', '#3730a3'), mut = cssVar('--muted', '#64748b');
    var cx = W0 / 2, cy = H0 / 2, Rpx = 40 + r * 12; if (Rpx > 130) Rpx = 130;
    var p = (tSec % period) / period; var sweep = p * 2 * Math.PI;
    ctx.clearRect(0, 0, W0, H0);
    /* контур круга */
    ctx.strokeStyle = pri; ctx.lineWidth = 2.5; ctx.beginPath(); ctx.arc(cx, cy, Rpx, 0, 2 * Math.PI); ctx.stroke();
    /* заметённый сектор */
    ctx.fillStyle = 'rgba(79,70,229,0.22)'; ctx.beginPath(); ctx.moveTo(cx, cy);
    ctx.arc(cx, cy, Rpx, -Math.PI / 2, -Math.PI / 2 + sweep); ctx.closePath(); ctx.fill();
    /* движущийся радиус */
    var ex = cx + Rpx * Math.cos(-Math.PI / 2 + sweep), ey = cy + Rpx * Math.sin(-Math.PI / 2 + sweep);
    ctx.strokeStyle = '#e11d48'; ctx.lineWidth = 2.5; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(ex, ey); ctx.stroke();
    /* подпись r */
    ctx.fillStyle = acc; ctx.font = '13px JetBrains Mono, monospace'; ctx.textAlign = 'left';
    ctx.fillText('r = ' + r, cx + 6, cy - 6);
    ctx.fillStyle = acc; ctx.beginPath(); ctx.arc(cx, cy, 3, 0, 2 * Math.PI); ctx.fill();
  }
  var L = loop(host, draw);
  cap.innerHTML = 'Радиус заметает круг — его площадь $S = \\pi r^2 = ' + kf(PI * r * r) + '$ (при $r=' + r + '$).';
  if (W.renderMathInElement) try { W.renderMathInElement(cap, { delimiters: [{ left: '$', right: '$', display: false }], throwOnError: false }); } catch (e) {}
  return { stop: L.stop, host: host };
};

/* ============================ ДЕМО 3: ПЛОЩАДНАЯ МОДЕЛЬ УМНОЖЕНИЯ (a · b) ============================ */
M.areaModel = function (host, opts) {
  opts = opts || {}; var a = opts.a || 1.2, b = opts.b || 0.3;
  var W0 = 360, H0 = 300; var sc = sceneCanvas(host, W0, H0);
  var cap = caption(host, '');
  var period = 2.8;
  function draw(tSec) {
    var ctx = sc.ctx; if (!ctx) return;
    var pri = cssVar('--pri', '#4f46e5'), acc = cssVar('--pri2', '#3730a3'), mut = cssVar('--muted', '#64748b'), bd = cssVar('--border', '#e2e8f0');
    var pad = 40, plotW = W0 - pad - 16, plotH = H0 - pad - 16;
    var maxU = Math.max(3, Math.ceil(a), Math.ceil(b)); /* единиц по каждой стороне на сетке (адаптивно) */
    var unit = Math.min(plotW, plotH) / maxU;
    var x0 = pad, y0 = H0 - pad;
    var aw = a * unit, bh = b * unit;
    var p = Math.min(1, (tSec % period) / (period * 0.7)); /* заполнение 0..1, потом пауза */
    ctx.clearRect(0, 0, W0, H0);
    /* сетка 0,1 */
    ctx.strokeStyle = bd; ctx.lineWidth = 0.7;
    for (var gx = 0; gx <= maxU * 10; gx++) { var X = x0 + gx * unit / 10; if (X > x0 + maxU * unit + 0.5) break; ctx.globalAlpha = (gx % 10 === 0) ? 1 : 0.4; ctx.beginPath(); ctx.moveTo(X, y0); ctx.lineTo(X, y0 - maxU * unit); ctx.stroke(); }
    for (var gy = 0; gy <= maxU * 10; gy++) { var Y = y0 - gy * unit / 10; ctx.globalAlpha = (gy % 10 === 0) ? 1 : 0.4; ctx.beginPath(); ctx.moveTo(x0, Y); ctx.lineTo(x0 + maxU * unit, Y); ctx.stroke(); }
    ctx.globalAlpha = 1;
    /* прямоугольник a×b, заполняется слева направо */
    ctx.fillStyle = 'rgba(79,70,229,0.28)';
    ctx.fillRect(x0, y0 - bh, aw * p, bh);
    ctx.strokeStyle = pri; ctx.lineWidth = 2.5; ctx.strokeRect(x0, y0 - bh, aw, bh);
    /* оси-подписи */
    ctx.fillStyle = acc; ctx.font = '13px JetBrains Mono, monospace';
    ctx.textAlign = 'center'; ctx.fillText('a = ' + kf(a), x0 + aw / 2, y0 + 22);
    ctx.save(); ctx.translate(x0 - 14, y0 - bh / 2); ctx.rotate(-Math.PI / 2); ctx.fillText('b = ' + kf(b), 0, 0); ctx.restore();
  }
  var L = loop(host, draw);
  var prod = Math.round(a * b * 1e6) / 1e6;
  cap.innerHTML = 'Площадь прямоугольника = произведению сторон: $' + kf(a) + ' \\cdot ' + kf(b) + ' = ' + kf(prod) + '$.';
  if (W.renderMathInElement) try { W.renderMathInElement(cap, { delimiters: [{ left: '$', right: '$', display: false }], throwOnError: false }); } catch (e) {}
  return { stop: L.stop, host: host };
};

/* ============================ ДЕМО 4: ПРЫЖКИ ПО ЧИСЛОВОЙ ПРЯМОЙ (a + b) ============================ */
M.numberLineWalk = function (host, opts) {
  opts = opts || {}; var a = opts.a != null ? opts.a : 3, b = opts.b != null ? opts.b : -5;
  var sum = a + b, W0 = 540, H0 = 150;
  var sc = sceneCanvas(host, W0, H0); var cap = caption(host, '');
  var min = Math.min(-3, 0, a, sum) - 1, max = Math.max(3, 0, a, sum) + 1, period = 4.2;
  function X(v) { var pad = 30; return pad + (v - min) / (max - min) * (W0 - 2 * pad); }
  function arrow(ctx, x1, x2, y, col) {
    ctx.strokeStyle = col; ctx.fillStyle = col; ctx.lineWidth = 3; ctx.lineCap = 'round';
    ctx.beginPath(); ctx.moveTo(x1, y); ctx.lineTo(x2, y); ctx.stroke();
    var d = x2 >= x1 ? 1 : -1; ctx.beginPath(); ctx.moveTo(x2, y); ctx.lineTo(x2 - d * 9, y - 5); ctx.lineTo(x2 - d * 9, y + 5); ctx.closePath(); ctx.fill();
  }
  function draw(t) {
    var ctx = sc.ctx; if (!ctx) return; var mut = cssVar('--muted', '#64748b'), acc = cssVar('--pri2', '#3730a3');
    var axisY = H0 - 50; ctx.clearRect(0, 0, W0, H0);
    ctx.strokeStyle = mut; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(15, axisY); ctx.lineTo(W0 - 15, axisY); ctx.stroke();
    ctx.font = '11px JetBrains Mono, monospace'; ctx.textAlign = 'center';
    for (var v = Math.ceil(min); v <= Math.floor(max); v++) { var x = X(v); ctx.strokeStyle = mut; ctx.beginPath(); ctx.moveTo(x, axisY - 5); ctx.lineTo(x, axisY + 5); ctx.stroke(); ctx.fillStyle = (v === 0 ? acc : mut); ctx.fillText(v, x, axisY + 20); }
    var p = (t % period) / period, p1 = Math.min(1, p / 0.4), p2 = Math.max(0, Math.min(1, (p - 0.45) / 0.4));
    arrow(ctx, X(0), X(0) + (X(a) - X(0)) * p1, axisY - 14, a >= 0 ? '#059669' : '#e11d48');
    if (p2 > 0) arrow(ctx, X(a), X(a) + (X(sum) - X(a)) * p2, axisY - 30, b >= 0 ? '#059669' : '#e11d48');
    if (p > 0.88) { ctx.fillStyle = '#e11d48'; ctx.beginPath(); ctx.arc(X(sum), axisY, 6, 0, 2 * Math.PI); ctx.fill(); ctx.fillStyle = acc; ctx.font = '14px Unbounded, sans-serif'; ctx.fillText('' + sum, X(sum), axisY - 42); }
  }
  var L = loop(host, draw);
  cap.innerHTML = '$' + a + ' + (' + b + ') = ' + sum + '$ — от нуля шагаем ' + (a >= 0 ? 'вправо' : 'влево') + ' на ' + Math.abs(a) + ', затем ' + (b >= 0 ? 'вправо' : 'влево') + ' на ' + Math.abs(b) + '.';
  if (W.renderMathInElement) try { W.renderMathInElement(cap, { delimiters: [{ left: '$', right: '$', display: false }], throwOnError: false }); } catch (e) {}
  return { stop: L.stop };
};

/* ============================ ДЕМО 5: МАШИНКА + ГРАФИК «ПУТЬ–ВРЕМЯ» ============================ */
M.carGraph = function (host, opts) {
  var W0 = 460, H0 = 330; var sc = sceneCanvas(host, W0, H0); var cap = caption(host, '');
  var J = [{ t: 0, s: 0 }, { t: 1, s: 40 }, { t: 2, s: 80 }, { t: 3, s: 80 }, { t: 4, s: 120 }, { t: 5, s: 160 }];
  var Tmax = 5, Smax = 160, period = 7;
  function sAt(tt) { for (var i = 0; i < J.length - 1; i++) { if (tt >= J[i].t && tt <= J[i + 1].t) { var f = (tt - J[i].t) / ((J[i + 1].t - J[i].t) || 1); return J[i].s + f * (J[i + 1].s - J[i].s); } } return J[J.length - 1].s; }
  function draw(t) {
    var ctx = sc.ctx; if (!ctx) return;
    var pri = cssVar('--pri', '#059669'), mut = cssVar('--muted', '#64748b'), bd = cssVar('--border', '#e2e8f0');
    var cur = (t % period) / period * Tmax;
    ctx.clearRect(0, 0, W0, H0);
    var roadY = 52, rx0 = 24, rx1 = W0 - 24;
    ctx.strokeStyle = bd; ctx.lineWidth = 8; ctx.lineCap = 'round'; ctx.beginPath(); ctx.moveTo(rx0, roadY); ctx.lineTo(rx1, roadY); ctx.stroke();
    var carX = rx0 + (sAt(cur) / Smax) * (rx1 - rx0);
    ctx.fillStyle = pri; ctx.fillRect(carX - 13, roadY - 11, 26, 14);
    ctx.fillStyle = '#1e293b'; ctx.beginPath(); ctx.arc(carX - 7, roadY + 4, 4, 0, 6.3); ctx.arc(carX + 7, roadY + 4, 4, 0, 6.3); ctx.fill();
    ctx.fillStyle = mut; ctx.font = '13px Inter, sans-serif'; ctx.textAlign = 'center';
    ctx.fillText('время: ' + (Math.round(cur * 10) / 10) + ' ч   ·   путь: ' + Math.round(sAt(cur)) + ' км', W0 / 2, 24);
    var gx0 = 46, gy0 = H0 - 28, gw = W0 - 70, gh = H0 - 120;
    function GX(tt) { return gx0 + (tt / Tmax) * gw; } function GY(ss) { return gy0 - (ss / Smax) * gh; }
    ctx.strokeStyle = mut; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(gx0, gy0 - gh); ctx.lineTo(gx0, gy0); ctx.lineTo(gx0 + gw, gy0); ctx.stroke();
    ctx.fillStyle = mut; ctx.font = '11px Inter, sans-serif'; ctx.textAlign = 'center'; ctx.fillText('t, ч', gx0 + gw, gy0 + 16); ctx.textAlign = 'left'; ctx.fillText('s, км', gx0 - 38, gy0 - gh + 4);
    ctx.strokeStyle = bd; ctx.lineWidth = 1.5; ctx.beginPath(); J.forEach(function (pt, i) { var x = GX(pt.t), y = GY(pt.s); if (i) ctx.lineTo(x, y); else ctx.moveTo(x, y); }); ctx.stroke();
    ctx.strokeStyle = pri; ctx.lineWidth = 3; ctx.lineCap = 'round'; ctx.beginPath(); ctx.moveTo(GX(0), GY(0));
    for (var tt = 0; tt <= cur; tt += 0.04) ctx.lineTo(GX(tt), GY(sAt(tt)));
    ctx.stroke();
    ctx.fillStyle = '#e11d48'; ctx.beginPath(); ctx.arc(GX(cur), GY(sAt(cur)), 5, 0, 2 * Math.PI); ctx.fill();
  }
  var L = loop(host, draw);
  cap.innerHTML = 'Машина едет — график «путь–время» вычерчивается сам. Где линия <b>горизонтальна</b> (с 2 до 3 ч) — машина <b>стоит</b>: время идёт, а путь не растёт.';
  return { stop: L.stop };
};

/* ============================ ДЕМО 6: ЖИВОЙ ГРАФИК y=kx / y=k/x ============================ */
M.plotLive = function (host, opts) {
  opts = opts || {};
  var W0 = 360, H0 = 360; var sc = sceneCanvas(host, W0, H0);
  var st = { k: opts.k != null ? opts.k : 2, target: opts.k != null ? opts.k : 2, mode: opts.mode || 'kx' };
  var XMIN = -6, XMAX = 6, YMIN = -6, YMAX = 6, pad = 24;
  function X(x) { return pad + (x - XMIN) / (XMAX - XMIN) * (W0 - 2 * pad); }
  function Y(y) { return H0 - pad - (y - YMIN) / (YMAX - YMIN) * (H0 - 2 * pad); }
  function draw() {
    var ctx = sc.ctx; if (!ctx) return;
    var pri = cssVar('--pri', '#059669'), acc = cssVar('--pri2', '#047857'), bd = cssVar('--border', '#e2e8f0'), axc = cssVar('--text', '#0f172a');
    st.k += (st.target - st.k) * 0.12;
    ctx.clearRect(0, 0, W0, H0);
    ctx.strokeStyle = bd; ctx.lineWidth = 0.8;
    for (var gx = XMIN; gx <= XMAX; gx++) { if (gx === 0) continue; ctx.beginPath(); ctx.moveTo(X(gx), Y(YMIN)); ctx.lineTo(X(gx), Y(YMAX)); ctx.stroke(); }
    for (var gy = YMIN; gy <= YMAX; gy++) { if (gy === 0) continue; ctx.beginPath(); ctx.moveTo(X(XMIN), Y(gy)); ctx.lineTo(X(XMAX), Y(gy)); ctx.stroke(); }
    ctx.strokeStyle = axc; ctx.lineWidth = 1.6; ctx.beginPath(); ctx.moveTo(X(XMIN), Y(0)); ctx.lineTo(X(XMAX), Y(0)); ctx.moveTo(X(0), Y(YMIN)); ctx.lineTo(X(0), Y(YMAX)); ctx.stroke();
    ctx.fillStyle = axc; ctx.font = 'italic 13px serif'; ctx.textAlign = 'left'; ctx.fillText('x', X(XMAX) - 4, Y(0) + 16); ctx.fillText('y', X(0) + 8, Y(YMAX) + 12);
    ctx.strokeStyle = pri; ctx.lineWidth = 3; ctx.lineJoin = 'round'; ctx.lineCap = 'round';
    if (st.mode === 'kx') {
      ctx.beginPath(); var started = false;
      for (var x = XMIN; x <= XMAX; x += 0.06) { var y = st.k * x; if (y < YMIN - 1 || y > YMAX + 1) { started = false; continue; } if (started) ctx.lineTo(X(x), Y(y)); else { ctx.moveTo(X(x), Y(y)); started = true; } }
      ctx.stroke();
    } else {
      [[XMIN, -0.12], [0.12, XMAX]].forEach(function (seg) {
        ctx.beginPath(); var s2 = false;
        for (var x2 = seg[0]; x2 <= seg[1]; x2 += 0.04) { if (Math.abs(x2) < 0.08) continue; var y2 = st.k / x2; if (y2 < YMIN - 1 || y2 > YMAX + 1) { s2 = false; continue; } if (s2) ctx.lineTo(X(x2), Y(y2)); else { ctx.moveTo(X(x2), Y(y2)); s2 = true; } }
        ctx.stroke();
      });
    }
    ctx.fillStyle = acc; ctx.font = 'bold 16px Inter, sans-serif'; ctx.textAlign = 'left';
    var kk = Math.round(st.k * 10) / 10;
    ctx.fillText(st.mode === 'kx' ? ('y = ' + kf(kk) + ' · x') : ('y = ' + kf(kk) + ' / x'), pad + 4, pad + 10);
  }
  var L = loop(host, draw);
  return {
    stop: L.stop,
    setK: function (v) { st.target = v; if (st.mode === 'kdx' && Math.abs(v) < 0.5) st.target = (v < 0 ? -0.5 : 0.5); },
    setMode: function (m) { st.mode = m; if (m === 'kdx' && Math.abs(st.target) < 0.5) st.target = 0.5; }
  };
};

})(window);
