UVENARO Unified Creative Editor

← ✨ UVENARO
Untitled Project
Unified Creative Editor
100%
Canvas: No media loaded
πŸ–ΌοΈ

Start your edit

Import an image to begin editing. UVENARO keeps the original source separate from the working project.

`); printWindow.document.close(); const printImage = printWindow.document.getElementById("print-image"); const startPrint = () => { printWindow.focus(); printWindow.print(); }; if (printImage?.complete) startPrint(); else printImage?.addEventListener("load", startPrint, { once: true }); addHistoryMessage(`Print ${page} ${desiredOrientation}`); notify("Print layout prepared.", "success"); } catch (error) { console.error("Print preparation failed.", error); if (url) URL.revokeObjectURL(url); printWindow.close(); notify("Print preview could not be prepared on this device.", "error"); } } function resetProject() { clearTimeout( autoSaveTimer ); autoSaveTimer = null; deleteRecovery() .catch( error => console.warn( "Recovery cleanup failed.", error ) ); globalThis.UvenaroProjectStore?.createProjectId?.(); currentFile = null; originalBitmap ?.close ?.(); originalBitmap = null; work.width = 1; work.height = 1; canvas.width = 1; canvas.height = 1; canvas.hidden = true; placeholder.hidden = false; canvasDocument .classList .remove( "has-media" ); canvasDocument.style.aspectRatio = "4/3"; selection .classList .remove( "visible" ); cropRect = null; documentName.textContent = "Untitled Project"; documentMeta.textContent = "Unified Creative Editor"; canvasInfo.textContent = "No media loaded"; widthInput.value = 1200; heightInput.value = 900; zoom = 1; compareMode = false; multiSelectMode = false; selectedLayerIds.clear(); resetVisualState(); clearHistory(); historyList.innerHTML = ""; addHistoryMessage( "New project" ); setMainLayer( "Background" ); setOperation( "select" ); updateZoom(); notify( "New project started.", "success" ); } async function toggleFullscreen() { try { if ( !document.fullscreenElement ) { await document.documentElement .requestFullscreen ?.(); } else { await document .exitFullscreen ?.(); } } catch ( error ) { notify( "Fullscreen is not available in this browser.", "warning" ); } } document .querySelectorAll( "[data-layer-action]" ) .forEach( button => { button.addEventListener( "click", () => { const action = button.dataset.layerAction; const active = activeLayer(); if ( action === "add" ) { if ( !currentFile ) { notify( "Import an image first." ); return; } pushUndo( "Before Add Layer" ); const layerCanvas = document.createElement( "canvas" ); layerCanvas.width = work.width; layerCanvas.height = work.height; const newLayer = { id: `layer-${Date.now()}-${layerCounter++}`, name: `Layer ${layerCounter - 1}`, canvas: layerCanvas, visible: true, locked: false, opacity: 1, blendMode: "source-over", groupId: null }; layers.push( newLayer ); selectLayer( newLayer.id, false ); renderAll(); addHistoryMessage( "Layer added" ); return; } if ( action === "multi-select" ) { multiSelectMode = !multiSelectMode; if ( !multiSelectMode ) { selectedLayerIds.clear(); } button.classList.toggle( "active", multiSelectMode ); renderLayerList(); notify( multiSelectMode ? "Multi-select enabled. Tap layers to select them." : "Multi-select disabled." ); return; } if ( action === "group" ) { const selected = layers.filter( layer => selectedLayerIds.has( layer.id ) ); if ( selected.length < 2 ) { notify( "Select at least two layers in Multi Select mode.", "warning" ); return; } pushUndo( "Before Group Layers" ); const groupId = `Group ${groupCounter++}`; selected.forEach( layer => layer.groupId = groupId ); renderLayerList(); addHistoryMessage( `${groupId} created` ); notify( `${selected.length} layers grouped as ${groupId}.`, "success" ); return; } if ( action === "ungroup" ) { const groupId = active?.groupId; if ( !groupId ) { notify( "Select a grouped layer first.", "warning" ); return; } pushUndo( "Before Ungroup Layers" ); layers .filter( layer => layer.groupId === groupId ) .forEach( layer => layer.groupId = null ); selectedLayerIds.clear(); renderLayerList(); addHistoryMessage( `${groupId} removed` ); notify( "Layer group removed.", "success" ); return; } if ( !active ) { notify( "Select a layer first." ); return; } if ( action === "duplicate" ) { pushUndo( "Before Duplicate Layer" ); const copyCanvas = document.createElement( "canvas" ); copyCanvas.width = active.canvas.width; copyCanvas.height = active.canvas.height; configurePixelContext( copyCanvas.getContext( "2d" ) ) .drawImage( active.canvas, 0, 0 ); let copyMask = null; if (active.maskCanvas) { copyMask = document.createElement("canvas"); copyMask.width = active.maskCanvas.width; copyMask.height = active.maskCanvas.height; configurePixelContext(copyMask.getContext("2d")).drawImage(active.maskCanvas, 0, 0); } const copy = { id: `layer-${Date.now()}-${layerCounter++}`, name: `${active.name} Copy`, kind: active.kind || "raster", contentData: active.contentData ? { ...active.contentData } : null, canvas: copyCanvas, maskCanvas: copyMask, maskEnabled: active.maskEnabled !== false, visible: active.visible, locked: false, opacity: Number.isFinite( active.opacity ) ? active.opacity : 1, blendMode: active.blendMode || "source-over", groupId: active.groupId || null }; layers.splice( layers.indexOf( active ) + 1, 0, copy ); selectLayer( copy.id, false ); renderAll(); addHistoryMessage( "Layer duplicated" ); return; } if ( action === "delete" ) { if (layers.length <= 1) { notify("At least one layer must remain.", "warning"); return; } requestConfirmation("Delete this layer?", "The selected layer will be removed. You can still use Undo after deletion.", "Delete Layer") .then(confirmed => { if (!confirmed) return; pushUndo("Before Delete Layer"); const activeIndex = layers.indexOf(active); layers.splice(activeIndex, 1); selectLayer(layers[Math.max(0, activeIndex - 1)].id, false); renderAll(); addHistoryMessage("Layer deleted"); scheduleAutoSave(); notify("Layer deleted. Undo is available.", "success"); }); return; } if ( action === "toggle" ) { pushUndo( "Before Layer Visibility" ); active.visible = !active.visible; renderLayerList(); renderAll(); return; } if ( action === "lock" ) { pushUndo( "Before Layer Lock" ); active.locked = !active.locked; renderLayerList(); syncTextControls( active ); syncShapeControls( active ); syncContentControls( active ); notify( active.locked ? "Layer locked." : "Layer unlocked." ); return; } if ( action === "up" ) { const index = layers.indexOf( active ); if ( index < layers.length - 1 ) { pushUndo( "Before Layer Reorder" ); layers.splice( index, 1 ); layers.splice( index + 1, 0, active ); renderLayerList(); renderAll(); } return; } if ( action === "down" ) { const index = layers.indexOf( active ); if ( index > 0 ) { pushUndo( "Before Layer Reorder" ); layers.splice( index, 1 ); layers.splice( index - 1, 0, active ); renderLayerList(); renderAll(); } } } ); } ); document .querySelectorAll( "[data-property-tab]" ) .forEach( button => { button.addEventListener( "click", () => { document .querySelectorAll( "[data-property-tab]" ) .forEach( tab => { tab.classList.remove( "active" ); } ); button.classList.add( "active" ); notify( `${formatOperation( button.dataset.propertyTab )} panel selected.` ); } ); } ); window.UvenaroEditor = { version: "2.0.0-foundation", getState: () => ({ state: cloneState(), width: work.width, height: work.height, activeTool, activeOperation }), getRenderedCanvas: () => render, getWorkingCanvas: () => work, applyExternalBitmap: ( source, label = "External edit" ) => { if ( !currentFile ) { return false; } pushUndo( `Before ${label}` ); work.width = source.width; work.height = source.height; wctx.clearRect( 0, 0, work.width, work.height ); wctx.drawImage( source, 0, 0 ); resetVisualState(); updateDocumentMeta(); renderAll(); addHistoryMessage( label ); return true; }, notify, render: renderAll }; function isEditableControl(target) { return Boolean( target?.closest?.("input, textarea, select, [contenteditable='true']") ); } document.addEventListener("keydown", event => { const modifier = event.ctrlKey || event.metaKey; const key = event.key.toLowerCase(); if (modifier && key === "z") { event.preventDefault(); event.shiftKey ? redo() : undo(); return; } if (modifier && (key === "y")) { event.preventDefault(); redo(); return; } if (modifier && key === "s") { event.preventDefault(); saveProject(); return; } if (isEditableControl(event.target)) return; if (event.key === "+" || event.key === "=") { event.preventDefault(); zoom = Math.min(4, +(zoom + .1).toFixed(2)); updateZoom(); return; } if (event.key === "-") { event.preventDefault(); zoom = Math.max(.2, +(zoom - .1).toFixed(2)); updateZoom(); return; } if (event.key === "0") { event.preventDefault(); zoom = 1; updateZoom(); return; } if (event.key === "Escape") { if (cropRect) cancelCrop(); else if (hasPixelSelection()) clearPixelSelection(); else setOperation("select"); return; } if ( ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key) && event.target?.matches?.("button") ) { const container = event.target.closest(".tool-rail, .tool-grid, .command-group, .modal-actions, .editor-topbar, .canvas-toolbar-group, .right-tabs, .mobile-nav"); if (!container) return; const buttons = [...container.querySelectorAll("button:not([disabled])")] .filter(button => button.offsetParent !== null); const index = buttons.indexOf(event.target); if (index < 0 || buttons.length < 2) return; event.preventDefault(); const backwards = event.key === "ArrowLeft" || event.key === "ArrowUp"; buttons[(index + (backwards ? -1 : 1) + buttons.length) % buttons.length].focus(); } }); const refreshViewportOverlays = () => { if (cropRect) updateCropOverlay(); else if (hasPixelSelection()) updatePixelSelectionOverlay(); updateContentOverlay(); }; window.visualViewport?.addEventListener("resize", refreshViewportOverlays); window.visualViewport?.addEventListener("scroll", refreshViewportOverlays); window.addEventListener("orientationchange", () => { setTimeout(refreshViewportOverlays, 120); }); window.addEventListener("online", () => { notify("Connection restored. Local changes remain protected.", "success"); }); window.addEventListener("offline", () => { notify("You are offline. Editing and local recovery remain available.", "warning"); }); window.addEventListener("error", event => { console.error("Editor runtime error.", event.error || event.message); notify("A recoverable editor issue occurred. Your latest work is being protected.", "error"); if (currentFile) { writeRecovery().catch(error => console.warn("Emergency recovery save failed.", error)); } }); window.addEventListener("unhandledrejection", event => { console.error("Unhandled editor operation.", event.reason); notify("An operation could not finish. Your project remains available.", "error"); if (currentFile) { writeRecovery().catch(error => console.warn("Emergency recovery save failed.", error)); } }); document.addEventListener("visibilitychange", () => { if (document.visibilityState === "hidden" && currentFile) { writeRecovery().catch(error => console.warn("Background save failed.", error)); } }); window.addEventListener("pagehide", () => { if (currentFile) { writeRecovery().catch(error => console.warn("Page-exit save failed.", error)); } }); globalThis.UvenaroBeforeBack = async () => { if (currentFile) await writeRecovery(); }; document.querySelectorAll("button").forEach(button => { if (!button.getAttribute("aria-label")) { const action = button.dataset.editorAction || button.dataset.editorOperation; if (action) button.setAttribute("aria-label", formatOperation(action)); } }); const faxAllowedPlans = new Set( (window.UvenaroConfig?.features?.faxAllowedPlans || ["premium", "business", "pro"]) .map(value => String(value).toLowerCase()) ); const updateFaxButtonAccess = authState => { const faxEnabled = Boolean(window.UvenaroConfig?.features?.fax); const plan = String(authState?.user?.plan || "free").toLowerCase(); const entitlements = Array.isArray(authState?.user?.entitlements) ? authState.user.entitlements.map(value => String(value).toLowerCase()) : []; const entitled = faxAllowedPlans.has(plan) || entitlements.includes("fax") || entitlements.includes("fax.send"); $("preview-fax").hidden = !(faxEnabled && entitled); }; updateFaxButtonAccess(window.UvenaroAuth?.getState?.()); window.UvenaroAuth?.subscribe?.(updateFaxButtonAccess); resetVisualState(); clearHistory(); updateZoom(); setOperation( "select" ); checkRecovery(); } );