Feature image occlusion (#2367)

* add note types with occlusions and image fields

* generate image occlusion cloze div data
- generate div element with data-* atrributes for canvas shape generate for reviewer

* getting image data & deck id and adding notes
the implementation added into backend
- added service index in backend.proto for image occlusion request
- created image_occlusion.proto with required message and service
- implementation in backend for getting image and adding notes, also during editing return imagecloze note and update notes
- add notes to selected deck, if no notetype then add image occlusion notetypes
- reuse notetype from stock notetypes when not exist

* script for generating shapes using canvas api in reviewer
- the flash issues fixed by loading image and using image size to draw canvas, also when image get resized, calculate scale using natural width and canvas width to draw shape at right position
- limit size of canvas for safari

* init image occlusion page in ts and build page
with
- fabricjs for editing shapes
- panzoom for drag and zoom
- pickr for color picker
- build page using web.rs

* implement top toolbar for canvas shapes
- undo & redo tools
- zoom in, zoom out and zoom fit
- group & ungroup
- copy & paste
- set transparency of shapes
- align tools

* implement side toolbar for drawing shapes
add top toolbar and the side toolbar contains following tools
- cursor for selecting shapes
- zoom for drag and zoom shapes in mask editor
- rectangle for creating it
- ellipse for creating it
- polygon for creating it using points
- shape fill color
- question mask color (currently only single color can be added for all shapes)

* add maskeditor page for editing mask
- add side toolbar and sidebar include toptoolbar
- load maskeditor in two mode
     - for adding note using path to image
     - for editing note using note id

* implement note editor page for adding notes
- the note editor page have simple button (B/I/U) and option to toggle html view
- option to select deck for adding notes into that deck
- option to generate to hide all, guess one & hide one, guess one notes

* add image occlusion page
add side toolbar, top toolbar, mask editor and note editor
- option to switch between mask editor and note editor

* implement generates notes and save notes
implemention to show toast components for messages

* removed pickr & implemented color picker component
- remove pickr
- implemented using html5 canvas
- range input for changing color
- another range input for opacity changes
- hex and rgba value support

* rename methods name & rust unwrap safety
- change plural names to singular
- create respone message in proto and return response with imagecloze note or error if not found with note id
- remove image_occlusion from post handler list
- rename service name in mediasrv.py
- rename methods name for image occlusion in backend and image_occlusion
- update frontend also for update functions' names
- handle error in frontend mask-editor.ts, when error getting notes then toast message shown to frontend

* extract to function & add comments & remove global
- extract function in mask-editor.ts to reduce duplicate
- remove unused global from css
- add comments to store.ts explaining usage
- changes id to noteId in lib.ts
- add comments for limitSize, becuase of duplicate implementation

* remove image_occlusion notetype
- remove from stock notetype, stdmodels
- add implementation for notetype to image occlusion
- add i18n for errors

* update smooth scroll, always show cursor tools
- change questionmask to qmask
- make selectable for shape true in all tools to simplify edits and draw shapes
- update image occlusion in reviewer ts to load image properly

* add and get notetype else return errors

* fix: not showing occlusion

* Use a oneof for ImageClozeNoteResponse

Makes it clearer that only one of them can be returned

* Don't crash if image filename not provided

The second unwrap should be ok, as the input is utf8

* Refactor get_image_cloze_note

- fixes crash when note doesn't exist - Ok(None) case was not covered
- decouples business logic from native error->proto error conversion
- no need for original copy
- field[x] is more idiomatic than field.get(x).unwrap()
- don't need mutable access to fields

* Fix crash if image file unreadable

+ Use our read_file helper for better error context

* Add metadata() helper

* Fix crash if file metadata can't be read

* remove color picker, qmask and shape color
- remove strings from ftl
- remove color picker component
- remove from cloze generation
- remove icons for two buttons
- use constant color for shapes

* update color in reviewer and ftl strings

* fix shape position in canvas & add border to shape
- rename mask to inactive shape and active shape color
- border witdth and border color
- change decimal point deserializing string and toFixed(2)
- add thin border in mask editor, may be image background was transparent

* fix shape position in canvas after modified
- do not draw fixed ratio shapes by turn of uniformScaling
- fix rectangle width,height
- fix ellipse rx,ry,width,height
- fix polygon postion and points
- draw outside of canvas also

* fix border width and color in reviewer canvas
- rename variable

* refactor cloze div generate and remove angle

* fix origin when drawn outside of canvas from right

* fix shape at boundry & not include rx,ry rectangle
- move shapes at boundry when pointer is outside of canvas
- include rx, ry for ellipse only
- include points for polygon only

* fix lint errors & update image size in editor canvas based on height and width

* remove unsupported layerX & layerX for touchscreen
- fix shapes at edges

* implemented undo redo with canvas state

- implemented undo redo using fabric canvas events
- polygon is special case and implemented only added and modified event
- rectangle and ellipse have object:added, object:modified and object:removed case
- change id to undo and redo

* remove background image from canvas and used css to put image tag below canvas editor

- set image width and height after adding image

* fix for polygon points, add br in cloze strings, & toogle masks button

- fix shapes at edges
- toggle masks button to show/hide masks
- hide clozes string, it contains <br>
- set height for div container (used 'relative' in css)

* refactor top toolbar, add space and border radius
- rename cursor tools
- add left and right border

* fix undo after undo happen, use transparent color in draw mode
This commit is contained in:
Mani 2023-03-29 10:33:19 +08:00 committed by GitHub
parent c691c9bcf6
commit 2bf134dc72
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 4750 additions and 7 deletions

View file

@ -334,6 +334,18 @@ fn build_and_check_pages(build: &mut Build) -> Result<()> {
":sass" ":sass"
], ],
)?; )?;
build_page(
"image-occlusion",
true,
inputs![
//
":ts:lib",
":ts:components",
":ts:sveltelib",
":ts:tag-editor",
":sass"
],
)?;
Ok(()) Ok(())
} }

View file

@ -108,6 +108,11 @@ importing-tag-updated-notes = Tag updated notes
importing-file = File importing-file = File
importing-match-scope = Match scope importing-match-scope = Match scope
importing-notetype-and-deck = Notetype and deck importing-notetype-and-deck = Notetype and deck
importing-cards-added =
{ $count ->
[one] { $count } card added.
*[other] { $count } cards added.
}
## NO NEED TO TRANSLATE. This text is no longer used by Anki, and will be removed in the future. ## NO NEED TO TRANSLATE. This text is no longer used by Anki, and will be removed in the future.

View file

@ -35,3 +35,18 @@ notetypes-note-types = Note Types
notetypes-options = Options notetypes-options = Options
notetypes-please-add-another-note-type-first = Please add another note type first. notetypes-please-add-another-note-type-first = Please add another note type first.
notetypes-type = Type notetypes-type = Type
## Image Occlusion
notetypes-image = Image
notetypes-occlusion = Occlusion
notetypes-occlusion-mask = Mask
notetypes-occlusion-note = Note
notetypes-comments-field = Comments
notetypes-toggle-masks = Toggle Masks
notetypes-image-occlusion-name = Image Occlusion
notetypes-hide-all-guess-one = Hide All, Guess One
notetypes-hide-one-guess-one = Hide One, Guess One
notetypes-error-generating-cloze = An error occurred when generating an image occlusion note
notetypes-error-getting-imagecloze = An error occurred while fetching an image occlusion note
notetypes-error-loading-image-occlusion = Error loading image occlusion. Is your Anki version up to date?

View file

@ -74,6 +74,7 @@
"codemirror": "^5.63.1", "codemirror": "^5.63.1",
"css-browser-selector": "^0.6.5", "css-browser-selector": "^0.6.5",
"d3": "^7.0.0", "d3": "^7.0.0",
"fabric": "^5.3.0",
"fuse.js": "^6.6.2", "fuse.js": "^6.6.2",
"gemoji": "^7.1.0", "gemoji": "^7.1.0",
"intl-pluralrules": "^1.2.2", "intl-pluralrules": "^1.2.2",
@ -82,6 +83,7 @@
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"marked": "^4.0.0", "marked": "^4.0.0",
"mathjax": "^3.1.2", "mathjax": "^3.1.2",
"panzoom": "^9.4.3",
"protobufjs": "^7" "protobufjs": "^7"
}, },
"resolutions": { "resolutions": {

View file

@ -31,6 +31,7 @@ enum ServiceIndex {
SERVICE_INDEX_LINKS = 15; SERVICE_INDEX_LINKS = 15;
SERVICE_INDEX_IMPORT_EXPORT = 16; SERVICE_INDEX_IMPORT_EXPORT = 16;
SERVICE_INDEX_ANKIDROID = 17; SERVICE_INDEX_ANKIDROID = 17;
SERVICE_INDEX_IMAGE_OCCLUSION = 18;
} }
message BackendInit { message BackendInit {

View file

@ -0,0 +1,67 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
syntax = "proto3";
option java_multiple_files = true;
package anki.image_occlusion;
import "anki/cards.proto";
import "anki/collection.proto";
import "anki/notes.proto";
import "anki/generic.proto";
service ImageOcclusionService {
rpc GetImageForOcclusion(GetImageForOcclusionRequest) returns (ImageData);
rpc AddImageOcclusionNote(AddImageOcclusionNoteRequest)
returns (collection.OpChanges);
rpc GetImageClozeNote(GetImageOcclusionNoteRequest)
returns (ImageClozeNoteResponse);
rpc UpdateImageOcclusionNote(UpdateImageOcclusionNoteRequest)
returns (collection.OpChanges);
}
message GetImageForOcclusionRequest {
string path = 1;
}
message ImageData {
bytes data = 1;
string name = 2;
}
message AddImageOcclusionNoteRequest {
string image_path = 1;
string occlusions = 2;
string header = 3;
string back_extra = 4;
repeated string tags = 5;
}
message ImageClozeNote {
bytes image_data = 1;
string occlusions = 2;
string header = 3;
string back_extra = 4;
repeated string tags = 5;
}
message GetImageOcclusionNoteRequest {
int64 note_id = 1;
}
message UpdateImageOcclusionNoteRequest {
int64 note_id = 1;
string occlusions = 2;
string header = 3;
string back_extra = 4;
repeated string tags = 5;
}
message ImageClozeNoteResponse {
oneof value {
ImageClozeNote note = 1;
string error = 2;
}
}

View file

@ -124,6 +124,7 @@ message StockNotetype {
BASIC_OPTIONAL_REVERSED = 2; BASIC_OPTIONAL_REVERSED = 2;
BASIC_TYPING = 3; BASIC_TYPING = 3;
CLOZE = 4; CLOZE = 4;
IMAGE_OCCLUSION = 5;
} }
Kind kind = 1; Kind kind = 1;

View file

@ -10,6 +10,7 @@ from anki import (
collection_pb2, collection_pb2,
config_pb2, config_pb2,
generic_pb2, generic_pb2,
image_occlusion_pb2,
import_export_pb2, import_export_pb2,
links_pb2, links_pb2,
search_pb2, search_pb2,
@ -40,6 +41,9 @@ CsvMetadata = import_export_pb2.CsvMetadata
DupeResolution = CsvMetadata.DupeResolution DupeResolution = CsvMetadata.DupeResolution
Delimiter = import_export_pb2.CsvMetadata.Delimiter Delimiter = import_export_pb2.CsvMetadata.Delimiter
TtsVoice = card_rendering_pb2.AllTtsVoicesResponse.TtsVoice TtsVoice = card_rendering_pb2.AllTtsVoicesResponse.TtsVoice
ImageData = image_occlusion_pb2.ImageData
AddImageOcclusionNoteRequest = image_occlusion_pb2.AddImageOcclusionNoteRequest
ImageClozeNoteResponse = image_occlusion_pb2.ImageClozeNoteResponse
import copy import copy
import os import os
@ -456,6 +460,46 @@ class Collection(DeprecatedNamesMixin):
def import_json_string(self, json: str) -> ImportLogWithChanges: def import_json_string(self, json: str) -> ImportLogWithChanges:
return self._backend.import_json_string(json) return self._backend.import_json_string(json)
# Image Occlusion
##########################################################################
def get_image_for_occlusion(self, path: str | None) -> ImageData:
return self._backend.get_image_for_occlusion(path=path)
def add_image_occlusion_note(
self,
image_path: str | None,
occlusions: str | None,
header: str | None,
back_extra: str | None,
tags: list[str] | None,
) -> OpChanges:
return self._backend.add_image_occlusion_note(
image_path=image_path,
occlusions=occlusions,
header=header,
back_extra=back_extra,
tags=tags,
)
def get_image_cloze_note(self, note_id: int | None) -> ImageClozeNoteResponse:
return self._backend.get_image_cloze_note(note_id=note_id)
def update_image_occlusion_note(
self,
note_id: int | None,
occlusions: str | None,
header: str | None,
back_extra: str | None,
tags: list[str] | None,
) -> OpChanges:
return self._backend.update_image_occlusion_note(
note_id=note_id,
occlusions=occlusions,
header=header,
back_extra=back_extra,
tags=tags,
)
# Object helpers # Object helpers
########################################################################## ##########################################################################

View file

@ -19,6 +19,7 @@ import anki.config_pb2
import anki.deckconfig_pb2 import anki.deckconfig_pb2
import anki.decks_pb2 import anki.decks_pb2
import anki.i18n_pb2 import anki.i18n_pb2
import anki.image_occlusion_pb2
import anki.import_export_pb2 import anki.import_export_pb2
import anki.links_pb2 import anki.links_pb2
import anki.media_pb2 import anki.media_pb2
@ -187,6 +188,7 @@ service_modules = dict(
MEDIA=anki.media_pb2, MEDIA=anki.media_pb2,
LINKS=anki.links_pb2, LINKS=anki.links_pb2,
IMPORT_EXPORT=anki.import_export_pb2, IMPORT_EXPORT=anki.import_export_pb2,
IMAGE_OCCLUSION=anki.image_occlusion_pb2,
) )
for service in anki.backend_pb2.ServiceIndex.DESCRIPTOR.values: for service in anki.backend_pb2.ServiceIndex.DESCRIPTOR.values:
@ -238,6 +240,7 @@ import anki.card_rendering_pb2
import anki.tags_pb2 import anki.tags_pb2
import anki.media_pb2 import anki.media_pb2
import anki.import_export_pb2 import anki.import_export_pb2
import anki.image_occlusion_pb2
class RustBackendGenerated: class RustBackendGenerated:
def _run_command(self, service: int, method: int, input: Any) -> bytes: def _run_command(self, service: int, method: int, input: Any) -> bytes:

View file

@ -479,6 +479,11 @@ exposed_backend_list = [
"set_graph_preferences", "set_graph_preferences",
# TagsService # TagsService
"complete_tag", "complete_tag",
# ImageOcclusionService
"get_image_for_occlusion",
"add_image_occlusion_note",
"get_image_cloze_note",
"update_image_occlusion_note",
] ]

View file

@ -0,0 +1,55 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use super::Backend;
pub(super) use crate::pb::image_occlusion::imageocclusion_service::Service as ImageOcclusionService;
use crate::pb::{self as pb,};
use crate::prelude::*;
impl ImageOcclusionService for Backend {
fn get_image_for_occlusion(
&self,
input: pb::image_occlusion::GetImageForOcclusionRequest,
) -> Result<pb::image_occlusion::ImageData> {
self.with_col(|col| col.get_image_for_occlusion(&input.path))
}
fn add_image_occlusion_note(
&self,
input: pb::image_occlusion::AddImageOcclusionNoteRequest,
) -> Result<pb::collection::OpChanges> {
self.with_col(|col| {
col.add_image_occlusion_note(
&input.image_path,
&input.occlusions,
&input.header,
&input.back_extra,
input.tags,
)
})
.map(Into::into)
}
fn get_image_cloze_note(
&self,
input: pb::image_occlusion::GetImageOcclusionNoteRequest,
) -> Result<pb::image_occlusion::ImageClozeNoteResponse> {
self.with_col(|col| col.get_image_cloze_note(input.note_id.into()))
}
fn update_image_occlusion_note(
&self,
input: pb::image_occlusion::UpdateImageOcclusionNoteRequest,
) -> Result<pb::collection::OpChanges> {
self.with_col(|col| {
col.update_image_occlusion_note(
input.note_id.into(),
&input.occlusions,
&input.header,
&input.back_extra,
input.tags,
)
})
.map(Into::into)
}
}

View file

@ -16,6 +16,7 @@ mod decks;
mod error; mod error;
mod generic; mod generic;
mod i18n; mod i18n;
mod image_occlusion;
mod import_export; mod import_export;
mod links; mod links;
mod media; mod media;
@ -48,6 +49,7 @@ use self::config::ConfigService;
use self::deckconfig::DeckConfigService; use self::deckconfig::DeckConfigService;
use self::decks::DecksService; use self::decks::DecksService;
use self::i18n::I18nService; use self::i18n::I18nService;
use self::image_occlusion::ImageOcclusionService;
use self::import_export::ImportExportService; use self::import_export::ImportExportService;
use self::links::LinksService; use self::links::LinksService;
use self::media::MediaService; use self::media::MediaService;
@ -142,6 +144,9 @@ impl Backend {
ServiceIndex::Collection => CollectionService::run_method(self, method, input), ServiceIndex::Collection => CollectionService::run_method(self, method, input),
ServiceIndex::Cards => CardsService::run_method(self, method, input), ServiceIndex::Cards => CardsService::run_method(self, method, input),
ServiceIndex::ImportExport => ImportExportService::run_method(self, method, input), ServiceIndex::ImportExport => ImportExportService::run_method(self, method, input),
ServiceIndex::ImageOcclusion => {
ImageOcclusionService::run_method(self, method, input)
}
}) })
.map_err(|err| { .map_err(|err| {
let backend_err = err.into_protobuf(&self.tr); let backend_err = err.into_protobuf(&self.tr);

View file

@ -15,6 +15,7 @@ use nom::IResult;
use regex::Captures; use regex::Captures;
use regex::Regex; use regex::Regex;
use crate::image_occlusion::imageocclusion::get_image_cloze_data;
use crate::latex::contains_latex; use crate::latex::contains_latex;
use crate::template::RenderContext; use crate::template::RenderContext;
use crate::text::strip_html_preserving_entities; use crate::text::strip_html_preserving_entities;
@ -138,6 +139,13 @@ impl ExtractedCloze<'_> {
buf.into() buf.into()
} }
/// If cloze starts with image-occlusion:, return the text following that.
fn image_occlusion(&self) -> Option<&str> {
let Some(first_node) = self.nodes.get(0) else { return None };
let TextOrCloze::Text(text) = first_node else { return None };
text.strip_prefix("image-occlusion:")
}
} }
fn parse_text_with_clozes(text: &str) -> Vec<TextOrCloze<'_>> { fn parse_text_with_clozes(text: &str) -> Vec<TextOrCloze<'_>> {
@ -212,6 +220,14 @@ fn reveal_cloze(
) { ) {
let active = cloze.ordinal == cloze_ord; let active = cloze.ordinal == cloze_ord;
*active_cloze_found_in_text |= active; *active_cloze_found_in_text |= active;
if let Some(image_occlusion_text) = cloze.image_occlusion() {
buf.push_str(&render_image_occlusion(
image_occlusion_text,
question,
active,
));
return;
}
match (question, active) { match (question, active) {
(true, true) => { (true, true) => {
// question side with active cloze; all inner content is elided // question side with active cloze; all inner content is elided
@ -275,6 +291,22 @@ fn reveal_cloze(
} }
} }
fn render_image_occlusion(text: &str, question_side: bool, active: bool) -> String {
if question_side && active {
format!(
r#"<div class="cloze" {}></div>"#,
&get_image_cloze_data(text)
)
} else if !active {
format!(
r#"<div class="cloze-inactive" {}></div>"#,
&get_image_cloze_data(text)
)
} else {
"".into()
}
}
pub fn reveal_cloze_text(text: &str, cloze_ord: u16, question: bool) -> Cow<str> { pub fn reveal_cloze_text(text: &str, cloze_ord: u16, question: bool) -> Cow<str> {
let mut buf = String::new(); let mut buf = String::new();
let mut active_cloze_found_in_text = false; let mut active_cloze_found_in_text = false;
@ -530,4 +562,18 @@ mod test {
fn non_latin() { fn non_latin() {
assert!(cloze_numbers_in_string("öaöaöööaö").is_empty()); assert!(cloze_numbers_in_string("öaöaöööaö").is_empty());
} }
#[test]
fn image_cloze() {
assert_eq!(
reveal_cloze_text(
"{{c1::image-occlusion:rect:left=10.0:top=20:width=30:height=10}}",
1,
true
),
format!(
r#"<div class="cloze" data-shape="rect" data-left="10.0" data-top="20" data-width="30" data-height="10" ></div>"#,
)
);
}
} }

View file

@ -33,6 +33,7 @@ pub enum FileOp {
CopyFrom(PathBuf), CopyFrom(PathBuf),
Persist, Persist,
Sync, Sync,
Metadata,
/// For legacy errors without any context. /// For legacy errors without any context.
Unknown, Unknown,
} }
@ -57,6 +58,7 @@ impl FileIoError {
FileOp::CopyFrom(p) => format!("copy from '{}' to", p.to_string_lossy()), FileOp::CopyFrom(p) => format!("copy from '{}' to", p.to_string_lossy()),
FileOp::Persist => "persist".into(), FileOp::Persist => "persist".into(),
FileOp::Sync => "sync".into(), FileOp::Sync => "sync".into(),
FileOp::Metadata => "get metadata".into(),
}, },
self.path.to_string_lossy(), self.path.to_string_lossy(),
self.source self.source

View file

@ -0,0 +1,41 @@
.image-occlusion-canvas {
--inactive-shape-color: #ffeba2;
--active-shape-color: #ff8e8e;
--inactive-shape-border: 1px #212121;
--active-shape-border: 1px #212121;
}
.card {
font-family: arial;
font-size: 20px;
text-align: center;
color: black;
background-color: white;
}
.cloze {
font-weight: bold;
color: blue;
}
.nightMode .cloze {
color: lightblue;
}
#container {
position: relative;
}
img {
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%, 0);
}
#canvas {
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%, 0);
}

View file

@ -0,0 +1,246 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use regex::Regex;
use crate::io::metadata;
use crate::io::read_file;
use crate::media::MediaManager;
use crate::notetype::CardGenContext;
use crate::notetype::Notetype;
use crate::notetype::NotetypeConfig;
use crate::pb::image_occlusion::image_cloze_note_response::Value;
use crate::pb::image_occlusion::ImageClozeNote;
use crate::pb::image_occlusion::ImageClozeNoteResponse;
pub use crate::pb::image_occlusion::ImageData;
use crate::prelude::*;
impl Collection {
pub fn get_image_for_occlusion(&mut self, path: &str) -> Result<ImageData> {
let mut metadata = ImageData {
..Default::default()
};
metadata.data = read_file(path)?;
Ok(metadata)
}
#[allow(clippy::too_many_arguments)]
pub fn add_image_occlusion_note(
&mut self,
image_path: &str,
occlusions: &str,
header: &str,
back_extra: &str,
tags: Vec<String>,
) -> Result<OpOutput<()>> {
// image file
let image_bytes = read_file(image_path)?;
let image_filename = Path::new(&image_path)
.file_name()
.or_not_found("expected filename")?
.to_str()
.unwrap()
.to_string();
let mgr = MediaManager::new(&self.media_folder, &self.media_db)?;
let actual_image_name_after_adding = mgr.add_file(&image_filename, &image_bytes)?;
let image_tag = format!(
"<img id='img' src='{}'></img>",
&actual_image_name_after_adding
);
let current_deck = self.get_current_deck()?;
self.transact(Op::ImageOcclusion, |col| {
let nt = col.get_or_create_io_notetype()?;
let mut note = nt.new_note();
note.set_field(0, occlusions)?;
note.set_field(1, &image_tag)?;
note.set_field(2, header)?;
note.set_field(3, back_extra)?;
note.tags = tags;
let last_deck = col.get_last_deck_added_to_for_notetype(note.notetype_id);
let ctx = CardGenContext::new(nt.as_ref(), last_deck, col.usn()?);
let norm = col.get_config_bool(BoolKey::NormalizeNoteText);
col.add_note_inner(&ctx, &mut note, current_deck.id, norm)?;
Ok(())
})
}
fn get_or_create_io_notetype(&mut self) -> Result<Arc<Notetype>> {
let tr = &self.tr;
let name = format!("{}", tr.notetypes_image_occlusion_name());
let nt = match self.get_notetype_by_name(&name)? {
Some(nt) => nt,
None => {
self.add_io_notetype()?;
if let Some(nt) = self.get_notetype_by_name(&name)? {
nt
} else {
return Err(AnkiError::TemplateError {
info: "IO notetype not found".to_string(),
});
}
}
};
if nt.fields.len() < 4 {
Err(AnkiError::TemplateError {
info: "IO notetype must have 4+ fields".to_string(),
})
} else {
Ok(nt)
}
}
fn add_io_notetype(&mut self) -> Result<()> {
let usn = self.usn()?;
let mut nt = self.image_occlusion_notetype();
self.add_notetype_inner(&mut nt, usn, false)?;
Ok(())
}
pub fn get_image_cloze_note(&mut self, note_id: NoteId) -> Result<ImageClozeNoteResponse> {
let value = match self.get_image_cloze_note_inner(note_id) {
Ok(note) => Value::Note(note),
Err(err) => Value::Error(format!("{:?}", err)),
};
Ok(ImageClozeNoteResponse { value: Some(value) })
}
pub fn get_image_cloze_note_inner(&mut self, note_id: NoteId) -> Result<ImageClozeNote> {
let note = self.storage.get_note(note_id)?.or_not_found(note_id)?;
let mut cloze_note = ImageClozeNote::default();
let fields = note.fields();
if fields.len() < 4 {
invalid_input!("Note does not have 4 fields");
}
cloze_note.occlusions = fields[0].clone();
cloze_note.header = fields[2].clone();
cloze_note.back_extra = fields[3].clone();
cloze_note.image_data = "".into();
cloze_note.tags = note.tags.clone();
let image_file_name = fields[1].clone();
let src = self
.extract_img_src(&image_file_name)
.unwrap_or_else(|| "".to_owned());
let final_path = self.media_folder.join(src);
if self.is_image_file(&final_path)? {
cloze_note.image_data = read_file(&final_path)?;
}
Ok(cloze_note)
}
pub fn update_image_occlusion_note(
&mut self,
note_id: NoteId,
occlusions: &str,
header: &str,
back_extra: &str,
tags: Vec<String>,
) -> Result<OpOutput<()>> {
let mut note = self.storage.get_note(note_id)?.or_not_found(note_id)?;
self.transact(Op::ImageOcclusion, |col| {
note.set_field(0, occlusions)?;
note.set_field(2, header)?;
note.set_field(3, back_extra)?;
note.tags = tags;
col.update_note_inner(&mut note)?;
Ok(())
})
}
fn extract_img_src(&mut self, html: &str) -> Option<String> {
let re = Regex::new(r#"<img\s+[^>]*src\s*=\s*"([^"]+)"#).unwrap();
re.captures(html).map(|cap| cap[1].to_owned())
}
fn is_image_file(&mut self, path: &PathBuf) -> Result<bool> {
let file_path = Path::new(&path);
let supported_extensions = vec![
"jpg", "jpeg", "png", "tif", "tiff", "gif", "svg", "webp", "ico",
];
if file_path.exists() {
let meta = metadata(file_path)?;
if meta.is_file() {
if let Some(ext_osstr) = file_path.extension() {
if let Some(ext_str) = ext_osstr.to_str() {
if supported_extensions.contains(&ext_str) {
return Ok(true);
}
}
}
}
}
Ok(false)
}
fn image_occlusion_notetype(&mut self) -> Notetype {
let tr = &self.tr;
const IMAGE_CLOZE_CSS: &str = include_str!("image_occlusion_styling.css");
let mut nt = Notetype {
name: tr.notetypes_image_occlusion_name().into(),
config: NotetypeConfig::new_cloze(),
..Default::default()
};
nt.config.css = IMAGE_CLOZE_CSS.to_string();
let occlusion = tr.notetypes_occlusion();
nt.add_field(occlusion.as_ref());
let image = tr.notetypes_image();
nt.add_field(image.as_ref());
let header = tr.notetypes_header();
nt.add_field(header.as_ref());
let back_extra = tr.notetypes_back_extra_field();
nt.add_field(back_extra.as_ref());
let comments = tr.notetypes_comments_field();
nt.add_field(comments.as_ref());
let qfmt = format!(
"<div style=\"display: none\">{{{{cloze:{}}}}}</div>
<div id=container>
{{{{{}}}}}
<canvas id=\"canvas\" class=\"image-occlusion-canvas\"></canvas>
</div>
<div id=\"err\"></div>
<script>
try {{
anki.setupImageCloze();
}} catch (exc) {{
document.getElementById(\"err\").innerHTML = `{}<br><br>${{exc}}`;
}}
</script>
",
occlusion,
image,
tr.notetypes_error_loading_image_occlusion(),
);
let afmt = format!(
"{{{{{}}}}}
{}
<button id=\"toggle\">{}</button>
<br>
{{{{{}}}}}
<br>
{{{{{}}}}}",
header,
qfmt,
tr.notetypes_toggle_masks(),
back_extra,
comments,
);
nt.add_template(nt.name.clone(), qfmt, afmt);
nt
}
}

View file

@ -0,0 +1,107 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::fmt::Write;
// split following
// text = "rect:399.01,99.52,167.09,33.78:fill=#0a2cee:stroke=1"
// with
// result = "data-shape="rect" data-left="399.01" data-top="99.52" data-width="167.09" data-height="33.78" data-fill="\#0a2cee" data-stroke="1""
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")
{
result.push_str(&format!("data-shape=\"{}\" ", parts[0]));
}
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]));
}
}
"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 comma
point_str.pop();
if !point_str.is_empty() {
result.push_str(&format!("data-points=\"[{}]\" ", point_str));
}
}
}
"hideinactive" => {
if !values[1].is_empty() {
result.push_str(&format!("data-hideinactive=\"{}\" ", values[1]));
}
}
_ => {}
}
}
}
}
result
}
fn is_empty_or_zero(text: &str) -> bool {
text.is_empty() || text == "0"
}
//----------------------------------------
// Tests
//----------------------------------------
#[test]
fn test_get_image_cloze_data() {
assert_eq!(
get_image_cloze_data("rect:left=10:top=20:width=30:height=10"),
format!(
r#"data-shape="rect" data-left="10" data-top="20" data-width="30" data-height="10" "#,
)
);
assert_eq!(
get_image_cloze_data("ellipse:left=15:top=20:width=10:height=20:rx=10:ry=5"),
r#"data-shape="ellipse" data-left="15" data-top="20" data-width="10" data-height="20" data-rx="10" data-ry="5" "#,
);
assert_eq!(
get_image_cloze_data("polygon:points=0,0 10,10 20,0"),
r#"data-shape="polygon" data-points="[[0,0],[10,10],[20,0]]" "#,
);
}

View file

@ -0,0 +1,5 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
pub mod imagedata;
pub mod imageocclusion;

View file

@ -93,6 +93,14 @@ fn read_locked_db_file_inner(path: impl AsRef<Path>) -> std::io::Result<Vec<u8>>
Ok(buf) Ok(buf)
} }
/// See [std::fs::metadata].
pub(crate) fn metadata(path: impl AsRef<Path>) -> Result<std::fs::Metadata> {
std::fs::metadata(&path).context(FileIoSnafu {
path: path.as_ref(),
op: FileOp::Metadata,
})
}
pub(crate) fn new_tempfile() -> Result<NamedTempFile> { pub(crate) fn new_tempfile() -> Result<NamedTempFile> {
NamedTempFile::new().context(FileIoSnafu { NamedTempFile::new().context(FileIoSnafu {
path: std::env::temp_dir(), path: std::env::temp_dir(),

View file

@ -17,6 +17,7 @@ pub mod decks;
pub mod error; pub mod error;
pub mod findreplace; pub mod findreplace;
pub mod i18n; pub mod i18n;
pub mod image_occlusion;
pub mod import_export; pub mod import_export;
mod io; mod io;
pub mod latex; pub mod latex;

View file

@ -9,7 +9,7 @@ mod notetypechange;
mod render; mod render;
mod schema11; mod schema11;
mod schemachange; mod schemachange;
mod stock; pub(crate) mod stock;
mod templates; mod templates;
pub(crate) mod undo; pub(crate) mod undo;

View file

@ -17,6 +17,7 @@ pub enum Op {
CreateCustomStudy, CreateCustomStudy,
EmptyFilteredDeck, EmptyFilteredDeck,
FindAndReplace, FindAndReplace,
ImageOcclusion,
Import, Import,
RebuildFilteredDeck, RebuildFilteredDeck,
RemoveDeck, RemoveDeck,
@ -90,6 +91,7 @@ impl Op {
Op::Custom(name) => name.into(), Op::Custom(name) => name.into(),
Op::ChangeNotetype => tr.browsing_change_notetype(), Op::ChangeNotetype => tr.browsing_change_notetype(),
Op::SkipUndo => return "".to_string(), Op::SkipUndo => return "".to_string(),
Op::ImageOcclusion => tr.notetypes_image_occlusion_name(),
} }
.into() .into()
} }

View file

@ -19,6 +19,7 @@ protobuf!(deckconfig, "deckconfig");
protobuf!(decks, "decks"); protobuf!(decks, "decks");
protobuf!(generic, "generic"); protobuf!(generic, "generic");
protobuf!(i18n, "i18n"); protobuf!(i18n, "i18n");
protobuf!(image_occlusion, "image_occlusion");
protobuf!(import_export, "import_export"); protobuf!(import_export, "import_export");
protobuf!(links, "links"); protobuf!(links, "links");
protobuf!(media, "media"); protobuf!(media, "media");

View file

@ -0,0 +1,106 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import * as tr from "@tslib/ftl";
import Container from "../components/Container.svelte";
import { saveImageNotes } from "./generate";
import MasksEditor from "./MaskEditor.svelte";
import Notes from "./Notes.svelte";
import StickyFooter from "./StickyFooter.svelte";
export let path: string | null;
export let noteId: number | null;
async function hideAllGuessOne(): Promise<void> {
saveImageNotes(path!, noteId!, false);
}
async function hideOneGuessOne(): Promise<void> {
saveImageNotes(path!, noteId!, true);
}
const items = [
{ label: tr.notetypesOcclusionMask(), value: 1 },
{ label: tr.notetypesOcclusionNote(), value: 2 },
];
let activeTabValue = 1;
const tabChange = (tabValue) => () => (activeTabValue = tabValue);
</script>
<Container class="image-occlusion">
<ul>
{#each items as item}
<li class={activeTabValue === item.value ? "active" : ""}>
<span on:click={tabChange(item.value)}>{item.label}</span>
</li>
{/each}
</ul>
<div hidden={activeTabValue != 1}>
<MasksEditor {path} {noteId} />
</div>
<div hidden={activeTabValue != 2}>
<div class="notes-page"><Notes /></div>
<StickyFooter {hideAllGuessOne} {hideOneGuessOne} {noteId} />
</div>
</Container>
<style lang="scss">
:global(.image-occlusion) {
--gutter-inline: 0.5rem;
:global(.row) {
// rows have negative margins by default
--bs-gutter-x: 0;
// ensure equal spacing between tall rows like
// dropdowns, and short rows like checkboxes
min-height: 2.5em;
align-items: center;
}
}
ul {
display: flex;
flex-wrap: wrap;
padding-left: 0;
list-style: none;
border-bottom: 1px solid var(--border);
margin-top: 2px;
}
li {
margin-bottom: -1px;
}
span {
border: 1px solid transparent;
border-top-left-radius: 0.25rem;
border-top-right-radius: 0.25rem;
display: block;
padding: 0.5rem 1rem;
cursor: pointer;
color: var(--fg-subtle);
}
span:hover {
border-color: var(--border) var(--border) var(--canvas);
}
li.active > span {
color: var(--fg);
background-color: var(--canvas);
border-color: var(--border) var(--border) var(--canvas);
}
:global(.notes-page) {
@media only screen and (min-width: 1024px) {
width: min(100vw, 70em);
margin: 6px auto;
padding-bottom: 5em;
padding-right: 12px;
}
}
</style>

View file

@ -0,0 +1,81 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import type { PanZoom } from "panzoom";
import panzoom from "panzoom";
import { setupMaskEditor, setupMaskEditorForEdit } from "./mask-editor";
import SideToolbar from "./SideToolbar.svelte";
export let path: string | null;
export let noteId: number | null;
let instance: PanZoom;
let innerWidth = 0;
$: canvas = null;
function initPanzoom(node) {
instance = panzoom(node, {
bounds: true,
maxZoom: 3,
minZoom: 0.1,
zoomDoubleClickSpeed: 1,
smoothScroll: false,
});
instance.pause();
if (path) {
setupMaskEditor(path, instance).then((canvas1) => {
canvas = canvas1;
});
}
if (noteId) {
setupMaskEditorForEdit(noteId, instance).then((canvas1) => {
canvas = canvas1;
});
}
}
</script>
<SideToolbar {instance} {canvas} />
<div class="editor-main" bind:clientWidth={innerWidth}>
<div class="editor-container" use:initPanzoom>
<!-- svelte-ignore a11y-missing-attribute -->
<img id="image" />
<canvas id="canvas" />
</div>
</div>
<style lang="scss">
.editor-main {
position: absolute;
top: 84px;
left: 36px;
bottom: 2px;
right: 2px;
border: 1px solid var(--border);
overflow: auto;
padding-bottom: 100px;
}
.editor-container {
width: 100%;
height: 100%;
position: relative;
}
#image {
position: absolute;
}
:global(.upper-canvas) {
border: 0.5px solid var(--border-strong);
}
:global(.canvas-container) {
position: absolute;
}
</style>

View file

@ -0,0 +1,93 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import * as tr from "@tslib/ftl";
import Col from "../components/Col.svelte";
import Container from "../components/Container.svelte";
import Row from "../components/Row.svelte";
import NotesToolbar from "./notes-toolbar/NotesToolbar.svelte";
import { notesDataStore, tagsWritable } from "./store";
import Tags from "./Tags.svelte";
const notesFields = [
{
id: "header",
title: tr.notetypesHeader(),
divValue: "",
textareaValue: "",
},
{
id: "back-extra",
title: tr.notetypesBackExtraField(),
divValue: "",
textareaValue: "",
},
];
$: notesDataStore.set(notesFields);
</script>
<div class="note-toolbar">
<NotesToolbar />
</div>
<Container>
{#each notesFields as field}
<Container>
<Row --cols={1}>
<Col --col-size={1}>
{field.title}
</Col>
</Row>
<Row --cols={1}>
<div id="note-container">
<div
id="{field.id}--div"
bind:innerHTML={field.divValue}
class="text-editor"
on:input={() => {
field.textareaValue = field.divValue;
}}
contenteditable
/>
<textarea
id="{field.id}--textarea"
class="text-area"
bind:value={field.textareaValue}
/>
</div>
</Row>
</Container>
{/each}
<Container>
<Tags {tagsWritable} />
</Container>
</Container>
<style lang="scss">
.text-area {
height: 120px;
width: 100%;
display: none;
background: var(--canvas-elevated);
border: 2px solid var(--border);
outline: none;
resize: none;
overflow: auto;
}
.text-editor {
height: 80px;
border: 1px solid var(--border);
padding: 5px;
overflow: auto;
outline: none;
background: var(--canvas-elevated);
}
.note-toolbar {
margin-left: 14px;
}
</style>

View file

@ -0,0 +1,97 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import IconButton from "../components/IconButton.svelte";
import { drawEllipse, drawPolygon, drawRectangle } from "./tools/index";
import { enableSelectable, stopDraw } from "./tools/lib";
import { tools } from "./tools/tool-buttons";
import TopToolbar from "./TopToolbar.svelte";
export let instance;
export let canvas;
const iconSize = 80;
let activeTool = "cursor";
function setActive(toolId) {
activeTool = toolId;
disableFunctions();
enableSelectable(canvas, true);
switch (toolId) {
case "magnify":
enableSelectable(canvas, false);
instance.resume();
break;
case "draw-rectangle":
drawRectangle(canvas);
break;
case "draw-ellipse":
drawEllipse(canvas);
break;
case "draw-polygon":
drawPolygon(canvas, instance);
break;
default:
break;
}
}
const disableFunctions = () => {
instance.pause();
stopDraw(canvas);
canvas.selectionColor = "rgba(100, 100, 255, 0.3)";
};
</script>
<TopToolbar {canvas} {instance} {iconSize} />
<div class="tool-bar-container">
{#each tools as tool}
<IconButton
class="tool-icon-button {activeTool == tool.id ? 'active-tool' : ''}"
{iconSize}
active={activeTool === tool.id}
on:click={() => {
setActive(tool.id);
}}>{@html tool.icon}</IconButton
>
{/each}
</div>
<style>
.tool-bar-container {
position: fixed;
top: 46px;
left: 0;
height: 100%;
border-right: 1px solid var(--border);
overflow-y: auto;
width: 32px;
z-index: 99;
background: var(--canvas-elevated);
padding-bottom: 100px;
}
:global(.tool-icon-button) {
border: unset;
display: block;
width: 32px;
height: 32px;
margin: unset;
padding: 6px !important;
}
:global(.active-tool) {
color: red !important;
background: unset !important;
}
::-webkit-scrollbar {
width: 0.2em !important;
height: 0.2em !important;
}
</style>

View file

@ -0,0 +1,67 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import * as tr from "@tslib/ftl";
import ButtonGroup from "../components/ButtonGroup.svelte";
import LabelButton from "../components/LabelButton.svelte";
export let hideAllGuessOne: () => void;
export let hideOneGuessOne: () => void;
export let noteId: number | null;
</script>
<div style:flex-grow="1" />
<div class="sticky-footer">
{#if noteId}
<div class="update-note-text">
{tr.actionsUpdateNote()}
</div>
{/if}
<ButtonGroup size={2}>
<LabelButton
--border-left-radius="5px"
--border-right-radius="5px"
on:click={hideAllGuessOne}
class=" bottom-btn"
>
{tr.notetypesHideAllGuessOne()}
</LabelButton>
<LabelButton
--border-left-radius="5px"
--border-right-radius="5px"
on:click={hideOneGuessOne}
class=" bottom-btn"
>
{tr.notetypesHideOneGuessOne()}
</LabelButton>
</ButtonGroup>
</div>
<style lang="scss">
.sticky-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 99;
margin: 0;
padding: 0.25rem;
background: var(--window-bg);
border-style: solid none none;
border-color: var(--border);
border-width: thin;
display: flex;
justify-content: flex-end;
}
:global(.bottom-btn) {
margin: 2px;
}
:global(.update-note-text) {
align-self: center;
padding-right: 20px;
}
</style>

View file

@ -0,0 +1,28 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import Row from "../components/Row.svelte";
import TagEditor from "../tag-editor/TagEditor.svelte";
export let tagsWritable;
let globalTags: string[];
</script>
<Row --cols={1}>
<TagEditor
tags={tagsWritable}
on:tagsupdate={({ detail }) => {
globalTags = detail.tags;
tagsWritable.set(globalTags);
}}
keyCombination={"Control+T"}
/>
</Row>
<style lang="scss">
:global(.tag-editor) {
margin-top: 14px;
}
</style>

View file

@ -0,0 +1,61 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import IconButton from "../components/IconButton.svelte";
import { mdiClose } from "./icons";
export let type: "success" | "error" = "success";
export let message;
export let showToast = false;
const closeToast = () => {
showToast = false;
};
</script>
{#if showToast}
<div class="toast-container">
<div class="toast {type === 'success' ? 'success' : 'error'}">
{message}
<IconButton iconSize={96} on:click={closeToast} class="toast-icon">
{@html mdiClose}</IconButton
>
</div>
</div>
{/if}
<style>
.toast-container {
position: fixed;
bottom: 3rem;
z-index: 100;
width: 100%;
text-align: center;
display: flex;
justify-content: center;
}
.toast {
display: flex;
align-items: center;
padding: 1rem;
background-color: #fff;
border-radius: 0.5rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
width: 60%;
justify-content: space-between;
}
.success {
background: #66bb6a;
color: white;
}
.error {
background: #ef5350;
color: white;
}
:global(.toast-icon) {
background: unset !important;
color: white !important;
border: none !important;
}
</style>

View file

@ -0,0 +1,194 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script>
import IconButton from "../components/IconButton.svelte";
import { mdiEye, mdiFormatAlignCenter } from "./icons";
import { makeMaskTransparent } from "./tools/lib";
import {
alignTools,
deleteDuplicateTools,
groupUngroupTools,
zoomTools,
} from "./tools/more-tools";
import { undoRedoTools } from "./tools/tool-undo-redo";
export let canvas;
export let instance;
export let iconSize;
let showAlignTools = false;
let leftPos = 82;
let maksOpacity = false;
document.addEventListener("click", (event) => {
const upperCanvas = document.querySelector(".upper-canvas");
if (event.target == upperCanvas) {
showAlignTools = false;
}
});
</script>
<div class="top-tool-bar-container">
<!-- undo & redo tools -->
<div class="undo-redo-button">
{#each undoRedoTools as tool}
<IconButton
class="top-tool-icon-button {tool.name === 'undo'
? 'left-border-radius'
: 'right-border-radius'}"
{iconSize}
on:click={() => {
tool.action(canvas);
}}
>
{@html tool.icon}
</IconButton>
{/each}
</div>
<!-- zoom tools -->
<div class="tool-button-container">
{#each zoomTools as tool}
<IconButton
class="top-tool-icon-button {tool.name === 'zoomOut'
? 'left-border-radius'
: ''} {tool.name === 'zoomReset' ? 'right-border-radius' : ''}"
{iconSize}
on:click={() => {
tool.action(instance);
}}
>
{@html tool.icon}
</IconButton>
{/each}
</div>
<div class="tool-button-container">
<!-- opacity tools -->
<IconButton
class="top-tool-icon-button left-border-radius"
{iconSize}
on:click={() => {
maksOpacity = !maksOpacity;
makeMaskTransparent(canvas, maksOpacity);
}}
>
{@html mdiEye}
</IconButton>
<!-- cursor tools -->
{#each deleteDuplicateTools as tool}
<IconButton
class="top-tool-icon-button {tool.name === 'duplicate'
? 'right-border-radius'
: ''}"
{iconSize}
on:click={() => {
tool.action(canvas);
}}
>
{@html tool.icon}
</IconButton>
{/each}
</div>
<div class="tool-button-container">
<!-- group & ungroup tools -->
{#each groupUngroupTools as tool}
<IconButton
class="top-tool-icon-button {tool.name === 'group'
? 'left-border-radius'
: ''}"
{iconSize}
on:click={() => {
tool.action(canvas);
}}
>
{@html tool.icon}
</IconButton>
{/each}
<IconButton
class="top-tool-icon-button dropdown-tool right-border-radius"
{iconSize}
on:click={(e) => {
showAlignTools = !showAlignTools;
leftPos = e.pageX - 100;
}}
>
{@html mdiFormatAlignCenter}
</IconButton>
</div>
</div>
<div class:show={showAlignTools} class="dropdown-content" style="left:{leftPos}px;">
{#each alignTools as alignTool}
<IconButton
class="top-tool-icon-button"
{iconSize}
on:click={() => {
alignTool.action(canvas);
}}
>
{@html alignTool.icon}
</IconButton>
{/each}
</div>
<style>
.top-tool-bar-container {
display: flex;
position: fixed;
top: 46px;
width: 98%;
overflow-y: auto;
z-index: 99;
}
.undo-redo-button {
margin-left: 28px;
margin-right: 2px;
display: flex;
}
.tool-button-container {
margin-left: 2px;
margin-right: 2px;
display: flex;
}
:global(.left-border-radius) {
border-radius: 5px 0 0 5px !important;
}
:global(.right-border-radius) {
border-radius: 0 5px 5px 0 !important;
}
:global(.top-tool-icon-button) {
border: unset;
display: inline;
width: 32px;
height: 32px;
margin: unset;
padding: 6px !important;
}
.dropdown-content {
display: none;
position: absolute;
z-index: 100;
top: 82px;
margin-top: 1px;
}
.show {
display: block;
}
::-webkit-scrollbar {
width: 0.1em !important;
height: 0.1em !important;
}
</style>

View file

@ -0,0 +1,166 @@
// 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";
import { fabric } from "fabric";
import { get } from "svelte/store";
import type { Collection } from "../lib/proto";
import { addImageOcclusionNote, updateImageOcclusionNote } from "./lib";
import { notesDataStore, tagsWritable } from "./store";
import Toast from "./Toast.svelte";
import { makeMaskTransparent } from "./tools/lib";
const divData = [
"height",
"left",
"top",
"type",
"width",
];
// Defines the number of fraction digits to use when serializing object values
fabric.Object.NUM_FRACTION_DIGITS = 2;
export function generate(hideInactive: boolean): { occlusionCloze: string; noteCount: number } {
const canvas = globalThis.canvas;
const canvasObjects = canvas.getObjects();
if (canvasObjects.length < 1) {
return { occlusionCloze: "", noteCount: 0 };
}
let occlusionCloze = "";
let clozeData = "";
let noteCount = 0;
makeMaskTransparent(canvas, false);
canvasObjects.forEach((object, index) => {
const obJson = object.toJSON();
noteCount++;
if (obJson.type === "group") {
clozeData += getGroupCloze(object, index, hideInactive);
} else {
clozeData += getCloze(object, index, null, hideInactive);
}
});
occlusionCloze += clozeData;
return { occlusionCloze, noteCount };
}
const getCloze = (object, index, relativePos, hideInactive): string => {
const obJson = object.toJSON();
let clozeData = "";
// generate cloze data in form of
// {{c1::image-occlusion:rect:top=100:left=100:width=100:height=100}}
Object.keys(obJson).forEach(function(key) {
if (divData.includes(key)) {
if (key === "type") {
clozeData += `:${obJson[key]}`;
if (obJson[key] === "ellipse") {
clozeData += `:rx=${obJson.rx.toFixed(2)}:ry=${obJson.ry.toFixed(2)}`;
}
if (obJson[key] === "polygon") {
const points = obJson.points;
let pnts = "";
points.forEach((point: { x: number; y: number }) => {
pnts += point.x.toFixed(2) + "," + point.y.toFixed(2) + " ";
});
clozeData += `:points=${pnts.trim()}`;
}
} else if (relativePos && key === "top") {
clozeData += `:top=${relativePos.top}`;
} else if (relativePos && key === "left") {
clozeData += `:left=${relativePos.left}`;
} else {
clozeData += `:${key}=${obJson[key]}`;
}
}
});
clozeData += `:hideinactive=${hideInactive}`;
clozeData = `{{c${index + 1}::image-occlusion${clozeData}}}<br>`;
return clozeData;
};
const getGroupCloze = (group, index, hideInactive): string => {
let clozeData = "";
const objects = group._objects;
objects.forEach((object) => {
const { top, left } = getObjectPositionInGroup(group, object);
clozeData += getCloze(object, index, { top, left }, hideInactive);
});
return clozeData;
};
const getObjectPositionInGroup = (group, object): { top: number; left: number } => {
let left = object.left + group.left + group.width / 2;
let top = object.top + group.top + group.height / 2;
left = left.toFixed(2);
top = top.toFixed(2);
return { top, left };
};
export const saveImageNotes = async function(
imagePath: string,
noteId: number,
hideInactive: boolean,
): Promise<void> {
const { occlusionCloze, noteCount } = generate(hideInactive);
if (noteCount === 0) {
return;
}
const fieldsData: { id: string; title: string; divValue: string; textareaValue: string }[] = get(notesDataStore);
const tags = get(tagsWritable);
let header = fieldsData[0].textareaValue;
let backExtra = fieldsData[1].textareaValue;
header = header ? `<div>${header}</div>` : "";
backExtra = header ? `<div>${backExtra}</div>` : "";
if (noteId) {
const result = await updateImageOcclusionNote(
noteId,
occlusionCloze,
header,
backExtra,
tags,
);
showResult(noteId, result, noteCount);
} else {
const result = await addImageOcclusionNote(
imagePath,
occlusionCloze,
header,
backExtra,
tags,
);
showResult(noteId, result, noteCount);
}
};
// show toast message
const showResult = (noteId: number, result: Collection.OpChanges, count: number) => {
const toastComponent = new Toast({
target: document.body,
props: {
message: "",
type: "error",
},
});
if (result.note) {
const msg = noteId ? tr.browsingCardsUpdated({ count: count }) : tr.importingCardsAdded({ count: count });
toastComponent.$set({ message: msg, type: "success", showToast: true });
} else {
const msg = tr.notetypesErrorGeneratingCloze();
toastComponent.$set({ message: msg, showToast: true });
}
};

View file

@ -0,0 +1,33 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
/// <reference types="../lib/image-import" />
export { default as mdiAlignHorizontalCenter } from "@mdi/svg/svg/align-horizontal-center.svg";
export { default as mdiAlignHorizontalLeft } from "@mdi/svg/svg/align-horizontal-left.svg";
export { default as mdiAlignHorizontalRight } from "@mdi/svg/svg/align-horizontal-right.svg";
export { default as mdiAlignVerticalBottom } from "@mdi/svg/svg/align-vertical-bottom.svg";
export { default as mdiAlignVerticalCenter } from "@mdi/svg/svg/align-vertical-center.svg";
export { default as mdiAlignVerticalTop } from "@mdi/svg/svg/align-vertical-top.svg";
export { default as mdiClose } from "@mdi/svg/svg/close.svg";
export { default as mdiCodeTags } from "@mdi/svg/svg/code-tags.svg";
export { default as mdiCopy } from "@mdi/svg/svg/content-copy.svg";
export { default as mdiCursorDefaultOutline } from "@mdi/svg/svg/cursor-default-outline.svg";
export { default as mdiDeleteOutline } from "@mdi/svg/svg/delete-outline.svg";
export { default as mdiEllipseOutline } from "@mdi/svg/svg/ellipse-outline.svg";
export { default as mdiEye } from "@mdi/svg/svg/eye.svg";
export { default as mdiFormatAlignCenter } from "@mdi/svg/svg/format-align-center.svg";
export { default as mdiFormatBold } from "@mdi/svg/svg/format-bold.svg";
export { default as mdiFormatItalic } from "@mdi/svg/svg/format-italic.svg";
export { default as mdiFormatUnderline } from "@mdi/svg/svg/format-underline.svg";
export { default as mdiGroup } from "@mdi/svg/svg/group.svg";
export { default as mdiZoomReset } from "@mdi/svg/svg/magnify-expand.svg";
export { default as mdiZoomOut } from "@mdi/svg/svg/magnify-minus-outline.svg";
export { default as mdiZoomIn } from "@mdi/svg/svg/magnify-plus-outline.svg";
export { default as mdiMagnifyScan } from "@mdi/svg/svg/magnify-scan.svg";
export { default as mdiRectangleOutline } from "@mdi/svg/svg/rectangle-outline.svg";
export { default as mdiRedo } from "@mdi/svg/svg/redo.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";
export { default as mdiVectorPolygonVariant } from "@mdi/svg/svg/vector-polygon-variant.svg";

View file

@ -0,0 +1,19 @@
@use "sass/vars";
@use "sass/bootstrap-dark";
@import "sass/base";
@import "bootstrap/scss/alert";
@import "bootstrap/scss/buttons";
@import "bootstrap/scss/button-group";
@import "bootstrap/scss/close";
@import "bootstrap/scss/grid";
@import "sass/bootstrap-forms";
.night-mode {
@include bootstrap-dark.night-mode;
}
html {
overflow: hidden;
}

View file

@ -0,0 +1,56 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import "./image-occlusion-base.scss";
import { ModuleName, setupI18n } from "@tslib/i18n";
import { checkNightMode } from "../lib/nightmode";
import ImageOcclusionPage from "./ImageOcclusionPage.svelte";
const i18n = setupI18n({
modules: [
ModuleName.IMPORTING,
ModuleName.DECKS,
ModuleName.EDITING,
ModuleName.NOTETYPES,
ModuleName.ACTIONS,
ModuleName.BROWSING,
],
});
export async function setupImageOcclusion(path: string): Promise<ImageOcclusionPage> {
checkNightMode();
await i18n;
return new ImageOcclusionPage({
target: document.body,
props: {
path: path,
noteId: null,
},
});
}
export async function setupImageOcclusionForEdit(noteId: number): Promise<ImageOcclusionPage> {
checkNightMode();
await i18n;
return new ImageOcclusionPage({
target: document.body,
props: {
path: null,
noteId: noteId,
},
});
}
if (window.location.hash.startsWith("#test-")) {
const path = window.location.hash.replace("#test-", "");
setupImageOcclusion(path);
}
if (window.location.hash.startsWith("#testforedit-")) {
const noteId = parseInt(window.location.hash.replace("#testforedit-", ""));
setupImageOcclusionForEdit(noteId);
}

61
ts/image-occlusion/lib.ts Normal file
View file

@ -0,0 +1,61 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { Collection } from "../lib/proto";
import { ImageOcclusion, imageOcclusion } from "../lib/proto";
export async function getImageForOcclusion(
path: string,
): Promise<ImageOcclusion.ImageData> {
return imageOcclusion.getImageForOcclusion(
ImageOcclusion.GetImageForOcclusionRequest.create({
path,
}),
);
}
export async function addImageOcclusionNote(
imagePath: string,
occlusions: string,
header: string,
backExtra: string,
tags: string[],
): Promise<Collection.OpChanges> {
return imageOcclusion.addImageOcclusionNote(
ImageOcclusion.AddImageOcclusionNoteRequest.create({
imagePath,
occlusions,
header,
backExtra,
tags,
}),
);
}
export async function getImageClozeNote(
noteId: number,
): Promise<ImageOcclusion.ImageClozeNoteResponse> {
return imageOcclusion.getImageClozeNote(
ImageOcclusion.GetImageOcclusionNoteRequest.create({
noteId,
}),
);
}
export async function updateImageOcclusionNote(
noteId: number,
occlusions: string,
header: string,
backExtra: string,
tags: string[],
): Promise<Collection.OpChanges> {
return imageOcclusion.updateImageOcclusionNote(
ImageOcclusion.UpdateImageOcclusionNoteRequest.create({
noteId,
occlusions,
header,
backExtra,
tags,
}),
);
}

View file

@ -0,0 +1,143 @@
// 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";
import type { ImageOcclusion } from "@tslib/proto";
import { fabric } from "fabric";
import type { PanZoom } from "panzoom";
import protobuf from "protobufjs";
import { get } from "svelte/store";
import { getImageClozeNote, getImageForOcclusion } from "./lib";
import { notesDataStore, tagsWritable, zoomResetValue } from "./store";
import Toast from "./Toast.svelte";
import { enableSelectable, moveShapeToCanvasBoundaries } from "./tools/lib";
import { generateShapeFromCloze } from "./tools/shape-generate";
import { undoRedoInit } from "./tools/tool-undo-redo";
export const setupMaskEditor = async (path: string, instance: PanZoom): Promise<fabric.Canvas> => {
const imageData = await getImageForOcclusion(path!);
const canvas = initCanvas();
// get image width and height
const image = document.getElementById("image") as HTMLImageElement;
image.src = getImageData(imageData.data!);
image.onload = function() {
const size = limitSize({ width: image.width, height: image.height });
canvas.setWidth(size.width);
canvas.setHeight(size.height);
image.height = size.height;
image.width = size.width;
setCanvasZoomRatio(canvas, instance);
};
return canvas;
};
export const setupMaskEditorForEdit = async (noteId: number, instance: PanZoom): Promise<fabric.Canvas> => {
const clozeNoteResponse: ImageOcclusion.ImageClozeNoteResponse = await getImageClozeNote(noteId);
if (clozeNoteResponse.error) {
new Toast({
target: document.body,
props: {
message: tr.notetypesErrorGettingImagecloze(),
type: "error",
},
}).$set({ showToast: true });
return;
}
const clozeNote = clozeNoteResponse.note!;
const canvas = initCanvas();
// get image width and height
const image = document.getElementById("image") as HTMLImageElement;
image.src = getImageData(clozeNote.imageData!);
image.onload = function() {
const size = limitSize({ width: image.width, height: image.height });
canvas.setWidth(size.width);
canvas.setHeight(size.height);
image.height = size.height;
image.width = size.width;
setCanvasZoomRatio(canvas, instance);
generateShapeFromCloze(canvas, clozeNote.occlusions);
enableSelectable(canvas, true);
addClozeNotesToTextEditor(clozeNote.header, clozeNote.backExtra, clozeNote.tags);
};
return canvas;
};
const initCanvas = (): fabric.Canvas => {
const canvas = new fabric.Canvas("canvas");
tagsWritable.set([]);
globalThis.canvas = canvas;
// enables uniform scaling by default without the need for the Shift key
canvas.uniformScaling = false;
canvas.uniScaleKey = "none";
moveShapeToCanvasBoundaries(canvas);
undoRedoInit(canvas);
return canvas;
};
const getImageData = (imageData): string => {
const b64encoded = protobuf.util.base64.encode(
imageData,
0,
imageData.length,
);
return "data:image/png;base64," + b64encoded;
};
const setCanvasZoomRatio = (
canvas: fabric.Canvas,
instance: PanZoom,
): void => {
const zoomRatioW = (innerWidth - 60) / canvas.width!;
const zoomRatioH = (innerHeight - 100) / canvas.height!;
const zoomRatio = zoomRatioW < zoomRatioH ? zoomRatioW : zoomRatioH;
zoomResetValue.set(zoomRatio);
instance.smoothZoom(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;
});
};
/**
* Fix for safari browser,
* Canvas area exceeds the maximum limit (width * height > 16777216),
* Following function also added in reviewer ts,
* so update both, if it changes
*/
const 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,
};
};

View file

@ -0,0 +1,29 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import IconButton from "../../components/IconButton.svelte";
import { mdiCodeTags } from "../icons";
import { changePreviewHTMLView } from "./lib";
export let iconSize;
const moreTools = [
{
name: "code",
title: "Code",
icon: mdiCodeTags,
action: changePreviewHTMLView,
},
];
</script>
{#each moreTools as tool}
<IconButton
class="note-tool-icon-button"
{iconSize}
on:click={() => tool.action()}
tooltip={tool.title}>{@html tool.icon}</IconButton
>
{/each}

View file

@ -0,0 +1,13 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import MoreTools from "./MoreTools.svelte";
import TextFormatting from "./TextFormatting.svelte";
const iconSize = 80;
</script>
<TextFormatting {iconSize} />
<MoreTools {iconSize} />

View file

@ -0,0 +1,58 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import * as tr from "@tslib/ftl";
import IconButton from "../../components/IconButton.svelte";
import { execCommand } from "../../domlib";
import { mdiFormatBold, mdiFormatItalic, mdiFormatUnderline } from "../icons";
export let iconSize;
const textFormatting = [
{
name: "b",
title: tr.editingBoldText(),
icon: mdiFormatBold,
action: "bold",
},
{
name: "i",
title: tr.editingItalicText(),
icon: mdiFormatItalic,
action: "italic",
},
{
name: "u",
title: tr.editingUnderlineText(),
icon: mdiFormatUnderline,
action: "underline",
},
];
const textFormat = (tool: { name; title; icon; action }) => {
execCommand(tool.action, false, tool.name);
};
</script>
{#each textFormatting as tool}
<IconButton
id={"note-tool-" + tool.name}
class="note-tool-icon-button"
{iconSize}
tooltip={tool.title}
on:click={() => {
// setActiveTool(tool);
textFormat(tool);
}}>{@html tool.icon}</IconButton
>
{/each}
<style lang="scss">
:global(.note-tool-icon-button) {
padding: 4px !important;
border-radius: 2px !important;
}
</style>

View file

@ -0,0 +1,6 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import NotesToolbar from "./NotesToolbar.svelte";
export default NotesToolbar;

View file

@ -0,0 +1,25 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export const changePreviewHTMLView = (): void => {
const activeElement = document.activeElement!;
if (!activeElement || !activeElement.id.includes("--")) {
return;
}
const noteId = activeElement.id.split("--")[0];
const divId = `${noteId}--div`;
const textAreaId = `${noteId}--textarea`;
const divElement = document.getElementById(divId)!;
const textAreaElement = document.getElementById(textAreaId)! as HTMLTextAreaElement;
if (divElement.style.display == "none") {
divElement.style.display = "block";
textAreaElement.style.display = "none";
divElement.focus();
} else {
divElement.style.display = "none";
textAreaElement.style.display = "block";
textAreaElement.focus();
}
};

View file

@ -0,0 +1,11 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { writable } from "svelte/store";
// it stores note's data for generate.ts, when function generate() is called it will be used to generate the note
export const notesDataStore = writable({ id: "", title: "", divValue: "", textareaValue: "" }[0]);
// it stores the value of zoom ratio for canvas
export const zoomResetValue = writable(1);
// it stores the tags for the note in note editor
export const tagsWritable = writable([""]);

View file

@ -0,0 +1,8 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { drawEllipse } from "./tool-ellipse";
import { drawPolygon } from "./tool-polygon";
import { drawRectangle } from "./tool-rect";
export { drawEllipse, drawPolygon, drawRectangle };

View file

@ -0,0 +1,208 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type fabric from "fabric";
import type { PanZoom } from "panzoom";
import { get } from "svelte/store";
import { zoomResetValue } from "../store";
export const shapeMaskColor = "#ffeba2";
export const borderColor = "#212121";
let _clipboard;
export const stopDraw = (canvas: fabric.Canvas): void => {
canvas.off("mouse:down");
canvas.off("mouse:up");
canvas.off("mouse:move");
};
export const enableSelectable = (canvas: fabric.Canvas, select: boolean): void => {
canvas.selection = select;
canvas.forEachObject(function(o) {
o.selectable = select;
});
canvas.renderAll();
};
export const deleteItem = (canvas: fabric.Canvas): void => {
const active = canvas.getActiveObject();
if (active) {
canvas.remove(active);
if (active.type == "activeSelection") {
active.getObjects().forEach((x) => canvas.remove(x));
canvas.discardActiveObject().renderAll();
}
}
};
export const duplicateItem = (canvas: fabric.Canvas): void => {
if (!canvas.getActiveObject()) {
return;
}
copyItem(canvas);
pasteItem(canvas);
};
export const groupShapes = (canvas: fabric.Canvas): void => {
if (
!canvas.getActiveObject()
|| canvas.getActiveObject().type !== "activeSelection"
) {
return;
}
canvas.getActiveObject().toGroup();
canvas.requestRenderAll();
};
export const unGroupShapes = (canvas: fabric.Canvas): void => {
if (
!canvas.getActiveObject()
|| canvas.getActiveObject().type !== "group"
) {
return;
}
const group = canvas.getActiveObject();
const items = group.getObjects();
group._restoreObjectsState();
canvas.remove(group);
items.forEach((item) => {
canvas.add(item);
});
canvas.requestRenderAll();
};
export const zoomIn = (instance: PanZoom): void => {
instance.smoothZoom(0, 0, 1.25);
};
export const zoomOut = (instance: PanZoom): void => {
instance.smoothZoom(0, 0, 0.5);
};
export const zoomReset = (instance: PanZoom): void => {
instance.moveTo(0, 0);
instance.smoothZoomAbs(0, 0, get(zoomResetValue));
};
const copyItem = (canvas: fabric.Canvas): void => {
if (!canvas.getActiveObject()) {
return;
}
// clone what are you copying since you
// may want copy and paste on different moment.
// and you do not want the changes happened
// later to reflect on the copy.
canvas.getActiveObject().clone(function(cloned) {
_clipboard = cloned;
});
};
const pasteItem = (canvas: fabric.Canvas): void => {
// clone again, so you can do multiple copies.
_clipboard.clone(function(clonedObj) {
canvas.discardActiveObject();
clonedObj.set({
left: clonedObj.left + 10,
top: clonedObj.top + 10,
evented: true,
});
if (clonedObj.type === "activeSelection") {
// active selection needs a reference to the canvas.
clonedObj.canvas = canvas;
clonedObj.forEachObject(function(obj) {
canvas.add(obj);
});
// this should solve the unselectability
clonedObj.setCoords();
} else {
canvas.add(clonedObj);
}
_clipboard.top += 10;
_clipboard.left += 10;
canvas.setActiveObject(clonedObj);
canvas.requestRenderAll();
});
};
export const makeMaskTransparent = (canvas: fabric.Canvas, opacity = false): void => {
const objects = canvas.getObjects();
objects.forEach((object) => {
object.set({
opacity: opacity ? 0.4 : 1,
transparentCorners: false,
});
});
canvas.renderAll();
};
export const moveShapeToCanvasBoundaries = (canvas: fabric.Canvas): void => {
canvas.on("object:modified", function(o) {
const activeObject = o.target;
if (!activeObject) {
return;
}
if (activeObject.type === "activeSelection" || activeObject.type === "rect") {
modifiedSelection(canvas, activeObject);
}
if (activeObject.type === "ellipse") {
modifiedEllipse(canvas, activeObject);
}
});
};
const modifiedSelection = (canvas: fabric.Canvas, object: fabric.Object): void => {
const newWidth = object.width * object.scaleX;
const newHeight = object.height * object.scaleY;
object.set({
width: newWidth,
height: newHeight,
scaleX: 1,
scaleY: 1,
});
setShapePosition(canvas, object);
};
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;
const newHeight = object.height * object.scaleY;
object.set({
rx: newRx,
ry: newRy,
width: newWidth,
height: newHeight,
scaleX: 1,
scaleY: 1,
});
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.top + object.height + object.strokeWidth > canvas.height) {
object.set({ top: canvas.height - object.height });
}
object.setCoords();
};

View file

@ -0,0 +1,104 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import {
mdiAlignHorizontalCenter,
mdiAlignHorizontalLeft,
mdiAlignHorizontalRight,
mdiAlignVerticalBottom,
mdiAlignVerticalCenter,
mdiAlignVerticalTop,
mdiCopy,
mdiDeleteOutline,
mdiGroup,
mdiUngroup,
mdiZoomIn,
mdiZoomOut,
mdiZoomReset,
} from "../icons";
import { deleteItem, duplicateItem, groupShapes, unGroupShapes, zoomIn, zoomOut, zoomReset } from "./lib";
import {
alignBottom,
alignHorizontalCenter,
alignLeft,
alignRight,
alignTop,
alignVerticalCenter,
} from "./tool-aligns";
export const groupUngroupTools = [
{
name: "group",
icon: mdiGroup,
action: groupShapes,
},
{
name: "ungroup",
icon: mdiUngroup,
action: unGroupShapes,
},
];
export const deleteDuplicateTools = [
{
name: "delete",
icon: mdiDeleteOutline,
action: deleteItem,
},
{
name: "duplicate",
icon: mdiCopy,
action: duplicateItem,
},
];
export const zoomTools = [
{
name: "zoomOut",
icon: mdiZoomOut,
action: zoomOut,
},
{
name: "zoomIn",
icon: mdiZoomIn,
action: zoomIn,
},
{
name: "zoomReset",
icon: mdiZoomReset,
action: zoomReset,
},
];
export const alignTools = [
{
id: 1,
icon: mdiAlignHorizontalLeft,
action: alignLeft,
},
{
id: 2,
icon: mdiAlignHorizontalCenter,
action: alignHorizontalCenter,
},
{
id: 3,
icon: mdiAlignHorizontalRight,
action: alignRight,
},
{
id: 4,
icon: mdiAlignVerticalTop,
action: alignTop,
},
{
id: 5,
icon: mdiAlignVerticalCenter,
action: alignVerticalCenter,
},
{
id: 6,
icon: mdiAlignVerticalBottom,
action: alignBottom,
},
];

View file

@ -0,0 +1,131 @@
// 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 { shapeMaskColor } from "./lib";
export const generateShapeFromCloze = (canvas: fabric.Canvas, clozeStr: string): void => {
// generate shapes from clozeStr similar to following
// {{c1::image-occlusion:rect:left=10.0:top=20:width=30:height=10:fill=#ffe34d}}
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);
}
for (const index in clozeList) {
let shape: fabric.Group | fabric.Rect | fabric.Ellipse | fabric.Polygon;
if (clozeList[index].length > 1) {
const group = new fabric.Group();
clozeList[index].forEach((shape) => {
const parts = shape.split(":");
const objectType = parts[0];
const objectProperties = {
angle: "0",
left: "0",
top: "0",
width: "0",
height: "0",
fill: shapeMaskColor,
rx: "0",
ry: "0",
points: "",
};
for (let i = 1; i < parts.length; i++) {
const keyValue = parts[i].split("=");
const key = keyValue[0];
const value = keyValue[1];
objectProperties[key] = value;
}
shape = drawShapes(objectType, objectProperties);
group.addWithUpdate(shape);
});
canvas.add(group);
} else {
const cloze = clozeList[index][0];
const parts = cloze.split(":");
const objectType = parts[0];
const objectProperties = {
angle: "0",
left: "0",
top: "0",
width: "0",
height: "0",
fill: shapeMaskColor,
rx: "0",
ry: "0",
points: "",
};
for (let i = 1; i < parts.length; i++) {
const keyValue = parts[i].split("=");
const key = keyValue[0];
const value = keyValue[1];
objectProperties[key] = value;
}
shape = drawShapes(objectType, objectProperties);
canvas.add(shape);
}
}
canvas.requestRenderAll();
};
const drawShapes = (objectType, objectProperties) => {
switch (objectType) {
case "rect": {
const rect = new fabric.Rect({
left: parseFloat(objectProperties.left),
top: parseFloat(objectProperties.top),
width: parseFloat(objectProperties.width),
height: parseFloat(objectProperties.height),
fill: objectProperties.fill,
selectable: false,
});
return rect;
}
case "ellipse": {
const ellipse = new fabric.Ellipse({
left: parseFloat(objectProperties.left),
top: parseFloat(objectProperties.top),
rx: parseFloat(objectProperties.rx),
ry: parseFloat(objectProperties.ry),
fill: objectProperties.fill,
selectable: false,
});
return ellipse;
}
case "polygon": {
const points = objectProperties.points.split(" ");
const polygon = new fabric.Polygon(points, {
left: parseFloat(objectProperties.left),
top: parseFloat(objectProperties.top),
fill: objectProperties.fill,
selectable: false,
});
return polygon;
}
default:
break;
}
};

View file

@ -0,0 +1,210 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { fabric } from "fabric";
export const alignLeft = (canvas: fabric.canvas): void => {
if (!canvas.getActiveObject()) {
return;
}
const activeObject = canvas.getActiveObject();
if (canvas.getActiveObject().type == "activeSelection") {
alignLeftGroup(canvas, activeObject);
} else {
activeObject.set({ left: 0 });
}
activeObject.setCoords();
canvas.renderAll();
};
export const alignHorizontalCenter = (canvas: fabric.Canvas): void => {
if (!canvas.getActiveObject()) {
return;
}
const activeObject = canvas.getActiveObject();
if (canvas.getActiveObject().type == "activeSelection") {
alignHorizontalCenterGroup(canvas, activeObject);
} else {
activeObject.set({ left: canvas.width / 2 - activeObject.width / 2 });
}
activeObject.setCoords();
canvas.renderAll();
};
export const alignRight = (canvas: fabric.Canvas): void => {
if (!canvas.getActiveObject()) {
return;
}
const activeObject = canvas.getActiveObject();
if (canvas.getActiveObject().type == "activeSelection") {
alignRightGroup(canvas, activeObject);
} else {
activeObject.set({ left: canvas.getWidth() - activeObject.width });
}
activeObject.setCoords();
canvas.renderAll();
};
export const alignTop = (canvas: fabric.Canvas): void => {
if (!canvas.getActiveObject()) {
return;
}
const activeObject = canvas.getActiveObject();
if (canvas.getActiveObject().type == "activeSelection") {
alignTopGroup(canvas, activeObject);
} else {
activeObject.set({ top: 0 });
}
activeObject.setCoords();
canvas.renderAll();
};
export const alignVerticalCenter = (canvas: fabric.Canvas): void => {
if (!canvas.getActiveObject()) {
return;
}
const activeObject = canvas.getActiveObject();
if (canvas.getActiveObject().type == "activeSelection") {
alignVerticalCenterGroup(canvas, activeObject);
} else {
activeObject.set({ top: canvas.getHeight() / 2 - activeObject.height / 2 });
}
activeObject.setCoords();
canvas.renderAll();
};
export const alignBottom = (canvas: fabric.Canvas): void => {
if (!canvas.getActiveObject()) {
return;
}
const activeObject = canvas.getActiveObject();
if (canvas.getActiveObject().type == "activeSelection") {
alignBottomGroup(canvas, activeObject);
} else {
activeObject.set({ top: canvas.height - activeObject.height });
}
activeObject.setCoords();
canvas.renderAll();
};
// group aligns
const alignLeftGroup = (canvas: fabric.Canvas, group: fabric.Group | fabric.IObject) => {
const objects = group.getObjects();
let leftmostShape = objects[0];
for (let i = 1; i < objects.length; i++) {
if (objects[i].left < leftmostShape.left) {
leftmostShape = objects[i];
}
}
objects.forEach((object) => {
object.left = leftmostShape.left;
object.setCoords();
});
};
const alignRightGroup = (canvas: fabric.Canvas, group: fabric.Group | fabric.IObject): void => {
const objects = group.getObjects();
let rightmostShape = objects[0];
for (let i = 1; i < objects.length; i++) {
if (objects[i].left > rightmostShape.left) {
rightmostShape = objects[i];
}
}
objects.forEach((object) => {
object.left = rightmostShape.left + rightmostShape.width - object.width;
object.setCoords();
});
};
const alignTopGroup = (canvas: fabric.Canvas, group: fabric.Group | fabric.IObject): void => {
const objects = group.getObjects();
let topmostShape = objects[0];
for (let i = 1; i < objects.length; i++) {
if (objects[i].top < topmostShape.top) {
topmostShape = objects[i];
}
}
objects.forEach((object) => {
object.top = topmostShape.top;
object.setCoords();
});
};
const alignBottomGroup = (canvas: fabric.Canvas, group: fabric.Group | fabric.IObject): void => {
const objects = group.getObjects();
let bottommostShape = objects[0];
for (let i = 1; i < objects.length; i++) {
if (objects[i].top + objects[i].height > bottommostShape.top + bottommostShape.height) {
bottommostShape = objects[i];
}
}
objects.forEach(function(object) {
if (object !== bottommostShape) {
object.set({ top: bottommostShape.top + bottommostShape.height - object.height });
object.setCoords();
}
});
};
const alignHorizontalCenterGroup = (canvas: fabric.Canvas, group: fabric.Group | fabric.IObject) => {
const objects = group.getObjects();
let leftmostShape = objects[0];
let rightmostShape = objects[0];
for (let i = 1; i < objects.length; i++) {
if (objects[i].left < leftmostShape.left) {
leftmostShape = objects[i];
}
if (objects[i].left > rightmostShape.left) {
rightmostShape = objects[i];
}
}
const centerX = (leftmostShape.left + rightmostShape.left + rightmostShape.width) / 2;
objects.forEach((object) => {
object.left = centerX - object.width / 2;
object.setCoords();
});
};
const alignVerticalCenterGroup = (canvas: fabric.Canvas, group: fabric.Group | fabric.IObject) => {
const objects = group.getObjects();
let topmostShape = objects[0];
let bottommostShape = objects[0];
for (let i = 1; i < objects.length; i++) {
if (objects[i].top < topmostShape.top) {
topmostShape = objects[i];
}
if (objects[i].top > bottommostShape.top) {
bottommostShape = objects[i];
}
}
const centerY = (topmostShape.top + bottommostShape.top + bottommostShape.height) / 2;
objects.forEach((object) => {
object.top = centerY - object.height / 2;
object.setCoords();
});
};

View file

@ -0,0 +1,33 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import {
mdiCursorDefaultOutline,
mdiEllipseOutline,
mdiMagnifyScan,
mdiRectangleOutline,
mdiVectorPolygonVariant,
} from "../icons";
export const tools = [
{
id: "cursor",
icon: mdiCursorDefaultOutline,
},
{
id: "magnify",
icon: mdiMagnifyScan,
},
{
id: "draw-rectangle",
icon: mdiRectangleOutline,
},
{
id: "draw-ellipse",
icon: mdiEllipseOutline,
},
{
id: "draw-polygon",
icon: mdiVectorPolygonVariant,
},
];

View file

@ -0,0 +1,121 @@
// 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 { borderColor, shapeMaskColor, stopDraw } from "./lib";
import { objectAdded } from "./tool-undo-redo";
const addedEllipseIds: string[] = [];
export const drawEllipse = (canvas: fabric.Canvas): void => {
canvas.selectionColor = "rgba(0, 0, 0, 0)";
let ellipse, isDown, origX, origY;
stopDraw(canvas);
canvas.on("mouse:down", function(o) {
if (o.target) {
return;
}
isDown = true;
const pointer = canvas.getPointer(o.e);
origX = pointer.x;
origY = pointer.y;
ellipse = new fabric.Ellipse({
id: "ellipse-" + new Date().getTime(),
left: origX,
top: origY,
originX: "left",
originY: "top",
rx: pointer.x - origX,
ry: pointer.y - origY,
fill: shapeMaskColor,
transparentCorners: false,
selectable: true,
stroke: borderColor,
strokeWidth: 1,
strokeUniform: true,
noScaleCache: false,
});
canvas.add(ellipse);
});
canvas.on("mouse:move", function(o) {
if (!isDown) return;
const pointer = canvas.getPointer(o.e);
let rx = Math.abs(origX - pointer.x) / 2;
let ry = Math.abs(origY - pointer.y) / 2;
const x = pointer.x;
const y = pointer.y;
if (rx > ellipse.strokeWidth) {
rx -= ellipse.strokeWidth / 2;
}
if (ry > ellipse.strokeWidth) {
ry -= ellipse.strokeWidth / 2;
}
if (x < origX) {
ellipse.set({ originX: "right" });
} else {
ellipse.set({ originX: "left" });
}
if (y < origY) {
ellipse.set({ originY: "bottom" });
} else {
ellipse.set({ originY: "top" });
}
// do not draw outside of canvas
if (x < ellipse.strokeWidth) {
rx = (origX + ellipse.strokeWidth + 0.5) / 2;
}
if (y < ellipse.strokeWidth) {
ry = (origY + ellipse.strokeWidth + 0.5) / 2;
}
if (x >= canvas.width - ellipse.strokeWidth) {
rx = (canvas.width - origX) / 2 - ellipse.strokeWidth + 0.5;
}
if (y > canvas.height - ellipse.strokeWidth) {
ry = (canvas.height - origY) / 2 - ellipse.strokeWidth + 0.5;
}
ellipse.set({ rx: rx, ry: ry });
canvas.renderAll();
});
canvas.on("mouse:up", function() {
isDown = false;
// probably changed from ellipse to rectangle
if (!ellipse) {
return;
}
if (ellipse.width < 5 || ellipse.height < 5) {
canvas.remove(ellipse);
return;
}
if (ellipse.originX === "right") {
ellipse.set({
originX: "left",
left: ellipse.left - ellipse.width + ellipse.strokeWidth,
});
}
if (ellipse.originY === "bottom") {
ellipse.set({
originY: "top",
top: ellipse.top - ellipse.height + ellipse.strokeWidth,
});
}
ellipse.setCoords();
objectAdded(canvas, addedEllipseIds, ellipse.id);
});
};

View file

@ -0,0 +1,233 @@
// 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 type { PanZoom } from "panzoom";
import { borderColor, shapeMaskColor } from "./lib";
import { objectAdded, saveCanvasState } from "./tool-undo-redo";
let activeLine;
let activeShape;
let linesList: fabric.Line = [];
let pointsList: fabric.Circle = [];
let drawMode = false;
let zoomValue = 1;
let panzoomX = 1, panzoomY = 1;
const addedPolygonIds: string[] = [];
export const drawPolygon = (canvas: fabric.Canvas, panzoom: PanZoom): void => {
canvas.selectionColor = "rgba(0, 0, 0, 0)";
canvas.on("mouse:down", function(options) {
try {
if (options.target && options.target.id === pointsList[0].id) {
generatePolygon(canvas, pointsList);
} else {
addPoint(canvas, options, panzoom);
}
} catch (e) {
// Cannot read properties of undefined (reading 'id')
}
});
canvas.on("mouse:move", function(options) {
if (activeLine && activeLine.class === "line") {
const pointer = canvas.getPointer(options.e);
activeLine.set({
x2: pointer.x,
y2: pointer.y,
});
const points = activeShape.get("points");
points[pointsList.length] = {
x: pointer.x,
y: pointer.y,
};
activeShape.set({ points });
}
canvas.renderAll();
});
};
const toggleDrawPolygon = (canvas: fabric.Canvas): void => {
drawMode = !drawMode;
if (drawMode) {
activeLine = null;
activeShape = null;
linesList = [];
pointsList = [];
drawMode = false;
canvas.selection = true;
} else {
drawMode = true;
canvas.selection = false;
}
};
const addPoint = (canvas: fabric.Canvas, options, panzoom): void => {
zoomValue = panzoom.getTransform().scale;
panzoomX = panzoom.getTransform().x;
panzoomY = panzoom.getTransform().y;
const canvasContainer = document.querySelector(".canvas-container")!.getBoundingClientRect()!;
let clientX = options.e.touches ? options.e.touches[0].clientX : options.e.clientX;
let clientY = options.e.touches ? options.e.touches[0].clientY : options.e.clientY;
clientX = (clientX - canvasContainer.left - panzoomX) / zoomValue;
clientY = (clientY - canvasContainer.top - panzoomY) / zoomValue;
const point = new fabric.Circle({
radius: 5,
fill: "#ffffff",
stroke: "#333333",
strokeWidth: 0.5,
originX: "left",
originY: "top",
left: clientX,
top: clientY,
selectable: false,
hasBorders: false,
hasControls: false,
objectCaching: false,
});
if (pointsList.length === 0) {
point.set({
fill: "red",
});
}
const linePoints = [clientX, clientY, clientX, clientY];
const line = new fabric.Line(linePoints, {
strokeWidth: 2,
fill: "#999999",
stroke: "#999999",
originX: "left",
originY: "top",
selectable: false,
hasBorders: false,
hasControls: false,
evented: false,
objectCaching: false,
});
line.class = "line";
if (activeShape) {
const pointer = canvas.getPointer(options.e);
const points = activeShape.get("points");
points.push({
x: pointer.x,
y: pointer.y,
});
const polygon = new fabric.Polygon(points, {
stroke: "#333333",
strokeWidth: 1,
fill: "#cccccc",
opacity: 0.3,
selectable: false,
hasBorders: false,
hasControls: false,
evented: false,
objectCaching: false,
});
canvas.remove(activeShape);
canvas.add(polygon);
activeShape = polygon;
canvas.renderAll();
} else {
const polyPoint = [{ x: clientX, y: clientY }];
const polygon = new fabric.Polygon(polyPoint, {
stroke: "#333333",
strokeWidth: 1,
fill: "#cccccc",
opacity: 0.3,
selectable: false,
hasBorders: false,
hasControls: false,
evented: false,
objectCaching: false,
});
activeShape = polygon;
canvas.add(polygon);
}
activeLine = line;
pointsList.push(point);
linesList.push(line);
canvas.add(line);
canvas.add(point);
};
const generatePolygon = (canvas: fabric.Canvas, pointsList): void => {
const points: { x: number; y: number }[] = [];
pointsList.forEach((point) => {
points.push({
x: point.left,
y: point.top,
});
canvas.remove(point);
});
linesList.forEach((line) => {
canvas.remove(line);
});
canvas.remove(activeShape).remove(activeLine);
const polygon = new fabric.Polygon(points, {
id: "polygon-" + new Date().getTime(),
fill: shapeMaskColor,
objectCaching: false,
stroke: borderColor,
strokeWidth: 1,
strokeUniform: true,
noScaleCache: false,
});
if (polygon.width > 5 && polygon.height > 5) {
canvas.add(polygon);
// view undo redo tools
objectAdded(canvas, addedPolygonIds, polygon.id);
}
polygon.on("modified", () => {
modifiedPolygon(canvas, polygon);
saveCanvasState(canvas);
});
toggleDrawPolygon(canvas);
};
const modifiedPolygon = (canvas: fabric.Canvas, polygon: fabric.Polygon): void => {
const matrix = polygon.calcTransformMatrix();
const transformedPoints = polygon.get("points")
.map(function(p) {
return new fabric.Point(p.x - polygon.pathOffset.x, p.y - polygon.pathOffset.y);
})
.map(function(p) {
return fabric.util.transformPoint(p, matrix);
});
const polygon1 = new fabric.Polygon(transformedPoints, {
id: polygon.id,
fill: shapeMaskColor,
objectCaching: false,
stroke: borderColor,
strokeWidth: 1,
strokeUniform: true,
noScaleCache: false,
});
polygon1.on("modified", () => {
modifiedPolygon(canvas, polygon1);
saveCanvasState(canvas);
});
canvas.remove(polygon);
canvas.add(polygon1);
};

View file

@ -0,0 +1,115 @@
// 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 { borderColor, shapeMaskColor, stopDraw } from "./lib";
import { objectAdded } from "./tool-undo-redo";
const addedRectangleIds: string[] = [];
export const drawRectangle = (canvas: fabric.Canvas): void => {
canvas.selectionColor = "rgba(0, 0, 0, 0)";
let rect, isDown, origX, origY;
stopDraw(canvas);
canvas.on("mouse:down", function(o) {
if (o.target) {
return;
}
isDown = true;
const pointer = canvas.getPointer(o.e);
origX = pointer.x;
origY = pointer.y;
rect = new fabric.Rect({
id: "rect-" + new Date().getTime(),
left: origX,
top: origY,
originX: "left",
originY: "top",
width: pointer.x - origX,
height: pointer.y - origY,
angle: 0,
fill: shapeMaskColor,
transparentCorners: false,
selectable: true,
stroke: borderColor,
strokeWidth: 1,
strokeUniform: true,
noScaleCache: false,
});
canvas.add(rect);
});
canvas.on("mouse:move", function(o) {
if (!isDown) return;
const pointer = canvas.getPointer(o.e);
let x = pointer.x;
let y = pointer.y;
if (x < origX) {
rect.set({ originX: "right" });
} else {
rect.set({ originX: "left" });
}
if (y < origY) {
rect.set({ originY: "bottom" });
} else {
rect.set({ originY: "top" });
}
// do not draw outside of canvas
if (x < rect.strokeWidth) {
x = -rect.strokeWidth + 0.5;
}
if (y < rect.strokeWidth) {
y = -rect.strokeWidth + 0.5;
}
if (x >= canvas.width - rect.strokeWidth) {
x = canvas.width - rect.strokeWidth + 0.5;
}
if (y >= canvas.height - rect.strokeWidth) {
y = canvas.height - rect.strokeWidth + 0.5;
}
rect.set({
width: Math.abs(x - rect.left),
height: Math.abs(y - rect.top),
});
canvas.renderAll();
});
canvas.on("mouse:up", function() {
isDown = false;
// probably changed from rectangle to ellipse
if (!rect) {
return;
}
if (rect.width < 5 || rect.height < 5) {
canvas.remove(rect);
return;
}
if (rect.originX === "right") {
rect.set({
originX: "left",
left: rect.left - rect.width + rect.strokeWidth,
});
}
if (rect.originY === "bottom") {
rect.set({
originY: "top",
top: rect.top - rect.height + rect.strokeWidth,
});
}
rect.setCoords();
objectAdded(canvas, addedRectangleIds, rect.id);
});
};

View file

@ -0,0 +1,90 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type fabric from "fabric";
import { mdiRedo, mdiUndo } from "../icons";
/**
* Undo redo for rectangle and ellipse handled here,
* view tool-polygon for handling undo redo in case of polygon
*/
let lockHistory = false;
const undoHistory: string[] = [];
const redoHistory: string[] = [];
const shapeType = ["rect", "ellipse"];
export const undoRedoInit = (canvas: fabric.Canvas): void => {
undoHistory.push(JSON.stringify(canvas));
canvas.on("object:modified", function(o) {
if (lockHistory) return;
if (!validShape(o.target as fabric.Object)) return;
saveCanvasState(canvas);
});
canvas.on("object:removed", function(o) {
if (lockHistory) return;
if (!validShape(o.target as fabric.Object)) return;
saveCanvasState(canvas);
});
};
const validShape = (shape: fabric.Object): boolean => {
if (shape.width <= 5 || shape.height <= 5) return false;
if (shapeType.indexOf(shape.type) === -1) return false;
return true;
};
export const undoAction = (canvas: fabric.Canvas): void => {
if (undoHistory.length > 0) {
lockHistory = true;
if (undoHistory.length > 1) redoHistory.push(undoHistory.pop() as string);
const content = undoHistory[undoHistory.length - 1];
canvas.loadFromJSON(content, function() {
canvas.renderAll();
lockHistory = false;
});
}
};
export const redoAction = (canvas: fabric.Canvas): void => {
if (redoHistory.length > 0) {
lockHistory = true;
const content = redoHistory.pop() as string;
undoHistory.push(content);
canvas.loadFromJSON(content, function() {
canvas.renderAll();
lockHistory = false;
});
}
};
export const objectAdded = (canvas: fabric.Canvas, shapeIdList: string[], shapeId: string): void => {
if (shapeIdList.includes(shapeId)) {
return;
}
shapeIdList.push(shapeId);
saveCanvasState(canvas);
};
export const saveCanvasState = (canvas: fabric.Canvas): void => {
undoHistory.push(JSON.stringify(canvas));
redoHistory.length = 0;
};
export const undoRedoTools = [
{
name: "undo",
icon: mdiUndo,
action: undoAction,
},
{
name: "redo",
icon: mdiRedo,
action: redoAction,
},
];

View file

@ -0,0 +1,17 @@
{
"extends": "../tsconfig.json",
"include": [
"*",
"tools/*",
"notes-toolbar"
],
"references": [
{ "path": "../lib" },
{ "path": "../sveltelib" },
{ "path": "../components" },
{ "path": "../tag-editor" }
],
"compilerOptions": {
"types": ["jest"]
}
}

View file

@ -15,6 +15,7 @@ import DeckConfig = anki.deckconfig;
import Decks = anki.decks; import Decks = anki.decks;
import Generic = anki.generic; import Generic = anki.generic;
import I18n = anki.i18n; import I18n = anki.i18n;
import ImageOcclusion = anki.image_occlusion;
import ImportExport = anki.import_export; import ImportExport = anki.import_export;
import Notes = anki.notes; import Notes = anki.notes;
import Notetypes = anki.notetypes; import Notetypes = anki.notetypes;
@ -81,3 +82,6 @@ export const stats = Stats.StatsService.create(serviceCallback as RPCImpl);
export { Tags }; export { Tags };
export const tags = Tags.TagsService.create(serviceCallback as RPCImpl); export const tags = Tags.TagsService.create(serviceCallback as RPCImpl);
export { ImageOcclusion };
export const imageOcclusion = ImageOcclusion.ImageOcclusionService.create(serviceCallback as RPCImpl);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,194 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
window.addEventListener("load", () => {
window.addEventListener("resize", setupImageCloze);
});
export function setupImageCloze(): void {
const canvas: HTMLCanvasElement = document.querySelector("canvas")! as HTMLCanvasElement;
canvas.style.maxWidth = "100%";
canvas.style.maxHeight = "95vh";
const ctx: CanvasRenderingContext2D = canvas.getContext("2d")!;
const image = document.getElementById("img") as HTMLImageElement;
const size = limitSize({ width: image.naturalWidth, height: image.naturalHeight });
canvas.width = size.width;
canvas.height = size.height;
// set height for div container (used 'relative' in css)
const container = document.getElementById("container") as HTMLDivElement;
container.style.height = `${image.height}px`;
// 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("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("canvas") as HTMLCanvasElement;
const display = canvas.style.display;
if (display === "none") {
canvas.style.display = "unset";
} else {
canvas.style.display = "none";
}
};

View file

@ -10,9 +10,11 @@ import "css-browser-selector/css_browser_selector.min";
export { default as $, default as jQuery } from "jquery/dist/jquery"; export { default as $, default as jQuery } from "jquery/dist/jquery";
import { mutateNextCardStates } from "./answering"; import { mutateNextCardStates } from "./answering";
import { setupImageCloze } from "./image_occlusion";
globalThis.anki = globalThis.anki || {}; globalThis.anki = globalThis.anki || {};
globalThis.anki.mutateNextCardStates = mutateNextCardStates; globalThis.anki.mutateNextCardStates = mutateNextCardStates;
globalThis.anki.setupImageCloze = setupImageCloze;
import { bridgeCommand } from "@tslib/bridgecommand"; import { bridgeCommand } from "@tslib/bridgecommand";

View file

@ -9,6 +9,8 @@
// When all clients are using reviewer.js directly, we can get rid of this. // When all clients are using reviewer.js directly, we can get rid of this.
import { mutateNextCardStates } from "./answering"; import { mutateNextCardStates } from "./answering";
import { setupImageCloze } from "./image_occlusion";
globalThis.anki = globalThis.anki || {}; globalThis.anki = globalThis.anki || {};
globalThis.anki.mutateNextCardStates = mutateNextCardStates; globalThis.anki.mutateNextCardStates = mutateNextCardStates;
globalThis.anki.setupImageCloze = setupImageCloze;

281
yarn.lock
View file

@ -620,6 +620,21 @@
dependencies: dependencies:
lodash "^4.17.21" lodash "^4.17.21"
"@mapbox/node-pre-gyp@^1.0.0":
version "1.0.10"
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz#8e6735ccebbb1581e5a7e652244cadc8a844d03c"
integrity sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==
dependencies:
detect-libc "^2.0.0"
https-proxy-agent "^5.0.0"
make-dir "^3.1.0"
node-fetch "^2.6.7"
nopt "^5.0.0"
npmlog "^5.0.1"
rimraf "^3.0.2"
semver "^7.3.5"
tar "^6.1.11"
"@mdi/svg@^7.0.96": "@mdi/svg@^7.0.96":
version "7.0.96" version "7.0.96"
resolved "https://registry.yarnpkg.com/@mdi/svg/-/svg-7.0.96.tgz#c7275a318da8594337243368c6b4dca6a90154f6" resolved "https://registry.yarnpkg.com/@mdi/svg/-/svg-7.0.96.tgz#c7275a318da8594337243368c6b4dca6a90154f6"
@ -1334,6 +1349,13 @@ ajv@^8.0.1:
require-from-string "^2.0.2" require-from-string "^2.0.2"
uri-js "^4.2.2" uri-js "^4.2.2"
amator@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/amator/-/amator-1.1.0.tgz#08c6b60bc93aec2b61bbfc0c4d677d30323cc0f1"
integrity sha512-V5+aH8pe+Z3u/UG3L3pG3BaFQGXAyXHVQDroRwjPHdh08bcUEchAVsU1MCuJSCaU5o60wTK6KaE6te5memzgYw==
dependencies:
bezier-easing "^2.0.3"
ansi-colors@^4.1.1: ansi-colors@^4.1.1:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
@ -1378,6 +1400,19 @@ anymatch@^3.0.3, anymatch@~3.1.2:
normalize-path "^3.0.0" normalize-path "^3.0.0"
picomatch "^2.0.4" picomatch "^2.0.4"
"aproba@^1.0.3 || ^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc"
integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==
are-we-there-yet@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c"
integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==
dependencies:
delegates "^1.0.0"
readable-stream "^3.6.0"
argparse@^1.0.7: argparse@^1.0.7:
version "1.0.10" version "1.0.10"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
@ -1507,6 +1542,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
bezier-easing@^2.0.3:
version "2.1.0"
resolved "https://registry.yarnpkg.com/bezier-easing/-/bezier-easing-2.1.0.tgz#c04dfe8b926d6ecaca1813d69ff179b7c2025d86"
integrity sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==
binary-extensions@^2.0.0: binary-extensions@^2.0.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
@ -1610,6 +1650,15 @@ caniuse-lite@^1.0.30001251, caniuse-lite@^1.0.30001317, caniuse-lite@^1.0.300014
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz#e7c59bd1bc518fae03a4656be442ce6c4887a795" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz#e7c59bd1bc518fae03a4656be442ce6c4887a795"
integrity sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ== integrity sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ==
canvas@^2.8.0:
version "2.11.0"
resolved "https://registry.yarnpkg.com/canvas/-/canvas-2.11.0.tgz#7f0c3e9ae94cf469269b5d3a7963a7f3a9936434"
integrity sha512-bdTjFexjKJEwtIo0oRx8eD4G2yWoUOXP9lj279jmQ2zMnTQhT8C3512OKz3s+ZOaQlLbE7TuVvRDYDB3Llyy5g==
dependencies:
"@mapbox/node-pre-gyp" "^1.0.0"
nan "^2.17.0"
simple-get "^3.0.3"
catharsis@^0.9.0: catharsis@^0.9.0:
version "0.9.0" version "0.9.0"
resolved "https://registry.yarnpkg.com/catharsis/-/catharsis-0.9.0.tgz#40382a168be0e6da308c277d3a2b3eb40c7d2121" resolved "https://registry.yarnpkg.com/catharsis/-/catharsis-0.9.0.tgz#40382a168be0e6da308c277d3a2b3eb40c7d2121"
@ -1659,6 +1708,11 @@ character-entities@^2.0.2:
optionalDependencies: optionalDependencies:
fsevents "~2.3.2" fsevents "~2.3.2"
chownr@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
ci-info@^2.0.0: ci-info@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
@ -1722,6 +1776,11 @@ color-name@~1.1.4:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
color-support@^1.1.2:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==
combined-stream@^1.0.8: combined-stream@^1.0.8:
version "1.0.8" version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
@ -1739,6 +1798,11 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
console-control-strings@^1.0.0, console-control-strings@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==
convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
version "1.8.0" version "1.8.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369"
@ -2084,6 +2148,13 @@ decimal.js@^10.3.1:
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783"
integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==
decompress-response@^4.2.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986"
integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==
dependencies:
mimic-response "^2.0.0"
dedent-js@^1.0.1: dedent-js@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/dedent-js/-/dedent-js-1.0.1.tgz#bee5fb7c9e727d85dffa24590d10ec1ab1255305" resolved "https://registry.yarnpkg.com/dedent-js/-/dedent-js-1.0.1.tgz#bee5fb7c9e727d85dffa24590d10ec1ab1255305"
@ -2123,11 +2194,21 @@ delayed-stream@~1.0.0:
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
delegates@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==
detect-indent@^6.0.0: detect-indent@^6.0.0:
version "6.1.0" version "6.1.0"
resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6"
integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==
detect-libc@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd"
integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==
detect-newline@^3.0.0: detect-newline@^3.0.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
@ -2675,6 +2756,14 @@ expect@^28.0.0-alpha.7:
jest-matcher-utils "^28.0.0-alpha.7" jest-matcher-utils "^28.0.0-alpha.7"
jest-message-util "^28.0.0-alpha.7" jest-message-util "^28.0.0-alpha.7"
fabric@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/fabric/-/fabric-5.3.0.tgz#199297b6409e3a6279c16c1166da2b2a9e3e8b9b"
integrity sha512-AVayKuzWoXM5cTn7iD3yNWBlfEa8r1tHaOe2g8NsZrmWKAHjryTxT/j6f9ncRfOWOF0I1Ci1AId3y78cC+GExQ==
optionalDependencies:
canvas "^2.8.0"
jsdom "^19.0.0"
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3" version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
@ -2797,6 +2886,13 @@ fs-extra@^7.0.1:
jsonfile "^4.0.0" jsonfile "^4.0.0"
universalify "^0.1.0" universalify "^0.1.0"
fs-minipass@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
dependencies:
minipass "^3.0.0"
fs.realpath@^1.0.0: fs.realpath@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@ -2822,6 +2918,21 @@ fuse.js@^6.6.2:
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-6.6.2.tgz#fe463fed4b98c0226ac3da2856a415576dc9a111" resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-6.6.2.tgz#fe463fed4b98c0226ac3da2856a415576dc9a111"
integrity sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA== integrity sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==
gauge@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395"
integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==
dependencies:
aproba "^1.0.3 || ^2.0.0"
color-support "^1.1.2"
console-control-strings "^1.0.0"
has-unicode "^2.0.1"
object-assign "^4.1.1"
signal-exit "^3.0.0"
string-width "^4.2.3"
strip-ansi "^6.0.1"
wide-align "^1.1.2"
gemoji@^7.1.0: gemoji@^7.1.0:
version "7.1.0" version "7.1.0"
resolved "https://registry.yarnpkg.com/gemoji/-/gemoji-7.1.0.tgz#165403777681a9690d649aabd104da037bdd7739" resolved "https://registry.yarnpkg.com/gemoji/-/gemoji-7.1.0.tgz#165403777681a9690d649aabd104da037bdd7739"
@ -2960,6 +3071,11 @@ has-tostringtag@^1.0.0:
dependencies: dependencies:
has-symbols "^1.0.2" has-symbols "^1.0.2"
has-unicode@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==
has@^1.0.3: has@^1.0.3:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
@ -3059,7 +3175,7 @@ inflight@^1.0.4:
once "^1.3.0" once "^1.3.0"
wrappy "1" wrappy "1"
inherits@2: inherits@2, inherits@^2.0.3:
version "2.0.4" version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@ -3941,7 +4057,7 @@ magic-string@^0.25.7:
dependencies: dependencies:
sourcemap-codec "^1.4.8" sourcemap-codec "^1.4.8"
make-dir@^3.0.0: make-dir@^3.0.0, make-dir@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
@ -4026,6 +4142,11 @@ mimic-fn@^2.1.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
mimic-response@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43"
integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==
min-indent@^1.0.0: min-indent@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
@ -4055,6 +4176,26 @@ minimist@^1.2.5, minimist@^1.2.6:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
minipass@^3.0.0:
version "3.3.6"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a"
integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==
dependencies:
yallist "^4.0.0"
minipass@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.0.3.tgz#00bfbaf1e16e35e804f4aa31a7c1f6b8d9f0ee72"
integrity sha512-OW2r4sQ0sI+z5ckEt5c1Tri4xTgZwYDxpE54eqWlQloQRoWtXjqt9udJ5Z4dSv7wK+nfFI7FRXyCpBSft+gpFw==
minizlib@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
dependencies:
minipass "^3.0.0"
yallist "^4.0.0"
mkdirp@^0.5.1: mkdirp@^0.5.1:
version "0.5.6" version "0.5.6"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
@ -4062,7 +4203,7 @@ mkdirp@^0.5.1:
dependencies: dependencies:
minimist "^1.2.6" minimist "^1.2.6"
mkdirp@^1.0.4: mkdirp@^1.0.3, mkdirp@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
@ -4087,11 +4228,21 @@ ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
nan@^2.17.0:
version "2.17.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
natural-compare@^1.4.0: natural-compare@^1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
ngraph.events@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/ngraph.events/-/ngraph.events-1.2.2.tgz#3ceb92d676a04a4e7ce60a09fa8e17a4f0346d7f"
integrity sha512-JsUbEOzANskax+WSYiAPETemLWYXmixuPAlmZmhIbIj6FH/WDgEGCGnRwUQBK0GjOnVm8Ui+e5IJ+5VZ4e32eQ==
nice-try@^1.0.4: nice-try@^1.0.4:
version "1.0.5" version "1.0.5"
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
@ -4105,6 +4256,13 @@ no-case@^3.0.4:
lower-case "^2.0.2" lower-case "^2.0.2"
tslib "^2.0.3" tslib "^2.0.3"
node-fetch@^2.6.7:
version "2.6.9"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6"
integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==
dependencies:
whatwg-url "^5.0.0"
node-int64@^0.4.0: node-int64@^0.4.0:
version "0.4.0" version "0.4.0"
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
@ -4149,11 +4307,26 @@ npm-run-path@^4.0.1:
dependencies: dependencies:
path-key "^3.0.0" path-key "^3.0.0"
npmlog@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0"
integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==
dependencies:
are-we-there-yet "^2.0.0"
console-control-strings "^1.1.0"
gauge "^3.0.0"
set-blocking "^2.0.0"
nwsapi@^2.2.0: nwsapi@^2.2.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==
object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
object-inspect@^1.11.0, object-inspect@^1.9.0: object-inspect@^1.11.0, object-inspect@^1.9.0:
version "1.12.0" version "1.12.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0"
@ -4183,7 +4356,7 @@ object.values@^1.1.5:
define-properties "^1.1.3" define-properties "^1.1.3"
es-abstract "^1.19.1" es-abstract "^1.19.1"
once@^1.3.0: once@^1.3.0, once@^1.3.1:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
@ -4286,6 +4459,15 @@ p-try@^2.0.0:
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
panzoom@^9.4.3:
version "9.4.3"
resolved "https://registry.yarnpkg.com/panzoom/-/panzoom-9.4.3.tgz#195c4031bb643f2e6c42f1de0ca87cc10e224042"
integrity sha512-xaxCpElcRbQsUtIdwlrZA90P90+BHip4Vda2BC8MEb4tkI05PmR6cKECdqUCZ85ZvBHjpI9htJrZBxV5Gp/q/w==
dependencies:
amator "^1.1.0"
ngraph.events "^1.2.2"
wheel "^1.0.0"
parent-module@^1.0.0: parent-module@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
@ -4526,6 +4708,15 @@ read-package-json@^4.0.0:
normalize-package-data "^3.0.0" normalize-package-data "^3.0.0"
npm-normalize-package-bin "^1.0.0" npm-normalize-package-bin "^1.0.0"
readable-stream@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
dependencies:
inherits "^2.0.3"
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
readdir-scoped-modules@^1.0.0: readdir-scoped-modules@^1.0.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309" resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309"
@ -4653,6 +4844,11 @@ safe-buffer@~5.1.1:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
"safer-buffer@>= 2.1.2 < 3.0.0": "safer-buffer@>= 2.1.2 < 3.0.0":
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
@ -4715,6 +4911,11 @@ semver@^7.1.2:
dependencies: dependencies:
lru-cache "^6.0.0" lru-cache "^6.0.0"
set-blocking@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
shebang-command@^1.2.0: shebang-command@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@ -4748,11 +4949,25 @@ side-channel@^1.0.4:
get-intrinsic "^1.0.2" get-intrinsic "^1.0.2"
object-inspect "^1.9.0" object-inspect "^1.9.0"
signal-exit@^3.0.3, signal-exit@^3.0.7: signal-exit@^3.0.0, signal-exit@^3.0.3, signal-exit@^3.0.7:
version "3.0.7" version "3.0.7"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
simple-concat@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==
simple-get@^3.0.3:
version "3.1.1"
resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.1.tgz#cc7ba77cfbe761036fbfce3d021af25fc5584d55"
integrity sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==
dependencies:
decompress-response "^4.2.0"
once "^1.3.1"
simple-concat "^1.0.0"
sisteransi@^1.0.5: sisteransi@^1.0.5:
version "1.0.5" version "1.0.5"
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
@ -4894,7 +5109,7 @@ string-length@^4.0.1:
char-regex "^1.0.2" char-regex "^1.0.2"
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -4919,6 +5134,13 @@ string.prototype.trimstart@^1.0.4:
call-bind "^1.0.2" call-bind "^1.0.2"
define-properties "^1.1.3" define-properties "^1.1.3"
string_decoder@^1.1.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
dependencies:
safe-buffer "~5.2.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1: strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
@ -5048,6 +5270,18 @@ table@^6.0.9:
string-width "^4.2.3" string-width "^4.2.3"
strip-ansi "^6.0.1" strip-ansi "^6.0.1"
tar@^6.1.11:
version "6.1.13"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.13.tgz#46e22529000f612180601a6fe0680e7da508847b"
integrity sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==
dependencies:
chownr "^2.0.0"
fs-minipass "^2.0.0"
minipass "^4.0.0"
minizlib "^2.1.1"
mkdirp "^1.0.3"
yallist "^4.0.0"
terminal-link@^2.0.0: terminal-link@^2.0.0:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994"
@ -5122,6 +5356,11 @@ tr46@^3.0.0:
dependencies: dependencies:
punycode "^2.1.1" punycode "^2.1.1"
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
treeify@^1.1.0: treeify@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/treeify/-/treeify-1.1.0.tgz#4e31c6a463accd0943879f30667c4fdaff411bb8" resolved "https://registry.yarnpkg.com/treeify/-/treeify-1.1.0.tgz#4e31c6a463accd0943879f30667c4fdaff411bb8"
@ -5246,6 +5485,11 @@ uri-js@^4.2.2:
dependencies: dependencies:
punycode "^2.1.0" punycode "^2.1.0"
util-deprecate@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
v8-compile-cache@^2.0.3: v8-compile-cache@^2.0.3:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
@ -5289,6 +5533,11 @@ walker@^1.0.7:
dependencies: dependencies:
makeerror "1.0.12" makeerror "1.0.12"
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
webidl-conversions@^7.0.0: webidl-conversions@^7.0.0:
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a"
@ -5314,6 +5563,19 @@ whatwg-url@^10.0.0:
tr46 "^3.0.0" tr46 "^3.0.0"
webidl-conversions "^7.0.0" webidl-conversions "^7.0.0"
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
dependencies:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
wheel@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/wheel/-/wheel-1.0.0.tgz#6cf46e06a854181adb8649228077f8b0d5c574ce"
integrity sha512-XiCMHibOiqalCQ+BaNSwRoZ9FDTAvOsXxGHXChBugewDj7HC8VBIER71dEOiRH1fSdLbRCQzngKTSiZ06ZQzeA==
which-boxed-primitive@^1.0.2: which-boxed-primitive@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
@ -5339,6 +5601,13 @@ which@^2.0.1:
dependencies: dependencies:
isexe "^2.0.0" isexe "^2.0.0"
wide-align@^1.1.2:
version "1.1.5"
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3"
integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==
dependencies:
string-width "^1.0.2 || 2 || 3 || 4"
word-wrap@^1.2.3, word-wrap@~1.2.3: word-wrap@^1.2.3, word-wrap@~1.2.3:
version "1.2.3" version "1.2.3"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"