diff --git a/package-lock.json b/package-lock.json index ac9d1148cea6528851f2fce6e30c9fd4c76247f0..8931a34d7ed2e87cd8d1e6e7b416d34eb3e2f92e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -437,6 +437,34 @@ "typescript": ">=5.4 <5.6" } }, + "node_modules/@angular/compiler-cli/node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "dev": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@angular/compiler-cli/node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "dev": true, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@angular/core": { "version": "18.2.9", "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.9.tgz", @@ -5154,18 +5182,39 @@ "dev": true }, "node_modules/chokidar": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", - "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, "dependencies": { - "readdirp": "^4.0.1" + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" }, "engines": { - "node": ">= 14.16.0" + "node": ">= 8.10.0" }, "funding": { "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" } }, "node_modules/chownr": { @@ -7990,30 +8039,6 @@ "source-map-support": "^0.5.5" } }, - "node_modules/karma/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, "node_modules/karma/node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -8031,18 +8056,6 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, - "node_modules/karma/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/karma/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -8052,30 +8065,6 @@ "node": ">=8" } }, - "node_modules/karma/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/karma/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/karma/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -10269,16 +10258,27 @@ } }, "node_modules/readdirp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", - "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, "engines": { - "node": ">= 14.16.0" + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" }, "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/reflect-metadata": { @@ -10680,66 +10680,6 @@ } } }, - "node_modules/sass/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/sass/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/sass/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/sass/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/sax": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", @@ -12765,30 +12705,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/webpack-dev-server/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, "node_modules/webpack-dev-server/node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -12809,18 +12725,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/webpack-dev-server/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", @@ -12860,30 +12764,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/webpack-dev-server/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/webpack-dev-server/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/webpack-dev-server/node_modules/rimraf": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", diff --git a/src/app/app.component.html b/src/app/app.component.html index 36093e1879779624f181733152bb55d71a711d3b..67e7bd4cd6a1695885480e285649088d90ea7180 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,336 +1 @@ - - - - - - - - - - - -
-
-
- -

Hello, {{ title }}

-

Congratulations! Your app is running. 🎉

-
- -
-
- @for (item of [ - { title: 'Explore the Docs', link: 'https://angular.dev' }, - { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' }, - { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' }, - { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' }, - { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' }, - ]; track item.title) { - - {{ item.title }} - - - - - } -
- -
-
-
- - - - - - - - - - diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index dc39edb5f23a35f788cba146dd7337127ba6c5ab..d79406c50ca38c291daeb26f6500aeca111c8da8 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,3 +1,6 @@ import { Routes } from '@angular/router'; +import { AnnotationComponent } from './components/annotation/annotation.component'; -export const routes: Routes = []; +export const routes: Routes = [ + { path: "", component: AnnotationComponent } +]; diff --git a/src/app/components/annotation/annotation.component.html b/src/app/components/annotation/annotation.component.html new file mode 100644 index 0000000000000000000000000000000000000000..4ab76e3404de240906069707006079c9a1de6712 --- /dev/null +++ b/src/app/components/annotation/annotation.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/app/components/annotation/annotation.component.scss b/src/app/components/annotation/annotation.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/components/annotation/annotation.component.spec.ts b/src/app/components/annotation/annotation.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..23ba211b7beeaa53d7b27a0758e5e092ea1797ea --- /dev/null +++ b/src/app/components/annotation/annotation.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AnnotationComponent } from './annotation.component'; + +describe('AnnotationComponent', () => { + let component: AnnotationComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AnnotationComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AnnotationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/annotation/annotation.component.ts b/src/app/components/annotation/annotation.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..fa2f83e18ced76b708c68927667d7df466086600 --- /dev/null +++ b/src/app/components/annotation/annotation.component.ts @@ -0,0 +1,51 @@ +import { Component, ElementRef, ViewChild } from '@angular/core'; +import { AnnotatorComponent } from "../annotator/annotator.component"; + +@Component({ + selector: 'amc-annotation', + standalone: true, + imports: [AnnotatorComponent], + templateUrl: './annotation.component.html', + styleUrl: './annotation.component.scss' +}) +export class AnnotationComponent { + @ViewChild("annotator", { static: true }) + private annotator!: AnnotatorComponent; + + public onUploadImage(event: DragEvent): void { + console.log(this.annotator); + + event.preventDefault(); + + const file = event.dataTransfer?.files[0]; + + if (file && file.type.startsWith("image/")) { + console.log(file); + + const reader = new FileReader(); + + reader.onload = (event: ProgressEvent) => { + console.log("Read"); + const image = new Image(); + + image.onload = (event: Event) => { + this.annotator.changeImage(image); + } + + if (typeof(event.target?.result) === "string") { + image.src = event.target.result; + } + }; + + reader.readAsDataURL(file); + } else { + alert("Only images are supported"); + } + } + + public onDragOver(event: DragEvent): void { + event.preventDefault(); + console.log(typeof (event)); + } + +} diff --git a/src/app/components/annotator/annotator.component.html b/src/app/components/annotator/annotator.component.html new file mode 100644 index 0000000000000000000000000000000000000000..40e31bdae650825d35154234af925c7b4404d85d --- /dev/null +++ b/src/app/components/annotator/annotator.component.html @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/src/app/components/annotator/annotator.component.scss b/src/app/components/annotator/annotator.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..6f7b7ddf8d44437c17c738b59bd6352320df8663 --- /dev/null +++ b/src/app/components/annotator/annotator.component.scss @@ -0,0 +1,5 @@ +canvas#annotator { + background-color: red; + max-width: 800px; + max-height: 600px; +} \ No newline at end of file diff --git a/src/app/components/annotator/annotator.component.spec.ts b/src/app/components/annotator/annotator.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..66ba8045729f7c82df173d625e5eaeaf303fc8ac --- /dev/null +++ b/src/app/components/annotator/annotator.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AnnotatorComponent } from './annotator.component'; + +describe('AnnotatorComponent', () => { + let component: AnnotatorComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AnnotatorComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AnnotatorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/annotator/annotator.component.ts b/src/app/components/annotator/annotator.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..722f01a590a99741855a86f37435e9fb5a33e5dd --- /dev/null +++ b/src/app/components/annotator/annotator.component.ts @@ -0,0 +1,93 @@ +import { Component, ElementRef, ViewChild } from '@angular/core'; + +@Component({ + selector: 'amc-annotator', + standalone: true, + imports: [], + templateUrl: './annotator.component.html', + styleUrl: './annotator.component.scss' +}) +export class AnnotatorComponent { + @ViewChild("annotator", { static: true }) + public canvas!: ElementRef; + + private startPoint?: [number, number]; + private backgroundImage?: HTMLImageElement; + + private isDrawing(): boolean { + return this.startPoint !== undefined; + } + + private getCanvasContext(): CanvasRenderingContext2D { + const context = this.canvas.nativeElement.getContext("2d"); + + if (context === null) { + throw new Error("canvas is not initialized"); + } else { + return context; + } + } + + public changeImage(image: HTMLImageElement): void { + this.backgroundImage = image; + this.repaintImage(); + } + + private repaintImage(): void { + const canvas = this.canvas.nativeElement; + + if (this.backgroundImage === undefined) { + const context = this.getCanvasContext(); + context.clearRect(0, 0, canvas.width, canvas.height); + } else { + canvas.width = this.backgroundImage.width; + canvas.height = this.backgroundImage.height; + + const context = this.getCanvasContext(); + context.clearRect(0, 0, canvas.width, canvas.height); + context.drawImage(this.backgroundImage, 0, 0, canvas.width, canvas.height); + } + } + + + public onMouseDown(event: MouseEvent): void { + this.startPoint = [event.offsetX, event.offsetY]; + } + + public onMouseMove(event: MouseEvent): void { + if (this.isDrawing() && this.startPoint !== undefined) { + const [x0, y0] = this.startPoint; + const [x, y, width, height] = this.adjustCoordinates(x0, y0, event.clientX - x0, event.clientY - y0); + + const canvas = this.canvas.nativeElement; + const context = this.getCanvasContext(); + + this.repaintImage(); + context.globalCompositeOperation = "xor"; + context.strokeStyle = "black"; + context.lineWidth = Math.ceil(Math.max(canvas.width, canvas.height) / 500); + context.beginPath(); + context.rect(x, y, width, height); + context.stroke(); + } + } + + private adjustCoordinates(x: number, y: number, width: number, height: number): [number, number, number, number] { + const canvas = this.canvas.nativeElement; + + if (canvas.width === canvas.clientWidth && canvas.height === canvas.clientHeight) { + return [x, y, width, height]; + } else { + return [ + x * canvas.width / canvas.clientWidth, + y * canvas.height / canvas.clientHeight, + width * canvas.width / canvas.clientWidth, + height * canvas.height / canvas.clientHeight + ]; + } + } + + public onMouseUp(event: MouseEvent): void { + this.startPoint = undefined; + } +}