diff --git a/src/apps/basic/Basic/index.html b/src/apps/basic/Basic/index.html
index 5b50433..4e51920 100644
--- a/src/apps/basic/Basic/index.html
+++ b/src/apps/basic/Basic/index.html
@@ -5,10 +5,30 @@
My Drawing
+
+
+ My Drawing
diff --git a/src/apps/basic/BouncyBall/bouncy_ball.js b/src/apps/basic/BouncyBall/bouncy_ball.js
index c5c6060..665125b 100644
--- a/src/apps/basic/BouncyBall/bouncy_ball.js
+++ b/src/apps/basic/BouncyBall/bouncy_ball.js
@@ -1,6 +1,12 @@
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
+// Respect the user's "reduce motion" OS/browser setting (accessibility). When
+// it's on, we draw the ball once and don't start the bouncing loop.
+const reducedMotionQuery =
+ window.matchMedia?.('(prefers-reduced-motion: reduce)');
+let rafId = 0;
+
// Ball properties
const ballRadius = canvas.width * 0.1;
const ballColor = 'red';
@@ -43,7 +49,25 @@ function animate() {
drawBall();
moveBall();
- requestAnimationFrame(animate);
+ rafId = requestAnimationFrame(animate);
+}
+
+// Draw the ball once, without moving it (used when motion is reduced).
+function drawStaticFrame() {
+ ctx.fillStyle = bgColor;
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ drawBall();
+}
+
+// Start bouncing — or, if the user prefers reduced motion, just draw the ball
+// as a single static frame instead of animating.
+function start() {
+ cancelAnimationFrame(rafId);
+ if (reducedMotionQuery?.matches) {
+ drawStaticFrame();
+ } else {
+ rafId = requestAnimationFrame(animate);
+ }
}
function resizeCanvas() {
@@ -61,8 +85,15 @@ function resizeCanvas() {
maxSpeed = Math.max(3, canvas.height * 0.01);
xSpeed = Math.random() * maxSpeed * initialDir; // Random x velocity
ySpeed = Math.random() * maxSpeed * initialDir; // Random y velocity
+
+ // No loop runs under reduced motion, so repaint the static frame ourselves.
+ if (reducedMotionQuery?.matches) drawStaticFrame();
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
-animate();
+start();
+
+// If the user toggles "reduce motion" while the page is open, react live
+// (start bouncing, or freeze to the static frame) without a reload.
+reducedMotionQuery?.addEventListener?.('change', start);
diff --git a/src/apps/basic/BouncyBall/index.html b/src/apps/basic/BouncyBall/index.html
index 23e6d11..68a972f 100644
--- a/src/apps/basic/BouncyBall/index.html
+++ b/src/apps/basic/BouncyBall/index.html
@@ -11,6 +11,10 @@
+
+ Bouncy Ball
diff --git a/src/apps/basic/BouncyBall/style.css b/src/apps/basic/BouncyBall/style.css
index b82bbbd..c3dfe50 100644
--- a/src/apps/basic/BouncyBall/style.css
+++ b/src/apps/basic/BouncyBall/style.css
@@ -15,3 +15,18 @@ canvas {
margin: 20px;
height: calc(100vh - 40px); /* Subtract 20px from top and 20px from bottom */
}
+
+/* Visually hidden: removed from view but kept in the accessibility tree so the
+ page heading is available for screen-reader navigation. */
+.visually-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ padding: 0;
+ border: 0;
+ clip: rect(0 0 0 0);
+ clip-path: inset(50%);
+ overflow: hidden;
+ white-space: nowrap;
+}
diff --git a/src/apps/graphics/LineSegment1-Basic/index.html b/src/apps/graphics/LineSegment1-Basic/index.html
index 400238a..9075eec 100644
--- a/src/apps/graphics/LineSegment1-Basic/index.html
+++ b/src/apps/graphics/LineSegment1-Basic/index.html
@@ -18,6 +18,10 @@
+
+ Line Segment 1: Basic
diff --git a/src/apps/graphics/LineSegment1-Basic/style.css b/src/apps/graphics/LineSegment1-Basic/style.css
index 39d8794..994668f 100644
--- a/src/apps/graphics/LineSegment1-Basic/style.css
+++ b/src/apps/graphics/LineSegment1-Basic/style.css
@@ -9,3 +9,18 @@ canvas {
width: 400px;
height: 400px;
}
+
+/* Visually hidden: removed from view but kept in the accessibility tree so the
+ page heading is available for screen-reader navigation. */
+.visually-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ padding: 0;
+ border: 0;
+ clip: rect(0 0 0 0);
+ clip-path: inset(50%);
+ overflow: hidden;
+ white-space: nowrap;
+}
diff --git a/src/apps/graphics/LineSegment2-Interactive/index.html b/src/apps/graphics/LineSegment2-Interactive/index.html
index 7bd3afa..d4995e9 100644
--- a/src/apps/graphics/LineSegment2-Interactive/index.html
+++ b/src/apps/graphics/LineSegment2-Interactive/index.html
@@ -18,6 +18,10 @@
+
+ Line Segment 2: Interactive
diff --git a/src/apps/graphics/LineSegment2-Interactive/style.css b/src/apps/graphics/LineSegment2-Interactive/style.css
index 39d8794..994668f 100644
--- a/src/apps/graphics/LineSegment2-Interactive/style.css
+++ b/src/apps/graphics/LineSegment2-Interactive/style.css
@@ -9,3 +9,18 @@ canvas {
width: 400px;
height: 400px;
}
+
+/* Visually hidden: removed from view but kept in the accessibility tree so the
+ page heading is available for screen-reader navigation. */
+.visually-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ padding: 0;
+ border: 0;
+ clip: rect(0 0 0 0);
+ clip-path: inset(50%);
+ overflow: hidden;
+ white-space: nowrap;
+}
diff --git a/src/apps/makelogo/MakeabilityLabLogo-AutoResize/index.html b/src/apps/makelogo/MakeabilityLabLogo-AutoResize/index.html
index c5e5101..226f4d8 100644
--- a/src/apps/makelogo/MakeabilityLabLogo-AutoResize/index.html
+++ b/src/apps/makelogo/MakeabilityLabLogo-AutoResize/index.html
@@ -18,6 +18,10 @@
+
+ Makeability Lab Logo: Autoresize
diff --git a/src/apps/makelogo/MakeabilityLabLogo-AutoResize/style.css b/src/apps/makelogo/MakeabilityLabLogo-AutoResize/style.css
index 83bdbfe..7d426db 100644
--- a/src/apps/makelogo/MakeabilityLabLogo-AutoResize/style.css
+++ b/src/apps/makelogo/MakeabilityLabLogo-AutoResize/style.css
@@ -16,3 +16,18 @@ canvas {
width: 100%;
height: 100%;
}
+
+/* Visually hidden: removed from view but kept in the accessibility tree so the
+ page heading is available for screen-reader navigation. */
+.visually-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ padding: 0;
+ border: 0;
+ clip: rect(0 0 0 0);
+ clip-path: inset(50%);
+ overflow: hidden;
+ white-space: nowrap;
+}
diff --git a/src/apps/makelogo/MakeabilityLabLogo-ReverseExplosion/index.html b/src/apps/makelogo/MakeabilityLabLogo-ReverseExplosion/index.html
index 9d38b47..f63648a 100644
--- a/src/apps/makelogo/MakeabilityLabLogo-ReverseExplosion/index.html
+++ b/src/apps/makelogo/MakeabilityLabLogo-ReverseExplosion/index.html
@@ -19,6 +19,10 @@
+
+ Makeability Lab Logo: Reverse Explosion
diff --git a/src/apps/makelogo/MakeabilityLabLogo-ReverseExplosion/style.css b/src/apps/makelogo/MakeabilityLabLogo-ReverseExplosion/style.css
index 565e3a3..06088d7 100644
--- a/src/apps/makelogo/MakeabilityLabLogo-ReverseExplosion/style.css
+++ b/src/apps/makelogo/MakeabilityLabLogo-ReverseExplosion/style.css
@@ -9,3 +9,18 @@ canvas {
width: 1000px;
height: 1000px;
}
+
+/* Visually hidden: removed from view but kept in the accessibility tree so the
+ page heading is available for screen-reader navigation. */
+.visually-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ padding: 0;
+ border: 0;
+ clip: rect(0 0 0 0);
+ clip-path: inset(50%);
+ overflow: hidden;
+ white-space: nowrap;
+}
diff --git a/src/apps/makelogo/MakeabilityLabLogo-ReverseExplosion2/index.html b/src/apps/makelogo/MakeabilityLabLogo-ReverseExplosion2/index.html
index a15f6e0..da532d4 100644
--- a/src/apps/makelogo/MakeabilityLabLogo-ReverseExplosion2/index.html
+++ b/src/apps/makelogo/MakeabilityLabLogo-ReverseExplosion2/index.html
@@ -19,6 +19,10 @@
+
+ Makeability Lab Logo: Reverse Explosion 2
diff --git a/src/apps/makelogo/MakeabilityLabLogo-ReverseExplosion2/style.css b/src/apps/makelogo/MakeabilityLabLogo-ReverseExplosion2/style.css
index 565e3a3..06088d7 100644
--- a/src/apps/makelogo/MakeabilityLabLogo-ReverseExplosion2/style.css
+++ b/src/apps/makelogo/MakeabilityLabLogo-ReverseExplosion2/style.css
@@ -9,3 +9,18 @@ canvas {
width: 1000px;
height: 1000px;
}
+
+/* Visually hidden: removed from view but kept in the accessibility tree so the
+ page heading is available for screen-reader navigation. */
+.visually-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ padding: 0;
+ border: 0;
+ clip: rect(0 0 0 0);
+ clip-path: inset(50%);
+ overflow: hidden;
+ white-space: nowrap;
+}
diff --git a/src/apps/makelogo/MakeabilityLabLogo-SantaMorph/index.html b/src/apps/makelogo/MakeabilityLabLogo-SantaMorph/index.html
index 016951b..3772c6e 100644
--- a/src/apps/makelogo/MakeabilityLabLogo-SantaMorph/index.html
+++ b/src/apps/makelogo/MakeabilityLabLogo-SantaMorph/index.html
@@ -18,6 +18,10 @@
+
+ Makeability Lab Logo Santa Morph
diff --git a/src/apps/makelogo/MakeabilityLabLogo-SantaMorph/sketch.js b/src/apps/makelogo/MakeabilityLabLogo-SantaMorph/sketch.js
index 4d9e4e0..d0294a7 100644
--- a/src/apps/makelogo/MakeabilityLabLogo-SantaMorph/sketch.js
+++ b/src/apps/makelogo/MakeabilityLabLogo-SantaMorph/sketch.js
@@ -90,6 +90,13 @@ let originalSantaTriangles = null;
/** @type {number} Current mouse/touch x-coordinate relative to the canvas. */
let mouseX = 0;
+// Respect the user's "reduce motion" OS/browser setting (accessibility). The
+// Santa→logo morph is mouse-driven, so it's only ever in motion while the user
+// moves the pointer; the one thing that animates on its own is the holiday
+// text's pulsing glow, which we hold steady when reduced motion is requested.
+const reducedMotionQuery =
+ window.matchMedia?.('(prefers-reduced-motion: reduce)');
+
// ---------------------------------------------------------------------------
// Initialization & Layout
// ---------------------------------------------------------------------------
@@ -317,8 +324,11 @@ function drawHolidayMessage() {
const textColor = lerpColor(COLOR_SANTA_SUIT_RED, COLOR_SANTA_BELT, lerpAmt);
// 4. Holiday Styling: Dynamic Sparkle/Glow
- // We use a sine wave based on time to make the glow pulse
- const sparkle = 5 + Math.sin(Date.now() / 300) * 5;
+ // We use a sine wave based on time to make the glow pulse — unless the user
+ // prefers reduced motion, in which case we hold the glow steady.
+ const sparkle = reducedMotionQuery?.matches
+ ? 5
+ : 5 + Math.sin(Date.now() / 300) * 5;
ctx.shadowColor = 'rgba(255, 255, 255, 0.9)';
ctx.shadowBlur = sparkle;
diff --git a/src/apps/makelogo/MakeabilityLabLogo-SantaMorph/style.css b/src/apps/makelogo/MakeabilityLabLogo-SantaMorph/style.css
index 63162f9..66dfc41 100644
--- a/src/apps/makelogo/MakeabilityLabLogo-SantaMorph/style.css
+++ b/src/apps/makelogo/MakeabilityLabLogo-SantaMorph/style.css
@@ -23,4 +23,19 @@ canvas {
max-height: 100%;
width: 100%;
height: 100%;
+}
+
+/* Visually hidden: removed from view but kept in the accessibility tree so the
+ page heading is available for screen-reader navigation. */
+.visually-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ padding: 0;
+ border: 0;
+ clip: rect(0 0 0 0);
+ clip-path: inset(50%);
+ overflow: hidden;
+ white-space: nowrap;
}
\ No newline at end of file
diff --git a/src/apps/makelogo/MakeabilityLabLogo/index.html b/src/apps/makelogo/MakeabilityLabLogo/index.html
index 092b68d..aa0576e 100644
--- a/src/apps/makelogo/MakeabilityLabLogo/index.html
+++ b/src/apps/makelogo/MakeabilityLabLogo/index.html
@@ -18,6 +18,10 @@
+
+ Makeability Lab Logo
diff --git a/src/apps/makelogo/MakeabilityLabLogo/style.css b/src/apps/makelogo/MakeabilityLabLogo/style.css
index 39d8794..994668f 100644
--- a/src/apps/makelogo/MakeabilityLabLogo/style.css
+++ b/src/apps/makelogo/MakeabilityLabLogo/style.css
@@ -9,3 +9,18 @@ canvas {
width: 400px;
height: 400px;
}
+
+/* Visually hidden: removed from view but kept in the accessibility tree so the
+ page heading is available for screen-reader navigation. */
+.visually-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ padding: 0;
+ border: 0;
+ clip: rect(0 0 0 0);
+ clip-path: inset(50%);
+ overflow: hidden;
+ white-space: nowrap;
+}
diff --git a/src/apps/makelogo/MakeabilityLabLogoGridFade/index.html b/src/apps/makelogo/MakeabilityLabLogoGridFade/index.html
index d35838c..b4f6589 100644
--- a/src/apps/makelogo/MakeabilityLabLogoGridFade/index.html
+++ b/src/apps/makelogo/MakeabilityLabLogoGridFade/index.html
@@ -17,6 +17,10 @@
+
+ Makeability Lab Logo — Grid Fade
Press R to replay · G toggle grid · H toggle logo
diff --git a/src/apps/makelogo/MakeabilityLabLogoGridFade/sketch.js b/src/apps/makelogo/MakeabilityLabLogoGridFade/sketch.js
index 19e0c21..db11621 100644
--- a/src/apps/makelogo/MakeabilityLabLogoGridFade/sketch.js
+++ b/src/apps/makelogo/MakeabilityLabLogoGridFade/sketch.js
@@ -33,6 +33,16 @@ const ctx = canvas.getContext('2d');
let makeLabLogo;
let gridFade;
let animationStartMs = 0;
+let rafId = 0;
+
+// Respect the user's "reduce motion" OS/browser setting (accessibility). When
+// it's on, we skip the reveal animation and just show the finished logo.
+const reducedMotionQuery =
+ window.matchMedia?.('(prefers-reduced-motion: reduce)');
+
+// A timestamp well past the end of the reveal. Every piece clamps its progress
+// to 1, so updating with this settles the whole animation to its final frame.
+const SETTLE_MS = 10_000_000;
/**
* (Re)builds the canvas, logo, and animation for the current window size.
@@ -95,7 +105,30 @@ function frame(now) {
gridFade.update(elapsedMs);
gridFade.draw(ctx);
- requestAnimationFrame(frame);
+ rafId = requestAnimationFrame(frame);
+}
+
+/** Draws the finished logo as a single static frame (no motion). */
+function drawSettledFrame() {
+ ctx.fillStyle = BACKGROUND_COLOR;
+ ctx.fillRect(0, 0, window.innerWidth, window.innerHeight);
+ gridFade.update(SETTLE_MS);
+ gridFade.draw(ctx);
+}
+
+/**
+ * Plays the reveal animation — or, if the user prefers reduced motion, draws
+ * the finished logo as one static frame instead of animating.
+ */
+function start() {
+ cancelAnimationFrame(rafId);
+ gridFade.reset();
+ animationStartMs = 0;
+ if (reducedMotionQuery?.matches) {
+ drawSettledFrame();
+ } else {
+ rafId = requestAnimationFrame(frame);
+ }
}
// Rebuild on resize (debounced). The clock keeps running, so an already-revealed
@@ -103,7 +136,11 @@ function frame(now) {
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
- resizeTimer = setTimeout(buildScene, 150);
+ resizeTimer = setTimeout(() => {
+ buildScene();
+ // No loop runs under reduced motion, so repaint the static frame ourselves.
+ if (reducedMotionQuery?.matches) drawSettledFrame();
+ }, 150);
});
// --- Interaction ---
@@ -139,4 +176,8 @@ document.addEventListener('keydown', (event) => {
});
buildScene();
-requestAnimationFrame(frame);
+start();
+
+// If the user toggles "reduce motion" while the page is open, react live
+// (start animating, or settle to the static frame) without a reload.
+reducedMotionQuery?.addEventListener?.('change', start);
diff --git a/src/apps/makelogo/MakeabilityLabLogoGridFade/style.css b/src/apps/makelogo/MakeabilityLabLogoGridFade/style.css
index 9373db3..bd79cb1 100644
--- a/src/apps/makelogo/MakeabilityLabLogoGridFade/style.css
+++ b/src/apps/makelogo/MakeabilityLabLogoGridFade/style.css
@@ -57,3 +57,18 @@ kbd {
display: block;
}
}
+
+/* Visually hidden: removed from view but kept in the accessibility tree so the
+ page heading is available for screen-reader navigation. */
+.visually-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ padding: 0;
+ border: 0;
+ clip: rect(0 0 0 0);
+ clip-path: inset(50%);
+ overflow: hidden;
+ white-space: nowrap;
+}
diff --git a/src/apps/makelogo/MakeabilityLabLogoLeafFall/index.html b/src/apps/makelogo/MakeabilityLabLogoLeafFall/index.html
index 740b5bd..8a103e0 100644
--- a/src/apps/makelogo/MakeabilityLabLogoLeafFall/index.html
+++ b/src/apps/makelogo/MakeabilityLabLogoLeafFall/index.html
@@ -17,6 +17,10 @@
+
+ Makeability Lab Logo — Leaf Fall
Press R to replay · F drop leaves · T toggle leaf fade · G toggle grid · H toggle logo
diff --git a/src/apps/makelogo/MakeabilityLabLogoLeafFall/sketch.js b/src/apps/makelogo/MakeabilityLabLogoLeafFall/sketch.js
index b37e74b..e8a1599 100644
--- a/src/apps/makelogo/MakeabilityLabLogoLeafFall/sketch.js
+++ b/src/apps/makelogo/MakeabilityLabLogoLeafFall/sketch.js
@@ -34,6 +34,16 @@ const ctx = canvas.getContext('2d');
let makeLabLogo;
let leafFall;
let animationStartMs = 0;
+let rafId = 0;
+
+// Respect the user's "reduce motion" OS/browser setting (accessibility). When
+// it's on, we skip the falling animation and just show the finished logo.
+const reducedMotionQuery =
+ window.matchMedia?.('(prefers-reduced-motion: reduce)');
+
+// A timestamp well past the end of the fall. Every piece clamps its progress
+// to 1, so updating with this settles the whole animation to its final frame.
+const SETTLE_MS = 10_000_000;
/**
* (Re)builds the canvas, logo, and animation for the current window size.
@@ -94,7 +104,30 @@ function frame(now) {
leafFall.update(elapsedMs);
leafFall.draw(ctx);
- requestAnimationFrame(frame);
+ rafId = requestAnimationFrame(frame);
+}
+
+/** Draws the finished logo as a single static frame (no motion). */
+function drawSettledFrame() {
+ ctx.fillStyle = BACKGROUND_COLOR;
+ ctx.fillRect(0, 0, window.innerWidth, window.innerHeight);
+ leafFall.update(SETTLE_MS);
+ leafFall.draw(ctx);
+}
+
+/**
+ * Plays the falling animation — or, if the user prefers reduced motion, draws
+ * the finished logo as one static frame instead of animating.
+ */
+function start() {
+ cancelAnimationFrame(rafId);
+ leafFall.reset();
+ animationStartMs = 0;
+ if (reducedMotionQuery?.matches) {
+ drawSettledFrame();
+ } else {
+ rafId = requestAnimationFrame(frame);
+ }
}
// Rebuild on resize (debounced). The clock keeps running, so an already-revealed
@@ -102,7 +135,11 @@ function frame(now) {
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
- resizeTimer = setTimeout(buildScene, 150);
+ resizeTimer = setTimeout(() => {
+ buildScene();
+ // No loop runs under reduced motion, so repaint the static frame ourselves.
+ if (reducedMotionQuery?.matches) drawSettledFrame();
+ }, 150);
});
// --- Interaction ---
@@ -144,4 +181,8 @@ document.addEventListener('keydown', (event) => {
});
buildScene();
-requestAnimationFrame(frame);
+start();
+
+// If the user toggles "reduce motion" while the page is open, react live
+// (start animating, or settle to the static frame) without a reload.
+reducedMotionQuery?.addEventListener?.('change', start);
diff --git a/src/apps/makelogo/MakeabilityLabLogoLeafFall/style.css b/src/apps/makelogo/MakeabilityLabLogoLeafFall/style.css
index 9373db3..bd79cb1 100644
--- a/src/apps/makelogo/MakeabilityLabLogoLeafFall/style.css
+++ b/src/apps/makelogo/MakeabilityLabLogoLeafFall/style.css
@@ -57,3 +57,18 @@ kbd {
display: block;
}
}
+
+/* Visually hidden: removed from view but kept in the accessibility tree so the
+ page heading is available for screen-reader navigation. */
+.visually-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ padding: 0;
+ border: 0;
+ clip: rect(0 0 0 0);
+ clip-path: inset(50%);
+ overflow: hidden;
+ white-space: nowrap;
+}
diff --git a/src/apps/makelogo/MakeabilityLabLogoZoomFade/index.html b/src/apps/makelogo/MakeabilityLabLogoZoomFade/index.html
index 56a3560..f2ad99d 100644
--- a/src/apps/makelogo/MakeabilityLabLogoZoomFade/index.html
+++ b/src/apps/makelogo/MakeabilityLabLogoZoomFade/index.html
@@ -17,6 +17,10 @@
+
+ Makeability Lab Logo — Z-Zoom
Press R to replay · G toggle grid · H toggle logo
diff --git a/src/apps/makelogo/MakeabilityLabLogoZoomFade/sketch.js b/src/apps/makelogo/MakeabilityLabLogoZoomFade/sketch.js
index e4de334..3880b1f 100644
--- a/src/apps/makelogo/MakeabilityLabLogoZoomFade/sketch.js
+++ b/src/apps/makelogo/MakeabilityLabLogoZoomFade/sketch.js
@@ -34,6 +34,16 @@ const ctx = canvas.getContext('2d');
let makeLabLogo;
let zoomFade;
let animationStartMs = 0;
+let rafId = 0;
+
+// Respect the user's "reduce motion" OS/browser setting (accessibility). When
+// it's on, we skip the zoom animation and just show the finished logo.
+const reducedMotionQuery =
+ window.matchMedia?.('(prefers-reduced-motion: reduce)');
+
+// A timestamp well past the end of the zoom. Every piece clamps its progress
+// to 1, so updating with this settles the whole animation to its final frame.
+const SETTLE_MS = 10_000_000;
/**
* (Re)builds the canvas, logo, and animation for the current window size.
@@ -96,7 +106,30 @@ function frame(now) {
zoomFade.update(elapsedMs);
zoomFade.draw(ctx);
- requestAnimationFrame(frame);
+ rafId = requestAnimationFrame(frame);
+}
+
+/** Draws the finished logo as a single static frame (no motion). */
+function drawSettledFrame() {
+ ctx.fillStyle = BACKGROUND_COLOR;
+ ctx.fillRect(0, 0, window.innerWidth, window.innerHeight);
+ zoomFade.update(SETTLE_MS);
+ zoomFade.draw(ctx);
+}
+
+/**
+ * Plays the zoom animation — or, if the user prefers reduced motion, draws the
+ * finished logo as one static frame instead of animating.
+ */
+function start() {
+ cancelAnimationFrame(rafId);
+ zoomFade.reset();
+ animationStartMs = 0;
+ if (reducedMotionQuery?.matches) {
+ drawSettledFrame();
+ } else {
+ rafId = requestAnimationFrame(frame);
+ }
}
// Rebuild on resize (debounced). The clock keeps running, so an already-revealed
@@ -104,7 +137,11 @@ function frame(now) {
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
- resizeTimer = setTimeout(buildScene, 150);
+ resizeTimer = setTimeout(() => {
+ buildScene();
+ // No loop runs under reduced motion, so repaint the static frame ourselves.
+ if (reducedMotionQuery?.matches) drawSettledFrame();
+ }, 150);
});
// --- Interaction ---
@@ -140,4 +177,8 @@ document.addEventListener('keydown', (event) => {
});
buildScene();
-requestAnimationFrame(frame);
+start();
+
+// If the user toggles "reduce motion" while the page is open, react live
+// (start animating, or settle to the static frame) without a reload.
+reducedMotionQuery?.addEventListener?.('change', start);
diff --git a/src/apps/makelogo/MakeabilityLabLogoZoomFade/style.css b/src/apps/makelogo/MakeabilityLabLogoZoomFade/style.css
index 9373db3..bd79cb1 100644
--- a/src/apps/makelogo/MakeabilityLabLogoZoomFade/style.css
+++ b/src/apps/makelogo/MakeabilityLabLogoZoomFade/style.css
@@ -57,3 +57,18 @@ kbd {
display: block;
}
}
+
+/* Visually hidden: removed from view but kept in the accessibility tree so the
+ page heading is available for screen-reader navigation. */
+.visually-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ padding: 0;
+ border: 0;
+ clip: rect(0 0 0 0);
+ clip-path: inset(50%);
+ overflow: hidden;
+ white-space: nowrap;
+}
diff --git a/src/apps/makelogo/TriangleArtMorphTest/index.html b/src/apps/makelogo/TriangleArtMorphTest/index.html
index 408cbbe..b1f03c8 100644
--- a/src/apps/makelogo/TriangleArtMorphTest/index.html
+++ b/src/apps/makelogo/TriangleArtMorphTest/index.html
@@ -28,6 +28,10 @@
+
+ Makeability Lab Shape Morph
diff --git a/src/apps/makelogo/TriangleArtMorphTest/style.css b/src/apps/makelogo/TriangleArtMorphTest/style.css
index ad2df32..8614eca 100644
--- a/src/apps/makelogo/TriangleArtMorphTest/style.css
+++ b/src/apps/makelogo/TriangleArtMorphTest/style.css
@@ -38,4 +38,19 @@ canvas {
font-family: 'Segoe UI', sans-serif;
}
label { font-weight: bold; margin-right: 8px; }
-select { font-size: 14px; padding: 6px; border-radius: 4px; border: 1px solid #ccc; }
\ No newline at end of file
+select { font-size: 14px; padding: 6px; border-radius: 4px; border: 1px solid #ccc; }
+
+/* Visually hidden: removed from view but kept in the accessibility tree so the
+ page heading is available for screen-reader navigation. */
+.visually-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ padding: 0;
+ border: 0;
+ clip: rect(0 0 0 0);
+ clip-path: inset(50%);
+ overflow: hidden;
+ white-space: nowrap;
+}
\ No newline at end of file
diff --git a/src/apps/makelogo/TriangleArtMorphTest2-MorphLib/index.html b/src/apps/makelogo/TriangleArtMorphTest2-MorphLib/index.html
index be2c503..45fdf9c 100644
--- a/src/apps/makelogo/TriangleArtMorphTest2-MorphLib/index.html
+++ b/src/apps/makelogo/TriangleArtMorphTest2-MorphLib/index.html
@@ -105,6 +105,10 @@
+
+ Makeability Lab Shape Morph
diff --git a/src/apps/makelogo/TriangleArtMorphTest2-MorphLib/style.css b/src/apps/makelogo/TriangleArtMorphTest2-MorphLib/style.css
index e6b319a..2ec6995 100644
--- a/src/apps/makelogo/TriangleArtMorphTest2-MorphLib/style.css
+++ b/src/apps/makelogo/TriangleArtMorphTest2-MorphLib/style.css
@@ -153,4 +153,19 @@ select {
#playbackControls.disabled {
opacity: 0.4;
pointer-events: none;
+}
+
+/* Visually hidden: removed from view but kept in the accessibility tree so the
+ page heading is available for screen-reader navigation. */
+.visually-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ padding: 0;
+ border: 0;
+ clip: rect(0 0 0 0);
+ clip-path: inset(50%);
+ overflow: hidden;
+ white-space: nowrap;
}
\ No newline at end of file