YurbaIE

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.

Usage
<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.

Modes
ModeThe resultTools
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.

Photo
Story frame

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

Config
OptionDefaultDescription
mode'photo''photo' or 'frame'.
frame1080×1920{ width, height } of the frame, in frame mode.
maxSide2560Photo mode: the longest side of the result.
type / quality'image/jpeg' / .92What the result is encoded as.
toolsallWhich tools, in order: crop, frame, trim, adjust, filters, draw, text, emoji, stickers. Crop is for photos, trim for videos, stickers only with stickers given.
aspects7 shapesCrop shapes { id, ratio }: 0 free, -1 the photo's own.
fonts5 fonts{ id, label, family, weight }. Load web fonts yourself; the editor waits for them before saving.
colors12 coloursThe palette for text, drawing and backgrounds.
maxDuration60Video: the longest the trim may keep, in seconds.
labelsEnglishAny text of the interface, by key.
iconsbuilt-inAny icon, by key: an HTML string, inserted as given. A key left out keeps its icon. See Icons and YurbaIE.ICONS.
pickEmojibuilt-in gridasync (button, editor) => ({ src } | { char }): your own picker.
stickers[]Stickers of your own, see Stickers.
statenoneYurbaIE.open() only: a result's state, to open the source again as it was left.
Icons
YurbaIE.open(file, { icons: { close: '<svg>...</svg>', undo: '<i class="my-icon-undo"></i>' } })
KeyDefaultWhere
closecloseCloses the editor.
undo / redoundo / redoHistory.
trashdeleteThe bin a layer is dragged onto.
remove / resizeclose / open_in_fullA picked layer's delete button and round handle.
crop, frame, trim, music, adjust, filters, draw, text, emoji, stickerscrop_rotate aspect_ratio content_cut music_note tune filter_vintage draw title add_reaction sticky_note_2The tools; music also on the music pill at the top.
toolextensionA tool of any other id.
brightness, contrast, saturation, warmth, fade, vignettelight_mode contrast palette thermostat blur_on vignetteAdjustments.
pen, marker, neon, eraseredit ink_highlighter wb_incandescent ink_eraserBrushes.
fit, fill, rotate, flip, resetfit_screen crop_free rotate_90_degrees_ccw flip restart_altCrop and frame actions.
sound / mutedvolume_up / volume_offThe video's own sound, on and off.
track, noMusic, loading, moreMusicmusic_note music_off more_horiz library_musicMusic: a track with no cover, none, still loading, pick more.
fontmatch_caseText: next font.
textPlain, textSolid, textSemi, textOutlineformat_color_text format_color_fill opacity border_colorText: the current style.
alignCenter, alignLeft, alignRightformat_align_center format_align_left format_align_rightText: 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.

Result

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.

Stickers
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>` },
    }],
})
Methods
MethodDescription
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.
Events
EventdetailWhen
yurba-ie.ready{ kind, width, height }A source is loaded.
yurba-ie.changeAny 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.cancelCancel, or Esc with nothing open.
yurba-ie.error{ error }The result couldn't be made.

Gestures and keys
DoTo
Drag a text, emoji or stickerMove it. It snaps to the middle.
Pinch it, or drag its round handleScale and turn it. It snaps to every 45°.
Drag it onto the bin, press its ✕, or DeleteTake it away.
Tap a picked text or stickerEdit it.
Drag, pinch or wheel the framePlace the photo or video in frame mode.
Wheel, Shift + wheelScale, turn what is picked.
Ctrl+Z, Ctrl+Shift+Z / Ctrl+YUndo, redo.
EscClose the tool, then drop the pick, then cancel.
Colour

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.

CSS variables
.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 */
}