# Paper Crumple

An image printed on a sheet of paper. Press and hold the sheet to crumple it into a ball, drag to move it, and release to let it unfold, stay crumpled, or unfold with crease marks left behind. The folds come from a small cloth solver that runs once per seed, and the sheet renders with three.js lighting, paper grain and soft shadows. With the keyboard, hold Space or Enter, use the arrow keys to move, and press Escape to reset.

<demo-paper-crumple>



</demo-paper-crumple>

## Installation

```bash
npx shadcn-vue@latest add "https://nxui.geoql.in/r/paper-crumple.json"
```

## Usage

```vue
<script setup lang="ts">
  import { ref } from 'vue';
  import PaperCrumple from '~/components/ui/paper-crumple/PaperCrumple.vue';
  import type { PaperCrumpleState } from '~/components/ui/paper-crumple/types';

  const state = ref<PaperCrumpleState>('flat');
  const resetKey = ref(0);

  function onState(next: PaperCrumpleState) {
    state.value = next;
  }
</script>

<template>
  <PaperCrumple
    src="/images/poem.jpg"
    alt="A handwritten poem on paper"
    :width="400"
    :height="400"
    :scene-height="360"
    image-fit="contain"
    release-behavior="creased"
    :reset-key="resetKey"
    @state-change="onState"
  />
</template>
```

Remote images must send CORS headers, because the image becomes a WebGL texture. The original image shows as a fallback while the scene loads or if WebGL is unavailable.

## Props

<table>
<thead>
  <tr>
    <th>
      Prop
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        src
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      required
    </td>
    
    <td>
      Image URL. PNG transparency is kept. Remote hosts must allow CORS.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        alt
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        'Crumplable image'
      </code>
    </td>
    
    <td>
      Text for the fallback image and the hold control.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        back-src
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        ''
      </code>
    </td>
    
    <td>
      Image printed on the back. Otherwise the back uses <code>
        paper-color
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        width
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        320
      </code>
    </td>
    
    <td>
      Unfolded paper width in px. Scales down to fit the stage (24px inset).
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        height
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        400
      </code>
    </td>
    
    <td>
      Unfolded paper height in px. Sets the aspect ratio.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        scene-height
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        560
      </code>
    </td>
    
    <td>
      Stage height in px, including room to drag.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        image-fit
      </code>
    </td>
    
    <td>
      <code>
        'cover' | 'contain'
      </code>
    </td>
    
    <td>
      <code>
        'cover'
      </code>
    </td>
    
    <td>
      <code>
        cover
      </code>
      
       crops the texture; <code>
        contain
      </code>
      
       keeps the whole image.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        release-behavior
      </code>
    </td>
    
    <td>
      <code>
        'stay' | 'restore' | 'creased'
      </code>
    </td>
    
    <td>
      <code>
        'restore'
      </code>
    </td>
    
    <td>
      Freeze the crumple, unfold fully, or unfold with lasting creases.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        crumple-amount
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0.85
      </code>
    </td>
    
    <td>
      Maximum fold while held, 0 (flat) to 1 (fully crumpled).
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        crumple-duration
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0.55
      </code>
    </td>
    
    <td>
      Approximate settle time in seconds while holding. 0 is instant.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        release-duration
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0.4
      </code>
    </td>
    
    <td>
      Approximate unfold time in seconds. A new press interrupts it.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        fold-count
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        6
      </code>
    </td>
    
    <td>
      Crease guides in the solver, 3 to 16.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        fold-sharpness
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0.6
      </code>
    </td>
    
    <td>
      Rounded wrinkles (0) to angular creases (1).
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        wrinkle-depth
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0.65
      </code>
    </td>
    
    <td>
      Compression depth, 0 to 2. Higher gives a tighter ball.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        crease-strength
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0.18
      </code>
    </td>
    
    <td>
      Residual relief for <code>
        creased
      </code>
      
       mode. Longer holds leave stronger creases.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        paper-color
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        '#f4f0e8'
      </code>
    </td>
    
    <td>
      Color of the unprinted back.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        roughness
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0.92
      </code>
    </td>
    
    <td>
      Material roughness, 0 (smooth) to 1 (matte).
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        paper-texture
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0.08
      </code>
    </td>
    
    <td>
      Paper-grain bump strength, 0 to 1.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        light-intensity
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        1.8
      </code>
    </td>
    
    <td>
      Strength of the directional light.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        light-angle
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        -35
      </code>
    </td>
    
    <td>
      Key light direction in degrees. 0 shines from above.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        shadow
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
    </td>
    
    <td>
      Self-shadowing and a soft drop shadow.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        shadow-opacity
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0.08
      </code>
    </td>
    
    <td>
      Opacity of the cast and drop shadows, 0 to 1.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        draggable
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
    </td>
    
    <td>
      Move the sheet while held. The grabbed point follows the pointer.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        drag-rotation
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        10
      </code>
    </td>
    
    <td>
      Maximum tilt in degrees while dragging. 0 turns tilt off.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        drag-radius
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        180
      </code>
    </td>
    
    <td>
      Maximum resting offset per axis in px. The stage also limits it.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        return-to-origin
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
    </td>
    
    <td>
      <code>
        restore
      </code>
      
       and <code>
        creased
      </code>
      
       return to the center. <code>
        stay
      </code>
      
       stays where dropped.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        rotation
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0
      </code>
    </td>
    
    <td>
      Initial sheet rotation in degrees.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        seed
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        7
      </code>
    </td>
    
    <td>
      Seed for a repeatable fold and grain layout.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        detail
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        64
      </code>
    </td>
    
    <td>
      Mesh detail, 32 to 96. Higher adds smaller folds and takes longer.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        disabled
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        false
      </code>
    </td>
    
    <td>
      Turn off input and finish any active hold.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        reset-key
      </code>
    </td>
    
    <td>
      <code>
        number | string
      </code>
    </td>
    
    <td>
      <code>
        0
      </code>
    </td>
    
    <td>
      Change it to reset position, crumple and creases at once.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        class
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        ''
      </code>
    </td>
    
    <td>
      Extra classes for the stage.
    </td>
  </tr>
</tbody>
</table>

## Events

<table>
<thead>
  <tr>
    <th>
      Event
    </th>
    
    <th>
      Payload
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        state-change
      </code>
    </td>
    
    <td>
      <code>
        'flat' | 'holding' | 'crumpled' | 'creased'
      </code>
    </td>
    
    <td>
      Fires on press (<code>
        holding
      </code>
      
      ), then with the release state.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        error
      </code>
    </td>
    
    <td>
      <code>
        Error
      </code>
    </td>
    
    <td>
      WebGL setup or image loading failed. The fallback image stays.
    </td>
  </tr>
</tbody>
</table>

## Accessibility

- The sheet is a real `<button>` with an `aria-label` that describes the controls, and `aria-pressed` while held.
- Keyboard: hold Space or Enter to crumple, arrow keys (Shift for bigger steps) to move, Escape to reset. A focus ring shows on keyboard focus.
- A polite `role="status"` region announces loading and errors.
- With `prefers-reduced-motion`, changes are instant and drag tilt is off.
- Rendering pauses when the stage is offscreen or the tab is hidden.
- Ported from [React Bits](https://reactbits.dev/micro/paper-crumple) by David Haz.
