9e147c6335
* Add text tool to IO * Remove unnecessary parentheses * Fix text objects always grouped * Remove log * Fix text objects hidden on back side * Implement text scaling * Add inverse text outline * Warn about IO notes with only text objects This will result in a different error message than the case where no objects are added at all though, and the user can bypass the warning. Maybe this is better to avoid discarding the user's work if they have spent some time adding text. * Add isValidType * Use matches! * Lock aspect ratio of text objects * Reword misleading comment The confusion probably comes from the Fabric docs, which apparently need updating: http://fabricjs.com/docs/fabric.Canvas.html#uniformScaling * Do not count text objects when calculating current index * Make text objects respond to size changes * Fix uniform scaling not working when editing * Use Arial font * Escape colons and unify parsing * Handle scale factor when restricting shape to view * Use 'cloned' * Add text background * Tweak drawShape's params
164 lines
5.6 KiB
TypeScript
164 lines
5.6 KiB
TypeScript
// Copyright: Ankitects Pty Ltd and contributors
|
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
import { protoBase64 } from "@bufbuild/protobuf";
|
|
import { getImageForOcclusion, getImageOcclusionNote } from "@tslib/backend";
|
|
import * as tr from "@tslib/ftl";
|
|
import { fabric } from "fabric";
|
|
import type { PanZoom } from "panzoom";
|
|
import { get } from "svelte/store";
|
|
|
|
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 { undoStack } from "./tools/tool-undo-redo";
|
|
import type { Size } from "./types";
|
|
|
|
export const setupMaskEditor = async (
|
|
path: string,
|
|
instance: PanZoom,
|
|
onChange: () => void,
|
|
): Promise<fabric.Canvas> => {
|
|
const imageData = await getImageForOcclusion({ path });
|
|
const canvas = initCanvas(onChange);
|
|
|
|
// get image width and height
|
|
const image = document.getElementById("image") as HTMLImageElement;
|
|
image.src = getImageData(imageData.data!);
|
|
image.onload = function() {
|
|
const size = optimumCssSizeForCanvas({ width: image.width, height: image.height }, containerSize());
|
|
canvas.setWidth(size.width);
|
|
canvas.setHeight(size.height);
|
|
image.height = size.height;
|
|
image.width = size.width;
|
|
setCanvasZoomRatio(canvas, instance);
|
|
undoStack.reset();
|
|
};
|
|
|
|
return canvas;
|
|
};
|
|
|
|
export const setupMaskEditorForEdit = async (
|
|
noteId: number,
|
|
instance: PanZoom,
|
|
onChange: () => void,
|
|
): Promise<fabric.Canvas> => {
|
|
const clozeNoteResponse = await getImageOcclusionNote({ noteId: BigInt(noteId) });
|
|
const kind = clozeNoteResponse.value?.case;
|
|
if (!kind || kind === "error") {
|
|
new Toast({
|
|
target: document.body,
|
|
props: {
|
|
message: tr.notetypesErrorGettingImagecloze(),
|
|
type: "error",
|
|
},
|
|
}).$set({ showToast: true });
|
|
return;
|
|
}
|
|
|
|
const clozeNote = clozeNoteResponse.value.value;
|
|
const canvas = initCanvas(onChange);
|
|
|
|
// get image width and height
|
|
const image = document.getElementById("image") as HTMLImageElement;
|
|
image.style.visibility = "hidden";
|
|
image.src = getImageData(clozeNote.imageData!);
|
|
image.onload = function() {
|
|
const size = optimumCssSizeForCanvas({ width: image.width, height: image.height }, containerSize());
|
|
canvas.setWidth(size.width);
|
|
canvas.setHeight(size.height);
|
|
image.height = size.height;
|
|
image.width = size.width;
|
|
|
|
setCanvasZoomRatio(canvas, instance);
|
|
addShapesToCanvasFromCloze(canvas, clozeNote.occlusions);
|
|
enableSelectable(canvas, true);
|
|
addClozeNotesToTextEditor(clozeNote.header, clozeNote.backExtra, clozeNote.tags);
|
|
undoStack.reset();
|
|
window.requestAnimationFrame(() => {
|
|
image.style.visibility = "visible";
|
|
});
|
|
};
|
|
|
|
return canvas;
|
|
};
|
|
|
|
function initCanvas(onChange: () => void): fabric.Canvas {
|
|
const canvas = new fabric.Canvas("canvas");
|
|
tagsWritable.set([]);
|
|
globalThis.canvas = canvas;
|
|
undoStack.setCanvas(canvas);
|
|
// Disable uniform scaling
|
|
canvas.uniformScaling = false;
|
|
canvas.uniScaleKey = "none";
|
|
moveShapeToCanvasBoundaries(canvas);
|
|
canvas.on("object:modified", onChange);
|
|
canvas.on("object:removed", onChange);
|
|
return canvas;
|
|
}
|
|
|
|
const getImageData = (imageData): string => {
|
|
const b64encoded = protoBase64.enc(imageData);
|
|
return "data:image/png;base64," + b64encoded;
|
|
};
|
|
|
|
export const setCanvasZoomRatio = (
|
|
canvas: fabric.Canvas,
|
|
instance: PanZoom,
|
|
): void => {
|
|
const zoomRatioW = (innerWidth - 40) / canvas.width!;
|
|
const zoomRatioH = (innerHeight - 100) / canvas.height!;
|
|
const zoomRatio = zoomRatioW < zoomRatioH ? zoomRatioW : zoomRatioH;
|
|
zoomResetValue.set(zoomRatio);
|
|
instance.zoomAbs(0, 0, zoomRatio);
|
|
};
|
|
|
|
const addClozeNotesToTextEditor = (header: string, backExtra: string, tags: string[]) => {
|
|
const noteFieldsData: { id: string; title: string; divValue: string; textareaValue: string }[] = get(
|
|
notesDataStore,
|
|
);
|
|
noteFieldsData[0].divValue = header;
|
|
noteFieldsData[1].divValue = backExtra;
|
|
noteFieldsData[0].textareaValue = header;
|
|
noteFieldsData[1].textareaValue = backExtra;
|
|
tagsWritable.set(tags);
|
|
|
|
noteFieldsData.forEach((note) => {
|
|
const divId = `${note.id}--div`;
|
|
const textAreaId = `${note.id}--textarea`;
|
|
const divElement = document.getElementById(divId)!;
|
|
const textAreaElement = document.getElementById(textAreaId)! as HTMLTextAreaElement;
|
|
divElement.innerHTML = note.divValue;
|
|
textAreaElement.value = note.textareaValue;
|
|
});
|
|
};
|
|
|
|
function containerSize(): Size {
|
|
const container = document.querySelector(".editor-main")!;
|
|
return {
|
|
width: container.clientWidth,
|
|
height: container.clientHeight,
|
|
};
|
|
}
|
|
|
|
export async function resetIOImage(path) {
|
|
const imageData = await getImageForOcclusion({ path });
|
|
const image = document.getElementById("image") as HTMLImageElement;
|
|
image.src = getImageData(imageData.data!);
|
|
const canvas = globalThis.canvas;
|
|
|
|
image.onload = function() {
|
|
const size = optimumCssSizeForCanvas(
|
|
{ width: image.naturalWidth, height: image.naturalHeight },
|
|
containerSize(),
|
|
);
|
|
canvas.setWidth(size.width);
|
|
canvas.setHeight(size.height);
|
|
image.height = size.height;
|
|
image.width = size.width;
|
|
};
|
|
}
|
|
globalThis.resetIOImage = resetIOImage;
|