mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
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:
parent
c691c9bcf6
commit
2bf134dc72
60 changed files with 4750 additions and 7 deletions
|
@ -334,6 +334,18 @@ fn build_and_check_pages(build: &mut Build) -> Result<()> {
|
|||
":sass"
|
||||
],
|
||||
)?;
|
||||
build_page(
|
||||
"image-occlusion",
|
||||
true,
|
||||
inputs![
|
||||
//
|
||||
":ts:lib",
|
||||
":ts:components",
|
||||
":ts:sveltelib",
|
||||
":ts:tag-editor",
|
||||
":sass"
|
||||
],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -108,6 +108,11 @@ importing-tag-updated-notes = Tag updated notes
|
|||
importing-file = File
|
||||
importing-match-scope = Match scope
|
||||
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.
|
||||
|
||||
|
|
|
@ -35,3 +35,18 @@ notetypes-note-types = Note Types
|
|||
notetypes-options = Options
|
||||
notetypes-please-add-another-note-type-first = Please add another note type first.
|
||||
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?
|
||||
|
|
|
@ -74,6 +74,7 @@
|
|||
"codemirror": "^5.63.1",
|
||||
"css-browser-selector": "^0.6.5",
|
||||
"d3": "^7.0.0",
|
||||
"fabric": "^5.3.0",
|
||||
"fuse.js": "^6.6.2",
|
||||
"gemoji": "^7.1.0",
|
||||
"intl-pluralrules": "^1.2.2",
|
||||
|
@ -82,6 +83,7 @@
|
|||
"lodash-es": "^4.17.21",
|
||||
"marked": "^4.0.0",
|
||||
"mathjax": "^3.1.2",
|
||||
"panzoom": "^9.4.3",
|
||||
"protobufjs": "^7"
|
||||
},
|
||||
"resolutions": {
|
||||
|
|
|
@ -31,6 +31,7 @@ enum ServiceIndex {
|
|||
SERVICE_INDEX_LINKS = 15;
|
||||
SERVICE_INDEX_IMPORT_EXPORT = 16;
|
||||
SERVICE_INDEX_ANKIDROID = 17;
|
||||
SERVICE_INDEX_IMAGE_OCCLUSION = 18;
|
||||
}
|
||||
|
||||
message BackendInit {
|
||||
|
|
67
proto/anki/image_occlusion.proto
Normal file
67
proto/anki/image_occlusion.proto
Normal 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;
|
||||
}
|
||||
}
|
|
@ -124,6 +124,7 @@ message StockNotetype {
|
|||
BASIC_OPTIONAL_REVERSED = 2;
|
||||
BASIC_TYPING = 3;
|
||||
CLOZE = 4;
|
||||
IMAGE_OCCLUSION = 5;
|
||||
}
|
||||
|
||||
Kind kind = 1;
|
||||
|
|
|
@ -10,6 +10,7 @@ from anki import (
|
|||
collection_pb2,
|
||||
config_pb2,
|
||||
generic_pb2,
|
||||
image_occlusion_pb2,
|
||||
import_export_pb2,
|
||||
links_pb2,
|
||||
search_pb2,
|
||||
|
@ -40,6 +41,9 @@ CsvMetadata = import_export_pb2.CsvMetadata
|
|||
DupeResolution = CsvMetadata.DupeResolution
|
||||
Delimiter = import_export_pb2.CsvMetadata.Delimiter
|
||||
TtsVoice = card_rendering_pb2.AllTtsVoicesResponse.TtsVoice
|
||||
ImageData = image_occlusion_pb2.ImageData
|
||||
AddImageOcclusionNoteRequest = image_occlusion_pb2.AddImageOcclusionNoteRequest
|
||||
ImageClozeNoteResponse = image_occlusion_pb2.ImageClozeNoteResponse
|
||||
|
||||
import copy
|
||||
import os
|
||||
|
@ -456,6 +460,46 @@ class Collection(DeprecatedNamesMixin):
|
|||
def import_json_string(self, json: str) -> ImportLogWithChanges:
|
||||
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
|
||||
##########################################################################
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import anki.config_pb2
|
|||
import anki.deckconfig_pb2
|
||||
import anki.decks_pb2
|
||||
import anki.i18n_pb2
|
||||
import anki.image_occlusion_pb2
|
||||
import anki.import_export_pb2
|
||||
import anki.links_pb2
|
||||
import anki.media_pb2
|
||||
|
@ -187,6 +188,7 @@ service_modules = dict(
|
|||
MEDIA=anki.media_pb2,
|
||||
LINKS=anki.links_pb2,
|
||||
IMPORT_EXPORT=anki.import_export_pb2,
|
||||
IMAGE_OCCLUSION=anki.image_occlusion_pb2,
|
||||
)
|
||||
|
||||
for service in anki.backend_pb2.ServiceIndex.DESCRIPTOR.values:
|
||||
|
@ -238,6 +240,7 @@ import anki.card_rendering_pb2
|
|||
import anki.tags_pb2
|
||||
import anki.media_pb2
|
||||
import anki.import_export_pb2
|
||||
import anki.image_occlusion_pb2
|
||||
|
||||
class RustBackendGenerated:
|
||||
def _run_command(self, service: int, method: int, input: Any) -> bytes:
|
||||
|
|
|
@ -479,6 +479,11 @@ exposed_backend_list = [
|
|||
"set_graph_preferences",
|
||||
# TagsService
|
||||
"complete_tag",
|
||||
# ImageOcclusionService
|
||||
"get_image_for_occlusion",
|
||||
"add_image_occlusion_note",
|
||||
"get_image_cloze_note",
|
||||
"update_image_occlusion_note",
|
||||
]
|
||||
|
||||
|
||||
|
|
55
rslib/src/backend/image_occlusion.rs
Normal file
55
rslib/src/backend/image_occlusion.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ mod decks;
|
|||
mod error;
|
||||
mod generic;
|
||||
mod i18n;
|
||||
mod image_occlusion;
|
||||
mod import_export;
|
||||
mod links;
|
||||
mod media;
|
||||
|
@ -48,6 +49,7 @@ use self::config::ConfigService;
|
|||
use self::deckconfig::DeckConfigService;
|
||||
use self::decks::DecksService;
|
||||
use self::i18n::I18nService;
|
||||
use self::image_occlusion::ImageOcclusionService;
|
||||
use self::import_export::ImportExportService;
|
||||
use self::links::LinksService;
|
||||
use self::media::MediaService;
|
||||
|
@ -142,6 +144,9 @@ impl Backend {
|
|||
ServiceIndex::Collection => CollectionService::run_method(self, method, input),
|
||||
ServiceIndex::Cards => CardsService::run_method(self, method, input),
|
||||
ServiceIndex::ImportExport => ImportExportService::run_method(self, method, input),
|
||||
ServiceIndex::ImageOcclusion => {
|
||||
ImageOcclusionService::run_method(self, method, input)
|
||||
}
|
||||
})
|
||||
.map_err(|err| {
|
||||
let backend_err = err.into_protobuf(&self.tr);
|
||||
|
|
|
@ -15,6 +15,7 @@ use nom::IResult;
|
|||
use regex::Captures;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::image_occlusion::imageocclusion::get_image_cloze_data;
|
||||
use crate::latex::contains_latex;
|
||||
use crate::template::RenderContext;
|
||||
use crate::text::strip_html_preserving_entities;
|
||||
|
@ -138,6 +139,13 @@ impl ExtractedCloze<'_> {
|
|||
|
||||
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<'_>> {
|
||||
|
@ -212,6 +220,14 @@ fn reveal_cloze(
|
|||
) {
|
||||
let active = cloze.ordinal == cloze_ord;
|
||||
*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) {
|
||||
(true, true) => {
|
||||
// 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> {
|
||||
let mut buf = String::new();
|
||||
let mut active_cloze_found_in_text = false;
|
||||
|
@ -530,4 +562,18 @@ mod test {
|
|||
fn non_latin() {
|
||||
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>"#,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ pub enum FileOp {
|
|||
CopyFrom(PathBuf),
|
||||
Persist,
|
||||
Sync,
|
||||
Metadata,
|
||||
/// For legacy errors without any context.
|
||||
Unknown,
|
||||
}
|
||||
|
@ -57,6 +58,7 @@ impl FileIoError {
|
|||
FileOp::CopyFrom(p) => format!("copy from '{}' to", p.to_string_lossy()),
|
||||
FileOp::Persist => "persist".into(),
|
||||
FileOp::Sync => "sync".into(),
|
||||
FileOp::Metadata => "get metadata".into(),
|
||||
},
|
||||
self.path.to_string_lossy(),
|
||||
self.source
|
||||
|
|
41
rslib/src/image_occlusion/image_occlusion_styling.css
Normal file
41
rslib/src/image_occlusion/image_occlusion_styling.css
Normal 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);
|
||||
}
|
246
rslib/src/image_occlusion/imagedata.rs
Normal file
246
rslib/src/image_occlusion/imagedata.rs
Normal 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
|
||||
}
|
||||
}
|
107
rslib/src/image_occlusion/imageocclusion.rs
Normal file
107
rslib/src/image_occlusion/imageocclusion.rs
Normal 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]]" "#,
|
||||
);
|
||||
}
|
5
rslib/src/image_occlusion/mod.rs
Normal file
5
rslib/src/image_occlusion/mod.rs
Normal 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;
|
|
@ -93,6 +93,14 @@ fn read_locked_db_file_inner(path: impl AsRef<Path>) -> std::io::Result<Vec<u8>>
|
|||
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> {
|
||||
NamedTempFile::new().context(FileIoSnafu {
|
||||
path: std::env::temp_dir(),
|
||||
|
|
|
@ -17,6 +17,7 @@ pub mod decks;
|
|||
pub mod error;
|
||||
pub mod findreplace;
|
||||
pub mod i18n;
|
||||
pub mod image_occlusion;
|
||||
pub mod import_export;
|
||||
mod io;
|
||||
pub mod latex;
|
||||
|
|
|
@ -9,7 +9,7 @@ mod notetypechange;
|
|||
mod render;
|
||||
mod schema11;
|
||||
mod schemachange;
|
||||
mod stock;
|
||||
pub(crate) mod stock;
|
||||
mod templates;
|
||||
pub(crate) mod undo;
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ pub enum Op {
|
|||
CreateCustomStudy,
|
||||
EmptyFilteredDeck,
|
||||
FindAndReplace,
|
||||
ImageOcclusion,
|
||||
Import,
|
||||
RebuildFilteredDeck,
|
||||
RemoveDeck,
|
||||
|
@ -90,6 +91,7 @@ impl Op {
|
|||
Op::Custom(name) => name.into(),
|
||||
Op::ChangeNotetype => tr.browsing_change_notetype(),
|
||||
Op::SkipUndo => return "".to_string(),
|
||||
Op::ImageOcclusion => tr.notetypes_image_occlusion_name(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ protobuf!(deckconfig, "deckconfig");
|
|||
protobuf!(decks, "decks");
|
||||
protobuf!(generic, "generic");
|
||||
protobuf!(i18n, "i18n");
|
||||
protobuf!(image_occlusion, "image_occlusion");
|
||||
protobuf!(import_export, "import_export");
|
||||
protobuf!(links, "links");
|
||||
protobuf!(media, "media");
|
||||
|
|
106
ts/image-occlusion/ImageOcclusionPage.svelte
Normal file
106
ts/image-occlusion/ImageOcclusionPage.svelte
Normal 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>
|
81
ts/image-occlusion/MaskEditor.svelte
Normal file
81
ts/image-occlusion/MaskEditor.svelte
Normal 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>
|
93
ts/image-occlusion/Notes.svelte
Normal file
93
ts/image-occlusion/Notes.svelte
Normal 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>
|
97
ts/image-occlusion/SideToolbar.svelte
Normal file
97
ts/image-occlusion/SideToolbar.svelte
Normal 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>
|
67
ts/image-occlusion/StickyFooter.svelte
Normal file
67
ts/image-occlusion/StickyFooter.svelte
Normal 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>
|
28
ts/image-occlusion/Tags.svelte
Normal file
28
ts/image-occlusion/Tags.svelte
Normal 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>
|
61
ts/image-occlusion/Toast.svelte
Normal file
61
ts/image-occlusion/Toast.svelte
Normal 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>
|
194
ts/image-occlusion/TopToolbar.svelte
Normal file
194
ts/image-occlusion/TopToolbar.svelte
Normal 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>
|
166
ts/image-occlusion/generate.ts
Normal file
166
ts/image-occlusion/generate.ts
Normal 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 });
|
||||
}
|
||||
};
|
33
ts/image-occlusion/icons.ts
Normal file
33
ts/image-occlusion/icons.ts
Normal 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";
|
19
ts/image-occlusion/image-occlusion-base.scss
Normal file
19
ts/image-occlusion/image-occlusion-base.scss
Normal 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;
|
||||
}
|
56
ts/image-occlusion/index.ts
Normal file
56
ts/image-occlusion/index.ts
Normal 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
61
ts/image-occlusion/lib.ts
Normal 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,
|
||||
}),
|
||||
);
|
||||
}
|
143
ts/image-occlusion/mask-editor.ts
Normal file
143
ts/image-occlusion/mask-editor.ts
Normal 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,
|
||||
};
|
||||
};
|
29
ts/image-occlusion/notes-toolbar/MoreTools.svelte
Normal file
29
ts/image-occlusion/notes-toolbar/MoreTools.svelte
Normal 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}
|
13
ts/image-occlusion/notes-toolbar/NotesToolbar.svelte
Normal file
13
ts/image-occlusion/notes-toolbar/NotesToolbar.svelte
Normal 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} />
|
58
ts/image-occlusion/notes-toolbar/TextFormatting.svelte
Normal file
58
ts/image-occlusion/notes-toolbar/TextFormatting.svelte
Normal 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>
|
6
ts/image-occlusion/notes-toolbar/index.ts
Normal file
6
ts/image-occlusion/notes-toolbar/index.ts
Normal 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;
|
25
ts/image-occlusion/notes-toolbar/lib.ts
Normal file
25
ts/image-occlusion/notes-toolbar/lib.ts
Normal 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();
|
||||
}
|
||||
};
|
11
ts/image-occlusion/store.ts
Normal file
11
ts/image-occlusion/store.ts
Normal 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([""]);
|
8
ts/image-occlusion/tools/index.ts
Normal file
8
ts/image-occlusion/tools/index.ts
Normal 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 };
|
208
ts/image-occlusion/tools/lib.ts
Normal file
208
ts/image-occlusion/tools/lib.ts
Normal 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();
|
||||
};
|
104
ts/image-occlusion/tools/more-tools.ts
Normal file
104
ts/image-occlusion/tools/more-tools.ts
Normal 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,
|
||||
},
|
||||
];
|
131
ts/image-occlusion/tools/shape-generate.ts
Normal file
131
ts/image-occlusion/tools/shape-generate.ts
Normal 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;
|
||||
}
|
||||
};
|
210
ts/image-occlusion/tools/tool-aligns.ts
Normal file
210
ts/image-occlusion/tools/tool-aligns.ts
Normal 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();
|
||||
});
|
||||
};
|
33
ts/image-occlusion/tools/tool-buttons.ts
Normal file
33
ts/image-occlusion/tools/tool-buttons.ts
Normal 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,
|
||||
},
|
||||
];
|
121
ts/image-occlusion/tools/tool-ellipse.ts
Normal file
121
ts/image-occlusion/tools/tool-ellipse.ts
Normal 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);
|
||||
});
|
||||
};
|
233
ts/image-occlusion/tools/tool-polygon.ts
Normal file
233
ts/image-occlusion/tools/tool-polygon.ts
Normal 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);
|
||||
};
|
115
ts/image-occlusion/tools/tool-rect.ts
Normal file
115
ts/image-occlusion/tools/tool-rect.ts
Normal 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);
|
||||
});
|
||||
};
|
90
ts/image-occlusion/tools/tool-undo-redo.ts
Normal file
90
ts/image-occlusion/tools/tool-undo-redo.ts
Normal 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,
|
||||
},
|
||||
];
|
17
ts/image-occlusion/tsconfig.json
Normal file
17
ts/image-occlusion/tsconfig.json
Normal 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"]
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ import DeckConfig = anki.deckconfig;
|
|||
import Decks = anki.decks;
|
||||
import Generic = anki.generic;
|
||||
import I18n = anki.i18n;
|
||||
import ImageOcclusion = anki.image_occlusion;
|
||||
import ImportExport = anki.import_export;
|
||||
import Notes = anki.notes;
|
||||
import Notetypes = anki.notetypes;
|
||||
|
@ -81,3 +82,6 @@ export const stats = Stats.StatsService.create(serviceCallback as RPCImpl);
|
|||
|
||||
export { Tags };
|
||||
export const tags = Tags.TagsService.create(serviceCallback as RPCImpl);
|
||||
|
||||
export { ImageOcclusion };
|
||||
export const imageOcclusion = ImageOcclusion.ImageOcclusionService.create(serviceCallback as RPCImpl);
|
||||
|
|
981
ts/licenses.json
981
ts/licenses.json
File diff suppressed because it is too large
Load diff
194
ts/reviewer/image_occlusion.ts
Normal file
194
ts/reviewer/image_occlusion.ts
Normal 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";
|
||||
}
|
||||
};
|
|
@ -10,9 +10,11 @@ import "css-browser-selector/css_browser_selector.min";
|
|||
export { default as $, default as jQuery } from "jquery/dist/jquery";
|
||||
|
||||
import { mutateNextCardStates } from "./answering";
|
||||
import { setupImageCloze } from "./image_occlusion";
|
||||
|
||||
globalThis.anki = globalThis.anki || {};
|
||||
globalThis.anki.mutateNextCardStates = mutateNextCardStates;
|
||||
globalThis.anki.setupImageCloze = setupImageCloze;
|
||||
|
||||
import { bridgeCommand } from "@tslib/bridgecommand";
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@
|
|||
// When all clients are using reviewer.js directly, we can get rid of this.
|
||||
|
||||
import { mutateNextCardStates } from "./answering";
|
||||
import { setupImageCloze } from "./image_occlusion";
|
||||
|
||||
globalThis.anki = globalThis.anki || {};
|
||||
globalThis.anki.mutateNextCardStates = mutateNextCardStates;
|
||||
globalThis.anki.setupImageCloze = setupImageCloze;
|
||||
|
|
281
yarn.lock
281
yarn.lock
|
@ -620,6 +620,21 @@
|
|||
dependencies:
|
||||
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":
|
||||
version "7.0.96"
|
||||
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"
|
||||
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:
|
||||
version "4.1.1"
|
||||
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"
|
||||
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:
|
||||
version "1.0.10"
|
||||
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"
|
||||
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:
|
||||
version "2.2.0"
|
||||
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"
|
||||
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:
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/catharsis/-/catharsis-0.9.0.tgz#40382a168be0e6da308c277d3a2b3eb40c7d2121"
|
||||
|
@ -1659,6 +1708,11 @@ character-entities@^2.0.2:
|
|||
optionalDependencies:
|
||||
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:
|
||||
version "2.0.0"
|
||||
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"
|
||||
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:
|
||||
version "1.0.8"
|
||||
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"
|
||||
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:
|
||||
version "1.8.0"
|
||||
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"
|
||||
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:
|
||||
version "1.0.1"
|
||||
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"
|
||||
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:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6"
|
||||
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:
|
||||
version "3.1.0"
|
||||
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-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:
|
||||
version "3.1.3"
|
||||
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"
|
||||
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:
|
||||
version "1.0.0"
|
||||
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"
|
||||
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:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/gemoji/-/gemoji-7.1.0.tgz#165403777681a9690d649aabd104da037bdd7739"
|
||||
|
@ -2960,6 +3071,11 @@ has-tostringtag@^1.0.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
|
||||
|
@ -3059,7 +3175,7 @@ inflight@^1.0.4:
|
|||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2:
|
||||
inherits@2, inherits@^2.0.3:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
@ -3941,7 +4057,7 @@ magic-string@^0.25.7:
|
|||
dependencies:
|
||||
sourcemap-codec "^1.4.8"
|
||||
|
||||
make-dir@^3.0.0:
|
||||
make-dir@^3.0.0, make-dir@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
|
||||
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"
|
||||
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:
|
||||
version "1.0.1"
|
||||
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"
|
||||
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:
|
||||
version "0.5.6"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
|
||||
|
@ -4062,7 +4203,7 @@ mkdirp@^0.5.1:
|
|||
dependencies:
|
||||
minimist "^1.2.6"
|
||||
|
||||
mkdirp@^1.0.4:
|
||||
mkdirp@^1.0.3, mkdirp@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
||||
|
@ -4087,11 +4228,21 @@ ms@^2.1.1:
|
|||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
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:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
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:
|
||||
version "1.0.5"
|
||||
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"
|
||||
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:
|
||||
version "0.4.0"
|
||||
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:
|
||||
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:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
|
||||
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:
|
||||
version "1.12.0"
|
||||
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"
|
||||
es-abstract "^1.19.1"
|
||||
|
||||
once@^1.3.0:
|
||||
once@^1.3.0, once@^1.3.1:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
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"
|
||||
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:
|
||||
version "1.0.1"
|
||||
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"
|
||||
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:
|
||||
version "1.1.0"
|
||||
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"
|
||||
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":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
|
@ -4715,6 +4911,11 @@ semver@^7.1.2:
|
|||
dependencies:
|
||||
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:
|
||||
version "1.2.0"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
|
||||
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:
|
||||
version "1.0.5"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
|
@ -4919,6 +5134,13 @@ string.prototype.trimstart@^1.0.4:
|
|||
call-bind "^1.0.2"
|
||||
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:
|
||||
version "6.0.1"
|
||||
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"
|
||||
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:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994"
|
||||
|
@ -5122,6 +5356,11 @@ tr46@^3.0.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/treeify/-/treeify-1.1.0.tgz#4e31c6a463accd0943879f30667c4fdaff411bb8"
|
||||
|
@ -5246,6 +5485,11 @@ uri-js@^4.2.2:
|
|||
dependencies:
|
||||
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:
|
||||
version "2.3.0"
|
||||
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:
|
||||
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:
|
||||
version "7.0.0"
|
||||
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"
|
||||
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:
|
||||
version "1.0.2"
|
||||
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:
|
||||
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:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
|
||||
|
|
Loading…
Reference in a new issue