mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:44:23 +08:00
This commit is contained in:
1 parent
cfcf1939d1
commit
1a90df1d83
6 files changed
+481
-30
No files matched your search
@@ -89,6 +89,18 @@
|
||||
<button id="insertImageButton" class="button" type="button" aria-label="添加图片">图片</button>
|
||||
<span>插入</span>
|
||||
</div>
|
||||
<div class="editor-group">
|
||||
<button id="drawLineButton" class="button" type="button" aria-label="绘制直线" aria-pressed="false">直线</button>
|
||||
<button id="drawRectangleButton" class="button" type="button" aria-label="绘制矩形" aria-pressed="false">矩形</button>
|
||||
<button id="drawEllipseButton" class="button" type="button" aria-label="绘制椭圆" aria-pressed="false">椭圆</button>
|
||||
<span>绘制</span>
|
||||
</div>
|
||||
<div class="editor-group">
|
||||
<div class="shape-paint"><label><input id="shapeFill" type="checkbox">填充</label><input id="shapeFillColor" class="color-input" type="color" value="#dcefeb" aria-label="填充颜色" title="填充颜色"></div>
|
||||
<div class="shape-paint"><label><input id="shapeStroke" type="checkbox" checked>描边</label><input id="shapeStrokeColor" class="color-input" type="color" value="#000000" aria-label="描边颜色" title="描边颜色"></div>
|
||||
<div class="form-unit text-size"><input id="shapeWidth" type="number" value="1" min="0" step="any" required aria-label="线宽" title="线宽"><span>pt</span></div>
|
||||
<span>图形</span>
|
||||
</div>
|
||||
<div class="editor-group">
|
||||
<button id="addPageButton" class="button" type="button" aria-label="新增页面">新增</button>
|
||||
<button id="copyPageButton" class="button" type="button" aria-label="复制页面">复制</button>
|
||||
|
||||
@@ -87,6 +87,8 @@ a:focus-visible {
|
||||
.editor-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
max-height: 40dvh;
|
||||
overflow: auto;
|
||||
gap: 12px;
|
||||
padding: 8px 12px 6px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
@@ -120,6 +122,19 @@ a:focus-visible {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.shape-paint,
|
||||
.shape-paint label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.shape-paint input[type="checkbox"] {
|
||||
margin: 0;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
.editor-group:has(#addPageButton) {
|
||||
grid-template-columns: repeat(6, max-content);
|
||||
}
|
||||
@@ -1162,6 +1177,30 @@ button.outline-link:hover {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.editing-draw .page-surface {
|
||||
cursor: crosshair;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.editing-draw .edit-layer,
|
||||
.editing-draw .page-surface > :not(.edit-preview) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.editing-draw .edit-object {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.edit-preview {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.edit-object {
|
||||
position: absolute;
|
||||
pointer-events: auto;
|
||||
@@ -1175,6 +1214,12 @@ button.outline-link:hover {
|
||||
outline: calc(1px / var(--surface-scale)) solid var(--accent);
|
||||
}
|
||||
|
||||
.edit-line::before {
|
||||
position: absolute;
|
||||
inset: calc(-4px / var(--surface-scale));
|
||||
content: "";
|
||||
}
|
||||
|
||||
.edit-object.selected {
|
||||
background: rgba(15, 123, 104, 0.04);
|
||||
}
|
||||
|
||||
+121
-24
@@ -94,6 +94,14 @@ const el = {
|
||||
textFont: document.querySelector("#textFont"),
|
||||
textSize: document.querySelector("#textSize"),
|
||||
textColor: document.querySelector("#textColor"),
|
||||
drawLineButton: document.querySelector("#drawLineButton"),
|
||||
drawRectangleButton: document.querySelector("#drawRectangleButton"),
|
||||
drawEllipseButton: document.querySelector("#drawEllipseButton"),
|
||||
shapeFill: document.querySelector("#shapeFill"),
|
||||
shapeFillColor: document.querySelector("#shapeFillColor"),
|
||||
shapeStroke: document.querySelector("#shapeStroke"),
|
||||
shapeStrokeColor: document.querySelector("#shapeStrokeColor"),
|
||||
shapeWidth: document.querySelector("#shapeWidth"),
|
||||
undoButton: document.querySelector("#undoButton"),
|
||||
redoButton: document.querySelector("#redoButton"),
|
||||
insertTextButton: document.querySelector("#insertTextButton"),
|
||||
@@ -231,6 +239,10 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
||||
change.x + item.x * (1 - change.scale), change.y + item.y * (1 - change.scale), change.scale),
|
||||
onDelete: (item) => changeDocument("ofdgoDeleteObject", item),
|
||||
onEdit: editCanvasObject,
|
||||
drawStyle: shapeStyle,
|
||||
onTool: updateDrawingControls,
|
||||
onDraw: (index, shape, box, style) => changeDocument("ofdgoInsertShape", null, index, shape,
|
||||
box.x, box.y, box.width, box.height, style.fill, style.fillColor, style.stroke, style.strokeColor, style.lineWidth),
|
||||
onCommitText: (item, value) => changeDocument("ofdgoUpdateText", item, value, null, item.size, null),
|
||||
onTextChange: () => {
|
||||
const editing = canvasEditor.input;
|
||||
@@ -246,7 +258,8 @@ el.selectObjectButton.addEventListener("click", () => {
|
||||
if (document.body.hasAttribute("aria-busy")) {
|
||||
return;
|
||||
}
|
||||
state.selectObjects = !state.selectObjects || state.panMode;
|
||||
state.selectObjects = !state.selectObjects || state.panMode || Boolean(canvasEditor.tool);
|
||||
canvasEditor.setTool("");
|
||||
if (state.selectObjects && state.panMode) {
|
||||
togglePan();
|
||||
}
|
||||
@@ -259,7 +272,7 @@ el.deleteObjectButton.addEventListener("click", () => {
|
||||
});
|
||||
el.editObjectButton.addEventListener("click", () => {
|
||||
if (canvasEditor.selected) {
|
||||
openInsertPanel(!canvasEditor.selected.image, canvasEditor.selected);
|
||||
openInsertPanel(canvasEditor.selected.type === "TextObject", canvasEditor.selected);
|
||||
}
|
||||
});
|
||||
el.textSize.addEventListener("change", () => changeTextStyle(false));
|
||||
@@ -267,19 +280,25 @@ el.textColor.addEventListener("change", () => changeTextStyle(true));
|
||||
el.textFont.addEventListener("change", () => {
|
||||
const item = canvasEditor.selected;
|
||||
const font = state.textFonts[Number(el.textFont.value)];
|
||||
if (item && !item.image && state.ready && !document.body.hasAttribute("aria-busy") && font && !font.embedded) {
|
||||
if (item?.type === "TextObject" && state.ready && !document.body.hasAttribute("aria-busy") && font && !font.embedded) {
|
||||
return changeDocument("ofdgoUpdateText", item, item.text, fontManager.read(font), item.size, null);
|
||||
}
|
||||
});
|
||||
el.textSize.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter" || event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
if (event.key === "Escape") {
|
||||
updateObjectControls(canvasEditor.selected, true);
|
||||
el.shapeWidth.addEventListener("focus", () => { el.shapeWidth.defaultValue = el.shapeWidth.value; });
|
||||
for (const input of [el.textSize, el.shapeWidth]) {
|
||||
input.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter" || event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
if (event.key === "Escape") {
|
||||
updateObjectControls(canvasEditor.selected, true);
|
||||
if (input === el.shapeWidth && canvasEditor.selected?.type !== "PathObject") {
|
||||
input.value = input.defaultValue;
|
||||
}
|
||||
}
|
||||
input.blur();
|
||||
}
|
||||
el.textSize.blur();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
el.copyObjectButton.addEventListener("click", () => {
|
||||
const item = canvasEditor.selected;
|
||||
if (!item) {
|
||||
@@ -310,6 +329,21 @@ el.objectAlign.addEventListener("change", () => {
|
||||
});
|
||||
el.undoButton.addEventListener("click", () => changeDocument("ofdgoUndo"));
|
||||
el.redoButton.addEventListener("click", () => changeDocument("ofdgoRedo"));
|
||||
for (const [button, tool] of [[el.drawLineButton, "line"], [el.drawRectangleButton, "rectangle"], [el.drawEllipseButton, "ellipse"]]) {
|
||||
button.addEventListener("click", () => {
|
||||
if (!state.editing || document.body.hasAttribute("aria-busy")) {
|
||||
return;
|
||||
}
|
||||
const next = canvasEditor.tool === tool ? "" : tool;
|
||||
state.selectObjects = true;
|
||||
setPan(false);
|
||||
canvasEditor.setTool(next);
|
||||
el.viewerPanel.focus({ preventScroll: true });
|
||||
});
|
||||
}
|
||||
for (const input of [el.shapeFill, el.shapeFillColor, el.shapeStroke, el.shapeStrokeColor, el.shapeWidth]) {
|
||||
input.addEventListener("change", changeShapeStyle);
|
||||
}
|
||||
el.addPageButton.addEventListener("click", () => {
|
||||
const page = currentPageInfo();
|
||||
return changeDocument("ofdgoChangePage", null, "add", state.pageIndex, page.width, page.height);
|
||||
@@ -651,7 +685,11 @@ async function createDocument(event) {
|
||||
}
|
||||
|
||||
async function editCanvasObject(item) {
|
||||
if (item.image) {
|
||||
if (item.type === "PathObject") {
|
||||
el.shapeWidth.focus();
|
||||
return;
|
||||
}
|
||||
if (item.type === "ImageObject") {
|
||||
return openInsertPanel(false, item);
|
||||
}
|
||||
if (!state.editing || document.body.hasAttribute("aria-busy") || canvasEditor.input) {
|
||||
@@ -709,7 +747,7 @@ async function openInsertPanel(text, item = null) {
|
||||
|
||||
function updateInsertFonts() {
|
||||
const selected = state.insertFonts[Number(el.insertFont.value)];
|
||||
state.insertFonts = editorFonts(state.insertObject?.image ? null : state.insertObject);
|
||||
state.insertFonts = editorFonts(state.insertObject?.type === "TextObject" ? state.insertObject : null);
|
||||
setFontOptions(el.insertFont, state.insertFonts, selected);
|
||||
el.insertSubmit.disabled = !el.insertTextRow.hidden && !state.insertFonts.length;
|
||||
el.insertStatus.textContent = el.insertSubmit.disabled ? "尚未添加字体" : "";
|
||||
@@ -736,7 +774,7 @@ function setFontOptions(select, fonts, selected) {
|
||||
}
|
||||
|
||||
function updateTextFonts(item, refresh = false) {
|
||||
const text = item && !item.image;
|
||||
const text = item?.type === "TextObject";
|
||||
if (refresh || state.textFonts[0]?.id !== (text ? `embedded:${item.font}` : undefined)) {
|
||||
state.textFonts = text ? editorFonts(item) : [];
|
||||
setFontOptions(el.textFont, state.textFonts, state.textFonts[0]);
|
||||
@@ -815,7 +853,7 @@ function textPoints(size) {
|
||||
|
||||
async function changeTextStyle(color) {
|
||||
const item = canvasEditor.selected;
|
||||
if (!item || item.image || !state.ready || state.exporting) {
|
||||
if (item?.type !== "TextObject" || !state.ready || state.exporting) {
|
||||
return;
|
||||
}
|
||||
if (!color && !el.textSize.checkValidity()) {
|
||||
@@ -831,6 +869,55 @@ async function changeTextStyle(color) {
|
||||
updateObjectControls(canvasEditor.selected, true);
|
||||
}
|
||||
|
||||
function shapeStyle() {
|
||||
return { fill: el.shapeFill.checked, fillColor: el.shapeFillColor.value,
|
||||
stroke: el.shapeStroke.checked, strokeColor: el.shapeStrokeColor.value,
|
||||
lineWidth: Number(el.shapeWidth.value) * 25.4 / 72 };
|
||||
}
|
||||
|
||||
function strokePoints(width) {
|
||||
return Number((width * 72 / 25.4).toPrecision(6));
|
||||
}
|
||||
|
||||
async function changeShapeStyle() {
|
||||
const item = canvasEditor.selected;
|
||||
if (!state.editing || document.body.hasAttribute("aria-busy")) {
|
||||
return;
|
||||
}
|
||||
if (!el.shapeWidth.checkValidity() || Number(el.shapeWidth.value) <= 0) {
|
||||
el.shapeWidth.value = String(item?.type === "PathObject" ? strokePoints(item.lineWidth) : 1);
|
||||
return;
|
||||
}
|
||||
if (!el.shapeFill.checked && !el.shapeStroke.checked) {
|
||||
el.shapeStroke.checked = true;
|
||||
}
|
||||
const style = shapeStyle();
|
||||
if (item?.type === "PathObject") {
|
||||
await changeDocument("ofdgoUpdatePathStyle", item, style.fill, style.fillColor, style.stroke, style.strokeColor,
|
||||
Number(el.shapeWidth.value) === strokePoints(item.lineWidth) ? item.lineWidth : style.lineWidth);
|
||||
updateObjectControls(canvasEditor.selected, true);
|
||||
}
|
||||
updateDrawingControls();
|
||||
}
|
||||
|
||||
function updateDrawingControls() {
|
||||
const tool = canvasEditor.tool;
|
||||
const line = tool === "line" || !tool && canvasEditor.selected?.line;
|
||||
if (line) {
|
||||
el.shapeFill.checked = false;
|
||||
el.shapeStroke.checked = true;
|
||||
}
|
||||
const disabled = !state.editing || !state.ready || state.exporting;
|
||||
for (const [button, name] of [[el.drawLineButton, "line"], [el.drawRectangleButton, "rectangle"], [el.drawEllipseButton, "ellipse"]]) {
|
||||
button.disabled = disabled;
|
||||
button.setAttribute("aria-pressed", String(tool === name));
|
||||
}
|
||||
el.selectObjectButton.setAttribute("aria-pressed", String(canvasEditor.enabled && !tool));
|
||||
el.shapeFill.disabled = el.shapeStroke.disabled = disabled || line;
|
||||
el.shapeFillColor.disabled = disabled || line || !el.shapeFill.checked;
|
||||
el.shapeStrokeColor.disabled = el.shapeWidth.disabled = disabled || !line && !el.shapeStroke.checked;
|
||||
}
|
||||
|
||||
async function changeDocument(name, item, ...args) {
|
||||
if (!state.editing || document.body.hasAttribute("aria-busy")) {
|
||||
return;
|
||||
@@ -855,12 +942,12 @@ async function changeDocument(name, item, ...args) {
|
||||
if (!item || name === "ofdgoDeleteObject") {
|
||||
canvasEditor.clear();
|
||||
}
|
||||
if (name === "ofdgoCopyObject") {
|
||||
if (name === "ofdgoCopyObject" || name === "ofdgoInsertShape") {
|
||||
canvasEditor.selectLast = true;
|
||||
}
|
||||
openSeq = ++state.openSeq;
|
||||
if (item) {
|
||||
await refreshEditorPage(doc, item.index, openSeq);
|
||||
if (item || name === "ofdgoInsertShape") {
|
||||
await refreshEditorPage(doc, item ? item.index : args[0], openSeq);
|
||||
} else {
|
||||
const samePage = doc.pages.findIndex((page) => page.id === previous.id);
|
||||
const pageIndex = doc.pageIndex ?? (samePage < 0 ? Math.min(state.pageIndex, doc.pageCount - 1) : samePage);
|
||||
@@ -2700,6 +2787,7 @@ function layoutPages() {
|
||||
if (!state.doc) {
|
||||
return;
|
||||
}
|
||||
canvasEditor.cancel();
|
||||
for (const page of state.doc.pages || []) {
|
||||
const shell = pageShell(page.index);
|
||||
if (shell) {
|
||||
@@ -3657,7 +3745,6 @@ function rotatePages() {
|
||||
if (document.body.hasAttribute("aria-busy")) {
|
||||
return;
|
||||
}
|
||||
canvasEditor.cancel();
|
||||
state.rotation = (state.rotation + 90) % 360;
|
||||
clearRegionHighlights();
|
||||
el.viewerPanel.classList.remove("single-page-fits-height");
|
||||
@@ -3938,7 +4025,6 @@ function updateEditorTools() {
|
||||
el.movePageNextButton.disabled = pagesDisabled || state.pageIndex === state.doc?.pageCount - 1;
|
||||
const enabled = state.editing && state.selectObjects && !state.panMode;
|
||||
canvasEditor.setEnabled(enabled);
|
||||
el.selectObjectButton.setAttribute("aria-pressed", String(enabled));
|
||||
updateObjectControls(canvasEditor.selected);
|
||||
el.undoButton.disabled = !state.editorInfo?.canUndo || !state.ready || state.exporting;
|
||||
el.redoButton.disabled = !state.editorInfo?.canRedo || !state.ready || state.exporting;
|
||||
@@ -3946,15 +4032,26 @@ function updateEditorTools() {
|
||||
|
||||
function updateObjectControls(item, reset = false) {
|
||||
const disabled = !item || !state.ready || state.exporting;
|
||||
el.deleteObjectButton.disabled = el.copyObjectButton.disabled = el.editObjectButton.disabled = el.objectAlign.disabled = disabled;
|
||||
el.textFont.disabled = el.textSize.disabled = el.textColor.disabled = disabled || item.image;
|
||||
el.deleteObjectButton.disabled = el.copyObjectButton.disabled = el.objectAlign.disabled = disabled;
|
||||
el.editObjectButton.disabled = disabled || item.type === "PathObject";
|
||||
el.textFont.disabled = el.textSize.disabled = el.textColor.disabled = disabled || item.type !== "TextObject";
|
||||
updateTextFonts(item);
|
||||
if (reset || el.textSize.disabled || document.activeElement !== el.textSize) {
|
||||
el.textSize.value = item && !item.image ? textPoints(item.size) : "";
|
||||
el.textSize.value = item?.type === "TextObject" ? textPoints(item.size) : "";
|
||||
}
|
||||
if (reset || el.textColor.disabled || document.activeElement !== el.textColor) {
|
||||
el.textColor.value = item && !item.image ? item.color : "#000000";
|
||||
el.textColor.value = item?.type === "TextObject" ? item.color : "#000000";
|
||||
}
|
||||
if (item?.type === "PathObject") {
|
||||
el.shapeFill.checked = item.fill;
|
||||
el.shapeStroke.checked = item.stroke;
|
||||
el.shapeFillColor.value = item.fillColor;
|
||||
el.shapeStrokeColor.value = item.strokeColor;
|
||||
if (reset || document.activeElement !== el.shapeWidth) {
|
||||
el.shapeWidth.value = strokePoints(item.lineWidth);
|
||||
}
|
||||
}
|
||||
updateDrawingControls();
|
||||
el.objectOrder.disabled = disabled || item.count <= 1;
|
||||
for (const option of el.objectOrder.options) {
|
||||
option.disabled = disabled || (["up", "top"].includes(option.value) ? item.order === item.count - 1 : item.order === 0);
|
||||
|
||||
+102
-3
@@ -24,7 +24,8 @@ export function objectTransform(box, page, dx, dy, corner = "") {
|
||||
(left ? box.x + box.width : page.width - box.x) / box.width,
|
||||
(top ? box.y + box.height : page.height - box.y) / box.height,
|
||||
);
|
||||
const scale = Math.max(Math.min(1 / Math.min(box.width, box.height), limit), Math.min(limit,
|
||||
const minimum = Math.min(1, 1 / (box.line ? Math.max(box.width, box.height) : Math.min(box.width, box.height)));
|
||||
const scale = Math.max(Math.min(minimum, limit), Math.min(limit,
|
||||
1 + (x * box.width + y * box.height) / (box.width ** 2 + box.height ** 2)));
|
||||
return { x: left ? box.width * (1 - scale) : 0, y: top ? box.height * (1 - scale) : 0, scale };
|
||||
}
|
||||
@@ -36,6 +37,8 @@ export class CanvasEditor {
|
||||
this.enabled = false;
|
||||
this.selected = null;
|
||||
this.nodes = new WeakMap();
|
||||
this.pages = new WeakMap();
|
||||
this.tool = "";
|
||||
viewer.addEventListener("pointerdown", (event) => this.start(event));
|
||||
viewer.addEventListener("pointermove", (event) => this.move(event));
|
||||
viewer.addEventListener("pointerup", (event) => this.end(event));
|
||||
@@ -57,19 +60,34 @@ export class CanvasEditor {
|
||||
this.enabled = enabled;
|
||||
this.viewer.classList.toggle("editing-select", enabled);
|
||||
if (!enabled) {
|
||||
this.setTool("");
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
|
||||
setTool(tool) {
|
||||
this.cancel();
|
||||
this.tool = tool;
|
||||
this.viewer.classList.toggle("editing-draw", Boolean(tool));
|
||||
if (tool) {
|
||||
this.select(null);
|
||||
}
|
||||
this.options.onTool?.(tool);
|
||||
}
|
||||
|
||||
mount(index, page, surface) {
|
||||
this.pages.set(surface, { index, width: page.width, height: page.height });
|
||||
const layer = document.createElement("div");
|
||||
layer.className = "edit-layer";
|
||||
for (const object of page.objects) {
|
||||
const node = document.createElement("div");
|
||||
node.className = "edit-object";
|
||||
if (object.line) {
|
||||
node.classList.add("edit-line");
|
||||
}
|
||||
node.tabIndex = 0;
|
||||
node.setAttribute("role", "button");
|
||||
node.setAttribute("aria-label", object.image ? "图片对象" : "文字对象");
|
||||
node.setAttribute("aria-label", { ImageObject: "图片对象", TextObject: "文字对象", PathObject: "图形对象" }[object.type]);
|
||||
const item = { ...object, index, page: { width: page.width, height: page.height }, node, surface };
|
||||
this.nodes.set(node, item);
|
||||
node.addEventListener("focus", () => this.select(item));
|
||||
@@ -120,6 +138,10 @@ export class CanvasEditor {
|
||||
if (!this.enabled || this.options.busy() || event.button !== 0 || this.drag) {
|
||||
return;
|
||||
}
|
||||
if (this.tool) {
|
||||
this.startShape(event);
|
||||
return;
|
||||
}
|
||||
const node = event.target.closest(".edit-object");
|
||||
if (node) {
|
||||
event.preventDefault();
|
||||
@@ -144,6 +166,10 @@ export class CanvasEditor {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
if (drag.shape) {
|
||||
this.moveShape(event, drag);
|
||||
return;
|
||||
}
|
||||
if (!drag.change && Math.hypot(event.clientX - drag.clientX, event.clientY - drag.clientY) < 3) {
|
||||
return;
|
||||
}
|
||||
@@ -159,6 +185,18 @@ export class CanvasEditor {
|
||||
return;
|
||||
}
|
||||
this.move(event);
|
||||
if (drag.shape) {
|
||||
this.drag = null;
|
||||
this.viewer.releasePointerCapture(drag.pointerID);
|
||||
if (!drag.box) {
|
||||
drag.preview.remove();
|
||||
return;
|
||||
}
|
||||
this.setTool("");
|
||||
Promise.resolve(this.options.onDraw(drag.page.index, drag.shape, drag.box, drag.style))
|
||||
.finally(() => drag.preview.remove());
|
||||
return;
|
||||
}
|
||||
this.cancel();
|
||||
if (drag.change && (drag.change.x || drag.change.y || drag.change.scale !== 1)) {
|
||||
this.options.onTransform(drag.item, drag.change);
|
||||
@@ -169,14 +207,75 @@ export class CanvasEditor {
|
||||
const drag = this.drag;
|
||||
this.drag = null;
|
||||
if (drag) {
|
||||
this.place(drag.item);
|
||||
if (drag.shape) {
|
||||
drag.preview.remove();
|
||||
} else {
|
||||
this.place(drag.item);
|
||||
}
|
||||
if (this.viewer.hasPointerCapture(drag.pointerID)) {
|
||||
this.viewer.releasePointerCapture(drag.pointerID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startShape(event) {
|
||||
const surface = event.target.closest(".page-surface");
|
||||
const page = this.pages.get(surface);
|
||||
if (!page) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
this.viewer.focus({ preventScroll: true });
|
||||
const preview = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
preview.classList.add("edit-preview");
|
||||
preview.setAttribute("viewBox", `0 0 ${page.width} ${page.height}`);
|
||||
const shape = this.tool;
|
||||
const node = document.createElementNS("http://www.w3.org/2000/svg", shape === "rectangle" ? "rect" : shape);
|
||||
const style = this.options.drawStyle();
|
||||
if (shape === "line") {
|
||||
style.fill = false;
|
||||
style.stroke = true;
|
||||
}
|
||||
node.setAttribute("fill", style.fill ? style.fillColor : "none");
|
||||
node.setAttribute("stroke", style.stroke ? style.strokeColor : "none");
|
||||
node.setAttribute("stroke-width", style.lineWidth);
|
||||
preview.append(node);
|
||||
surface.append(preview);
|
||||
this.drag = { shape, page, preview, node, style, pointerID: event.pointerId,
|
||||
rect: surface.getBoundingClientRect(), rotation: this.options.rotation(),
|
||||
clientX: event.clientX, clientY: event.clientY };
|
||||
this.viewer.setPointerCapture(event.pointerId);
|
||||
}
|
||||
|
||||
moveShape(event, drag) {
|
||||
const point = (x, y) => {
|
||||
const p = pagePoint(x, y, drag.rect, drag.page, drag.rotation);
|
||||
return { x: Math.max(0, Math.min(p.x, drag.page.width)), y: Math.max(0, Math.min(p.y, drag.page.height)) };
|
||||
};
|
||||
const from = point(drag.clientX, drag.clientY);
|
||||
const to = point(event.clientX, event.clientY);
|
||||
const x = Math.min(from.x, to.x), y = Math.min(from.y, to.y);
|
||||
const width = Math.abs(to.x - from.x), height = Math.abs(to.y - from.y);
|
||||
const valid = Math.hypot(event.clientX - drag.clientX, event.clientY - drag.clientY) >= 3
|
||||
&& (drag.shape === "line" ? width > 0 || height > 0 : width > 0 && height > 0);
|
||||
drag.box = valid ? drag.shape === "line"
|
||||
? { x: from.x, y: from.y, width: to.x - from.x, height: to.y - from.y }
|
||||
: { x, y, width, height } : null;
|
||||
const attributes = drag.shape === "line" ? { x1: from.x, y1: from.y, x2: to.x, y2: to.y }
|
||||
: drag.shape === "ellipse" ? { cx: x + width / 2, cy: y + height / 2, rx: width / 2, ry: height / 2 }
|
||||
: { x, y, width, height };
|
||||
for (const [key, value] of Object.entries(attributes)) {
|
||||
drag.node.setAttribute(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
keyDown(event) {
|
||||
if (event.key === "Escape" && this.tool && !this.options.busy()) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.setTool("");
|
||||
return;
|
||||
}
|
||||
if (this.input || !this.enabled || !this.selected || this.options.busy() || event.ctrlKey || event.metaKey || event.altKey || event.isComposing) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user