anki/ts/reviewer/image_occlusion.ts
Damien Elmes 7686cb8de8
Fix misaligned image occlusions (#2512)
* Cloze styling is not required in I/O notetype

* Use raw string for IO template

* Rename to notetype.css and use more specific ids

* Move internal i/o styling into runtime

Storing it in the notetype makes it difficult to make changes, and
makes it easier for the user to break.

* Fix misaligned occlusions

At larger screen sizes, the canvas was not increasing above its configured
size, so it ended up being placed top center instead of expanding to fit
the entire container area.

To resolve this, both the image and canvas are forced to the container
size, and the container is constrained to the size of the viewport,
with the same aspect ratio as the image.

Closes #2492
2023-05-23 11:59:50 +10:00

207 lines
7.1 KiB
TypeScript

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import * as tr from "@tslib/ftl";
window.addEventListener("load", () => {
window.addEventListener("resize", setupImageCloze);
});
export function setupImageCloze(): void {
window.requestAnimationFrame(setupImageClozeInner);
}
function setupImageClozeInner(): void {
const canvas = document.querySelector("#image-occlusion-canvas") as HTMLCanvasElement | null;
if (canvas == null) {
return;
}
const ctx: CanvasRenderingContext2D = canvas.getContext("2d")!;
const container = document.getElementById("image-occlusion-container") as HTMLDivElement;
const image = document.querySelector("#image-occlusion-container img") as HTMLImageElement;
if (image == null) {
container.innerText = tr.notetypeErrorNoImageToShow();
return;
}
const size = limitSize({ width: image.naturalWidth, height: image.naturalHeight });
canvas.width = size.width;
canvas.height = size.height;
// Enforce aspect ratio of image
container.style.aspectRatio = `${size.width / size.height}`;
// setup button for toggle image occlusion
const button = document.getElementById("toggle");
if (button) {
button.addEventListener("click", toggleMasks);
}
drawShapes(ctx);
}
function drawShapes(ctx: CanvasRenderingContext2D): void {
const activeCloze = document.querySelectorAll(".cloze");
const inActiveCloze = document.querySelectorAll(".cloze-inactive");
const shapeProperty = getShapeProperty();
for (const clz of activeCloze) {
const cloze = (<HTMLDivElement> clz);
const shape = cloze.dataset.shape!;
const fill = shapeProperty.activeShapeColor;
draw(ctx, cloze, shape, fill, shapeProperty.activeBorder);
}
for (const clz of inActiveCloze) {
const cloze = (<HTMLDivElement> clz);
const shape = cloze.dataset.shape!;
const fill = shapeProperty.inActiveShapeColor;
const hideinactive = cloze.dataset.hideinactive == "true";
if (!hideinactive) {
draw(ctx, cloze, shape, fill, shapeProperty.inActiveBorder);
}
}
}
function draw(
ctx: CanvasRenderingContext2D,
cloze: HTMLDivElement,
shape: string,
color: string,
border: { width: number; color: string },
): void {
ctx.fillStyle = color;
const posLeft = parseFloat(cloze.dataset.left!);
const posTop = parseFloat(cloze.dataset.top!);
const width = parseFloat(cloze.dataset.width!);
const height = parseFloat(cloze.dataset.height!);
switch (shape) {
case "rect":
{
ctx.strokeStyle = border.color;
ctx.lineWidth = border.width;
ctx.fillRect(posLeft, posTop, width, height);
ctx.strokeRect(posLeft, posTop, width, height);
}
break;
case "ellipse":
{
const rx = parseFloat(cloze.dataset.rx!);
const ry = parseFloat(cloze.dataset.ry!);
const newLeft = posLeft + rx;
const newTop = posTop + ry;
ctx.beginPath();
ctx.strokeStyle = border.color;
ctx.lineWidth = border.width;
ctx.ellipse(newLeft, newTop, rx, ry, 0, 0, Math.PI * 2, false);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
break;
case "polygon":
{
const points = JSON.parse(cloze.dataset.points!);
ctx.beginPath();
ctx.strokeStyle = border.color;
ctx.lineWidth = border.width;
ctx.moveTo(points[0][0], points[0][1]);
for (let i = 1; i < points.length; i++) {
ctx.lineTo(points[i][0], points[i][1]);
}
ctx.closePath();
ctx.fill();
ctx.stroke();
}
break;
default:
break;
}
}
// following function copy+pasted from mask-editor.ts,
// so update both, if it changes
function limitSize(size: { width: number; height: number }): { width: number; height: number; scalar: number } {
const maximumPixels = 1000000;
const { width, height } = size;
const requiredPixels = width * height;
if (requiredPixels <= maximumPixels) return { width, height, scalar: 1 };
const scalar = Math.sqrt(maximumPixels) / Math.sqrt(requiredPixels);
return {
width: Math.floor(width * scalar),
height: Math.floor(height * scalar),
scalar: scalar,
};
}
function getShapeProperty(): {
activeShapeColor: string;
inActiveShapeColor: string;
activeBorder: { width: number; color: string };
inActiveBorder: { width: number; color: string };
} {
const canvas = document.getElementById("image-occlusion-canvas");
const computedStyle = window.getComputedStyle(canvas!);
// it may throw error if the css variable is not defined
try {
// shape color
const activeShapeColor = computedStyle.getPropertyValue("--active-shape-color");
const inActiveShapeColor = computedStyle.getPropertyValue("--inactive-shape-color");
// inactive shape border
const inActiveShapeBorder = computedStyle.getPropertyValue("--inactive-shape-border");
const inActiveBorder = inActiveShapeBorder.split(" ").filter((x) => x);
const inActiveShapeBorderWidth = parseFloat(inActiveBorder[0]);
const inActiveShapeBorderColor = inActiveBorder[1];
// active shape border
const activeShapeBorder = computedStyle.getPropertyValue("--active-shape-border");
const activeBorder = activeShapeBorder.split(" ").filter((x) => x);
const activeShapeBorderWidth = parseFloat(activeBorder[0]);
const activeShapeBorderColor = activeBorder[1];
return {
activeShapeColor: activeShapeColor ? activeShapeColor : "#ff8e8e",
inActiveShapeColor: inActiveShapeColor ? inActiveShapeColor : "#ffeba2",
activeBorder: {
width: activeShapeBorderWidth ? activeShapeBorderWidth : 1,
color: activeShapeBorderColor ? activeShapeBorderColor : "#212121",
},
inActiveBorder: {
width: inActiveShapeBorderWidth ? inActiveShapeBorderWidth : 1,
color: inActiveShapeBorderColor ? inActiveShapeBorderColor : "#212121",
},
};
} catch {
// return default values
return {
activeShapeColor: "#ff8e8e",
inActiveShapeColor: "#ffeba2",
activeBorder: {
width: 1,
color: "#212121",
},
inActiveBorder: {
width: 1,
color: "#212121",
},
};
}
}
const toggleMasks = (): void => {
const canvas = document.getElementById("image-occlusion-canvas") as HTMLCanvasElement;
const display = canvas.style.display;
if (display === "none") {
canvas.style.display = "unset";
} else {
canvas.style.display = "none";
}
};