# Text Displacement Cursor Repel (letters & words flee the cursor)

## Goal
Build a full-viewport dark typographic page where **two giant headings and a justified paragraph are split into individual letters (headings) and words (paragraph), and every fragment is elastically pushed away from the mouse cursor: any fragment whose original center lies within a 150px radius of the cursor gets a displacement force pointing radially away from the mouse, scaling linearly up to 300px at zero distance, and every fragment eases toward its target each frame with per-element lerp smoothing (factor 0.1), so text bulges away from the cursor and springs softly back to place when the mouse leaves**. The star effect is this smooth repel-and-settle displacement field over live text.

## Tech
Vanilla HTML/CSS/JS with an ES module script (`<script type="module" src="./script.js">`). **No GSAP, no plugins, no Lenis, no libraries at all** — the entire effect is manual DOM text splitting, one `mousemove` listener, per-element linear interpolation (lerp), and an infinite `requestAnimationFrame` loop writing inline CSS `transform: translate(x, y)`. There is no scroll interaction.

## Layout / HTML
```
div.container                      (full-viewport flex stage)
  h1.anime-header                  "one subscription"
  p.anime-text                     (long justified paragraph, see text below)
  h1.anime-header                  "endless web design"
```
Use this exact paragraph copy for `.anime-text` (a single `<p>`, no line breaks needed):

> Sharing all the sauce behind building dope interactive experiences and the finest websites that truly stand out. Recently, MotionpromptsPRO was introduced, a subscription-based service tailored to the needs of passionate web designers. As a PRO member, you gain exclusive access to source code for each tutorial and monthly website templates. These resources are carefully curated to support and inspire your creativity, helping you take your web design skills to the next level. MotionpromptsPRO opens up a realm of opportunities for professional growth and empowers you to bring your ideas to life with ease. Delve into coding without clutter. Access the source code for every tutorial published on the Motionprompts YouTube channel and build elegant website components effortlessly. Take the fast lane to mastery. Each month, you'll receive a fresh complete responsive website template. Inspired by award winning web experiences, these templates allow you to build standout websites without starting from scratch, serving as the perfect foundation for your next project.

The markup starts as plain text — JS replaces the text content of all three elements with `<span>` fragments at runtime (see effect section).

## Styling
- `* { margin:0; padding:0; box-sizing:border-box; user-select:none; }` — text must NOT be selectable (the cursor sweeps across it constantly).
- `body { background-color:#1a1a1a; color:#fff; font-family:"TWK Lausanne", monospace; }` — near-black page, white text. "TWK Lausanne" is a commercial font that is not loaded, so keep the declaration exactly as-is and let it fall back to the generic **monospace** system font (that fallback IS the intended look).
- `.container { position:relative; width:100vw; height:100vh; padding:2em; display:flex; flex-direction:column; justify-content:center; align-items:center; gap:4em; overflow:hidden; }` — heading / paragraph / heading stacked and centered with 4em gaps; displaced fragments that overshoot the viewport get clipped.
- `.anime-text { font-size:16px; font-weight:400; line-height:1.25; text-align:justify; -webkit-font-smoothing:antialiased; -moz-osx-font-smoothing:grayscale; }`
- `.anime-header { font-size:10vw; font-weight:400; }` — the two headings scale with the viewport (huge, roughly one line each).
- `.word { position:relative; margin-right:0.75em; margin-bottom:0.5em; display:inline-block; will-change:transform; }` — paragraph fragments.
- `.letter { position:relative; display:inline-block; will-change:transform; }` — heading fragments (no margins; real space text nodes separate the words).

## The effect (exhaustive — vanilla JS, mousemove + lerp + rAF)

### Entry point & calls
On DOM ready, run one generic function twice:
```js
animateTextElements(".anime-text", "words");
animateTextElements(".anime-header", "letters");
```
`animateTextElements(selector, splitBy)` loops over ALL elements matching the selector (so both `h1.anime-header` are handled by the same call) and, per element, performs the split, position capture, mouse tracking and rAF loop described below.

Bootstrapping: since the script is a deferred ES module, use the robust pattern — define an `init` that does the two calls, then:
```js
if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", init);
} else {
  init();
}
```

### 1. Manual text splitting (no SplitText — hand-rolled)
For each target element:
- **`splitBy === "words"`** (the paragraph): `element.textContent.trim().split(/\s+/)` → array of words. Element type/class: `word`.
- **`splitBy === "letters"`** (the headings): split into words the same way, then flatten each word into its individual characters, pushing a literal `" "` entry between words (after every word except the last). Element type/class: `letter`.

Then clear the container (`textContent = ""`) and rebuild it:
- For each entry, create a `<span>` with class `word` or `letter` and the fragment as text, append it to the container.
- **Spaces are plain text nodes, never spans, never animated:** in letters mode, when the entry is `" "`, append `document.createTextNode(" ")` and skip it (no tracking object). In words mode, append a `createTextNode(" ")` after every word span except the last.
- For every created span, push a tracking object into an `animatedElements` array:
  ```js
  { element: span, originalX: 0, originalY: 0, currentX: 0, currentY: 0, targetX: 0, targetY: 0 }
  ```

### 2. Original-position capture (once, 100ms after split)
Inside a `setTimeout(..., 100)` (lets layout settle), for each tracked span read `getBoundingClientRect()` and store its **center in viewport coordinates**:
```js
originalX = rect.left + rect.width / 2;
originalY = rect.top + rect.height / 2;
```
and reset `currentX/Y` and `targetX/Y` to 0. These originals are measured **once and never re-measured** — no resize/scroll recalculation (the layout is a static 100vh page).

### 3. Mouse tracking — the repel field
A `document`-level `mousemove` listener updates every element's target each time the mouse moves, using these exact constants:
```js
const radius = 150;           // px — influence radius around the cursor
const maxDisplacement = 300;  // px — force at distance 0
```
For each tracked element:
```js
const dx = originalX - mouseX;          // vector FROM the cursor TO the element
const dy = originalY - mouseY;
const distance = Math.sqrt(dx * dx + dy * dy);

if (distance < radius) {
  const force = (1 - distance / radius) * maxDisplacement;  // linear falloff: 300 at 0px → 0 at 150px
  targetX = (dx / distance) * force;    // normalized direction × force → pushed AWAY from cursor
  targetY = (dy / distance) * force;
} else {
  targetX = 0;                          // outside the radius → spring back home
  targetY = 0;
}
```
Key semantics: the distance test uses the **original** (rest) center, not the displaced position — so an element that has been pushed out of the radius keeps being driven while the cursor stays near its home spot, and elements can never be "chased" indefinitely.

### 4. rAF render loop — lerp smoothing
An infinite `requestAnimationFrame` loop (started immediately, per container) eases every element toward its target with **lerp factor 0.1**:
```js
const lerpFactor = 0.1;
currentX += (targetX - currentX) * lerpFactor;
currentY += (targetY - currentY) * lerpFactor;
element.style.transform = `translate(${currentX}px, ${currentY}px)`;
```
This single loop produces both halves of the motion: the springy push-away while the cursor is near (targets jump on every mousemove, positions catch up smoothly) and the soft settle back to `translate(0, 0)` when the cursor leaves the radius (targets snap to 0, positions decay exponentially). No easing curves, no durations — the feel comes entirely from the 0.1 lerp at display refresh rate.

### Timeline summary (there is no timeline)
No GSAP, no tweens, no ScrollTrigger, no SplitText, no CustomEase. Trigger is `mousemove` only; motion model is per-frame exponential smoothing (`current += (target − current) × 0.1`) applied to `transform: translate()` on every letter/word span, at all times, forever.

## Assets / images
**None.** The component is pure typography on a flat `#1a1a1a` background. No images, no icons, no external fonts to load.

## Behavior notes
- Runs on any viewport (no desktop gate), but it is a mouse-driven effect — on touch devices nothing moves. No `prefers-reduced-motion` branch.
- The rAF loop never stops; with target 0 and lerp 0.1 the transforms decay to ~0 and idle cost is negligible.
- Because original centers are captured once at load (viewport coords) and the page is a fixed 100vh/100vw flex layout with `overflow:hidden`, there is no scrolling and no need to re-measure.
- The paragraph is `text-align: justify` with inline-block `.word` spans (0.75em right / 0.5em bottom margins) plus real spaces — expect a slightly airy, grid-like word spacing; that is intentional.
- Heading spaces are unstyled text nodes between `.letter` spans, so word gaps in the headings never displace.
