diff --git a/README.md b/README.md index 27f46ac..a5329ad 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,8 @@ default ("light"): running CSS animations repaint the whole drawing every frame, which keeps a slow laptop busy. The ▷ button in the view toolbar switches between light, smooth and off, and the browser remembers the choice. In `isokit.toml`, `[build] animation = "smooth"` sets the default for built pages. +When the system asks for less motion (Windows: Settings → Accessibility → +Visual effects → Animation effects off), views start still; ▷ turns them on. ## Commands diff --git a/tools/check_web.py b/tools/check_web.py index a626995..fc39f7d 100644 --- a/tools/check_web.py +++ b/tools/check_web.py @@ -326,6 +326,26 @@ try: check("(from robot-b)" in loads and flips == 32, f"robot arms pass the goods on ({flips} poses): " + loads) shot(pg, "web-factory.jpg") + # ---------------------------------------------------------------- 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") + rp = rctx.new_page() + rp.goto(BASE) + ready(rp) + rp.wait_for_timeout(500) + ride = "() => Array.from(document.querySelectorAll('.iso-svg .iso-a')).map(e => getComputedStyle(e).transform + getComputedStyle(e).opacity).join('|')" + moves = lambda: (lambda a: (rp.wait_for_timeout(700), a != rp.evaluate(ride))[1])(rp.evaluate(ride)) + check(rp.evaluate("() => __studio.view.animation()") == "off" and rp.locator('.iso-bar [data-a="anim"]').is_visible() and not moves(), + "reduced motion: the view starts still, with the animation button there") + rp.locator('.iso-bar [data-a="anim"]').click() + rp.wait_for_timeout(500) + check(rp.evaluate("() => __studio.view.animation()") == "light" and moves(), "one click turns animation on despite reduced motion") + rp.reload() + ready(rp) + rp.wait_for_timeout(500) + check(rp.evaluate("() => __studio.view.animation()") == "light" and moves(), "and the choice is remembered") + rctx.close() + # ---------------------------------------------------------------- folder project (OPFS stands in for a picked folder) demo = {f.relative_to(ROOT / "examples" / "demo").as_posix(): f.read_text(encoding="utf-8") for f in (ROOT / "examples" / "demo").rglob("*") if f.is_file() and "dist" not in f.parts} diff --git a/viewer/isokit-view.css b/viewer/isokit-view.css index 30883e2..2b72066 100644 --- a/viewer/isokit-view.css +++ b/viewer/isokit-view.css @@ -160,10 +160,11 @@ html[data-theme=light] .iso-svg .iso-glint { display: inline; } 92% { transform: translate(var(--dx), var(--dy)); opacity: 1; } 96%, 100% { transform: translate(var(--dx), var(--dy)); opacity: 0; } } +/* a system asking for less motion: still pictures, until the viewer turns animation on (iso-anim-on) */ @media (prefers-reduced-motion: reduce) { - .iso-svg .iso-a, .iso-svg .iso-ledr, .iso-svg .iso-ledo { animation: none !important; } - .iso-svg .iso-puff, .iso-svg .iso-puff2, .iso-svg .iso-smoke, .iso-svg .iso-disc, .iso-svg .iso-glint, .iso-svg .iso-rip { display: none; } - .iso-svg .iso-park { display: inline; } + .iso-wrap:not(.iso-anim-on) .iso-svg .iso-a, .iso-wrap:not(.iso-anim-on) .iso-svg .iso-ledr, .iso-wrap:not(.iso-anim-on) .iso-svg .iso-ledo { animation: none !important; } + .iso-wrap:not(.iso-anim-on) .iso-svg :is(.iso-puff, .iso-puff2, .iso-smoke, .iso-disc, .iso-glint, .iso-rip) { display: none; } + .iso-wrap:not(.iso-anim-on) .iso-svg .iso-park { display: inline; } } @media (max-width: 640px) { .iso-stage { aspect-ratio: 1 / 1; min-height: 320px; } diff --git a/viewer/isokit-view.js b/viewer/isokit-view.js index ac85ab7..286aa8a 100644 --- a/viewer/isokit-view.js +++ b/viewer/isokit-view.js @@ -138,24 +138,32 @@ // CSS animations inside the drawing repaint all of it every frame: a fifth of a fast core, // all of a slow one. "light" (the default) pauses them and steps them on together about ten // times a second, "smooth" lets them run, "off" stops them. Pan and zoom gestures pause them. + // A system asking for less motion (Windows: Animation effects off) starts them off, but the + // button stays: what the viewer picks there wins and is remembered. + var reducedMotion = !!(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches); var ANIM = ["light", "smooth", "off"], ANIM_UI = { light: ["\u25B7", "Animation: light, about 10 steps a second (click: smooth)"], smooth: ["\u25B6", "Animation: smooth (click: off)"], - off: ["\u2016", "Animation: off (click: light)"] + off: ["\u2016", reducedMotion ? "Animation: off, as your system asks for less motion (click: on)" : "Animation: off (click: light)"] }; var animMode = (function () { var m = null; try { m = localStorage.getItem("isokit-anim"); } catch (e) { /* storage blocked */ } - return ANIM.indexOf(m) >= 0 ? m : ANIM.indexOf(opts.animation) >= 0 ? opts.animation : "light"; + if (ANIM.indexOf(m) >= 0) return m; + if (reducedMotion) return "off"; + return ANIM.indexOf(opts.animation) >= 0 ? opts.animation : "light"; })(); - var animTimer = 0, animLast = 0, animList = [], animBtn = bar.querySelector('[data-a="anim"]'); - var reducedMotion = !!(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches); + // animScripted: the animations of this drawing were paused by script (light mode) and ignore + // CSS play states from then on, so smooth needs the scene drawn again + var animTimer = 0, animLast = 0, animList = [], animScripted = false, animBtn = bar.querySelector('[data-a="anim"]'); // after every new scene and every mode change function animSetup() { clearInterval(animTimer); animTimer = 0; animList = []; wrap.classList.toggle("iso-anim-off", animMode === "off"); + // the viewer turned them on: past the reduced-motion rules in the CSS + wrap.classList.toggle("iso-anim-on", animMode !== "off"); if (animBtn) { - animBtn.hidden = reducedMotion || !svg.querySelector(".iso-a, .iso-drive, .iso-ledr, .iso-ledo"); + animBtn.hidden = !svg.querySelector(".iso-a, .iso-drive, .iso-ledr, .iso-ledo"); animBtn.textContent = ANIM_UI[animMode][0]; animBtn.title = ANIM_UI[animMode][1]; animBtn.setAttribute("aria-label", ANIM_UI[animMode][1]); } @@ -163,6 +171,7 @@ animList = svg.getAnimations({ subtree: true }); if (!animList.length) return; animList.forEach(function (a) { a.pause(); }); + animScripted = true; animLast = performance.now(); animTimer = setInterval(animStep, 100); } @@ -178,7 +187,7 @@ animMode = m; try { localStorage.setItem("isokit-anim", m); } catch (e) { /* storage blocked */ } // animations paused by script ignore CSS from then on: draw the scene again to let them run - if (was === "light" && m === "smooth" && scn) applyScene(scn); else animSetup(); + if (m === "smooth" && animScripted && scn) applyScene(scn); else animSetup(); call(opts.onAnimation, m); } @@ -215,7 +224,7 @@ var hadScene = !!scn; scn = d; svg.innerHTML = d.html; - lastK = null; + lastK = null; animScripted = false; wrap.classList.toggle("iso-still", st.cfg.running === false); metas = Array.prototype.map.call(svg.querySelectorAll(".iso-tag"), function (g, i) { return { el: g, p: +g.getAttribute("data-p"), w: +g.getAttribute("data-w"), x: +g.getAttribute("data-x"), y: +g.getAttribute("data-y"), i: i, hid: false };