Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions static/canvas.html
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,20 @@
<button class="crop-ratio-btn" type="button" data-crop-ratio="3:2">3:2</button>
<button class="crop-ratio-btn" type="button" data-crop-ratio="2:3">2:3</button>
</div>
<div id="imageOutpaintTools" class="image-edit-tools crop-ratio-tools">
<span class="image-edit-sub" data-i18n="canvas.outpaintRatio">扩图比例</span>
<button class="crop-ratio-btn active" type="button" data-outpaint-ratio="free" data-i18n="canvas.ratioFree">自由</button>
<button class="crop-ratio-btn" type="button" data-outpaint-ratio="source" data-i18n="canvas.ratioSource">原图</button>
<button class="crop-ratio-btn" type="button" data-outpaint-ratio="1:1">1:1</button>
<button class="crop-ratio-btn" type="button" data-outpaint-ratio="4:3">4:3</button>
<button class="crop-ratio-btn" type="button" data-outpaint-ratio="3:4">3:4</button>
<button class="crop-ratio-btn" type="button" data-outpaint-ratio="16:9">16:9</button>
<button class="crop-ratio-btn" type="button" data-outpaint-ratio="9:16">9:16</button>
<button class="crop-ratio-btn" type="button" data-outpaint-ratio="3:2">3:2</button>
<button class="crop-ratio-btn" type="button" data-outpaint-ratio="2:3">2:3</button>
<button class="crop-ratio-btn" type="button" data-outpaint-ratio="21:9">21:9</button>
<button class="crop-ratio-btn" type="button" data-outpaint-ratio="9:21">9:21</button>
</div>
<div id="imageMaskTools" class="image-edit-tools">
<label><span data-i18n="canvas.brushSize">笔刷</span> <input id="maskBrushSize" type="range" min="4" max="160" value="42"></label>
<button id="maskUndoBtn" class="image-edit-btn secondary" type="button" onclick="undoEditDrawing()" title="撤销"><i data-lucide="undo-2" class="w-4 h-4"></i></button>
Expand Down Expand Up @@ -351,6 +365,7 @@
</div>

<script src="/static/js/ltx-director-timeline.js?v=2026.07.15.1782696715"></script>
<script src="/static/js/outpaint-ratios.js"></script>
<script src="/static/js/canvas.js?v=2026.07.15.1784105557"></script>
</body>
</html>
5 changes: 3 additions & 2 deletions static/js/canvas-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -465,10 +465,11 @@ function attachCardDrag(card, c){
function openCanvas(c){
const enc = encodeURIComponent(c.id);
const project = encodeURIComponent(c.project || currentProjectId || 'default');
const version = Date.now();
rememberProjectId(c.project || currentProjectId || 'default');
window.location.href = (c.kind === 'smart')
? `/static/smart-canvas.html?id=${enc}&project=${project}&v=2026.07.03.4`
: `/static/canvas.html?id=${enc}&project=${project}&v=2026.07.03.4`;
? `/static/smart-canvas.html?id=${enc}&project=${project}&v=${version}`
: `/static/canvas.html?id=${enc}&project=${project}&v=${version}`;
}

/* ===== Card create flow ===== */
Expand Down
38 changes: 38 additions & 0 deletions static/js/canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@ let cropState = null;
let cropDrag = null;
let cropAspectPreset = 'free';
let cropAspectRatio = null;
let outpaintAspectPreset = 'free';
let imageEditMode = 'crop';
let imageEditModeTouched = false;
let imageResizeScale = 0.5;
Expand Down Expand Up @@ -2350,6 +2351,12 @@ document.querySelectorAll('[data-crop-ratio]').forEach(btn => {
setCropAspectPreset(btn.dataset.cropRatio || 'free');
});
});
document.querySelectorAll('[data-outpaint-ratio]').forEach(btn => {
btn.addEventListener('click', event => {
event.stopPropagation();
setOutpaintAspectPreset(btn.dataset.outpaintRatio || 'free');
});
});
document.getElementById('outpaintFrame')?.addEventListener('mousedown', event => {
if(event.target.closest('[data-outpaint-handle]')) return;
document.getElementById('cropCanvas')?.classList.add('dragging-image');
Expand Down Expand Up @@ -4514,6 +4521,7 @@ function setImageEditMode(mode, userTouched=false){
_syncGridCustomCursor();
document.querySelectorAll('[data-image-edit-mode]').forEach(btn => btn.classList.toggle('active', btn.dataset.imageEditMode === imageEditMode));
document.getElementById('imageCropTools')?.classList.toggle('active', imageEditMode === 'crop');
document.getElementById('imageOutpaintTools')?.classList.toggle('active', imageEditMode === 'outpaint');
document.getElementById('imageMaskTools').classList.toggle('active', imageEditMode === 'mask');
document.getElementById('imageBrushTools').classList.toggle('active', imageEditMode === 'brush');
document.getElementById('imageResizeTools')?.classList.toggle('active', imageEditMode === 'resize');
Expand Down Expand Up @@ -5247,6 +5255,28 @@ function resetOutpaintBox(){
cropState.h = h;
renderCropBox();
}
function syncOutpaintRatioButtons(){
document.querySelectorAll('[data-outpaint-ratio]').forEach(btn => {
btn.classList.toggle('active', btn.dataset.outpaintRatio === outpaintAspectPreset);
});
}
function setOutpaintAspectPreset(preset='free'){
outpaintAspectPreset = preset || 'free';
syncOutpaintRatioButtons();
if(!cropState || imageEditMode !== 'outpaint' || outpaintAspectPreset === 'free') return;
if(outpaintAspectPreset === 'source') return resetOutpaintBox();
const img = document.getElementById('cropImage');
const fit = window.OutpaintRatios?.fitContaining(img?.naturalWidth, img?.naturalHeight, outpaintAspectPreset);
if(!img || !fit) return;
const scaleX = Math.max(1, Number(img.naturalWidth || 1)) / Math.max(1, Number(img.clientWidth || 1));
const scaleY = Math.max(1, Number(img.naturalHeight || 1)) / Math.max(1, Number(img.clientHeight || 1));
cropState.w = fit.width / scaleX;
cropState.h = fit.height / scaleY;
cropState.x = fit.offsetX / scaleX;
cropState.y = fit.offsetY / scaleY;
clampOutpaint();
renderCropBox();
}
function cropRatioFromPreset(preset){
if(!preset || preset === 'free') return null;
if(preset === 'source'){
Expand Down Expand Up @@ -5324,7 +5354,9 @@ function openImageEditor(nodeId, initialMode='crop'){
imageEditModeTouched = false;
cropAspectPreset = 'free';
cropAspectRatio = null;
outpaintAspectPreset = 'free';
syncCropRatioButtons();
syncOutpaintRatioButtons();
editTextItems = [];
editTextSelectedId = '';
editTextDrag = null;
Expand Down Expand Up @@ -5404,7 +5436,9 @@ function closeImageEditor(){
imageEditModeTouched = false;
cropAspectPreset = 'free';
cropAspectRatio = null;
outpaintAspectPreset = 'free';
syncCropRatioButtons();
syncOutpaintRatioButtons();
document.getElementById('imageEditStage')?.classList.remove('overflowing', 'overflow-x', 'overflow-y');
const cropCanvasEl = document.getElementById('cropCanvas');
cropCanvasEl.classList.remove('grid-custom-h', 'grid-custom-v', 'outpaint-mode', 'outpaint-warning', 'dragging-image', 'text-mode', 'resize-mode');
Expand All @@ -5430,6 +5464,10 @@ function beginCropDrag(event, mode){
event.preventDefault();
event.stopPropagation();
if(imageEditMode === 'outpaint' && mode === 'move') return;
if(imageEditMode === 'outpaint' && String(mode || '').startsWith('outpaint-') && outpaintAspectPreset !== 'free'){
outpaintAspectPreset = 'free';
syncOutpaintRatioButtons();
}
cropDrag = {mode, sx:event.clientX, sy:event.clientY, start:{...cropState}};
}
function resizeOutpaintFromDrag(dx, dy){
Expand Down
4 changes: 3 additions & 1 deletion static/js/i18n.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
(function(){
const VERSION = '2026.07.04.rec-ui.1';
// Translation bundles change independently from this loader. A fresh token
// prevents an older cached dictionary from leaving new UI keys untranslated.
const VERSION = Date.now();
const scripts = [
'/static/js/i18n-core.js',
'/static/js/i18n/common.js',
Expand Down
5 changes: 4 additions & 1 deletion static/js/i18n/canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@
"canvas.editImageSub": { zh: "选择裁剪、遮罩或画笔模式", en: "Select crop, mask, brush, or grid mode" },
"canvas.modeCrop": { zh: "裁剪", en: "Crop" },
"canvas.modeOutpaint": { zh: "扩图", en: "Expand" },
"canvas.outpaintRatio": { zh: "扩图比例", en: "Expand ratio" },
"canvas.ratioFree": { zh: "自由", en: "Free" },
"canvas.ratioSource": { zh: "原图", en: "Source" },
"canvas.modeMask": { zh: "遮罩", en: "Mask" },
"canvas.modeBrush": { zh: "画笔", en: "Brush" },
"canvas.modeGrid": { zh: "宫格切分", en: "Grid Split" },
Expand All @@ -150,7 +153,7 @@
"canvas.applyBrush": { zh: "应用画笔", en: "Apply Brush" },
"canvas.applyOutpaint": { zh: "应用扩图", en: "Apply Expand" },
"canvas.outpaintImage": { zh: "扩图", en: "Expand Image" },
"canvas.outpaintHint": { zh: "拖动红框调整画布范围,最大不超过 4K 尺寸,确认后空白区域填充白色", en: "Drag the red box to extend the canvas. It is capped at 4K size, and empty areas are filled white." },
"canvas.outpaintHint": { zh: "选择常用比例或拖动边框扩展画布;适用于 Banana 2、Banana Pro 与 Image 2,空白区域会交给模型补全", en: "Choose a common ratio or drag the frame to expand; works with Banana 2, Banana Pro, and Image 2, which will fill the blank area." },
"canvas.applyGrid": { zh: "切分到 Output", en: "Split to Output" },
"canvas.gridHint": { zh: "按切分线分割图片,间隔会从线两侧扣除", en: "Split image along cut lines. Gap removes pixels on each side of every line." },
"canvas.gridCustom": { zh: "自定义", en: "Custom" },
Expand Down
46 changes: 46 additions & 0 deletions static/js/outpaint-ratios.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
(function(root, factory){
const api = factory();
if(typeof module !== 'undefined' && module.exports) module.exports = api;
if(root) root.OutpaintRatios = api;
})(typeof globalThis !== 'undefined' ? globalThis : this, function(){
const PRESETS = ['free', 'source', '1:1', '4:3', '3:4', '16:9', '9:16', '3:2', '2:3', '21:9', '9:21'];

function gcd(a, b){
a = Math.abs(Math.round(Number(a) || 0));
b = Math.abs(Math.round(Number(b) || 0));
while(b){ const next = a % b; a = b; b = next; }
return a || 1;
}

function parsePreset(preset){
const value = String(preset || '').trim();
if(value === 'free' || value === 'source') return null;
const match = value.match(/^(\d+)\s*:\s*(\d+)$/);
if(!match) return null;
const width = Number(match[1]);
const height = Number(match[2]);
if(width <= 0 || height <= 0) return null;
const divisor = gcd(width, height);
return {width:width / divisor, height:height / divisor, ratio:width / height};
}

function fitContaining(sourceWidth, sourceHeight, preset){
const width = Math.max(1, Math.round(Number(sourceWidth) || 1));
const height = Math.max(1, Math.round(Number(sourceHeight) || 1));
const parsed = parsePreset(preset);
if(!parsed) return {width, height, offsetX:0, offsetY:0, preset:preset || 'free'};
const rawUnits = Math.max(1, Math.ceil(Math.max(width / parsed.width, height / parsed.height)));
const units = Math.ceil(rawUnits / 16) * 16;
const targetWidth = units * parsed.width;
const targetHeight = units * parsed.height;
return {
width:targetWidth,
height:targetHeight,
offsetX:Math.floor((targetWidth - width) / 2),
offsetY:Math.floor((targetHeight - height) / 2),
preset:String(preset),
};
}

return {PRESETS, parsePreset, fitContaining};
});
37 changes: 37 additions & 0 deletions static/js/smart-canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ let cropState = null;
let cropDrag = null;
let cropAspectPreset = 'free';
let cropAspectRatio = null;
let outpaintAspectPreset = 'free';
let imageEditMode = 'crop';
let imageEditModeTouched = false;
let imageResizeScale = 0.5;
Expand Down Expand Up @@ -9051,6 +9052,7 @@ function setImageEditMode(mode, userTouched=false){
document.querySelectorAll('[data-image-edit-mode]').forEach(btn => btn.classList.toggle('active', btn.dataset.imageEditMode === imageEditMode));
document.getElementById('imagePreviewTools').classList.toggle('active', isPreview && !isVideoPreview);
document.getElementById('imageCropTools')?.classList.toggle('active', imageEditMode === 'crop');
document.getElementById('imageOutpaintTools')?.classList.toggle('active', imageEditMode === 'outpaint');
document.getElementById('imageMaskTools').classList.toggle('active', imageEditMode === 'mask');
document.getElementById('imageBrushTools').classList.toggle('active', imageEditMode === 'brush');
document.getElementById('imageResizeTools')?.classList.toggle('active', imageEditMode === 'resize');
Expand Down Expand Up @@ -10693,6 +10695,29 @@ function resetOutpaintBox(){
clampOutpaint();
renderCropBox();
}
function syncOutpaintRatioButtons(){
document.querySelectorAll('[data-outpaint-ratio]').forEach(btn => {
btn.classList.toggle('active', btn.dataset.outpaintRatio === outpaintAspectPreset);
});
}
function setOutpaintAspectPreset(preset='free'){
outpaintAspectPreset = preset || 'free';
syncOutpaintRatioButtons();
if(!cropState || imageEditMode !== 'outpaint' || outpaintAspectPreset === 'free') return;
if(outpaintAspectPreset === 'source') return resetOutpaintBox();
const img = document.getElementById('cropImage');
const fit = window.OutpaintRatios?.fitContaining(img?.naturalWidth, img?.naturalHeight, outpaintAspectPreset);
if(!img || !fit) return;
const display = cropImageDisplaySize();
const scaleX = Math.max(1, Number(img.naturalWidth || 1)) / Math.max(1, Number(display.w || img.clientWidth || 1));
const scaleY = Math.max(1, Number(img.naturalHeight || 1)) / Math.max(1, Number(display.h || img.clientHeight || 1));
cropState.w = fit.width / scaleX;
cropState.h = fit.height / scaleY;
cropState.x = fit.offsetX / scaleX;
cropState.y = fit.offsetY / scaleY;
clampOutpaint();
renderCropBox();
}
function cropRatioFromPreset(preset){
if(!preset || preset === 'free') return null;
if(preset === 'source'){
Expand Down Expand Up @@ -10832,6 +10857,7 @@ function openImageEditor(nodeId, imageIndex=0){
gridOperationMode = 'split'; gridJoinLayout = null; gridJoinDrag = null; gridJoinImageCache = new Map(); gridJoinUserMoved = false; gridJoinGroupId = '';
imageEditZoom = 1.0; imageEditBaseW = 0; imageEditBaseH = 0; imageResizeScale = 0.5; imageEditModeTouched = false;
cropAspectPreset = 'free'; cropAspectRatio = null; syncCropRatioButtons();
outpaintAspectPreset = 'free'; syncOutpaintRatioButtons();
editTextItems = []; editTextSelectedId = ''; editTextDrag = null; editTextDirty = false;
const toggle = document.getElementById('gridCustomToggle');
if(toggle){ toggle.classList.add('secondary'); toggle.classList.remove('primary'); }
Expand Down Expand Up @@ -10933,6 +10959,7 @@ function closeImageEditor(){
previewNavState = {nodeId:'', index:0, count:0};
imageEditZoom = 1.0; imageEditBaseW = 0; imageEditBaseH = 0; imageResizeScale = 0.5; imageEditModeTouched = false;
cropAspectPreset = 'free'; cropAspectRatio = null; syncCropRatioButtons();
outpaintAspectPreset = 'free'; syncOutpaintRatioButtons();
disposePanoramaPreview();
previewPanDrag = null; previewCompareDrag = false; imageEditPanDrag = null; resetPreviewTransform();
document.getElementById('imageEditStage')?.classList.remove('overflow-x', 'overflow-y', 'preview-mode');
Expand All @@ -10958,6 +10985,10 @@ function beginCropDrag(event, mode){
if(!cropState) return;
event.preventDefault(); event.stopPropagation();
if(imageEditMode === 'outpaint' && mode === 'move') return;
if(imageEditMode === 'outpaint' && String(mode || '').startsWith('outpaint-') && outpaintAspectPreset !== 'free'){
outpaintAspectPreset = 'free';
syncOutpaintRatioButtons();
}
cropDrag = {mode, sx:event.clientX, sy:event.clientY, start:{...cropState}};
}
function resizeOutpaintFromDrag(dx, dy){
Expand Down Expand Up @@ -16769,6 +16800,12 @@ document.querySelectorAll('[data-crop-ratio]').forEach(btn => {
setCropAspectPreset(btn.dataset.cropRatio || 'free');
});
});
document.querySelectorAll('[data-outpaint-ratio]').forEach(btn => {
btn.addEventListener('click', event => {
event.stopPropagation();
setOutpaintAspectPreset(btn.dataset.outpaintRatio || 'free');
});
});
document.getElementById('outpaintFrame').addEventListener('mousedown', event => {
if(event.target.closest('[data-outpaint-handle]')) return;
beginCropDrag(event, 'image');
Expand Down
15 changes: 15 additions & 0 deletions static/smart-canvas.html
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,20 @@
<button class="crop-ratio-btn" type="button" data-crop-ratio="3:2">3:2</button>
<button class="crop-ratio-btn" type="button" data-crop-ratio="2:3">2:3</button>
</div>
<div id="imageOutpaintTools" class="image-edit-tools crop-ratio-tools">
<span class="image-edit-sub" data-i18n="canvas.outpaintRatio">扩图比例</span>
<button class="crop-ratio-btn active" type="button" data-outpaint-ratio="free" data-i18n="canvas.ratioFree">自由</button>
<button class="crop-ratio-btn" type="button" data-outpaint-ratio="source" data-i18n="canvas.ratioSource">原图</button>
<button class="crop-ratio-btn" type="button" data-outpaint-ratio="1:1">1:1</button>
<button class="crop-ratio-btn" type="button" data-outpaint-ratio="4:3">4:3</button>
<button class="crop-ratio-btn" type="button" data-outpaint-ratio="3:4">3:4</button>
<button class="crop-ratio-btn" type="button" data-outpaint-ratio="16:9">16:9</button>
<button class="crop-ratio-btn" type="button" data-outpaint-ratio="9:16">9:16</button>
<button class="crop-ratio-btn" type="button" data-outpaint-ratio="3:2">3:2</button>
<button class="crop-ratio-btn" type="button" data-outpaint-ratio="2:3">2:3</button>
<button class="crop-ratio-btn" type="button" data-outpaint-ratio="21:9">21:9</button>
<button class="crop-ratio-btn" type="button" data-outpaint-ratio="9:21">9:21</button>
</div>
<div id="imagePreviewTools" class="image-edit-tools">
<span id="previewMetaHint" class="image-edit-sub" style="margin-left:auto"></span>
<div id="panoramaControls" class="panorama-controls" style="display:none">
Expand Down Expand Up @@ -437,6 +451,7 @@
<div id="toast" class="toast"></div>
<div id="mentionPreview" class="mention-preview"><img alt="preview"></div>
</div>
<script src="/static/js/outpaint-ratios.js"></script>
<script src="/static/js/smart-canvas.js?v=2026.07.15.1784105576"></script>
</body>
</html>
Loading