# Without React

> Edit and export photos from plain JavaScript with createPhoto().

`createPhoto()` from `photocn/photo` loads an image and returns a `Photo`: a stateful object you edit with method calls. It doesn't depend on React, so you can use it in Vue, Svelte, web components, plain scripts, or a React app that only needs to process images.

```tsx
"use client";

import { useEffect, useRef } from "react";
import { createPhoto } from "photocn/photo";

// Plain DOM: no React state, no components. Works the same in Vue, Svelte,
// a web component or a <script type="module">.
async function mountPhotoEditor(root: HTMLElement, signal: AbortSignal) {
  root.innerHTML = `
    <canvas class="aspect-[3/2] w-full rounded-lg bg-muted object-contain"></canvas>
    <div class="flex flex-wrap items-center gap-3 text-sm">
      <label class="flex items-center gap-2">Exposure <input name="exposure" type="range" min="-1" max="1" step="0.01" value="0" /></label>
      <label class="flex items-center gap-2">Saturation <input name="saturation" type="range" min="-1" max="1" step="0.01" value="0" /></label>
      <button data-filter="juno" class="rounded-md border px-2.5 py-1">Juno</button>
      <button data-filter="moon" class="rounded-md border px-2.5 py-1">Moon</button>
      <button data-action="rotate" class="rounded-md border px-2.5 py-1">Rotate</button>
      <button data-action="undo" class="rounded-md border px-2.5 py-1">Undo</button>
      <button data-action="export" class="rounded-md border bg-primary px-2.5 py-1 text-primary-foreground">Export</button>
      <output class="text-muted-foreground"></output>
    </div>`;

  const canvas = root.querySelector("canvas")!;
  const inputs = root.querySelectorAll<HTMLInputElement>("input[type=range]");
  const output = root.querySelector("output")!;

  const photo = await createPhoto("/samples/street.jpg");
  if (signal.aborted) return photo.dispose();
  signal.addEventListener("abort", () => photo.dispose());
  photo.attach(canvas);

  inputs.forEach((input) =>
    input.addEventListener("input", () => photo.adjust({ [input.name]: Number(input.value) }), { signal }),
  );
  root.addEventListener("click", async (event) => {
    const button = (event.target as HTMLElement).closest("button");
    if (!button) return;
    if (button.dataset.filter) photo.filter(button.dataset.filter);
    if (button.dataset.action === "rotate") photo.rotate(1);
    if (button.dataset.action === "undo") photo.undo();
    if (button.dataset.action === "export") {
      const { blob, width, height } = await photo.export({ format: "jpeg", width: 1200 });
      output.textContent = `${width}×${height}, ${Math.round(blob.size / 1024)} KB`;
    }
  }, { signal });
  // Keep the controls in sync after undo.
  photo.subscribe(() => {
    inputs[0]!.value = String(photo.params.lights.exposure);
    inputs[1]!.value = String(photo.params.colors.saturation);
  });
}

// Only this wrapper is React, to show the demo on this page.
export default function VanillaExample() {
  const ref = useRef<HTMLDivElement>(null);
  useEffect(() => {
    const controller = new AbortController();
    void mountPhotoEditor(ref.current!, controller.signal);
    return () => controller.abort();
  }, []);
  return <div className="flex flex-col gap-3" ref={ref} />;
}
```

## Edit and export

Every edit returns the photo, so calls chain. Nothing is baked in until you export.

```ts
import { createPhoto } from "photocn/photo";

const photo = await createPhoto(file); // File, Blob, URL, ArrayBuffer or <img>

photo
  .adjust({ exposure: 0.3, contrast: 0.2, saturation: -1 })
  .filter("juno", 0.8)
  .straighten(4)
  .aspectRatio("1:1");

const { blob, width, height, filename } = await photo.export({ format: "jpeg", width: 1080 });
photo.dispose();
```

To process many photos the same way, export one edit as a recipe and apply it to the others:

```ts
const look = photo.recipe;
for (const file of files) {
  const next = await createPhoto(file);
  await next.applyRecipe(look!);
  upload(await next.export({ format: "webp" }));
  next.dispose();
}
```

## Live preview

`attach(canvas)` renders every change into a `<canvas>`, in a Web Worker when the browser supports it. `subscribe()` tells you when to update your own controls.

```ts
const detach = photo.attach(document.querySelector("canvas")!);
const unsubscribe = photo.subscribe((photo) => {
  exposureInput.value = String(photo.params.lights.exposure);
});
```

A canvas handed to the worker can't be reused, so give each `attach()` a fresh `<canvas>`.

## API

| Member | Description |
|---|---|
| `adjust(values)` | Light, color and effect sliders, each `-1..1`: `brightness`, `exposure`, `gamma`, `contrast`, `shadows`, `highlights`, `bloom`, `temperature`, `tint`, `vibrance`, `saturation`, `sepia`, `clarity`, `noise`, `vignette`. |
| `filter(name, strength?)` | Apply a look (`"juno"`, `"moon"`…) at `0..1` strength; `null` removes it. |
| `filterStrength(strength)` | Change the strength of the current look. |
| `curves(channels)` | Tone curves `[rgb, r, g, b]`, each a list of `[input, output]` points or `null`. |
| `blur({ bokeh, gaussian, focus, centerX, centerY })` | Lens or gaussian blur around a sharp center. |
| `rotate(1 \| -1)`, `flip(axis)` | Quarter turns and mirroring. |
| `straighten(degrees)` | `±45°`. The crop shrinks to stay inside the photo. |
| `perspective({ x, y })` | Keystone correction, `-1..1`. |
| `crop(rect)`, `aspectRatio(ratio)` | Normalized crop rect, or a ratio (`16 / 9`, `"4:3"`, `"original"`, `null`). |
| `undo()`, `redo()`, `reset()` | One step per call. |
| `params`, `setParams(params)` | The full edit as plain data (save it, restore it later). |
| `recipe`, `applyRecipe(recipe)` | A portable copy of the edit. |
| `export(options)` | `{ format, quality, width, height, preserveExif }` → `{ blob, type, width, height, filename }`. |
| `histogram()` | RGB histogram of the result. |
| `attach(canvas)`, `detach()` | Live preview. |
| `subscribe(listener)` | Called after every change; returns an unsubscribe function. |
| `ready()` | Resolves once pending filters have loaded. `export()` waits for it. |
| `size`, `outputSize`, `metadata`, `name` | Source size, size after crop, EXIF, filename. |
| `dispose()` | Free the renderer and worker. |

In React, use [`useImageEditorState`](https://photocn.dev/docs/headless) instead: it adds tools, transient slider edits, crop interactions and everything the components need.
