Animations, GIFs and the globe now move for every reader
Build / Build (push) Successful in 33s

decePub no longer looks at the system's "reduce motion" setting, so everything moves the same way whatever it says:
- a GIF video in a post plays by itself, muted and looping, instead of waiting for a tap; it now also starts on a page
  just opened, before the first tap, where the browser used to hold it still;
- animated custom emoji move in the emoji picker instead of showing a still picture;
- the globe turns by itself while no located post is in view, flies to the posts, and its pulses, arcs and Sun move;
- going back to the top of a feed scrolls smoothly;
- tooltips, dropdowns, menus, folding sections, toasts, loading spinners, placeholders, progress bars, radio buttons
  and ratings keep their animations: the build now takes daisyUI's reduce-motion rules out of the stylesheet
  (tools/css/motion.py, run after every Tailwind build, in watch mode too).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4YtEKqxKjKyHkFzuRVMNA
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-11-09 20:48:47 +01:00
1 parent eb4ea2947c
commit 697b2c8897
11 files changed
+113 -55

No files matched your search

+1 -1
View File
@@ -46,7 +46,7 @@ export function scrollToOffset(rootId, top) {
}
export function scrollToTop(rootId) {
document.getElementById(rootId)?.scrollTo({ top: 0, behavior: matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth" })
document.getElementById(rootId)?.scrollTo({ top: 0, behavior: "smooth" })
}
export function dispose() {
+11 -16
View File
@@ -9,7 +9,7 @@
// night map (city lights) where it is down, blended across a twilight band, and brighter or dimmer with the Earth–Sun
// distance. It shows now, or, while a post is hovered or focused, the moment that post was written; the Sun moves there
// and back smoothly. Land borders are thin lines, and country names sit inside each country in the reader's language,
// more of them and larger as the camera comes closer. Motion follows prefers-reduced-motion. It renders only while
// more of them and larger as the camera comes closer. It renders only while
// something moves (a camera flight, the Sun moving, a hovered post's pulse, auto-rotation, the reader dragging or
// zooming) and never while the page or the globe is hidden: an idle globe costs no GPU time. No .NET round-trip on
// scroll or hover: the posts' places come in one call per feed change (setPosts), the cards are matched by
@@ -21,7 +21,6 @@ import { createInstruments } from "./instruments.js"
const vendor = new URL("../vendor/globe/", import.meta.url).href
const textures = { day: vendor + "earth-blue-marble.jpg", night: vendor + "earth-night.jpg" }
const countriesUrl = vendor + "countries.json"
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)")
// a touch screen: a tap on a card opens its thread there (main.js), so cards are pinned with a mouse only
const canHover = matchMedia("(hover: hover)")
@@ -218,7 +217,7 @@ function resize() {
if (state.view)
scheduleView()
else
state.globe.pointOfView({ ...state.globe.pointOfView(), altitude: 2.6 * shape }, reducedMotion.matches ? 0 : 600)
state.globe.pointOfView({ ...state.globe.pointOfView(), altitude: 2.6 * shape }, 600)
}
state.shape = shape
}
@@ -243,7 +242,7 @@ async function build(width, height) {
.pointRadius(p => 0.25 + Math.min(Math.sqrt(p.count), 6) * 0.12)
.pointColor(p => colour(p.src))
.pointLabel(p => label(p))
.pointsTransitionDuration(reducedMotion.matches ? 0 : 600)
.pointsTransitionDuration(600)
.ringColor(r => t => r.colour.replace(/[\d.]+\)$/, `${(1 - t).toFixed(2)})`))
.ringMaxRadius(r => r.radius)
.ringPropagationSpeed(r => r.speed)
@@ -254,7 +253,7 @@ async function build(width, height) {
.arcDashLength(0.45)
.arcDashGap(0.25)
.arcDashInitialGap(1)
.arcDashAnimateTime(reducedMotion.matches ? 0 : 1600)
.arcDashAnimateTime(1600)
.htmlLat(d => d.lat)
.htmlLng(d => d.lng)
.htmlAltitude(d => d.kind === "country" ? 0.004 : 0.12)
@@ -419,13 +418,10 @@ function loadNight() {
image.src = textures.night
}
// The Sun moves to a new moment: its subsolar point travels there over about a second (the shorter way round), or
// jumps with reduced motion.
// The Sun moves to a new moment: its subsolar point travels there over about a second (the shorter way round).
function aimSun(sun) {
state.sun = sun
state.sunUniforms.sunIntensity.value = sun.irradiance
if (reducedMotion.matches)
state.sunPoint = { lat: sun.lat, lng: sun.lng }
state.sunUniforms.sunDirection.value = directionOf(state.sunPoint)
state.instruments?.setMoment(sun, state.focus)
state.instruments?.setSun(state.sunPoint)
@@ -659,7 +655,6 @@ function watchTheme() {
function watchVisibility() {
listen(document, "visibilitychange", () => pause(document.visibilityState !== "visible" || !hasSize()))
listen(reducedMotion, "change", () => scheduleView())
}
function listen(target, type, listener) {
@@ -832,13 +827,13 @@ function drawRings() {
const rings = []
const hovered = state.hovered && state.points.get(state.hovered)
if (placed(hovered))
rings.push({ lat: hovered.lat, lng: hovered.lng, radius: 4, speed: 3, period: reducedMotion.matches ? 100000 : 900, colour: colour(hovered.src, 1) })
rings.push({ lat: hovered.lat, lng: hovered.lng, radius: 4, speed: 3, period: 900, colour: colour(hovered.src, 1) })
else if (hovered && Number.isFinite(hovered.beforeLat))
rings.push({ lat: hovered.beforeLat, lng: hovered.beforeLng, radius: 3, speed: 1.5, period: reducedMotion.matches ? 100000 : 1400, colour: colour("country", 1) })
rings.push({ lat: hovered.beforeLat, lng: hovered.beforeLng, radius: 3, speed: 1.5, period: 1400, colour: colour("country", 1) })
const reach = Math.max(0, ...[...state.points.values()].filter(p => p.src === "nearyou").map(p => p.range ?? 0))
const reader = [...state.points.values()].find(p => p.src === "nearyou")
if (reader && reach > 0)
rings.push({ lat: reader.lat, lng: reader.lng, radius: reach / 111, speed: reach / 111 / 2, period: reducedMotion.matches ? 100000 : 2400, colour: colour("nearyou", 0.8) })
rings.push({ lat: reader.lat, lng: reader.lng, radius: reach / 111, speed: reach / 111 / 2, period: 2400, colour: colour("nearyou", 0.8) })
state.globe.ringsData(rings)
}
@@ -894,7 +889,7 @@ function updateView() {
state.view = null
state.spreadKm = 0
state.instruments?.setView(null, 0, [], { located: 0, visible: state.visible.size })
controls.autoRotate = !reducedMotion.matches
controls.autoRotate = true
wake()
return
}
@@ -921,8 +916,8 @@ function updateView() {
function fly(view, ms) {
if (!view || !state?.globe)
return
wake((reducedMotion.matches ? 0 : ms) + 400)
state.globe.pointOfView(view, reducedMotion.matches ? 0 : ms)
wake(ms + 400)
state.globe.pointOfView(view, ms)
}
// A column taller than wide shows less of the globe across: the camera moves back to keep it whole.