mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
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:
parent
24e5912448
commit
be1f889211
21 changed files with 545 additions and 170 deletions
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,157 +173,215 @@ 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 class="top-tool-bar-container">
|
<div dir={$direction}>
|
||||||
<WithFloating
|
<div class="top-tool-bar-container">
|
||||||
show={showFloating}
|
<WithFloating
|
||||||
closeOnInsideClick
|
show={showFloating}
|
||||||
inline
|
closeOnInsideClick
|
||||||
on:close={() => (showFloating = false)}
|
inline
|
||||||
>
|
on:close={() => (showFloating = false)}
|
||||||
<IconButton
|
|
||||||
class="top-tool-icon-button right-border-radius dropdown-tool-mode"
|
|
||||||
slot="reference"
|
|
||||||
tooltip={tr.editingImageOcclusionMode()}
|
|
||||||
{iconSize}
|
|
||||||
on:click={() => (showFloating = !showFloating)}
|
|
||||||
>
|
>
|
||||||
{@html $hideAllGuessOne ? mdiViewDashboard : mdiSquare}
|
|
||||||
</IconButton>
|
|
||||||
|
|
||||||
<Popover slot="floating">
|
|
||||||
<DropdownItem
|
|
||||||
active={$hideAllGuessOne}
|
|
||||||
on:click={() => changeOcclusionType("all")}
|
|
||||||
>
|
|
||||||
<span>{tr.notetypesHideAllGuessOne()}</span>
|
|
||||||
</DropdownItem>
|
|
||||||
<DropdownItem
|
|
||||||
active={!$hideAllGuessOne}
|
|
||||||
on:click={() => changeOcclusionType("one")}
|
|
||||||
>
|
|
||||||
<span>{tr.notetypesHideOneGuessOne()}</span>
|
|
||||||
</DropdownItem>
|
|
||||||
</Popover>
|
|
||||||
</WithFloating>
|
|
||||||
|
|
||||||
<!-- undo & redo tools -->
|
|
||||||
<div class="undo-redo-button">
|
|
||||||
{#each undoRedoTools as tool}
|
|
||||||
<IconButton
|
<IconButton
|
||||||
class="top-tool-icon-button {tool.name === 'undo'
|
class="top-tool-icon-button border-radius dropdown-tool-mode"
|
||||||
? 'left-border-radius'
|
slot="reference"
|
||||||
: 'right-border-radius'}"
|
tooltip={tr.editingImageOcclusionMode()}
|
||||||
{iconSize}
|
{iconSize}
|
||||||
on:click={tool.action}
|
on:click={() => (showFloating = !showFloating)}
|
||||||
tooltip={tool.tooltip()}
|
|
||||||
disabled={tool.name === "undo"
|
|
||||||
? !$undoStack.undoable
|
|
||||||
: !$undoStack.redoable}
|
|
||||||
>
|
>
|
||||||
{@html tool.icon}
|
{@html $hideAllGuessOne ? mdiViewDashboard : mdiSquare}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- zoom tools -->
|
<Popover slot="floating">
|
||||||
<div class="tool-button-container">
|
<DropdownItem
|
||||||
{#each zoomTools as tool}
|
active={$hideAllGuessOne}
|
||||||
|
on:click={() => changeOcclusionType("all")}
|
||||||
|
>
|
||||||
|
<span>{tr.notetypesHideAllGuessOne()}</span>
|
||||||
|
</DropdownItem>
|
||||||
|
<DropdownItem
|
||||||
|
active={!$hideAllGuessOne}
|
||||||
|
on:click={() => changeOcclusionType("one")}
|
||||||
|
>
|
||||||
|
<span>{tr.notetypesHideOneGuessOne()}</span>
|
||||||
|
</DropdownItem>
|
||||||
|
</Popover>
|
||||||
|
</WithFloating>
|
||||||
|
|
||||||
|
<!-- undo & redo tools -->
|
||||||
|
<div class="undo-redo-button">
|
||||||
|
{#each undoRedoTools as tool}
|
||||||
|
<IconButton
|
||||||
|
class="top-tool-icon-button {tool.name === 'undo'
|
||||||
|
? 'left-border-radius'
|
||||||
|
: 'right-border-radius'}"
|
||||||
|
{iconSize}
|
||||||
|
on:click={tool.action}
|
||||||
|
tooltip="{tool.tooltip()} ({getPlatformString(tool.shortcut)})"
|
||||||
|
disabled={tool.name === "undo"
|
||||||
|
? !$undoStack.undoable
|
||||||
|
: !$undoStack.redoable}
|
||||||
|
>
|
||||||
|
{@html tool.icon}
|
||||||
|
</IconButton>
|
||||||
|
{#if $ioMaskEditorVisible}
|
||||||
|
<Shortcut keyCombination={tool.shortcut} on:action={tool.action} />
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- zoom tools -->
|
||||||
|
<div class="tool-button-container">
|
||||||
|
{#each zoomTools as tool}
|
||||||
|
<IconButton
|
||||||
|
class="top-tool-icon-button {tool.name === 'zoomOut'
|
||||||
|
? 'left-border-radius'
|
||||||
|
: ''} {tool.name === 'zoomReset' ? 'right-border-radius' : ''}"
|
||||||
|
{iconSize}
|
||||||
|
tooltip="{tool.tooltip()} ({getPlatformString(tool.shortcut)})"
|
||||||
|
on:click={() => {
|
||||||
|
tool.action(instance);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{@html tool.icon}
|
||||||
|
</IconButton>
|
||||||
|
{#if $ioMaskEditorVisible}
|
||||||
|
<Shortcut
|
||||||
|
keyCombination={tool.shortcut}
|
||||||
|
on:action={() => {
|
||||||
|
tool.action(instance);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tool-button-container">
|
||||||
|
<!-- opacity tools -->
|
||||||
<IconButton
|
<IconButton
|
||||||
class="top-tool-icon-button {tool.name === 'zoomOut'
|
class="top-tool-icon-button left-border-radius"
|
||||||
? 'left-border-radius'
|
|
||||||
: ''} {tool.name === 'zoomReset' ? 'right-border-radius' : ''}"
|
|
||||||
{iconSize}
|
{iconSize}
|
||||||
tooltip={tool.tooltip()}
|
tooltip={tr.editingImageOcclusionToggleTranslucent()}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
tool.action(instance);
|
maksOpacity = !maksOpacity;
|
||||||
|
makeMaskTransparent(canvas, maksOpacity);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{@html tool.icon}
|
{@html mdiEye}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
{/each}
|
{#if $ioMaskEditorVisible}
|
||||||
</div>
|
<Shortcut
|
||||||
|
keyCombination={toggleTranslucentKeyCombination}
|
||||||
|
on:action={() => {
|
||||||
|
maksOpacity = !maksOpacity;
|
||||||
|
makeMaskTransparent(canvas, maksOpacity);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="tool-button-container">
|
<!-- cursor tools -->
|
||||||
<!-- opacity tools -->
|
{#each deleteDuplicateTools as tool}
|
||||||
<IconButton
|
<IconButton
|
||||||
class="top-tool-icon-button left-border-radius"
|
class="top-tool-icon-button {tool.name === 'duplicate'
|
||||||
{iconSize}
|
? 'right-border-radius'
|
||||||
tooltip={tr.editingImageOcclusionToggleTranslucent()}
|
: ''}"
|
||||||
on:click={() => {
|
{iconSize}
|
||||||
maksOpacity = !maksOpacity;
|
tooltip="{tool.tooltip()} ({getPlatformString(tool.shortcut)})"
|
||||||
makeMaskTransparent(canvas, maksOpacity);
|
on:click={() => {
|
||||||
}}
|
tool.action(canvas);
|
||||||
>
|
}}
|
||||||
{@html mdiEye}
|
>
|
||||||
</IconButton>
|
{@html tool.icon}
|
||||||
|
</IconButton>
|
||||||
|
{#if $ioMaskEditorVisible}
|
||||||
|
<Shortcut
|
||||||
|
keyCombination={tool.shortcut}
|
||||||
|
on:action={() => {
|
||||||
|
tool.action(canvas);
|
||||||
|
emitChangeSignal();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tool-button-container">
|
||||||
|
<!-- group & ungroup tools -->
|
||||||
|
{#each groupUngroupTools as tool}
|
||||||
|
<IconButton
|
||||||
|
class="top-tool-icon-button {tool.name === 'group'
|
||||||
|
? 'left-border-radius'
|
||||||
|
: ''}"
|
||||||
|
{iconSize}
|
||||||
|
tooltip="{tool.tooltip()} ({getPlatformString(tool.shortcut)})"
|
||||||
|
on:click={() => {
|
||||||
|
tool.action(canvas);
|
||||||
|
emitChangeSignal();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{@html tool.icon}
|
||||||
|
</IconButton>
|
||||||
|
{#if $ioMaskEditorVisible}
|
||||||
|
<Shortcut
|
||||||
|
keyCombination={tool.shortcut}
|
||||||
|
on:action={() => {
|
||||||
|
tool.action(canvas);
|
||||||
|
emitChangeSignal();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
|
||||||
<!-- cursor tools -->
|
|
||||||
{#each deleteDuplicateTools as tool}
|
|
||||||
<IconButton
|
<IconButton
|
||||||
class="top-tool-icon-button {tool.name === 'duplicate'
|
class="top-tool-icon-button dropdown-tool right-border-radius"
|
||||||
? 'right-border-radius'
|
|
||||||
: ''}"
|
|
||||||
{iconSize}
|
{iconSize}
|
||||||
tooltip={tool.tooltip()}
|
tooltip={tr.editingImageOcclusionAlignment()}
|
||||||
on:click={() => {
|
on:click={(e) => {
|
||||||
tool.action(canvas);
|
showAlignTools = !showAlignTools;
|
||||||
|
leftPos = e.pageX - 100;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{@html tool.icon}
|
{@html mdiFormatAlignCenter}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
{/each}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tool-button-container">
|
<div class:show={showAlignTools} class="dropdown-content" style="left:{leftPos}px;">
|
||||||
<!-- group & ungroup tools -->
|
{#each alignTools as alignTool}
|
||||||
{#each groupUngroupTools as tool}
|
|
||||||
<IconButton
|
<IconButton
|
||||||
class="top-tool-icon-button {tool.name === 'group'
|
class="top-tool-icon-button"
|
||||||
? 'left-border-radius'
|
|
||||||
: ''}"
|
|
||||||
{iconSize}
|
{iconSize}
|
||||||
tooltip={tool.tooltip()}
|
tooltip="{alignTool.tooltip()} ({getPlatformString(
|
||||||
|
alignTool.shortcut,
|
||||||
|
)})"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
tool.action(canvas);
|
alignTool.action(canvas);
|
||||||
emitChangeSignal();
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{@html tool.icon}
|
{@html alignTool.icon}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
{#if $ioMaskEditorVisible}
|
||||||
|
<Shortcut
|
||||||
|
keyCombination={alignTool.shortcut}
|
||||||
|
on:action={() => {
|
||||||
|
alignTool.action(canvas);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<IconButton
|
|
||||||
class="top-tool-icon-button dropdown-tool right-border-radius"
|
|
||||||
{iconSize}
|
|
||||||
tooltip={tr.editingImageOcclusionAlignment()}
|
|
||||||
on:click={(e) => {
|
|
||||||
showAlignTools = !showAlignTools;
|
|
||||||
leftPos = e.pageX - 100;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{@html mdiFormatAlignCenter}
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class:show={showAlignTools} class="dropdown-content" style="left:{leftPos}px;">
|
|
||||||
{#each alignTools as alignTool}
|
|
||||||
<IconButton
|
|
||||||
class="top-tool-icon-button"
|
|
||||||
{iconSize}
|
|
||||||
tooltip={alignTool.tooltip()}
|
|
||||||
on:click={() => {
|
|
||||||
alignTool.action(canvas);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{@html alignTool.icon}
|
|
||||||
</IconButton>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.top-tool-bar-container {
|
.top-tool-bar-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -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;
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
25
ts/image-occlusion/tools/shortcuts.ts
Normal file
25
ts/image-occlusion/tools/shortcuts.ts
Normal 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";
|
|
@ -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,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
Loading…
Reference in a new issue