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) {