# Cropper

> [llms.txt](https://zriz.dev/llms.txt)

`createCropper` makes the store behind every `Root`. Use it for [vanilla JS](https://zriz.dev/docs/vanilla-js.md), or to test your crop logic without a browser.

Import it from `@zriz/core`, installed as in [exporting](https://zriz.dev/docs/exporting.md), give it the photo's size and write to it:

```ts
import { createCropper } from "@zriz/core";

const cropper = createCropper({
  image: { width: 4032, height: 3024 },
});

cropper.setRotation(7);
```

A write changes `getValue()` at once. `change` and `commit` fire on the next frame. In React, `Root` makes the cropper for you, and [hooks](https://zriz.dev/docs/hooks.md) shows how to reach it.

## Options

- `image` `Size | null` = `null`

  Sets the photo's natural size. Most writes wait for it. A side of about 1e18 or more can drop the tilt and open level
- `value` `CropValue`

  Sets the start in place of `defaultValue`. `reset()` still goes to `defaultValue`
- `defaultValue` `Partial<CropValue>`

  Merges over the core `defaultValue`. The cropper starts here and `reset()` returns here. Read once
- `minSize` `Size` = `{ width: 1, height: 1 }`

  Sets the smallest crop in source pixels, along the photo's own sides
- `maxZoom` `number` = `15`

  Sets the largest zoom. 1 is the largest crop
- `focalLength` `number` = `26`

  Sets the lens as a 35 mm equivalent, for perspective correction. At a strong tilt, zriz lengthens a lens too short to keep the photo in front of it. A length that overflows falls back to 26 mm
- `disabled` `boolean` = `false`

  Refuses gestures and most writes
- `clock` `Clock` = `frameClock()`

  Schedules frames. Tests pass a manual one, see test without a browser below
- `onValueChange` `(value, details) => void`

  Runs at most once per frame while the crop moves
- `onValueCommit` `(value, details) => void`

  Runs when a gesture or write commits
- `plugins` `readonly CropperPlugin[]` = `[]`

  Adds behaviour, see add a plugin and clean it up below

`minSize` also caps the angle once its diagonal is longer than a side of the photo. Only its size next to the photo counts, not the photo's pixel count. While `disabled`, `setValue()`, `setImage()` and `setOptions()` still work. Both callbacks are on [events](https://zriz.dev/docs/events.md).

## Read the crop

- `getValue` `() => CropValue`

  Returns the live [value](https://zriz.dev/docs/value.md), frames inside a gesture included
- `getState` `() => CropperSnapshot`

  Returns the snapshot that `useCropperState` reads, see [hooks](https://zriz.dev/docs/hooks.md)
- `getCropState` `() => CropState | null`

  Returns the crop in image pixels, or `null` before a photo
- `getZoom` `() => number`

  Returns the zoom, 1 before a photo loads. 1 is the largest crop that fits
- `getMaxZoom` `() => number`

  Returns the `maxZoom` option, 15 by default
- `getRotationLimit` `() => number`

  Returns 45, or less when `minSize` leaves too little room
- `getPixelCrop` `(scale?) => ExportPlan | null`

  Returns the export plan, or `null` before a photo, see [exporting](https://zriz.dev/docs/exporting.md)

## Change the crop

- `setValue` `(value, options?) => void`

  Merges the fields you pass into the current value
- `setRotation` `(degrees, options?) => void`

  Straightens the photo
- `setZoom` `(zoom, options?) => void`

  Zooms around `anchor`, a point from 0 to 1 on the photo, or the crop's centre
- `panBy` `(dx, dy, options?) => void`

  Moves the crop by fractions of the photo's width and height
- `setAspect` `(aspect, options?) => void`

  Locks the crop to an aspect
- `setCropState` `(state, options?) => void`

  Sets the crop in image pixels. `keep: "center"` holds the centre. The default, `keep: "size"`, holds the size. A state from a photo of another size does nothing, and the state's `aspect` and `focal` are ignored
- `rotate90` `(direction?, options?) => void`

  Turns counter-clockwise by default, or clockwise with `"cw"`. A numeric aspect turns with it, so 4:3 becomes 3:4
- `flip` `(axis?, options?) => void`

  Mirrors left to right by default. `"y"` mirrors top to bottom
- `reset` `(options?) => void`

  Returns to `defaultValue`, with or without a photo
- `setImage` `(size | null) => void`

  Sets the photo's natural size. A new size refits the crop with reason `coverage`. A size that is not finite and positive counts as `null`
- `setOptions` `(options) => void`

  Changes `disabled`, `maxZoom`, `minSize`, `focalLength` and the two callbacks. The type takes only those six. `image`, `value`, `defaultValue`, `clock` and `plugins` stay as created. A new `focalLength` takes effect on the next `setValue()`, `reset()` or `setImage()`. Call `setImage()` for a new photo

Every write keeps the crop valid, see [value](https://zriz.dev/docs/value.md). Before the cropper has a photo, only `setValue()`, `reset()`, `setImage()` and `setOptions()` do anything.

## Tell listeners why

Every write except `setImage()` and `setOptions()` takes action options last. Outside a gesture, `setValue()`, `setRotation()`, `setZoom()` and `setCropState()` report `imperative` unless you pass a reason. Here the write straightens the photo 5°, and on the next frame the commit logs `5 history`:

```ts
import { createCropper } from "@zriz/core";

const cropper = createCropper({
  image: { width: 4032, height: 3024 },
  onValueCommit: (value, details) => {
    console.log(value.rotation, details.reason);
  },
});

cropper.setRotation(5, { reason: "history" });
```

- `reason` `ChangeReason`

  Names the change for listeners, see [events](https://zriz.dev/docs/events.md)
- `event` `Event`

  Passes the DOM event behind the change
- `emit` `boolean`

  With `false`, no change or commit fires, and the new value counts as committed. It also drops a change still waiting from the same frame

## Drive the crop from your own input

Call `begin()` when your input takes hold and `end()` when it lets go. Every write between them is one gesture with one commit. Pass the same `owner` object to both, so no other input can end your gesture:

```ts
import { createCropper } from "@zriz/core";

const cropper = createCropper({
  image: { width: 4032, height: 3024 },
});
const owner = {};

export const bind = (input: HTMLInputElement) => {
  input.addEventListener("pointerdown", (event) => {
    cropper.begin("dial", event, owner);
  });

  input.addEventListener("input", () => {
    cropper.setRotation(input.valueAsNumber);
  });

  const end = (event: PointerEvent) => {
    cropper.end(event, owner);
  };

  input.addEventListener("pointerup", end);
  input.addEventListener("pointercancel", end);
};
```

- `begin` `(reason, event?, owner?) => boolean`

  Starts a gesture and stops any animation that holds the commit. Returns `false` when the cropper is disabled or another owner holds a gesture. A dial glide or a wheel turn gives way to it
- `end` `(event?, owner?) => void`

  Ends the gesture. The commit follows once blocking tasks stop
- `cancel` `(event?, owner?) => void`

  Ends the gesture and restores the crop from before it
- `run` `(task, blocking?) => () => void`

  Calls `task(time, delta)` every frame until it returns `false`. Blocking tasks, the default, hold the commit back. Returns a cancel function
- `invalidate` `() => void`

  Asks for one more `frame` event
- `on` `(type, listener) => () => void`

  Listens to one event, see [events](https://zriz.dev/docs/events.md). Returns a remove function
- `subscribe` `(listener) => () => void`

  Runs the listener when the snapshot changes

## Test without a browser

In Node, the default clock waits about 16 ms per frame on `setTimeout`. Pass a clock of your own and step the frames yourself, so every event fires when your test says:

```ts
import { createCropper } from "@zriz/core";
import type { Clock } from "@zriz/core";

const queue = new Map<number, (time: number) => void>();
let id = 0;
let now = 0;

const clock: Clock = {
  request: (step) => {
    id += 1;
    queue.set(id, step);

    return id;
  },
  cancel: (handle) => {
    queue.delete(handle);
  },
};

const advance = (ms: number) => {
  now += ms;
  const steps = [...queue.values()];

  queue.clear();
  for (const step of steps) step(now);
};

const commits: number[] = [];
const cropper = createCropper({
  clock,
  image: { width: 4032, height: 3024 },
  onValueCommit: (value) => commits.push(value.rotation),
});

cropper.setRotation(7);
advance(16);
console.log(commits);
```

`setRotation()` changes `getValue()` at once, and `advance(16)` runs the frame that sends the commit. The log reads `[7]`.

## Add a plugin and clean it up

A plugin is an object with a `name`, an `init` and an optional `destroy`. `init` runs right after `createCropper` makes the cropper. `Root` and `useCropper` take `plugins` too, read them once and run `init` once on the client, after mount, never during a render.

```ts
import { createCropper } from "@zriz/core";
import type { CropperPlugin } from "@zriz/core";

let off = () => {};

const logger: CropperPlugin = {
  name: "logger",
  init: (cropper) => {
    off = cropper.on("commit", (value) => console.log(value));
  },
  destroy: () => off(),
};

const cropper = createCropper({ plugins: [logger] });

cropper.destroy();
```

`destroy` runs only when you call `destroy()`. `Root` never calls it. When the component that made the cropper unmounts, `Root` or the one that called `useCropper`, it calls `stop()` instead, on purpose: `StrictMode` and `<Activity>` unmount a component and mount the same cropper again, and a destroyed cropper cannot come back.

If a plugin holds a timer or a socket, call `destroy()` yourself when you are done with the cropper, for example through `actionsRef`.

- `stop` `() => void`

  Ends any gesture, drops all motion, commits what is there and emits `stop`. The cropper keeps working
- `destroy` `() => void`

  Runs each plugin's `destroy`, emits `destroy`, detaches the photo and drops every listener. A change or commit still waiting is dropped, and a running gesture ends with no `gestureend`, so call `stop()` first to send them. The cropper is finished, and every later write is ignored

## Read the crop in image pixels

`getCropState()` and `useCropperFrame` give a `CropState`, and `setCropState()` takes one. `crop.x` and `crop.y` are not a corner. They are the crop's centre measured from the photo's centre, so they can be negative, with x to the right and y down, after the quarter turns.

- `image` `Size`

  The photo's size before any turn
- `turns` `0 | 1 | 2 | 3`

  Quarter turns, 0 to 3
- `flip` `boolean`

  `true` while mirrored
- `angle` `number`

  The straighten angle in degrees
- `crop` `{ x, y, width, height }`

  The crop in image pixels, after the quarter turns
- `aspect` `number | null`

  The ratio as a number, or `null` when free
- `perspective` `{ vertical, horizontal, focal }`

  The tilt in degrees, with `focal` in pixels
