@manufosela/capture-image

@manufosela/capture-image

Webcam capture component with zoom, pan, snap, mask overlay, and save. Frame exactly what you want before capturing.

ℹ️ This component uses navigator.mediaDevices.getUserMedia. Your browser will prompt for camera access. Served over HTTPS or localhost only.
Events will appear here...

Usage

<capture-image
  size-x="480"
  size-y="360"
  mask
  maskpercent="20"
></capture-image>

<script type="module">
  import '@manufosela/capture-image';

  const cam = document.querySelector('capture-image');

  cam.addEventListener('capture-image-snap', (e) => {
    console.log('Snap!', e.detail.image); // base64 data URL
  });

  cam.addEventListener('capture-image-save', (e) => {
    console.log('Saved!', e.detail.image);
  });

  cam.addEventListener('capture-image-error', (e) => {
    console.error('Camera error:', e.detail.error);
  });
</script>

Attributes

Attribute Type Default Description
size-x Number 320 Video/canvas width in pixels
size-y Number 240 Video/canvas height in pixels
mask Boolean false Show a circular mask overlay on the video feed
maskpercent Number 0 Size of the mask hole as percentage of the smallest dimension

Events

Event Detail When
capture-image-snap { image } User clicks Snap — image is a base64 data URL
capture-image-save { image } User clicks Save — image is a base64 data URL
capture-image-error { error } Camera access denied or getUserMedia fails
Demo code (CodePen-ready HTML, CSS, JS)
HTML (html)
<div class="container">
    

    <div class="warning">
      ℹ️ This component uses <code>navigator.mediaDevices.getUserMedia</code>. Your browser will
      prompt for camera access. Served over HTTPS or localhost only.
    </div>

    <div class="demo-card">
      <capture-image id="cam" size-x="480" size-y="360" mask maskpercent="20"></capture-image>
    </div>

    <div id="event-output" style="background: #1c1b1b; border-radius: 0.5rem; padding: 1rem 1.25rem; font-family: monospace; font-size: 0.85rem; color: #b9cbc2; min-height: 2rem; margin-bottom: 2rem;">
      Events will appear here...
    </div>

    <h2 style="margin-top: 2rem;">Usage</h2>

    <div style="background: #1c1b1b; border-radius: 0.5rem; padding: 1.25rem; margin-bottom: 2rem;">
      <pre style="margin: 0; color: #e0e0e0; font-size: 0.85rem; white-space: pre-wrap; overflow-x: auto;"><code>&lt;capture-image
  size-x="480"
  size-y="360"
  mask
  maskpercent="20"
&gt;&lt;/capture-image&gt;

&lt;script type="module"&gt;
  import '@manufosela/capture-image';

  const cam = document.querySelector('capture-image');

  cam.addEventListener('capture-image-snap', (e) =&gt; {
    console.log('Snap!', e.detail.image); // base64 data URL
  });

  cam.addEventListener('capture-image-save', (e) =&gt; {
    console.log('Saved!', e.detail.image);
  });

  cam.addEventListener('capture-image-error', (e) =&gt; {
    console.error('Camera error:', e.detail.error);
  });
&lt;/script&gt;</code></pre>
    </div>

    <h2>Attributes</h2>

    <div style="overflow-x: auto; margin-bottom: 2rem;">
      <table style="width: 100%; border-collapse: collapse; font-size: 0.9rem;">
        <thead>
          <tr style="border-bottom: 1px solid #333; text-align: left;">
            <th style="padding: 0.75rem 1rem; color: #b9cbc2;">Attribute</th>
            <th style="padding: 0.75rem 1rem; color: #b9cbc2;">Type</th>
            <th style="padding: 0.75rem 1rem; color: #b9cbc2;">Default</th>
            <th style="padding: 0.75rem 1rem; color: #b9cbc2;">Description</th>
          </tr>
        </thead>
        <tbody>
          <tr style="border-bottom: 1px solid #222;">
            <td style="padding: 0.75rem 1rem;"><code>size-x</code></td>
            <td style="padding: 0.75rem 1rem;">Number</td>
            <td style="padding: 0.75rem 1rem;">320</td>
            <td style="padding: 0.75rem 1rem;">Video/canvas width in pixels</td>
          </tr>
          <tr style="border-bottom: 1px solid #222;">
            <td style="padding: 0.75rem 1rem;"><code>size-y</code></td>
            <td style="padding: 0.75rem 1rem;">Number</td>
            <td style="padding: 0.75rem 1rem;">240</td>
            <td style="padding: 0.75rem 1rem;">Video/canvas height in pixels</td>
          </tr>
          <tr style="border-bottom: 1px solid #222;">
            <td style="padding: 0.75rem 1rem;"><code>mask</code></td>
            <td style="padding: 0.75rem 1rem;">Boolean</td>
            <td style="padding: 0.75rem 1rem;">false</td>
            <td style="padding: 0.75rem 1rem;">Show a circular mask overlay on the video feed</td>
          </tr>
          <tr>
            <td style="padding: 0.75rem 1rem;"><code>maskpercent</code></td>
            <td style="padding: 0.75rem 1rem;">Number</td>
            <td style="padding: 0.75rem 1rem;">0</td>
            <td style="padding: 0.75rem 1rem;">Size of the mask hole as percentage of the smallest dimension</td>
          </tr>
        </tbody>
      </table>
    </div>

    <h2>Events</h2>

    <div style="overflow-x: auto; margin-bottom: 2rem;">
      <table style="width: 100%; border-collapse: collapse; font-size: 0.9rem;">
        <thead>
          <tr style="border-bottom: 1px solid #333; text-align: left;">
            <th style="padding: 0.75rem 1rem; color: #b9cbc2;">Event</th>
            <th style="padding: 0.75rem 1rem; color: #b9cbc2;">Detail</th>
            <th style="padding: 0.75rem 1rem; color: #b9cbc2;">When</th>
          </tr>
        </thead>
        <tbody>
          <tr style="border-bottom: 1px solid #222;">
            <td style="padding: 0.75rem 1rem;"><code>capture-image-snap</code></td>
            <td style="padding: 0.75rem 1rem;"><code>{ image }</code></td>
            <td style="padding: 0.75rem 1rem;">User clicks Snap — <code>image</code> is a base64 data URL</td>
          </tr>
          <tr style="border-bottom: 1px solid #222;">
            <td style="padding: 0.75rem 1rem;"><code>capture-image-save</code></td>
            <td style="padding: 0.75rem 1rem;"><code>{ image }</code></td>
            <td style="padding: 0.75rem 1rem;">User clicks Save — <code>image</code> is a base64 data URL</td>
          </tr>
          <tr>
            <td style="padding: 0.75rem 1rem;"><code>capture-image-error</code></td>
            <td style="padding: 0.75rem 1rem;"><code>{ error }</code></td>
            <td style="padding: 0.75rem 1rem;">Camera access denied or getUserMedia fails</td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
CSS (css)
:root {
  --bg: #0c0f14;
  --bg-elevated: #141923;
  --bg-panel: #171d28;
  --border: #262f3f;
  --text: #f4f6fb;
  --text-muted: #a7b0c2;
  --text-dim: #7d879b;
  --accent: #ff8a3d;
  --accent-strong: #ff6a00;
  --accent-soft: rgba(255, 138, 61, 0.16);
  --shadow: 0 20px 50px rgba(5, 8, 14, 0.45);
  --radius-lg: 22px;
  --radius-md: 14px;
  --radius-sm: 10px;
  --max-width: 1160px;
}

.container { max-width: 960px; margin: 0 auto; }
    header { margin-bottom: 3rem; }
    h1 {
      font-family: "Manrope", system-ui, sans-serif;
      font-weight: 800;
      letter-spacing: -0.02em;
    }
    .subtitle { color: #b9cbc2; margin: 0.5rem 0 0.75rem; }
    .back-link { font-size: 0.85rem; color: #b9cbc2; text-decoration: none; }
    .back-link:hover { color: #00e0b3; }
    .demo-card {
      background: #1c1b1b;
      border-radius: 1rem;
      padding: 2rem;
      margin-bottom: 2rem;
      display: flex;
      justify-content: center;
    }
    .warning {
      background: rgba(0, 224, 179, 0.08);
      color: #b9cbc2;
      padding: 1rem 1.25rem;
      border-radius: 0.5rem;
      font-size: 0.9rem;
      margin-bottom: 1.5rem;
    }
JS (js)
import "https://esm.sh/@manufosela/capture-image";

    const cam = document.getElementById('cam');
    const output = document.getElementById('event-output');

    const log = (msg) => {
      const line = document.createElement('div');
      line.textContent = `${new Date().toLocaleTimeString()} — ${msg}`;
      output.prepend(line);
    };

    cam.addEventListener('capture-image-snap', () => log('capture-image-snap fired'));
    cam.addEventListener('capture-image-save', () => log('capture-image-save fired'));
    cam.addEventListener('capture-image-error', (e) => log(`capture-image-error: ${e.detail.error}`));