CVAT Canvas updates (#607)

* Canvas VIEW
* Some stubs added
* Changed type of frame storage
* Frame setup
* Big canvas
* Zoom and fit
* Move
* Minimized CVAT version
main
Boris Sekachev 7 years ago committed by Nikita Manovich
parent 5733423b13
commit 85ae933af5

@ -5,40 +5,48 @@
*/ */
module.exports = { module.exports = {
"env": { 'env': {
"node": true, 'node': true,
"browser": true, 'browser': true,
"es6": true, 'es6': true,
}, },
"parserOptions": { 'parserOptions': {
"parser": "@typescript-eslint/parser", 'parser': '@typescript-eslint/parser',
"sourceType": "module", 'sourceType': 'module',
"ecmaVersion": 6, 'ecmaVersion': 6,
}, },
"plugins": [ 'plugins': [
"security", 'security',
"no-unsanitized", 'no-unsanitized',
"no-unsafe-innerhtml", 'no-unsafe-innerhtml',
"@typescript-eslint", '@typescript-eslint',
], ],
"extends": [ 'extends': [
"eslint:recommended", 'eslint:recommended',
"plugin:security/recommended", 'plugin:security/recommended',
"plugin:no-unsanitized/DOM", 'plugin:no-unsanitized/DOM',
"plugin:@typescript-eslint/recommended", 'plugin:@typescript-eslint/recommended',
"airbnb", 'airbnb',
], ],
"rules": { 'rules': {
"no-new": [0], 'no-new': [0],
"class-methods-use-this": [0], 'class-methods-use-this': [0],
"no-plusplus": [0], 'no-plusplus': [0],
"no-restricted-syntax": [0, {"selector": "ForOfStatement"}], 'no-restricted-syntax': [0, {'selector': 'ForOfStatement'}],
"no-continue": [0], 'no-continue': [0],
"security/detect-object-injection": 0, 'security/detect-object-injection': 0,
"indent": ["warn", 4], 'indent': ['warn', 4],
"no-useless-constructor": 0, 'no-useless-constructor': 0,
"func-names": [0], 'func-names': [0],
"no-console": [0], // this rule deprecates console.log, console.warn etc. because "it is not good in production code" '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], '@typescript-eslint/no-explicit-any': [0],
'lines-between-class-members': [0],
}, },
'settings': {
'import/resolver': {
'node': {
'extensions': ['.ts', '.js', '.json'],
},
},
},
}; };

@ -50,7 +50,7 @@ Canvas itself handles:
All methods are sync. All methods are sync.
```ts ```ts
html(): HTMLElement; html(): HTMLDivElement;
setup(frameData: FrameData, objectStates: ObjectState): void; setup(frameData: FrameData, objectStates: ObjectState): void;
activate(clientID: number, attributeID?: number): void; activate(clientID: number, attributeID?: number): void;
rotate(direction: Rotation): void; rotate(direction: Rotation): void;
@ -76,6 +76,7 @@ All methods are sync.
```canvas_shape_drawing``` ```canvas_shape_drawing```
- Tags has a class ```canvas_tag``` - Tags has a class ```canvas_tag```
- Canvas image has ID ```canvas_image``` - Canvas image has ID ```canvas_image```
- Grid on the canvas has ID ```canvas_grid_pattern```
### Events ### Events

@ -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;}
}

@ -0,0 +1,16 @@
<!DOCTYPE HTML>
<html>
<head>
<title> CVAT-CANVAS Dev Server </title>
<script type="text/javascript" src="index.js"></script>
<script type="text/javascript" src="cvat-core.js"></script>
<script type="text/javascript" src="cvat-canvas.js"></script>
<link rel = "stylesheet" type = "text/css" href = "canvas.css" />
</head>
<body>
<div id="htmlContainer" style="width: 1920px;height: 1080px;">
</div>
</body>
</html>

@ -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, []);
});

@ -5,7 +5,7 @@
"main": "babel.config.js", "main": "babel.config.js",
"scripts": { "scripts": {
"build": "tsc && webpack --config ./webpack.config.js", "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", "author": "Intel",
"license": "MIT", "license": "MIT",
@ -17,10 +17,11 @@
"@babel/core": "^7.5.5", "@babel/core": "^7.5.5",
"@babel/preset-env": "^7.5.5", "@babel/preset-env": "^7.5.5",
"@babel/preset-typescript": "^7.3.3", "@babel/preset-typescript": "^7.3.3",
"eslint": "^6.1.0", "@types/node": "^12.6.8",
"@typescript-eslint/eslint-plugin": "^1.13.0", "@typescript-eslint/eslint-plugin": "^1.13.0",
"@typescript-eslint/parser": "^1.13.0", "@typescript-eslint/parser": "^1.13.0",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
"eslint": "^6.1.0",
"nodemon": "^1.19.1", "nodemon": "^1.19.1",
"typescript": "^3.5.3", "typescript": "^3.5.3",
"webpack": "^4.36.1", "webpack": "^4.36.1",

@ -3,11 +3,12 @@
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
*/ */
/* eslint-disable */ import { CanvasModel, CanvasModelImpl, Rotation } from './canvasModel';
// Temporary disable eslint import { CanvasController, CanvasControllerImpl } from './canvasController';
import { CanvasView, CanvasViewImpl } from './canvasView';
interface CanvasInterface { interface Canvas {
html(): HTMLElement; html(): HTMLDivElement;
setup(frameData: any, objectStates: any[]): void; setup(frameData: any, objectStates: any[]): void;
activate(clientID: number, attributeID?: number): void; activate(clientID: number, attributeID?: number): void;
rotate(direction: Rotation): void; rotate(direction: Rotation): void;
@ -15,7 +16,7 @@ interface CanvasInterface {
fit(): void; fit(): void;
grid(stepX: number, stepY: number): 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; split(enabled?: boolean): any;
group(enabled?: boolean): any; group(enabled?: boolean): any;
merge(enabled?: boolean): any; merge(enabled?: boolean): any;
@ -23,61 +24,67 @@ interface CanvasInterface {
cancel(): void; cancel(): void;
} }
export enum Rotation { class CanvasImpl implements Canvas {
CLOCKWISE90, private model: CanvasModel;
ANTICLOCKWISE90, private controller: CanvasController;
} private view: CanvasView;
export class Canvas implements CanvasInterface {
public constructor() { 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 { public html(): HTMLDivElement {
throw new Error('Method not implemented.'); return this.view.html();
} }
public setup(frameData: any, objectStates: any[]): void { 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 { public activate(clientID: number, attributeID: number = null): void {
throw new Error('Method not implemented.'); this.model.activate(clientID, attributeID);
} }
public rotate(direction: Rotation): void { public rotate(direction: Rotation): void {
throw new Error('Method not implemented.'); this.model.rotate(direction);
} }
public focus(clientID: number, padding: number = 0): void { public focus(clientID: number, padding: number = 0): void {
throw new Error('Method not implemented.'); this.model.focus(clientID, padding);
} }
public fit(): void { public fit(): void {
throw new Error('Method not implemented.'); this.model.fit();
} }
public grid(stepX: number, stepY: number): void { 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 { public draw(enabled: boolean = false, shapeType: string = '', numberOfPoints: number = 0, initialState: any = null): any {
throw new Error('Method not implemented.'); return this.model.draw(enabled, shapeType, numberOfPoints, initialState);
} }
public split(enabled: boolean = false): any { public split(enabled: boolean = false): any {
throw new Error('Method not implemented.'); return this.model.split(enabled);
} }
public group(enabled: boolean = false): any { public group(enabled: boolean = false): any {
throw new Error('Method not implemented.'); return this.model.group(enabled);
} }
public merge(enabled: boolean = false): any { public merge(enabled: boolean = false): any {
throw new Error('Method not implemented.'); return this.model.merge(enabled);
} }
public cancel(): void { public cancel(): void {
throw new Error('Method not implemented.'); this.model.cancel();
} }
} }
export {
CanvasImpl as Canvas,
Rotation,
};

@ -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;
}
}

@ -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

@ -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;
}
}

@ -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);
}
}
}

@ -1,13 +1,14 @@
{ {
"compilerOptions": { "compilerOptions": {
"emitDeclarationOnly": true, "emitDeclarationOnly": true,
"module": "commonjs", "module": "es6",
"target": "es6",
"noImplicitAny": true, "noImplicitAny": true,
"preserveConstEnums": true, "preserveConstEnums": true,
"declaration": true, "declaration": true,
"declarationDir": "dist/declaration" "declarationDir": "dist/declaration"
}, },
"include": [ "include": [
"src/**/*" "src/*.ts"
], ],
} }

@ -14,13 +14,16 @@ module.exports = {
path: path.resolve(__dirname, 'dist'), path: path.resolve(__dirname, 'dist'),
filename: 'cvat-canvas.js', filename: 'cvat-canvas.js',
library: 'canvas', library: 'canvas',
libraryTarget: 'commonjs', libraryTarget: 'window',
}, },
devServer: { devServer: {
contentBase: path.join(__dirname, 'dist'), contentBase: path.join(__dirname, 'dist'),
compress: true, compress: false,
inline: true, inline: true,
port: 9000, port: 3000,
},
resolve: {
extensions: ['.ts', '.js', '.json'],
}, },
module: { module: {
rules: [{ rules: [{

@ -28,6 +28,7 @@
}, },
"dependencies": { "dependencies": {
"axios": "^0.18.0", "axios": "^0.18.0",
"browser-or-node": "^1.2.1",
"error-stack-parser": "^2.0.2", "error-stack-parser": "^2.0.2",
"form-data": "^2.5.0", "form-data": "^2.5.0",
"jest-config": "^24.8.0", "jest-config": "^24.8.0",

@ -12,6 +12,7 @@
const PluginRegistry = require('./plugins'); const PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy');
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
const { isBrowser, isNode } = require('browser-or-node');
// This is the frames storage // This is the frames storage
const frameDataCache = {}; const frameDataCache = {};
@ -65,29 +66,43 @@
* @memberof module:API.cvat.classes.FrameData * @memberof module:API.cvat.classes.FrameData
* @instance * @instance
* @async * @async
* @param {function} [onServerRequest = () => {}]
* callback which will be called if data absences local
* @throws {module:API.cvat.exception.ServerError} * @throws {module:API.cvat.exception.ServerError}
* @throws {module:API.cvat.exception.PluginError} * @throws {module:API.cvat.exception.PluginError}
*/ */
async data() { async data(onServerRequest = () => {}) {
const result = await PluginRegistry const result = await PluginRegistry
.apiWrapper.call(this, FrameData.prototype.data); .apiWrapper.call(this, FrameData.prototype.data, onServerRequest);
return result; return result;
} }
} }
FrameData.prototype.data.implementation = async function () { FrameData.prototype.data.implementation = async function (onServerRequest) {
if (!(this.number in frameCache[this.tid])) { return new Promise(async (resolve, reject) => {
const frame = await serverProxy.frames.getData(this.tid, this.number); 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) { if (isNode) {
frameCache[this.tid][this.number] = global.Buffer.from(frame, 'binary').toString('base64'); frameCache[this.tid][this.number] = global.Buffer.from(frame, 'binary').toString('base64');
} else { resolve(frameCache[this.tid][this.number]);
const url = URL.createObjectURL(new Blob([frame])); } else if (isBrowser) {
frameCache[this.tid][this.number] = url; 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) { async function getFrame(taskID, mode, frame) {

Loading…
Cancel
Save