mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
Add IO mask colour fill tool (#4048)
* add fill tool * add fill tool logic * open colour picker on fill tool activation * refactor/add fill attr to io clozes * fill masks in editor * fill text and inactive masks in reviewer * fix lint * remove debug option
This commit is contained in:
parent
27c1ed1899
commit
174b199164
12 changed files with 109 additions and 28 deletions
|
@ -96,6 +96,7 @@ editing-image-occlusion-rectangle-tool = Rectangle
|
||||||
editing-image-occlusion-ellipse-tool = Ellipse
|
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-fill-tool = Fill with colour
|
||||||
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-reset = Reset Image Occlusion
|
||||||
editing-image-occlusion-confirm-reset = Are you sure you want to reset this image occlusion?
|
editing-image-occlusion-confirm-reset = Are you sure you want to reset this image occlusion?
|
||||||
|
|
|
@ -64,19 +64,9 @@ pub fn get_image_cloze_data(text: &str) -> String {
|
||||||
}
|
}
|
||||||
for property in occlusion.properties {
|
for property in occlusion.properties {
|
||||||
match property.name.as_str() {
|
match property.name.as_str() {
|
||||||
"left" => {
|
"left" | "top" | "angle" | "fill" => {
|
||||||
if !property.value.is_empty() {
|
if !property.value.is_empty() {
|
||||||
result.push_str(&format!("data-left=\"{}\" ", property.value));
|
result.push_str(&format!("data-{}=\"{}\" ", property.name, property.value));
|
||||||
}
|
|
||||||
}
|
|
||||||
"top" => {
|
|
||||||
if !property.value.is_empty() {
|
|
||||||
result.push_str(&format!("data-top=\"{}\" ", property.value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"angle" => {
|
|
||||||
if !property.value.is_empty() {
|
|
||||||
result.push_str(&format!("data-angle=\"{}\" ", property.value));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"width" => {
|
"width" => {
|
||||||
|
|
|
@ -59,6 +59,8 @@ import FormatAlignCenter_ from "@mdi/svg/svg/format-align-center.svg?component";
|
||||||
import formatAlignCenter_ from "@mdi/svg/svg/format-align-center.svg?url";
|
import formatAlignCenter_ from "@mdi/svg/svg/format-align-center.svg?url";
|
||||||
import FormatBold_ from "@mdi/svg/svg/format-bold.svg?component";
|
import FormatBold_ from "@mdi/svg/svg/format-bold.svg?component";
|
||||||
import formatBold_ from "@mdi/svg/svg/format-bold.svg?url";
|
import formatBold_ from "@mdi/svg/svg/format-bold.svg?url";
|
||||||
|
import FormatColorFill_ from "@mdi/svg/svg/format-color-fill.svg?component";
|
||||||
|
import formatColorFill_ from "@mdi/svg/svg/format-color-fill.svg?url";
|
||||||
import HighlightColor_ from "@mdi/svg/svg/format-color-highlight.svg?component";
|
import HighlightColor_ from "@mdi/svg/svg/format-color-highlight.svg?component";
|
||||||
import highlightColor_ from "@mdi/svg/svg/format-color-highlight.svg?url";
|
import highlightColor_ from "@mdi/svg/svg/format-color-highlight.svg?url";
|
||||||
import TextColor_ from "@mdi/svg/svg/format-color-text.svg?component";
|
import TextColor_ from "@mdi/svg/svg/format-color-text.svg?component";
|
||||||
|
@ -264,6 +266,7 @@ export const mdiEllipseOutline = { url: ellipseOutline_, component: EllipseOutli
|
||||||
export const mdiEye = { url: eye_, component: Eye_ };
|
export const mdiEye = { url: eye_, component: Eye_ };
|
||||||
export const mdiFormatAlignCenter = { url: formatAlignCenter_, component: FormatAlignCenter_ };
|
export const mdiFormatAlignCenter = { url: formatAlignCenter_, component: FormatAlignCenter_ };
|
||||||
export const mdiFormatBold = { url: formatBold_, component: FormatBold_ };
|
export const mdiFormatBold = { url: formatBold_, component: FormatBold_ };
|
||||||
|
export const mdiFormatColorFill = { url: formatColorFill_, component: FormatColorFill_ };
|
||||||
export const mdiFormatItalic = { url: formatItalic_, component: FormatItalic_ };
|
export const mdiFormatItalic = { url: formatItalic_, component: FormatItalic_ };
|
||||||
export const mdiFormatUnderline = { url: formatUnderline_, component: FormatUnderline_ };
|
export const mdiFormatUnderline = { url: formatUnderline_, component: FormatUnderline_ };
|
||||||
export const mdiGroup = { url: group_, component: Group_ };
|
export const mdiGroup = { url: group_, component: Group_ };
|
||||||
|
|
|
@ -10,7 +10,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import type { Callback } from "@tslib/typing";
|
import type { Callback } from "@tslib/typing";
|
||||||
import { singleCallback } from "@tslib/typing";
|
import { singleCallback } from "@tslib/typing";
|
||||||
import { getContext, onDestroy, onMount } from "svelte";
|
import { getContext, onDestroy, onMount } from "svelte";
|
||||||
import type { Readable } from "svelte/store";
|
import { writable, type Readable } from "svelte/store";
|
||||||
|
|
||||||
import DropdownItem from "$lib/components/DropdownItem.svelte";
|
import DropdownItem from "$lib/components/DropdownItem.svelte";
|
||||||
import Icon from "$lib/components/Icon.svelte";
|
import Icon from "$lib/components/Icon.svelte";
|
||||||
|
@ -33,7 +33,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
opacityStateStore,
|
opacityStateStore,
|
||||||
} from "./store";
|
} 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, SHAPE_MASK_COLOR } from "./tools/lib";
|
||||||
import { enableSelectable, stopDraw } from "./tools/lib";
|
import { enableSelectable, stopDraw } from "./tools/lib";
|
||||||
import {
|
import {
|
||||||
alignTools,
|
alignTools,
|
||||||
|
@ -42,7 +42,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
zoomTools,
|
zoomTools,
|
||||||
} from "./tools/more-tools";
|
} from "./tools/more-tools";
|
||||||
import { toggleTranslucentKeyCombination } from "./tools/shortcuts";
|
import { toggleTranslucentKeyCombination } from "./tools/shortcuts";
|
||||||
import { tools } from "./tools/tool-buttons";
|
import { tools, type ActiveTool } from "./tools/tool-buttons";
|
||||||
import { drawCursor } from "./tools/tool-cursor";
|
import { drawCursor } from "./tools/tool-cursor";
|
||||||
import { removeUnfinishedPolygon } from "./tools/tool-polygon";
|
import { removeUnfinishedPolygon } from "./tools/tool-polygon";
|
||||||
import { undoRedoTools, undoStack } from "./tools/tool-undo-redo";
|
import { undoRedoTools, undoStack } from "./tools/tool-undo-redo";
|
||||||
|
@ -54,10 +54,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
onWheelDrag,
|
onWheelDrag,
|
||||||
onWheelDragX,
|
onWheelDragX,
|
||||||
} from "./tools/tool-zoom";
|
} from "./tools/tool-zoom";
|
||||||
|
import { fillMask } from "./tools/tool-fill";
|
||||||
|
|
||||||
export let canvas;
|
export let canvas;
|
||||||
export let iconSize;
|
export let iconSize;
|
||||||
export let activeTool = "cursor";
|
export let activeTool: ActiveTool = "cursor";
|
||||||
let showAlignTools = false;
|
let showAlignTools = false;
|
||||||
let leftPos = 82;
|
let leftPos = 82;
|
||||||
let maskOpacity = false;
|
let maskOpacity = false;
|
||||||
|
@ -72,6 +73,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
const controlKey = "Control";
|
const controlKey = "Control";
|
||||||
const shiftKey = "Shift";
|
const shiftKey = "Shift";
|
||||||
let removeHandlers: Callback;
|
let removeHandlers: Callback;
|
||||||
|
let colourRef: HTMLInputElement | undefined;
|
||||||
|
const colour = writable(SHAPE_MASK_COLOR);
|
||||||
|
|
||||||
function onClick(event: MouseEvent) {
|
function onClick(event: MouseEvent) {
|
||||||
const upperCanvas = document.querySelector(".upper-canvas");
|
const upperCanvas = document.querySelector(".upper-canvas");
|
||||||
|
@ -168,7 +171,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleToolChanges = (newActiveTool: string) => {
|
const handleToolChanges = (newActiveTool: ActiveTool, clicked: boolean = false) => {
|
||||||
disableFunctions();
|
disableFunctions();
|
||||||
enableSelectable(canvas, true);
|
enableSelectable(canvas, true);
|
||||||
// remove unfinished polygon when switching to other tools
|
// remove unfinished polygon when switching to other tools
|
||||||
|
@ -193,6 +196,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
handleToolChanges(activeTool);
|
handleToolChanges(activeTool);
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case "fill-mask":
|
||||||
|
if (clicked) {
|
||||||
|
colourRef?.click();
|
||||||
|
}
|
||||||
|
fillMask(canvas, colour);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -231,16 +240,30 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="tool-bar-container">
|
<datalist id="colour-palette">
|
||||||
|
<option value={SHAPE_MASK_COLOR}></option>
|
||||||
|
</datalist>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
bind:this={colourRef}
|
||||||
|
style:display="none"
|
||||||
|
list="colour-palette"
|
||||||
|
value={SHAPE_MASK_COLOR}
|
||||||
|
on:input={(e) => ($colour = e.currentTarget!.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="tool-bar-container" style:--fill-tool-colour={$colour}>
|
||||||
{#each tools as tool}
|
{#each tools as tool}
|
||||||
|
{@const active = activeTool == tool.id}
|
||||||
<IconButton
|
<IconButton
|
||||||
class="tool-icon-button {activeTool == tool.id ? 'active-tool' : ''}"
|
class="tool-icon-button {active ? 'active-tool' : ''} {tool.id}"
|
||||||
{iconSize}
|
iconSize={iconSize * (tool["iconSizeMult"] ?? 1)}
|
||||||
tooltip="{tool.tooltip()} ({getPlatformString(tool.shortcut)})"
|
tooltip="{tool.tooltip()} ({getPlatformString(tool.shortcut)})"
|
||||||
active={activeTool === tool.id}
|
{active}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
activeTool = tool.id;
|
activeTool = tool.id;
|
||||||
handleToolChanges(activeTool);
|
handleToolChanges(activeTool, true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon={tool.icon} />
|
<Icon icon={tool.icon} />
|
||||||
|
@ -250,7 +273,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
keyCombination={tool.shortcut}
|
keyCombination={tool.shortcut}
|
||||||
on:action={() => {
|
on:action={() => {
|
||||||
activeTool = tool.id;
|
activeTool = tool.id;
|
||||||
handleToolChanges(activeTool);
|
handleToolChanges(activeTool, true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -551,6 +574,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
padding-bottom: 100px;
|
padding-bottom: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.fill-mask svg) {
|
||||||
|
fill: var(--fill-tool-colour) !important;
|
||||||
|
stroke: black;
|
||||||
|
stroke-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
:global([dir="rtl"] .tool-bar-container) {
|
:global([dir="rtl"] .tool-bar-container) {
|
||||||
left: unset;
|
left: unset;
|
||||||
right: 2px;
|
right: 2px;
|
||||||
|
|
|
@ -217,7 +217,7 @@ function drawShapes(
|
||||||
context,
|
context,
|
||||||
size,
|
size,
|
||||||
shape,
|
shape,
|
||||||
fill: properties.inActiveShapeColor,
|
fill: shape.fill ?? properties.inActiveShapeColor,
|
||||||
stroke: properties.inActiveBorder.color,
|
stroke: properties.inActiveBorder.color,
|
||||||
strokeWidth: properties.inActiveBorder.width,
|
strokeWidth: properties.inActiveBorder.width,
|
||||||
});
|
});
|
||||||
|
@ -358,7 +358,7 @@ function drawShape({
|
||||||
maxWidth + TEXT_PADDING,
|
maxWidth + TEXT_PADDING,
|
||||||
totalHeight + TEXT_PADDING,
|
totalHeight + TEXT_PADDING,
|
||||||
);
|
);
|
||||||
ctx.fillStyle = "#000";
|
ctx.fillStyle = shape.fill ?? "#000";
|
||||||
for (const line of linePositions) {
|
for (const line of linePositions) {
|
||||||
ctx.fillText(line.text, line.x, line.y);
|
ctx.fillText(line.text, line.x, line.y);
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,6 +77,7 @@ function extractShapeFromRenderedCloze(cloze: HTMLDivElement): Shape | null {
|
||||||
scale: cloze.dataset.scale,
|
scale: cloze.dataset.scale,
|
||||||
fs: cloze.dataset.fontSize,
|
fs: cloze.dataset.fontSize,
|
||||||
angle: cloze.dataset.angle,
|
angle: cloze.dataset.angle,
|
||||||
|
...(cloze.dataset.fill == null ? {} : { fill: cloze.dataset.fill }),
|
||||||
};
|
};
|
||||||
return buildShape(type, props);
|
return buildShape(type, props);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,11 +19,12 @@ export class Text extends Shape {
|
||||||
text = "",
|
text = "",
|
||||||
scaleX = 1,
|
scaleX = 1,
|
||||||
scaleY = 1,
|
scaleY = 1,
|
||||||
|
fill = TEXT_COLOR,
|
||||||
fontSize,
|
fontSize,
|
||||||
...rest
|
...rest
|
||||||
}: ConstructorParams<Text> = {}) {
|
}: ConstructorParams<Text> = {}) {
|
||||||
super(rest);
|
super(rest);
|
||||||
this.fill = TEXT_COLOR;
|
this.fill = fill;
|
||||||
this.text = text;
|
this.text = text;
|
||||||
this.scaleX = scaleX;
|
this.scaleX = scaleX;
|
||||||
this.scaleY = scaleY;
|
this.scaleY = scaleY;
|
||||||
|
@ -38,6 +39,7 @@ export class Text extends Shape {
|
||||||
// scaleX and scaleY are guaranteed to be equal since we lock the aspect ratio
|
// scaleX and scaleY are guaranteed to be equal since we lock the aspect ratio
|
||||||
scale: floatToDisplay(this.scaleX),
|
scale: floatToDisplay(this.scaleX),
|
||||||
fs: this.fontSize ? floatToDisplay(this.fontSize) : undefined,
|
fs: this.fontSize ? floatToDisplay(this.fontSize) : undefined,
|
||||||
|
...(this.fill === TEXT_COLOR ? {} : { fill: this.fill }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -105,6 +105,21 @@ export const unGroupShapes = (canvas: fabric.Canvas): void => {
|
||||||
redraw(canvas);
|
redraw(canvas);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Check for the target within a (potentially nested) group
|
||||||
|
* NOTE: assumes that masks do not overlap */
|
||||||
|
export const findTargetInGroup = (group: fabric.Group, p: fabric.Point): fabric.Object | undefined => {
|
||||||
|
if (!group) { return; }
|
||||||
|
const point = fabric.util.transformPoint(p, fabric.util.invertTransform(group.calcOwnMatrix()));
|
||||||
|
for (const shape of group.getObjects()) {
|
||||||
|
if (shape instanceof fabric.Group) {
|
||||||
|
const ret = findTargetInGroup(shape, point);
|
||||||
|
if (ret) { return ret; }
|
||||||
|
} else if (shape.containsPoint(point)) {
|
||||||
|
return shape;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const copyItem = (canvas: fabric.Canvas): void => {
|
const copyItem = (canvas: fabric.Canvas): void => {
|
||||||
const activeObject = canvas.getActiveObject();
|
const activeObject = canvas.getActiveObject();
|
||||||
if (!activeObject) {
|
if (!activeObject) {
|
||||||
|
|
|
@ -6,6 +6,7 @@ export const rectangleKeyCombination = "R";
|
||||||
export const ellipseKeyCombination = "E";
|
export const ellipseKeyCombination = "E";
|
||||||
export const polygonKeyCombination = "P";
|
export const polygonKeyCombination = "P";
|
||||||
export const textKeyCombination = "T";
|
export const textKeyCombination = "T";
|
||||||
|
export const fillKeyCombination = "C";
|
||||||
export const magnifyKeyCombination = "M";
|
export const magnifyKeyCombination = "M";
|
||||||
export const undoKeyCombination = "Control+Z";
|
export const undoKeyCombination = "Control+Z";
|
||||||
export const redoKeyCombination = "Control+Y";
|
export const redoKeyCombination = "Control+Y";
|
||||||
|
|
|
@ -6,6 +6,7 @@ import * as tr from "@generated/ftl";
|
||||||
import {
|
import {
|
||||||
mdiCursorDefaultOutline,
|
mdiCursorDefaultOutline,
|
||||||
mdiEllipseOutline,
|
mdiEllipseOutline,
|
||||||
|
mdiFormatColorFill,
|
||||||
mdiRectangleOutline,
|
mdiRectangleOutline,
|
||||||
mdiTextBox,
|
mdiTextBox,
|
||||||
mdiVectorPolygonVariant,
|
mdiVectorPolygonVariant,
|
||||||
|
@ -14,6 +15,7 @@ import {
|
||||||
import {
|
import {
|
||||||
cursorKeyCombination,
|
cursorKeyCombination,
|
||||||
ellipseKeyCombination,
|
ellipseKeyCombination,
|
||||||
|
fillKeyCombination,
|
||||||
polygonKeyCombination,
|
polygonKeyCombination,
|
||||||
rectangleKeyCombination,
|
rectangleKeyCombination,
|
||||||
textKeyCombination,
|
textKeyCombination,
|
||||||
|
@ -50,4 +52,13 @@ export const tools = [
|
||||||
tooltip: tr.editingImageOcclusionTextTool,
|
tooltip: tr.editingImageOcclusionTextTool,
|
||||||
shortcut: textKeyCombination,
|
shortcut: textKeyCombination,
|
||||||
},
|
},
|
||||||
];
|
{
|
||||||
|
id: "fill-mask",
|
||||||
|
icon: mdiFormatColorFill,
|
||||||
|
iconSizeMult: 1.4,
|
||||||
|
tooltip: tr.editingImageOcclusionFillTool,
|
||||||
|
shortcut: fillKeyCombination,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type ActiveTool = typeof tools[number]["id"];
|
||||||
|
|
28
ts/routes/image-occlusion/tools/tool-fill.ts
Normal file
28
ts/routes/image-occlusion/tools/tool-fill.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
import { fabric } from "fabric";
|
||||||
|
|
||||||
|
import { get, type Readable } from "svelte/store";
|
||||||
|
import { findTargetInGroup, stopDraw } from "./lib";
|
||||||
|
import { undoStack } from "./tool-undo-redo";
|
||||||
|
|
||||||
|
export const fillMask = (canvas: fabric.Canvas, colourStore: Readable<string>): void => {
|
||||||
|
// remove selectable for shapes
|
||||||
|
canvas.discardActiveObject();
|
||||||
|
canvas.forEachObject(function(o) {
|
||||||
|
o.selectable = false;
|
||||||
|
});
|
||||||
|
canvas.selectionColor = "rgba(0, 0, 0, 0)";
|
||||||
|
stopDraw(canvas);
|
||||||
|
|
||||||
|
canvas.on("mouse:down", function(o) {
|
||||||
|
const target = o.target instanceof fabric.Group
|
||||||
|
? findTargetInGroup(o.target, canvas.getPointer(o.e) as fabric.Point)
|
||||||
|
: o.target;
|
||||||
|
const colour = get(colourStore);
|
||||||
|
if (!target || target.fill === colour) { return; }
|
||||||
|
target.fill = colour;
|
||||||
|
undoStack.onObjectModified();
|
||||||
|
});
|
||||||
|
};
|
|
@ -223,7 +223,7 @@ export const modifiedPolygon = (canvas: fabric.Canvas, polygon: fabric.Polygon):
|
||||||
});
|
});
|
||||||
|
|
||||||
const polygon1 = new fabric.Polygon(transformedPoints, {
|
const polygon1 = new fabric.Polygon(transformedPoints, {
|
||||||
fill: SHAPE_MASK_COLOR,
|
fill: polygon.fill ?? SHAPE_MASK_COLOR,
|
||||||
objectCaching: false,
|
objectCaching: false,
|
||||||
stroke: BORDER_COLOR,
|
stroke: BORDER_COLOR,
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
|
|
Loading…
Reference in a new issue