mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-17 21:52:21 +02:00
94 lines
3.5 KiB
JavaScript
94 lines
3.5 KiB
JavaScript
/** Direct on-canvas editor for retained text layers. */
|
|
|
|
function textFont(text) {
|
|
const family = text.fontFamily.includes(',') ? text.fontFamily : `"${text.fontFamily.replace(/["\\]/g, '')}", Arial, sans-serif`;
|
|
return `${text.fontStyle} ${text.fontWeight} ${text.fontSize}px/${text.lineHeight} ${family}`;
|
|
}
|
|
|
|
export function createTextEditOverlay({ state, onInput, onCommit, onCancel }) {
|
|
let input = null;
|
|
let layer = null;
|
|
let resizeObserver = null;
|
|
let frame = 0;
|
|
|
|
function position() {
|
|
if (!input || !layer || !state.mainCanvas?.isConnected) return;
|
|
const canvasRect = state.mainCanvas.getBoundingClientRect();
|
|
const offset = state.layerOffsets.get(layer.id) || { x: 0, y: 0 };
|
|
const scaleX = canvasRect.width / Math.max(1, state.mainCanvas.width);
|
|
const scaleY = canvasRect.height / Math.max(1, state.mainCanvas.height);
|
|
const text = layer.text;
|
|
input.style.left = `${canvasRect.left + offset.x * scaleX}px`;
|
|
input.style.top = `${canvasRect.top + offset.y * scaleY}px`;
|
|
input.style.minWidth = `${Math.max(80, text.frameWidth * scaleX)}px`;
|
|
input.style.width = `${Math.max(80, text.frameWidth * scaleX)}px`;
|
|
input.style.minHeight = `${Math.max(36, layer.canvas.height * scaleY)}px`;
|
|
input.style.font = textFont({ ...text, fontSize: text.fontSize * scaleY });
|
|
input.style.letterSpacing = `${text.letterSpacing * scaleX}px`;
|
|
input.style.textAlign = text.align;
|
|
input.style.color = text.color;
|
|
}
|
|
|
|
function loop() {
|
|
position();
|
|
if (input) frame = requestAnimationFrame(loop);
|
|
}
|
|
|
|
function close(commit = true) {
|
|
if (!input) return;
|
|
const value = input.value;
|
|
resizeObserver?.disconnect();
|
|
cancelAnimationFrame(frame);
|
|
input.remove();
|
|
input = null;
|
|
const target = layer;
|
|
layer = null;
|
|
if (commit) onCommit?.(target, value); else onCancel?.(target);
|
|
}
|
|
|
|
function open(target, { selectAll = false } = {}) {
|
|
close(true);
|
|
layer = target;
|
|
input = document.createElement('textarea');
|
|
input.className = 'ge-direct-text-editor';
|
|
input.value = target.text.content;
|
|
input.setAttribute('aria-label', 'Edit text on canvas');
|
|
input.spellcheck = true;
|
|
input.addEventListener('input', () => onInput?.(target, input.value, input));
|
|
input.addEventListener('keydown', event => {
|
|
event.stopPropagation();
|
|
if (event.key === 'Escape') {
|
|
event.preventDefault();
|
|
close(false);
|
|
} else if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
|
|
event.preventDefault();
|
|
close(true);
|
|
}
|
|
});
|
|
let ready = false;
|
|
input.addEventListener('blur', () => setTimeout(() => {
|
|
if (ready && input && document.activeElement !== input) close(true);
|
|
}, 0));
|
|
document.body.appendChild(input);
|
|
resizeObserver = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(() => {
|
|
if (!input || !layer) return;
|
|
const canvasRect = state.mainCanvas.getBoundingClientRect();
|
|
const scale = canvasRect.width / Math.max(1, state.mainCanvas.width);
|
|
const width = Math.max(1, input.getBoundingClientRect().width / Math.max(0.01, scale));
|
|
onInput?.(layer, input.value, input, width);
|
|
}) : null;
|
|
resizeObserver?.observe(input);
|
|
position();
|
|
frame = requestAnimationFrame(() => {
|
|
if (!input) return;
|
|
input.focus({ preventScroll: true });
|
|
if (selectAll) input.select();
|
|
ready = true;
|
|
frame = requestAnimationFrame(loop);
|
|
});
|
|
return input;
|
|
}
|
|
|
|
return { open, close, position, isOpen: () => !!input, activeLayer: () => layer };
|
|
}
|