mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
Allow rotating IO masks (#3987)
* Revert "Disable rotation globally"
This reverts commit 22736238c1
.
* alt. impl for hiding rotation marker when selecting/ungrouping
* (de)serialise angles
* rotate masks in reviewer
* update bounds checking
* floats.ts -> lib.ts
* add convenience fns
* store mask angles (deg) in steps of 10000
* update CONTRIBUTORS
This commit is contained in:
parent
5cc44b3f68
commit
573f59fab1
12 changed files with 84 additions and 29 deletions
|
@ -201,7 +201,7 @@ Dongjin Ouyang <1113117424@qq.com>
|
|||
Sawan Sunar <sawansunar24072002@gmail.com>
|
||||
hideo aoyama <https://github.com/boukendesho>
|
||||
Ross Brown <rbrownwsws@googlemail.com>
|
||||
🦙 <github.com/iamllama>
|
||||
🦙 <gh@siid.sh>
|
||||
Lukas Sommer <sommerluk@gmail.com>
|
||||
Luca Auer <lolle2000.la@gmail.com>
|
||||
Niclas Heinz <nheinz@hpost.net>
|
||||
|
|
|
@ -74,6 +74,11 @@ pub fn get_image_cloze_data(text: &str) -> String {
|
|||
result.push_str(&format!("data-top=\"{}\" ", property.value));
|
||||
}
|
||||
}
|
||||
"angle" => {
|
||||
if !property.value.is_empty() {
|
||||
result.push_str(&format!("data-angle=\"{}\" ", property.value));
|
||||
}
|
||||
}
|
||||
"width" => {
|
||||
if !is_empty_or_zero(&property.value) {
|
||||
result.push_str(&format!("data-width=\"{}\" ", property.value));
|
||||
|
|
|
@ -99,8 +99,6 @@ function initCanvas(): fabric.Canvas {
|
|||
// Disable uniform scaling
|
||||
canvas.uniformScaling = false;
|
||||
canvas.uniScaleKey = "none";
|
||||
// disable rotation globally
|
||||
delete fabric.Object.prototype.controls.mtr;
|
||||
// disable object caching
|
||||
fabric.Object.prototype.objectCaching = false;
|
||||
// add a border to corner to handle blend of control
|
||||
|
@ -108,6 +106,11 @@ function initCanvas(): fabric.Canvas {
|
|||
fabric.Object.prototype.cornerStyle = "circle";
|
||||
fabric.Object.prototype.cornerStrokeColor = "#000000";
|
||||
fabric.Object.prototype.padding = 8;
|
||||
// disable rotation when selecting
|
||||
canvas.on("selection:created", () => {
|
||||
const g = canvas.getActiveObject();
|
||||
if (g && g instanceof fabric.Group) { g.setControlsVisibility({ mtr: false }); }
|
||||
});
|
||||
canvas.on("object:modified", (evt) => {
|
||||
if (evt.target instanceof fabric.Polygon) {
|
||||
modifiedPolygon(canvas, evt.target);
|
||||
|
|
|
@ -263,15 +263,29 @@ function drawShape({
|
|||
ctx.fillStyle = fill;
|
||||
ctx.strokeStyle = stroke;
|
||||
ctx.lineWidth = strokeWidth;
|
||||
const angle = ((shape.angle ?? 0) * Math.PI) / 180;
|
||||
if (shape instanceof Rectangle) {
|
||||
if (angle) {
|
||||
ctx.save();
|
||||
ctx.translate(shape.left, shape.top);
|
||||
ctx.rotate(angle);
|
||||
ctx.translate(-shape.left, -shape.top);
|
||||
}
|
||||
ctx.fillRect(shape.left, shape.top, shape.width, shape.height);
|
||||
// ctx stroke methods will draw a visible stroke, even if the width is 0
|
||||
if (strokeWidth) {
|
||||
ctx.strokeRect(shape.left, shape.top, shape.width, shape.height);
|
||||
}
|
||||
if (angle) { ctx.restore(); }
|
||||
} else if (shape instanceof Ellipse) {
|
||||
const adjustedLeft = shape.left + shape.rx;
|
||||
const adjustedTop = shape.top + shape.ry;
|
||||
if (angle) {
|
||||
ctx.save();
|
||||
ctx.translate(shape.left, shape.top);
|
||||
ctx.rotate(angle);
|
||||
ctx.translate(-shape.left, -shape.top);
|
||||
}
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(
|
||||
adjustedLeft,
|
||||
|
@ -288,6 +302,7 @@ function drawShape({
|
|||
if (strokeWidth) {
|
||||
ctx.stroke();
|
||||
}
|
||||
if (angle) { ctx.restore(); }
|
||||
} else if (shape instanceof Polygon) {
|
||||
const offset = getPolygonOffset(shape);
|
||||
ctx.save();
|
||||
|
@ -329,10 +344,17 @@ function drawShape({
|
|||
}
|
||||
totalHeight += lineHeight;
|
||||
}
|
||||
const left = shape.left / shape.scaleX;
|
||||
const top = shape.top / shape.scaleY;
|
||||
if (angle) {
|
||||
ctx.translate(left, top);
|
||||
ctx.rotate(angle);
|
||||
ctx.translate(-left, -top);
|
||||
}
|
||||
ctx.fillStyle = TEXT_BACKGROUND_COLOR;
|
||||
ctx.fillRect(
|
||||
shape.left / shape.scaleX,
|
||||
shape.top / shape.scaleY,
|
||||
left,
|
||||
top,
|
||||
maxWidth + TEXT_PADDING,
|
||||
totalHeight + TEXT_PADDING,
|
||||
);
|
||||
|
|
|
@ -5,7 +5,7 @@ import { fabric } from "fabric";
|
|||
|
||||
import { SHAPE_MASK_COLOR } from "../tools/lib";
|
||||
import type { ConstructorParams, Size } from "../types";
|
||||
import { floatToDisplay } from "./floats";
|
||||
import { angleToStored, floatToDisplay } from "./lib";
|
||||
import { xFromNormalized, xToNormalized, yFromNormalized, yToNormalized } from "./position";
|
||||
|
||||
export type ShapeOrShapes = Shape | Shape[];
|
||||
|
@ -18,6 +18,7 @@ export type ShapeOrShapes = Shape | Shape[];
|
|||
export class Shape {
|
||||
left: number;
|
||||
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.
|
||||
|
@ -27,11 +28,12 @@ export class Shape {
|
|||
ordinal: number | undefined;
|
||||
|
||||
constructor(
|
||||
{ left = 0, top = 0, fill = SHAPE_MASK_COLOR, occludeInactive, ordinal = undefined }: ConstructorParams<Shape> =
|
||||
{},
|
||||
{ left = 0, top = 0, angle = 0, fill = SHAPE_MASK_COLOR, occludeInactive, ordinal = undefined }:
|
||||
ConstructorParams<Shape> = {},
|
||||
) {
|
||||
this.left = left;
|
||||
this.top = top;
|
||||
this.angle = angle;
|
||||
this.fill = fill;
|
||||
this.occludeInactive = occludeInactive;
|
||||
this.ordinal = ordinal;
|
||||
|
@ -41,9 +43,11 @@ export class Shape {
|
|||
* text.
|
||||
*/
|
||||
toDataForCloze(): ShapeDataForCloze {
|
||||
const angle = angleToStored(this.angle);
|
||||
return {
|
||||
left: floatToDisplay(this.left),
|
||||
top: floatToDisplay(this.top),
|
||||
...(!angle ? {} : { angle: angle.toString() }),
|
||||
...(this.fill === SHAPE_MASK_COLOR ? {} : { fill: this.fill }),
|
||||
};
|
||||
}
|
||||
|
@ -85,6 +89,7 @@ export class Shape {
|
|||
export interface ShapeDataForCloze {
|
||||
left: string;
|
||||
top: string;
|
||||
angle?: string;
|
||||
fill?: string;
|
||||
oi?: string;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { fabric } from "fabric";
|
|||
import type { ConstructorParams, Size } from "../types";
|
||||
import type { ShapeDataForCloze } from "./base";
|
||||
import { Shape } from "./base";
|
||||
import { floatToDisplay } from "./floats";
|
||||
import { floatToDisplay } from "./lib";
|
||||
import { xFromNormalized, xToNormalized, yFromNormalized, yToNormalized } from "./position";
|
||||
|
||||
export class Ellipse extends Shape {
|
||||
|
|
|
@ -9,6 +9,7 @@ import type { GetImageOcclusionNoteResponse_ImageOcclusion } from "@generated/an
|
|||
|
||||
import type { Shape, ShapeOrShapes } from "./base";
|
||||
import { Ellipse } from "./ellipse";
|
||||
import { storedToAngle } from "./lib";
|
||||
import { Point, Polygon } from "./polygon";
|
||||
import { Rectangle } from "./rectangle";
|
||||
import { Text } from "./text";
|
||||
|
@ -75,6 +76,7 @@ function extractShapeFromRenderedCloze(cloze: HTMLDivElement): Shape | null {
|
|||
text: cloze.dataset.text,
|
||||
scale: cloze.dataset.scale,
|
||||
fs: cloze.dataset.fontSize,
|
||||
angle: cloze.dataset.angle,
|
||||
};
|
||||
return buildShape(type, props);
|
||||
}
|
||||
|
@ -92,6 +94,7 @@ function buildShape(type: ShapeType, props: Record<string, any>): Shape {
|
|||
props.top = parseFloat(
|
||||
Number.isNaN(Number(props.top)) ? ".0000" : props.top,
|
||||
);
|
||||
props.angle = storedToAngle(props.angle) ?? 0;
|
||||
switch (type) {
|
||||
case "rect": {
|
||||
return new Rectangle({
|
||||
|
|
|
@ -11,3 +11,15 @@ export function floatToDisplay(number: number): string {
|
|||
}
|
||||
return number.toFixed(4).replace(/^0+|0+$/g, "");
|
||||
}
|
||||
|
||||
const ANGLE_STEPS = 10000;
|
||||
|
||||
export function angleToStored(angle: any): number | null {
|
||||
const angleDeg = Number(angle) % 360;
|
||||
return Number.isNaN(angleDeg) ? null : Math.round((angleDeg / 360) * ANGLE_STEPS);
|
||||
}
|
||||
|
||||
export function storedToAngle(x: any): number | null {
|
||||
const angleSteps = Number(x) % ANGLE_STEPS;
|
||||
return Number.isNaN(angleSteps) ? null : (angleSteps / ANGLE_STEPS) * 360;
|
||||
}
|
|
@ -6,7 +6,7 @@ import { fabric } from "fabric";
|
|||
import type { ConstructorParams, Size } from "../types";
|
||||
import type { ShapeDataForCloze } from "./base";
|
||||
import { Shape } from "./base";
|
||||
import { floatToDisplay } from "./floats";
|
||||
import { floatToDisplay } from "./lib";
|
||||
import { xFromNormalized, xToNormalized, yFromNormalized, yToNormalized } from "./position";
|
||||
|
||||
export class Polygon extends Shape {
|
||||
|
|
|
@ -6,7 +6,7 @@ import { fabric } from "fabric";
|
|||
import type { ConstructorParams, Size } from "../types";
|
||||
import type { ShapeDataForCloze } from "./base";
|
||||
import { Shape } from "./base";
|
||||
import { floatToDisplay } from "./floats";
|
||||
import { floatToDisplay } from "./lib";
|
||||
import { xFromNormalized, xToNormalized, yFromNormalized, yToNormalized } from "./position";
|
||||
|
||||
export class Rectangle extends Shape {
|
||||
|
|
|
@ -7,7 +7,7 @@ import { TEXT_BACKGROUND_COLOR, TEXT_COLOR, TEXT_FONT_FAMILY, TEXT_FONT_SIZE, TE
|
|||
import type { ConstructorParams, Size } from "../types";
|
||||
import type { ShapeDataForCloze } from "./base";
|
||||
import { Shape } from "./base";
|
||||
import { floatToDisplay } from "./floats";
|
||||
import { floatToDisplay } from "./lib";
|
||||
|
||||
export class Text extends Shape {
|
||||
text: string;
|
||||
|
|
|
@ -76,7 +76,7 @@ export const groupShapes = (canvas: fabric.Canvas): void => {
|
|||
|
||||
activeObject.toGroup().set({
|
||||
opacity: get(opacityStateStore) ? 0.4 : 1,
|
||||
});
|
||||
}).setControlsVisibility({ mtr: false });
|
||||
|
||||
redraw(canvas);
|
||||
};
|
||||
|
@ -228,18 +228,21 @@ const setShapePosition = (
|
|||
boundingBox: fabric.Rect,
|
||||
object: fabric.Object,
|
||||
): void => {
|
||||
if (object.left! < 0) {
|
||||
object.set({ left: 0 });
|
||||
const { left, top, width, height } = object.getBoundingRect(true);
|
||||
|
||||
if (left < 0) {
|
||||
object.set({ left: Math.max(object.left! - left, 0) });
|
||||
}
|
||||
if (object.top! < 0) {
|
||||
object.set({ top: 0 });
|
||||
if (top < 0) {
|
||||
object.set({ top: Math.max(object.top! - top, 0) });
|
||||
}
|
||||
if (object.left! + object.width! * object.scaleX! + object.strokeWidth! > boundingBox.width!) {
|
||||
object.set({ left: boundingBox.width! - object.width! * object.scaleX! });
|
||||
if (left > boundingBox.width!) {
|
||||
object.set({ left: object.left! - left - width + boundingBox.width! });
|
||||
}
|
||||
if (object.top! + object.height! * object.scaleY! + object.strokeWidth! > boundingBox.height!) {
|
||||
object.set({ top: boundingBox.height! - object.height! * object.scaleY! });
|
||||
if (top > boundingBox.height!) {
|
||||
object.set({ top: object.top! - top - height + boundingBox.height! });
|
||||
}
|
||||
|
||||
object.setCoords();
|
||||
};
|
||||
|
||||
|
@ -277,23 +280,25 @@ export const makeShapesRemainInCanvas = (canvas: fabric.Canvas, boundingBox: fab
|
|||
canvas.on("object:moving", function(e) {
|
||||
const obj = e.target!;
|
||||
|
||||
const objWidth = obj.getScaledWidth();
|
||||
const objHeight = obj.getScaledHeight();
|
||||
const { left: objBbLeft, top: objBbTop, width: objBbWidth, height: objBbHeight } = obj.getBoundingRect(
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
if (objWidth > boundingBox.width! || objHeight > boundingBox.height!) {
|
||||
if (objBbWidth > boundingBox.width! || objBbHeight > boundingBox.height!) {
|
||||
return;
|
||||
}
|
||||
|
||||
const top = obj.top!;
|
||||
const left = obj.left!;
|
||||
|
||||
const topBound = boundingBox.top!;
|
||||
const bottomBound = topBound + boundingBox.height! + 5;
|
||||
const leftBound = boundingBox.left!;
|
||||
const rightBound = leftBound + boundingBox.width! + 5;
|
||||
|
||||
obj.left = Math.min(Math.max(left, leftBound), rightBound - objWidth);
|
||||
obj.top = Math.min(Math.max(top, topBound), bottomBound - objHeight);
|
||||
const newBbLeft = Math.min(Math.max(objBbLeft, leftBound), rightBound - objBbWidth);
|
||||
const newBbTop = Math.min(Math.max(objBbTop, topBound), bottomBound - objBbHeight);
|
||||
|
||||
obj.left = obj.left! + newBbLeft - objBbLeft;
|
||||
obj.top = obj.top! + newBbTop - objBbTop;
|
||||
});
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue