CVAT Canvas updates (#607)
* Canvas VIEW * Some stubs added * Changed type of frame storage * Frame setup * Big canvas * Zoom and fit * Move * Minimized CVAT versionmain
parent
5733423b13
commit
85ae933af5
@ -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, []);
|
||||||
|
});
|
||||||
@ -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"
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue