Draggable Homepage Headshot — Experiment Report
Draggable Homepage Headshot — Experiment Report
What changed
#home-headshot on the homepage is draggable (x/y) with window/page bounds using
anime.js v4 createDraggable(). It
sits above the posts-grid (z-index: 1). Dragging uses CSS transforms, so it never
affects text wrapping or layout flow of other elements.
Bounds (current behavior)
- Left/right walls = the edges of the viewport (
window.innerWidth). - Top wall = the top of the page (not the visible viewport).
- Bottom wall = the bottom of the page (
document.documentElement.scrollHeight). containerFriction: 1hard-clamps the element at the walls while dragging, so it can never be dragged off-screen.releaseContainerFriction: 0.8makes a flick glide smoothly into a wall and settle there with a damped spring (see “Known bug” below — it does not yet visibly bounce past the wall).
The bounds are supplied as a function returning [top, right, bottom, left] translate
offsets relative to the element’s initial position, recomputed on init/resize only.
Horizontal scroll offset (scrollX) is included for the left/right walls (no
horizontal scrolling on this site, so it is effectively 0).
Files touched (all changes live on the draggable-components branch)
_layouts/default.html— added anime.js UMD bundle from jsDelivr CDN, withdefer:<script src="https://cdn.jsdelivr.net/npm/animejs/dist/bundles/anime.umd.min.js" defer></script>_layouts/home.html— inside the existingDOMContentLoadedhandler:const { createDraggable } = anime; const imgRect = img.getBoundingClientRect(); const imgX = imgRect.left + window.scrollX; const imgY = imgRect.top + window.scrollY; createDraggable(img, { container: () => { const docH = document.documentElement.scrollHeight; const vw = window.innerWidth; const sx = window.scrollX; return [ -imgY, vw - imgX - imgRect.width + sx, docH - imgY - imgRect.height, -imgX + sx, ]; }, containerFriction: 1, releaseContainerFriction: 0.8, });_sass/_home.scss— two edits:.home-imagegainedposition: relative; z-index: 1;so the image renders in front of the posts-grid when dragged over it.@keyframes gentle-rotatenow animates the standalonerotate:property instead oftransform: rotate(...). Required because anime.js drags via the inlinetransformstyle; the oldtransformkeyframe animation would have overridden it and made dragging invisible. Therotateproperty composes with the drag transform, so the wobble animation still plays.
How to revert to the version without this feature
Option A — switch branches (simplest). The main deployed branch is gh-pages,
which does not contain these changes:
git checkout gh-pages
Option B — stay on this branch and undo the changes. The edits are in
_layouts/default.html, _layouts/home.html, _sass/_home.scss:
git checkout gh-pages -- _layouts/default.html _layouts/home.html _sass/_home.scss
To rebuild after reverting: bundle exec jekyll build
Notes
- The anime.js CDN script is loaded on every page (it was added to the global
default.htmllayout), so other pages still fetch it even though only the homepage uses it. Removing the script tag from_layouts/default.htmleliminates that too. - This report file (
docs/draggable-feature-report.md) is documentation only and has no effect on the site; delete it if you don’t want it in the revert.
Known bug: release bounce does not overshoot the wall (WIP)
Goal: on a fast flick toward a wall, the headshot should visibly bounce past the wall and settle back onto it.
Current actual behavior (verified in a headless Chrome + CDP test): on release the element glides to the wall and settles exactly AT it (damped). It never visibly overshoots past the wall, so there is no visible “bounce”.
What was verified working:
- Hard containment on all four walls during drag (
containerFriction: 1): the element stays within[scrollX, scrollX + innerWidth]horizontally and[0, scrollHeight]vertically at all times while dragged. - A flick releases with captured velocity (
this.velocity≈ 11.7 px/ms for a fast synthetic flick) and the element animates to the wall over ~1.1–1.4 s.
Root-cause analysis (from anime.js v4 source, master + the v4.5.0 UMD on CDN):
- The release-overshoot path only runs when
!hasReleaseSpring && isOutOfBounds && (durationX || durationY). When released while already touching the wall,dest === currentsodurationX/durationY === 0and the overshoot path is skipped entirely (no bounce when pushing against a wall). - When released mid-air toward a wall, the overshoot path does run — it animates
overshootCoordstobx(past the wall) then back todx(the wall) using two blended animations (composition: blend). Empirically the blended value never exceeds the wall (overshootCoords.ypeaked exactly at the wall in testing), so no visible overshoot is produced. - The default release spring (
releaseStiffness: 80,releaseDamping: 20,releaseMass: 1) has damping ratio ζ ≈ 1.12 — overdamped — so the spring-settle ease also never overshoots.
Suggested next steps (unfinished):
- Try an underdamped release spring via the
releaseEaseparam (a spring object is detected via its.easeproperty, which setshasReleaseSpring):releaseEase: anime.spring({ mass: 1, stiffness: 200, damping: 20 })(ζ ≈ 0.71 → oscillates). Verify whether the spring-ease path produces a visible bounce past the wall and settles back; tunestiffness/dampingfor feel. - If the
blendovershoot path is preferred, investigate anime.js v4’scompositionTypes.blendfor theovershootCoordsanimations to makebxvisibly exceed the wall. - Re-run the CDP drag test (harness:
/var/folders/sn/56xvvtb5667fmg2m_307jbj00000gn/T/opencode/dragtest.mjs, headless Chrome on:9222, Jekyll serve on:4001) — theflick-left/flick-downcases assert that the element’s edge goes past the wall (> vw + 15/> docH + 30) then settles back within 15px of the wall.