fixes: remove unfinished shapes, remove selectable and make shapes remain inside canvas (#2809)

* remove unfinished polygon and remove selectable for shapes in polygon mode

* make group and polygon position remain inside canvas area

* click through transparent area in grouped object

* add some shortcuts for basic usages

* tools button icon in center & switch mode border

* fix load svg image

* basic rtl support, panzoom have issues in rtl mode

* better zoom option both in ltr and rtl

* handle zoom event in mask editor

* add h button to handle toggle mask

* add more mime type

* use capital M (shift+m) for toggle mask

* allow io shortcuts in mask editor only

* make other shapes also remain in canvas bound area

* better zoom implementation, zoom from center
add zoom to resize event listener

* add a border to corner to handle blend of control

* add refresh button to go to  selection menu

* add tooltip to shortcuts and also add shortcut for other tools

* make opacity remain in same state when toggled on

* opacity for group/ungroup objects

* update shortcuts implementation
This commit is contained in:
Mani 2023-11-24 12:06:40 +08:00 committed by GitHub
parent 24e5912448
commit be1f889211
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 545 additions and 170 deletions

View file

@ -93,6 +93,8 @@ editing-image-occlusion-ellipse-tool = Ellipse
editing-image-occlusion-polygon-tool = Polygon editing-image-occlusion-polygon-tool = Polygon
editing-image-occlusion-text-tool = Text editing-image-occlusion-text-tool = Text
editing-image-occlusion-toggle-mask-editor = Toggle Mask Editor editing-image-occlusion-toggle-mask-editor = Toggle Mask Editor
editing-image-occlusion-reset = Reset Image Occlusion
editing-image-occlusion-confirm-reset = Are you sure you want to reset this image occlusion?
## You don't need to translate these strings, as they will be replaced with different ones soon. ## You don't need to translate these strings, as they will be replaced with different ones soon.

View file

@ -74,6 +74,7 @@ message GetImageOcclusionNoteResponse {
string header = 3; string header = 3;
string back_extra = 4; string back_extra = 4;
repeated string tags = 5; repeated string tags = 5;
string image_file_name = 6;
} }
oneof value { oneof value {

View file

@ -110,6 +110,12 @@ impl Collection {
if self.is_image_file(&final_path)? { if self.is_image_file(&final_path)? {
cloze_note.image_data = read_file(&final_path)?; cloze_note.image_data = read_file(&final_path)?;
cloze_note.image_file_name = final_path
.file_name()
.or_not_found("expected filename")?
.to_str()
.unwrap()
.to_string();
} }
Ok(cloze_note) Ok(cloze_note)

View file

@ -388,13 +388,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import ImageOcclusionPicker from "image-occlusion/ImageOcclusionPicker.svelte"; import ImageOcclusionPicker from "image-occlusion/ImageOcclusionPicker.svelte";
import type { IOMode } from "image-occlusion/lib"; import type { IOMode } from "image-occlusion/lib";
import { exportShapesToClozeDeletions } from "image-occlusion/shapes/to-cloze"; import { exportShapesToClozeDeletions } from "image-occlusion/shapes/to-cloze";
import { hideAllGuessOne, ioMaskEditorVisible } from "image-occlusion/store"; import {
hideAllGuessOne,
ioImageLoadedStore,
ioMaskEditorVisible,
} from "image-occlusion/store";
import { mathjaxConfig } from "../editable/mathjax-element"; import { mathjaxConfig } from "../editable/mathjax-element";
import CollapseLabel from "./CollapseLabel.svelte"; import CollapseLabel from "./CollapseLabel.svelte";
import * as oldEditorAdapter from "./old-editor-adapter"; import * as oldEditorAdapter from "./old-editor-adapter";
let isIOImageLoaded = false; $: isIOImageLoaded = false;
$: ioImageLoadedStore.set(isIOImageLoaded);
let imageOcclusionMode: IOMode | undefined; let imageOcclusionMode: IOMode | undefined;
let ioFields = new ImageOcclusionFieldIndexes({}); let ioFields = new ImageOcclusionFieldIndexes({});
@ -456,6 +461,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
function resetIOImageLoaded() { function resetIOImageLoaded() {
isIOImageLoaded = false; isIOImageLoaded = false;
globalThis.canvas.clear(); globalThis.canvas.clear();
globalThis.canvas = undefined;
const page = document.querySelector(".image-occlusion"); const page = document.querySelector(".image-occlusion");
if (page) { if (page) {
page.remove(); page.remove();
@ -791,6 +797,7 @@ the AddCards dialog) should be implemented in the user of this component.
} }
:global(.top-tool-bar-container .icon-button) { :global(.top-tool-bar-container .icon-button) {
height: 36px !important; height: 36px !important;
line-height: 1;
} }
:global(.image-occlusion .tool-bar-container) { :global(.image-occlusion .tool-bar-container) {
top: unset !important; top: unset !important;

View file

@ -7,14 +7,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import ButtonGroup from "components/ButtonGroup.svelte"; import ButtonGroup from "components/ButtonGroup.svelte";
import DynamicallySlottable from "components/DynamicallySlottable.svelte"; import DynamicallySlottable from "components/DynamicallySlottable.svelte";
import IconButton from "components/IconButton.svelte"; import IconButton from "components/IconButton.svelte";
import { ioMaskEditorVisible } from "image-occlusion/store"; import { ioImageLoadedStore, ioMaskEditorVisible } from "image-occlusion/store";
import ButtonGroupItem, { import ButtonGroupItem, {
createProps, createProps,
setSlotHostContext, setSlotHostContext,
updatePropsList, updatePropsList,
} from "../../components/ButtonGroupItem.svelte"; } from "../../components/ButtonGroupItem.svelte";
import { mdiViewDashboard } from "./icons"; import { mdiTableRefresh, mdiViewDashboard } from "./icons";
export let api = {}; export let api = {};
</script> </script>
@ -39,6 +39,22 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{@html mdiViewDashboard} {@html mdiViewDashboard}
</IconButton> </IconButton>
</ButtonGroupItem> </ButtonGroupItem>
<ButtonGroupItem>
<IconButton
id="io-reset-btn"
disabled={!$ioImageLoadedStore}
on:click={() => {
if (confirm(tr.editingImageOcclusionConfirmReset())) {
globalThis.resetIOImageLoaded();
} else {
return;
}
}}
tooltip={tr.editingImageOcclusionReset()}
>
{@html mdiTableRefresh}
</IconButton>
</ButtonGroupItem>
</DynamicallySlottable> </DynamicallySlottable>
</ButtonGroup> </ButtonGroup>

View file

@ -12,6 +12,7 @@ export { default as superscriptIcon } from "@mdi/svg/svg/format-superscript.svg"
export { default as functionIcon } from "@mdi/svg/svg/function-variant.svg"; export { default as functionIcon } from "@mdi/svg/svg/function-variant.svg";
export { default as paperclipIcon } from "@mdi/svg/svg/paperclip.svg"; export { default as paperclipIcon } from "@mdi/svg/svg/paperclip.svg";
export { default as mdiRefresh } from "@mdi/svg/svg/refresh.svg"; export { default as mdiRefresh } from "@mdi/svg/svg/refresh.svg";
export { default as mdiTableRefresh } from "@mdi/svg/svg/table-refresh.svg";
export { default as mdiViewDashboard } from "@mdi/svg/svg/view-dashboard.svg"; export { default as mdiViewDashboard } from "@mdi/svg/svg/view-dashboard.svg";
export { default as eraserIcon } from "bootstrap-icons/icons/eraser.svg"; export { default as eraserIcon } from "bootstrap-icons/icons/eraser.svg";
export { default as justifyFullIcon } from "bootstrap-icons/icons/justify.svg"; export { default as justifyFullIcon } from "bootstrap-icons/icons/justify.svg";

View file

@ -26,6 +26,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
} from "./mask-editor"; } from "./mask-editor";
import Toolbar from "./Toolbar.svelte"; import Toolbar from "./Toolbar.svelte";
import { MaskEditorAPI } from "./tools/api"; import { MaskEditorAPI } from "./tools/api";
import { setCenterXForZoom } from "./tools/lib";
export let mode: IOMode; export let mode: IOMode;
const iconSize = 80; const iconSize = 80;
@ -57,8 +58,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
minZoom: 0.1, minZoom: 0.1,
zoomDoubleClickSpeed: 1, zoomDoubleClickSpeed: 1,
smoothScroll: false, smoothScroll: false,
transformOrigin: { x: 0.5, y: 0.5 },
}); });
instance.pause(); instance.pause();
globalThis.panzoom = instance;
if (mode.kind == "add") { if (mode.kind == "add") {
setupMaskEditor(mode.imagePath, instance, onChange, onImageLoaded).then( setupMaskEditor(mode.imagePath, instance, onChange, onImageLoaded).then(
@ -78,12 +81,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
onMount(() => { onMount(() => {
window.addEventListener("resize", () => { window.addEventListener("resize", () => {
setCanvasZoomRatio(canvas, instance); setCanvasZoomRatio(canvas, instance);
setCenterXForZoom(canvas);
}); });
}); });
onDestroy(() => { onDestroy(() => {
window.removeEventListener("resize", () => { window.removeEventListener("resize", () => {
setCanvasZoomRatio(canvas, instance); setCanvasZoomRatio(canvas, instance);
setCenterXForZoom(canvas);
}); });
}); });
</script> </script>
@ -109,10 +114,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
padding-bottom: 100px; padding-bottom: 100px;
} }
:global([dir="rtl"]) .editor-main {
left: 2px;
right: 36px;
}
.editor-container { .editor-container {
width: 100%; width: 100%;
height: 100%; height: 100%;
position: relative; position: relative;
direction: ltr;
} }
#image { #image {

View file

@ -3,15 +3,20 @@ 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 lang="ts"> <script lang="ts">
import { directionKey } from "@tslib/context-keys";
import * as tr from "@tslib/ftl"; import * as tr from "@tslib/ftl";
import { getPlatformString } from "@tslib/shortcuts";
import DropdownItem from "components/DropdownItem.svelte"; import DropdownItem from "components/DropdownItem.svelte";
import IconButton from "components/IconButton.svelte"; import IconButton from "components/IconButton.svelte";
import Popover from "components/Popover.svelte"; import Popover from "components/Popover.svelte";
import Shortcut from "components/Shortcut.svelte";
import WithFloating from "components/WithFloating.svelte"; import WithFloating from "components/WithFloating.svelte";
import { getContext, onMount } from "svelte";
import type { Readable } from "svelte/store";
import { mdiEye, mdiFormatAlignCenter, mdiSquare, mdiViewDashboard } from "./icons"; import { mdiEye, mdiFormatAlignCenter, mdiSquare, mdiViewDashboard } from "./icons";
import { emitChangeSignal } from "./MaskEditor.svelte"; import { emitChangeSignal } from "./MaskEditor.svelte";
import { hideAllGuessOne } from "./store"; import { hideAllGuessOne, ioMaskEditorVisible } 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";
@ -21,7 +26,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
groupUngroupTools, groupUngroupTools,
zoomTools, zoomTools,
} from "./tools/more-tools"; } from "./tools/more-tools";
import { toggleTranslucentKeyCombination } from "./tools/shortcuts";
import { tools } from "./tools/tool-buttons"; import { tools } from "./tools/tool-buttons";
import { removeUnfinishedPolygon } from "./tools/tool-polygon";
import { undoRedoTools, undoStack } from "./tools/tool-undo-redo"; import { undoRedoTools, undoStack } from "./tools/tool-undo-redo";
export let canvas; export let canvas;
@ -32,6 +39,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let leftPos = 82; let leftPos = 82;
let maksOpacity = false; let maksOpacity = false;
let showFloating = false; let showFloating = false;
const direction = getContext<Readable<"ltr" | "rtl">>(directionKey);
document.addEventListener("click", (event) => { document.addEventListener("click", (event) => {
const upperCanvas = document.querySelector(".upper-canvas"); const upperCanvas = document.querySelector(".upper-canvas");
@ -40,10 +48,77 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
} }
}); });
// handle zoom event when mouse scroll and ctrl key are hold for panzoom
let clicked = false;
let dbclicked = false;
let move = false;
let wheel = false;
onMount(() => {
window.addEventListener("mousedown", (event) => {
if (event.ctrlKey) {
clicked = true;
}
});
window.addEventListener("mouseup", (event) => {
if (event.ctrlKey) {
clicked = false;
}
});
window.addEventListener("mousemove", (event) => {
if (event.ctrlKey) {
move = true;
}
});
window.addEventListener("wheel", (event) => {
if (event.ctrlKey) {
wheel = true;
}
});
window.addEventListener("dblclick", (event) => {
if (event.ctrlKey) {
dbclicked = true;
}
});
window.addEventListener("keyup", (event) => {
if (event.key == "Control") {
clicked = false;
move = false;
wheel = false;
dbclicked = false;
}
});
window.addEventListener("keydown", (event) => {
if (event.key == "Control") {
clicked = false;
move = false;
wheel = false;
dbclicked = false;
}
});
window.addEventListener("keydown", (event) => {
if (event.key == "Control" && activeTool != "magnify") {
instance.resume();
}
});
window.addEventListener("keyup", (event) => {
if (event.key == "Control" && activeTool != "magnify") {
instance.pause();
}
});
window.addEventListener("wheel", () => {
if (clicked && move && wheel && !dbclicked) {
enableMagnify();
}
});
});
// handle tool changes after initialization // handle tool changes after initialization
$: if (instance && canvas) { $: if (instance && canvas) {
disableFunctions(); disableFunctions();
enableSelectable(canvas, true); enableSelectable(canvas, true);
// remove unfinished polygon when switching to other tools
removeUnfinishedPolygon(canvas);
switch (activeTool) { switch (activeTool) {
case "magnify": case "magnify":
@ -77,6 +152,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
$hideAllGuessOne = occlusionType === "all"; $hideAllGuessOne = occlusionType === "all";
emitChangeSignal(); emitChangeSignal();
} }
const enableMagnify = () => {
disableFunctions();
enableSelectable(canvas, false);
instance.resume();
activeTool = "magnify";
};
</script> </script>
<div class="tool-bar-container"> <div class="tool-bar-container">
@ -84,7 +165,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<IconButton <IconButton
class="tool-icon-button {activeTool == tool.id ? 'active-tool' : ''}" class="tool-icon-button {activeTool == tool.id ? 'active-tool' : ''}"
{iconSize} {iconSize}
tooltip={tool.tooltip()} tooltip="{tool.tooltip()} ({getPlatformString(tool.shortcut)})"
active={activeTool === tool.id} active={activeTool === tool.id}
on:click={() => { on:click={() => {
activeTool = tool.id; activeTool = tool.id;
@ -92,9 +173,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
> >
{@html tool.icon} {@html tool.icon}
</IconButton> </IconButton>
{#if $ioMaskEditorVisible}
<Shortcut
keyCombination={tool.shortcut}
on:action={() => {
activeTool = tool.id;
}}
/>
{/if}
{/each} {/each}
</div> </div>
<div dir={$direction}>
<div class="top-tool-bar-container"> <div class="top-tool-bar-container">
<WithFloating <WithFloating
show={showFloating} show={showFloating}
@ -103,7 +193,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
on:close={() => (showFloating = false)} on:close={() => (showFloating = false)}
> >
<IconButton <IconButton
class="top-tool-icon-button right-border-radius dropdown-tool-mode" class="top-tool-icon-button border-radius dropdown-tool-mode"
slot="reference" slot="reference"
tooltip={tr.editingImageOcclusionMode()} tooltip={tr.editingImageOcclusionMode()}
{iconSize} {iconSize}
@ -137,13 +227,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
: 'right-border-radius'}" : 'right-border-radius'}"
{iconSize} {iconSize}
on:click={tool.action} on:click={tool.action}
tooltip={tool.tooltip()} tooltip="{tool.tooltip()} ({getPlatformString(tool.shortcut)})"
disabled={tool.name === "undo" disabled={tool.name === "undo"
? !$undoStack.undoable ? !$undoStack.undoable
: !$undoStack.redoable} : !$undoStack.redoable}
> >
{@html tool.icon} {@html tool.icon}
</IconButton> </IconButton>
{#if $ioMaskEditorVisible}
<Shortcut keyCombination={tool.shortcut} on:action={tool.action} />
{/if}
{/each} {/each}
</div> </div>
@ -155,13 +248,21 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
? 'left-border-radius' ? 'left-border-radius'
: ''} {tool.name === 'zoomReset' ? 'right-border-radius' : ''}" : ''} {tool.name === 'zoomReset' ? 'right-border-radius' : ''}"
{iconSize} {iconSize}
tooltip={tool.tooltip()} tooltip="{tool.tooltip()} ({getPlatformString(tool.shortcut)})"
on:click={() => { on:click={() => {
tool.action(instance); tool.action(instance);
}} }}
> >
{@html tool.icon} {@html tool.icon}
</IconButton> </IconButton>
{#if $ioMaskEditorVisible}
<Shortcut
keyCombination={tool.shortcut}
on:action={() => {
tool.action(instance);
}}
/>
{/if}
{/each} {/each}
</div> </div>
@ -178,6 +279,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
> >
{@html mdiEye} {@html mdiEye}
</IconButton> </IconButton>
{#if $ioMaskEditorVisible}
<Shortcut
keyCombination={toggleTranslucentKeyCombination}
on:action={() => {
maksOpacity = !maksOpacity;
makeMaskTransparent(canvas, maksOpacity);
}}
/>
{/if}
<!-- cursor tools --> <!-- cursor tools -->
{#each deleteDuplicateTools as tool} {#each deleteDuplicateTools as tool}
@ -186,13 +296,22 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
? 'right-border-radius' ? 'right-border-radius'
: ''}" : ''}"
{iconSize} {iconSize}
tooltip={tool.tooltip()} tooltip="{tool.tooltip()} ({getPlatformString(tool.shortcut)})"
on:click={() => { on:click={() => {
tool.action(canvas); tool.action(canvas);
}} }}
> >
{@html tool.icon} {@html tool.icon}
</IconButton> </IconButton>
{#if $ioMaskEditorVisible}
<Shortcut
keyCombination={tool.shortcut}
on:action={() => {
tool.action(canvas);
emitChangeSignal();
}}
/>
{/if}
{/each} {/each}
</div> </div>
@ -204,7 +323,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
? 'left-border-radius' ? 'left-border-radius'
: ''}" : ''}"
{iconSize} {iconSize}
tooltip={tool.tooltip()} tooltip="{tool.tooltip()} ({getPlatformString(tool.shortcut)})"
on:click={() => { on:click={() => {
tool.action(canvas); tool.action(canvas);
emitChangeSignal(); emitChangeSignal();
@ -212,6 +331,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
> >
{@html tool.icon} {@html tool.icon}
</IconButton> </IconButton>
{#if $ioMaskEditorVisible}
<Shortcut
keyCombination={tool.shortcut}
on:action={() => {
tool.action(canvas);
emitChangeSignal();
}}
/>
{/if}
{/each} {/each}
<IconButton <IconButton
@ -233,15 +361,26 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<IconButton <IconButton
class="top-tool-icon-button" class="top-tool-icon-button"
{iconSize} {iconSize}
tooltip={alignTool.tooltip()} tooltip="{alignTool.tooltip()} ({getPlatformString(
alignTool.shortcut,
)})"
on:click={() => { on:click={() => {
alignTool.action(canvas); alignTool.action(canvas);
}} }}
> >
{@html alignTool.icon} {@html alignTool.icon}
</IconButton> </IconButton>
{#if $ioMaskEditorVisible}
<Shortcut
keyCombination={alignTool.shortcut}
on:action={() => {
alignTool.action(canvas);
}}
/>
{/if}
{/each} {/each}
</div> </div>
</div>
<style> <style>
.top-tool-bar-container { .top-tool-bar-container {
@ -252,6 +391,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
margin-top: 2px; margin-top: 2px;
} }
:global([dir="rtl"] .top-tool-bar-container) {
margin-left: unset;
margin-right: 28px;
}
.undo-redo-button { .undo-redo-button {
margin-right: 2px; margin-right: 2px;
display: flex; display: flex;
@ -267,10 +411,22 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
border-radius: 5px 0 0 5px !important; border-radius: 5px 0 0 5px !important;
} }
:global([dir="rtl"] .left-border-radius) {
border-radius: 0 5px 5px 0 !important;
}
:global(.right-border-radius) { :global(.right-border-radius) {
border-radius: 0 5px 5px 0 !important; border-radius: 0 5px 5px 0 !important;
} }
:global([dir="rtl"] .right-border-radius) {
border-radius: 5px 0 0 5px !important;
}
:global(.border-radius) {
border-radius: 5px !important;
}
:global(.top-tool-icon-button) { :global(.top-tool-icon-button) {
border: unset; border: unset;
display: inline; display: inline;
@ -290,7 +446,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
} }
.show { .show {
display: block; display: flex;
} }
::-webkit-scrollbar { ::-webkit-scrollbar {
@ -311,6 +467,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
padding-bottom: 100px; padding-bottom: 100px;
} }
:global([dir="rtl"] .tool-bar-container) {
left: unset;
right: 2px;
}
:global(.tool-icon-button) { :global(.tool-icon-button) {
border: unset; border: unset;
display: block; display: block;

View file

@ -12,7 +12,13 @@ import { optimumCssSizeForCanvas } from "./canvas-scale";
import { notesDataStore, tagsWritable, zoomResetValue } from "./store"; import { notesDataStore, tagsWritable, zoomResetValue } 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, moveShapeToCanvasBoundaries } from "./tools/lib"; import {
enableSelectable,
makeShapeRemainInCanvas,
moveShapeToCanvasBoundaries,
setCenterXForZoom,
zoomReset,
} from "./tools/lib";
import { modifiedPolygon } from "./tools/tool-polygon"; import { modifiedPolygon } from "./tools/tool-polygon";
import { undoStack } from "./tools/tool-undo-redo"; import { undoStack } from "./tools/tool-undo-redo";
import type { Size } from "./types"; import type { Size } from "./types";
@ -33,7 +39,7 @@ export const setupMaskEditor = async (
// 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(imageData.data!); image.src = getImageData(imageData.data!, path);
image.onload = function() { image.onload = function() {
const size = optimumCssSizeForCanvas({ width: image.width, height: image.height }, containerSize()); const size = optimumCssSizeForCanvas({ width: image.width, height: image.height }, containerSize());
canvas.setWidth(size.width); canvas.setWidth(size.width);
@ -73,7 +79,7 @@ export const setupMaskEditorForEdit = async (
// 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.style.visibility = "hidden"; image.style.visibility = "hidden";
image.src = getImageData(clozeNote.imageData!); image.src = getImageData(clozeNote.imageData!, clozeNote.imageFileName!);
image.onload = function() { image.onload = function() {
const size = optimumCssSizeForCanvas({ width: image.width, height: image.height }, containerSize()); const size = optimumCssSizeForCanvas({ width: image.width, height: image.height }, containerSize());
canvas.setWidth(size.width); canvas.setWidth(size.width);
@ -100,12 +106,19 @@ function initCanvas(onChange: () => void): fabric.Canvas {
tagsWritable.set([]); tagsWritable.set([]);
globalThis.canvas = canvas; globalThis.canvas = canvas;
undoStack.setCanvas(canvas); undoStack.setCanvas(canvas);
// find object per-pixel basis rather than according to bounding box,
// allow click through transparent area
canvas.perPixelTargetFind = true;
// Disable uniform scaling // Disable uniform scaling
canvas.uniformScaling = false; canvas.uniformScaling = false;
canvas.uniScaleKey = "none"; canvas.uniScaleKey = "none";
// disable rotation globally // disable rotation globally
delete fabric.Object.prototype.controls.mtr; delete fabric.Object.prototype.controls.mtr;
// add a border to corner to handle blend of control
fabric.Object.prototype.cornerStyle = "circle";
fabric.Object.prototype.cornerStrokeColor = "#000000";
moveShapeToCanvasBoundaries(canvas); moveShapeToCanvasBoundaries(canvas);
makeShapeRemainInCanvas(canvas);
canvas.on("object:modified", (evt) => { canvas.on("object:modified", (evt) => {
if (evt.target instanceof fabric.Polygon) { if (evt.target instanceof fabric.Polygon) {
modifiedPolygon(canvas, evt.target); modifiedPolygon(canvas, evt.target);
@ -114,12 +127,25 @@ function initCanvas(onChange: () => void): fabric.Canvas {
onChange(); onChange();
}); });
canvas.on("object:removed", onChange); canvas.on("object:removed", onChange);
setCenterXForZoom(canvas);
return canvas; return canvas;
} }
const getImageData = (imageData): string => { const getImageData = (imageData, path): string => {
const b64encoded = protoBase64.enc(imageData); const b64encoded = protoBase64.enc(imageData);
return "data:image/png;base64," + b64encoded; const extension = path.split(".").pop();
const mimeTypes = {
"jpg": "jpeg",
"jpeg": "jpeg",
"gif": "gif",
"svg": "svg+xml",
"webp": "webp",
"avif": "avif",
"png": "png",
};
const type = mimeTypes[extension] || "png";
return `data:image/${type};base64,${b64encoded}`;
}; };
export const setCanvasZoomRatio = ( export const setCanvasZoomRatio = (
@ -130,7 +156,7 @@ export const setCanvasZoomRatio = (
const zoomRatioH = (innerHeight - 100) / canvas.height!; const zoomRatioH = (innerHeight - 100) / canvas.height!;
const zoomRatio = zoomRatioW < zoomRatioH ? zoomRatioW : zoomRatioH; const zoomRatio = zoomRatioW < zoomRatioH ? zoomRatioW : zoomRatioH;
zoomResetValue.set(zoomRatio); zoomResetValue.set(zoomRatio);
instance.zoomAbs(0, 0, zoomRatio); zoomReset(instance);
}; };
const addClozeNotesToTextEditor = (header: string, backExtra: string, tags: string[]) => { const addClozeNotesToTextEditor = (header: string, backExtra: string, tags: string[]) => {
@ -164,7 +190,7 @@ function containerSize(): Size {
export async function resetIOImage(path: string, onImageLoaded: (event: ImageLoadedEvent) => void) { export async function resetIOImage(path: string, onImageLoaded: (event: ImageLoadedEvent) => void) {
const imageData = await getImageForOcclusion({ path }); const imageData = await getImageForOcclusion({ path });
const image = document.getElementById("image") as HTMLImageElement; const image = document.getElementById("image") as HTMLImageElement;
image.src = getImageData(imageData.data!); image.src = getImageData(imageData.data!, path);
const canvas = globalThis.canvas; const canvas = globalThis.canvas;
image.onload = function() { image.onload = function() {

View file

@ -154,6 +154,12 @@ function setupImageOcclusionInner(setupOptions?: SetupImageOcclusionOptions): vo
} else { } else {
button.style.display = "none"; button.style.display = "none";
} }
window.addEventListener("keydown", (event) => {
if (event.key === "M") {
toggleMasks();
}
});
} }
drawShapes(canvas, setupOptions?.onWillDrawShapes, setupOptions?.onDidDrawShapes); drawShapes(canvas, setupOptions?.onWillDrawShapes, setupOptions?.onDidDrawShapes);

View file

@ -5,7 +5,6 @@ import type { Canvas, Object as FabricObject } from "fabric";
import { fabric } from "fabric"; import { fabric } from "fabric";
import { cloneDeep } from "lodash-es"; import { cloneDeep } from "lodash-es";
import { makeMaskTransparent } 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";
@ -36,7 +35,6 @@ export function exportShapesToClozeDeletions(occludeInactive: boolean): {
*/ */
export function baseShapesFromFabric(): ShapeOrShapes[] { export function baseShapesFromFabric(): ShapeOrShapes[] {
const canvas = globalThis.canvas as Canvas; const canvas = globalThis.canvas as Canvas;
makeMaskTransparent(canvas, false);
const activeObject = canvas.getActiveObject(); const activeObject = canvas.getActiveObject();
const selectionContainingMultipleObjects = activeObject instanceof fabric.ActiveSelection const selectionContainingMultipleObjects = activeObject instanceof fabric.ActiveSelection
&& (activeObject.size() > 1) && (activeObject.size() > 1)

View file

@ -13,3 +13,9 @@ export const tagsWritable = writable([""]);
export const ioMaskEditorVisible = writable(true); export const ioMaskEditorVisible = writable(true);
// it store hide all or hide one mode // it store hide all or hide one mode
export const hideAllGuessOne = writable(true); export const hideAllGuessOne = writable(true);
// store initial value of x for zoom reset
export const zoomResetX = writable(0);
// ioImageLoadedStore is used to store the image loaded event
export const ioImageLoadedStore = writable(false);
// store opacity state of objects in canvas
export const opacityStateStore = writable(false);

View file

@ -5,7 +5,7 @@ import type fabric from "fabric";
import type { PanZoom } from "panzoom"; import type { PanZoom } from "panzoom";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { zoomResetValue } from "../store"; import { opacityStateStore, zoomResetValue, zoomResetX } from "../store";
export const SHAPE_MASK_COLOR = "#ffeba2"; export const SHAPE_MASK_COLOR = "#ffeba2";
export const BORDER_COLOR = "#212121"; export const BORDER_COLOR = "#212121";
@ -59,8 +59,14 @@ export const groupShapes = (canvas: fabric.Canvas): void => {
return; return;
} }
canvas.getActiveObject().toGroup(); const activeObject = canvas.getActiveObject();
const items = activeObject.getObjects();
items.forEach((item) => {
item.set({ opacity: 1 });
});
activeObject.toGroup().set({
opacity: get(opacityStateStore) ? 0.4 : 1,
});
redraw(canvas); redraw(canvas);
}; };
@ -78,6 +84,7 @@ export const unGroupShapes = (canvas: fabric.Canvas): void => {
canvas.remove(group); canvas.remove(group);
items.forEach((item) => { items.forEach((item) => {
item.set({ opacity: get(opacityStateStore) ? 0.4 : 1 });
canvas.add(item); canvas.add(item);
}); });
@ -85,16 +92,35 @@ export const unGroupShapes = (canvas: fabric.Canvas): void => {
}; };
export const zoomIn = (instance: PanZoom): void => { export const zoomIn = (instance: PanZoom): void => {
instance.smoothZoom(0, 0, 1.25); const center = getCanvasCenter();
instance.smoothZoom(center.x, center.y, 1.25);
}; };
export const zoomOut = (instance: PanZoom): void => { export const zoomOut = (instance: PanZoom): void => {
instance.smoothZoom(0, 0, 0.5); const center = getCanvasCenter();
instance.smoothZoom(center.x, center.y, 0.8);
}; };
export const zoomReset = (instance: PanZoom): void => { export const zoomReset = (instance: PanZoom): void => {
instance.moveTo(0, 0); setCenterXForZoom(globalThis.canvas);
instance.smoothZoomAbs(0, 0, get(zoomResetValue)); instance.moveTo(get(zoomResetX), 0);
instance.smoothZoomAbs(get(zoomResetX), 0, get(zoomResetValue));
};
export const getCanvasCenter = () => {
const canvas = globalThis.canvas.getElement();
const rect = canvas.getBoundingClientRect();
const centerX = rect.x + rect.width / 2;
const centerY = rect.y + rect.height / 2;
return { x: centerX, y: centerY };
};
export const setCenterXForZoom = (canvas: fabric.Canvas) => {
const editor = document.querySelector(".editor-main")!;
const editorWidth = editor.clientWidth;
const canvasWidth = canvas.getElement().offsetWidth;
const centerX = editorWidth / 2 - canvasWidth / 2;
zoomResetX.set(centerX);
}; };
const copyItem = (canvas: fabric.Canvas): void => { const copyItem = (canvas: fabric.Canvas): void => {
@ -146,6 +172,7 @@ export const makeMaskTransparent = (
canvas: fabric.Canvas, canvas: fabric.Canvas,
opacity = false, opacity = false,
): void => { ): void => {
opacityStateStore.set(opacity);
const objects = canvas.getObjects(); const objects = canvas.getObjects();
objects.forEach((object) => { objects.forEach((object) => {
object.set({ object.set({
@ -261,3 +288,33 @@ export const redraw = (canvas: fabric.Canvas): void => {
export const clear = (canvas: fabric.Canvas): void => { export const clear = (canvas: fabric.Canvas): void => {
canvas.clear(); canvas.clear();
}; };
export const makeShapeRemainInCanvas = (canvas: fabric.Canvas) => {
canvas.on("object:moving", function(e) {
const obj = e.target;
if (obj.getScaledHeight() > obj.canvas.height || obj.getScaledWidth() > obj.canvas.width) {
return;
}
obj.setCoords();
if (obj.getBoundingRect().top < 0 || obj.getBoundingRect().left < 0) {
obj.top = Math.max(obj.top, obj.top - obj.getBoundingRect().top);
obj.left = Math.max(obj.left, obj.left - obj.getBoundingRect().left);
}
if (
obj.getBoundingRect().top + obj.getBoundingRect().height > obj.canvas.height
|| obj.getBoundingRect().left + obj.getBoundingRect().width > obj.canvas.width
) {
obj.top = Math.min(
obj.top,
obj.canvas.height - obj.getBoundingRect().height + obj.top - obj.getBoundingRect().top,
);
obj.left = Math.min(
obj.left,
obj.canvas.width - obj.getBoundingRect().width + obj.left - obj.getBoundingRect().left,
);
}
});
};

View file

@ -19,6 +19,21 @@ import {
mdiZoomReset, mdiZoomReset,
} from "../icons"; } from "../icons";
import { deleteItem, duplicateItem, groupShapes, unGroupShapes, zoomIn, zoomOut, zoomReset } from "./lib"; import { deleteItem, duplicateItem, groupShapes, unGroupShapes, zoomIn, zoomOut, zoomReset } from "./lib";
import {
alignBottomKeyCombination,
alignHorizontalCenterKeyCombination,
alignLeftKeyCombination,
alignRightKeyCombination,
alignTopKeyCombination,
alignVerticalCenterKeyCombination,
deleteKeyCombination,
duplicateKeyCombination,
groupKeyCombination,
ungroupKeyCombination,
zoomInKeyCombination,
zoomOutKeyCombination,
zoomResetKeyCombination,
} from "./shortcuts";
import { import {
alignBottom, alignBottom,
alignHorizontalCenter, alignHorizontalCenter,
@ -34,12 +49,14 @@ export const groupUngroupTools = [
icon: mdiGroup, icon: mdiGroup,
action: groupShapes, action: groupShapes,
tooltip: tr.editingImageOcclusionGroup, tooltip: tr.editingImageOcclusionGroup,
shortcut: groupKeyCombination,
}, },
{ {
name: "ungroup", name: "ungroup",
icon: mdiUngroup, icon: mdiUngroup,
action: unGroupShapes, action: unGroupShapes,
tooltip: tr.editingImageOcclusionUngroup, tooltip: tr.editingImageOcclusionUngroup,
shortcut: ungroupKeyCombination,
}, },
]; ];
@ -49,12 +66,14 @@ export const deleteDuplicateTools = [
icon: mdiDeleteOutline, icon: mdiDeleteOutline,
action: deleteItem, action: deleteItem,
tooltip: tr.editingImageOcclusionDelete, tooltip: tr.editingImageOcclusionDelete,
shortcut: deleteKeyCombination,
}, },
{ {
name: "duplicate", name: "duplicate",
icon: mdiCopy, icon: mdiCopy,
action: duplicateItem, action: duplicateItem,
tooltip: tr.editingImageOcclusionDuplicate, tooltip: tr.editingImageOcclusionDuplicate,
shortcut: duplicateKeyCombination,
}, },
]; ];
@ -64,18 +83,21 @@ export const zoomTools = [
icon: mdiZoomOut, icon: mdiZoomOut,
action: zoomOut, action: zoomOut,
tooltip: tr.editingImageOcclusionZoomOut, tooltip: tr.editingImageOcclusionZoomOut,
shortcut: zoomOutKeyCombination,
}, },
{ {
name: "zoomIn", name: "zoomIn",
icon: mdiZoomIn, icon: mdiZoomIn,
action: zoomIn, action: zoomIn,
tooltip: tr.editingImageOcclusionZoomIn, tooltip: tr.editingImageOcclusionZoomIn,
shortcut: zoomInKeyCombination,
}, },
{ {
name: "zoomReset", name: "zoomReset",
icon: mdiZoomReset, icon: mdiZoomReset,
action: zoomReset, action: zoomReset,
tooltip: tr.editingImageOcclusionZoomReset, tooltip: tr.editingImageOcclusionZoomReset,
shortcut: zoomResetKeyCombination,
}, },
]; ];
@ -85,35 +107,41 @@ export const alignTools = [
icon: mdiAlignHorizontalLeft, icon: mdiAlignHorizontalLeft,
action: alignLeft, action: alignLeft,
tooltip: tr.editingImageOcclusionAlignLeft, tooltip: tr.editingImageOcclusionAlignLeft,
shortcut: alignLeftKeyCombination,
}, },
{ {
id: 2, id: 2,
icon: mdiAlignHorizontalCenter, icon: mdiAlignHorizontalCenter,
action: alignHorizontalCenter, action: alignHorizontalCenter,
tooltip: tr.editingImageOcclusionAlignHCenter, tooltip: tr.editingImageOcclusionAlignHCenter,
shortcut: alignHorizontalCenterKeyCombination,
}, },
{ {
id: 3, id: 3,
icon: mdiAlignHorizontalRight, icon: mdiAlignHorizontalRight,
action: alignRight, action: alignRight,
tooltip: tr.editingImageOcclusionAlignRight, tooltip: tr.editingImageOcclusionAlignRight,
shortcut: alignRightKeyCombination,
}, },
{ {
id: 4, id: 4,
icon: mdiAlignVerticalTop, icon: mdiAlignVerticalTop,
action: alignTop, action: alignTop,
tooltip: tr.editingImageOcclusionAlignTop, tooltip: tr.editingImageOcclusionAlignTop,
shortcut: alignTopKeyCombination,
}, },
{ {
id: 5, id: 5,
icon: mdiAlignVerticalCenter, icon: mdiAlignVerticalCenter,
action: alignVerticalCenter, action: alignVerticalCenter,
tooltip: tr.editingImageOcclusionAlignVCenter, tooltip: tr.editingImageOcclusionAlignVCenter,
shortcut: alignVerticalCenterKeyCombination,
}, },
{ {
id: 6, id: 6,
icon: mdiAlignVerticalBottom, icon: mdiAlignVerticalBottom,
action: alignBottom, action: alignBottom,
tooltip: tr.editingImageOcclusionAlignBottom, tooltip: tr.editingImageOcclusionAlignBottom,
shortcut: alignBottomKeyCombination,
}, },
]; ];

View file

@ -0,0 +1,25 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export const cursorKeyCombination = "Control+S";
export const rectangleKeyCombination = "Control+R";
export const ellipseKeyCombination = "Control+E";
export const polygonKeyCombination = "Control+P";
export const textKeyCombination = "Control+T";
export const magnifyKeyCombination = "Control+M";
export const undoKeyCombination = "Control+Z";
export const redoKeyCombination = "Control+Y";
export const zoomOutKeyCombination = "Control+-";
export const zoomInKeyCombination = "Control++";
export const zoomResetKeyCombination = "Control+F";
export const toggleTranslucentKeyCombination = "Control+O";
export const deleteKeyCombination = "Delete";
export const duplicateKeyCombination = "Control+C";
export const groupKeyCombination = "Control+G";
export const ungroupKeyCombination = "Control+U";
export const alignLeftKeyCombination = "Control+Shift+L";
export const alignHorizontalCenterKeyCombination = "Control+Shift+H";
export const alignRightKeyCombination = "Control+Shift+R";
export const alignTopKeyCombination = "Control+Shift+T";
export const alignVerticalCenterKeyCombination = "Control+Shift+V";
export const alignBottomKeyCombination = "Control+Shift+B";

View file

@ -11,36 +11,50 @@ import {
mdiTextBox, mdiTextBox,
mdiVectorPolygonVariant, mdiVectorPolygonVariant,
} from "../icons"; } from "../icons";
import {
cursorKeyCombination,
ellipseKeyCombination,
magnifyKeyCombination,
polygonKeyCombination,
rectangleKeyCombination,
textKeyCombination,
} from "./shortcuts";
export const tools = [ export const tools = [
{ {
id: "cursor", id: "cursor",
icon: mdiCursorDefaultOutline, icon: mdiCursorDefaultOutline,
tooltip: tr.editingImageOcclusionSelectTool, tooltip: tr.editingImageOcclusionSelectTool,
shortcut: cursorKeyCombination,
}, },
{ {
id: "magnify", id: "magnify",
icon: mdiMagnifyScan, icon: mdiMagnifyScan,
tooltip: tr.editingImageOcclusionZoomTool, tooltip: tr.editingImageOcclusionZoomTool,
shortcut: magnifyKeyCombination,
}, },
{ {
id: "draw-rectangle", id: "draw-rectangle",
icon: mdiRectangleOutline, icon: mdiRectangleOutline,
tooltip: tr.editingImageOcclusionRectangleTool, tooltip: tr.editingImageOcclusionRectangleTool,
shortcut: rectangleKeyCombination,
}, },
{ {
id: "draw-ellipse", id: "draw-ellipse",
icon: mdiEllipseOutline, icon: mdiEllipseOutline,
tooltip: tr.editingImageOcclusionEllipseTool, tooltip: tr.editingImageOcclusionEllipseTool,
shortcut: ellipseKeyCombination,
}, },
{ {
id: "draw-polygon", id: "draw-polygon",
icon: mdiVectorPolygonVariant, icon: mdiVectorPolygonVariant,
tooltip: tr.editingImageOcclusionPolygonTool, tooltip: tr.editingImageOcclusionPolygonTool,
shortcut: polygonKeyCombination,
}, },
{ {
id: "draw-text", id: "draw-text",
icon: mdiTextBox, icon: mdiTextBox,
tooltip: tr.editingImageOcclusionTextTool, tooltip: tr.editingImageOcclusionTextTool,
shortcut: textKeyCombination,
}, },
]; ];

View file

@ -2,6 +2,8 @@
// 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
import { fabric } from "fabric"; import { fabric } from "fabric";
import { opacityStateStore } from "image-occlusion/store";
import { get } from "svelte/store";
import { BORDER_COLOR, SHAPE_MASK_COLOR, stopDraw } from "./lib"; import { BORDER_COLOR, SHAPE_MASK_COLOR, stopDraw } from "./lib";
import { undoStack } from "./tool-undo-redo"; import { undoStack } from "./tool-undo-redo";
@ -37,6 +39,7 @@ export const drawEllipse = (canvas: fabric.Canvas): void => {
strokeWidth: 1, strokeWidth: 1,
strokeUniform: true, strokeUniform: true,
noScaleCache: false, noScaleCache: false,
opacity: get(opacityStateStore) ? 0.4 : 1,
}); });
canvas.add(ellipse); canvas.add(ellipse);
}); });
@ -70,20 +73,6 @@ export const drawEllipse = (canvas: fabric.Canvas): void => {
ellipse.set({ originY: "top" }); ellipse.set({ originY: "top" });
} }
// do not draw outside of canvas
if (x < ellipse.strokeWidth) {
rx = (origX + ellipse.strokeWidth + 0.5) / 2;
}
if (y < ellipse.strokeWidth) {
ry = (origY + ellipse.strokeWidth + 0.5) / 2;
}
if (x >= canvas.width - ellipse.strokeWidth) {
rx = (canvas.width - origX) / 2 - ellipse.strokeWidth + 0.5;
}
if (y > canvas.height - ellipse.strokeWidth) {
ry = (canvas.height - origY) / 2 - ellipse.strokeWidth + 0.5;
}
ellipse.set({ rx: rx, ry: ry }); ellipse.set({ rx: rx, ry: ry });
canvas.renderAll(); canvas.renderAll();

View file

@ -2,7 +2,9 @@
// 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
import { fabric } from "fabric"; import { fabric } from "fabric";
import { opacityStateStore } from "image-occlusion/store";
import type { PanZoom } from "panzoom"; import type { PanZoom } from "panzoom";
import { get } from "svelte/store";
import { BORDER_COLOR, SHAPE_MASK_COLOR } from "./lib"; import { BORDER_COLOR, SHAPE_MASK_COLOR } from "./lib";
import { undoStack } from "./tool-undo-redo"; import { undoStack } from "./tool-undo-redo";
@ -15,6 +17,12 @@ let drawMode = false;
let zoomValue = 1; let zoomValue = 1;
export const drawPolygon = (canvas: fabric.Canvas, panzoom: PanZoom): void => { export const drawPolygon = (canvas: fabric.Canvas, panzoom: PanZoom): void => {
// remove selectable for shapes
canvas.discardActiveObject();
canvas.forEachObject(function(o) {
o.selectable = false;
});
canvas.selectionColor = "rgba(0, 0, 0, 0)"; canvas.selectionColor = "rgba(0, 0, 0, 0)";
canvas.on("mouse:down", function(options) { canvas.on("mouse:down", function(options) {
try { try {
@ -184,6 +192,7 @@ const generatePolygon = (canvas: fabric.Canvas, pointsList): void => {
strokeWidth: 1, strokeWidth: 1,
strokeUniform: true, strokeUniform: true,
noScaleCache: false, noScaleCache: false,
opacity: get(opacityStateStore) ? 0.4 : 1,
}); });
if (polygon.width > 5 && polygon.height > 5) { if (polygon.width > 5 && polygon.height > 5) {
canvas.add(polygon); canvas.add(polygon);
@ -214,8 +223,25 @@ export const modifiedPolygon = (canvas: fabric.Canvas, polygon: fabric.Polygon):
strokeWidth: 1, strokeWidth: 1,
strokeUniform: true, strokeUniform: true,
noScaleCache: false, noScaleCache: false,
opacity: get(opacityStateStore) ? 0.4 : 1,
}); });
canvas.remove(polygon); canvas.remove(polygon);
canvas.add(polygon1); canvas.add(polygon1);
}; };
export const removeUnfinishedPolygon = (canvas: fabric.Canvas): void => {
canvas.remove(activeShape).remove(activeLine);
pointsList.forEach((point) => {
canvas.remove(point);
});
linesList.forEach((line) => {
canvas.remove(line);
});
activeLine = null;
activeShape = null;
linesList = [];
pointsList = [];
drawMode = false;
canvas.selection = true;
};

View file

@ -2,6 +2,8 @@
// 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
import { fabric } from "fabric"; import { fabric } from "fabric";
import { opacityStateStore } from "image-occlusion/store";
import { get } from "svelte/store";
import { BORDER_COLOR, SHAPE_MASK_COLOR, stopDraw } from "./lib"; import { BORDER_COLOR, SHAPE_MASK_COLOR, stopDraw } from "./lib";
import { undoStack } from "./tool-undo-redo"; import { undoStack } from "./tool-undo-redo";
@ -38,6 +40,7 @@ export const drawRectangle = (canvas: fabric.Canvas): void => {
strokeWidth: 1, strokeWidth: 1,
strokeUniform: true, strokeUniform: true,
noScaleCache: false, noScaleCache: false,
opacity: get(opacityStateStore) ? 0.4 : 1,
}); });
canvas.add(rect); canvas.add(rect);
}); });
@ -47,8 +50,8 @@ export const drawRectangle = (canvas: fabric.Canvas): void => {
return; return;
} }
const pointer = canvas.getPointer(o.e); const pointer = canvas.getPointer(o.e);
let x = pointer.x; const x = pointer.x;
let y = pointer.y; const y = pointer.y;
if (x < origX) { if (x < origX) {
rect.set({ originX: "right" }); rect.set({ originX: "right" });
@ -62,20 +65,6 @@ export const drawRectangle = (canvas: fabric.Canvas): void => {
rect.set({ originY: "top" }); rect.set({ originY: "top" });
} }
// do not draw outside of canvas
if (x < rect.strokeWidth) {
x = -rect.strokeWidth + 0.5;
}
if (y < rect.strokeWidth) {
y = -rect.strokeWidth + 0.5;
}
if (x >= canvas.width - rect.strokeWidth) {
x = canvas.width - rect.strokeWidth + 0.5;
}
if (y >= canvas.height - rect.strokeWidth) {
y = canvas.height - rect.strokeWidth + 0.5;
}
rect.set({ rect.set({
width: Math.abs(x - rect.left), width: Math.abs(x - rect.left),
height: Math.abs(y - rect.top), height: Math.abs(y - rect.top),

View file

@ -2,6 +2,8 @@
// 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
import { fabric } from "fabric"; import { fabric } from "fabric";
import { opacityStateStore } from "image-occlusion/store";
import { get } from "svelte/store";
import { enableUniformScaling, stopDraw, TEXT_BACKGROUND_COLOR, TEXT_FONT_FAMILY, TEXT_PADDING } from "./lib"; import { enableUniformScaling, stopDraw, TEXT_BACKGROUND_COLOR, TEXT_FONT_FAMILY, TEXT_PADDING } from "./lib";
import { undoStack } from "./tool-undo-redo"; import { undoStack } from "./tool-undo-redo";
@ -27,6 +29,7 @@ export const drawText = (canvas: fabric.Canvas): void => {
fontFamily: TEXT_FONT_FAMILY, fontFamily: TEXT_FONT_FAMILY,
backgroundColor: TEXT_BACKGROUND_COLOR, backgroundColor: TEXT_BACKGROUND_COLOR,
padding: TEXT_PADDING, padding: TEXT_PADDING,
opacity: get(opacityStateStore) ? 0.4 : 1,
}); });
enableUniformScaling(canvas, text); enableUniformScaling(canvas, text);
canvas.add(text); canvas.add(text);

View file

@ -7,6 +7,7 @@ import { writable } from "svelte/store";
import { mdiRedo, mdiUndo } from "../icons"; import { mdiRedo, mdiUndo } from "../icons";
import { emitChangeSignal } from "../MaskEditor.svelte"; import { emitChangeSignal } from "../MaskEditor.svelte";
import { redoKeyCombination, undoKeyCombination } from "./shortcuts";
/** /**
* Undo redo for rectangle and ellipse handled here, * Undo redo for rectangle and ellipse handled here,
@ -133,11 +134,13 @@ export const undoRedoTools = [
icon: mdiUndo, icon: mdiUndo,
action: () => undoStack.undo(), action: () => undoStack.undo(),
tooltip: tr.undoUndo, tooltip: tr.undoUndo,
shortcut: undoKeyCombination,
}, },
{ {
name: "redo", name: "redo",
icon: mdiRedo, icon: mdiRedo,
action: () => undoStack.redo(), action: () => undoStack.redo(),
tooltip: tr.undoRedo, tooltip: tr.undoRedo,
shortcut: redoKeyCombination,
}, },
]; ];