7686cb8de8
* 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
207 lines
7.1 KiB
TypeScript
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";
|
|
}
|
|
};
|