Skip to content

Development notes

Keeping Canvas, GSAP, and local interactions on static Astro pages

How this site divides work between static HTML and browser scripts, using its retained Canvas implementation, GSAP page transitions, and project categories without bringing back an entire Vue app.

林予晴
Published
  • Astro
  • JavaScript
  • GSAP
  • Progressive enhancement

After a site moves to static output, readers can still switch project categories, enlarge images, and see an animation before navigating away. These behaviors need browser scripts, but they do not require wrapping the whole page in a Vue app again.

The rule I used for this architecture was to let the document deliver the content first, then give each interaction the DOM and resources it needs. What matters when deciding what to keep is which capabilities an effect depends on, not which kind of component used to contain it.

This article describes the implementation inspected on October 20, 2026. One distinction needs to be clear from the outset: the site’s shared background at that point was a CSS/SVG engineering grid whose CSS variables were updated by browser scripts. The earlier Canvas flow component remained in the code, but was not mounted in the shared layout. It appears below as an example of drawing responsibilities that can be isolated, not as a description of the background on screen.

Static output determines when content is generated, not whether browser scripts can run

Astro components can generate HTML first and load browser scripts through a component’s script to handle events and animation. These behaviors do not require adding a UI framework first.[1]

The two stages run at different times and have access to different data. On this site, data and components generate project titles, images, category navigation, and Demo links first. The project page’s script then finds the page root and handles category visibility and the image-grid layout. The browser does not start with an empty container and wait for a script to discover which projects exist.

This also makes the failure boundary clearer. Without JavaScript, categories do not become an enhanced view that switches between individual sections, and the script does not lay out the image grid. But the initial document still contains project text, images, and primary links. Script-driven actions such as enlarging images cannot be described as usable without JavaScript merely because the thumbnails are present.

A Canvas initializer does not need to know the whole site

The retained flow implementation accepts a background root through initVectorFlow(background). It finds the canvas inside that root, obtains a drawing context, and creates path and particle data. It uses the browser’s frame scheduling, dimensions, and visibility state; it does not need projects, articles, or the current route from a Vue store.

That lets the drawing algorithm and site content be maintained separately. The root passed in defines the lookup scope. The cleanup function returned by the initializer removes the listeners it created, cancels frames, and cleans up the optional glow animation. The outer component decides when to initialize and when to clean up.

The interface is worth preserving: a root node with a defined structure goes in, and a function that releases resources comes out. Renaming the old onMounted hook is not enough to complete the migration.

After moving away from the component lifecycle, for example, the code still needs to check whether the canvas exists, decide how to exit if it cannot obtain a context, and account for the original page having been left while an animation package loads asynchronously. These problems already existed. Browser scripts now need to take explicit responsibility for them.

These are observations from reading the retained implementation, not results from remounting, measuring, or running acceptance tests on the Canvas behavior during the October 20 inspection. Historical flow measurements are covered in the background flow article.

GSAP can decorate navigation without taking over the destination

The page-transition effect in the inspected implementation takes a different approach: shared controls initialize first, then a motion module loads asynchronously. That module uses GSAP to animate a shutter-style mask and finally calls location.assign() to navigate to the link’s destination.

The destination still comes from a real a element. The code skips external URLs, links with a query or hash, downloads, links that open another window, and clicks with modifier keys. It also skips the mask sequence when the reduced-motion preference is enabled.

The division is specific: HTML provides the link, and the script intervenes in the departure effect only when its conditions are met. It does not create a client router or insert the next page’s content into the current document.

If the motion module fails to load, the outer code clears the page-reveal marker. The primary controls have already initialized, and native links remain in the document. This is a fallback arrangement visible in the code, not proof that every network failure, canceled navigation, or browser-back path was exercised during the inspection.

Keep local state within the area it affects

The project page has three categories in the inspected implementation: development, design, and AIGC. Each category entry has its own hash. The script reads the hash to determine which section to show, updates the URL when a category is clicked, and synchronizes the states of the sections and navigation entries.

This state does not need to move into a site-wide store. It affects only the project page, and the URL can express the selected category. The page’s own observers, image events, and frame scheduling handle image-grid reflow. Neither the article page nor the homepage needs to know the grid’s height.

The following table is a way to check those boundaries, rather than deciding in advance that everything must move into the same kind of component:

Behavior Responsibility on this site Basic content that must remain
Project categories Project-page root and URL hash Category navigation and each section’s content
Image-grid layout The page’s grid cells, cards, and image events A readable initial grid
GSAP page transitions Shared motion module Real page links
Retained Canvas flow The supplied background root and drawing resources A decorative layer that does not carry body content

This table summarizes responsibilities in the inspected code. It is not a recommendation to rewrite every interaction with manual DOM handling. If requirements grow to include many interdependent fields, live state shared across sections, or a complex editor, the state model and framework choice need another look. Scattered event handlers should not be a rule imposed on every product.

Resource cleanup must distinguish leaving from caching

Once initialization has been extracted into a function, it is easy to think only in terms of removing everything when the page is left. But the browser may place the document in the back/forward cache and reuse its existing DOM on return.

The engineering grid’s outer component cleans up animation instances on pagehide and reinitializes them on pageshow when the cache-restoration conditions are met. The project page cancels pending frames when leaving; whether it disconnects observers and removes control listeners also depends on the event’s persisted branch.

These are different cleanup strategies. Animations can be recreated, while native controls may need to remain. A global function that stops everything cannot replace each feature’s lifecycle contract. The background lifecycle article covers specific states and operational evidence. Here, the subject is architectural ownership; older background tests are not evidence for the implementation inspected here.

Check the content before checking the enhancements

When connecting local interactions to static pages, I start with the initial HTML: are the text, images, and destinations readers need already there? Then I check whether scripts control only their agreed nodes and who releases the resources they create.

The actual behavior still needs to be exercised. Frame cancellation in the code does not mean every return-navigation scenario has been tested. Native links do not mean every path remains trouble-free after animation intervenes. This article does not include new measurements of animation smoothness, package size, performance, or search results, nor does it report interaction acceptance testing across all devices.

The article on routes and responsibility boundaries explains who assembles each page. This article adds how interactions get their own scope. As long as content is not tied to the conditions needed to start an animation, keeping an effect does not have to mean keeping the entire app.

Sources

[1] https://docs.astro.build/en/guides/client-side-scripts/