# Pause optional WebGL without losing the still page

An animated canvas should be able to stop without taking the reading content with it. In this small dart study, the headings, explanatory text, ordinary links and a static SVG are already in the document. WebGL supplies a decorative layer of motion; none of the reading content waits for it.

The [published example on CodePen](https://codepen.io/editor/wearedardo/pen/01a11468-28aa-7a29-b89f-2a60e73cc647) includes the complete source. Expand the editor preview if the illustration is outside its visible area, then use **Pause motion** and **Resume motion** to compare the canvas with the existing still illustration. The example deliberately waits for scene visibility when an intersection observer is available.

## Make the still page the starting state

The SVG exists before JavaScript runs. The canvas and motion button start with the `hidden` attribute, so a visitor does not receive a motion control that depends on a script that has not initialized.

These are excerpts from the actual document:

```html
<canvas id="dart-canvas" aria-hidden="true" hidden></canvas>
<button id="motion-toggle" type="button" aria-pressed="false" hidden>Pause motion</button>
```

The SVG and canvas are decorative and excluded from the accessibility tree. The reading content explains the example independently. This is a structural choice, not evidence that a complete accessibility audit passed.

Once the renderer has drawn successfully, JavaScript reveals the canvas and adds `data-animated` to the stage. CSS hides the SVG while that attribute is present. Stopping motion removes the attribute and hides the canvas, revealing the same SVG. No replacement illustration needs to be fetched or constructed after a graphics failure.

## Keep animation eligibility in one function

The browser preference is read before any WebGL context request:

```javascript
const preference = window.matchMedia('(prefers-reduced-motion: reduce)');
```

[`matchMedia()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia) provides the media-query result and its change notifications. The example combines that preference with its other conditions:

```javascript
function eligible() {
  return !disposed && !preference.matches && !paused && !document.hidden && inView && !failed && !contextLost;
}
```

Those flags describe different reasons to stop. `paused` records a manual choice. Reduced motion is a browser-reported preference. Document visibility and scene intersection are temporary conditions. Graphics failure, context loss and disposal have separate meanings.

When `IntersectionObserver` is available, `inView` begins false and the example waits for its first intersection callback. Without that API, the source allows the illustration to run without an intersection gate. Neither branch proves that the entire page is visible to a person.

Every relevant change calls `update()`. When the predicate passes, the source creates or reuses its renderer, draws, and then schedules the next frame:

```javascript
renderer ||= createRenderer();
renderer.draw(elapsed);
canvas.hidden = false;
stage.setAttribute('data-animated','');
if (!frame) frame = window.requestAnimationFrame(tick);
```

The immediate draw happens after eligibility passes. It is not a promise that animated graphics appear in the first document paint.

## Let a native button own manual pause

The animation is time-driven; it does not implement dragging or cursor-following graphics. Pointer interaction is activation of a normal button:

```javascript
const toggle = () => { paused = !paused; update(); };
button.addEventListener('click',toggle);
```

There is no separate custom keyboard handler. A native [`button`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button) supplies normal activation semantics. The source updates its visible label and `aria-pressed` value from the manual state; screen-reader announcements have not been verified.

The manual flag survives a preference change. If someone pauses, enables reduced motion, and later disables that preference, the example remains manually paused. A system preference change does not silently undo the earlier button choice.

The control is disabled while reduced motion is active or graphics are unavailable. That disabled state is distinct from the manual `paused` flag.

## Keep the CSS fallback active too

JavaScript responds to preference changes, while CSS independently protects the still illustration:

```css
@media (prefers-reduced-motion:reduce) {
  #dart-canvas { display:none !important; }
  .dart-stage[data-animated] .still-dart { visibility:visible; }
}
```

This keeps the SVG visible during the interval before JavaScript handles a change. [`prefers-reduced-motion`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/prefers-reduced-motion) expresses a preference to reduce nonessential motion. It does not measure graphics performance.

## Stop scheduling and restore the existing artwork

The stop path cancels the scheduled frame, resets its previous timestamp and restores the still state:

```javascript
function stop() {
  if (frame) window.cancelAnimationFrame(frame);
  frame = 0;
  previous = 0;
  canvas.hidden = true;
  stage.removeAttribute('data-animated');
}
```

The source uses the [Page Visibility API](https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API) for hidden documents and an [intersection observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) for the illustration's viewport intersection. These are separate inputs to the same predicate. Restoring eligibility can resume the existing renderer rather than creating another one on every pause.

The animation clock caps an individual time step at 0.05 seconds. Canvas dimensions follow the stage size, with device-pixel ratio capped at two. These choices limit particular kinds of work; no frame-rate target, power saving or performance improvement was measured.

## Treat graphics failure and page lifecycle separately

An unavailable WebGL context or failed shader setup leaves the canvas hidden. Partial renderer setup releases its tracked resources before rethrowing the error to the fallback path.

The [`webglcontextlost` event](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/webglcontextlost_event) returns the illustration to its still state. Context restoration permits renderer creation again. A lost context is a different transition from a missing context at initial setup.

On a non-persisted `pagehide`, the example stops scheduling, disconnects the intersection observer, removes its preference/visibility/button listeners and disposes the renderer. A persisted page exit suspends motion without discarding the manual pause choice, so `pageshow` can reconsider eligibility after a back-forward-cache return. Those lifecycle transitions were exercised by the mock harness; actual browser event delivery was not established by that harness.

## Separate what ran from what was simulated

The verification record has three distinct scopes:

| Date and method | Recorded result | Practical limit |
| --- | --- | --- |
| 7 October 2026: JavaScript syntax and static/mock harness | All 13 checks passed after an English-link-only change. Checks cover eligibility transitions, renderer reuse, fallback, cleanup and exact standalone/panel parity. | DOM, media preferences, observer callbacks and WebGL calls were mocked. This does not compile real shaders or verify operating-system events. |
| 6 October 2026: recorded Chrome pass | Desktop rendering; pointer pause and Space-key resume; an emulated reduced-motion reload; a 390×844 viewport without horizontal overflow. | Browser emulation and a small viewport do not establish native OS preference delivery or physical mobile GPU behavior. |
| 7 October 2026: accepted public CodePen and actual signed-out preview | Native creation, Build Success and saved publication; anonymous preview rendered the example, and expanding it allowed motion. Pause restored the still state, resume restored motion, and reload retained the publication. | This is a narrow public preview and pause/resume smoke check, not a new cross-device, screen-reader or performance audit. |

The earlier unsaved CodePen editor attempts are historical. The exact Pen linked above is now published; that acceptance does not resolve unrelated editor or account workflows.

Native OS preference delivery, real offscreen/hidden-document event delivery, physical mobile GPU hardware and screen-reader behavior remain unverified. The 13 checks are useful evidence about the code's state transitions. The dated Chrome checks and current anonymous public preview establish different, narrower browser observations.

For another optional graphic, define its stop conditions before adding more motion. Keep the reading content and still artwork available, then test the transitions independently from the real browser checks.

*AI disclosure: Codex assisted with drafting this article and comparing its explanations with the source code and recorded evidence. The publisher remains responsible for reviewing the accuracy and originality of the finished article.*
