diff --git a/cvat-canvas/.eslintrc.js b/cvat-canvas/.eslintrc.js
index 923ddd26..9d123380 100644
--- a/cvat-canvas/.eslintrc.js
+++ b/cvat-canvas/.eslintrc.js
@@ -5,40 +5,48 @@
*/
module.exports = {
- "env": {
- "node": true,
- "browser": true,
- "es6": true,
+ 'env': {
+ 'node': true,
+ 'browser': true,
+ 'es6': true,
},
- "parserOptions": {
- "parser": "@typescript-eslint/parser",
- "sourceType": "module",
- "ecmaVersion": 6,
+ 'parserOptions': {
+ 'parser': '@typescript-eslint/parser',
+ 'sourceType': 'module',
+ 'ecmaVersion': 6,
},
- "plugins": [
- "security",
- "no-unsanitized",
- "no-unsafe-innerhtml",
- "@typescript-eslint",
+ 'plugins': [
+ 'security',
+ 'no-unsanitized',
+ 'no-unsafe-innerhtml',
+ '@typescript-eslint',
],
- "extends": [
- "eslint:recommended",
- "plugin:security/recommended",
- "plugin:no-unsanitized/DOM",
- "plugin:@typescript-eslint/recommended",
- "airbnb",
+ 'extends': [
+ 'eslint:recommended',
+ 'plugin:security/recommended',
+ 'plugin:no-unsanitized/DOM',
+ 'plugin:@typescript-eslint/recommended',
+ 'airbnb',
],
- "rules": {
- "no-new": [0],
- "class-methods-use-this": [0],
- "no-plusplus": [0],
- "no-restricted-syntax": [0, {"selector": "ForOfStatement"}],
- "no-continue": [0],
- "security/detect-object-injection": 0,
- "indent": ["warn", 4],
- "no-useless-constructor": 0,
- "func-names": [0],
- "no-console": [0], // this rule deprecates console.log, console.warn etc. because "it is not good in production code"
- "@typescript-eslint/no-explicit-any": [0],
+ 'rules': {
+ 'no-new': [0],
+ 'class-methods-use-this': [0],
+ 'no-plusplus': [0],
+ 'no-restricted-syntax': [0, {'selector': 'ForOfStatement'}],
+ 'no-continue': [0],
+ 'security/detect-object-injection': 0,
+ 'indent': ['warn', 4],
+ 'no-useless-constructor': 0,
+ 'func-names': [0],
+ 'no-console': [0], // this rule deprecates console.log, console.warn etc. because 'it is not good in production code'
+ '@typescript-eslint/no-explicit-any': [0],
+ 'lines-between-class-members': [0],
},
+ 'settings': {
+ 'import/resolver': {
+ 'node': {
+ 'extensions': ['.ts', '.js', '.json'],
+ },
+ },
+ },
};
diff --git a/cvat-canvas/README.md b/cvat-canvas/README.md
index 783c5b46..911aaaff 100644
--- a/cvat-canvas/README.md
+++ b/cvat-canvas/README.md
@@ -50,7 +50,7 @@ Canvas itself handles:
All methods are sync.
```ts
- html(): HTMLElement;
+ html(): HTMLDivElement;
setup(frameData: FrameData, objectStates: ObjectState): void;
activate(clientID: number, attributeID?: number): void;
rotate(direction: Rotation): void;
@@ -76,6 +76,7 @@ All methods are sync.
```canvas_shape_drawing```
- Tags has a class ```canvas_tag```
- Canvas image has ID ```canvas_image```
+- Grid on the canvas has ID ```canvas_grid_pattern```
### Events
diff --git a/cvat-canvas/dist/canvas.css b/cvat-canvas/dist/canvas.css
new file mode 100644
index 00000000..db6093c4
--- /dev/null
+++ b/cvat-canvas/dist/canvas.css
@@ -0,0 +1,82 @@
+.canvas_hidden {
+ display: none;
+}
+
+#canvas_wrapper {
+ width: 100%;
+ height: 80%;
+ border: 1px black solid;
+ border-radius: 5px;
+ background-color: #B0C4DE;
+ overflow: hidden;
+ position: relative;
+}
+
+#canvas_rotation_wrapper {
+ width: 100%;
+ height: 100%;
+ position: relative;
+}
+
+#canvas_loading_animation {
+ z-index: 1;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ transform-origin: top left;
+}
+
+#canvas_loading_circle {
+ fill-opacity: 0;
+ stroke: #09c;
+ stroke-width: 3px;
+ stroke-dasharray: 50;
+ animation: loadingAnimation 1s linear infinite;
+}
+
+#canvas_text_content {
+ position: absolute;
+ z-index: 3;
+ transform-origin: center center;
+ pointer-events: none;
+ width: 100%;
+ height: 100%;
+}
+
+#canvas_background {
+ position: absolute;
+ z-index: 0;
+ background-repeat: no-repeat;
+ transform-origin: top left;
+ width: 100%;
+ height: 100%;
+}
+
+#canvas_grid {
+ position: absolute;
+ z-index: 2;
+ transform-origin: top left;
+ pointer-events: none;
+ width: 100%;
+ height: 100%;
+}
+
+#canvas_grid_pattern {
+ opacity: 1;
+ stroke: white;
+}
+
+#canvas_content {
+ position: absolute;
+ z-index: 2;
+ outline: 10px solid black;
+ transform-origin: top left;
+ width: 100%;
+ height: 100%;
+}
+
+@keyframes loadingAnimation {
+ 0% {stroke-dashoffset: 1; stroke: #09c;}
+ 50% {stroke-dashoffset: 100; stroke: #f44;}
+ 100% {stroke-dashoffset: 300; stroke: #09c;}
+}
\ No newline at end of file
diff --git a/cvat-canvas/dist/index.html b/cvat-canvas/dist/index.html
new file mode 100644
index 00000000..ac996e9d
--- /dev/null
+++ b/cvat-canvas/dist/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+ CVAT-CANVAS Dev Server
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/cvat-canvas/dist/index.js b/cvat-canvas/dist/index.js
new file mode 100644
index 00000000..b13d7c6d
--- /dev/null
+++ b/cvat-canvas/dist/index.js
@@ -0,0 +1,28 @@
+window.addEventListener('DOMContentLoaded', async () => {
+ await window.cvat.server.login('admin', 'nimda760');
+ const [job] = (await window.cvat.jobs.get({ jobID: 21 }));
+ const canvas = new window.canvas.Canvas();
+ const htmlContainer = window.document.getElementById('htmlContainer');
+
+
+ htmlContainer.appendChild(canvas.html());
+
+ let frame = 0;
+ const callback = async () => {
+ canvas.fit();
+ const frameData = await job.frames.get(frame);
+ canvas.setup(frameData, []);
+ frame += 1;
+
+ if (frame > 50) {
+ frame = 0;
+ }
+ };
+
+ canvas.html().addEventListener('canvas.setup', async () => {
+ setTimeout(callback, 30);
+ });
+
+ const frameData = await job.frames.get(frame);
+ canvas.setup(frameData, []);
+});
diff --git a/cvat-canvas/package.json b/cvat-canvas/package.json
index 0e6a808c..e6b980f3 100644
--- a/cvat-canvas/package.json
+++ b/cvat-canvas/package.json
@@ -5,7 +5,7 @@
"main": "babel.config.js",
"scripts": {
"build": "tsc && webpack --config ./webpack.config.js",
- "server": "nodemon --watch config --exec 'webpack-dev-server --config ./webpack.config.js --open'"
+ "server": "nodemon --watch config --exec 'webpack-dev-server --config ./webpack.config.js --mode=development --open'"
},
"author": "Intel",
"license": "MIT",
@@ -17,10 +17,11 @@
"@babel/core": "^7.5.5",
"@babel/preset-env": "^7.5.5",
"@babel/preset-typescript": "^7.3.3",
- "eslint": "^6.1.0",
+ "@types/node": "^12.6.8",
"@typescript-eslint/eslint-plugin": "^1.13.0",
"@typescript-eslint/parser": "^1.13.0",
"babel-loader": "^8.0.6",
+ "eslint": "^6.1.0",
"nodemon": "^1.19.1",
"typescript": "^3.5.3",
"webpack": "^4.36.1",
diff --git a/cvat-canvas/src/canvas.ts b/cvat-canvas/src/canvas.ts
index 41a6a565..f2d47abe 100644
--- a/cvat-canvas/src/canvas.ts
+++ b/cvat-canvas/src/canvas.ts
@@ -3,11 +3,12 @@
* SPDX-License-Identifier: MIT
*/
-/* eslint-disable */
-// Temporary disable eslint
+import { CanvasModel, CanvasModelImpl, Rotation } from './canvasModel';
+import { CanvasController, CanvasControllerImpl } from './canvasController';
+import { CanvasView, CanvasViewImpl } from './canvasView';
-interface CanvasInterface {
- html(): HTMLElement;
+interface Canvas {
+ html(): HTMLDivElement;
setup(frameData: any, objectStates: any[]): void;
activate(clientID: number, attributeID?: number): void;
rotate(direction: Rotation): void;
@@ -15,7 +16,7 @@ interface CanvasInterface {
fit(): void;
grid(stepX: number, stepY: number): void;
- draw(shapeType: string, numberOfPoints: number, initialState: any): any;
+ draw(enabled?: boolean, shapeType?: string, numberOfPoints?: number, initialState?: any): any;
split(enabled?: boolean): any;
group(enabled?: boolean): any;
merge(enabled?: boolean): any;
@@ -23,61 +24,67 @@ interface CanvasInterface {
cancel(): void;
}
-export enum Rotation {
- CLOCKWISE90,
- ANTICLOCKWISE90,
-}
+class CanvasImpl implements Canvas {
+ private model: CanvasModel;
+ private controller: CanvasController;
+ private view: CanvasView;
-export class Canvas implements CanvasInterface {
public constructor() {
- return this;
+ this.model = new CanvasModelImpl();
+ this.controller = new CanvasControllerImpl(this.model);
+ this.view = new CanvasViewImpl(this.model, this.controller);
}
- public html(): HTMLElement {
- throw new Error('Method not implemented.');
+ public html(): HTMLDivElement {
+ return this.view.html();
}
public setup(frameData: any, objectStates: any[]): void {
- throw new Error('Method not implemented.');
+ this.model.setup(frameData, objectStates);
}
public activate(clientID: number, attributeID: number = null): void {
- throw new Error('Method not implemented.');
+ this.model.activate(clientID, attributeID);
}
public rotate(direction: Rotation): void {
- throw new Error('Method not implemented.');
+ this.model.rotate(direction);
}
public focus(clientID: number, padding: number = 0): void {
- throw new Error('Method not implemented.');
+ this.model.focus(clientID, padding);
}
public fit(): void {
- throw new Error('Method not implemented.');
+ this.model.fit();
}
public grid(stepX: number, stepY: number): void {
- throw new Error('Method not implemented.');
+ this.model.grid(stepX, stepY);
}
- public draw(shapeType: string, numberOfPoints: number, initialState: any): any {
- throw new Error('Method not implemented.');
+ public draw(enabled: boolean = false, shapeType: string = '', numberOfPoints: number = 0, initialState: any = null): any {
+ return this.model.draw(enabled, shapeType, numberOfPoints, initialState);
}
public split(enabled: boolean = false): any {
- throw new Error('Method not implemented.');
+ return this.model.split(enabled);
}
public group(enabled: boolean = false): any {
- throw new Error('Method not implemented.');
+ return this.model.group(enabled);
}
public merge(enabled: boolean = false): any {
- throw new Error('Method not implemented.');
+ return this.model.merge(enabled);
}
public cancel(): void {
- throw new Error('Method not implemented.');
+ this.model.cancel();
}
}
+
+export {
+ CanvasImpl as Canvas,
+ Rotation,
+};
diff --git a/cvat-canvas/src/canvasController.ts b/cvat-canvas/src/canvasController.ts
new file mode 100644
index 00000000..4e2d9892
--- /dev/null
+++ b/cvat-canvas/src/canvasController.ts
@@ -0,0 +1,78 @@
+/*
+* Copyright (C) 2018 Intel Corporation
+* SPDX-License-Identifier: MIT
+*/
+
+import {
+ CanvasModel,
+ Geometry,
+ Size,
+ Position,
+} from './canvasModel';
+
+
+export interface CanvasController {
+ readonly geometry: Geometry;
+ canvasSize: Size;
+
+ zoom(x: number, y: number, direction: number): void;
+ enableDrag(x: number, y: number): void;
+ drag(x: number, y: number): void;
+ disableDrag(): void;
+
+ fit(): void;
+}
+
+export class CanvasControllerImpl implements CanvasController {
+ private model: CanvasModel;
+ private lastDragPosition: Position;
+ private isDragging: boolean;
+
+ public constructor(model: CanvasModel) {
+ this.model = model;
+ }
+
+ public get geometry(): Geometry {
+ return this.model.geometry;
+ }
+
+ public zoom(x: number, y: number, direction: number): void {
+ this.model.zoom(x, y, direction);
+ }
+
+ public fit(): void {
+ this.model.fit();
+ }
+
+ public set canvasSize(value: Size) {
+ this.model.canvasSize = value;
+ }
+
+ public get canvasSize(): Size {
+ return this.model.canvasSize;
+ }
+
+ public enableDrag(x: number, y: number): void {
+ this.lastDragPosition = {
+ x,
+ y,
+ };
+ this.isDragging = true;
+ }
+
+ public drag(x: number, y: number): void {
+ if (this.isDragging) {
+ const topOffset: number = y - this.lastDragPosition.y;
+ const leftOffset: number = x - this.lastDragPosition.x;
+ this.lastDragPosition = {
+ x,
+ y,
+ };
+ this.model.move(topOffset, leftOffset);
+ }
+ }
+
+ public disableDrag(): void {
+ this.isDragging = false;
+ }
+}
diff --git a/cvat-canvas/src/canvasModel.ts b/cvat-canvas/src/canvasModel.ts
new file mode 100644
index 00000000..05eda925
--- /dev/null
+++ b/cvat-canvas/src/canvasModel.ts
@@ -0,0 +1,255 @@
+/*
+* Copyright (C) 2018 Intel Corporation
+* SPDX-License-Identifier: MIT
+*/
+
+import { MasterImpl } from './master';
+
+export interface Size {
+ width: number;
+ height: number;
+}
+
+export interface Position {
+ x: number;
+ y: number;
+}
+
+export interface Geometry {
+ image: Size;
+ canvas: Size;
+ top: number;
+ left: number;
+ scale: number;
+ offset: number;
+}
+
+export enum FrameZoom {
+ MIN = 0.1,
+ MAX = 10,
+}
+
+export enum Rotation {
+ CLOCKWISE90,
+ ANTICLOCKWISE90,
+}
+
+export enum UpdateReasons {
+ IMAGE = 'image',
+ ZOOM = 'zoom',
+ FIT = 'fit',
+ MOVE = 'move',
+}
+
+export interface CanvasModel extends MasterImpl {
+ image: string;
+ geometry: Geometry;
+ imageSize: Size;
+ canvasSize: Size;
+
+ zoom(x: number, y: number, direction: number): void;
+ move(topOffset: number, leftOffset: number): void;
+
+ setup(frameData: any, objectStates: any[]): void;
+ activate(clientID: number, attributeID: number): void;
+ rotate(direction: Rotation): void;
+ focus(clientID: number, padding: number): void;
+ fit(): void;
+ grid(stepX: number, stepY: number): void;
+
+ draw(enabled: boolean, shapeType: string, numberOfPoints: number, initialState: any): any;
+ split(enabled: boolean): any;
+ group(enabled: boolean): any;
+ merge(enabled: boolean): any;
+
+ cancel(): void;
+}
+
+export class CanvasModelImpl extends MasterImpl implements CanvasModel {
+ private data: {
+ image: string;
+ imageSize: Size;
+ canvasSize: Size;
+ imageOffset: number;
+ scale: number;
+ top: number;
+ left: number;
+ };
+
+ public constructor() {
+ super();
+
+ this.data = {
+ image: '',
+ imageSize: {
+ width: 0,
+ height: 0,
+ },
+ canvasSize: {
+ width: 0,
+ height: 0,
+ },
+ imageOffset: 0,
+ scale: 1,
+ top: 0,
+ left: 0,
+ };
+ }
+
+ public zoom(x: number, y: number, direction: number): void {
+ const oldScale: number = this.data.scale;
+ const newScale: number = direction > 0 ? oldScale * 6 / 5 : oldScale * 5 / 6;
+ this.data.scale = Math.min(Math.max(newScale, FrameZoom.MIN), FrameZoom.MAX);
+ this.data.left += (x * (oldScale / this.data.scale - 1)) * this.data.scale;
+ this.data.top += (y * (oldScale / this.data.scale - 1)) * this.data.scale;
+
+ this.notify(UpdateReasons.ZOOM);
+ }
+
+ public move(topOffset: number, leftOffset: number): void {
+ this.data.top += topOffset;
+ this.data.left += leftOffset;
+ this.notify(UpdateReasons.MOVE);
+ }
+
+
+ public setup(frameData: any, objectStates: any[]): void {
+ frameData.data(
+ (): void => {
+ this.data.image = '';
+ this.notify(UpdateReasons.IMAGE);
+ },
+ ).then((data: string): void => {
+ this.data.imageSize = {
+ width: (frameData.width as number),
+ height: (frameData.height as number),
+ };
+
+ this.data.image = data;
+ this.notify(UpdateReasons.IMAGE);
+ }).catch((exception: any): void => {
+ console.log(exception.toString());
+ });
+
+ console.log(objectStates);
+ }
+
+ public activate(clientID: number, attributeID: number): void {
+ console.log(clientID, attributeID);
+ }
+
+ public rotate(direction: Rotation): void {
+ console.log(direction);
+ }
+
+ public focus(clientID: number, padding: number): void {
+ console.log(clientID, padding);
+ }
+
+ public fit(): void {
+ this.data.scale = Math.min(
+ this.data.canvasSize.width / this.data.imageSize.width,
+ this.data.canvasSize.height / this.data.imageSize.height,
+ );
+
+ this.data.scale = Math.min(
+ Math.max(this.data.scale, FrameZoom.MIN),
+ FrameZoom.MAX,
+ );
+
+ this.data.top = (this.data.canvasSize.height
+ - this.data.imageSize.height * this.data.scale) / 2;
+ this.data.left = (this.data.canvasSize.width
+ - this.data.imageSize.width * this.data.scale) / 2;
+
+ this.notify(UpdateReasons.FIT);
+ }
+
+ public grid(stepX: number, stepY: number): void {
+ console.log(stepX, stepY);
+ }
+
+ public draw(enabled: boolean, shapeType: string,
+ numberOfPoints: number, initialState: any): any {
+ return {
+ enabled,
+ shapeType,
+ numberOfPoints,
+ initialState,
+ };
+ }
+
+ public split(enabled: boolean): any {
+ return enabled;
+ }
+
+ public group(enabled: boolean): any {
+ return enabled;
+ }
+
+ public merge(enabled: boolean): any {
+ return enabled;
+ }
+
+ public cancel(): void {
+
+ }
+
+ public get geometry(): Geometry {
+ return {
+ image: {
+ width: this.data.imageSize.width,
+ height: this.data.imageSize.height,
+ },
+ canvas: {
+ width: this.data.canvasSize.width,
+ height: this.data.canvasSize.height,
+ },
+ top: this.data.top,
+ left: this.data.left,
+ scale: this.data.scale,
+ offset: this.data.imageOffset,
+ };
+ }
+
+ public get image(): string {
+ return this.data.image;
+ }
+
+ public set imageSize(value: Size) {
+ this.data.imageSize = {
+ width: value.width,
+ height: value.height,
+ };
+ }
+
+ public get imageSize(): Size {
+ return {
+ width: this.data.imageSize.width,
+ height: this.data.imageSize.height,
+ };
+ }
+
+ public set canvasSize(value: Size) {
+ this.data.canvasSize = {
+ width: value.width,
+ height: value.height,
+ };
+
+ this.data.imageOffset = Math.floor(Math.max(
+ this.data.canvasSize.height / FrameZoom.MIN,
+ this.data.canvasSize.width / FrameZoom.MIN,
+ ));
+ }
+
+ public get canvasSize(): Size {
+ return {
+ width: this.data.canvasSize.width,
+ height: this.data.canvasSize.height,
+ };
+ }
+}
+
+// TODO List:
+// 2) Rotate image
+// 3) Draw objects
diff --git a/cvat-canvas/src/canvasView.ts b/cvat-canvas/src/canvasView.ts
new file mode 100644
index 00000000..dc7d3c80
--- /dev/null
+++ b/cvat-canvas/src/canvasView.ts
@@ -0,0 +1,223 @@
+/*
+* Copyright (C) 2018 Intel Corporation
+* SPDX-License-Identifier: MIT
+*/
+
+import { CanvasModel, UpdateReasons, Geometry } from './canvasModel';
+import { Listener, Master } from './master';
+import { CanvasController } from './canvasController';
+
+export interface CanvasView {
+ html(): HTMLDivElement;
+}
+
+interface HTMLAttribute {
+ [index: string]: string;
+}
+
+
+function translateToSVG(svg: SVGSVGElement, points: number[]): number[] {
+ const output = [];
+ const transformationMatrix = svg.getScreenCTM().inverse();
+ let pt = svg.createSVGPoint();
+ for (let i = 0; i < points.length; i += 2) {
+ [pt.x] = points;
+ [, pt.y] = points;
+ pt = pt.matrixTransform(transformationMatrix);
+ output.push(pt.x, pt.y);
+ }
+
+ return output;
+}
+
+function translateFromSVG(svg: SVGSVGElement, points: number[]): number[] {
+ const output = [];
+ const transformationMatrix = svg.getScreenCTM();
+ let pt = svg.createSVGPoint();
+ for (let i = 0; i < points.length; i += 2) {
+ [pt.x] = points;
+ [, pt.y] = points;
+ pt = pt.matrixTransform(transformationMatrix);
+ output.push(pt.x, pt.y);
+ }
+
+ return output;
+}
+
+export class CanvasViewImpl implements CanvasView, Listener {
+ private loadingAnimation: SVGSVGElement;
+ private text: SVGSVGElement;
+ private background: SVGSVGElement;
+ private grid: SVGSVGElement;
+ private content: SVGSVGElement;
+ private rotationWrapper: HTMLDivElement;
+ private canvas: HTMLDivElement;
+ private gridPath: SVGPathElement;
+ private controller: CanvasController;
+
+ public constructor(model: CanvasModel & Master, controller: CanvasController) {
+ this.controller = controller;
+
+ // Create HTML elements
+ this.loadingAnimation = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.text = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.background = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+
+ this.grid = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.gridPath = window.document.createElementNS('http://www.w3.org/2000/svg', 'path');
+
+ this.content = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.rotationWrapper = window.document.createElement('div');
+ this.canvas = window.document.createElement('div');
+
+ const loadingCircle: SVGCircleElement = window.document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ const gridDefs: SVGDefsElement = window.document.createElementNS('http://www.w3.org/2000/svg', 'defs');
+ const gridPattern: SVGPatternElement = window.document.createElementNS('http://www.w3.org/2000/svg', 'pattern');
+ const gridRect: SVGRectElement = window.document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+
+ // Setup loading animation
+ this.loadingAnimation.setAttribute('id', 'canvas_loading_animation');
+ loadingCircle.setAttribute('id', 'canvas_loading_circle');
+ loadingCircle.setAttribute('r', '30');
+ loadingCircle.setAttribute('cx', '50%');
+ loadingCircle.setAttribute('cy', '50%');
+
+ // Setup grid
+ this.grid.setAttribute('id', 'canvas_grid');
+ this.grid.setAttribute('version', '2');
+ this.gridPath.setAttribute('d', 'M 1000 0 L 0 0 0 1000');
+ this.gridPath.setAttribute('fill', 'none');
+ this.gridPath.setAttribute('stroke-width', '1.5');
+ gridPattern.setAttribute('id', 'canvas_grid_pattern');
+ gridPattern.setAttribute('width', '100');
+ gridPattern.setAttribute('height', '100');
+ gridPattern.setAttribute('patternUnits', 'userSpaceOnUse');
+ gridRect.setAttribute('width', '100%');
+ gridRect.setAttribute('height', '100%');
+ gridRect.setAttribute('fill', 'url(#canvas_grid_pattern)');
+
+
+ // Setup content
+ this.text.setAttribute('id', 'canvas_text_content');
+ this.background.setAttribute('id', 'canvas_background');
+ this.content.setAttribute('id', 'canvas_content');
+
+ // Setup wrappers
+ this.rotationWrapper.setAttribute('id', 'canvas_rotation_wrapper');
+ this.canvas.setAttribute('id', 'canvas_wrapper');
+
+ // Unite created HTML elements together
+ this.loadingAnimation.appendChild(loadingCircle);
+ this.grid.appendChild(gridDefs);
+ this.grid.appendChild(gridRect);
+
+ gridDefs.appendChild(gridPattern);
+ gridPattern.appendChild(this.gridPath);
+
+ this.rotationWrapper.appendChild(this.loadingAnimation);
+ this.rotationWrapper.appendChild(this.text);
+ this.rotationWrapper.appendChild(this.background);
+ this.rotationWrapper.appendChild(this.grid);
+ this.rotationWrapper.appendChild(this.content);
+
+ this.canvas.appendChild(this.rotationWrapper);
+
+ // A little hack to get size after first mounting
+ // http://www.backalleycoder.com/2012/04/25/i-want-a-damnodeinserted/
+ const self = this;
+ const canvasFirstMounted = (event: AnimationEvent): void => {
+ if (event.animationName === 'loadingAnimation') {
+ self.controller.canvasSize = {
+ width: self.rotationWrapper.clientWidth,
+ height: self.rotationWrapper.clientHeight,
+ };
+
+ self.rotationWrapper.removeEventListener('animationstart', canvasFirstMounted);
+ }
+ };
+
+ this.canvas.addEventListener('animationstart', canvasFirstMounted);
+ this.content.addEventListener('dblclick', (): void => {
+ self.controller.fit();
+ });
+
+ this.content.addEventListener('mousedown', (event): void => {
+ self.controller.enableDrag(event.clientX, event.clientY);
+ });
+
+ this.content.addEventListener('mousemove', (event): void => {
+ self.controller.drag(event.clientX, event.clientY);
+ });
+
+ window.document.addEventListener('mouseup', (): void => {
+ self.controller.disableDrag();
+ });
+
+ this.content.addEventListener('wheel', (event): void => {
+ const point = translateToSVG(self.background, [event.clientX, event.clientY]);
+ self.controller.zoom(point[0], point[1], event.deltaY > 0 ? -1 : 1);
+ event.preventDefault();
+ });
+
+ model.subscribe(this);
+ }
+
+ public notify(model: CanvasModel & Master, reason: UpdateReasons): void {
+ function transform(geometry: Geometry): void {
+ for (const obj of [this.background, this.grid, this.loadingAnimation, this.content]) {
+ obj.style.transform = `scale(${geometry.scale})`;
+ }
+ }
+
+ function resize(geometry: Geometry): void {
+ for (const obj of [this.background, this.grid, this.loadingAnimation]) {
+ obj.style.width = `${geometry.image.width}`;
+ obj.style.height = `${geometry.image.height}`;
+ }
+
+ for (const obj of [this.content, this.text]) {
+ obj.style.width = `${geometry.image.width + geometry.offset * 2}`;
+ obj.style.height = `${geometry.image.height + geometry.offset * 2}`;
+ }
+ }
+
+ function move(geometry: Geometry): void {
+ for (const obj of [this.background, this.grid, this.loadingAnimation]) {
+ obj.style.top = `${geometry.top}`;
+ obj.style.left = `${geometry.left}`;
+ }
+
+ for (const obj of [this.content, this.text]) {
+ obj.style.top = `${geometry.top - geometry.offset * geometry.scale}`;
+ obj.style.left = `${geometry.left - geometry.offset * geometry.scale}`;
+ }
+
+ this.content.style.transform = `scale(${geometry.scale})`;
+ }
+
+ const { geometry } = this.controller;
+ if (reason === UpdateReasons.IMAGE) {
+ if (!model.image.length) {
+ this.loadingAnimation.classList.remove('canvas_hidden');
+ } else {
+ this.loadingAnimation.classList.add('canvas_hidden');
+ this.background.style.backgroundImage = `url("${model.image}")`;
+ move.call(this, geometry);
+ resize.call(this, geometry);
+ transform.call(this, geometry);
+ const event: Event = new Event('canvas.setup');
+ this.canvas.dispatchEvent(event);
+ }
+ } else if (reason === UpdateReasons.ZOOM || reason === UpdateReasons.FIT) {
+ move.call(this, geometry);
+ resize.call(this, geometry);
+ transform.call(this, geometry);
+ } else if (reason === UpdateReasons.MOVE) {
+ move.call(this, geometry);
+ }
+ }
+
+ public html(): HTMLDivElement {
+ return this.canvas;
+ }
+}
diff --git a/cvat-canvas/src/master.ts b/cvat-canvas/src/master.ts
new file mode 100644
index 00000000..bb4dc8b1
--- /dev/null
+++ b/cvat-canvas/src/master.ts
@@ -0,0 +1,40 @@
+export interface Master {
+ subscribe(listener: Listener): void;
+ unsubscribe(listener: Listener): void;
+ unsubscribeAll(): void;
+ notify(reason: string): void;
+}
+
+export interface Listener {
+ notify(master: Master, reason: string): void;
+}
+
+export class MasterImpl implements Master {
+ private listeners: Listener[];
+
+ public constructor() {
+ this.listeners = [];
+ }
+
+ public subscribe(listener: Listener): void {
+ this.listeners.push(listener);
+ }
+
+ public unsubscribe(listener: Listener): void {
+ for (let i = 0; i < this.listeners.length; i++) {
+ if (this.listeners[i] === listener) {
+ this.listeners.splice(i, 1);
+ }
+ }
+ }
+
+ public unsubscribeAll(): void {
+ this.listeners = [];
+ }
+
+ public notify(reason: string): void {
+ for (const listener of this.listeners) {
+ listener.notify(this, reason);
+ }
+ }
+}
diff --git a/cvat-canvas/tsconfig.json b/cvat-canvas/tsconfig.json
index a15fa72d..959ec02b 100644
--- a/cvat-canvas/tsconfig.json
+++ b/cvat-canvas/tsconfig.json
@@ -1,13 +1,14 @@
{
"compilerOptions": {
"emitDeclarationOnly": true,
- "module": "commonjs",
+ "module": "es6",
+ "target": "es6",
"noImplicitAny": true,
"preserveConstEnums": true,
"declaration": true,
"declarationDir": "dist/declaration"
},
"include": [
- "src/**/*"
+ "src/*.ts"
],
}
diff --git a/cvat-canvas/webpack.config.js b/cvat-canvas/webpack.config.js
index ef230d26..8b40dfa3 100644
--- a/cvat-canvas/webpack.config.js
+++ b/cvat-canvas/webpack.config.js
@@ -14,13 +14,16 @@ module.exports = {
path: path.resolve(__dirname, 'dist'),
filename: 'cvat-canvas.js',
library: 'canvas',
- libraryTarget: 'commonjs',
+ libraryTarget: 'window',
},
devServer: {
contentBase: path.join(__dirname, 'dist'),
- compress: true,
+ compress: false,
inline: true,
- port: 9000,
+ port: 3000,
+ },
+ resolve: {
+ extensions: ['.ts', '.js', '.json'],
},
module: {
rules: [{
diff --git a/cvat-core/package.json b/cvat-core/package.json
index dca386ad..e94bfe4f 100644
--- a/cvat-core/package.json
+++ b/cvat-core/package.json
@@ -28,6 +28,7 @@
},
"dependencies": {
"axios": "^0.18.0",
+ "browser-or-node": "^1.2.1",
"error-stack-parser": "^2.0.2",
"form-data": "^2.5.0",
"jest-config": "^24.8.0",
diff --git a/cvat-core/src/frames.js b/cvat-core/src/frames.js
index 8a672387..67f7a8fb 100644
--- a/cvat-core/src/frames.js
+++ b/cvat-core/src/frames.js
@@ -12,6 +12,7 @@
const PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy');
const { ArgumentError } = require('./exceptions');
+ const { isBrowser, isNode } = require('browser-or-node');
// This is the frames storage
const frameDataCache = {};
@@ -65,29 +66,43 @@
* @memberof module:API.cvat.classes.FrameData
* @instance
* @async
+ * @param {function} [onServerRequest = () => {}]
+ * callback which will be called if data absences local
* @throws {module:API.cvat.exception.ServerError}
* @throws {module:API.cvat.exception.PluginError}
*/
- async data() {
+ async data(onServerRequest = () => {}) {
const result = await PluginRegistry
- .apiWrapper.call(this, FrameData.prototype.data);
+ .apiWrapper.call(this, FrameData.prototype.data, onServerRequest);
return result;
}
}
- FrameData.prototype.data.implementation = async function () {
- if (!(this.number in frameCache[this.tid])) {
- const frame = await serverProxy.frames.getData(this.tid, this.number);
+ FrameData.prototype.data.implementation = async function (onServerRequest) {
+ return new Promise(async (resolve, reject) => {
+ try {
+ if (this.number in frameCache[this.tid]) {
+ resolve(frameCache[this.tid][this.number]);
+ } else {
+ onServerRequest();
+ const frame = await serverProxy.frames.getData(this.tid, this.number);
- if (typeof (module) !== 'undefined' && module.exports) {
- frameCache[this.tid][this.number] = global.Buffer.from(frame, 'binary').toString('base64');
- } else {
- const url = URL.createObjectURL(new Blob([frame]));
- frameCache[this.tid][this.number] = url;
+ if (isNode) {
+ frameCache[this.tid][this.number] = global.Buffer.from(frame, 'binary').toString('base64');
+ resolve(frameCache[this.tid][this.number]);
+ } else if (isBrowser) {
+ const reader = new FileReader();
+ reader.onload = () => {
+ frameCache[this.tid][this.number] = reader.result;
+ resolve(frameCache[this.tid][this.number]);
+ };
+ reader.readAsDataURL(frame);
+ }
+ }
+ } catch (exception) {
+ reject(exception);
}
- }
-
- return frameCache[this.tid][this.number];
+ });
};
async function getFrame(taskID, mode, frame) {