mirror of https://github.com/Squidex/squidex.git
Browse Source
* Improved image editor and focus point. * added missing files. * Folder renamed * Focus point improvement. * Focus point in API. * Tags improved. * Another style fix.pull/483/head
committed by
GitHub
68 changed files with 1117 additions and 833 deletions
@ -0,0 +1,20 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.Assets |
||||
|
{ |
||||
|
public enum ResizeMode |
||||
|
{ |
||||
|
Crop, |
||||
|
CropUpsize, |
||||
|
Pad, |
||||
|
BoxPad, |
||||
|
Max, |
||||
|
Min, |
||||
|
Stretch |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,57 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Text; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Assets |
||||
|
{ |
||||
|
public sealed class ResizeOptions |
||||
|
{ |
||||
|
public int? Width { get; set; } |
||||
|
|
||||
|
public int? Height { get; set; } |
||||
|
|
||||
|
public int? Quality { get; set; } |
||||
|
|
||||
|
public float? FocusX { get; set; } |
||||
|
|
||||
|
public float? FocusY { get; set; } |
||||
|
|
||||
|
public ResizeMode Mode { get; set; } |
||||
|
|
||||
|
public override string ToString() |
||||
|
{ |
||||
|
var sb = new StringBuilder(); |
||||
|
|
||||
|
sb.Append(Width); |
||||
|
sb.Append("_"); |
||||
|
sb.Append(Height); |
||||
|
sb.Append("_"); |
||||
|
sb.Append(Mode); |
||||
|
|
||||
|
if (Quality.HasValue) |
||||
|
{ |
||||
|
sb.Append("_"); |
||||
|
sb.Append(Quality); |
||||
|
} |
||||
|
|
||||
|
if (FocusX.HasValue) |
||||
|
{ |
||||
|
sb.Append("_focusX_"); |
||||
|
sb.Append(FocusX); |
||||
|
} |
||||
|
|
||||
|
if (FocusY.HasValue) |
||||
|
{ |
||||
|
sb.Append("_focusY_"); |
||||
|
sb.Append(FocusY); |
||||
|
} |
||||
|
|
||||
|
return sb.ToString(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
<div class="menu"> |
||||
|
<button type="button" class="btn" (click)="rotate(-90)" title="Rotate Left"> |
||||
|
<i class="icon-rotate_left"></i> |
||||
|
</button> |
||||
|
|
||||
|
<button type="button" class="btn" (click)="rotate(90)" title="Rotate Right"> |
||||
|
<i class="icon-rotate_right"></i> |
||||
|
</button> |
||||
|
|
||||
|
<span class="separator"></span> |
||||
|
|
||||
|
<button type="button" class="btn" (click)="flip(false)" title="Flip Vertically"> |
||||
|
<i class="icon-flip"></i> |
||||
|
</button> |
||||
|
|
||||
|
<button type="button" class="btn rotate 90" (click)="flip(true)" title="Flip Horizontally"> |
||||
|
<i class="icon-flip"></i> |
||||
|
</button> |
||||
|
|
||||
|
<span class="separator"></span> |
||||
|
|
||||
|
<button type="button" class="btn" (click)="reset()" title="Reset"> |
||||
|
<i class="icon-close"></i> |
||||
|
</button> |
||||
|
</div> |
||||
|
|
||||
|
<div class="editor"> |
||||
|
<canvas #editor></canvas> |
||||
|
</div> |
||||
@ -0,0 +1,46 @@ |
|||||
|
:host ::ng-deep { |
||||
|
@import '~cropperjs/dist/cropper'; |
||||
|
|
||||
|
img { |
||||
|
background-image: $asset-background; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:host { |
||||
|
background: $color-dark-black; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
flex-grow: 1; |
||||
|
height: 100%; |
||||
|
overflow-x: hidden; |
||||
|
overflow-y: hidden; |
||||
|
} |
||||
|
|
||||
|
.editor { |
||||
|
flex-grow: 1; |
||||
|
overflow-x: hidden; |
||||
|
overflow-y: hidden; |
||||
|
} |
||||
|
|
||||
|
.menu { |
||||
|
flex-shrink: 0; |
||||
|
padding: 1rem; |
||||
|
text-align: center; |
||||
|
text-decoration: none; |
||||
|
|
||||
|
.btn { |
||||
|
color: $color-dark-foreground; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.rotate { |
||||
|
transform: rotate(90deg); |
||||
|
} |
||||
|
|
||||
|
.separator { |
||||
|
border-left: 1px solid $color-dark-foreground; |
||||
|
display: inline-block; |
||||
|
height: .75rem; |
||||
|
margin-left: 1rem; |
||||
|
margin-right: 1rem; |
||||
|
} |
||||
@ -0,0 +1,134 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnDestroy, ViewChild } from '@angular/core'; |
||||
|
import Cropper from 'cropperjs'; |
||||
|
|
||||
|
import { Types } from '@app/framework'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-image-editor', |
||||
|
styleUrls: ['./image-cropper.component.scss'], |
||||
|
templateUrl: './image-cropper.component.html', |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush |
||||
|
}) |
||||
|
export class ImageCropperComponent implements AfterViewInit, OnDestroy, OnChanges { |
||||
|
private cropper: Cropper | null = null; |
||||
|
private data: Cropper.Data; |
||||
|
|
||||
|
@Input() |
||||
|
public imageSource: string; |
||||
|
|
||||
|
@ViewChild('editor', { static: false }) |
||||
|
public editor: ElementRef<HTMLCanvasElement>; |
||||
|
|
||||
|
public ngOnDestroy() { |
||||
|
if (this.cropper) { |
||||
|
this.cropper.destroy(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public ngOnChanges() { |
||||
|
if (this.cropper) { |
||||
|
this.cropper.replace(this.imageSource); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public ngAfterViewInit() { |
||||
|
this.cropper = new Cropper(this.editor.nativeElement, { |
||||
|
ready: () => { |
||||
|
if (this.cropper) { |
||||
|
this.data = this.cropper.getData(); |
||||
|
} |
||||
|
}, |
||||
|
autoCrop: false, |
||||
|
background: false, |
||||
|
minContainerHeight: 0, |
||||
|
minContainerWidth: 0, |
||||
|
movable: false, |
||||
|
zoomOnTouch: false, |
||||
|
zoomOnWheel: false, |
||||
|
viewMode: 0 |
||||
|
}); |
||||
|
|
||||
|
this.cropper.replace(this.imageSource); |
||||
|
} |
||||
|
|
||||
|
public rotate(value: number) { |
||||
|
if (this.cropper) { |
||||
|
this.cropper.rotate(-90); |
||||
|
|
||||
|
const canvasData = this.cropper.getCanvasData(); |
||||
|
const containerData = this.cropper.getContainerData(); |
||||
|
|
||||
|
const dx = containerData.width / canvasData.naturalWidth; |
||||
|
const dy = containerData.height / canvasData.naturalHeight; |
||||
|
|
||||
|
this.cropper.zoomTo(Math.min(dx, dy), { |
||||
|
x: containerData.width / 2, |
||||
|
y: containerData.height / 2 |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public flip(vertically: boolean) { |
||||
|
if (this.cropper) { |
||||
|
const { rotate, scaleX, scaleY } = this.cropper.getData(); |
||||
|
|
||||
|
if (rotate === 90 || rotate === 270) { |
||||
|
vertically = !vertically; |
||||
|
} |
||||
|
|
||||
|
if (vertically) { |
||||
|
this.cropper.scale(scaleX, -1 * scaleY); |
||||
|
} else { |
||||
|
this.cropper.scale(-1 * scaleX, scaleY); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public zoomIn() { |
||||
|
if (this.cropper) { |
||||
|
this.cropper.zoom(.1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public zoomOut() { |
||||
|
if (this.cropper) { |
||||
|
this.cropper.zoom(-.1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public reset() { |
||||
|
if (this.cropper) { |
||||
|
this.cropper.reset(); |
||||
|
this.cropper.clear(); |
||||
|
|
||||
|
this.data = this.cropper.getData(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public toFile(): Promise<Blob | null> { |
||||
|
return new Promise<Blob | null>(resolve => { |
||||
|
if (!this.cropper) { |
||||
|
return resolve(null); |
||||
|
} else { |
||||
|
const data = this.cropper.getData(); |
||||
|
|
||||
|
if (Types.equals(data, this.data)) { |
||||
|
resolve(null); |
||||
|
} else { |
||||
|
this.data = data; |
||||
|
|
||||
|
this.cropper.getCroppedCanvas().toBlob(blob => { |
||||
|
resolve(blob); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -1 +0,0 @@ |
|||||
<div #editor></div> |
|
||||
@ -1,29 +0,0 @@ |
|||||
:host { |
|
||||
height: 100%; |
|
||||
} |
|
||||
|
|
||||
:host /deep/ { |
|
||||
.tui-image-editor-header { |
|
||||
display: none; |
|
||||
} |
|
||||
|
|
||||
* { |
|
||||
box-sizing: content-box; |
|
||||
} |
|
||||
|
|
||||
.tie-btn-delete { |
|
||||
display: none !important; |
|
||||
} |
|
||||
|
|
||||
.tie-btn-delete-all { |
|
||||
display: none !important; |
|
||||
|
|
||||
& + li { |
|
||||
display: none !important; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
svg { |
|
||||
vertical-align: baseline; |
|
||||
} |
|
||||
} |
|
||||
@ -1,199 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|
||||
*/ |
|
||||
|
|
||||
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, ViewChild } from '@angular/core'; |
|
||||
|
|
||||
import { ResourceLoaderService } from '@app/shared/internal'; |
|
||||
|
|
||||
declare var tui: any; |
|
||||
|
|
||||
const blackTheme = { |
|
||||
'common.bi.image': 'https://uicdn.toast.com/toastui/img/tui-image-editor-bi.png', |
|
||||
'common.bisize.width': '251px', |
|
||||
'common.bisize.height': '21px', |
|
||||
'common.backgroundImage': 'none', |
|
||||
'common.backgroundColor': '#000', |
|
||||
'common.border': '0px', |
|
||||
|
|
||||
// header
|
|
||||
'header.backgroundImage': 'none', |
|
||||
'header.backgroundColor': 'transparent', |
|
||||
'header.border': '0px', |
|
||||
|
|
||||
// load button
|
|
||||
'loadButton.backgroundColor': '#fff', |
|
||||
'loadButton.border': '1px solid #ddd', |
|
||||
'loadButton.color': '#222', |
|
||||
'loadButton.fontFamily': '\'Noto Sans\', sans-serif', |
|
||||
'loadButton.fontSize': '12px', |
|
||||
|
|
||||
// download button
|
|
||||
'downloadButton.backgroundColor': '#fdba3b', |
|
||||
'downloadButton.border': '1px solid #fdba3b', |
|
||||
'downloadButton.color': '#fff', |
|
||||
'downloadButton.fontFamily': '\'Noto Sans\', sans-serif', |
|
||||
'downloadButton.fontSize': '12px', |
|
||||
|
|
||||
// main icons
|
|
||||
'menu.normalIcon.path': 'https://unpkg.com/tui-image-editor@3.7.3/dist/svg/icon-d.svg', |
|
||||
'menu.normalIcon.name': 'icon-d', |
|
||||
'menu.activeIcon.path': 'https://unpkg.com/tui-image-editor@3.7.3/dist/svg/icon-b.svg', |
|
||||
'menu.activeIcon.name': 'icon-b', |
|
||||
'menu.disabledIcon.path': 'https://unpkg.com/tui-image-editor@3.7.3/dist/svg/icon-a.svg', |
|
||||
'menu.disabledIcon.name': 'icon-a', |
|
||||
'menu.hoverIcon.path': 'https://unpkg.com/tui-image-editor@3.7.3/dist/svg/icon-c.svg', |
|
||||
'menu.hoverIcon.name': 'icon-c', |
|
||||
'menu.iconSize.width': '24px', |
|
||||
'menu.iconSize.height': '24px', |
|
||||
|
|
||||
// submenu primary color
|
|
||||
'submenu.backgroundColor': '#000', |
|
||||
'submenu.partition.color': '#3c3c3c', |
|
||||
|
|
||||
// submenu icons
|
|
||||
'submenu.normalIcon.path': 'https://unpkg.com/tui-image-editor@3.7.3/dist/svg/icon-d.svg', |
|
||||
'submenu.normalIcon.name': 'icon-d', |
|
||||
'submenu.activeIcon.path': 'https://unpkg.com/tui-image-editor@3.7.3/dist/svg/icon-c.svg', |
|
||||
'submenu.activeIcon.name': 'icon-c', |
|
||||
'submenu.iconSize.width': '32px', |
|
||||
'submenu.iconSize.height': '32px', |
|
||||
|
|
||||
// submenu labels
|
|
||||
'submenu.normalLabel.color': '#8a8a8a', |
|
||||
'submenu.normalLabel.fontWeight': 'normal', |
|
||||
'submenu.activeLabel.color': '#fff', |
|
||||
'submenu.activeLabel.fontWeight': 'normal', |
|
||||
|
|
||||
// checkbox style
|
|
||||
'checkbox.border': '0px', |
|
||||
'checkbox.backgroundColor': '#fff', |
|
||||
|
|
||||
// range style
|
|
||||
'range.pointer.color': '#fff', |
|
||||
'range.bar.color': '#666', |
|
||||
'range.subbar.color': '#d1d1d1', |
|
||||
|
|
||||
'range.disabledPointer.color': '#414141', |
|
||||
'range.disabledBar.color': '#282828', |
|
||||
'range.disabledSubbar.color': '#414141', |
|
||||
|
|
||||
'range.value.color': '#fff', |
|
||||
'range.value.fontWeight': 'normal', |
|
||||
'range.value.fontSize': '11px', |
|
||||
'range.value.border': '1px solid #353535', |
|
||||
'range.value.backgroundColor': '#151515', |
|
||||
'range.title.color': '#fff', |
|
||||
'range.title.fontWeight': 'normal', |
|
||||
|
|
||||
// colorpicker style
|
|
||||
'colorpicker.button.border': '1px solid #1e1e1e', |
|
||||
'colorpicker.title.color': '#fff' |
|
||||
}; |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'sqx-image-editor', |
|
||||
styleUrls: ['./image-editor.component.scss'], |
|
||||
templateUrl: './image-editor.component.html', |
|
||||
changeDetection: ChangeDetectionStrategy.OnPush |
|
||||
}) |
|
||||
export class ImageEditorComponent implements AfterViewInit, OnChanges { |
|
||||
private imageEditor: any; |
|
||||
private isChanged = false; |
|
||||
private isChangedBefore = false; |
|
||||
|
|
||||
@Input() |
|
||||
public accessToken: string; |
|
||||
|
|
||||
@Input() |
|
||||
public imageUrl: string; |
|
||||
|
|
||||
@ViewChild('editor', { static: false }) |
|
||||
public editor: ElementRef; |
|
||||
|
|
||||
constructor( |
|
||||
private readonly resourceLoader: ResourceLoaderService |
|
||||
) { |
|
||||
} |
|
||||
|
|
||||
public ngOnChanges() { |
|
||||
if (this.imageEditor && this.imageUrl) { |
|
||||
this.imageEditor.loadImageFromURL(this.imageUrl); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public toFile(): Blob | null { |
|
||||
if (!this.isChanged) { |
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
this.isChanged = false; |
|
||||
|
|
||||
const dataURI = this.imageEditor.toDataURL(); |
|
||||
|
|
||||
const byteString = atob(dataURI.split(',')[1]); |
|
||||
const byteBuffer = new ArrayBuffer(byteString.length); |
|
||||
|
|
||||
const type = dataURI.split(',')[0].split(':')[1].split(';')[0]; |
|
||||
|
|
||||
const array = new Uint8Array(byteBuffer); |
|
||||
|
|
||||
for (let i = 0; i < byteString.length; i++) { |
|
||||
array[i] = byteString.charCodeAt(i); |
|
||||
} |
|
||||
|
|
||||
return new Blob([array], { type }); |
|
||||
} |
|
||||
|
|
||||
public ngAfterViewInit() { |
|
||||
const styles = [ |
|
||||
'https://uicdn.toast.com/tui-color-picker/latest/tui-color-picker.css', |
|
||||
'https://uicdn.toast.com/tui-image-editor/latest/tui-image-editor.css' |
|
||||
]; |
|
||||
|
|
||||
const scripts = [ |
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/jquery/1.8.3/jquery.min.js', |
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.3.2/fabric.js', |
|
||||
'https://uicdn.toast.com/tui.code-snippet/latest/tui-code-snippet.min.js', |
|
||||
'https://uicdn.toast.com/tui-color-picker/latest/tui-color-picker.js', |
|
||||
'https://uicdn.toast.com/tui-image-editor/latest/tui-image-editor.js' |
|
||||
]; |
|
||||
|
|
||||
let path = this.imageUrl; |
|
||||
|
|
||||
if (this.accessToken) { |
|
||||
path += `&access_token=${this.accessToken}`; |
|
||||
} |
|
||||
|
|
||||
styles.forEach(style => this.resourceLoader.loadStyle(style)); |
|
||||
Promise.all(scripts.map(script => this.resourceLoader.loadScript(script))).then(() => { |
|
||||
this.imageEditor = new tui.ImageEditor(this.editor.nativeElement, { |
|
||||
includeUI: { |
|
||||
loadImage: { |
|
||||
path, name: 'image' |
|
||||
}, |
|
||||
menu: [ |
|
||||
'crop', |
|
||||
'flip', |
|
||||
'mask', |
|
||||
'filter' |
|
||||
], |
|
||||
theme: blackTheme |
|
||||
}, |
|
||||
cssMaxWidth: 700, |
|
||||
cssMaxHeight: 500 |
|
||||
}); |
|
||||
|
|
||||
this.imageEditor.on('undoStackChanged', () => { |
|
||||
if (this.isChangedBefore) { |
|
||||
this.isChanged = true; |
|
||||
} else { |
|
||||
this.isChangedBefore = true; |
|
||||
} |
|
||||
}); |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,29 @@ |
|||||
|
<div class="row"> |
||||
|
<div class="col-5"> |
||||
|
<Label>Select position of focus point</Label> |
||||
|
|
||||
|
<div class="image"> |
||||
|
<div class="image-container absolute align-items-center"> |
||||
|
<div> |
||||
|
<img #image [src]="imageSource" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-7"> |
||||
|
<Label>Preview for different sizes</Label> |
||||
|
|
||||
|
<div class="preview-wide"> |
||||
|
<img #previewWide [src]="imageSource" /> |
||||
|
</div> |
||||
|
|
||||
|
<div class="preview-line2"> |
||||
|
<div class="preview-small absolute"> |
||||
|
<img #previewSmall [src]="imageSource" /> |
||||
|
</div> |
||||
|
<div class="preview-normal absolute"> |
||||
|
<img #previewNormal [src]="imageSource" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,41 @@ |
|||||
|
.image { |
||||
|
background: $color-dark-black; |
||||
|
padding: 0; |
||||
|
padding-top: 100%; |
||||
|
position: relative; |
||||
|
|
||||
|
&-container { |
||||
|
@include absolute(0, 0, 0, 0); |
||||
|
display: flex; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.preview { |
||||
|
&-wide { |
||||
|
padding-top: 20%; |
||||
|
} |
||||
|
|
||||
|
&-line2 { |
||||
|
margin-top: 1rem; |
||||
|
max-height: 20rem; |
||||
|
min-height: 20rem; |
||||
|
position: relative; |
||||
|
} |
||||
|
|
||||
|
&-normal { |
||||
|
@include absolute(0, 9rem, 0, 0); |
||||
|
} |
||||
|
|
||||
|
&-small { |
||||
|
@include absolute(0, 0, 0, null); |
||||
|
width: 8rem; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.absolute { |
||||
|
position: absolute !important; |
||||
|
} |
||||
|
|
||||
|
img { |
||||
|
background-image: $asset-background; |
||||
|
} |
||||
@ -0,0 +1,108 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
// tslint:disable: readonly-array
|
||||
|
|
||||
|
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnDestroy, ViewChild } from '@angular/core'; |
||||
|
import { FocusedImage, FocusPicker } from 'image-focus'; |
||||
|
|
||||
|
import { Types } from '@app/framework'; |
||||
|
|
||||
|
import { AnnotateAssetDto, AssetDto } from '@app/shared/services/assets.service'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-image-focus-point', |
||||
|
styleUrls: ['./image-focus-point.component.scss'], |
||||
|
templateUrl: './image-focus-point.component.html', |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush |
||||
|
}) |
||||
|
export class ImageFocusPointComponent implements AfterViewInit, OnDestroy, OnChanges { |
||||
|
private readonly previewImages: FocusedImage[] = []; |
||||
|
private focusPicker: FocusPicker | null = null; |
||||
|
private x = 0; |
||||
|
private y = 0; |
||||
|
|
||||
|
@Input() |
||||
|
public imageSource: string; |
||||
|
|
||||
|
@Input() |
||||
|
public focusPoint: any; |
||||
|
|
||||
|
@ViewChild('image', { static: false }) |
||||
|
public image: ElementRef<HTMLImageElement>; |
||||
|
|
||||
|
@ViewChild('previewWide', { static: false }) |
||||
|
public previewWide: ElementRef<HTMLImageElement>; |
||||
|
|
||||
|
@ViewChild('previewSmall', { static: false }) |
||||
|
public previewSmall: ElementRef<HTMLImageElement>; |
||||
|
|
||||
|
@ViewChild('previewNormal', { static: false }) |
||||
|
public previewNormal: ElementRef<HTMLImageElement>; |
||||
|
|
||||
|
public ngOnDestroy() { |
||||
|
if (this.focusPicker) { |
||||
|
this.focusPicker.stopListening(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public ngOnChanges() { |
||||
|
const { x, y } = getFocusPoint(this.focusPoint); |
||||
|
|
||||
|
this.x = x; |
||||
|
this.y = y; |
||||
|
} |
||||
|
|
||||
|
public ngAfterViewInit() { |
||||
|
const focus = { x: this.x, y: this.y }; |
||||
|
|
||||
|
const properties = { focus, debounceTime: 50 }; |
||||
|
|
||||
|
this.previewImages.push(new FocusedImage(this.previewWide.nativeElement, properties)); |
||||
|
this.previewImages.push(new FocusedImage(this.previewSmall.nativeElement, properties)); |
||||
|
this.previewImages.push(new FocusedImage(this.previewNormal.nativeElement, properties)); |
||||
|
|
||||
|
this.focusPicker = new FocusPicker(this.image.nativeElement, { |
||||
|
focus, |
||||
|
onChange: newFocus => { |
||||
|
this.x = newFocus.x; |
||||
|
this.y = newFocus.y; |
||||
|
|
||||
|
for (let preview of this.previewImages) { |
||||
|
preview.setFocus(newFocus); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public submit(asset: AssetDto): AnnotateAssetDto | null { |
||||
|
const previous = getFocusPoint(asset.metadata); |
||||
|
|
||||
|
if (previous.x === this.x && previous.y === this.y) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
const metadata = { ...asset.metadata, focusX: this.x, focusY: this.y }; |
||||
|
|
||||
|
return { metadata }; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function getFocusPoint(value: any): { x: number, y: number } { |
||||
|
let x = 0; |
||||
|
let y = 0; |
||||
|
|
||||
|
if (value && Types.isNumber(value.focusX)) { |
||||
|
x = value.focusX; |
||||
|
} |
||||
|
|
||||
|
if (value && Types.isNumber(value.focusY)) { |
||||
|
y = value.focusY; |
||||
|
} |
||||
|
|
||||
|
return { x, y }; |
||||
|
} |
||||
Binary file not shown.
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 100 KiB |
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Loading…
Reference in new issue