mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
Fix occlusion drift again (#3443)
* Fix occlusion drift * Fix image editor occasionally not loading fully * Fix occlusion disassociation when browsing * Address oversights * Fix translucent modifier applies to newly created shapes incorrectly * Fix i-text turns yellow upon immediate note change * Fix image occlusion hot keys not disabled when typing * Improve text label creation experience * Remove redundant functions * Fix error when adding occlusion (dae)
This commit is contained in:
parent
59969f62f5
commit
d6aa95950d
15 changed files with 111 additions and 84 deletions
|
@ -1125,7 +1125,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
||||||
self.web.eval("resetIOImageLoaded()")
|
self.web.eval("resetIOImageLoaded()")
|
||||||
|
|
||||||
def update_occlusions_field(self) -> None:
|
def update_occlusions_field(self) -> None:
|
||||||
self.web.eval("updateOcclusionsField()")
|
self.web.eval("saveOcclusions()")
|
||||||
|
|
||||||
def _setup_mask_editor(self, io_options: dict):
|
def _setup_mask_editor(self, io_options: dict):
|
||||||
self.web.eval(
|
self.web.eval(
|
||||||
|
|
|
@ -459,7 +459,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
globalThis.setImageField = setImageField;
|
globalThis.setImageField = setImageField;
|
||||||
|
|
||||||
function updateOcclusionsField(): void {
|
function saveOcclusions(): void {
|
||||||
if (isImageOcclusion && globalThis.canvas) {
|
if (isImageOcclusion && globalThis.canvas) {
|
||||||
const occlusionsData = exportShapesToClozeDeletions($hideAllGuessOne);
|
const occlusionsData = exportShapesToClozeDeletions($hideAllGuessOne);
|
||||||
fieldStores[ioFields.occlusions].set(occlusionsData.clozes);
|
fieldStores[ioFields.occlusions].set(occlusionsData.clozes);
|
||||||
|
@ -572,7 +572,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
triggerChanges,
|
triggerChanges,
|
||||||
setIsImageOcclusion,
|
setIsImageOcclusion,
|
||||||
setupMaskEditor,
|
setupMaskEditor,
|
||||||
updateOcclusionsField,
|
saveOcclusions,
|
||||||
...oldEditorAdapter,
|
...oldEditorAdapter,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -637,7 +637,7 @@ the AddCards dialog) should be implemented in the user of this component.
|
||||||
<div style="display: {$ioMaskEditorVisible ? 'block' : 'none'};">
|
<div style="display: {$ioMaskEditorVisible ? 'block' : 'none'};">
|
||||||
<ImageOcclusionPage
|
<ImageOcclusionPage
|
||||||
mode={imageOcclusionMode}
|
mode={imageOcclusionMode}
|
||||||
on:change={updateOcclusionsField}
|
on:save={saveOcclusions}
|
||||||
on:image-loaded={onImageLoaded}
|
on:image-loaded={onImageLoaded}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -40,7 +40,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div hidden={activeTabValue != 1}>
|
<div hidden={activeTabValue != 1}>
|
||||||
<MasksEditor {mode} on:change on:image-loaded />
|
<MasksEditor {mode} on:save on:image-loaded />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div hidden={activeTabValue != 2}>
|
<div hidden={activeTabValue != 2}>
|
||||||
|
|
|
@ -2,15 +2,6 @@
|
||||||
Copyright: Ankitects Pty Ltd and contributors
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script context="module" lang="ts">
|
|
||||||
import { writable } from "svelte/store";
|
|
||||||
|
|
||||||
const changeSignal = writable(Symbol());
|
|
||||||
|
|
||||||
export function emitChangeSignal() {
|
|
||||||
changeSignal.set(Symbol());
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { fabric } from "fabric";
|
import type { fabric } from "fabric";
|
||||||
|
@ -25,6 +16,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import Toolbar from "./Toolbar.svelte";
|
import Toolbar from "./Toolbar.svelte";
|
||||||
import { MaskEditorAPI } from "./tools/api";
|
import { MaskEditorAPI } from "./tools/api";
|
||||||
import { onResize } from "./tools/tool-zoom";
|
import { onResize } from "./tools/tool-zoom";
|
||||||
|
import { saveNeededStore } from "./store";
|
||||||
|
|
||||||
export let mode: IOMode;
|
export let mode: IOMode;
|
||||||
const iconSize = 80;
|
const iconSize = 80;
|
||||||
|
@ -38,27 +30,29 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
function onChange() {
|
|
||||||
dispatch("change", { canvas });
|
|
||||||
}
|
|
||||||
|
|
||||||
function onImageLoaded({ path, noteId }: ImageLoadedEvent) {
|
function onImageLoaded({ path, noteId }: ImageLoadedEvent) {
|
||||||
dispatch("image-loaded", { path, noteId });
|
dispatch("image-loaded", { path, noteId });
|
||||||
}
|
}
|
||||||
|
|
||||||
$: $changeSignal, onChange();
|
const unsubscribe = saveNeededStore.subscribe((saveNeeded: boolean) => {
|
||||||
|
if (saveNeeded === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch("save");
|
||||||
|
saveNeededStore.set(false);
|
||||||
|
});
|
||||||
|
|
||||||
function init(_node: HTMLDivElement) {
|
function init(_node: HTMLDivElement) {
|
||||||
if (mode.kind == "add") {
|
if (mode.kind == "add") {
|
||||||
setupMaskEditor(mode.imagePath, onChange, onImageLoaded).then((canvas1) => {
|
// Editing occlusions on a new note through the "Add" window
|
||||||
|
setupMaskEditor(mode.imagePath, onImageLoaded).then((canvas1) => {
|
||||||
canvas = canvas1;
|
canvas = canvas1;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setupMaskEditorForEdit(mode.noteId, onChange, onImageLoaded).then(
|
// Editing occlusions on an existing note through the "Browser" window
|
||||||
(canvas1) => {
|
setupMaskEditorForEdit(mode.noteId, onImageLoaded).then((canvas1) => {
|
||||||
canvas = canvas1;
|
canvas = canvas1;
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,10 +62,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
window.removeEventListener("resize", resizeEvent);
|
window.removeEventListener("resize", resizeEvent);
|
||||||
|
unsubscribe();
|
||||||
});
|
});
|
||||||
|
|
||||||
const resizeEvent = () => {
|
const resizeEvent = () => {
|
||||||
onResize(canvas!);
|
if (canvas === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onResize(canvas);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -25,8 +25,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import Shortcut from "$lib/components/Shortcut.svelte";
|
import Shortcut from "$lib/components/Shortcut.svelte";
|
||||||
import WithFloating from "$lib/components/WithFloating.svelte";
|
import WithFloating from "$lib/components/WithFloating.svelte";
|
||||||
|
|
||||||
import { emitChangeSignal } from "./MaskEditor.svelte";
|
import {
|
||||||
import { hideAllGuessOne, ioMaskEditorVisible, textEditingState } from "./store";
|
hideAllGuessOne,
|
||||||
|
ioMaskEditorVisible,
|
||||||
|
textEditingState,
|
||||||
|
saveNeededStore,
|
||||||
|
opacityStateStore,
|
||||||
|
} from "./store";
|
||||||
import { drawEllipse, drawPolygon, drawRectangle, drawText } from "./tools/index";
|
import { drawEllipse, drawPolygon, drawRectangle, drawText } from "./tools/index";
|
||||||
import { makeMaskTransparent } from "./tools/lib";
|
import { makeMaskTransparent } from "./tools/lib";
|
||||||
import { enableSelectable, stopDraw } from "./tools/lib";
|
import { enableSelectable, stopDraw } from "./tools/lib";
|
||||||
|
@ -55,7 +60,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
export let activeTool = "cursor";
|
export let activeTool = "cursor";
|
||||||
let showAlignTools = false;
|
let showAlignTools = false;
|
||||||
let leftPos = 82;
|
let leftPos = 82;
|
||||||
let maksOpacity = false;
|
let maskOpacity = false;
|
||||||
let showFloating = false;
|
let showFloating = false;
|
||||||
const direction = getContext<Readable<"ltr" | "rtl">>(directionKey);
|
const direction = getContext<Readable<"ltr" | "rtl">>(directionKey);
|
||||||
// handle zoom event when mouse scroll and ctrl key are hold for panzoom
|
// handle zoom event when mouse scroll and ctrl key are hold for panzoom
|
||||||
|
@ -158,13 +163,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleToolChanges = (activeTool: string) => {
|
const handleToolChanges = (newActiveTool: string) => {
|
||||||
disableFunctions();
|
disableFunctions();
|
||||||
enableSelectable(canvas, true);
|
enableSelectable(canvas, true);
|
||||||
// remove unfinished polygon when switching to other tools
|
// remove unfinished polygon when switching to other tools
|
||||||
removeUnfinishedPolygon(canvas);
|
removeUnfinishedPolygon(canvas);
|
||||||
|
|
||||||
switch (activeTool) {
|
switch (newActiveTool) {
|
||||||
case "cursor":
|
case "cursor":
|
||||||
drawCursor(canvas);
|
drawCursor(canvas);
|
||||||
break;
|
break;
|
||||||
|
@ -178,9 +183,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
drawPolygon(canvas);
|
drawPolygon(canvas);
|
||||||
break;
|
break;
|
||||||
case "draw-text":
|
case "draw-text":
|
||||||
drawText(canvas);
|
drawText(canvas, () => {
|
||||||
break;
|
activeTool = "cursor";
|
||||||
default:
|
handleToolChanges(activeTool);
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -198,10 +204,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
function changeOcclusionType(occlusionType: "all" | "one"): void {
|
function changeOcclusionType(occlusionType: "all" | "one"): void {
|
||||||
$hideAllGuessOne = occlusionType === "all";
|
$hideAllGuessOne = occlusionType === "all";
|
||||||
emitChangeSignal();
|
saveNeededStore.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
opacityStateStore.set(maskOpacity);
|
||||||
removeHandlers = singleCallback(
|
removeHandlers = singleCallback(
|
||||||
on(document, "click", onClick),
|
on(document, "click", onClick),
|
||||||
on(window, "mousemove", onMousemove),
|
on(window, "mousemove", onMousemove),
|
||||||
|
@ -336,8 +343,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
toggleTranslucentKeyCombination,
|
toggleTranslucentKeyCombination,
|
||||||
)})"
|
)})"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
maksOpacity = !maksOpacity;
|
maskOpacity = !maskOpacity;
|
||||||
makeMaskTransparent(canvas, maksOpacity);
|
makeMaskTransparent(canvas, maskOpacity);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon={mdiEye} />
|
<Icon icon={mdiEye} />
|
||||||
|
@ -346,8 +353,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
<Shortcut
|
<Shortcut
|
||||||
keyCombination={toggleTranslucentKeyCombination}
|
keyCombination={toggleTranslucentKeyCombination}
|
||||||
on:action={() => {
|
on:action={() => {
|
||||||
maksOpacity = !maksOpacity;
|
maskOpacity = !maskOpacity;
|
||||||
makeMaskTransparent(canvas, maksOpacity);
|
makeMaskTransparent(canvas, maskOpacity);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -372,7 +379,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
keyCombination={tool.shortcut}
|
keyCombination={tool.shortcut}
|
||||||
on:action={() => {
|
on:action={() => {
|
||||||
tool.action(canvas);
|
tool.action(canvas);
|
||||||
emitChangeSignal();
|
saveNeededStore.set(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -400,7 +407,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
keyCombination={tool.shortcut}
|
keyCombination={tool.shortcut}
|
||||||
on:action={() => {
|
on:action={() => {
|
||||||
tool.action(canvas);
|
tool.action(canvas);
|
||||||
emitChangeSignal();
|
saveNeededStore.set(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { fabric } from "fabric";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
import { optimumCssSizeForCanvas } from "./canvas-scale";
|
import { optimumCssSizeForCanvas } from "./canvas-scale";
|
||||||
import { notesDataStore, tagsWritable } from "./store";
|
import { notesDataStore, saveNeededStore, tagsWritable, textEditingState } from "./store";
|
||||||
import Toast from "./Toast.svelte";
|
import Toast from "./Toast.svelte";
|
||||||
import { addShapesToCanvasFromCloze } from "./tools/add-from-cloze";
|
import { addShapesToCanvasFromCloze } from "./tools/add-from-cloze";
|
||||||
import { enableSelectable, makeShapesRemainInCanvas, moveShapeToCanvasBoundaries } from "./tools/lib";
|
import { enableSelectable, makeShapesRemainInCanvas, moveShapeToCanvasBoundaries } from "./tools/lib";
|
||||||
|
@ -24,11 +24,10 @@ export interface ImageLoadedEvent {
|
||||||
|
|
||||||
export const setupMaskEditor = async (
|
export const setupMaskEditor = async (
|
||||||
path: string,
|
path: string,
|
||||||
onChange: () => void,
|
|
||||||
onImageLoaded: (event: ImageLoadedEvent) => void,
|
onImageLoaded: (event: ImageLoadedEvent) => void,
|
||||||
): Promise<fabric.Canvas> => {
|
): Promise<fabric.Canvas> => {
|
||||||
const imageData = await getImageForOcclusion({ path });
|
const imageData = await getImageForOcclusion({ path });
|
||||||
const canvas = initCanvas(onChange);
|
const canvas = initCanvas();
|
||||||
|
|
||||||
// get image width and height
|
// get image width and height
|
||||||
const image = document.getElementById("image") as HTMLImageElement;
|
const image = document.getElementById("image") as HTMLImageElement;
|
||||||
|
@ -46,7 +45,6 @@ export const setupMaskEditor = async (
|
||||||
|
|
||||||
export const setupMaskEditorForEdit = async (
|
export const setupMaskEditorForEdit = async (
|
||||||
noteId: number,
|
noteId: number,
|
||||||
onChange: () => void,
|
|
||||||
onImageLoaded: (event: ImageLoadedEvent) => void,
|
onImageLoaded: (event: ImageLoadedEvent) => void,
|
||||||
): Promise<fabric.Canvas> => {
|
): Promise<fabric.Canvas> => {
|
||||||
const clozeNoteResponse = await getImageOcclusionNote({ noteId: BigInt(noteId) });
|
const clozeNoteResponse = await getImageOcclusionNote({ noteId: BigInt(noteId) });
|
||||||
|
@ -63,14 +61,17 @@ export const setupMaskEditorForEdit = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
const clozeNote = clozeNoteResponse.value.value;
|
const clozeNote = clozeNoteResponse.value.value;
|
||||||
const canvas = initCanvas(onChange);
|
const canvas = initCanvas();
|
||||||
|
|
||||||
// get image width and height
|
// get image width and height
|
||||||
const image = document.getElementById("image") as HTMLImageElement;
|
const image = document.getElementById("image") as HTMLImageElement;
|
||||||
image.src = getImageData(clozeNote.imageData!, clozeNote.imageFileName!);
|
image.src = getImageData(clozeNote.imageData!, clozeNote.imageFileName!);
|
||||||
|
|
||||||
image.onload = async function() {
|
image.onload = async function() {
|
||||||
const size = optimumCssSizeForCanvas({ width: image.width, height: image.height }, containerSize());
|
const size = optimumCssSizeForCanvas(
|
||||||
|
{ width: image.naturalWidth, height: image.naturalHeight },
|
||||||
|
containerSize(),
|
||||||
|
);
|
||||||
setCanvasSize(canvas);
|
setCanvasSize(canvas);
|
||||||
const boundingBox = setupBoundingBox(canvas, size);
|
const boundingBox = setupBoundingBox(canvas, size);
|
||||||
addShapesToCanvasFromCloze(canvas, boundingBox, clozeNote.occlusions);
|
addShapesToCanvasFromCloze(canvas, boundingBox, clozeNote.occlusions);
|
||||||
|
@ -85,7 +86,7 @@ export const setupMaskEditorForEdit = async (
|
||||||
return canvas;
|
return canvas;
|
||||||
};
|
};
|
||||||
|
|
||||||
function initCanvas(onChange: () => void): fabric.Canvas {
|
function initCanvas(): fabric.Canvas {
|
||||||
const canvas = new fabric.Canvas("canvas");
|
const canvas = new fabric.Canvas("canvas");
|
||||||
tagsWritable.set([]);
|
tagsWritable.set([]);
|
||||||
globalThis.canvas = canvas;
|
globalThis.canvas = canvas;
|
||||||
|
@ -110,9 +111,18 @@ function initCanvas(onChange: () => void): fabric.Canvas {
|
||||||
modifiedPolygon(canvas, evt.target);
|
modifiedPolygon(canvas, evt.target);
|
||||||
undoStack.onObjectModified();
|
undoStack.onObjectModified();
|
||||||
}
|
}
|
||||||
onChange();
|
saveNeededStore.set(true);
|
||||||
|
});
|
||||||
|
canvas.on("text:editing:entered", function() {
|
||||||
|
textEditingState.set(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.on("text:editing:exited", function() {
|
||||||
|
textEditingState.set(false);
|
||||||
|
});
|
||||||
|
canvas.on("object:removed", () => {
|
||||||
|
saveNeededStore.set(true);
|
||||||
});
|
});
|
||||||
canvas.on("object:removed", onChange);
|
|
||||||
return canvas;
|
return canvas;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ export type ShapeOrShapes = Shape | Shape[];
|
||||||
export class Shape {
|
export class Shape {
|
||||||
left: number;
|
left: number;
|
||||||
top: number;
|
top: number;
|
||||||
fill: string = SHAPE_MASK_COLOR;
|
fill: string;
|
||||||
/** Whether occlusions from other cloze numbers should be shown on the
|
/** Whether occlusions from other cloze numbers should be shown on the
|
||||||
* question side. Used only in reviewer code.
|
* question side. Used only in reviewer code.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
import { fabric } from "fabric";
|
import { fabric } from "fabric";
|
||||||
|
|
||||||
import { TEXT_BACKGROUND_COLOR, TEXT_FONT_FAMILY, TEXT_FONT_SIZE, TEXT_PADDING } from "../tools/lib";
|
import { TEXT_BACKGROUND_COLOR, TEXT_COLOR, TEXT_FONT_FAMILY, TEXT_FONT_SIZE, TEXT_PADDING } from "../tools/lib";
|
||||||
import type { ConstructorParams, Size } from "../types";
|
import type { ConstructorParams, Size } from "../types";
|
||||||
import type { ShapeDataForCloze } from "./base";
|
import type { ShapeDataForCloze } from "./base";
|
||||||
import { Shape } from "./base";
|
import { Shape } from "./base";
|
||||||
|
@ -23,6 +23,7 @@ export class Text extends Shape {
|
||||||
...rest
|
...rest
|
||||||
}: ConstructorParams<Text> = {}) {
|
}: ConstructorParams<Text> = {}) {
|
||||||
super(rest);
|
super(rest);
|
||||||
|
this.fill = TEXT_COLOR;
|
||||||
this.text = text;
|
this.text = text;
|
||||||
this.scaleX = scaleX;
|
this.scaleX = scaleX;
|
||||||
this.scaleY = scaleY;
|
this.scaleY = scaleY;
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
import { fabric } from "fabric";
|
import { fabric } from "fabric";
|
||||||
import { cloneDeep } from "lodash-es";
|
import { cloneDeep } from "lodash-es";
|
||||||
|
|
||||||
import { getBoundingBox } from "../tools/lib";
|
import { getBoundingBoxSize } from "../tools/lib";
|
||||||
import type { Size } from "../types";
|
import type { Size } from "../types";
|
||||||
import type { Shape, ShapeOrShapes } from "./base";
|
import type { Shape, ShapeOrShapes } from "./base";
|
||||||
import { Ellipse } from "./ellipse";
|
import { Ellipse } from "./ellipse";
|
||||||
|
@ -97,7 +97,7 @@ export function baseShapesFromFabric(): ShapeOrShapes[] {
|
||||||
? activeObject
|
? activeObject
|
||||||
: null;
|
: null;
|
||||||
const objects = canvas.getObjects() as fabric.Object[];
|
const objects = canvas.getObjects() as fabric.Object[];
|
||||||
const boundingBox = getBoundingBox();
|
const boundingBox = getBoundingBoxSize();
|
||||||
// filter transparent rectangles
|
// filter transparent rectangles
|
||||||
return objects
|
return objects
|
||||||
.map((object) => {
|
.map((object) => {
|
||||||
|
@ -170,10 +170,6 @@ function fabricObjectToBaseShapeOrShapes(
|
||||||
shape.top = newPosition.y;
|
shape.top = newPosition.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (size == undefined) {
|
|
||||||
size = { width: 0, height: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
shape = shape.toNormal(size);
|
shape = shape.toNormal(size);
|
||||||
return shape;
|
return shape;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,3 +17,5 @@ export const ioImageLoadedStore = writable(false);
|
||||||
export const opacityStateStore = writable(false);
|
export const opacityStateStore = writable(false);
|
||||||
// store state of text editing
|
// store state of text editing
|
||||||
export const textEditingState = writable(false);
|
export const textEditingState = writable(false);
|
||||||
|
// Stores if the canvas shapes data needs to be saved
|
||||||
|
export const saveNeededStore = writable(false);
|
||||||
|
|
|
@ -12,9 +12,13 @@ export const addShape = (
|
||||||
shape: Shape,
|
shape: Shape,
|
||||||
): void => {
|
): void => {
|
||||||
const fabricShape = shape.toFabric(boundingBox.getBoundingRect(true));
|
const fabricShape = shape.toFabric(boundingBox.getBoundingRect(true));
|
||||||
addBorder(fabricShape);
|
|
||||||
if (fabricShape.type === "i-text") {
|
if (fabricShape.type === "i-text") {
|
||||||
enableUniformScaling(canvas, fabricShape);
|
enableUniformScaling(canvas, fabricShape);
|
||||||
|
} else {
|
||||||
|
// No border around i-text shapes since it will be interpretted
|
||||||
|
// as character stroke, this is supposed to create an outline
|
||||||
|
// around the entire shape.
|
||||||
|
addBorder(fabricShape);
|
||||||
}
|
}
|
||||||
canvas.add(fabricShape);
|
canvas.add(fabricShape);
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { fabric } from "fabric";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
import { opacityStateStore } from "../store";
|
import { opacityStateStore } from "../store";
|
||||||
|
import type { Size } from "../types";
|
||||||
|
|
||||||
export const SHAPE_MASK_COLOR = "#ffeba2";
|
export const SHAPE_MASK_COLOR = "#ffeba2";
|
||||||
export const BORDER_COLOR = "#212121";
|
export const BORDER_COLOR = "#212121";
|
||||||
|
@ -12,6 +13,7 @@ export const TEXT_BACKGROUND_COLOR = "#ffffff";
|
||||||
export const TEXT_FONT_FAMILY = "Arial";
|
export const TEXT_FONT_FAMILY = "Arial";
|
||||||
export const TEXT_PADDING = 5;
|
export const TEXT_PADDING = 5;
|
||||||
export const TEXT_FONT_SIZE = 40;
|
export const TEXT_FONT_SIZE = 40;
|
||||||
|
export const TEXT_COLOR = "#000000";
|
||||||
|
|
||||||
let _clipboard;
|
let _clipboard;
|
||||||
|
|
||||||
|
@ -310,20 +312,31 @@ export const selectAllShapes = (canvas: fabric.Canvas) => {
|
||||||
|
|
||||||
export const isPointerInBoundingBox = (pointer): boolean => {
|
export const isPointerInBoundingBox = (pointer): boolean => {
|
||||||
const boundingBox = getBoundingBox();
|
const boundingBox = getBoundingBox();
|
||||||
|
if (boundingBox === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
boundingBox.selectable = false;
|
boundingBox.selectable = false;
|
||||||
boundingBox.evented = false;
|
boundingBox.evented = false;
|
||||||
if (
|
if (
|
||||||
pointer.x < boundingBox.left
|
pointer.x < boundingBox.left!
|
||||||
|| pointer.x > boundingBox.left + boundingBox.width
|
|| pointer.x > boundingBox.left! + boundingBox.width!
|
||||||
|| pointer.y < boundingBox.top
|
|| pointer.y < boundingBox.top!
|
||||||
|| pointer.y > boundingBox.top + boundingBox.height
|
|| pointer.y > boundingBox.top! + boundingBox.height!
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBoundingBox = () => {
|
export const getBoundingBox = (): fabric.Rect | undefined => {
|
||||||
const canvas = globalThis.canvas;
|
const canvas: fabric.Canvas = globalThis.canvas;
|
||||||
return canvas.getObjects().find((obj) => obj.fill === "transparent");
|
return canvas.getObjects().find((obj) => obj.fill === "transparent");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getBoundingBoxSize = (): Size => {
|
||||||
|
const boundingBoxSize = getBoundingBox()?.getBoundingRect(true);
|
||||||
|
if (boundingBoxSize) {
|
||||||
|
return { width: boundingBoxSize.width, height: boundingBoxSize.height };
|
||||||
|
}
|
||||||
|
return { width: 0, height: 0 };
|
||||||
|
};
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
import { fabric } from "fabric";
|
import { fabric } from "fabric";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
import { opacityStateStore, textEditingState } from "../store";
|
import type { Callback } from "@tslib/helpers";
|
||||||
|
import { opacityStateStore } from "../store";
|
||||||
import {
|
import {
|
||||||
enableUniformScaling,
|
enableUniformScaling,
|
||||||
isPointerInBoundingBox,
|
isPointerInBoundingBox,
|
||||||
|
@ -16,11 +17,11 @@ import {
|
||||||
import { undoStack } from "./tool-undo-redo";
|
import { undoStack } from "./tool-undo-redo";
|
||||||
import { onPinchZoom } from "./tool-zoom";
|
import { onPinchZoom } from "./tool-zoom";
|
||||||
|
|
||||||
export const drawText = (canvas: fabric.Canvas): void => {
|
export const drawText = (canvas: fabric.Canvas, onActivated: Callback): void => {
|
||||||
canvas.selectionColor = "rgba(0, 0, 0, 0)";
|
canvas.selectionColor = "rgba(0, 0, 0, 0)";
|
||||||
stopDraw(canvas);
|
stopDraw(canvas);
|
||||||
|
|
||||||
let text;
|
let text: fabric.IText;
|
||||||
|
|
||||||
canvas.on("mouse:down", function(o) {
|
canvas.on("mouse:down", function(o) {
|
||||||
if (o.target) {
|
if (o.target) {
|
||||||
|
@ -52,7 +53,9 @@ export const drawText = (canvas: fabric.Canvas): void => {
|
||||||
canvas.add(text);
|
canvas.add(text);
|
||||||
canvas.setActiveObject(text);
|
canvas.setActiveObject(text);
|
||||||
undoStack.onObjectAdded(text.id);
|
undoStack.onObjectAdded(text.id);
|
||||||
|
text.enterEditing();
|
||||||
text.selectAll();
|
text.selectAll();
|
||||||
|
onActivated();
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas.on("mouse:move", function(o) {
|
canvas.on("mouse:move", function(o) {
|
||||||
|
@ -62,12 +65,4 @@ export const drawText = (canvas: fabric.Canvas): void => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas.on("text:editing:entered", function() {
|
|
||||||
textEditingState.set(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
canvas.on("text:editing:exited", function() {
|
|
||||||
textEditingState.set(false);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { writable } from "svelte/store";
|
||||||
|
|
||||||
import { mdiRedo, mdiUndo } from "$lib/components/icons";
|
import { mdiRedo, mdiUndo } from "$lib/components/icons";
|
||||||
|
|
||||||
import { emitChangeSignal } from "../MaskEditor.svelte";
|
import { saveNeededStore } from "../store";
|
||||||
import { redoKeyCombination, undoKeyCombination } from "./shortcuts";
|
import { redoKeyCombination, undoKeyCombination } from "./shortcuts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -84,7 +84,7 @@ class UndoStack {
|
||||||
this.locked = true;
|
this.locked = true;
|
||||||
this.canvas?.loadFromJSON(this.stack[this.index], () => {
|
this.canvas?.loadFromJSON(this.stack[this.index], () => {
|
||||||
this.canvas?.renderAll();
|
this.canvas?.renderAll();
|
||||||
emitChangeSignal();
|
saveNeededStore.set(true);
|
||||||
this.locked = false;
|
this.locked = false;
|
||||||
});
|
});
|
||||||
// make bounding box unselectable
|
// make bounding box unselectable
|
||||||
|
@ -100,12 +100,12 @@ class UndoStack {
|
||||||
this.push();
|
this.push();
|
||||||
}
|
}
|
||||||
this.shapeIds.add(id);
|
this.shapeIds.add(id);
|
||||||
emitChangeSignal();
|
saveNeededStore.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
onObjectModified(): void {
|
onObjectModified(): void {
|
||||||
this.push();
|
this.push();
|
||||||
emitChangeSignal();
|
saveNeededStore.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private maybePush(obj: fabric.IEvent<MouseEvent>): void {
|
private maybePush(obj: fabric.IEvent<MouseEvent>): void {
|
||||||
|
|
|
@ -9,7 +9,8 @@ import Hammer from "hammerjs";
|
||||||
|
|
||||||
import { isDesktop } from "$lib/tslib/platform";
|
import { isDesktop } from "$lib/tslib/platform";
|
||||||
|
|
||||||
import { getBoundingBox, redraw } from "./lib";
|
import type { Size } from "../types";
|
||||||
|
import { getBoundingBoxSize, redraw } from "./lib";
|
||||||
|
|
||||||
const minScale = 0.5;
|
const minScale = 0.5;
|
||||||
const maxScale = 5;
|
const maxScale = 5;
|
||||||
|
@ -192,7 +193,7 @@ const onMouseUp = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const constrainBoundsAroundBgImage = (canvas: fabric.Canvas) => {
|
export const constrainBoundsAroundBgImage = (canvas: fabric.Canvas) => {
|
||||||
const boundingBox = getBoundingBox();
|
const boundingBox = getBoundingBoxSize();
|
||||||
const ioImage = document.getElementById("image") as HTMLImageElement;
|
const ioImage = document.getElementById("image") as HTMLImageElement;
|
||||||
|
|
||||||
const width = boundingBox.width * canvas.getZoom();
|
const width = boundingBox.width * canvas.getZoom();
|
||||||
|
@ -217,7 +218,7 @@ export const setCanvasSize = (canvas: fabric.Canvas) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fitCanvasVptScale = (canvas: fabric.Canvas) => {
|
const fitCanvasVptScale = (canvas: fabric.Canvas) => {
|
||||||
const boundingBox = getBoundingBox();
|
const boundingBox = getBoundingBoxSize();
|
||||||
const ratio = getScaleRatio(boundingBox);
|
const ratio = getScaleRatio(boundingBox);
|
||||||
const vpt = canvas.viewportTransform!;
|
const vpt = canvas.viewportTransform!;
|
||||||
|
|
||||||
|
@ -237,7 +238,7 @@ const fitCanvasVptScale = (canvas: fabric.Canvas) => {
|
||||||
redraw(canvas);
|
redraw(canvas);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getScaleRatio = (boundingBox: fabric.Rect) => {
|
const getScaleRatio = (boundingBox: Size) => {
|
||||||
const h1 = boundingBox.height!;
|
const h1 = boundingBox.height!;
|
||||||
const w1 = boundingBox.width!;
|
const w1 = boundingBox.width!;
|
||||||
const w2 = innerWidth - 42;
|
const w2 = innerWidth - 42;
|
||||||
|
|
Loading…
Reference in a new issue