YurbaImageEditor
Edit photos and videos right in the browser: crop, adjust colour, apply filters, draw, add text, emoji and stickers, and undo any step. The saved file looks exactly like the preview.
<link rel="stylesheet" href="/dist/yurba-ie.min.css">
<script src="/dist/yurba-ie.min.js"></script>
// Full screen: the result on Done, null on Cancel
const result = await YurbaIE.open(file)
if (result) upload(result.blob)
// In a page of your own
const editor = YurbaIE.create(document.querySelector('#editor'))
await editor.load('/photos/beach.jpg')
The source is a File or Blob, a URL, { url, type: 'video' }, an <img> or a <video>. A picture from another origin needs CORS headers, or the canvas can't be read back. The icons are Material Symbols Rounded, and any of them can be replaced with icons.
| Mode | The result | Tools |
|---|---|---|
photo |
The photo itself, cropped and turned, up to maxSide pixels on its longest side. |
Crop, adjust, filters, draw, text, emoji, stickers |
frame |
A fixed frame, 1080×1920 by default: a story. The photo or video inside can be moved, scaled and turned, over a blurred background or a colour. | Frame, trim (video), adjust, filters, draw, text, emoji, stickers |
Live demo
Both editors are real: press Done to see what they give back.
A 9:16 frame with a video, trimmed to at most 15 seconds, and a poll sticker of the page's own: it stays out of the picture and comes back as data.
API
| Option | Default | Description |
|---|---|---|
mode | 'photo' | 'photo' or 'frame'. |
frame | 1080×1920 | { width, height } of the frame, in frame mode. |
maxSide | 2560 | Photo mode: the longest side of the result. |
type / quality | 'image/jpeg' / .92 | What the result is encoded as. |
tools | all | Which tools, in order: crop, frame, trim, adjust, filters, draw, text, emoji, stickers. Crop is for photos, trim for videos, stickers only with stickers given. |
aspects | 7 shapes | Crop shapes { id, ratio }: 0 free, -1 the photo's own. |
fonts | 5 fonts | { id, label, family, weight }. Load web fonts yourself; the editor waits for them before saving. |
colors | 12 colours | The palette for text, drawing and backgrounds. |
maxDuration | 60 | Video: the longest the trim may keep, in seconds. |
labels | English | Any text of the interface, by key. |
icons | built-in | Any icon, by key: an HTML string, inserted as given. A key left out keeps its icon. See Icons and YurbaIE.ICONS. |
pickEmoji | built-in grid | async (button, editor) => ({ src } | { char }): your own picker. |
stickers | [] | Stickers of your own, see Stickers. |
state | none | YurbaIE.open() only: a result's state, to open the source again as it was left. |
YurbaIE.open(file, { icons: { close: '<svg>...</svg>', undo: '<i class="my-icon-undo"></i>' } })
| Key | Default | Where |
|---|---|---|
close | close | Closes the editor. |
undo / redo | undo / redo | History. |
trash | delete | The bin a layer is dragged onto. |
remove / resize | close / open_in_full | A picked layer's delete button and round handle. |
crop, frame, trim, music, adjust, filters, draw, text, emoji, stickers | crop_rotate aspect_ratio content_cut music_note tune filter_vintage draw title add_reaction sticky_note_2 | The tools; music also on the music pill at the top. |
tool | extension | A tool of any other id. |
brightness, contrast, saturation, warmth, fade, vignette | light_mode contrast palette thermostat blur_on vignette | Adjustments. |
pen, marker, neon, eraser | edit ink_highlighter wb_incandescent ink_eraser | Brushes. |
fit, fill, rotate, flip, reset | fit_screen crop_free rotate_90_degrees_ccw flip restart_alt | Crop and frame actions. |
sound / muted | volume_up / volume_off | The video's own sound, on and off. |
track, noMusic, loading, moreMusic | music_note music_off more_horiz library_music | Music: a track with no cover, none, still loading, pick more. |
font | match_case | Text: next font. |
textPlain, textSolid, textSemi, textOutline | format_color_text format_color_fill opacity border_color | Text: the current style. |
alignCenter, alignLeft, alignRight | format_align_center format_align_left format_align_right | Text: the current alignment. |
Defaults are Material Symbols names. A tool button styles the element before its label as the icon's round pad, whatever it is. A sticker's own icon is a Material Symbols name or HTML.
export() and the yurba-ie.done event give an image as one picture with everything drawn in. A video isn't encoded in the browser: it comes back as a recipe for a server, with a transparent picture of what is drawn over it.
// An image
{ kind: 'image', blob, width, height, stickers }
// A video
{
kind: 'video', width, height, stickers,
overlay, // PNG the size of the frame: vignette, drawing, texts, emoji
poster, // JPEG: the first kept frame with everything on it
recipe: {
trim: { start, end }, // seconds
muted,
rotation, flipX, // turned by 0/90/180/270, then mirrored
media: { x, y, scale, rotation }, // centre over the frame 0..1, pixel scale, radians
background, // 'blur' or a colour
matrix, // 4x5 colour matrix over 0..1 values
},
state, // YurbaIE.open(source, { state }) opens it as it was
}
Stickers are never drawn into the picture: stickers lists them with their place (x, y over 0..1, scale, rotation) and your data, for your viewer to draw and make clickable.
YurbaIE.open(file, {
mode: 'frame',
stickers: [{
id: 'poll', icon: 'ballot', label: 'Poll',
// Its data when added; null leaves it out
create: async (editor) => ({ question: 'Coffee or tea?' }),
// Tapped again once picked
edit: async (data, editor) => ({ ...data, question: prompt('Question', data.question) }),
// Draws it into el as if the frame were 1080 pixels wide
render: (data, el) => { el.innerHTML = `<div class="poll">${data.question}</div>` },
}],
})
| Method | Description |
|---|---|
YurbaIE.create(parent, config) | A new editor, put into parent if given. |
YurbaIE.open(source, config) | A full-screen editor; a promise of the result, or null. |
load(source) | Opens a photo or, in frame mode, a video. Starts the history over. |
export(options?) | The result. { type, quality } override the config. |
setTool(id) | Opens a tool, or closes it if open; null closes any. |
addText(text?) | Opens the text editor for a new text. |
addEmoji({ src } | { char }) | Adds an emoji or a small picture. |
addSticker(id, data) / updateSticker(layerId, data) | Adds or changes one of your stickers. |
undo() / redo() | A step back or forward. |
hasChanges() | Whether anything changed since load(). |
getState() / setState(state) | Everything but the source, to open it again later. |
destroy() | Stops the video and lets go of the listeners. |
| Event | detail | When |
|---|---|---|
yurba-ie.ready | { kind, width, height } | A source is loaded. |
yurba-ie.change | Any change, once finished: a slider let go, a gesture over. | |
yurba-ie.tool | { tool } | A tool was opened or closed. |
yurba-ie.done | { result } | Done was pressed and the result is ready. |
yurba-ie.cancel | Cancel, or Esc with nothing open. | |
yurba-ie.error | { error } | The result couldn't be made. |
| Do | To |
|---|---|
| Drag a text, emoji or sticker | Move it. It snaps to the middle. |
| Pinch it, or drag its round handle | Scale and turn it. It snaps to every 45°. |
Drag it onto the bin, press its ✕, or Delete | Take it away. |
| Tap a picked text or sticker | Edit it. |
| Drag, pinch or wheel the frame | Place the photo or video in frame mode. |
Wheel, Shift + wheel | Scale, turn what is picked. |
Ctrl+Z, Ctrl+Shift+Z / Ctrl+Y | Undo, redo. |
Esc | Close the tool, then drop the pick, then cancel. |
Every adjustment and filter comes down to one 4×5 colour matrix, YurbaIE.colorMatrix(adjust, filterId). The preview applies it through an SVG filter and the saved picture pixel by pixel, with the same numbers in sRGB, so both look the same. For a video it goes into the recipe.
.y-ie {
--y-ie-bg: #000;
--y-ie-panel: #161618;
--y-ie-control: rgba(255, 255, 255, .1);
--y-ie-text: #fff;
--y-ie-muted: rgba(255, 255, 255, .6);
--y-ie-accent: var(--yurba-brand-color, #3d7bfd);
--y-ie-danger: var(--yurba-red-color, #ff3b30);
--y-ie-radius: var(--yurba-radius-l, 14px);
--y-ie-zindex: 10000; /* full screen */
}