diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 7d7eb8d..0af1ba4 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -101,6 +101,13 @@ the tags only); hover 22 → 15 ms per move (tooltip placed once per frame on it readout ten times a second); rotate 226 → 154 ms, edit 409 → 305 ms until painted (WASM built with opt-level 3: engine render 11.7 → 9.4 ms at full speed). +Sharp zooming: the moved layer is a picture at the scale last drawn, so a long touchpad pinch showed +an ever more stretched, blurry view (3.6x at the end of a 1.2 s pinch) and every zoom ended with a +blur-then-snap 180 ms after its last step. Now the view is drawn anew during the gesture once the +picture is 30% off in scale (at most every 120 ms), and at its true scale 90 ms after the last step: +sharp about 130 instead of 240 ms after a pinch, at a cost of 11.4 → 14.6 ms per wheel step at 6x +slowdown (still no frame over 50 ms). + Production lines: Producer (a press making cartons, crates, drums, sacks or parts at a set rate per minute, its ram going down once per item) and Conveyor (drawn the way it runs, belt width, height, rise, speed; corners join without overlap). A conveyor starting at a producer's output, or diff --git a/tools/check_web.py b/tools/check_web.py index fc4b236..18f989c 100644 --- a/tools/check_web.py +++ b/tools/check_web.py @@ -359,6 +359,28 @@ try: check(herd == "1 bull, 1 cow, 1 calf" and abs(rot - 45) < 1e-6, f"elephants: {herd}; the lane bends at {rot} degrees") shot(pg, "web-country.jpg") + # a long pinch (Ctrl + small wheel steps) stays sharp: the view is drawn anew during the + # gesture whenever the stretched picture is 30% off, and at its true scale soon after it ends + pg.evaluate("""() => { const svg = document.querySelector('.iso-stage .iso-svg'); window.__vbs = []; + new MutationObserver(() => __vbs.push(performance.now())).observe(svg, {attributes: true, attributeFilter: ['viewBox']}); }""") + sb = pg.locator(".iso-stage").bounding_box() + pg.mouse.move(sb["x"] + sb["width"] * 0.6, sb["y"] + sb["height"] * 0.5) + pg.keyboard.down("Control") + t0 = pg.evaluate("performance.now()") + for i in range(40): + pg.mouse.wheel(0, -8) + pg.wait_for_timeout(12) + t1 = pg.evaluate("performance.now()") + pg.keyboard.up("Control") + pg.wait_for_timeout(400) + draws = pg.evaluate("__vbs") + during = [t for t in draws if t0 <= t <= t1] + after = [t - t1 for t in draws if t > t1] + tf = pg.evaluate("document.querySelector('.iso-stage .iso-svg').style.transform") + check(len(during) >= 2 and after and after[0] < 150 and tf == "", + f"a long pinch is drawn anew {len(during)} times on the way and sharp {round(after[0]) if after else None} ms after it ends") + pg.evaluate("__studio.view.fit()") + # ---------------------------------------------------------------- a system asking for less motion # (Windows: Animation effects off): the view starts still, but the button stays and turns it on rctx = b.new_context(viewport={"width": 1200, "height": 800}, reduced_motion="reduce") diff --git a/viewer/isokit-view.js b/viewer/isokit-view.js index 286aa8a..69b3623 100644 --- a/viewer/isokit-view.js +++ b/viewer/isokit-view.js @@ -250,30 +250,42 @@ } // Pan and zoom gestures move the drawing as one compositor layer (a CSS transform from the // viewBox last drawn, vbC, to the view asked for, vb): no repaint and no style work per step. - // apply() commits vb as the viewBox (one repaint) when the gesture ends, and for every other change. - var vbC = null, live = false, commitT = 0, lastK = null; + // That layer is a picture at the scale last drawn, so zooming in stretches it (blurry) and + // zooming out shrinks it (blank edges): once it is 30% off, draw() paints the view anew in the + // middle of the gesture, at most every 120 ms. apply() ends the gesture 90 ms after its last + // step and commits every other change. A draw costs one repaint (~10 ms for 2000 nodes). + var vbC = null, live = false, commitT = 0, lastK = null, drawnAt = 0; + var now = window.performance ? function () { return performance.now(); } : Date.now; function applyLive() { if (!vb) return; if (!vbC) { apply(); return; } if (!live) wrap.classList.add("iso-gesture"); live = true; + clearTimeout(commitT); commitT = setTimeout(apply, 90); + var s = vbC.w / vb.w; + if ((s > 1.3 || s < 0.7) && now() - drawnAt > 120) { draw(); return; } var s1 = size()[0] / vb.w, fitNow = vb.w >= fitW * 0.995; - // zoomed in, touch drags pan the view instead of the page (as apply() does) + // zoomed in, touch drags pan the view instead of the page (as draw() does) if (fitNow !== atFit) { atFit = fitNow; svg.style.touchAction = atFit ? "pan-y" : "none"; } - svg.style.transform = "translate(" + ((vbC.x - vb.x) * s1).toFixed(2) + "px," + ((vbC.y - vb.y) * s1).toFixed(2) + "px) scale(" + (vbC.w / vb.w).toFixed(5) + ")"; - clearTimeout(commitT); commitT = setTimeout(apply, 180); + svg.style.transform = "translate(" + ((vbC.x - vb.x) * s1).toFixed(2) + "px," + ((vbC.y - vb.y) * s1).toFixed(2) + "px) scale(" + s.toFixed(5) + ")"; } - function apply() { - if (!vb) return; - clearTimeout(commitT); commitT = 0; - if (live) { live = false; svg.style.transform = ""; wrap.classList.remove("iso-gesture"); } + // paints vb at its true scale: the viewBox, tag size and touch panning follow it + function draw() { vbC = { x: vb.x, y: vb.y, w: vb.w }; + drawnAt = now(); + svg.style.transform = ""; svg.setAttribute("viewBox", r1(vb.x) + " " + r1(vb.y) + " " + r1(vb.w) + " " + r1(vb.h)); var sz = size(), k = (vb.w / sz[0]).toFixed(4); // tags keep their screen size by --iso-k; set on their group alone, so the rest is not restyled if (k !== lastK) { lastK = k; var tg = svg.querySelector(".iso-tags"); (tg || svg).style.setProperty("--iso-k", k); } atFit = vb.w >= fitW * 0.995; svg.style.touchAction = atFit ? "pan-y" : "none"; + } + function apply() { + if (!vb) return; + clearTimeout(commitT); commitT = 0; + if (live) { live = false; wrap.classList.remove("iso-gesture"); } + draw(); wrap.classList.toggle("iso-zoomed", !atFit); schedule(); }