diff --git a/ftl/core/notetypes.ftl b/ftl/core/notetypes.ftl
index f7eccd08b..03159ba98 100644
--- a/ftl/core/notetypes.ftl
+++ b/ftl/core/notetypes.ftl
@@ -47,6 +47,7 @@ notetypes-toggle-masks = Toggle Masks
notetypes-image-occlusion-name = Image Occlusion
notetypes-hide-all-guess-one = Hide All, Guess One
notetypes-hide-one-guess-one = Hide One, Guess One
+notetypes-hide-all-but-one = Hide All But One
notetypes-error-generating-cloze = An error occurred when generating an image occlusion note
notetypes-error-getting-imagecloze = An error occurred while fetching an image occlusion note
notetypes-error-loading-image-occlusion = Error loading image occlusion. Is your Anki version up to date?
diff --git a/proto/anki/image_occlusion.proto b/proto/anki/image_occlusion.proto
index edbfe9ffd..f54206131 100644
--- a/proto/anki/image_occlusion.proto
+++ b/proto/anki/image_occlusion.proto
@@ -69,6 +69,12 @@ message GetImageOcclusionNoteResponse {
uint32 ordinal = 2;
}
+ enum OcclusionMode {
+ HIDE_ONE = 0;
+ HIDE_ALL = 1;
+ HIDE_ALL_BUT_ONE = 2;
+ }
+
message ImageOcclusionNote {
bytes image_data = 1;
repeated ImageOcclusion occlusions = 2;
@@ -76,7 +82,7 @@ message GetImageOcclusionNoteResponse {
string back_extra = 4;
repeated string tags = 5;
string image_file_name = 6;
- bool occlude_inactive = 7;
+ OcclusionMode occlusion_mode = 7;
}
oneof value {
diff --git a/rslib/src/image_occlusion/imagedata.rs b/rslib/src/image_occlusion/imagedata.rs
index fdf8ea4fd..b8609ccdd 100644
--- a/rslib/src/image_occlusion/imagedata.rs
+++ b/rslib/src/image_occlusion/imagedata.rs
@@ -7,6 +7,7 @@ use std::path::PathBuf;
use anki_io::metadata;
use anki_io::read_file;
use anki_proto::image_occlusion::get_image_occlusion_note_response::ImageOcclusionNote;
+use anki_proto::image_occlusion::get_image_occlusion_note_response::OcclusionMode;
use anki_proto::image_occlusion::get_image_occlusion_note_response::Value;
use anki_proto::image_occlusion::AddImageOcclusionNoteRequest;
use anki_proto::image_occlusion::GetImageForOcclusionResponse;
@@ -97,14 +98,22 @@ impl Collection {
let idxs = nt.get_io_field_indexes()?;
cloze_note.occlusions = parse_image_occlusions(fields[idxs.occlusions as usize].as_str());
- cloze_note.occlude_inactive = cloze_note.occlusions.iter().any(|oc| {
- oc.shapes.iter().any(|sh| {
- sh.properties
- .iter()
- .find(|p| p.name == "oi")
- .is_some_and(|p| p.value == "1")
+ cloze_note.occlusion_mode = cloze_note
+ .occlusions
+ .iter()
+ .find_map(|oc| {
+ oc.shapes.iter().find_map(|sh| {
+ sh.properties
+ .iter()
+ .find(|p| p.name == "oi")
+ .and_then(|p| match p.value.as_str() {
+ "1" => Some(OcclusionMode::HideAll as i32),
+ "2" => Some(OcclusionMode::HideAllButOne as i32),
+ _ => None,
+ })
+ })
})
- });
+ .unwrap_or(OcclusionMode::HideOne as i32);
cloze_note.header.clone_from(&fields[idxs.header as usize]);
cloze_note
.back_extra
diff --git a/ts/editor/NoteEditor.svelte b/ts/editor/NoteEditor.svelte
index 17ced575b..8bce99512 100644
--- a/ts/editor/NoteEditor.svelte
+++ b/ts/editor/NoteEditor.svelte
@@ -423,9 +423,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { IOMode } from "../routes/image-occlusion/lib";
import { exportShapesToClozeDeletions } from "../routes/image-occlusion/shapes/to-cloze";
import {
- hideAllGuessOne,
ioImageLoadedStore,
ioMaskEditorVisible,
+ occlusionMode,
} from "../routes/image-occlusion/store";
import CollapseLabel from "./CollapseLabel.svelte";
import * as oldEditorAdapter from "./old-editor-adapter";
@@ -477,7 +477,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
function saveOcclusions(): void {
if (isImageOcclusion && globalThis.canvas) {
- const occlusionsData = exportShapesToClozeDeletions($hideAllGuessOne);
+ const occlusionsData = exportShapesToClozeDeletions($occlusionMode);
fieldStores[ioFields.occlusions].set(occlusionsData.clozes);
}
}
diff --git a/ts/lib/components/icons.ts b/ts/lib/components/icons.ts
index ab07cbf17..f3dbd64c8 100644
--- a/ts/lib/components/icons.ts
+++ b/ts/lib/components/icons.ts
@@ -15,6 +15,8 @@ import AlignVerticalCenter_ from "@mdi/svg/svg/align-vertical-center.svg?compone
import alignVerticalCenter_ from "@mdi/svg/svg/align-vertical-center.svg?url";
import AlignVerticalTop_ from "@mdi/svg/svg/align-vertical-top.svg?component";
import alignVerticalTop_ from "@mdi/svg/svg/align-vertical-top.svg?url";
+import CheckboxBlankOutline_ from "@mdi/svg/svg/checkbox-blank-outline.svg?component";
+import checkboxBlankOutline_ from "@mdi/svg/svg/checkbox-blank-outline.svg?url";
import CheckCircle_ from "@mdi/svg/svg/check-circle.svg?component";
import checkCircle_ from "@mdi/svg/svg/check-circle.svg?url";
import ChevronDown_ from "@mdi/svg/svg/chevron-down.svg?component";
@@ -251,6 +253,7 @@ export const underlineIcon = { url: underline_, component: Underline_ };
export const deleteIcon = { url: delete_, component: Delete_ };
export const inlineIcon = { url: inline_, component: Inline_ };
export const blockIcon = { url: block_, component: Block_ };
+export const mdiCheckboxBlankOutline = { url: checkboxBlankOutline_, component: CheckboxBlankOutline_ };
export const mdiAlignHorizontalCenter = { url: alignHorizontalCenter_, component: AlignHorizontalCenter_ };
export const mdiAlignHorizontalLeft = { url: alignHorizontalLeft_, component: AlignHorizontalLeft_ };
export const mdiAlignHorizontalRight = { url: alignHorizontalRight_, component: AlignHorizontalRight_ };
diff --git a/ts/routes/image-occlusion/Toolbar.svelte b/ts/routes/image-occlusion/Toolbar.svelte
index b00e42087..354ae89f4 100644
--- a/ts/routes/image-occlusion/Toolbar.svelte
+++ b/ts/routes/image-occlusion/Toolbar.svelte
@@ -16,6 +16,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import Icon from "$lib/components/Icon.svelte";
import IconButton from "$lib/components/IconButton.svelte";
import {
+ mdiCheckboxBlankOutline,
mdiEye,
mdiFormatAlignCenter,
mdiSquare,
@@ -26,11 +27,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import WithFloating from "$lib/components/WithFloating.svelte";
import {
- hideAllGuessOne,
ioMaskEditorVisible,
- textEditingState,
- saveNeededStore,
+ OcclusionMode,
+ occlusionMode,
opacityStateStore,
+ saveNeededStore,
+ textEditingState,
} from "./store";
import { get } from "svelte/store";
import { drawEllipse, drawPolygon, drawRectangle, drawText } from "./tools/index";
@@ -228,8 +230,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
disablePan(canvas);
};
- function changeOcclusionType(occlusionType: "all" | "one"): void {
- $hideAllGuessOne = occlusionType === "all";
+ function changeOcclusionType(mode: OcclusionMode): void {
+ $occlusionMode = mode;
saveNeededStore.set(true);
}
@@ -312,22 +314,34 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{iconSize}
on:click={() => (showFloating = !showFloating)}
>
-
+
changeOcclusionType("all")}
+ active={$occlusionMode === OcclusionMode.HideAll}
+ on:click={() => changeOcclusionType(OcclusionMode.HideAll)}
>
{tr.notetypesHideAllGuessOne()}
changeOcclusionType("one")}
+ active={$occlusionMode === OcclusionMode.HideOne}
+ on:click={() => changeOcclusionType(OcclusionMode.HideOne)}
>
{tr.notetypesHideOneGuessOne()}
+ changeOcclusionType(OcclusionMode.HideAllButOne)}
+ >
+ {tr.notetypesHideAllButOne()}
+
diff --git a/ts/routes/image-occlusion/[...imagePathOrNoteId]/+page.ts b/ts/routes/image-occlusion/[...imagePathOrNoteId]/+page.ts
index 6f83cc44c..6de4cd347 100644
--- a/ts/routes/image-occlusion/[...imagePathOrNoteId]/+page.ts
+++ b/ts/routes/image-occlusion/[...imagePathOrNoteId]/+page.ts
@@ -5,11 +5,11 @@ import { get } from "svelte/store";
import { addOrUpdateNote } from "../add-or-update-note.svelte";
import type { IOMode } from "../lib";
-import { hideAllGuessOne } from "../store";
+import { occlusionMode } from "../store";
import type { PageLoad } from "./$types";
async function save(): Promise {
- addOrUpdateNote(globalThis["anki"].imageOcclusion.mode, get(hideAllGuessOne));
+ addOrUpdateNote(globalThis["anki"].imageOcclusion.mode, get(occlusionMode));
}
export const load = (async ({ params }) => {
diff --git a/ts/routes/image-occlusion/add-or-update-note.svelte.ts b/ts/routes/image-occlusion/add-or-update-note.svelte.ts
index ce31eaaaf..c8af2d970 100644
--- a/ts/routes/image-occlusion/add-or-update-note.svelte.ts
+++ b/ts/routes/image-occlusion/add-or-update-note.svelte.ts
@@ -9,14 +9,14 @@ import { get } from "svelte/store";
import { mount } from "svelte";
import type { IOAddingMode, IOMode } from "./lib";
import { exportShapesToClozeDeletions } from "./shapes/to-cloze";
-import { notesDataStore, tagsWritable } from "./store";
+import { notesDataStore, OcclusionMode, tagsWritable } from "./store";
import Toast from "./Toast.svelte";
export const addOrUpdateNote = async function(
mode: IOMode,
- occludeInactive: boolean,
+ occlusionMode: OcclusionMode,
): Promise {
- const { clozes: occlusionCloze, noteCount } = exportShapesToClozeDeletions(occludeInactive);
+ const { clozes: occlusionCloze, noteCount } = exportShapesToClozeDeletions(occlusionMode);
if (noteCount === 0) {
return;
}
diff --git a/ts/routes/image-occlusion/index.ts b/ts/routes/image-occlusion/index.ts
index 59720b1e0..942babb13 100644
--- a/ts/routes/image-occlusion/index.ts
+++ b/ts/routes/image-occlusion/index.ts
@@ -10,7 +10,7 @@ import { get } from "svelte/store";
import { addOrUpdateNote } from "./add-or-update-note.svelte";
import ImageOcclusionPage from "./ImageOcclusionPage.svelte";
import type { IOMode } from "./lib";
-import { hideAllGuessOne } from "./store";
+import { occlusionMode } from "./store";
globalThis.anki = globalThis.anki || {};
@@ -31,7 +31,7 @@ export async function setupImageOcclusion(mode: IOMode, target = document.body):
await i18n;
async function addNote(): Promise {
- addOrUpdateNote(mode, get(hideAllGuessOne));
+ addOrUpdateNote(mode, get(occlusionMode));
}
// for adding note from mobile devices
diff --git a/ts/routes/image-occlusion/mask-editor.ts b/ts/routes/image-occlusion/mask-editor.ts
index 41adbe423..aca9f69ee 100644
--- a/ts/routes/image-occlusion/mask-editor.ts
+++ b/ts/routes/image-occlusion/mask-editor.ts
@@ -9,8 +9,8 @@ import { get } from "svelte/store";
import { optimumCssSizeForCanvas } from "./canvas-scale";
import {
- hideAllGuessOne,
notesDataStore,
+ occlusionMode,
opacityStateStore,
saveNeededStore,
tagsWritable,
@@ -75,7 +75,7 @@ export const setupMaskEditorForEdit = async (
const clozeNote = clozeNoteResponse.value.value;
const canvas = initCanvas();
- hideAllGuessOne.set(clozeNote.occludeInactive);
+ occlusionMode.set(clozeNote.occlusionMode);
// get image width and height
const image = document.getElementById("image") as HTMLImageElement;
diff --git a/ts/routes/image-occlusion/review.ts b/ts/routes/image-occlusion/review.ts
index 4425a8141..22e81b432 100644
--- a/ts/routes/image-occlusion/review.ts
+++ b/ts/routes/image-occlusion/review.ts
@@ -168,7 +168,7 @@ async function setupImageOcclusionInner(setupOptions?: SetupImageOcclusionOption
// setup button for toggle image occlusion
const button = document.getElementById("toggle");
if (button) {
- if (document.querySelector("[data-occludeinactive=\"1\"]")) {
+ if (document.querySelector("[data-occludeinactive=\"1\"], [data-occludeinactive=\"2\"]")) {
button.addEventListener("click", () => toggleMasks(setupOptions));
} else {
button.style.display = "none";
@@ -202,35 +202,55 @@ function drawShapes(
properties = processed.properties;
}
- for (const shape of activeShapes) {
- drawShape({
- context,
- size,
- shape,
- fill: properties.activeShapeColor,
- stroke: properties.activeBorder.color,
- strokeWidth: properties.activeBorder.width,
- });
- }
- for (const shape of inactiveShapes.filter((s) => s.occludeInactive)) {
- drawShape({
- context,
- size,
- shape,
- fill: shape.fill !== SHAPE_MASK_COLOR ? shape.fill : properties.inActiveShapeColor,
- stroke: properties.inActiveBorder.color,
- strokeWidth: properties.inActiveBorder.width,
- });
- }
- for (const shape of highlightShapes) {
- drawShape({
- context,
- size,
- shape,
- fill: properties.highlightShapeColor,
- stroke: properties.highlightShapeBorder.color,
- strokeWidth: properties.highlightShapeBorder.width,
- });
+ // Determine occlusion mode from the first shape
+ const occlusionMode = activeShapes[0]?.occlusionMode ?? inactiveShapes[0]?.occlusionMode ?? 0;
+
+ // Mode 0 (HideOne): Draw active only (front), reveal answer with highlight (back)
+ // Mode 1 (HideAll): Draw both active and inactive (front & back)
+ // Mode 2 (HideAllButOne): Draw inactive only (front), draw nothing (back)
+
+ // Check if we're on the back side (highlightShapes only exist on back)
+ const isBackSide = highlightShapes.length > 0;
+
+ // For mode 2 on the back side, draw nothing (show full unoccluded image)
+ if (occlusionMode === 2 && isBackSide) {
+ // Don't draw any shapes on the back for "Hide All But One" mode
+ } else {
+ // Normal drawing logic for all other cases
+ if (occlusionMode !== 2) {
+ for (const shape of activeShapes) {
+ drawShape({
+ context,
+ size,
+ shape,
+ fill: properties.activeShapeColor,
+ stroke: properties.activeBorder.color,
+ strokeWidth: properties.activeBorder.width,
+ });
+ }
+ }
+ if (occlusionMode === 1 || occlusionMode === 2) {
+ for (const shape of inactiveShapes) {
+ drawShape({
+ context,
+ size,
+ shape,
+ fill: shape.fill !== SHAPE_MASK_COLOR ? shape.fill : properties.inActiveShapeColor,
+ stroke: properties.inActiveBorder.color,
+ strokeWidth: properties.inActiveBorder.width,
+ });
+ }
+ }
+ for (const shape of highlightShapes) {
+ drawShape({
+ context,
+ size,
+ shape,
+ fill: properties.highlightShapeColor,
+ stroke: properties.highlightShapeBorder.color,
+ strokeWidth: properties.highlightShapeBorder.width,
+ });
+ }
}
onDidDrawShapes?.({
diff --git a/ts/routes/image-occlusion/shapes/base.ts b/ts/routes/image-occlusion/shapes/base.ts
index 6dd65f122..bb3da0e73 100644
--- a/ts/routes/image-occlusion/shapes/base.ts
+++ b/ts/routes/image-occlusion/shapes/base.ts
@@ -20,23 +20,23 @@ export class Shape {
top: number;
angle?: number; // polygons don't use it
fill: string;
- /** Whether occlusions from other cloze numbers should be shown on the
- * question side. Used only in reviewer code.
+ /** Occlusion mode: 0=HideOne, 1=HideAll, 2=HideAllButOne.
+ * Used only in reviewer code.
*/
- occludeInactive?: boolean;
+ occlusionMode?: number;
/* Cloze ordinal */
ordinal: number | undefined;
id: string | undefined;
constructor(
- { left = 0, top = 0, angle = 0, fill = SHAPE_MASK_COLOR, occludeInactive, ordinal = undefined }:
+ { left = 0, top = 0, angle = 0, fill = SHAPE_MASK_COLOR, occlusionMode, ordinal = undefined }:
ConstructorParams = {},
) {
this.left = left;
this.top = top;
this.angle = angle;
this.fill = fill;
- this.occludeInactive = occludeInactive;
+ this.occlusionMode = occlusionMode;
this.ordinal = ordinal;
}
diff --git a/ts/routes/image-occlusion/shapes/from-cloze.ts b/ts/routes/image-occlusion/shapes/from-cloze.ts
index 0db496740..aeb8f1487 100644
--- a/ts/routes/image-occlusion/shapes/from-cloze.ts
+++ b/ts/routes/image-occlusion/shapes/from-cloze.ts
@@ -64,7 +64,7 @@ function extractShapeFromRenderedCloze(cloze: HTMLDivElement): Shape | null {
return null;
}
const props = {
- occludeInactive: cloze.dataset.occludeinactive === "1",
+ occlusionMode: cloze.dataset.occludeinactive ? parseInt(cloze.dataset.occludeinactive) : undefined,
ordinal: parseInt(cloze.dataset.ordinal!),
left: cloze.dataset.left,
top: cloze.dataset.top,
diff --git a/ts/routes/image-occlusion/shapes/to-cloze.ts b/ts/routes/image-occlusion/shapes/to-cloze.ts
index 102e9bbd4..6c1027f6f 100644
--- a/ts/routes/image-occlusion/shapes/to-cloze.ts
+++ b/ts/routes/image-occlusion/shapes/to-cloze.ts
@@ -4,6 +4,7 @@
import { fabric } from "fabric";
import { cloneDeep } from "lodash-es";
+import { OcclusionMode } from "../store";
import { getBoundingBoxSize } from "../tools/lib";
import type { Size } from "../types";
import type { Shape, ShapeOrShapes } from "./base";
@@ -12,7 +13,7 @@ import { Polygon } from "./polygon";
import { Rectangle } from "./rectangle";
import { Text } from "./text";
-export function exportShapesToClozeDeletions(occludeInactive: boolean): {
+export function exportShapesToClozeDeletions(mode: OcclusionMode): {
clozes: string;
noteCount: number;
} {
@@ -76,7 +77,7 @@ export function exportShapesToClozeDeletions(occludeInactive: boolean): {
clozes += shapeOrShapesToCloze(
shapeOrShapes,
ordinal,
- occludeInactive,
+ mode,
);
if (!(shapeOrShapes instanceof Text)) {
@@ -179,7 +180,7 @@ function fabricObjectToBaseShapeOrShapes(
function shapeOrShapesToCloze(
shapeOrShapes: ShapeOrShapes,
ordinal: number,
- occludeInactive: boolean,
+ mode: OcclusionMode,
): string {
let text = "";
function addKeyValue(key: string, value: string) {
@@ -190,7 +191,7 @@ function shapeOrShapesToCloze(
let type: string;
if (Array.isArray(shapeOrShapes)) {
return shapeOrShapes
- .map((shape) => shapeOrShapesToCloze(shape, ordinal, occludeInactive))
+ .map((shape) => shapeOrShapesToCloze(shape, ordinal, mode))
.join("");
} else if (shapeOrShapes instanceof Rectangle) {
type = "rect";
@@ -207,8 +208,8 @@ function shapeOrShapesToCloze(
for (const [key, value] of Object.entries(shapeOrShapes.toDataForCloze())) {
addKeyValue(key, value);
}
- if (occludeInactive) {
- addKeyValue("oi", "1");
+ if (mode !== OcclusionMode.HideOne) {
+ addKeyValue("oi", mode.toString());
}
text = `{{c${ordinal}::image-occlusion:${type}${text}}}
`;
diff --git a/ts/routes/image-occlusion/store.ts b/ts/routes/image-occlusion/store.ts
index 0af7696eb..97b1d9a22 100644
--- a/ts/routes/image-occlusion/store.ts
+++ b/ts/routes/image-occlusion/store.ts
@@ -3,14 +3,20 @@
import { writable } from "svelte/store";
+export enum OcclusionMode {
+ HideOne = 0,
+ HideAll = 1,
+ HideAllButOne = 2,
+}
+
// it stores note's data for generate.ts, when function generate() is called it will be used to generate the note
export const notesDataStore = writable({ id: "", title: "", divValue: "", textareaValue: "" }[0]);
// it stores the tags for the note in note editor
export const tagsWritable = writable([""]);
// it stores the visibility of mask editor
export const ioMaskEditorVisible = writable(true);
-// it store hide all or hide one mode
-export const hideAllGuessOne = writable(true);
+// it stores the occlusion mode (hide one, hide all, or hide all reveal one)
+export const occlusionMode = writable(OcclusionMode.HideAll);
// ioImageLoadedStore is used to store the image loaded event
export const ioImageLoadedStore = writable(false);
// store opacity state of objects in canvas