mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02: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-text-tool = Text
|
||||
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.
|
||||
|
||||
|
|
|
@ -74,6 +74,7 @@ message GetImageOcclusionNoteResponse {
|
|||
string header = 3;
|
||||
string back_extra = 4;
|
||||
repeated string tags = 5;
|
||||
string image_file_name = 6;
|
||||
}
|
||||
|
||||
oneof value {
|
||||
|
|
|
@ -110,6 +110,12 @@ impl Collection {
|
|||
|
||||
if self.is_image_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)
|
||||
|
|
|
@ -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 type { IOMode } from "image-occlusion/lib";
|
||||
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 CollapseLabel from "./CollapseLabel.svelte";
|
||||
import * as oldEditorAdapter from "./old-editor-adapter";
|
||||
|
||||
let isIOImageLoaded = false;
|
||||
$: isIOImageLoaded = false;
|
||||
$: ioImageLoadedStore.set(isIOImageLoaded);
|
||||
let imageOcclusionMode: IOMode | undefined;
|
||||
let ioFields = new ImageOcclusionFieldIndexes({});
|
||||
|
||||
|
@ -456,6 +461,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
function resetIOImageLoaded() {
|
||||
isIOImageLoaded = false;
|
||||
globalThis.canvas.clear();
|
||||
globalThis.canvas = undefined;
|
||||
const page = document.querySelector(".image-occlusion");
|
||||
if (page) {
|
||||
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) {
|
||||
height: 36px !important;
|
||||
line-height: 1;
|
||||
}
|
||||
:global(.image-occlusion .tool-bar-container) {
|
||||
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 DynamicallySlottable from "components/DynamicallySlottable.svelte";
|
||||
import IconButton from "components/IconButton.svelte";
|
||||
import { ioMaskEditorVisible } from "image-occlusion/store";
|
||||
import { ioImageLoadedStore, ioMaskEditorVisible } from "image-occlusion/store";
|
||||
|
||||
import ButtonGroupItem, {
|
||||
createProps,
|
||||
setSlotHostContext,
|
||||
updatePropsList,
|
||||
} from "../../components/ButtonGroupItem.svelte";
|
||||
import { mdiViewDashboard } from "./icons";
|
||||
import { mdiTableRefresh, mdiViewDashboard } from "./icons";
|
||||
|
||||
export let api = {};
|
||||
</script>
|
||||
|
@ -39,6 +39,22 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
{@html mdiViewDashboard}
|
||||
</IconButton>
|
||||
</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>
|
||||
</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 paperclipIcon } from "@mdi/svg/svg/paperclip.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 eraserIcon } from "bootstrap-icons/icons/eraser.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";
|
||||
import Toolbar from "./Toolbar.svelte";
|
||||
import { MaskEditorAPI } from "./tools/api";
|
||||
import { setCenterXForZoom } from "./tools/lib";
|
||||
|
||||
export let mode: IOMode;
|
||||
const iconSize = 80;
|
||||
|
@ -57,8 +58,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
minZoom: 0.1,
|
||||
zoomDoubleClickSpeed: 1,
|
||||
smoothScroll: false,
|
||||
transformOrigin: { x: 0.5, y: 0.5 },
|
||||
});
|
||||
instance.pause();
|
||||
globalThis.panzoom = instance;
|
||||
|
||||
if (mode.kind == "add") {
|
||||
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(() => {
|
||||
window.addEventListener("resize", () => {
|
||||
setCanvasZoomRatio(canvas, instance);
|
||||
setCenterXForZoom(canvas);
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener("resize", () => {
|
||||
setCanvasZoomRatio(canvas, instance);
|
||||
setCenterXForZoom(canvas);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
@ -109,10 +114,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
padding-bottom: 100px;
|
||||
}
|
||||
|
||||
:global([dir="rtl"]) .editor-main {
|
||||
left: 2px;
|
||||
right: 36px;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
#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
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { directionKey } from "@tslib/context-keys";
|
||||
import * as tr from "@tslib/ftl";
|
||||
import { getPlatformString } from "@tslib/shortcuts";
|
||||
import DropdownItem from "components/DropdownItem.svelte";
|
||||
import IconButton from "components/IconButton.svelte";
|
||||
import Popover from "components/Popover.svelte";
|
||||
import Shortcut from "components/Shortcut.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 { emitChangeSignal } from "./MaskEditor.svelte";
|
||||
import { hideAllGuessOne } from "./store";
|
||||
import { hideAllGuessOne, ioMaskEditorVisible } from "./store";
|
||||
import { drawEllipse, drawPolygon, drawRectangle, drawText } from "./tools/index";
|
||||
import { makeMaskTransparent } 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,
|
||||
zoomTools,
|
||||
} from "./tools/more-tools";
|
||||
import { toggleTranslucentKeyCombination } from "./tools/shortcuts";
|
||||
import { tools } from "./tools/tool-buttons";
|
||||
import { removeUnfinishedPolygon } from "./tools/tool-polygon";
|
||||
import { undoRedoTools, undoStack } from "./tools/tool-undo-redo";
|
||||
|
||||
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 maksOpacity = false;
|
||||
let showFloating = false;
|
||||
const direction = getContext<Readable<"ltr" | "rtl">>(directionKey);
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
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
|
||||
$: if (instance && canvas) {
|
||||
disableFunctions();
|
||||
enableSelectable(canvas, true);
|
||||
// remove unfinished polygon when switching to other tools
|
||||
removeUnfinishedPolygon(canvas);
|
||||
|
||||
switch (activeTool) {
|
||||
case "magnify":
|
||||
|
@ -77,6 +152,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
$hideAllGuessOne = occlusionType === "all";
|
||||
emitChangeSignal();
|
||||
}
|
||||
const enableMagnify = () => {
|
||||
disableFunctions();
|
||||
enableSelectable(canvas, false);
|
||||
instance.resume();
|
||||
activeTool = "magnify";
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="tool-bar-container">
|
||||
|
@ -84,7 +165,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<IconButton
|
||||
class="tool-icon-button {activeTool == tool.id ? 'active-tool' : ''}"
|
||||
{iconSize}
|
||||
tooltip={tool.tooltip()}
|
||||
tooltip="{tool.tooltip()} ({getPlatformString(tool.shortcut)})"
|
||||
active={activeTool === tool.id}
|
||||
on:click={() => {
|
||||
activeTool = tool.id;
|
||||
|
@ -92,157 +173,215 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
>
|
||||
{@html tool.icon}
|
||||
</IconButton>
|
||||
{#if $ioMaskEditorVisible}
|
||||
<Shortcut
|
||||
keyCombination={tool.shortcut}
|
||||
on:action={() => {
|
||||
activeTool = tool.id;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="top-tool-bar-container">
|
||||
<WithFloating
|
||||
show={showFloating}
|
||||
closeOnInsideClick
|
||||
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)}
|
||||
<div dir={$direction}>
|
||||
<div class="top-tool-bar-container">
|
||||
<WithFloating
|
||||
show={showFloating}
|
||||
closeOnInsideClick
|
||||
inline
|
||||
on:close={() => (showFloating = false)}
|
||||
>
|
||||
{@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
|
||||
class="top-tool-icon-button {tool.name === 'undo'
|
||||
? 'left-border-radius'
|
||||
: 'right-border-radius'}"
|
||||
class="top-tool-icon-button border-radius dropdown-tool-mode"
|
||||
slot="reference"
|
||||
tooltip={tr.editingImageOcclusionMode()}
|
||||
{iconSize}
|
||||
on:click={tool.action}
|
||||
tooltip={tool.tooltip()}
|
||||
disabled={tool.name === "undo"
|
||||
? !$undoStack.undoable
|
||||
: !$undoStack.redoable}
|
||||
on:click={() => (showFloating = !showFloating)}
|
||||
>
|
||||
{@html tool.icon}
|
||||
{@html $hideAllGuessOne ? mdiViewDashboard : mdiSquare}
|
||||
</IconButton>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- zoom tools -->
|
||||
<div class="tool-button-container">
|
||||
{#each zoomTools as tool}
|
||||
<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
|
||||
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
|
||||
class="top-tool-icon-button {tool.name === 'zoomOut'
|
||||
? 'left-border-radius'
|
||||
: ''} {tool.name === 'zoomReset' ? 'right-border-radius' : ''}"
|
||||
class="top-tool-icon-button left-border-radius"
|
||||
{iconSize}
|
||||
tooltip={tool.tooltip()}
|
||||
tooltip={tr.editingImageOcclusionToggleTranslucent()}
|
||||
on:click={() => {
|
||||
tool.action(instance);
|
||||
maksOpacity = !maksOpacity;
|
||||
makeMaskTransparent(canvas, maksOpacity);
|
||||
}}
|
||||
>
|
||||
{@html tool.icon}
|
||||
{@html mdiEye}
|
||||
</IconButton>
|
||||
{/each}
|
||||
</div>
|
||||
{#if $ioMaskEditorVisible}
|
||||
<Shortcut
|
||||
keyCombination={toggleTranslucentKeyCombination}
|
||||
on:action={() => {
|
||||
maksOpacity = !maksOpacity;
|
||||
makeMaskTransparent(canvas, maksOpacity);
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="tool-button-container">
|
||||
<!-- opacity tools -->
|
||||
<IconButton
|
||||
class="top-tool-icon-button left-border-radius"
|
||||
{iconSize}
|
||||
tooltip={tr.editingImageOcclusionToggleTranslucent()}
|
||||
on:click={() => {
|
||||
maksOpacity = !maksOpacity;
|
||||
makeMaskTransparent(canvas, maksOpacity);
|
||||
}}
|
||||
>
|
||||
{@html mdiEye}
|
||||
</IconButton>
|
||||
<!-- cursor tools -->
|
||||
{#each deleteDuplicateTools as tool}
|
||||
<IconButton
|
||||
class="top-tool-icon-button {tool.name === 'duplicate'
|
||||
? 'right-border-radius'
|
||||
: ''}"
|
||||
{iconSize}
|
||||
tooltip="{tool.tooltip()} ({getPlatformString(tool.shortcut)})"
|
||||
on:click={() => {
|
||||
tool.action(canvas);
|
||||
}}
|
||||
>
|
||||
{@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
|
||||
class="top-tool-icon-button {tool.name === 'duplicate'
|
||||
? 'right-border-radius'
|
||||
: ''}"
|
||||
class="top-tool-icon-button dropdown-tool right-border-radius"
|
||||
{iconSize}
|
||||
tooltip={tool.tooltip()}
|
||||
on:click={() => {
|
||||
tool.action(canvas);
|
||||
tooltip={tr.editingImageOcclusionAlignment()}
|
||||
on:click={(e) => {
|
||||
showAlignTools = !showAlignTools;
|
||||
leftPos = e.pageX - 100;
|
||||
}}
|
||||
>
|
||||
{@html tool.icon}
|
||||
{@html mdiFormatAlignCenter}
|
||||
</IconButton>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tool-button-container">
|
||||
<!-- group & ungroup tools -->
|
||||
{#each groupUngroupTools as tool}
|
||||
<div class:show={showAlignTools} class="dropdown-content" style="left:{leftPos}px;">
|
||||
{#each alignTools as alignTool}
|
||||
<IconButton
|
||||
class="top-tool-icon-button {tool.name === 'group'
|
||||
? 'left-border-radius'
|
||||
: ''}"
|
||||
class="top-tool-icon-button"
|
||||
{iconSize}
|
||||
tooltip={tool.tooltip()}
|
||||
tooltip="{alignTool.tooltip()} ({getPlatformString(
|
||||
alignTool.shortcut,
|
||||
)})"
|
||||
on:click={() => {
|
||||
tool.action(canvas);
|
||||
emitChangeSignal();
|
||||
alignTool.action(canvas);
|
||||
}}
|
||||
>
|
||||
{@html tool.icon}
|
||||
{@html alignTool.icon}
|
||||
</IconButton>
|
||||
{#if $ioMaskEditorVisible}
|
||||
<Shortcut
|
||||
keyCombination={alignTool.shortcut}
|
||||
on:action={() => {
|
||||
alignTool.action(canvas);
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{/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 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>
|
||||
.top-tool-bar-container {
|
||||
display: flex;
|
||||
|
@ -252,6 +391,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
margin-top: 2px;
|
||||
}
|
||||
|
||||
:global([dir="rtl"] .top-tool-bar-container) {
|
||||
margin-left: unset;
|
||||
margin-right: 28px;
|
||||
}
|
||||
|
||||
.undo-redo-button {
|
||||
margin-right: 2px;
|
||||
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;
|
||||
}
|
||||
|
||||
:global([dir="rtl"] .left-border-radius) {
|
||||
border-radius: 0 5px 5px 0 !important;
|
||||
}
|
||||
|
||||
:global(.right-border-radius) {
|
||||
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) {
|
||||
border: unset;
|
||||
display: inline;
|
||||
|
@ -290,7 +446,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
|
||||
.show {
|
||||
display: block;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
|
@ -311,6 +467,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
padding-bottom: 100px;
|
||||
}
|
||||
|
||||
:global([dir="rtl"] .tool-bar-container) {
|
||||
left: unset;
|
||||
right: 2px;
|
||||
}
|
||||
|
||||
:global(.tool-icon-button) {
|
||||
border: unset;
|
||||
display: block;
|
||||
|
|
|
@ -12,7 +12,13 @@ import { optimumCssSizeForCanvas } from "./canvas-scale";
|
|||
import { notesDataStore, tagsWritable, zoomResetValue } from "./store";
|
||||
import Toast from "./Toast.svelte";
|
||||
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 { undoStack } from "./tools/tool-undo-redo";
|
||||
import type { Size } from "./types";
|
||||
|
@ -33,7 +39,7 @@ export const setupMaskEditor = async (
|
|||
|
||||
// get image width and height
|
||||
const image = document.getElementById("image") as HTMLImageElement;
|
||||
image.src = getImageData(imageData.data!);
|
||||
image.src = getImageData(imageData.data!, path);
|
||||
image.onload = function() {
|
||||
const size = optimumCssSizeForCanvas({ width: image.width, height: image.height }, containerSize());
|
||||
canvas.setWidth(size.width);
|
||||
|
@ -73,7 +79,7 @@ export const setupMaskEditorForEdit = async (
|
|||
// get image width and height
|
||||
const image = document.getElementById("image") as HTMLImageElement;
|
||||
image.style.visibility = "hidden";
|
||||
image.src = getImageData(clozeNote.imageData!);
|
||||
image.src = getImageData(clozeNote.imageData!, clozeNote.imageFileName!);
|
||||
image.onload = function() {
|
||||
const size = optimumCssSizeForCanvas({ width: image.width, height: image.height }, containerSize());
|
||||
canvas.setWidth(size.width);
|
||||
|
@ -100,12 +106,19 @@ function initCanvas(onChange: () => void): fabric.Canvas {
|
|||
tagsWritable.set([]);
|
||||
globalThis.canvas = 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
|
||||
canvas.uniformScaling = false;
|
||||
canvas.uniScaleKey = "none";
|
||||
// disable rotation globally
|
||||
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);
|
||||
makeShapeRemainInCanvas(canvas);
|
||||
canvas.on("object:modified", (evt) => {
|
||||
if (evt.target instanceof fabric.Polygon) {
|
||||
modifiedPolygon(canvas, evt.target);
|
||||
|
@ -114,12 +127,25 @@ function initCanvas(onChange: () => void): fabric.Canvas {
|
|||
onChange();
|
||||
});
|
||||
canvas.on("object:removed", onChange);
|
||||
setCenterXForZoom(canvas);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
const getImageData = (imageData): string => {
|
||||
const getImageData = (imageData, path): string => {
|
||||
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 = (
|
||||
|
@ -130,7 +156,7 @@ export const setCanvasZoomRatio = (
|
|||
const zoomRatioH = (innerHeight - 100) / canvas.height!;
|
||||
const zoomRatio = zoomRatioW < zoomRatioH ? zoomRatioW : zoomRatioH;
|
||||
zoomResetValue.set(zoomRatio);
|
||||
instance.zoomAbs(0, 0, zoomRatio);
|
||||
zoomReset(instance);
|
||||
};
|
||||
|
||||
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) {
|
||||
const imageData = await getImageForOcclusion({ path });
|
||||
const image = document.getElementById("image") as HTMLImageElement;
|
||||
image.src = getImageData(imageData.data!);
|
||||
image.src = getImageData(imageData.data!, path);
|
||||
const canvas = globalThis.canvas;
|
||||
|
||||
image.onload = function() {
|
||||
|
|
|
@ -154,6 +154,12 @@ function setupImageOcclusionInner(setupOptions?: SetupImageOcclusionOptions): vo
|
|||
} else {
|
||||
button.style.display = "none";
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if (event.key === "M") {
|
||||
toggleMasks();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
drawShapes(canvas, setupOptions?.onWillDrawShapes, setupOptions?.onDidDrawShapes);
|
||||
|
|
|
@ -5,7 +5,6 @@ import type { Canvas, Object as FabricObject } from "fabric";
|
|||
import { fabric } from "fabric";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
|
||||
import { makeMaskTransparent } from "../tools/lib";
|
||||
import type { Size } from "../types";
|
||||
import type { Shape, ShapeOrShapes } from "./base";
|
||||
import { Ellipse } from "./ellipse";
|
||||
|
@ -36,7 +35,6 @@ export function exportShapesToClozeDeletions(occludeInactive: boolean): {
|
|||
*/
|
||||
export function baseShapesFromFabric(): ShapeOrShapes[] {
|
||||
const canvas = globalThis.canvas as Canvas;
|
||||
makeMaskTransparent(canvas, false);
|
||||
const activeObject = canvas.getActiveObject();
|
||||
const selectionContainingMultipleObjects = activeObject instanceof fabric.ActiveSelection
|
||||
&& (activeObject.size() > 1)
|
||||
|
|
|
@ -13,3 +13,9 @@ export const tagsWritable = writable([""]);
|
|||
export const ioMaskEditorVisible = writable(true);
|
||||
// it store hide all or hide one mode
|
||||
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 { get } from "svelte/store";
|
||||
|
||||
import { zoomResetValue } from "../store";
|
||||
import { opacityStateStore, zoomResetValue, zoomResetX } from "../store";
|
||||
|
||||
export const SHAPE_MASK_COLOR = "#ffeba2";
|
||||
export const BORDER_COLOR = "#212121";
|
||||
|
@ -59,8 +59,14 @@ export const groupShapes = (canvas: fabric.Canvas): void => {
|
|||
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);
|
||||
};
|
||||
|
||||
|
@ -78,6 +84,7 @@ export const unGroupShapes = (canvas: fabric.Canvas): void => {
|
|||
canvas.remove(group);
|
||||
|
||||
items.forEach((item) => {
|
||||
item.set({ opacity: get(opacityStateStore) ? 0.4 : 1 });
|
||||
canvas.add(item);
|
||||
});
|
||||
|
||||
|
@ -85,16 +92,35 @@ export const unGroupShapes = (canvas: fabric.Canvas): 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 => {
|
||||
instance.smoothZoom(0, 0, 0.5);
|
||||
const center = getCanvasCenter();
|
||||
instance.smoothZoom(center.x, center.y, 0.8);
|
||||
};
|
||||
|
||||
export const zoomReset = (instance: PanZoom): void => {
|
||||
instance.moveTo(0, 0);
|
||||
instance.smoothZoomAbs(0, 0, get(zoomResetValue));
|
||||
setCenterXForZoom(globalThis.canvas);
|
||||
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 => {
|
||||
|
@ -146,6 +172,7 @@ export const makeMaskTransparent = (
|
|||
canvas: fabric.Canvas,
|
||||
opacity = false,
|
||||
): void => {
|
||||
opacityStateStore.set(opacity);
|
||||
const objects = canvas.getObjects();
|
||||
objects.forEach((object) => {
|
||||
object.set({
|
||||
|
@ -261,3 +288,33 @@ export const redraw = (canvas: fabric.Canvas): void => {
|
|||
export const clear = (canvas: fabric.Canvas): void => {
|
||||
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,
|
||||
} from "../icons";
|
||||
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 {
|
||||
alignBottom,
|
||||
alignHorizontalCenter,
|
||||
|
@ -34,12 +49,14 @@ export const groupUngroupTools = [
|
|||
icon: mdiGroup,
|
||||
action: groupShapes,
|
||||
tooltip: tr.editingImageOcclusionGroup,
|
||||
shortcut: groupKeyCombination,
|
||||
},
|
||||
{
|
||||
name: "ungroup",
|
||||
icon: mdiUngroup,
|
||||
action: unGroupShapes,
|
||||
tooltip: tr.editingImageOcclusionUngroup,
|
||||
shortcut: ungroupKeyCombination,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -49,12 +66,14 @@ export const deleteDuplicateTools = [
|
|||
icon: mdiDeleteOutline,
|
||||
action: deleteItem,
|
||||
tooltip: tr.editingImageOcclusionDelete,
|
||||
shortcut: deleteKeyCombination,
|
||||
},
|
||||
{
|
||||
name: "duplicate",
|
||||
icon: mdiCopy,
|
||||
action: duplicateItem,
|
||||
tooltip: tr.editingImageOcclusionDuplicate,
|
||||
shortcut: duplicateKeyCombination,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -64,18 +83,21 @@ export const zoomTools = [
|
|||
icon: mdiZoomOut,
|
||||
action: zoomOut,
|
||||
tooltip: tr.editingImageOcclusionZoomOut,
|
||||
shortcut: zoomOutKeyCombination,
|
||||
},
|
||||
{
|
||||
name: "zoomIn",
|
||||
icon: mdiZoomIn,
|
||||
action: zoomIn,
|
||||
tooltip: tr.editingImageOcclusionZoomIn,
|
||||
shortcut: zoomInKeyCombination,
|
||||
},
|
||||
{
|
||||
name: "zoomReset",
|
||||
icon: mdiZoomReset,
|
||||
action: zoomReset,
|
||||
tooltip: tr.editingImageOcclusionZoomReset,
|
||||
shortcut: zoomResetKeyCombination,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -85,35 +107,41 @@ export const alignTools = [
|
|||
icon: mdiAlignHorizontalLeft,
|
||||
action: alignLeft,
|
||||
tooltip: tr.editingImageOcclusionAlignLeft,
|
||||
shortcut: alignLeftKeyCombination,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: mdiAlignHorizontalCenter,
|
||||
action: alignHorizontalCenter,
|
||||
tooltip: tr.editingImageOcclusionAlignHCenter,
|
||||
shortcut: alignHorizontalCenterKeyCombination,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: mdiAlignHorizontalRight,
|
||||
action: alignRight,
|
||||
tooltip: tr.editingImageOcclusionAlignRight,
|
||||
shortcut: alignRightKeyCombination,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
icon: mdiAlignVerticalTop,
|
||||
action: alignTop,
|
||||
tooltip: tr.editingImageOcclusionAlignTop,
|
||||
shortcut: alignTopKeyCombination,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
icon: mdiAlignVerticalCenter,
|
||||
action: alignVerticalCenter,
|
||||
tooltip: tr.editingImageOcclusionAlignVCenter,
|
||||
shortcut: alignVerticalCenterKeyCombination,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
icon: mdiAlignVerticalBottom,
|
||||
action: alignBottom,
|
||||
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,
|
||||
mdiVectorPolygonVariant,
|
||||
} from "../icons";
|
||||
import {
|
||||
cursorKeyCombination,
|
||||
ellipseKeyCombination,
|
||||
magnifyKeyCombination,
|
||||
polygonKeyCombination,
|
||||
rectangleKeyCombination,
|
||||
textKeyCombination,
|
||||
} from "./shortcuts";
|
||||
|
||||
export const tools = [
|
||||
{
|
||||
id: "cursor",
|
||||
icon: mdiCursorDefaultOutline,
|
||||
tooltip: tr.editingImageOcclusionSelectTool,
|
||||
shortcut: cursorKeyCombination,
|
||||
},
|
||||
{
|
||||
id: "magnify",
|
||||
icon: mdiMagnifyScan,
|
||||
tooltip: tr.editingImageOcclusionZoomTool,
|
||||
shortcut: magnifyKeyCombination,
|
||||
},
|
||||
{
|
||||
id: "draw-rectangle",
|
||||
icon: mdiRectangleOutline,
|
||||
tooltip: tr.editingImageOcclusionRectangleTool,
|
||||
shortcut: rectangleKeyCombination,
|
||||
},
|
||||
{
|
||||
id: "draw-ellipse",
|
||||
icon: mdiEllipseOutline,
|
||||
tooltip: tr.editingImageOcclusionEllipseTool,
|
||||
shortcut: ellipseKeyCombination,
|
||||
},
|
||||
{
|
||||
id: "draw-polygon",
|
||||
icon: mdiVectorPolygonVariant,
|
||||
tooltip: tr.editingImageOcclusionPolygonTool,
|
||||
shortcut: polygonKeyCombination,
|
||||
},
|
||||
{
|
||||
id: "draw-text",
|
||||
icon: mdiTextBox,
|
||||
tooltip: tr.editingImageOcclusionTextTool,
|
||||
shortcut: textKeyCombination,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
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 { undoStack } from "./tool-undo-redo";
|
||||
|
@ -37,6 +39,7 @@ export const drawEllipse = (canvas: fabric.Canvas): void => {
|
|||
strokeWidth: 1,
|
||||
strokeUniform: true,
|
||||
noScaleCache: false,
|
||||
opacity: get(opacityStateStore) ? 0.4 : 1,
|
||||
});
|
||||
canvas.add(ellipse);
|
||||
});
|
||||
|
@ -70,20 +73,6 @@ export const drawEllipse = (canvas: fabric.Canvas): void => {
|
|||
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 });
|
||||
|
||||
canvas.renderAll();
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import { fabric } from "fabric";
|
||||
import { opacityStateStore } from "image-occlusion/store";
|
||||
import type { PanZoom } from "panzoom";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
import { BORDER_COLOR, SHAPE_MASK_COLOR } from "./lib";
|
||||
import { undoStack } from "./tool-undo-redo";
|
||||
|
@ -15,6 +17,12 @@ let drawMode = false;
|
|||
let zoomValue = 1;
|
||||
|
||||
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.on("mouse:down", function(options) {
|
||||
try {
|
||||
|
@ -184,6 +192,7 @@ const generatePolygon = (canvas: fabric.Canvas, pointsList): void => {
|
|||
strokeWidth: 1,
|
||||
strokeUniform: true,
|
||||
noScaleCache: false,
|
||||
opacity: get(opacityStateStore) ? 0.4 : 1,
|
||||
});
|
||||
if (polygon.width > 5 && polygon.height > 5) {
|
||||
canvas.add(polygon);
|
||||
|
@ -214,8 +223,25 @@ export const modifiedPolygon = (canvas: fabric.Canvas, polygon: fabric.Polygon):
|
|||
strokeWidth: 1,
|
||||
strokeUniform: true,
|
||||
noScaleCache: false,
|
||||
opacity: get(opacityStateStore) ? 0.4 : 1,
|
||||
});
|
||||
|
||||
canvas.remove(polygon);
|
||||
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
|
||||
|
||||
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 { undoStack } from "./tool-undo-redo";
|
||||
|
@ -38,6 +40,7 @@ export const drawRectangle = (canvas: fabric.Canvas): void => {
|
|||
strokeWidth: 1,
|
||||
strokeUniform: true,
|
||||
noScaleCache: false,
|
||||
opacity: get(opacityStateStore) ? 0.4 : 1,
|
||||
});
|
||||
canvas.add(rect);
|
||||
});
|
||||
|
@ -47,8 +50,8 @@ export const drawRectangle = (canvas: fabric.Canvas): void => {
|
|||
return;
|
||||
}
|
||||
const pointer = canvas.getPointer(o.e);
|
||||
let x = pointer.x;
|
||||
let y = pointer.y;
|
||||
const x = pointer.x;
|
||||
const y = pointer.y;
|
||||
|
||||
if (x < origX) {
|
||||
rect.set({ originX: "right" });
|
||||
|
@ -62,20 +65,6 @@ export const drawRectangle = (canvas: fabric.Canvas): void => {
|
|||
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({
|
||||
width: Math.abs(x - rect.left),
|
||||
height: Math.abs(y - rect.top),
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
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 { undoStack } from "./tool-undo-redo";
|
||||
|
@ -27,6 +29,7 @@ export const drawText = (canvas: fabric.Canvas): void => {
|
|||
fontFamily: TEXT_FONT_FAMILY,
|
||||
backgroundColor: TEXT_BACKGROUND_COLOR,
|
||||
padding: TEXT_PADDING,
|
||||
opacity: get(opacityStateStore) ? 0.4 : 1,
|
||||
});
|
||||
enableUniformScaling(canvas, text);
|
||||
canvas.add(text);
|
||||
|
|
|
@ -7,6 +7,7 @@ import { writable } from "svelte/store";
|
|||
|
||||
import { mdiRedo, mdiUndo } from "../icons";
|
||||
import { emitChangeSignal } from "../MaskEditor.svelte";
|
||||
import { redoKeyCombination, undoKeyCombination } from "./shortcuts";
|
||||
|
||||
/**
|
||||
* Undo redo for rectangle and ellipse handled here,
|
||||
|
@ -133,11 +134,13 @@ export const undoRedoTools = [
|
|||
icon: mdiUndo,
|
||||
action: () => undoStack.undo(),
|
||||
tooltip: tr.undoUndo,
|
||||
shortcut: undoKeyCombination,
|
||||
},
|
||||
{
|
||||
name: "redo",
|
||||
icon: mdiRedo,
|
||||
action: () => undoStack.redo(),
|
||||
tooltip: tr.undoRedo,
|
||||
shortcut: redoKeyCombination,
|
||||
},
|
||||
];
|
||||
|
|
Loading…
Reference in a new issue