AgentStack
SKILL verified MIT Self-run

Tearable Cloth

skill-jane-xiaoer-claude-skill-tearable-cloth-claude-skill-tearable-cloth · by Jane-xiaoer

Build an interactive "tearable paper" entry-gate effect for web apps using Three.js + Verlet cloth physics. Use when adding a dramatic onboarding/intro animation where users tear through layers of paper to enter, or any cloth/fabric tearing interaction. Includes battle-tested physics parameters, the cut-vs-grab algorithm, real-hole rendering by skipping torn quads, multi-layer staging, and a sess…

No reviews yet
0 installs
5 views
0.0% view→install

Install

$ agentstack add skill-jane-xiaoer-claude-skill-tearable-cloth-claude-skill-tearable-cloth

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Tearable Cloth? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Tearable Cloth Entry Gate

An interactive onboarding gate where the user must tear through one or more layers of "paper" to reveal the actual content. Inspired by pushmatrix/tearable but rebuilt from scratch in pure Three.js + Next.js (no React-Three-Fiber, no Web Worker).

> Use when: you want a memorable, tactile intro that turns "loading the page" into a small ritual. Don't use for utility-app entry — only for portfolio / brand / playful sites where 5-10 seconds of interaction is welcome.


🎯 The Effect (in one paragraph)

User loads page → sees a sheet of designed "paper" covering the viewport. Mouse drag pulls the paper, stretches it, and breaks visible holes through it. Through the holes the underlying layer (or the actual app) shows. After ~5 drags the paper collapses with gravity and unmounts, revealing the next layer / app.

Critical visual qualities that make this feel real, not gimmicky:

  1. Real holes — torn regions disappear from the mesh (broken triangles culled), not just deformed
  2. Visible stretch before tear — cloth elastically pulls long before snapping (TEAR_DISTANCE ≥ 4× rest length)
  3. Asymmetric tear edges — random per-link tear thresholds (TEAR_NOISE) so rips look natural, not zip-like
  4. Tear recoil — when a link breaks, both endpoints get an outward velocity kick + small +z lift → flap curls out of plane
  5. Differential gravity — fully connected cloth has near-zero gravity (poster on wall); fragmented bits have heavy gravity (falling debris). This is the single biggest source of "weight" feeling.
  6. Top-of-mouse-down enables gravity — initial state is perfectly stable (zero gravity until first interaction). Otherwise cloth sags before user touches it.

🏗 Architecture

HomeShell (client)
├── ToolsWall  (z=0,  always rendered behind)
├── TearGate variant="middle" (z=99,  always mounted; user sees through holes)
└── TearGate variant="cover"  (z=100, mounted until torn → unmounts on threshold)

Why two simultaneous gates instead of swap-on-passed? If you swap (unmount cover → mount middle), users see a 1-frame flash of the underlying app before middle mounts. By rendering both from the start with cover on top, tearing cover immediately reveals middle through the holes — no flash, no transition cut.

SessionStorage gate: First visit shows the gate; same-tab refreshes skip it. Use sessionStorage (not localStorage) so a new tab session brings the ritual back — that's the whole point of an entry ritual.

const TORN_KEY = "xa-torn";
useEffect(() => {
  const torn = sessionStorage.getItem(TORN_KEY);
  setStage(torn ? "wall" : "both");
}, []);

🔬 Final Tuned Parameters (proven, do not change without testing)

These were converged after ~25 iterations against pushmatrix as reference standard. Each one matters; changing any single value noticeably shifts the feel.

// Cloth grid
const COLS = 90;            // Width grid resolution. 90 = pleasingly detailed without lag
const ROWS = 56;            // Height. Aspect should match your texture (90/56 ≈ 1.6)
const SPACING = 0.22;       // World units between adjacent points

// Verlet integration
const REST = SPACING;       // Resting length of each constraint
const STIFFNESS = 0.18;     // 0..1. LOWER = softer, more elastic stretch.
                            // 0.18 is the magic "fabric, not paper-stiff" value.
const ITERATIONS = 2;       // Constraint solve iterations / frame.
                            // PUSHMATRIX USES 2 because they have Web Worker substeps.
                            // We compensate by boosting iterations to 6 during drag (see below).
const STRETCH_BOOST_ITERS = 4;  // Extra iterations during drag → cloth follows mouse precisely
const DAMP = 0.95;          // Velocity retention. 0.95 = bouncy/swingy.  6 → "rubber band, never tears"
const TEAR_NOISE = 0.65;    // Per-link random ±65% of TEAR_DISTANCE. Higher = more ragged edges.
const TEAR_THRESHOLD = 0.075;  // % of links broken to trigger collapse. ~5 drags worth.

// Mouse interaction (the "moveGrab" pattern)
const GRAB_RADIUS = 0.95;     // World-space radius of the "hand"
const GRAB_STRENGTH = 0.72;   // 0..1. How hard cloth follows mouse. > 0.6 feels "sticky"
const CUT_RADIUS = 0.22;      // Sweep cut radius — links along mouse path get severed
const FAST_CUT_VELOCITY = SPACING * 0.3;  // Below this speed, no path-cut (only grab)

// Gravity (differential — the secret of "weight")
const GRAVITY = 0.00045;      // Healthy cloth (4 connected neighbors). Tiny.
const GRAVITY_TORN_EDGE = GRAVITY * 4.5;  // 2-3 neighbors. Half-attached → droops.
const GRAVITY_FRAGMENT = 0.008;  // ≤1 neighbor → falls fast, like torn paper.

🧠 Key Algorithm Insights

1. moveGrab > spring force

When user pointer-downs, don't switch to "spring force toward mouse on every frame for nearby points." That feels squishy and unresponsive.

Instead, the moveGrab pattern (from pushmatrix's reverse-engineered worker code):

// On pointerdown: snapshot the offset of every grabbed point relative to mouse
let grabbedPts: { i: number; offX: number; offY: number; w: number }[] = [];

onDown(e) {
  const { wx, wy } = screenToWorld(e.x, e.y);
  for (let i = 0; i  0.95 else soft pull
      const k = g.w > 0.95 ? 1.0 : g.w * GRAB_STRENGTH;
      p.x += (targetX - p.x) * k;
      p.y += (targetY - p.y) * k;
    }
  }
  // ... verlet integration + constraint solving
}

This makes the grabbed region behave like a piece of fabric stuck under your hand — moves rigidly, stretches the surrounding cloth.

2. Real holes via mesh index rebuilding

The single biggest visual upgrade. Don't just deform geometry — actively skip broken triangles:

// Cache: linkH[r][c] = link from (r,c) to (r,c+1); linkV[r][c] = link from (r,c) to (r+1,c)
// On every frame where dead-link count changed, rebuild indices:
let curDead = 0;
for (const l of links) if (!l.alive) curDead++;
if (curDead !== lastDeadCount) {
  const newIndices: number[] = [];
  for (let r = 0; r  l.tearAt) {
  l.alive = false;
  points[l.a].liveLinks--;
  points[l.b].liveLinks--;
  const nx = dx / d, ny = dy / d;
  const KICK = SPACING * 0.18;
  // Manipulate p.px/p.py to set velocity (verlet velocity = x - px)
  if (!A.pinned) { A.px += nx * KICK; A.py += ny * KICK; A.pz -= 0.05; A.z += 0.05; }
  if (!B.pinned) { B.px -= nx * KICK; B.py -= ny * KICK; B.pz -= 0.05; B.z += 0.05; }
  continue;
}

This makes torn flaps visibly curl out of plane (the +z lift) — like real paper recoils when you rip it.

4. Differential gravity by liveLinks count

Track liveLinks per point (initial: 4 for interior, 2-3 for edges). Decrement on each broken link. In the integration step:

const isFragment = p.liveLinks 
    
    {(stage === "both" || stage === "middle") && (
      
    )}
    {stage === "both" && (
      
    )}
  
);

Tip: make the second layer slightly harder to tear (different threshold) — counterintuitively, this maintains the "earned reveal" feeling. Easy → easy is anticlimactic.

const effectiveThreshold = variant === "middle" ? TEAR_THRESHOLD * 1.2 : TEAR_THRESHOLD;

🎨 Tuning Cheat Sheet

| Symptom | Knob to turn | |---|---| | Cloth feels rigid, snaps too quickly | ↓ STIFFNESS, ↑ TEARDISTANCE | | Cloth too floppy, never stretches | ↑ STIFFNESS, ↑ ITERATIONS | | Tears too easily (1-2 drags through) | ↑ TEARTHRESHOLD or ↑ TEARDISTANCE | | Won't tear no matter what (10+ drags) | ↓ TEARDISTANCE, ↓ TEARTHRESHOLD | | Holes look zip-like (straight lines) | ↑ TEARNOISE (more random per-link) | | Cloth sags at idle | Enable sleep-until-first-interaction OR pin more edges | | Torn pieces don't fall | ↑ GRAVITYFRAGMENT | | Torn edges look stiff (no flap droop) | Verify liveLinks decrement; ↑ GRAVITYTORN_EDGE | | Underlying layer doesn't show through holes | Verify mesh index rebuilding (broken triangles must be culled) | | 1-frame flash between layers | Mount both layers simultaneously, top z=100, bottom z=99 |


⚠️ Common Pitfalls

  1. Forgetting to track liveLinks — without it you can't differentiate fragments from healthy cloth → weight feel breaks.
  2. Rebuilding indices every frame instead of only on dead-link-count change — wasteful, drops fps.
  3. Setting opaque background on TearGate — torn holes will show that color, not your underlying app.
  4. No warmup loop — the first 60 frames after mount show the cloth deforming under gravity into a stable state. Run physics 60 steps before first render.
  5. Using localStorage instead of sessionStorage — once-per-session ritual becomes once-forever, and users who genuinely want to see it again can't (without dev tools).
  6. Trying to share Web Worker physics — you can, but the marshalling overhead for a single mesh isn't worth it. Single-thread JS at 60fps on 90×56 grid is comfortable.

📦 Reference Implementation

A complete working implementation is in reference/TearGate.tsx (~540 lines, copy-pasteable into any Next.js app). Tested in production at https://xiaoer-tools-wall.vercel.app.

Required dependencies:

npm install three @types/three

Pair with a paintPaperTexture(w, h, ...) function that draws on a ` and returns it (reference/paper-texture.ts). Use it as THREE.CanvasTexture` on the cloth mesh.


🎬 The Anti-Pitfall Story

The first 5 iterations on this skill all failed because of one mistaken intuition: "tearing is a knife slice" — i.e., user moves mouse, links along the path get cut. That doesn't feel right. Real cloth tearing in pushmatrix is grab + pull until something gives — the user grabs a fistful, drags, the constraints stretch beyond their tolerance and snap as a consequence of stretching, not a primitive cut operation.

The cut-radius along mouse path does add value as a fallback (when user moves fast), but the grab-pull-stretch-snap cycle is the soul of the effect. Get that right first.


🔗 Source

Original implementation: ~/projects/xiaoer-tools-wall/components/TearGate.tsx Reference (target standard): https://pushmatrix.github.io/tearable/ This skill version: 1.0.0 · 2026-05

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.