A live WebGL2 pipeline, not a series of one-shot filters
Every slider in the Light, Color, Detail, Sharpen, and Effects sections — exposure, contrast, highlights, shadows, whites, blacks, temperature, tint, saturation, vibrance, clarity, texture, dehaze, sharpen amount and radius, vignette — feeds a single WebGL2 shader that re-renders the whole image on every change, on the GPU, at interactive speed. Nothing is baked into pixels as you adjust: the underlying source bitmap never changes, only the numbers the shader reads each frame, which is what makes moving a slider back to zero a true undo of that one control with no accumulated rounding or quality loss.
Every numeric control is hard-clamped, not just visually limited
Rotation runs -180° to 180°, Straighten -45° to 45° as a separate fine-leveling control, Exposure -5 to 5 stops, the six-strong Light group (contrast, highlights, shadows, whites, blacks) and the four-strong Color group (temperature, tint, saturation, vibrance) all run -100 to 100, Clarity/Texture/Dehaze the same, Sharpen Amount 0 to 100 with Radius 0.5 to 3.0, and Vignette -100 to 100. These ranges are enforced in the editor's own state, not only in the slider UI — a value outside its range is silently clamped back into range the moment it is set, so no combination of inputs can push a control past what the shader was actually built to handle.
Transform, crop, and perspective correction as separate modes on the same image
Rotation and Straighten solve different problems: Rotation turns the image in large steps for a genuine orientation change, while Straighten is a narrower ±45° control meant for leveling a slightly tilted horizon without touching the overall orientation. Flip Horizontal and Flip Vertical mirror the image independently of either rotation control. Crop and Perspective switch the canvas into their own interactive editing modes rather than exposing a slider — perspective correction is represented internally as a four-corner quad in normalized image coordinates, defaulting to the plain rectangle of an unedited image, so correcting perspective is literally dragging those four corners until converging verticals look parallel again.
Text layers: positioned as fractions of the image, not fixed pixels
Adding a text layer creates an independent object with its own content, a font chosen from a fixed system-font list, a size from 8 to 400px, a weight from Thin through Extrabold, a color, an opacity from 0 to 100%, and a rotation of its own, separate from the image's rotation. Its position is stored as an x/y fraction of the output image — 0.5, 0.5 is dead center — rather than as fixed pixel coordinates, which is what lets the same layer land in the same relative spot whether you are viewing it at fit-to-window zoom or exporting at the image's full native resolution.
Where the work actually happens: your GPU, never a server
Opening a file decodes it locally with the browser's own createImageBitmap — no image data is read by, or sent to, anything outside the tab. HEIC and HEIF files, which almost no browser besides Safari decodes natively, are detected by file extension or, when the browser reports no file type at all, by sniffing the file's own header bytes, then converted to a JPEG at 95% quality by a separately loaded decoder library before the rest of the editor ever sees them. An image wider or taller than the GPU's own maximum texture size (assumed to be 4096px if that limit cannot be read directly) is downscaled to fit immediately on load, since a texture larger than the hardware supports simply cannot be uploaded to the graphics card at all — that downscale is why an unusually large source photo can appear slightly smaller than its original dimensions the moment it loads.
Export: full resolution, three formats, and a worked edit start to finish
Export re-renders the image at its original full pixel dimensions in a second, separate offscreen WebGL2 pass — independent of the on-screen zoom level — then resizes down to the Resize panel's target (identity if untouched), and only then draws the text layers on top at that final size, which is why text stays crisp regardless of how much the image was shrunk. Choose JPEG, PNG, or WebP with a quality slider from 50% to 100% (92% default); JPEG has no alpha channel, so transparency in a PNG or WebP source flattens to opaque on export to JPEG, and since every export is a fresh re-encode from the canvas rather than a copy of the source bytes, EXIF data, GPS tags, and any other original metadata never reach the exported file.
Concretely: load a 6000×4000 JPEG portrait, raise Exposure to +0.6 and lower Highlights to -40, add a text layer reading "2026 Portfolio" at 64px in the lower third, then export as WebP at 90% quality. What downloads is a fresh WebP re-encoded from the full 6000×4000 source with those adjustments baked in and the text overlaid at final size — not the original file with edits stacked on top, and carrying none of its metadata.