mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
Add text tool to image occlusion (#2705)
* 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
This commit is contained in:
parent
b35a11ffd6
commit
9e147c6335
17 changed files with 494 additions and 184 deletions
|
@ -54,9 +54,19 @@ message GetImageOcclusionNoteRequest {
|
|||
}
|
||||
|
||||
message GetImageOcclusionNoteResponse {
|
||||
message ImageOcclusionProperty {
|
||||
string name = 1;
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
message ImageOcclusion {
|
||||
string shape = 1;
|
||||
repeated ImageOcclusionProperty properties = 2;
|
||||
}
|
||||
|
||||
message ImageClozeNote {
|
||||
bytes image_data = 1;
|
||||
string occlusions = 2;
|
||||
repeated ImageOcclusion occlusions = 2;
|
||||
string header = 3;
|
||||
string back_extra = 4;
|
||||
repeated string tags = 5;
|
||||
|
|
|
@ -5,6 +5,7 @@ use std::borrow::Cow;
|
|||
use std::collections::HashSet;
|
||||
use std::fmt::Write;
|
||||
|
||||
use anki_proto::image_occlusion::get_image_occlusion_note_response::ImageOcclusion;
|
||||
use htmlescape::encode_attribute;
|
||||
use lazy_static::lazy_static;
|
||||
use nom::branch::alt;
|
||||
|
@ -16,6 +17,7 @@ use regex::Captures;
|
|||
use regex::Regex;
|
||||
|
||||
use crate::image_occlusion::imageocclusion::get_image_cloze_data;
|
||||
use crate::image_occlusion::imageocclusion::parse_image_cloze;
|
||||
use crate::latex::contains_latex;
|
||||
use crate::template::RenderContext;
|
||||
use crate::text::strip_html_preserving_entities;
|
||||
|
@ -229,6 +231,7 @@ fn reveal_cloze(
|
|||
image_occlusion_text,
|
||||
question,
|
||||
active,
|
||||
cloze.ordinal,
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
@ -295,8 +298,8 @@ fn reveal_cloze(
|
|||
}
|
||||
}
|
||||
|
||||
fn render_image_occlusion(text: &str, question_side: bool, active: bool) -> String {
|
||||
if question_side && active {
|
||||
fn render_image_occlusion(text: &str, question_side: bool, active: bool, ordinal: u16) -> String {
|
||||
if (question_side && active) || ordinal == 0 {
|
||||
format!(
|
||||
r#"<div class="cloze" {}></div>"#,
|
||||
&get_image_cloze_data(text)
|
||||
|
@ -311,6 +314,18 @@ fn render_image_occlusion(text: &str, question_side: bool, active: bool) -> Stri
|
|||
}
|
||||
}
|
||||
|
||||
pub fn parse_image_occlusions(text: &str) -> Vec<ImageOcclusion> {
|
||||
parse_text_with_clozes(text)
|
||||
.iter()
|
||||
.filter_map(|node| match node {
|
||||
TextOrCloze::Cloze(cloze) if cloze.image_occlusion().is_some() => {
|
||||
parse_image_cloze(cloze.image_occlusion().unwrap())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn reveal_cloze_text(text: &str, cloze_ord: u16, question: bool) -> Cow<str> {
|
||||
let mut buf = String::new();
|
||||
let mut active_cloze_found_in_text = false;
|
||||
|
@ -377,7 +392,7 @@ pub fn expand_clozes_to_reveal_latex(text: &str) -> String {
|
|||
pub(crate) fn contains_cloze(text: &str) -> bool {
|
||||
parse_text_with_clozes(text)
|
||||
.iter()
|
||||
.any(|node| matches!(node, TextOrCloze::Cloze(_)))
|
||||
.any(|node| matches!(node, TextOrCloze::Cloze(e) if e.ordinal != 0))
|
||||
}
|
||||
|
||||
pub fn cloze_numbers_in_string(html: &str) -> HashSet<u16> {
|
||||
|
@ -389,8 +404,10 @@ pub fn cloze_numbers_in_string(html: &str) -> HashSet<u16> {
|
|||
fn add_cloze_numbers_in_text_with_clozes(nodes: &[TextOrCloze], set: &mut HashSet<u16>) {
|
||||
for node in nodes {
|
||||
if let TextOrCloze::Cloze(cloze) = node {
|
||||
set.insert(cloze.ordinal);
|
||||
add_cloze_numbers_in_text_with_clozes(&cloze.nodes, set);
|
||||
if !(cloze.image_occlusion().is_some() && cloze.ordinal == 0) {
|
||||
set.insert(cloze.ordinal);
|
||||
add_cloze_numbers_in_text_with_clozes(&cloze.nodes, set);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ use anki_proto::image_occlusion::ImageOcclusionFieldIndexes;
|
|||
use anki_proto::notetypes::ImageOcclusionField;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::cloze::parse_image_occlusions;
|
||||
use crate::media::MediaManager;
|
||||
use crate::prelude::*;
|
||||
|
||||
|
@ -92,7 +93,7 @@ impl Collection {
|
|||
.or_not_found(note.notetype_id)?;
|
||||
let idxs = nt.get_io_field_indexes()?;
|
||||
|
||||
cloze_note.occlusions = fields[idxs.occlusions as usize].clone();
|
||||
cloze_note.occlusions = parse_image_occlusions(fields[idxs.occlusions as usize].as_str());
|
||||
cloze_note.header = fields[idxs.header as usize].clone();
|
||||
cloze_note.back_extra = fields[idxs.back_extra as usize].clone();
|
||||
cloze_note.image_data = "".into();
|
||||
|
|
|
@ -3,6 +3,48 @@
|
|||
|
||||
use std::fmt::Write;
|
||||
|
||||
use anki_proto::image_occlusion::get_image_occlusion_note_response::ImageOcclusion;
|
||||
use anki_proto::image_occlusion::get_image_occlusion_note_response::ImageOcclusionProperty;
|
||||
use htmlescape::encode_attribute;
|
||||
use nom::bytes::complete::escaped;
|
||||
use nom::bytes::complete::is_not;
|
||||
use nom::bytes::complete::tag;
|
||||
use nom::character::complete::char;
|
||||
use nom::error::ErrorKind;
|
||||
use nom::sequence::preceded;
|
||||
use nom::sequence::separated_pair;
|
||||
|
||||
fn unescape(text: &str) -> String {
|
||||
text.replace("\\:", ":")
|
||||
}
|
||||
|
||||
pub fn parse_image_cloze(text: &str) -> Option<ImageOcclusion> {
|
||||
if let Some((shape, _)) = text.split_once(':') {
|
||||
let mut properties = vec![];
|
||||
let mut remaining = &text[shape.len()..];
|
||||
while let Ok((rem, (name, value))) = separated_pair::<_, _, _, _, (_, ErrorKind), _, _, _>(
|
||||
preceded(tag(":"), is_not("=")),
|
||||
tag("="),
|
||||
escaped(is_not("\\:"), '\\', char(':')),
|
||||
)(remaining)
|
||||
{
|
||||
remaining = rem;
|
||||
let value = unescape(value);
|
||||
properties.push(ImageOcclusionProperty {
|
||||
name: name.to_string(),
|
||||
value,
|
||||
})
|
||||
}
|
||||
|
||||
return Some(ImageOcclusion {
|
||||
shape: shape.to_string(),
|
||||
properties,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
// convert text like
|
||||
// rect:left=.2325:top=.3261:width=.202:height=.0975
|
||||
// to something like
|
||||
|
@ -10,72 +52,83 @@ use std::fmt::Write;
|
|||
// data-width="167.09" data-height="33.78"
|
||||
pub fn get_image_cloze_data(text: &str) -> String {
|
||||
let mut result = String::new();
|
||||
let parts: Vec<&str> = text.split(':').collect();
|
||||
|
||||
if parts.len() >= 2 {
|
||||
if !parts[0].is_empty()
|
||||
&& (parts[0] == "rect" || parts[0] == "ellipse" || parts[0] == "polygon")
|
||||
if let Some(occlusion) = parse_image_cloze(text) {
|
||||
if !occlusion.shape.is_empty()
|
||||
&& matches!(
|
||||
occlusion.shape.as_str(),
|
||||
"rect" | "ellipse" | "polygon" | "text"
|
||||
)
|
||||
{
|
||||
result.push_str(&format!("data-shape=\"{}\" ", parts[0]));
|
||||
result.push_str(&format!("data-shape=\"{}\" ", occlusion.shape));
|
||||
}
|
||||
|
||||
for part in parts[1..].iter() {
|
||||
let values: Vec<&str> = part.split('=').collect();
|
||||
if values.len() >= 2 {
|
||||
match values[0] {
|
||||
"left" => {
|
||||
if !values[1].is_empty() {
|
||||
result.push_str(&format!("data-left=\"{}\" ", values[1]));
|
||||
}
|
||||
for property in occlusion.properties {
|
||||
match property.name.as_str() {
|
||||
"left" => {
|
||||
if !property.value.is_empty() {
|
||||
result.push_str(&format!("data-left=\"{}\" ", property.value));
|
||||
}
|
||||
"top" => {
|
||||
if !values[1].is_empty() {
|
||||
result.push_str(&format!("data-top=\"{}\" ", values[1]));
|
||||
}
|
||||
}
|
||||
"width" => {
|
||||
if !is_empty_or_zero(values[1]) {
|
||||
result.push_str(&format!("data-width=\"{}\" ", values[1]));
|
||||
}
|
||||
}
|
||||
"height" => {
|
||||
if !is_empty_or_zero(values[1]) {
|
||||
result.push_str(&format!("data-height=\"{}\" ", values[1]));
|
||||
}
|
||||
}
|
||||
"rx" => {
|
||||
if !is_empty_or_zero(values[1]) {
|
||||
result.push_str(&format!("data-rx=\"{}\" ", values[1]));
|
||||
}
|
||||
}
|
||||
"ry" => {
|
||||
if !is_empty_or_zero(values[1]) {
|
||||
result.push_str(&format!("data-ry=\"{}\" ", values[1]));
|
||||
}
|
||||
}
|
||||
"points" => {
|
||||
if !values[1].is_empty() {
|
||||
let mut point_str = String::new();
|
||||
for point_pair in values[1].split(' ') {
|
||||
let Some((x, y)) = point_pair.split_once(',') else {
|
||||
continue;
|
||||
};
|
||||
write!(&mut point_str, "{},{} ", x, y).unwrap();
|
||||
}
|
||||
// remove the trailing space
|
||||
point_str.pop();
|
||||
if !point_str.is_empty() {
|
||||
result.push_str(&format!("data-points=\"{point_str}\" "));
|
||||
}
|
||||
}
|
||||
}
|
||||
"oi" => {
|
||||
if !values[1].is_empty() {
|
||||
result.push_str(&format!("data-occludeInactive=\"{}\" ", values[1]));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
"top" => {
|
||||
if !property.value.is_empty() {
|
||||
result.push_str(&format!("data-top=\"{}\" ", property.value));
|
||||
}
|
||||
}
|
||||
"width" => {
|
||||
if !is_empty_or_zero(&property.value) {
|
||||
result.push_str(&format!("data-width=\"{}\" ", property.value));
|
||||
}
|
||||
}
|
||||
"height" => {
|
||||
if !is_empty_or_zero(&property.value) {
|
||||
result.push_str(&format!("data-height=\"{}\" ", property.value));
|
||||
}
|
||||
}
|
||||
"rx" => {
|
||||
if !is_empty_or_zero(&property.value) {
|
||||
result.push_str(&format!("data-rx=\"{}\" ", property.value));
|
||||
}
|
||||
}
|
||||
"ry" => {
|
||||
if !is_empty_or_zero(&property.value) {
|
||||
result.push_str(&format!("data-ry=\"{}\" ", property.value));
|
||||
}
|
||||
}
|
||||
"points" => {
|
||||
if !property.value.is_empty() {
|
||||
let mut point_str = String::new();
|
||||
for point_pair in property.value.split(' ') {
|
||||
let Some((x, y)) = point_pair.split_once(',') else {
|
||||
continue;
|
||||
};
|
||||
write!(&mut point_str, "{},{} ", x, y).unwrap();
|
||||
}
|
||||
// remove the trailing space
|
||||
point_str.pop();
|
||||
if !point_str.is_empty() {
|
||||
result.push_str(&format!("data-points=\"{point_str}\" "));
|
||||
}
|
||||
}
|
||||
}
|
||||
"oi" => {
|
||||
if !property.value.is_empty() {
|
||||
result.push_str(&format!("data-occludeInactive=\"{}\" ", property.value));
|
||||
}
|
||||
}
|
||||
"text" => {
|
||||
if !property.value.is_empty() {
|
||||
result.push_str(&format!(
|
||||
"data-text=\"{}\" ",
|
||||
encode_attribute(&property.value)
|
||||
));
|
||||
}
|
||||
}
|
||||
"scale" => {
|
||||
if !is_empty_or_zero(&property.value) {
|
||||
result.push_str(&format!("data-scale=\"{}\" ", property.value));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -107,4 +160,8 @@ fn test_get_image_cloze_data() {
|
|||
get_image_cloze_data("polygon:points=0,0 10,10 20,0"),
|
||||
r#"data-shape="polygon" data-points="0,0 10,10 20,0" "#,
|
||||
);
|
||||
assert_eq!(
|
||||
get_image_cloze_data("text:text=foo\\:bar:left=10"),
|
||||
r#"data-shape="text" data-text="foo:bar" data-left="10" "#,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
import { mdiEye, mdiFormatAlignCenter, mdiSquare, mdiViewDashboard } from "./icons";
|
||||
import { hideAllGuessOne } from "./store";
|
||||
import { drawEllipse, drawPolygon, drawRectangle } from "./tools/index";
|
||||
import { drawEllipse, drawPolygon, drawRectangle, drawText } from "./tools/index";
|
||||
import { makeMaskTransparent } from "./tools/lib";
|
||||
import { enableSelectable, stopDraw } from "./tools/lib";
|
||||
import {
|
||||
|
@ -58,6 +58,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
case "draw-polygon":
|
||||
drawPolygon(canvas, instance);
|
||||
break;
|
||||
case "draw-text":
|
||||
drawText(canvas);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ export { default as mdiRectangleOutline } from "@mdi/svg/svg/rectangle-outline.s
|
|||
export { default as mdiRedo } from "@mdi/svg/svg/redo.svg";
|
||||
export { default as mdiRefresh } from "@mdi/svg/svg/refresh.svg";
|
||||
export { default as mdiSquare } from "@mdi/svg/svg/square.svg";
|
||||
export { default as mdiTextBox } from "@mdi/svg/svg/text-box.svg";
|
||||
export { default as mdiUndo } from "@mdi/svg/svg/undo.svg";
|
||||
export { default as mdiUnfoldMoreHorizontal } from "@mdi/svg/svg/unfold-more-horizontal.svg";
|
||||
export { default as mdiUngroup } from "@mdi/svg/svg/ungroup.svg";
|
||||
|
|
|
@ -90,7 +90,7 @@ function initCanvas(onChange: () => void): fabric.Canvas {
|
|||
tagsWritable.set([]);
|
||||
globalThis.canvas = canvas;
|
||||
undoStack.setCanvas(canvas);
|
||||
// enables uniform scaling by default without the need for the Shift key
|
||||
// Disable uniform scaling
|
||||
canvas.uniformScaling = false;
|
||||
canvas.uniScaleKey = "none";
|
||||
moveShapeToCanvasBoundaries(canvas);
|
||||
|
|
|
@ -9,6 +9,8 @@ import { Ellipse } from "./shapes/ellipse";
|
|||
import { extractShapesFromRenderedClozes } from "./shapes/from-cloze";
|
||||
import { Polygon } from "./shapes/polygon";
|
||||
import { Rectangle } from "./shapes/rectangle";
|
||||
import { Text } from "./shapes/text";
|
||||
import { TEXT_FONT_FAMILY, TEXT_PADDING } from "./tools/lib";
|
||||
import type { Size } from "./types";
|
||||
|
||||
export function setupImageCloze(): void {
|
||||
|
@ -19,14 +21,20 @@ export function setupImageCloze(): void {
|
|||
}
|
||||
|
||||
function setupImageClozeInner(): void {
|
||||
const canvas = document.querySelector("#image-occlusion-canvas") as HTMLCanvasElement | null;
|
||||
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;
|
||||
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;
|
||||
|
@ -51,17 +59,18 @@ function setupImageClozeInner(): void {
|
|||
drawShapes(canvas, ctx);
|
||||
}
|
||||
|
||||
function drawShapes(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D): void {
|
||||
const shapeProperty = getShapeProperty();
|
||||
function drawShapes(
|
||||
canvas: HTMLCanvasElement,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
): void {
|
||||
const properties = getShapeProperties();
|
||||
const size = canvas;
|
||||
for (const active of extractShapesFromRenderedClozes(".cloze")) {
|
||||
const fill = shapeProperty.activeShapeColor;
|
||||
drawShape(ctx, size, active, fill, shapeProperty.activeBorder);
|
||||
for (const shape of extractShapesFromRenderedClozes(".cloze")) {
|
||||
drawShape(ctx, size, shape, properties, true);
|
||||
}
|
||||
for (const inactive of extractShapesFromRenderedClozes(".cloze-inactive")) {
|
||||
const fill = shapeProperty.inActiveShapeColor;
|
||||
if (inactive.occludeInactive) {
|
||||
drawShape(ctx, size, inactive, fill, shapeProperty.inActiveBorder);
|
||||
for (const shape of extractShapesFromRenderedClozes(".cloze-inactive")) {
|
||||
if (shape.occludeInactive) {
|
||||
drawShape(ctx, size, shape, properties, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -70,10 +79,21 @@ function drawShape(
|
|||
ctx: CanvasRenderingContext2D,
|
||||
size: Size,
|
||||
shape: Shape,
|
||||
color: string,
|
||||
border: { width: number; color: string },
|
||||
properties: ShapeProperties,
|
||||
active: boolean,
|
||||
): void {
|
||||
shape.makeAbsolute(size);
|
||||
|
||||
const { color, border } = active
|
||||
? {
|
||||
color: properties.activeShapeColor,
|
||||
border: properties.activeBorder,
|
||||
}
|
||||
: {
|
||||
color: properties.inActiveShapeColor,
|
||||
border: properties.inActiveBorder,
|
||||
};
|
||||
|
||||
ctx.fillStyle = color;
|
||||
ctx.strokeStyle = border.color;
|
||||
ctx.lineWidth = border.width;
|
||||
|
@ -84,7 +104,16 @@ function drawShape(
|
|||
const adjustedLeft = shape.left + shape.rx;
|
||||
const adjustedTop = shape.top + shape.ry;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(adjustedLeft, adjustedTop, shape.rx, shape.ry, 0, 0, Math.PI * 2, false);
|
||||
ctx.ellipse(
|
||||
adjustedLeft,
|
||||
adjustedTop,
|
||||
shape.rx,
|
||||
shape.ry,
|
||||
0,
|
||||
0,
|
||||
Math.PI * 2,
|
||||
false,
|
||||
);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
|
@ -101,6 +130,26 @@ function drawShape(
|
|||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
} else if (shape instanceof Text) {
|
||||
ctx.save();
|
||||
ctx.font = `40px ${TEXT_FONT_FAMILY}`;
|
||||
ctx.textBaseline = "top";
|
||||
ctx.scale(shape.scaleX, shape.scaleY);
|
||||
const textMetrics = ctx.measureText(shape.text);
|
||||
ctx.fillStyle = properties.inActiveShapeColor;
|
||||
ctx.fillRect(
|
||||
shape.left / shape.scaleX,
|
||||
shape.top / shape.scaleY,
|
||||
textMetrics.width + TEXT_PADDING,
|
||||
textMetrics.actualBoundingBoxDescent + TEXT_PADDING,
|
||||
);
|
||||
ctx.fillStyle = "#000";
|
||||
ctx.fillText(
|
||||
shape.text,
|
||||
shape.left / shape.scaleX,
|
||||
shape.top / shape.scaleY,
|
||||
);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,7 +158,10 @@ function getPolygonOffset(polygon: Polygon): { x: number; y: number } {
|
|||
return { x: polygon.left - topLeft.x, y: polygon.top - topLeft.y };
|
||||
}
|
||||
|
||||
function topLeftOfPoints(points: { x: number; y: number }[]): { x: number; y: number } {
|
||||
function topLeftOfPoints(points: { x: number; y: number }[]): {
|
||||
x: number;
|
||||
y: number;
|
||||
} {
|
||||
let top = points[0].y;
|
||||
let left = points[0].x;
|
||||
for (const point of points) {
|
||||
|
@ -123,40 +175,55 @@ function topLeftOfPoints(points: { x: number; y: number }[]): { x: number; y: nu
|
|||
return { x: left, y: top };
|
||||
}
|
||||
|
||||
function getShapeProperty(): {
|
||||
type ShapeProperties = {
|
||||
activeShapeColor: string;
|
||||
inActiveShapeColor: string;
|
||||
activeBorder: { width: number; color: string };
|
||||
inActiveBorder: { width: number; color: string };
|
||||
} {
|
||||
};
|
||||
function getShapeProperties(): ShapeProperties {
|
||||
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");
|
||||
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 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 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",
|
||||
inActiveShapeColor: inActiveShapeColor
|
||||
? inActiveShapeColor
|
||||
: "#ffeba2",
|
||||
activeBorder: {
|
||||
width: activeShapeBorderWidth ? activeShapeBorderWidth : 1,
|
||||
color: activeShapeBorderColor ? activeShapeBorderColor : "#212121",
|
||||
color: activeShapeBorderColor
|
||||
? activeShapeBorderColor
|
||||
: "#212121",
|
||||
},
|
||||
inActiveBorder: {
|
||||
width: inActiveShapeBorderWidth ? inActiveShapeBorderWidth : 1,
|
||||
color: inActiveShapeBorderColor ? inActiveShapeBorderColor : "#212121",
|
||||
color: inActiveShapeBorderColor
|
||||
? inActiveShapeBorderColor
|
||||
: "#212121",
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
|
@ -177,7 +244,9 @@ function getShapeProperty(): {
|
|||
}
|
||||
|
||||
const toggleMasks = (): void => {
|
||||
const canvas = document.getElementById("image-occlusion-canvas") as HTMLCanvasElement;
|
||||
const canvas = document.getElementById(
|
||||
"image-occlusion-canvas",
|
||||
) as HTMLCanvasElement;
|
||||
const display = canvas.style.display;
|
||||
if (display === "none") {
|
||||
canvas.style.display = "unset";
|
||||
|
|
|
@ -5,76 +5,28 @@
|
|||
@typescript-eslint/no-explicit-any: "off",
|
||||
*/
|
||||
|
||||
import type { GetImageOcclusionNoteResponse_ImageOcclusion } from "@tslib/anki/image_occlusion_pb";
|
||||
|
||||
import type { Shape, ShapeOrShapes } from "./base";
|
||||
import { Ellipse } from "./ellipse";
|
||||
import { Point, Polygon } from "./polygon";
|
||||
import { Rectangle } from "./rectangle";
|
||||
import { Text } from "./text";
|
||||
|
||||
/** Given a cloze field with text like the following, extract the shapes from it:
|
||||
* {{c1::image-occlusion:rect:left=10.0:top=20:width=30:height=10:fill=#ffe34d}}
|
||||
*/
|
||||
export function extractShapesFromClozedField(clozeStr: string): ShapeOrShapes[] {
|
||||
const regex = /{{(.*?)}}/g;
|
||||
const clozeStrList: string[] = [];
|
||||
let match: string[] | null;
|
||||
|
||||
while ((match = regex.exec(clozeStr)) !== null) {
|
||||
clozeStrList.push(match[1]);
|
||||
}
|
||||
|
||||
const clozeList = {};
|
||||
for (const str of clozeStrList) {
|
||||
const [prefix, value] = str.split("::image-occlusion:");
|
||||
if (!clozeList[prefix]) {
|
||||
clozeList[prefix] = [];
|
||||
}
|
||||
clozeList[prefix].push(value);
|
||||
}
|
||||
|
||||
export function extractShapesFromClozedField(
|
||||
occlusions: GetImageOcclusionNoteResponse_ImageOcclusion[],
|
||||
): ShapeOrShapes[] {
|
||||
const output: ShapeOrShapes[] = [];
|
||||
|
||||
for (const index in clozeList) {
|
||||
if (clozeList[index].length > 1) {
|
||||
const group: Shape[] = [];
|
||||
clozeList[index].forEach((cloze) => {
|
||||
let shape: Shape | null = null;
|
||||
if ((shape = extractShapeFromClozeText(cloze))) {
|
||||
group.push(shape);
|
||||
}
|
||||
});
|
||||
output.push(group);
|
||||
} else {
|
||||
let shape: Shape | null = null;
|
||||
if ((shape = extractShapeFromClozeText(clozeList[index][0]))) {
|
||||
output.push(shape);
|
||||
}
|
||||
for (const occlusion of occlusions) {
|
||||
if (isValidType(occlusion.shape)) {
|
||||
const props = Object.fromEntries(occlusion.properties.map(prop => [prop.name, prop.value]));
|
||||
output.push(buildShape(occlusion.shape, props));
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function extractShapeFromClozeText(text: string): Shape | null {
|
||||
const [type, props] = extractTypeAndPropsFromClozeText(text);
|
||||
if (!type) {
|
||||
return null;
|
||||
}
|
||||
return buildShape(type, props);
|
||||
}
|
||||
|
||||
function extractTypeAndPropsFromClozeText(text: string): [ShapeType | null, Record<string, any>] {
|
||||
const parts = text.split(":");
|
||||
const type = parts[0];
|
||||
if (type !== "rect" && type !== "ellipse" && type !== "polygon") {
|
||||
return [null, {}];
|
||||
}
|
||||
const props = {};
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
const [key, value] = parts[i].split("=");
|
||||
props[key] = value;
|
||||
}
|
||||
return [type, props];
|
||||
}
|
||||
|
||||
/** Locate all cloze divs in the review screen for the given selector, and convert them into BaseShapes.
|
||||
*/
|
||||
export function extractShapesFromRenderedClozes(selector: string): Shape[] {
|
||||
|
@ -89,7 +41,12 @@ export function extractShapesFromRenderedClozes(selector: string): Shape[] {
|
|||
|
||||
function extractShapeFromRenderedCloze(cloze: HTMLDivElement): Shape | null {
|
||||
const type = cloze.dataset.shape!;
|
||||
if (type !== "rect" && type !== "ellipse" && type !== "polygon") {
|
||||
if (
|
||||
type !== "rect"
|
||||
&& type !== "ellipse"
|
||||
&& type !== "polygon"
|
||||
&& type !== "text"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const props = {
|
||||
|
@ -101,18 +58,32 @@ function extractShapeFromRenderedCloze(cloze: HTMLDivElement): Shape | null {
|
|||
rx: cloze.dataset.rx,
|
||||
ry: cloze.dataset.ry,
|
||||
points: cloze.dataset.points,
|
||||
text: cloze.dataset.text,
|
||||
scale: cloze.dataset.scale,
|
||||
};
|
||||
return buildShape(type, props);
|
||||
}
|
||||
|
||||
type ShapeType = "rect" | "ellipse" | "polygon";
|
||||
type ShapeType = "rect" | "ellipse" | "polygon" | "text";
|
||||
|
||||
function isValidType(type: string): type is ShapeType {
|
||||
return ["rect", "ellipse", "polygon", "text"].includes(type);
|
||||
}
|
||||
|
||||
function buildShape(type: ShapeType, props: Record<string, any>): Shape {
|
||||
props.left = parseFloat(Number.isNaN(Number(props.left)) ? ".0000" : props.left);
|
||||
props.top = parseFloat(Number.isNaN(Number(props.top)) ? ".0000" : props.top);
|
||||
props.left = parseFloat(
|
||||
Number.isNaN(Number(props.left)) ? ".0000" : props.left,
|
||||
);
|
||||
props.top = parseFloat(
|
||||
Number.isNaN(Number(props.top)) ? ".0000" : props.top,
|
||||
);
|
||||
switch (type) {
|
||||
case "rect": {
|
||||
return new Rectangle({ ...props, width: parseFloat(props.width), height: parseFloat(props.height) });
|
||||
return new Rectangle({
|
||||
...props,
|
||||
width: parseFloat(props.width),
|
||||
height: parseFloat(props.height),
|
||||
});
|
||||
}
|
||||
case "ellipse": {
|
||||
return new Ellipse({
|
||||
|
@ -132,5 +103,12 @@ function buildShape(type: ShapeType, props: Record<string, any>): Shape {
|
|||
}
|
||||
return new Polygon(props);
|
||||
}
|
||||
case "text": {
|
||||
return new Text({
|
||||
...props,
|
||||
scaleX: parseFloat(props.scale),
|
||||
scaleY: parseFloat(props.scale),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
65
ts/image-occlusion/shapes/text.ts
Normal file
65
ts/image-occlusion/shapes/text.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import { fabric } from "fabric";
|
||||
|
||||
import { SHAPE_MASK_COLOR, TEXT_FONT_FAMILY, TEXT_PADDING } from "../tools/lib";
|
||||
import type { ConstructorParams, Size } from "../types";
|
||||
import type { ShapeDataForCloze } from "./base";
|
||||
import { Shape } from "./base";
|
||||
import { floatToDisplay } from "./floats";
|
||||
import { xFromNormalized, xToNormalized, yFromNormalized, yToNormalized } from "./position";
|
||||
|
||||
export class Text extends Shape {
|
||||
text: string;
|
||||
scaleX: number;
|
||||
scaleY: number;
|
||||
|
||||
constructor({
|
||||
text = "",
|
||||
scaleX = 1,
|
||||
scaleY = 1,
|
||||
...rest
|
||||
}: ConstructorParams<Text> = {}) {
|
||||
super(rest);
|
||||
this.text = text;
|
||||
this.scaleX = scaleX;
|
||||
this.scaleY = scaleY;
|
||||
}
|
||||
|
||||
toDataForCloze(): TextDataForCloze {
|
||||
return {
|
||||
...super.toDataForCloze(),
|
||||
text: this.text,
|
||||
// scaleX and scaleY are guaranteed to be equal since we lock the aspect ratio
|
||||
scale: floatToDisplay(this.scaleX),
|
||||
};
|
||||
}
|
||||
|
||||
toFabric(size: Size): fabric.IText {
|
||||
this.makeAbsolute(size);
|
||||
return new fabric.IText(this.text, {
|
||||
...this,
|
||||
fontFamily: TEXT_FONT_FAMILY,
|
||||
backgroundColor: SHAPE_MASK_COLOR,
|
||||
padding: TEXT_PADDING,
|
||||
});
|
||||
}
|
||||
|
||||
makeNormal(size: Size): void {
|
||||
super.makeNormal(size);
|
||||
this.scaleX = xToNormalized(size, this.scaleX);
|
||||
this.scaleY = yToNormalized(size, this.scaleY);
|
||||
}
|
||||
|
||||
makeAbsolute(size: Size): void {
|
||||
super.makeAbsolute(size);
|
||||
this.scaleX = xFromNormalized(size, this.scaleX);
|
||||
this.scaleY = yFromNormalized(size, this.scaleY);
|
||||
}
|
||||
}
|
||||
|
||||
interface TextDataForCloze extends ShapeDataForCloze {
|
||||
text: string;
|
||||
scale: string;
|
||||
}
|
|
@ -11,6 +11,7 @@ import type { Shape, ShapeOrShapes } from "./base";
|
|||
import { Ellipse } from "./ellipse";
|
||||
import { Polygon } from "./polygon";
|
||||
import { Rectangle } from "./rectangle";
|
||||
import { Text } from "./text";
|
||||
|
||||
export function exportShapesToClozeDeletions(occludeInactive: boolean): {
|
||||
clozes: string;
|
||||
|
@ -19,8 +20,12 @@ export function exportShapesToClozeDeletions(occludeInactive: boolean): {
|
|||
const shapes = baseShapesFromFabric(occludeInactive);
|
||||
|
||||
let clozes = "";
|
||||
shapes.forEach((shapeOrShapes, index) => {
|
||||
let index = 0;
|
||||
shapes.forEach((shapeOrShapes) => {
|
||||
clozes += shapeOrShapesToCloze(shapeOrShapes, index);
|
||||
if (!(shapeOrShapes instanceof Text)) {
|
||||
index++;
|
||||
}
|
||||
});
|
||||
|
||||
return { clozes, noteCount: shapes.length };
|
||||
|
@ -67,6 +72,9 @@ function fabricObjectToBaseShapeOrShapes(
|
|||
case "polygon":
|
||||
shape = new Polygon(cloned);
|
||||
break;
|
||||
case "i-text":
|
||||
shape = new Text(cloned);
|
||||
break;
|
||||
case "group":
|
||||
return object._objects.map((child) => {
|
||||
return fabricObjectToBaseShapeOrShapes(
|
||||
|
@ -101,9 +109,7 @@ function shapeOrShapesToCloze(
|
|||
): string {
|
||||
let text = "";
|
||||
function addKeyValue(key: string, value: string) {
|
||||
if (key !== "points" && Number.isNaN(Number(value))) {
|
||||
value = ".0000";
|
||||
}
|
||||
value = value.replace(":", "\\:");
|
||||
text += `:${key}=${value}`;
|
||||
}
|
||||
|
||||
|
@ -118,6 +124,8 @@ function shapeOrShapesToCloze(
|
|||
type = "ellipse";
|
||||
} else if (shapeOrShapes instanceof Polygon) {
|
||||
type = "polygon";
|
||||
} else if (shapeOrShapes instanceof Text) {
|
||||
type = "text";
|
||||
} else {
|
||||
throw new Error("Unknown shape type");
|
||||
}
|
||||
|
@ -126,6 +134,13 @@ function shapeOrShapesToCloze(
|
|||
addKeyValue(key, value);
|
||||
}
|
||||
|
||||
text = `{{c${index + 1}::image-occlusion:${type}${text}}}<br>`;
|
||||
let ordinal: number;
|
||||
if (type === "text") {
|
||||
ordinal = 0;
|
||||
} else {
|
||||
ordinal = index + 1;
|
||||
}
|
||||
text = `{{c${ordinal}::image-occlusion:${type}${text}}}<br>`;
|
||||
|
||||
return text;
|
||||
}
|
||||
|
|
|
@ -1,15 +1,19 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import type { GetImageOcclusionNoteResponse_ImageOcclusion } from "@tslib/anki/image_occlusion_pb";
|
||||
import { fabric } from "fabric";
|
||||
import { extractShapesFromClozedField } from "image-occlusion/shapes/from-cloze";
|
||||
|
||||
import type { Size } from "../types";
|
||||
import { addBorder, disableRotation } from "./lib";
|
||||
import { addBorder, disableRotation, enableUniformScaling } from "./lib";
|
||||
|
||||
export const addShapesToCanvasFromCloze = (canvas: fabric.Canvas, clozeStr: string): void => {
|
||||
export const addShapesToCanvasFromCloze = (
|
||||
canvas: fabric.Canvas,
|
||||
occlusions: GetImageOcclusionNoteResponse_ImageOcclusion[],
|
||||
): void => {
|
||||
const size: Size = canvas;
|
||||
for (const shapeOrShapes of extractShapesFromClozedField(clozeStr)) {
|
||||
for (const shapeOrShapes of extractShapesFromClozedField(occlusions)) {
|
||||
if (Array.isArray(shapeOrShapes)) {
|
||||
const group = new fabric.Group();
|
||||
shapeOrShapes.map((shape) => {
|
||||
|
@ -23,6 +27,9 @@ export const addShapesToCanvasFromCloze = (canvas: fabric.Canvas, clozeStr: stri
|
|||
const shape = shapeOrShapes.toFabric(size);
|
||||
addBorder(shape);
|
||||
disableRotation(shape);
|
||||
if (shape.type === "i-text") {
|
||||
enableUniformScaling(canvas, shape);
|
||||
}
|
||||
canvas.add(shape);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,5 +4,6 @@
|
|||
import { drawEllipse } from "./tool-ellipse";
|
||||
import { drawPolygon } from "./tool-polygon";
|
||||
import { drawRectangle } from "./tool-rect";
|
||||
import { drawText } from "./tool-text";
|
||||
|
||||
export { drawEllipse, drawPolygon, drawRectangle };
|
||||
export { drawEllipse, drawPolygon, drawRectangle, drawText };
|
||||
|
|
|
@ -9,6 +9,8 @@ import { zoomResetValue } from "../store";
|
|||
|
||||
export const SHAPE_MASK_COLOR = "#ffeba2";
|
||||
export const BORDER_COLOR = "#212121";
|
||||
export const TEXT_FONT_FAMILY = "Arial";
|
||||
export const TEXT_PADDING = 5;
|
||||
|
||||
let _clipboard;
|
||||
|
||||
|
@ -18,7 +20,10 @@ export const stopDraw = (canvas: fabric.Canvas): void => {
|
|||
canvas.off("mouse:move");
|
||||
};
|
||||
|
||||
export const enableSelectable = (canvas: fabric.Canvas, select: boolean): void => {
|
||||
export const enableSelectable = (
|
||||
canvas: fabric.Canvas,
|
||||
select: boolean,
|
||||
): void => {
|
||||
canvas.selection = select;
|
||||
canvas.forEachObject(function(o) {
|
||||
o.selectable = select;
|
||||
|
@ -135,7 +140,10 @@ const pasteItem = (canvas: fabric.Canvas): void => {
|
|||
});
|
||||
};
|
||||
|
||||
export const makeMaskTransparent = (canvas: fabric.Canvas, opacity = false): void => {
|
||||
export const makeMaskTransparent = (
|
||||
canvas: fabric.Canvas,
|
||||
opacity = false,
|
||||
): void => {
|
||||
const objects = canvas.getObjects();
|
||||
objects.forEach((object) => {
|
||||
object.set({
|
||||
|
@ -152,16 +160,25 @@ export const moveShapeToCanvasBoundaries = (canvas: fabric.Canvas): void => {
|
|||
if (!activeObject) {
|
||||
return;
|
||||
}
|
||||
if (activeObject.type === "activeSelection" || activeObject.type === "rect") {
|
||||
if (
|
||||
activeObject.type === "activeSelection"
|
||||
|| activeObject.type === "rect"
|
||||
) {
|
||||
modifiedSelection(canvas, activeObject);
|
||||
}
|
||||
if (activeObject.type === "ellipse") {
|
||||
modifiedEllipse(canvas, activeObject);
|
||||
}
|
||||
if (activeObject.type === "i-text") {
|
||||
modifiedText(canvas, activeObject);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const modifiedSelection = (canvas: fabric.Canvas, object: fabric.Object): void => {
|
||||
const modifiedSelection = (
|
||||
canvas: fabric.Canvas,
|
||||
object: fabric.Object,
|
||||
): void => {
|
||||
const newWidth = object.width * object.scaleX;
|
||||
const newHeight = object.height * object.scaleY;
|
||||
|
||||
|
@ -174,7 +191,10 @@ const modifiedSelection = (canvas: fabric.Canvas, object: fabric.Object): void =
|
|||
setShapePosition(canvas, object);
|
||||
};
|
||||
|
||||
const modifiedEllipse = (canvas: fabric.Canvas, object: fabric.Object): void => {
|
||||
const modifiedEllipse = (
|
||||
canvas: fabric.Canvas,
|
||||
object: fabric.Object,
|
||||
): void => {
|
||||
const newRx = object.rx * object.scaleX;
|
||||
const newRy = object.ry * object.scaleY;
|
||||
const newWidth = object.width * object.scaleX;
|
||||
|
@ -191,18 +211,25 @@ const modifiedEllipse = (canvas: fabric.Canvas, object: fabric.Object): void =>
|
|||
setShapePosition(canvas, object);
|
||||
};
|
||||
|
||||
const setShapePosition = (canvas: fabric.Canvas, object: fabric.Object): void => {
|
||||
const modifiedText = (canvas: fabric.Canvas, object: fabric.Object): void => {
|
||||
setShapePosition(canvas, object);
|
||||
};
|
||||
|
||||
const setShapePosition = (
|
||||
canvas: fabric.Canvas,
|
||||
object: fabric.Object,
|
||||
): void => {
|
||||
if (object.left < 0) {
|
||||
object.set({ left: 0 });
|
||||
}
|
||||
if (object.top < 0) {
|
||||
object.set({ top: 0 });
|
||||
}
|
||||
if (object.left + object.width + object.strokeWidth > canvas.width) {
|
||||
object.set({ left: canvas.width - object.width });
|
||||
if (object.left + object.width * object.scaleX + object.strokeWidth > canvas.width) {
|
||||
object.set({ left: canvas.width - object.width * object.scaleX });
|
||||
}
|
||||
if (object.top + object.height + object.strokeWidth > canvas.height) {
|
||||
object.set({ top: canvas.height - object.height });
|
||||
if (object.top + object.height * object.scaleY + object.strokeWidth > canvas.height) {
|
||||
object.set({ top: canvas.height - object.height * object.scaleY });
|
||||
}
|
||||
object.setCoords();
|
||||
};
|
||||
|
@ -213,6 +240,20 @@ export function disableRotation(obj: fabric.Object): void {
|
|||
});
|
||||
}
|
||||
|
||||
export function enableUniformScaling(canvas: fabric.Canvas, obj: fabric.Object): void {
|
||||
obj.setControlsVisibility({ mb: false, ml: false, mt: false, mr: false });
|
||||
let timer: number;
|
||||
obj.on("scaling", (e) => {
|
||||
if (["bl", "br", "tr", "tl"].includes(e.transform.corner)) {
|
||||
clearTimeout(timer);
|
||||
canvas.uniformScaling = true;
|
||||
timer = setTimeout(() => {
|
||||
canvas.uniformScaling = false;
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function addBorder(obj: fabric.Object): void {
|
||||
obj.stroke = BORDER_COLOR;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
mdiEllipseOutline,
|
||||
mdiMagnifyScan,
|
||||
mdiRectangleOutline,
|
||||
mdiTextBox,
|
||||
mdiVectorPolygonVariant,
|
||||
} from "../icons";
|
||||
|
||||
|
@ -30,4 +31,8 @@ export const tools = [
|
|||
id: "draw-polygon",
|
||||
icon: mdiVectorPolygonVariant,
|
||||
},
|
||||
{
|
||||
id: "draw-text",
|
||||
icon: mdiTextBox,
|
||||
},
|
||||
];
|
||||
|
|
43
ts/image-occlusion/tools/tool-text.ts
Normal file
43
ts/image-occlusion/tools/tool-text.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import { fabric } from "fabric";
|
||||
|
||||
import {
|
||||
disableRotation,
|
||||
enableUniformScaling,
|
||||
SHAPE_MASK_COLOR,
|
||||
stopDraw,
|
||||
TEXT_FONT_FAMILY,
|
||||
TEXT_PADDING,
|
||||
} from "./lib";
|
||||
import { undoStack } from "./tool-undo-redo";
|
||||
|
||||
export const drawText = (canvas: fabric.Canvas): void => {
|
||||
canvas.selectionColor = "rgba(0, 0, 0, 0)";
|
||||
stopDraw(canvas);
|
||||
|
||||
canvas.on("mouse:down", function(o) {
|
||||
if (o.target) {
|
||||
return;
|
||||
}
|
||||
const pointer = canvas.getPointer(o.e);
|
||||
const text = new fabric.IText("text", {
|
||||
id: "text-" + new Date().getTime(),
|
||||
left: pointer.x,
|
||||
top: pointer.y,
|
||||
originX: "left",
|
||||
originY: "top",
|
||||
selectable: true,
|
||||
strokeWidth: 1,
|
||||
noScaleCache: false,
|
||||
fontFamily: TEXT_FONT_FAMILY,
|
||||
backgroundColor: SHAPE_MASK_COLOR,
|
||||
padding: TEXT_PADDING,
|
||||
});
|
||||
disableRotation(text);
|
||||
enableUniformScaling(canvas, text);
|
||||
canvas.add(text);
|
||||
undoStack.onObjectAdded(text.id);
|
||||
});
|
||||
};
|
|
@ -17,7 +17,7 @@ type UndoState = {
|
|||
redoable: boolean;
|
||||
};
|
||||
|
||||
const shapeType = ["rect", "ellipse"];
|
||||
const shapeType = ["rect", "ellipse", "i-text"];
|
||||
|
||||
const validShape = (shape: fabric.Object): boolean => {
|
||||
if (shape.width <= 5 || shape.height <= 5) return false;
|
||||
|
@ -90,10 +90,7 @@ class UndoStack {
|
|||
}
|
||||
|
||||
private maybePush(opts): void {
|
||||
if (
|
||||
!this.locked
|
||||
&& validShape(opts.target as fabric.Object)
|
||||
) {
|
||||
if (!this.locked && validShape(opts.target as fabric.Object)) {
|
||||
this.push();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue