mirror of https://github.com/Squidex/squidex.git
51 changed files with 171 additions and 2477 deletions
@ -0,0 +1,41 @@ |
|||||
|
// ==========================================================================
|
||||
|
// WrongEventVersionException.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.CQRS.Events |
||||
|
{ |
||||
|
public class WrongEventVersionException : Exception |
||||
|
{ |
||||
|
private readonly int currentVersion; |
||||
|
private readonly int expectedVersion; |
||||
|
|
||||
|
public int CurrentVersion |
||||
|
{ |
||||
|
get { return currentVersion; } |
||||
|
} |
||||
|
|
||||
|
public int ExpectedVersion |
||||
|
{ |
||||
|
get { return expectedVersion; } |
||||
|
} |
||||
|
|
||||
|
public WrongEventVersionException(int currentVersion, int expectedVersion) |
||||
|
: base(FormatMessage(currentVersion, expectedVersion)) |
||||
|
{ |
||||
|
this.currentVersion = currentVersion; |
||||
|
|
||||
|
this.expectedVersion = expectedVersion; |
||||
|
} |
||||
|
|
||||
|
private static string FormatMessage(int currentVersion, int expectedVersion) |
||||
|
{ |
||||
|
return $"Requested version {expectedVersion}, but found {currentVersion}."; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,30 +0,0 @@ |
|||||
import { TestBed } from '@angular/core/testing'; |
|
||||
|
|
||||
/* tslint:disable ordered-imports */ |
|
||||
import { RouterModule, provideRoutes } from '@angular/router'; |
|
||||
import { RouterTestingModule } from '@angular/router/testing'; |
|
||||
|
|
||||
import { AppComponent } from './app.component'; |
|
||||
|
|
||||
describe('App', () => { |
|
||||
beforeEach(() => { |
|
||||
TestBed.configureTestingModule({ |
|
||||
declarations: [ |
|
||||
AppComponent |
|
||||
], |
|
||||
imports: [ |
|
||||
RouterModule, |
|
||||
RouterTestingModule |
|
||||
], |
|
||||
providers: [ |
|
||||
provideRoutes([]) |
|
||||
] |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
it('should work', () => { |
|
||||
const fixture = TestBed.createComponent(AppComponent); |
|
||||
|
|
||||
expect(fixture.componentInstance instanceof AppComponent).toBe(true, 'should create AppComponent'); |
|
||||
}); |
|
||||
}); |
|
||||
@ -1,65 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Sebastian Stehle. All rights reserved |
|
||||
*/ |
|
||||
|
|
||||
import { Subject } from 'rxjs'; |
|
||||
|
|
||||
import { Action } from './../'; |
|
||||
|
|
||||
class MockupObject { |
|
||||
public isDestroyCalled = false; |
|
||||
|
|
||||
@Action() |
|
||||
public event1 = new Subject<string>().map(_ => { return { type: 'MOCK_ACTION' }; }); |
|
||||
|
|
||||
@Action() |
|
||||
public event2 = new Subject<string>().map(_ => { return { type: 'MOCK_ACTION' }; }); |
|
||||
|
|
||||
constructor(private readonly store: any) { } |
|
||||
|
|
||||
public log() { |
|
||||
this.store.log(); |
|
||||
} |
|
||||
|
|
||||
public init() { |
|
||||
this.event2 = new Subject<string>().map(_ => { return { type: 'MOCK_ACTION' }; }); |
|
||||
} |
|
||||
|
|
||||
public ngOnDestroy() { |
|
||||
this.isDestroyCalled = true; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
describe('Action', () => { |
|
||||
it('should make complete flow to subscribe and unsubscribe and to trigger actions', () => { |
|
||||
let dispatchCount = 0; |
|
||||
|
|
||||
const state = { |
|
||||
next: (e: any) => { |
|
||||
dispatchCount++; |
|
||||
|
|
||||
expect(e.type).toBe('MOCK_ACTION'); |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
const mock = new MockupObject(state); |
|
||||
|
|
||||
mock.init(); |
|
||||
|
|
||||
(<any>mock.event1).next('TEST'); |
|
||||
(<any>mock.event2).next('TEST'); |
|
||||
|
|
||||
expect(dispatchCount).toBe(2); |
|
||||
|
|
||||
mock.ngOnDestroy(); |
|
||||
|
|
||||
(<any>mock.event1).next('TEST'); |
|
||||
(<any>mock.event2).next('TEST'); |
|
||||
|
|
||||
expect(dispatchCount).toBe(2); |
|
||||
expect(mock.isDestroyCalled).toBeTruthy(); |
|
||||
}); |
|
||||
}); |
|
||||
@ -1,72 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Sebastian Stehle. All rights reserved |
|
||||
*/ |
|
||||
|
|
||||
/* tslint:disable:no-empty */ |
|
||||
/* tslint:disable:only-arrow-functions */ |
|
||||
|
|
||||
const EMPTY_FUNC = () => {}; |
|
||||
|
|
||||
export function Action() { |
|
||||
return function (target: any, key: string) { |
|
||||
let observable: any; |
|
||||
let instance: any; |
|
||||
let subscriptions: any; |
|
||||
let subscription: any; |
|
||||
|
|
||||
function subscribe() { |
|
||||
const store = instance.store; |
|
||||
|
|
||||
if (store && observable && observable.subscribe && typeof observable.subscribe === 'function') { |
|
||||
subscription = observable.subscribe((a: any) => { if (a) { store.next(a); } }); |
|
||||
|
|
||||
subscriptions.push(subscription); |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
function unsubscribe() { |
|
||||
if (subscription) { |
|
||||
subscription.unsubscribe(); |
|
||||
|
|
||||
subscriptions.splice(subscriptions.indexOf(subscribe), 1); |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
if (delete target[key]) { |
|
||||
Object.defineProperty(target, key, { |
|
||||
get: function () { |
|
||||
return observable; |
|
||||
}, |
|
||||
set: function (v) { |
|
||||
instance = this; |
|
||||
|
|
||||
if (!instance.___subscriptions) { |
|
||||
instance.___subscriptions = []; |
|
||||
|
|
||||
let destroy = instance.ngOnDestroy ? instance.ngOnDestroy.bind(instance) : EMPTY_FUNC; |
|
||||
|
|
||||
instance.ngOnDestroy = () => { |
|
||||
for (let s of subscriptions) { |
|
||||
s.unsubscribe(); |
|
||||
} |
|
||||
|
|
||||
instance.___subscriptions = null; |
|
||||
|
|
||||
destroy(); |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
subscriptions = instance.___subscriptions; |
|
||||
|
|
||||
observable = v; |
|
||||
|
|
||||
unsubscribe(); |
|
||||
subscribe(); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
}; |
|
||||
} |
|
||||
@ -1,11 +0,0 @@ |
|||||
<div class="btn-group" [class.open]="isOpen"> |
|
||||
<button type="button" class="btn btn-secondary btn-sm" [disabled]="isDisabled" (click)="toggleOpen()" [style.background]="selectedColor.toString()"></button> |
|
||||
|
|
||||
<div class="dropdown-menu dropdown-menu-{{dropdownSide}}"> |
|
||||
<div class="color-palette"> |
|
||||
<span *ngFor="let color of palette.colors" class="color-palette-box" (click)="selectColor(color)" [class.selected]="selectedColor && color.eq(selectedColor)"> |
|
||||
<span class="color-palette-color" [style.background]="color.toString()"></span> |
|
||||
</span> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
@ -1,68 +0,0 @@ |
|||||
@import '_mixins'; |
|
||||
@import '_vars'; |
|
||||
|
|
||||
$color-size: 24px; |
|
||||
$color-button: #1875cc; |
|
||||
$color-button-hover: #1460a8; |
|
||||
$color-border: #fff; |
|
||||
$button-size: 1.81rem; |
|
||||
|
|
||||
.color-palette { |
|
||||
& { |
|
||||
@include clearfix; |
|
||||
padding: 6px; |
|
||||
width: 8 * ($color-size + 6); |
|
||||
} |
|
||||
|
|
||||
&-box { |
|
||||
& { |
|
||||
border: 2px solid transparent; |
|
||||
margin: 2px; |
|
||||
height: $color-size; |
|
||||
width: $color-size; |
|
||||
float: left; |
|
||||
} |
|
||||
|
|
||||
&:hover { |
|
||||
border-color: $color-button-hover; |
|
||||
} |
|
||||
|
|
||||
&.selected { |
|
||||
& { |
|
||||
border-color: $color-button; |
|
||||
} |
|
||||
|
|
||||
&:hover { |
|
||||
border-color: $color-button; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
&.disabled { |
|
||||
border-color: transparent; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
&-color { |
|
||||
border: 1px solid $color-border; |
|
||||
width: $color-size - 4; |
|
||||
height: $color-size - 4; |
|
||||
display: block; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.dropdown-menu { |
|
||||
background: $color-border; |
|
||||
} |
|
||||
|
|
||||
.btn-group { |
|
||||
> .btn { |
|
||||
&:first-child { |
|
||||
@include border-radius(.25em); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.btn { |
|
||||
width: $button-size; |
|
||||
height: $button-size; |
|
||||
} |
|
||||
@ -1,117 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Sebastian Stehle. All rights reserved |
|
||||
*/ |
|
||||
|
|
||||
import { Color } from './../'; |
|
||||
import { ColorPickerComponent } from './color-picker.component'; |
|
||||
|
|
||||
describe('ColorPickerComponent', () => { |
|
||||
it('should instantiate', () => { |
|
||||
const colorPicker = new ColorPickerComponent({ nativeElement: {} }); |
|
||||
|
|
||||
expect(colorPicker).toBeDefined(); |
|
||||
}); |
|
||||
|
|
||||
it('should close color picker when clicking outside of the modal', () => { |
|
||||
const element = { |
|
||||
nativeElement: { |
|
||||
contains: () => { |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
const colorPicker = new ColorPickerComponent(element); |
|
||||
colorPicker.open(); |
|
||||
colorPicker.onClick({}); |
|
||||
|
|
||||
expect(colorPicker.isOpen).toBeFalsy(); |
|
||||
}); |
|
||||
|
|
||||
it('should not close color picker when clicking inside the modal', () => { |
|
||||
const element = { |
|
||||
nativeElement: { |
|
||||
contains: () => { |
|
||||
return true; |
|
||||
} |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
const colorPicker = new ColorPickerComponent(element); |
|
||||
colorPicker.open(); |
|
||||
colorPicker.onClick({}); |
|
||||
|
|
||||
expect(colorPicker.isOpen).toBeTruthy(); |
|
||||
}); |
|
||||
|
|
||||
it('should close modal and emit event when setting color', () => { |
|
||||
const colorPicker = new ColorPickerComponent({ nativeElement: {} }); |
|
||||
const selectedColor = Color.RED; |
|
||||
|
|
||||
let lastColor: Color | null = null; |
|
||||
|
|
||||
colorPicker.registerOnChange((c: Color) => { |
|
||||
lastColor = c; |
|
||||
}); |
|
||||
|
|
||||
colorPicker.open(); |
|
||||
colorPicker.selectColor(selectedColor); |
|
||||
|
|
||||
expect(lastColor).toBe(selectedColor); |
|
||||
expect(colorPicker.isOpen).toBeFalsy(); |
|
||||
}); |
|
||||
|
|
||||
it('should not emit event when selecting same color', () => { |
|
||||
const colorPicker = new ColorPickerComponent({ nativeElement: {} }); |
|
||||
const selectedColor = Color.RED; |
|
||||
|
|
||||
colorPicker.selectColor(selectedColor); |
|
||||
|
|
||||
let lastColor: Color | null = null; |
|
||||
|
|
||||
colorPicker.registerOnChange((c: Color) => { |
|
||||
lastColor = c; |
|
||||
}); |
|
||||
|
|
||||
colorPicker.selectColor(selectedColor); |
|
||||
|
|
||||
expect(lastColor).toBeNull(); |
|
||||
}); |
|
||||
|
|
||||
it('should update selected color when writing value', () => { |
|
||||
const colorPicker = new ColorPickerComponent({ nativeElement: {} }); |
|
||||
const selectedColor = Color.RED; |
|
||||
|
|
||||
colorPicker.writeValue(selectedColor); |
|
||||
|
|
||||
expect(colorPicker.selectedColor).toBe(selectedColor); |
|
||||
}); |
|
||||
|
|
||||
it('should update selected color with palette default if setting invalid color', () => { |
|
||||
const colorPicker = new ColorPickerComponent({ nativeElement: {} }); |
|
||||
|
|
||||
colorPicker.writeValue('invalid'); |
|
||||
|
|
||||
expect(colorPicker.selectedColor).toBe(colorPicker.palette.defaultColor); |
|
||||
}); |
|
||||
|
|
||||
it('should update selected color with black if setting invalid color and palette is null', () => { |
|
||||
const colorPicker = new ColorPickerComponent({ nativeElement: {} }); |
|
||||
|
|
||||
colorPicker.palette = undefined!; |
|
||||
colorPicker.writeValue('invalid'); |
|
||||
|
|
||||
expect(colorPicker.selectedColor).toBe(Color.BLACK); |
|
||||
}); |
|
||||
|
|
||||
it('should update isOpen prperty when toggleOpen is invoked', () => { |
|
||||
const colorPicker = new ColorPickerComponent({ nativeElement: {} }); |
|
||||
|
|
||||
colorPicker.toggleOpen(); |
|
||||
|
|
||||
expect(colorPicker.isOpen).toBeTruthy(); |
|
||||
}); |
|
||||
}); |
|
||||
@ -1,125 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Sebastian Stehle. All rights reserved |
|
||||
*/ |
|
||||
|
|
||||
import { Component, ElementRef, forwardRef, HostListener, Input } from '@angular/core'; |
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; |
|
||||
|
|
||||
import { Color } from './../utils/color'; |
|
||||
import { ColorPalette } from './../utils/color-palette'; |
|
||||
|
|
||||
/* tslint:disable:no-empty */ |
|
||||
|
|
||||
const NOOP = () => { }; |
|
||||
|
|
||||
export const SQX_COLOR_PICKER_CONTROL_VALUE_ACCESSOR: any = { |
|
||||
provide: NG_VALUE_ACCESSOR, |
|
||||
useExisting: forwardRef(() => ColorPickerComponent), |
|
||||
multi: true |
|
||||
}; |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'sqx-color-picker', |
|
||||
styleUrls: ['./color-picker.component.scss'], |
|
||||
templateUrl: './color-picker.component.html', |
|
||||
providers: [SQX_COLOR_PICKER_CONTROL_VALUE_ACCESSOR] |
|
||||
}) |
|
||||
export class ColorPickerComponent implements ControlValueAccessor { |
|
||||
private changeCallback: (value: any) => void = NOOP; |
|
||||
private touchedCallback: () => void = NOOP; |
|
||||
|
|
||||
public selectedColor: Color = Color.BLACK; |
|
||||
|
|
||||
@Input() |
|
||||
public palette = ColorPalette.colors(); |
|
||||
|
|
||||
@Input() |
|
||||
public dropdownSide: 'left'; |
|
||||
|
|
||||
@Input() |
|
||||
public isOpen = false; |
|
||||
|
|
||||
@Input() |
|
||||
public isDisabled = false; |
|
||||
|
|
||||
constructor(private readonly element: ElementRef) { |
|
||||
this.updateColor(Color.BLACK); |
|
||||
} |
|
||||
|
|
||||
public writeValue(value: any) { |
|
||||
this.updateColor(value); |
|
||||
} |
|
||||
|
|
||||
public setDisabledState(isDisabled: boolean): void { |
|
||||
this.isDisabled = isDisabled; |
|
||||
} |
|
||||
|
|
||||
public registerOnChange(fn: any) { |
|
||||
this.changeCallback = fn; |
|
||||
} |
|
||||
|
|
||||
public registerOnTouched(fn: any) { |
|
||||
this.touchedCallback = fn; |
|
||||
} |
|
||||
|
|
||||
@HostListener('document:click', ['$event.target']) |
|
||||
public onClick(targetElement: any) { |
|
||||
const clickedInside = this.element.nativeElement.contains(targetElement); |
|
||||
|
|
||||
if (!clickedInside) { |
|
||||
this.close(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public toggleOpen() { |
|
||||
if (this.isOpen) { |
|
||||
this.close(); |
|
||||
} else { |
|
||||
this.open(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public open() { |
|
||||
this.isOpen = true; |
|
||||
} |
|
||||
|
|
||||
public close() { |
|
||||
this.isOpen = false; |
|
||||
|
|
||||
this.touchedCallback(); |
|
||||
} |
|
||||
|
|
||||
public selectColor(color: Color) { |
|
||||
this.updateParent(color); |
|
||||
this.updateColor(color); |
|
||||
this.close(); |
|
||||
} |
|
||||
|
|
||||
private updateParent(color: Color) { |
|
||||
if (this.selectedColor.ne(color)) { |
|
||||
this.changeCallback(color); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private updateColor(color: Color) { |
|
||||
let hasColor = false; |
|
||||
try { |
|
||||
this.selectedColor = Color.fromValue(color); |
|
||||
|
|
||||
hasColor = true; |
|
||||
} catch (e) { |
|
||||
hasColor = false; |
|
||||
} |
|
||||
|
|
||||
if (!hasColor || !this.selectedColor) { |
|
||||
if (this.palette) { |
|
||||
this.selectedColor = this.palette.defaultColor; |
|
||||
} else { |
|
||||
this.selectedColor = Color.BLACK; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,118 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Sebastian Stehle. All rights reserved |
|
||||
*/ |
|
||||
|
|
||||
import { Directive, ElementRef, HostListener, Input, Renderer }from '@angular/core'; |
|
||||
|
|
||||
import { DragService } from './../services/drag.service'; |
|
||||
import { Vec2 } from './../utils/vec2'; |
|
||||
|
|
||||
@Directive({ |
|
||||
selector: '[sqxDragModel]' |
|
||||
}) |
|
||||
export class DragModelDirective { |
|
||||
private startOffset: Vec2; |
|
||||
private startPosition: Vec2; |
|
||||
private mouseMoveSubscription: Function | null; |
|
||||
private mouseUpSubscription: Function | null; |
|
||||
private clonedElement: HTMLElement | null; |
|
||||
|
|
||||
@Input('sqxDragModel') |
|
||||
public model: any; |
|
||||
|
|
||||
constructor( |
|
||||
private readonly element: ElementRef, |
|
||||
private readonly renderer: Renderer, |
|
||||
private readonly dragService: DragService |
|
||||
) { |
|
||||
} |
|
||||
|
|
||||
@HostListener('mousedown', ['$event']) |
|
||||
public onMouseDown(event: MouseEvent) { |
|
||||
this.startOffset = new Vec2(event.offsetX, event.offsetY); |
|
||||
this.startPosition = new Vec2(event.clientX, event.clientY); |
|
||||
|
|
||||
this.mouseMoveSubscription = |
|
||||
this.renderer.listenGlobal('window', 'mousemove', (e: MouseEvent) => { |
|
||||
this.onMouseMove(e); |
|
||||
}); |
|
||||
|
|
||||
this.mouseUpSubscription = |
|
||||
this.renderer.listenGlobal('window', 'mouseup', (e: MouseEvent) => { |
|
||||
this.onMouseUp(e); |
|
||||
}); |
|
||||
|
|
||||
this.stopEvent(event); |
|
||||
} |
|
||||
|
|
||||
private onMouseMove(event: MouseEvent) { |
|
||||
const position = new Vec2(event.clientX, event.clientY); |
|
||||
|
|
||||
if (!this.clonedElement && position.sub(this.startPosition).lengtSquared > 100) { |
|
||||
this.clonedElement = this.element.nativeElement.cloneNode(true); |
|
||||
|
|
||||
this.clonedElement!.style.position = 'fixed'; |
|
||||
this.clonedElement!.style.zIndex = '10000'; |
|
||||
|
|
||||
document.body.appendChild(this.clonedElement!); |
|
||||
} |
|
||||
|
|
||||
if (this.clonedElement) { |
|
||||
const elementPosition = position.sub(this.startOffset); |
|
||||
|
|
||||
this.clonedElement.style.left = elementPosition.x + 'px'; |
|
||||
this.clonedElement.style.top = elementPosition.y + 'px'; |
|
||||
} |
|
||||
|
|
||||
this.stopEvent(event); |
|
||||
} |
|
||||
|
|
||||
private onMouseUp(event: MouseEvent) { |
|
||||
if (this.clonedElement) { |
|
||||
this.clonedElement.remove(); |
|
||||
} |
|
||||
|
|
||||
let dropCandidate: Element | null = document.elementFromPoint(event.clientX, event.clientY); |
|
||||
|
|
||||
while (dropCandidate && (dropCandidate.classList && !dropCandidate.classList.contains('sqx-drop'))) { |
|
||||
dropCandidate = dropCandidate.parentNode as Element; |
|
||||
} |
|
||||
|
|
||||
if (dropCandidate && dropCandidate.id) { |
|
||||
const position = this.getRelativeCoordinates(event, dropCandidate).sub(this.startOffset).round(); |
|
||||
|
|
||||
this.dragService.emitDrop({ position, model: this.model, dropTarget: dropCandidate.id }); |
|
||||
} |
|
||||
|
|
||||
if (this.mouseMoveSubscription) { |
|
||||
this.mouseMoveSubscription(); |
|
||||
this.mouseMoveSubscription = null; |
|
||||
} |
|
||||
|
|
||||
if (this.mouseUpSubscription) { |
|
||||
this.mouseUpSubscription(); |
|
||||
this.mouseUpSubscription = null; |
|
||||
} |
|
||||
|
|
||||
this.clonedElement = null; |
|
||||
|
|
||||
this.stopEvent(event); |
|
||||
} |
|
||||
|
|
||||
private stopEvent(event: Event) { |
|
||||
event.preventDefault(); |
|
||||
event.stopPropagation(); |
|
||||
} |
|
||||
|
|
||||
private getRelativeCoordinates(e: any, container: any): Vec2 { |
|
||||
const rect = container.getBoundingClientRect(); |
|
||||
|
|
||||
const x = !!e.touches ? e.touches[0].pageX : e.pageX; |
|
||||
const y = !!e.touches ? e.touches[0].pageY : e.pageY; |
|
||||
|
|
||||
return new Vec2(x - rect.left, y - rect.top); |
|
||||
} |
|
||||
} |
|
||||
@ -1,128 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Sebastian Stehle. All rights reserved |
|
||||
*/ |
|
||||
|
|
||||
import { Directive, ElementRef, HostListener } from '@angular/core'; |
|
||||
|
|
||||
import { DragService } from './../services/drag.service'; |
|
||||
import { Vec2 } from './../utils/vec2'; |
|
||||
|
|
||||
@Directive({ |
|
||||
selector: '.sqx-image-drop' |
|
||||
}) |
|
||||
export class ImageDropDirective { |
|
||||
constructor( |
|
||||
private readonly element: ElementRef, |
|
||||
private readonly dragService: DragService |
|
||||
) { |
|
||||
} |
|
||||
|
|
||||
@HostListener('dragenter', ['$event']) |
|
||||
public onDragEnter(event: DragDropEvent) { |
|
||||
this.tryStopEvent(event); |
|
||||
} |
|
||||
|
|
||||
@HostListener('dragover', ['$event']) |
|
||||
public onDragOver(event: DragDropEvent) { |
|
||||
this.tryStopEvent(event); |
|
||||
} |
|
||||
|
|
||||
@HostListener('drop', ['$event']) |
|
||||
public onDrop(event: DragDropEvent) { |
|
||||
const image = this.findImage(event); |
|
||||
|
|
||||
if (!image) { |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
const position = this.getRelativeCoordinates(event, this.element.nativeElement).round(); |
|
||||
|
|
||||
const reader = new FileReader(); |
|
||||
|
|
||||
reader.onload = (loadedFile: any) => { |
|
||||
const imageSource: string = loadedFile.target.result; |
|
||||
const imageElement = document.createElement('img'); |
|
||||
|
|
||||
imageElement.onload = () => { |
|
||||
this.dragService.emitDrop({ |
|
||||
position, dropTarget: this.element.nativeElement.id, model: { |
|
||||
sizeX: imageElement.width, |
|
||||
sizeY: imageElement.height, |
|
||||
source: imageSource |
|
||||
} |
|
||||
}); |
|
||||
}; |
|
||||
imageElement.src = imageSource; |
|
||||
}; |
|
||||
reader.readAsDataURL(image); |
|
||||
|
|
||||
this.stopEvent(event); |
|
||||
} |
|
||||
|
|
||||
private stopEvent(event: Event) { |
|
||||
event.preventDefault(); |
|
||||
event.stopPropagation(); |
|
||||
} |
|
||||
|
|
||||
private tryStopEvent(event: DragDropEvent) { |
|
||||
const hasFiles = this.hasFiles(event.dataTransfer.types); |
|
||||
|
|
||||
if (!hasFiles) { |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
this.stopEvent(event); |
|
||||
} |
|
||||
|
|
||||
private hasFiles(types: any): boolean { |
|
||||
if (!types) { |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
if (isFunction(types.indexOf)) { |
|
||||
return types.indexOf('Files') !== -1; |
|
||||
} else if (isFunction(types.contains)) { |
|
||||
return types.contains('Files'); |
|
||||
} else { |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private findImage(event: DragDropEvent): File | null { |
|
||||
let image: File | null = null; |
|
||||
|
|
||||
/* tslint:disable:prefer-for-of */ |
|
||||
for (let i = 0; i < event.dataTransfer.files.length; i++) { |
|
||||
const file = event.dataTransfer.files[i]; |
|
||||
|
|
||||
if (file.type.match('image.*')) { |
|
||||
image = file; |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return image; |
|
||||
} |
|
||||
|
|
||||
private getRelativeCoordinates(e: any, container: any): Vec2 { |
|
||||
const rect = container.getBoundingClientRect(); |
|
||||
|
|
||||
const pos = { x: 0, y: 0 }; |
|
||||
|
|
||||
pos.x = !!e.touches ? e.touches[0].pageX : e.pageX; |
|
||||
pos.y = !!e.touches ? e.touches[0].pageY : e.pageY; |
|
||||
|
|
||||
return new Vec2(pos.x - rect.left, pos.y - rect.top); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
function isFunction(obj: any): boolean { |
|
||||
return !!(obj && obj.constructor && obj.call && obj.apply); |
|
||||
}; |
|
||||
|
|
||||
export interface DragDropEvent extends MouseEvent { |
|
||||
readonly dataTransfer: DataTransfer; |
|
||||
} |
|
||||
@ -1,40 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Sebastian Stehle. All rights reserved |
|
||||
*/ |
|
||||
|
|
||||
import { Component, ElementRef } from '@angular/core'; |
|
||||
|
|
||||
declare var Spinner: any; |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'sqx-spinner', |
|
||||
template: '' |
|
||||
}) |
|
||||
export class SpinnerComponent { |
|
||||
constructor(element: ElementRef) { |
|
||||
const mediumOptions = { |
|
||||
lines: 12, |
|
||||
length: 5, |
|
||||
width: 2, |
|
||||
radius: 6, |
|
||||
corners: 1, |
|
||||
rotate: 0, |
|
||||
direction: 1, |
|
||||
color: '#000', |
|
||||
speed: 1.5, |
|
||||
trail: 40, |
|
||||
shadow: false, |
|
||||
hwaccel: false, |
|
||||
className: 'spinner', |
|
||||
zIndex: 0, |
|
||||
position: 'relative' |
|
||||
}; |
|
||||
|
|
||||
element.nativeElement.classList.add('spinner-medium'); |
|
||||
|
|
||||
new Spinner(mediumOptions).spin(element.nativeElement); |
|
||||
} |
|
||||
} |
|
||||
@ -1,42 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Sebastian Stehle. All rights reserved |
|
||||
*/ |
|
||||
|
|
||||
import { |
|
||||
DragService, |
|
||||
DragServiceFactory, |
|
||||
DropEvent |
|
||||
} from './../'; |
|
||||
|
|
||||
describe('DragService', () => { |
|
||||
it('should instantiate from factory', () => { |
|
||||
const dragService = DragServiceFactory(); |
|
||||
|
|
||||
expect(dragService).toBeDefined(); |
|
||||
}); |
|
||||
|
|
||||
it('should instantiate', () => { |
|
||||
const dragService = new DragService(); |
|
||||
|
|
||||
expect(dragService).toBeDefined(); |
|
||||
}); |
|
||||
|
|
||||
it('should raise event handler when dropped', () => { |
|
||||
let emittedEvent: DropEvent | null = null; |
|
||||
|
|
||||
const dragService = new DragService(); |
|
||||
|
|
||||
dragService.onDrop.subscribe(e => { |
|
||||
emittedEvent = e; |
|
||||
}); |
|
||||
|
|
||||
const event: DropEvent = { position: null!, model: null!, dropTarget: null! }; |
|
||||
|
|
||||
dragService.emitDrop(event); |
|
||||
|
|
||||
expect(emittedEvent).toBe(event); |
|
||||
}); |
|
||||
}); |
|
||||
@ -1,30 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Sebastian Stehle. All rights reserved |
|
||||
*/ |
|
||||
|
|
||||
import { Injectable } from '@angular/core'; |
|
||||
import { Observable, Subject } from 'rxjs'; |
|
||||
|
|
||||
import { Vec2 } from './../utils/vec2'; |
|
||||
|
|
||||
export interface DropEvent { position: Vec2; model: any; dropTarget: string; } |
|
||||
|
|
||||
export const DragServiceFactory = () => { |
|
||||
return new DragService(); |
|
||||
}; |
|
||||
|
|
||||
@Injectable() |
|
||||
export class DragService { |
|
||||
private readonly dropEvent = new Subject<DropEvent>(); |
|
||||
|
|
||||
public get onDrop(): Observable<DropEvent> { |
|
||||
return this.dropEvent; |
|
||||
} |
|
||||
|
|
||||
public emitDrop(event: DropEvent) { |
|
||||
this.dropEvent.next(event); |
|
||||
} |
|
||||
} |
|
||||
@ -1,74 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Sebastian Stehle. All rights reserved |
|
||||
*/ |
|
||||
|
|
||||
import { ArrayHelper } from './../'; |
|
||||
|
|
||||
describe('ArrayHelper', () => { |
|
||||
it('should push item', () => { |
|
||||
const oldArray = [1, 2, 3]; |
|
||||
const newArray = ArrayHelper.push(oldArray, 4); |
|
||||
|
|
||||
expect(newArray).toEqual([1, 2, 3, 4]); |
|
||||
}); |
|
||||
|
|
||||
it('should remove item if found', () => { |
|
||||
const oldArray = [1, 2, 3]; |
|
||||
const newArray = ArrayHelper.remove(oldArray, 2); |
|
||||
|
|
||||
expect(newArray).toEqual([1, 3]); |
|
||||
}); |
|
||||
|
|
||||
it('should not remove item if not found', () => { |
|
||||
const oldArray = [1, 2, 3]; |
|
||||
const newArray = ArrayHelper.remove(oldArray, 5); |
|
||||
|
|
||||
expect(newArray).toEqual([1, 2, 3]); |
|
||||
}); |
|
||||
|
|
||||
it('should remove all by predicate', () => { |
|
||||
const oldArray: number[] = [1, 2, 3, 4]; |
|
||||
const newArray = ArrayHelper.removeAll(oldArray, (i: number) => i % 2 === 0); |
|
||||
|
|
||||
expect(newArray).toEqual([1, 3]); |
|
||||
}); |
|
||||
|
|
||||
it('should return original if nothing has been removed', () => { |
|
||||
const oldArray: number[] = [1, 2, 3, 4]; |
|
||||
const newArray = ArrayHelper.removeAll(oldArray, (i: number) => i % 200 === 0); |
|
||||
|
|
||||
expect(newArray).toEqual(oldArray); |
|
||||
}); |
|
||||
|
|
||||
it('should replace item if found', () => { |
|
||||
const oldArray = [1, 2, 3]; |
|
||||
const newArray = ArrayHelper.replace(oldArray, 2, 4); |
|
||||
|
|
||||
expect(newArray).toEqual([1, 4, 3]); |
|
||||
}); |
|
||||
|
|
||||
it('should not replace item if not found', () => { |
|
||||
const oldArray = [1, 2, 3]; |
|
||||
const newArray = ArrayHelper.replace(oldArray, 5, 5); |
|
||||
|
|
||||
expect(newArray).toEqual([1, 2, 3]); |
|
||||
}); |
|
||||
|
|
||||
it('should replace all by predicate', () => { |
|
||||
const oldArray: number[] = [1, 2, 3, 4]; |
|
||||
const newArray = ArrayHelper.replaceAll(oldArray, (i: number) => i % 2 === 0, i => i * 2); |
|
||||
|
|
||||
expect(newArray).toEqual([1, 4, 3, 8]); |
|
||||
}); |
|
||||
|
|
||||
it('should return original if nothing has been replace', () => { |
|
||||
const oldArray: number[] = [1, 2, 3, 4]; |
|
||||
const newArray = ArrayHelper.replaceAll(oldArray, (i: number) => i % 200 === 0, i => i); |
|
||||
|
|
||||
expect(newArray).toEqual(oldArray); |
|
||||
}); |
|
||||
|
|
||||
}); |
|
||||
@ -1,74 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Sebastian Stehle. All rights reserved |
|
||||
*/ |
|
||||
|
|
||||
export module ArrayHelper { |
|
||||
export function push<T>(array: T[], item: T): T[] { |
|
||||
return [...array, item]; |
|
||||
} |
|
||||
|
|
||||
export function remove<T>(array: T[], item: T): T[] { |
|
||||
const index = array.indexOf(item); |
|
||||
|
|
||||
if (index >= 0) { |
|
||||
return [...array.slice(0, index), ...array.slice(index + 1)]; |
|
||||
} else { |
|
||||
return array; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export function removeAll<T>(array: T[], predicate: (item: T, index: number) => boolean): T[] { |
|
||||
const copy = array.slice(); |
|
||||
|
|
||||
let hasChange = false; |
|
||||
|
|
||||
for (let i = 0; i < copy.length; ) { |
|
||||
if (predicate(copy[i], i)) { |
|
||||
copy.splice(i, 1); |
|
||||
|
|
||||
hasChange = true; |
|
||||
} else { |
|
||||
++i; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return hasChange ? copy : array; |
|
||||
} |
|
||||
|
|
||||
export function replace<T>(array: T[], oldItem: T, newItem: T): T[] { |
|
||||
const index = array.indexOf(oldItem); |
|
||||
|
|
||||
if (index >= 0) { |
|
||||
const copy = array.slice(); |
|
||||
|
|
||||
copy[index] = newItem; |
|
||||
|
|
||||
return copy; |
|
||||
} else { |
|
||||
return array; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export function replaceAll<T>(array: T[], predicate: (item: T, index: number) => boolean, replacer: (item: T) => T): T[] { |
|
||||
const copy = array.slice(); |
|
||||
|
|
||||
let hasChange = false; |
|
||||
|
|
||||
for (let i = 0; i < copy.length; i++) { |
|
||||
if (predicate(copy[i], i)) { |
|
||||
const newItem = replacer(copy[i]); |
|
||||
|
|
||||
if (copy[i] !== newItem) { |
|
||||
copy[i] = newItem; |
|
||||
|
|
||||
hasChange = true; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return hasChange ? copy : array; |
|
||||
} |
|
||||
} |
|
||||
@ -1,16 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Sebastian Stehle. All rights reserved |
|
||||
*/ |
|
||||
|
|
||||
import { ColorPalette } from './../'; |
|
||||
|
|
||||
describe('ColorPalatte', () => { |
|
||||
it('should generate colors', () => { |
|
||||
const palette = ColorPalette.colors(); |
|
||||
|
|
||||
expect(palette.colors.length).toBeGreaterThan(20); |
|
||||
}); |
|
||||
}); |
|
||||
@ -1,41 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Sebastian Stehle. All rights reserved |
|
||||
*/ |
|
||||
|
|
||||
import { Color } from './color'; |
|
||||
|
|
||||
const BREWERS = { |
|
||||
greys: [0xffffff, 0xF0F0F0, 0xbdbdbd, 0x969696, 0x737373, 0x525252, 0x252525, 0x000000], |
|
||||
oranges: [0xfee6ce, 0xfdd0a2, 0xfdae6b, 0xfd8d3c, 0xf16913, 0xd94801, 0xa63603, 0x7f2704], |
|
||||
reds: [0xfee0d2, 0xfcbba1, 0xfc9272, 0xfb6a4a, 0xef3b2c, 0xcb181d, 0xa50f15, 0x67000d], |
|
||||
greens: [0xe5f5e0, 0xc7e9c0, 0xa1d99b, 0x74c476, 0x41ab5d, 0x238b45, 0x006d2c, 0x00441b], |
|
||||
purples: [0xefedf5, 0xdadaeb, 0xbcbddc, 0x9e9ac8, 0x807dba, 0x6a51a3, 0x54278f, 0x3f007d], |
|
||||
blues: [0xdeebf7, 0xc6dbef, 0x9ecae1, 0x6baed6, 0x4292c6, 0x2171b5, 0x08519c, 0x08306b] |
|
||||
}; |
|
||||
|
|
||||
export class ColorPalette { |
|
||||
constructor( |
|
||||
public readonly colors: Color[], |
|
||||
public readonly defaultColor: Color |
|
||||
) { |
|
||||
} |
|
||||
|
|
||||
public static colors(): ColorPalette { |
|
||||
const colors: Color[] = []; |
|
||||
|
|
||||
for (let key in BREWERS) { |
|
||||
if (BREWERS.hasOwnProperty(key)) { |
|
||||
const brewer = BREWERS[key]; |
|
||||
|
|
||||
for (let color of brewer) { |
|
||||
colors.push(Color.fromNumber(color)); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return new ColorPalette(colors, colors[7]); |
|
||||
} |
|
||||
} |
|
||||
@ -1,273 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Sebastian Stehle. All rights reserved |
|
||||
*/ |
|
||||
|
|
||||
import { Color } from './../'; |
|
||||
|
|
||||
describe('Color', () => { |
|
||||
it('should instantiate', () => { |
|
||||
const color = new Color(0.3, 0.6, 0.9); |
|
||||
|
|
||||
expect(color.r).toBe(0.3); |
|
||||
expect(color.g).toBe(0.6); |
|
||||
expect(color.b).toBe(0.9); |
|
||||
}); |
|
||||
|
|
||||
it('should adjust values when instantiating', () => { |
|
||||
const color = new Color(-1, 0.5, 2); |
|
||||
|
|
||||
expect(color.r).toBe(0); |
|
||||
expect(color.g).toBe(0.5); |
|
||||
expect(color.b).toBe(1); |
|
||||
}); |
|
||||
|
|
||||
it('should convert to color', () => { |
|
||||
const color = Color.fromHex(23, 46, 59); |
|
||||
|
|
||||
expect(color.toNumber()).toBe(0x234659); |
|
||||
}); |
|
||||
|
|
||||
it('should convert to string', () => { |
|
||||
const color = Color.fromHex(23, 46, 59); |
|
||||
|
|
||||
expect(color.toString()).toBe('#234659'); |
|
||||
}); |
|
||||
|
|
||||
it('should convert to string with leading zeros', () => { |
|
||||
const color = Color.fromHex(3, 6, 9); |
|
||||
|
|
||||
expect(color.toString()).toBe('#030609'); |
|
||||
}); |
|
||||
|
|
||||
it('should be created from long string', () => { |
|
||||
const color = Color.fromValue('#336699'); |
|
||||
|
|
||||
expect(color.r).toBe(0.2); |
|
||||
expect(color.g).toBe(0.4); |
|
||||
expect(color.b).toBe(0.6); |
|
||||
}); |
|
||||
|
|
||||
it('should be created from short string', () => { |
|
||||
const color = Color.fromValue('#369'); |
|
||||
|
|
||||
expect(color.r).toBe(0.2); |
|
||||
expect(color.g).toBe(0.4); |
|
||||
expect(color.b).toBe(0.6); |
|
||||
}); |
|
||||
|
|
||||
it('should be created from rgb string', () => { |
|
||||
const color = Color.fromValue('rgb(51, 102, 153)'); |
|
||||
|
|
||||
expect(color.r).toBe(0.2); |
|
||||
expect(color.g).toBe(0.4); |
|
||||
expect(color.b).toBe(0.6); |
|
||||
}); |
|
||||
|
|
||||
it('should be created from rgb number', () => { |
|
||||
const color = Color.fromValue(0x336699); |
|
||||
|
|
||||
expect(color.r).toBe(0.2); |
|
||||
expect(color.g).toBe(0.4); |
|
||||
expect(color.b).toBe(0.6); |
|
||||
}); |
|
||||
|
|
||||
it('should be created from rgb values', () => { |
|
||||
const color = Color.fromHex(33, 66, 99); |
|
||||
|
|
||||
expect(color.r).toBe(0.2); |
|
||||
expect(color.g).toBe(0.4); |
|
||||
expect(color.b).toBe(0.6); |
|
||||
}); |
|
||||
|
|
||||
it('should convert from hsl with black', () => { |
|
||||
const color = Color.fromHsl(0, 0, 0); |
|
||||
|
|
||||
expect(color.r).toBe(0); |
|
||||
expect(color.g).toBe(0); |
|
||||
expect(color.b).toBe(0); |
|
||||
}); |
|
||||
|
|
||||
it('should convert from hsl with dark black', () => { |
|
||||
const color = Color.fromHsl(0, 1, 0.1); |
|
||||
|
|
||||
expect(color.r).toBe(0.2); |
|
||||
expect(color.g).toBe(0); |
|
||||
expect(color.b).toBe(0); |
|
||||
}); |
|
||||
|
|
||||
it('should convert from hsl with red', () => { |
|
||||
const color = Color.fromHsl(0, 1, 0.5); |
|
||||
|
|
||||
expect(color.r).toBeCloseTo(1, 1); |
|
||||
expect(color.g).toBe(0); |
|
||||
expect(color.b).toBe(0); |
|
||||
}); |
|
||||
|
|
||||
it('should convert from hsl with yellow', () => { |
|
||||
const color = Color.fromHsl(60, 1, 0.5); |
|
||||
|
|
||||
expect(color.r).toBeCloseTo(1, 1); |
|
||||
expect(color.g).toBeCloseTo(1, 1); |
|
||||
expect(color.b).toBe(0); |
|
||||
}); |
|
||||
|
|
||||
it('should convert from hsl with blue', () => { |
|
||||
const color = Color.fromHsl(120, 1, 0.5); |
|
||||
|
|
||||
expect(color.r).toBe(0); |
|
||||
expect(color.g).toBe(1); |
|
||||
expect(color.b).toBe(0); |
|
||||
}); |
|
||||
|
|
||||
it('should convert from hsl with turkis', () => { |
|
||||
const color = Color.fromHsl(180, 1, 0.5); |
|
||||
|
|
||||
expect(color.r).toBe(0); |
|
||||
expect(color.g).toBeCloseTo(1, 1); |
|
||||
expect(color.b).toBeCloseTo(1, 1); |
|
||||
}); |
|
||||
|
|
||||
it('should convert from hsv with red', () => { |
|
||||
const color = Color.fromHsv(240, 1, 1); |
|
||||
|
|
||||
expect(color.r).toBe(0); |
|
||||
expect(color.g).toBe(0); |
|
||||
expect(color.b).toBe(1); |
|
||||
}); |
|
||||
|
|
||||
it('should convert from hsv with pink', () => { |
|
||||
const color = Color.fromHsv(300, 1, 1); |
|
||||
|
|
||||
expect(color.r).toBe(1); |
|
||||
expect(color.g).toBe(0); |
|
||||
expect(color.b).toBe(1); |
|
||||
}); |
|
||||
|
|
||||
it('should convert from hsl with another red', () => { |
|
||||
const color = Color.fromHsl(360, 1, 0.5); |
|
||||
|
|
||||
expect(color.r).toBeCloseTo(1, 1); |
|
||||
expect(color.g).toBe(0); |
|
||||
expect(color.b).toBe(0); |
|
||||
}); |
|
||||
|
|
||||
it('should convert from hsv with red', () => { |
|
||||
const color = Color.fromHsv(0, 1, 1); |
|
||||
|
|
||||
expect(color.r).toBe(1); |
|
||||
expect(color.g).toBe(0); |
|
||||
expect(color.b).toBe(0); |
|
||||
}); |
|
||||
|
|
||||
it('should convert from hsv with yellow', () => { |
|
||||
const color = Color.fromHsv(60, 1, 1); |
|
||||
|
|
||||
expect(color.r).toBe(1); |
|
||||
expect(color.g).toBe(1); |
|
||||
expect(color.b).toBe(0); |
|
||||
}); |
|
||||
|
|
||||
it('should convert from hsv with blue', () => { |
|
||||
const color = Color.fromHsv(120, 1, 1); |
|
||||
|
|
||||
expect(color.r).toBe(0); |
|
||||
expect(color.g).toBe(1); |
|
||||
expect(color.b).toBe(0); |
|
||||
}); |
|
||||
|
|
||||
it('should convert from hsv with turkis', () => { |
|
||||
const color = Color.fromHsv(180, 1, 1); |
|
||||
|
|
||||
expect(color.r).toBe(0); |
|
||||
expect(color.g).toBe(1); |
|
||||
expect(color.b).toBe(1); |
|
||||
}); |
|
||||
|
|
||||
it('should convert from hsv with red', () => { |
|
||||
const color = Color.fromHsv(240, 1, 1); |
|
||||
|
|
||||
expect(color.r).toBe(0); |
|
||||
expect(color.g).toBe(0); |
|
||||
expect(color.b).toBe(1); |
|
||||
}); |
|
||||
|
|
||||
it('should convert from hsv with pink', () => { |
|
||||
const color = Color.fromHsv(300, 1, 1); |
|
||||
|
|
||||
expect(color.r).toBe(1); |
|
||||
expect(color.g).toBe(0); |
|
||||
expect(color.b).toBe(1); |
|
||||
}); |
|
||||
|
|
||||
it('should be valid black', () => { |
|
||||
const color = Color.BLACK; |
|
||||
|
|
||||
expect(color.r).toBe(0); |
|
||||
expect(color.g).toBe(0); |
|
||||
expect(color.b).toBe(0); |
|
||||
}); |
|
||||
|
|
||||
it('should be valid white', () => { |
|
||||
const color = Color.WHITE; |
|
||||
|
|
||||
expect(color.r).toBe(1); |
|
||||
expect(color.g).toBe(1); |
|
||||
expect(color.b).toBe(1); |
|
||||
}); |
|
||||
|
|
||||
it('should be valid red', () => { |
|
||||
const color = Color.RED; |
|
||||
|
|
||||
expect(color.r).toBe(1); |
|
||||
expect(color.g).toBe(0); |
|
||||
expect(color.b).toBe(0); |
|
||||
}); |
|
||||
|
|
||||
it('should be valid green', () => { |
|
||||
const color = Color.GREEN; |
|
||||
|
|
||||
expect(color.r).toBe(0); |
|
||||
expect(color.g).toBe(1); |
|
||||
expect(color.b).toBe(0); |
|
||||
}); |
|
||||
|
|
||||
it('should be valid blue', () => { |
|
||||
const color = Color.BLUE; |
|
||||
|
|
||||
expect(color.r).toBe(0); |
|
||||
expect(color.g).toBe(0); |
|
||||
expect(color.b).toBe(1); |
|
||||
}); |
|
||||
|
|
||||
it('should calculate correct luminance', () => { |
|
||||
expect(new Color(1, 1, 1).luminance).toBe(1); |
|
||||
expect(new Color(0, 0, 0).luminance).toBe(0); |
|
||||
expect(new Color(1, 0, 0).luminance).toBe(1 / 3); |
|
||||
expect(new Color(0, 1, 0).luminance).toBe(1 / 2); |
|
||||
expect(new Color(0, 0, 1).luminance).toBe(1 / 6); |
|
||||
}); |
|
||||
|
|
||||
it('should make valid equal comparisons', () => { |
|
||||
expect(new Color(0.1, 0.1, 0.1).eq(new Color(0.1, 0.1, 0.1))).toBeTruthy(); |
|
||||
expect(new Color(0.1, 0.1, 0.4).eq(new Color(0.1, 0.1, 0.1))).toBeFalsy(); |
|
||||
}); |
|
||||
|
|
||||
it('should make valid not equal comparisons', () => { |
|
||||
expect(new Color(0.1, 0.1, 0.1).ne(new Color(0.1, 0.1, 0.4))).toBeTruthy(); |
|
||||
expect(new Color(0.1, 0.1, 0.1).ne(new Color(0.1, 0.1, 0.1))).toBeFalsy(); |
|
||||
}); |
|
||||
|
|
||||
it('should return color when creating from color', () => { |
|
||||
const color = Color.fromHsv(300, 1, 1); |
|
||||
const created = Color.fromValue(color); |
|
||||
|
|
||||
expect(created).toBe(color); |
|
||||
}); |
|
||||
|
|
||||
it('should throw error for invalid string', () => { |
|
||||
expect(() => Color.fromValue('INVALID')).toThrowError('Color is not in a valid format.'); |
|
||||
}); |
|
||||
}); |
|
||||
@ -1,200 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Sebastian Stehle. All rights reserved |
|
||||
*/ |
|
||||
|
|
||||
/* tslint:disable:no-bitwise */ |
|
||||
|
|
||||
interface IColorDefinition { |
|
||||
regex: RegExp; |
|
||||
|
|
||||
process(bots: RegExpExecArray): Color; |
|
||||
} |
|
||||
|
|
||||
const ColorDefinitions: IColorDefinition[] = [ |
|
||||
{ |
|
||||
regex: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, |
|
||||
process: bits => new Color( |
|
||||
parseInt(bits[1], 10) / 255, |
|
||||
parseInt(bits[2], 10) / 255, |
|
||||
parseInt(bits[3], 10) / 255) |
|
||||
}, |
|
||||
{ |
|
||||
regex: /^(\w{2})(\w{2})(\w{2})$/, |
|
||||
process: bits => new Color( |
|
||||
parseInt(bits[1], 16) / 255, |
|
||||
parseInt(bits[2], 16) / 255, |
|
||||
parseInt(bits[3], 16) / 255) |
|
||||
}, |
|
||||
{ |
|
||||
regex: /^(\w{1})(\w{1})(\w{1})$/, |
|
||||
process: bits => new Color( |
|
||||
parseInt(bits[1] + bits[1], 16) / 255, |
|
||||
parseInt(bits[2] + bits[2], 16) / 255, |
|
||||
parseInt(bits[3] + bits[3], 16) / 255) |
|
||||
} |
|
||||
]; |
|
||||
|
|
||||
export class Color { |
|
||||
public static readonly BLACK = new Color(0, 0, 0); |
|
||||
public static readonly WHITE = new Color(1, 1, 1); |
|
||||
public static readonly GREEN = new Color(0, 1, 0); |
|
||||
public static readonly BLUE = new Color(0, 0, 1); |
|
||||
public static readonly RED = new Color(1, 0, 0); |
|
||||
|
|
||||
public readonly r: number; |
|
||||
public readonly g: number; |
|
||||
public readonly b: number; |
|
||||
|
|
||||
public get luminance(): number { |
|
||||
return (this.r + this.r + this.b + this.g + this.g + this.g) / 6; |
|
||||
} |
|
||||
|
|
||||
constructor(r: number, g: number, b: number) { |
|
||||
this.r = Math.min(1, Math.max(0, r)); |
|
||||
this.g = Math.min(1, Math.max(0, g)); |
|
||||
this.b = Math.min(1, Math.max(0, b)); |
|
||||
|
|
||||
Object.freeze(this); |
|
||||
} |
|
||||
|
|
||||
public eq(v: Color): boolean { |
|
||||
return this.r === v.r && this.g === v.g && this.b === v.b; |
|
||||
} |
|
||||
|
|
||||
public ne(v: Color): boolean { |
|
||||
return this.r !== v.r || this.g !== v.g || this.b !== v.b; |
|
||||
} |
|
||||
|
|
||||
public toNumber(): number { |
|
||||
return ((this.r * 255) << 16) + ((this.g * 255) << 8) + (this.b * 255); |
|
||||
} |
|
||||
|
|
||||
public toString(): string { |
|
||||
let r = Math.round(this.r * 255).toString(16); |
|
||||
let g = Math.round(this.g * 255).toString(16); |
|
||||
let b = Math.round(this.b * 255).toString(16); |
|
||||
|
|
||||
if (r.length === 1) { |
|
||||
r = '0' + r; |
|
||||
} |
|
||||
if (g.length === 1) { |
|
||||
g = '0' + g; |
|
||||
} |
|
||||
if (b.length === 1) { |
|
||||
b = '0' + b; |
|
||||
} |
|
||||
|
|
||||
return '#' + r + g + b; |
|
||||
} |
|
||||
|
|
||||
public static fromHex(r: number, g: number, b: number): Color { |
|
||||
return new Color( |
|
||||
parseInt('' + r, 16) / 255, |
|
||||
parseInt('' + g, 16) / 255, |
|
||||
parseInt('' + b, 16) / 255); |
|
||||
} |
|
||||
|
|
||||
public static fromNumber(rgb: number): Color { |
|
||||
return new Color( |
|
||||
((rgb >> 16) & 0xff) / 255, |
|
||||
((rgb >> 8) & 0xff) / 255, |
|
||||
((rgb >> 0) & 0xff) / 255); |
|
||||
} |
|
||||
|
|
||||
public static fromValue(value: number | string | Color): Color { |
|
||||
if (typeof value === 'string') { |
|
||||
return Color.fromString(value); |
|
||||
} else if (typeof value === 'number') { |
|
||||
return Color.fromNumber(value); |
|
||||
} else { |
|
||||
return value; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public static fromString(value: string) { |
|
||||
if (value.charAt(0) === '#') { |
|
||||
value = value.substr(1, 6); |
|
||||
} |
|
||||
|
|
||||
value = value.replace(/ /g, '').toLowerCase(); |
|
||||
|
|
||||
for (let colorDefinition of ColorDefinitions) { |
|
||||
const bits = colorDefinition.regex.exec(value); |
|
||||
|
|
||||
if (bits) { |
|
||||
return colorDefinition.process(bits); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
throw new Error('Color is not in a valid format.'); |
|
||||
} |
|
||||
|
|
||||
public static fromHsv(h: number, s: number, v: number): Color { |
|
||||
const hi = Math.floor(h / 60) % 6; |
|
||||
|
|
||||
const f = (h / 60) - Math.floor(h / 60); |
|
||||
|
|
||||
v = v * 255; |
|
||||
|
|
||||
const p = (v * (1 - s)); |
|
||||
const q = (v * (1 - (f * s))); |
|
||||
const t = (v * (1 - ((1 - f) * s))); |
|
||||
|
|
||||
switch (hi) { |
|
||||
case 0: |
|
||||
return new Color(v, t, p); |
|
||||
case 1: |
|
||||
return new Color(q, v, p); |
|
||||
case 2: |
|
||||
return new Color(p, v, t); |
|
||||
case 3: |
|
||||
return new Color(p, q, v); |
|
||||
case 4: |
|
||||
return new Color(t, p, v); |
|
||||
default: |
|
||||
return new Color(v, p, q); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public static fromHsl(h: number, s: number, l: number): Color { |
|
||||
let r = 0, g = 0, b = 0; |
|
||||
|
|
||||
h = h / 360; |
|
||||
|
|
||||
if (s === 0) { |
|
||||
r = g = b = l; |
|
||||
} else { |
|
||||
const hue2rgb = (p: number, q: number, t: number) => { |
|
||||
if (t < 0) { |
|
||||
t += 1; |
|
||||
} |
|
||||
|
|
||||
if (t > 1) { |
|
||||
t -= 1; |
|
||||
} |
|
||||
if (t < 1 / 6) { |
|
||||
return p + (q - p) * 6 * t; |
|
||||
} |
|
||||
if (t < 1 / 2) { |
|
||||
return q; |
|
||||
} |
|
||||
if (t < 2 / 3) { |
|
||||
return p + (q - p) * (2 / 3 - t) * 6; |
|
||||
} |
|
||||
return p; |
|
||||
}; |
|
||||
|
|
||||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s; |
|
||||
const p = 2 * l - q; |
|
||||
|
|
||||
r = hue2rgb(p, q, h + 1 / 3); |
|
||||
g = hue2rgb(p, q, h); |
|
||||
b = hue2rgb(p, q, h - 1 / 3); |
|
||||
} |
|
||||
|
|
||||
return new Color(r, g, b); |
|
||||
} |
|
||||
} |
|
||||
@ -1,237 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Sebastian Stehle. All rights reserved |
|
||||
*/ |
|
||||
|
|
||||
import { |
|
||||
Rect2, |
|
||||
Rotation, |
|
||||
Vec2 |
|
||||
} from './../'; |
|
||||
|
|
||||
describe('Rect2', () => { |
|
||||
it('should provide values from constructor', () => { |
|
||||
const rect = new Rect2(new Vec2(10, 20), new Vec2(50, 30)); |
|
||||
|
|
||||
expect(rect.x).toBe(10); |
|
||||
expect(rect.y).toBe(20); |
|
||||
expect(rect.top).toBe(20); |
|
||||
expect(rect.left).toBe(10); |
|
||||
expect(rect.right).toBe(60); |
|
||||
expect(rect.width).toBe(50); |
|
||||
expect(rect.height).toBe(30); |
|
||||
expect(rect.bottom).toBe(50); |
|
||||
expect(rect.centerX).toBe(35); |
|
||||
expect(rect.centerY).toBe(35); |
|
||||
|
|
||||
expect(rect.size).toEqual(new Vec2(50, 30)); |
|
||||
expect(rect.center).toEqual(new Vec2(35, 35)); |
|
||||
expect(rect.position).toEqual(new Vec2(10, 20)); |
|
||||
|
|
||||
expect(rect.area).toBe(1500); |
|
||||
|
|
||||
expect(rect.toString()).toBe('(x: 10, y: 20, w: 50, h: 30)'); |
|
||||
}); |
|
||||
|
|
||||
it('should calculate isEmpty correctly', () => { |
|
||||
expect(new Rect2(new Vec2(10, 20), new Vec2(50, 30)).isEmpty).toBeFalsy(); |
|
||||
expect(new Rect2(new Vec2(10, 20), new Vec2(-1, 30)).isEmpty).toBeTruthy(); |
|
||||
expect(new Rect2(new Vec2(10, 20), new Vec2(50, -9)).isEmpty).toBeTruthy(); |
|
||||
}); |
|
||||
|
|
||||
it('should calculate isInfinite correctly', () => { |
|
||||
expect(new Rect2(new Vec2(10, 20), new Vec2(50, 30)).isInfinite).toBeFalsy(); |
|
||||
expect(new Rect2(new Vec2(10, 20), new Vec2(Number.POSITIVE_INFINITY, 30)).isInfinite).toBeTruthy(); |
|
||||
expect(new Rect2(new Vec2(10, 20), new Vec2(50, Number.POSITIVE_INFINITY)).isInfinite).toBeTruthy(); |
|
||||
}); |
|
||||
|
|
||||
it('should inflate correctly', () => { |
|
||||
const rect = new Rect2(new Vec2(10, 20), new Vec2(50, 30)); |
|
||||
|
|
||||
const actual1 = rect.inflateV(new Vec2(10, 20)); |
|
||||
const actual2 = rect.inflate(10, 20); |
|
||||
const expected = new Rect2(new Vec2(0, 0), new Vec2(70, 70)); |
|
||||
|
|
||||
expect(actual1).toEqual(expected); |
|
||||
expect(actual2).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should deflate correctly', () => { |
|
||||
const rect = new Rect2(new Vec2(10, 20), new Vec2(50, 30)); |
|
||||
|
|
||||
const actual1 = rect.deflateV(new Vec2(25, 15)); |
|
||||
const actual2 = rect.deflate(25, 15); |
|
||||
const expected = new Rect2(new Vec2(35, 35), new Vec2(0, 0)); |
|
||||
|
|
||||
expect(actual1).toEqual(expected); |
|
||||
expect(actual2).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should return true for intersection with infinite rect', () => { |
|
||||
const rect = new Rect2(new Vec2(10, 20), new Vec2(50, 30)); |
|
||||
|
|
||||
expect(rect.intersectsWith(Rect2.INFINITE)).toBeTruthy(); |
|
||||
|
|
||||
expect(Rect2.INFINITE.intersectsWith(rect)).toBeTruthy(); |
|
||||
expect(Rect2.INFINITE.intersectsWith(Rect2.INFINITE)).toBeTruthy(); |
|
||||
}); |
|
||||
|
|
||||
it('should return false for intersection with empty rect', () => { |
|
||||
const rect = new Rect2(new Vec2(10, 20), new Vec2(50, 30)); |
|
||||
|
|
||||
expect(rect.intersectsWith(Rect2.EMPTY)).toBeFalsy(); |
|
||||
|
|
||||
expect(Rect2.EMPTY.intersectsWith(rect)).toBeFalsy(); |
|
||||
expect(Rect2.EMPTY.intersectsWith(Rect2.INFINITE)).toBeFalsy(); |
|
||||
}); |
|
||||
|
|
||||
it('should return empty for no intersection', () => { |
|
||||
const rect = new Rect2(new Vec2(10, 20), new Vec2(50, 30)); |
|
||||
const outer = new Rect2(new Vec2(100, 20), new Vec2(50, 30)); |
|
||||
|
|
||||
const actual = rect.intersect(outer); |
|
||||
const expected = Rect2.EMPTY; |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should return result for intersection', () => { |
|
||||
const rect = new Rect2(new Vec2(10, 20), new Vec2(50, 30)); |
|
||||
const inner = new Rect2(new Vec2(35, 35), new Vec2(100, 30)); |
|
||||
|
|
||||
const actual = rect.intersect(inner); |
|
||||
const expected = new Rect2(new Vec2(35, 35), new Vec2(25, 15)); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should make correct contains by vector tests', () => { |
|
||||
const rect = new Rect2(new Vec2(10, 20), new Vec2(50, 30)); |
|
||||
|
|
||||
expect(rect.containsVec(rect.center)).toBeTruthy(); |
|
||||
expect(rect.containsVec(new Vec2(rect.left, rect.top))).toBeTruthy(); |
|
||||
|
|
||||
expect(rect.containsVec(new Vec2(rect.centerX, 0))).toBeFalsy(); |
|
||||
expect(rect.containsVec(new Vec2(rect.centerX, 100))).toBeFalsy(); |
|
||||
expect(rect.containsVec(new Vec2(100, rect.centerY))).toBeFalsy(); |
|
||||
expect(rect.containsVec(new Vec2(-50, rect.centerY))).toBeFalsy(); |
|
||||
}); |
|
||||
|
|
||||
it('should return true when rect contains other rect', () => { |
|
||||
const rect = new Rect2(new Vec2(400, 400), new Vec2(400, 400)); |
|
||||
const other = new Rect2(new Vec2(500, 500), new Vec2(200, 200)); |
|
||||
|
|
||||
expect(rect.containsRect(other)).toBeTruthy(); |
|
||||
}); |
|
||||
|
|
||||
it('should return false when other rect is too top', () => { |
|
||||
const rect = new Rect2(new Vec2(400, 400), new Vec2(400, 400)); |
|
||||
const other = new Rect2(new Vec2(300, 900), new Vec2(300, 100)); |
|
||||
|
|
||||
expect(rect.containsRect(other)).toBeFalsy(); |
|
||||
}); |
|
||||
|
|
||||
it('should return false when other rect is too bottom', () => { |
|
||||
const rect = new Rect2(new Vec2(400, 400), new Vec2(400, 400)); |
|
||||
const other = new Rect2(new Vec2(300, 900), new Vec2(100, 300)); |
|
||||
|
|
||||
expect(rect.containsRect(other)).toBeFalsy(); |
|
||||
}); |
|
||||
|
|
||||
it('should return false when other rect is too left', () => { |
|
||||
const rect = new Rect2(new Vec2(400, 400), new Vec2(400, 400)); |
|
||||
const other = new Rect2(new Vec2(200, 200), new Vec2(100, 300)); |
|
||||
|
|
||||
expect(rect.containsRect(other)).toBeFalsy(); |
|
||||
}); |
|
||||
|
|
||||
it('should return false when other right is too left', () => { |
|
||||
const rect = new Rect2(new Vec2(400, 400), new Vec2(400, 400)); |
|
||||
const other = new Rect2(new Vec2(900, 200), new Vec2(100, 300)); |
|
||||
|
|
||||
expect(rect.containsRect(other)).toBeFalsy(); |
|
||||
}); |
|
||||
|
|
||||
it('should return empty when creating from null vectors', () => { |
|
||||
const actual = Rect2.createFromVecs(null); |
|
||||
const expected = Rect2.EMPTY; |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should return empty when creating from null rects', () => { |
|
||||
const actual = Rect2.createFromRects(null); |
|
||||
const expected = Rect2.EMPTY; |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should provide valid zero instance', () => { |
|
||||
const actual = Rect2.ZERO; |
|
||||
const expected = new Rect2(new Vec2(0, 0), new Vec2(0, 0)); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should provide valid empty instance', () => { |
|
||||
const actual = Rect2.EMPTY; |
|
||||
const expected = new Rect2(Vec2.NEGATIVE_INFINITE, Vec2.NEGATIVE_INFINITE); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should provide valid infinite instance', () => { |
|
||||
const actual = Rect2.INFINITE; |
|
||||
const expected = new Rect2(Vec2.POSITIVE_INFINITE, Vec2.POSITIVE_INFINITE); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should create correct rect from vectors', () => { |
|
||||
const actual = |
|
||||
Rect2.createFromVecs([ |
|
||||
new Vec2(100, 100), |
|
||||
new Vec2(500, 300), |
|
||||
new Vec2(900, 800)]); |
|
||||
const expected = new Rect2(new Vec2(100, 100), new Vec2(800, 700)); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should create correct rect from rects', () => { |
|
||||
const actual = |
|
||||
Rect2.createFromRects([ |
|
||||
new Rect2(new Vec2(100, 100), new Vec2(100, 100)), |
|
||||
new Rect2(new Vec2(500, 300), new Vec2(100, 100)), |
|
||||
new Rect2(new Vec2(150, 150), new Vec2(750, 650))]); |
|
||||
const expected = new Rect2(new Vec2(100, 100), new Vec2(800, 700)); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should create rect from rotation', () => { |
|
||||
const actual = Rect2.createRotated(new Vec2(400, 300), new Vec2(600, 400), Rotation.createFromRadian(Math.PI / 2)); |
|
||||
const expected = new Rect2(new Vec2(500, 200), new Vec2(400, 600)); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should create rect from zero rotation', () => { |
|
||||
const actual = Rect2.createRotated(new Vec2(400, 300), new Vec2(600, 400), Rotation.ZERO); |
|
||||
const expected = new Rect2(new Vec2(400, 300), new Vec2(600, 400)); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should make valid equal comparisons', () => { |
|
||||
expect(new Rect2(new Vec2(10, 10), new Vec2(10, 10)).eq(new Rect2(new Vec2(10, 10), new Vec2(10, 10)))).toBeTruthy(); |
|
||||
expect(new Rect2(new Vec2(10, 10), new Vec2(10, 10)).eq(new Rect2(new Vec2(20, 20), new Vec2(20, 20)))).toBeFalsy(); |
|
||||
}); |
|
||||
|
|
||||
it('should make valid not equal comparisons', () => { |
|
||||
expect(new Rect2(new Vec2(10, 10), new Vec2(10, 10)).ne(new Rect2(new Vec2(10, 10), new Vec2(10, 10)))).toBeFalsy(); |
|
||||
expect(new Rect2(new Vec2(10, 10), new Vec2(10, 10)).ne(new Rect2(new Vec2(20, 20), new Vec2(20, 20)))).toBeTruthy(); |
|
||||
}); |
|
||||
}); |
|
||||
@ -1,191 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Sebastian Stehle. All rights reserved |
|
||||
*/ |
|
||||
|
|
||||
import { Rotation } from './rotation'; |
|
||||
import { Vec2 } from './vec2'; |
|
||||
|
|
||||
export class Rect2 { |
|
||||
public static readonly ZERO = new Rect2(Vec2.ZERO, Vec2.ZERO); |
|
||||
public static readonly EMPTY = new Rect2(Vec2.NEGATIVE_INFINITE, Vec2.NEGATIVE_INFINITE); |
|
||||
public static readonly INFINITE = new Rect2(Vec2.POSITIVE_INFINITE, Vec2.POSITIVE_INFINITE); |
|
||||
|
|
||||
public get center(): Vec2 { |
|
||||
return new Vec2(this.position.x + (0.5 * this.size.x), this.position.y + (0.5 * this.size.y)); |
|
||||
} |
|
||||
|
|
||||
public get area(): number { |
|
||||
return this.size.x * this.size.y; |
|
||||
} |
|
||||
|
|
||||
public get x(): number { |
|
||||
return this.position.x; |
|
||||
} |
|
||||
|
|
||||
public get y(): number { |
|
||||
return this.position.y; |
|
||||
} |
|
||||
|
|
||||
public get left(): number { |
|
||||
return this.position.x; |
|
||||
} |
|
||||
|
|
||||
public get top(): number { |
|
||||
return this.position.y; |
|
||||
} |
|
||||
|
|
||||
public get right(): number { |
|
||||
return this.position.x + this.size.x; |
|
||||
} |
|
||||
|
|
||||
public get bottom(): number { |
|
||||
return this.position.y + this.size.y; |
|
||||
} |
|
||||
|
|
||||
public get width(): number { |
|
||||
return this.size.x; |
|
||||
} |
|
||||
|
|
||||
public get height(): number { |
|
||||
return this.size.y; |
|
||||
} |
|
||||
|
|
||||
public get centerX(): number { |
|
||||
return this.position.x + (0.5 * this.size.x); |
|
||||
} |
|
||||
|
|
||||
public get centerY(): number { |
|
||||
return this.position.y + (0.5 * this.size.y); |
|
||||
} |
|
||||
|
|
||||
public get isEmpty(): boolean { |
|
||||
return this.size.x < 0 || this.size.y < 0; |
|
||||
} |
|
||||
|
|
||||
public get isInfinite(): boolean { |
|
||||
return this.size.x === Number.POSITIVE_INFINITY || this.size.y === Number.POSITIVE_INFINITY; |
|
||||
} |
|
||||
|
|
||||
constructor( |
|
||||
public readonly position: Vec2, |
|
||||
public readonly size: Vec2 |
|
||||
) { |
|
||||
} |
|
||||
|
|
||||
public static createFromVecs(vecs: Vec2[] | null): Rect2 { |
|
||||
if (!vecs || vecs.length === 0) { |
|
||||
return Rect2.EMPTY; |
|
||||
} |
|
||||
|
|
||||
let minX = Number.MAX_VALUE; |
|
||||
let minY = Number.MAX_VALUE; |
|
||||
let maxX = Number.MIN_VALUE; |
|
||||
let maxY = Number.MIN_VALUE; |
|
||||
|
|
||||
for (let v of vecs) { |
|
||||
minX = Math.min(minX, v.x); |
|
||||
minY = Math.min(minY, v.y); |
|
||||
maxX = Math.max(maxX, v.x); |
|
||||
maxY = Math.max(maxY, v.y); |
|
||||
} |
|
||||
|
|
||||
return new Rect2(new Vec2(minX, minY), new Vec2(Math.max(0, maxX - minX), Math.max(0, maxY - minY))); |
|
||||
} |
|
||||
|
|
||||
public static createFromRects(rects: Rect2[] | null): Rect2 { |
|
||||
if (!rects || rects.length === 0) { |
|
||||
return Rect2.EMPTY; |
|
||||
} |
|
||||
|
|
||||
let minX = Number.MAX_VALUE; |
|
||||
let minY = Number.MAX_VALUE; |
|
||||
let maxX = Number.MIN_VALUE; |
|
||||
let maxY = Number.MIN_VALUE; |
|
||||
|
|
||||
for (let r of rects) { |
|
||||
minX = Math.min(minX, r.left); |
|
||||
minY = Math.min(minY, r.top); |
|
||||
maxX = Math.max(maxX, r.right); |
|
||||
maxY = Math.max(maxY, r.bottom); |
|
||||
} |
|
||||
|
|
||||
return new Rect2(new Vec2(minX, minY), new Vec2(Math.max(0, maxX - minX), Math.max(0, maxY - minY))); |
|
||||
} |
|
||||
|
|
||||
public static createRotated(position: Vec2, size: Vec2, rotation: Rotation): Rect2 { |
|
||||
const x = position.x; |
|
||||
const y = position.y; |
|
||||
const w = size.x; |
|
||||
const h = size.y; |
|
||||
|
|
||||
if (Math.abs(rotation.sin) < Number.EPSILON) { |
|
||||
return new Rect2(position, size); |
|
||||
} |
|
||||
|
|
||||
const center = new Vec2(x + (w * 0.5), y + (h * 0.5)); |
|
||||
|
|
||||
const lt = Vec2.createRotated(new Vec2(x + 0, y + 0), center, rotation); |
|
||||
const rt = Vec2.createRotated(new Vec2(x + w, y + 0), center, rotation); |
|
||||
const rb = Vec2.createRotated(new Vec2(x + w, y + h), center, rotation); |
|
||||
const lb = Vec2.createRotated(new Vec2(x + 0, y + h), center, rotation); |
|
||||
|
|
||||
return Rect2.createFromVecs([lb, lt, rb, rt]); |
|
||||
} |
|
||||
|
|
||||
public eq(r: Rect2): boolean { |
|
||||
return this.position.eq(r.position) && this.size.eq(r.size); |
|
||||
} |
|
||||
|
|
||||
public ne(r: Rect2): boolean { |
|
||||
return this.position.ne(r.position) || this.size.ne(r.size); |
|
||||
} |
|
||||
|
|
||||
public toString(): string { |
|
||||
return `(x: ${this.x}, y: ${this.y}, w: ${this.width}, h: ${this.height})`; |
|
||||
} |
|
||||
|
|
||||
public inflateV(v: Vec2): Rect2 { |
|
||||
return this.inflate(v.x, v.y); |
|
||||
} |
|
||||
|
|
||||
public inflate(w: number, h: number): Rect2 { |
|
||||
return new Rect2(new Vec2(this.position.x - w, this.position.y - h), new Vec2(this.size.x + (2 * w), this.size.y + (2 * h))); |
|
||||
} |
|
||||
|
|
||||
public deflateV(v: Vec2): Rect2 { |
|
||||
return this.deflate(v.x, v.y); |
|
||||
} |
|
||||
|
|
||||
public deflate(w: number, h: number): Rect2 { |
|
||||
return new Rect2(new Vec2(this.position.x + w, this.position.y + h), new Vec2(Math.max(0, this.size.x - (2 * w)), Math.max(0, this.size.y - (2 * h)))); |
|
||||
} |
|
||||
|
|
||||
public containsRect(r: Rect2): boolean { |
|
||||
return r.left >= this.left && r.right <= this.right && r.top >= this.top && r.bottom <= this.bottom; |
|
||||
} |
|
||||
|
|
||||
public containsVec(v: Vec2): boolean { |
|
||||
return v.x >= this.position.x && v.x - this.size.x <= this.position.x && v.y >= this.position.y && v.y - this.size.y <= this.position.y; |
|
||||
} |
|
||||
|
|
||||
public intersectsWith(r: Rect2): boolean { |
|
||||
return !this.isEmpty && !r.isEmpty && ((r.left <= this.right && r.right >= this.left && r.top <= this.bottom && r.bottom >= this.top) || this.isInfinite || r.isInfinite); |
|
||||
} |
|
||||
|
|
||||
public intersect(r: Rect2): Rect2 { |
|
||||
if (!this.intersectsWith(r)) { |
|
||||
return Rect2.EMPTY; |
|
||||
} |
|
||||
|
|
||||
const minX = Math.max(this.x, r.x); |
|
||||
const minY = Math.max(this.y, r.y); |
|
||||
|
|
||||
const w = Math.min(this.position.x + this.size.x, r.position.x + r.size.x) - minX; |
|
||||
const h = Math.min(this.position.y + this.size.y, r.position.y + r.size.y) - minY; |
|
||||
|
|
||||
return new Rect2(new Vec2(minX, minY), new Vec2(Math.max(w, 0.0), Math.max(h, 0.0))); |
|
||||
} |
|
||||
} |
|
||||
@ -1,73 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Sebastian Stehle. All rights reserved |
|
||||
*/ |
|
||||
|
|
||||
import { Rotation } from './../'; |
|
||||
|
|
||||
describe('Rotation', () => { |
|
||||
it('should sub correctly', () => { |
|
||||
const rotation1 = Rotation.createFromDegree(180); |
|
||||
const rotation2 = Rotation.createFromDegree(45); |
|
||||
|
|
||||
const actual = rotation1.sub(rotation2); |
|
||||
const expected = Rotation.createFromDegree(135); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should add correctly', () => { |
|
||||
const rotation1 = Rotation.createFromDegree(180); |
|
||||
const rotation2 = Rotation.createFromDegree(45); |
|
||||
|
|
||||
const actual = rotation1.add(rotation2); |
|
||||
const expected = Rotation.createFromDegree(225); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should calculate negated rotation', () => { |
|
||||
const rotation = Rotation.createFromDegree(180); |
|
||||
|
|
||||
const actual = rotation.negate(); |
|
||||
const expected = Rotation.createFromDegree(-180); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should create rotation by degree', () => { |
|
||||
const rotation = Rotation.createFromDegree(180); |
|
||||
|
|
||||
expect(rotation.degree).toBe(180); |
|
||||
expect(rotation.radian).toBe(Math.PI); |
|
||||
|
|
||||
expect(rotation.cos).toBe(Math.cos(Math.PI)); |
|
||||
expect(rotation.sin).toBe(Math.sin(Math.PI)); |
|
||||
|
|
||||
expect(rotation.toString()).toBe('180°'); |
|
||||
}); |
|
||||
|
|
||||
it('should create rotation by radian', () => { |
|
||||
const rotation = Rotation.createFromRadian(Math.PI); |
|
||||
|
|
||||
expect(rotation.degree).toBe(180); |
|
||||
expect(rotation.radian).toBe(Math.PI); |
|
||||
|
|
||||
expect(rotation.cos).toBe(Math.cos(Math.PI)); |
|
||||
expect(rotation.sin).toBe(Math.sin(Math.PI)); |
|
||||
|
|
||||
expect(rotation.toString()).toBe('180°'); |
|
||||
}); |
|
||||
|
|
||||
it('should make correct equal comparisons', () => { |
|
||||
expect(Rotation.createFromDegree(123).eq(Rotation.createFromDegree(123))).toBeTruthy(); |
|
||||
expect(Rotation.createFromDegree(123).eq(Rotation.createFromDegree(234))).toBeFalsy(); |
|
||||
}); |
|
||||
|
|
||||
it('should make correct not equal comparisons', () => { |
|
||||
expect(Rotation.createFromDegree(123).ne(Rotation.createFromDegree(123))).toBeFalsy(); |
|
||||
expect(Rotation.createFromDegree(123).ne(Rotation.createFromDegree(234))).toBeTruthy(); |
|
||||
}); |
|
||||
}); |
|
||||
@ -1,57 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Sebastian Stehle. All rights reserved |
|
||||
*/ |
|
||||
|
|
||||
import { MathHelper } from './math-helper'; |
|
||||
|
|
||||
export class Rotation { |
|
||||
public static readonly ZERO = Rotation.createFromRadian(0); |
|
||||
|
|
||||
public cos: number; |
|
||||
public sin: number; |
|
||||
|
|
||||
constructor( |
|
||||
public readonly radian: number, |
|
||||
public readonly degree: number |
|
||||
) { |
|
||||
this.cos = Math.cos(radian); |
|
||||
this.sin = Math.sin(radian); |
|
||||
|
|
||||
Object.freeze(this); |
|
||||
} |
|
||||
|
|
||||
public static createFromRadian(radian: number): Rotation { |
|
||||
return new Rotation(radian, MathHelper.toDegree(radian)); |
|
||||
} |
|
||||
|
|
||||
public static createFromDegree(degree: number): Rotation { |
|
||||
return new Rotation(MathHelper.toRad(degree), degree); |
|
||||
} |
|
||||
|
|
||||
public eq(r: Rotation): boolean { |
|
||||
return this.radian === r.radian; |
|
||||
} |
|
||||
|
|
||||
public ne(r: Rotation): boolean { |
|
||||
return this.radian !== r.radian; |
|
||||
} |
|
||||
|
|
||||
public toString(): string { |
|
||||
return `${this.degree}°`; |
|
||||
} |
|
||||
|
|
||||
public add(r: Rotation): Rotation { |
|
||||
return Rotation.createFromDegree(MathHelper.toPositiveDegree(this.degree + r.degree)); |
|
||||
} |
|
||||
|
|
||||
public sub(r: Rotation): Rotation { |
|
||||
return Rotation.createFromDegree(MathHelper.toPositiveDegree(this.degree - r.degree)); |
|
||||
} |
|
||||
|
|
||||
public negate(): Rotation { |
|
||||
return Rotation.createFromDegree(-this.degree); |
|
||||
} |
|
||||
} |
|
||||
@ -1,166 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Sebastian Stehle. All rights reserved |
|
||||
*/ |
|
||||
|
|
||||
import { Rotation, Vec2 } from './../'; |
|
||||
|
|
||||
describe('Vec2', () => { |
|
||||
it('should instantiate from x and y value', () => { |
|
||||
const v = new Vec2(10, 20); |
|
||||
|
|
||||
expect(v.x).toBe(10); |
|
||||
expect(v.y).toBe(20); |
|
||||
|
|
||||
expect(v.toString()).toBe('(10, 20)'); |
|
||||
}); |
|
||||
|
|
||||
it('should make valid equal comparisons', () => { |
|
||||
expect(new Vec2(10, 10).eq(new Vec2(10, 10))).toBeTruthy(); |
|
||||
expect(new Vec2(10, 10).eq(new Vec2(20, 20))).toBeFalsy(); |
|
||||
}); |
|
||||
|
|
||||
it('should make valid not equal comparisons', () => { |
|
||||
expect(new Vec2(10, 10).ne(new Vec2(20, 20))).toBeTruthy(); |
|
||||
expect(new Vec2(10, 10).ne(new Vec2(10, 10))).toBeFalsy(); |
|
||||
}); |
|
||||
|
|
||||
it('should round with default value', () => { |
|
||||
const actual = new Vec2(1.3, 1.6).round(); |
|
||||
const expected = new Vec2(1, 2); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should calculate multiple of 10', () => { |
|
||||
const actual = new Vec2(13, 16).round(10); |
|
||||
const expected = new Vec2(10, 20); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should calculate multiple of 2', () => { |
|
||||
const actual = new Vec2(13, 12.2).roundToMultipleOfTwo(); |
|
||||
const expected = new Vec2(14, 12); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should add by vector correctly', () => { |
|
||||
const actual = new Vec2(15, 12).add(new Vec2(4, 1)); |
|
||||
const expected = new Vec2(19, 13); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should add by scalar correctly', () => { |
|
||||
const actual = new Vec2(15, 12).addScalar(3); |
|
||||
const expected = new Vec2(18, 15); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should subtract by vector correctly', () => { |
|
||||
const actual = new Vec2(15, 12).sub(new Vec2(4, 1)); |
|
||||
const expected = new Vec2(11, 11); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should subtract by scalar correctly', () => { |
|
||||
const actual = new Vec2(15, 12).subScalar(3); |
|
||||
const expected = new Vec2(12, 9); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should multiply by vector correctly', () => { |
|
||||
const actual = new Vec2(15, 12).mul(new Vec2(4, 2)); |
|
||||
const expected = new Vec2(60, 24); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should multiply by scalar correctly', () => { |
|
||||
const actual = new Vec2(15, 12).mulScalar(3); |
|
||||
const expected = new Vec2(45, 36); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should divide by vector correctly', () => { |
|
||||
const actual = new Vec2(15, 12).div(new Vec2(5, 2)); |
|
||||
const expected = new Vec2(3, 6); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should divide by scalar correctly', () => { |
|
||||
const actual = new Vec2(15, 12).divScalar(3); |
|
||||
const expected = new Vec2(5, 4); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should negate correctly', () => { |
|
||||
const actual = new Vec2(15, 12).negate(); |
|
||||
const expected = new Vec2(-15, -12); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should calculate max vector', () => { |
|
||||
const actual = Vec2.max(new Vec2(20, 10), new Vec2(15, 30)); |
|
||||
const expected = new Vec2(20, 30); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should calculate min vector', () => { |
|
||||
const actual = Vec2.min(new Vec2(20, 10), new Vec2(15, 30)); |
|
||||
const expected = new Vec2(15, 10); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should calculate length', () => { |
|
||||
const actual = new Vec2(10, 10).length; |
|
||||
const expected = Math.sqrt(200); |
|
||||
|
|
||||
expect(actual).toBe(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should calculate length squared', () => { |
|
||||
const actual = new Vec2(10, 10).lengtSquared; |
|
||||
const expected = 200; |
|
||||
|
|
||||
expect(actual).toBe(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should calculate median', () => { |
|
||||
const actual = Vec2.createMedian(new Vec2(10, 20), new Vec2(20, 20), new Vec2(60, 20)); |
|
||||
const expected = new Vec2(30, 20); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should calculate rotated vector correctly', () => { |
|
||||
const source = new Vec2(40, 20); |
|
||||
const center = new Vec2(20, 20); |
|
||||
|
|
||||
const rotation = Rotation.createFromRadian(Math.PI / 2); |
|
||||
|
|
||||
const actual = Vec2.createRotated(source, center, rotation); |
|
||||
const expected = new Vec2(20, 40); |
|
||||
|
|
||||
expect(actual).toEqual(expected); |
|
||||
}); |
|
||||
|
|
||||
it('should calculate angles between vectors', () => { |
|
||||
expect(Vec2.angleBetween(new Vec2(1, 0), new Vec2(1, 0))).toBe(0); |
|
||||
expect(Vec2.angleBetween(new Vec2(1, 0), new Vec2(1, 1))).toBe(45); |
|
||||
expect(Vec2.angleBetween(new Vec2(1, 0), new Vec2(0, 1))).toBe(90); |
|
||||
}); |
|
||||
}); |
|
||||
@ -1,127 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Sebastian Stehle. All rights reserved |
|
||||
*/ |
|
||||
|
|
||||
import { MathHelper } from './math-helper'; |
|
||||
import { Rotation } from './rotation'; |
|
||||
|
|
||||
export class Vec2 { |
|
||||
public static readonly ZERO = new Vec2(0, 0); |
|
||||
|
|
||||
public static readonly ONE = new Vec2(1, 1); |
|
||||
|
|
||||
public static readonly POSITIVE_INFINITE = new Vec2(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY); |
|
||||
public static readonly NEGATIVE_INFINITE = new Vec2(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY); |
|
||||
|
|
||||
public get length(): number { |
|
||||
return Math.sqrt(this.x * this.x + this.y * this.y); |
|
||||
} |
|
||||
|
|
||||
public get lengtSquared(): number { |
|
||||
return this.x * this.x + this.y * this.y; |
|
||||
} |
|
||||
|
|
||||
constructor( |
|
||||
public readonly x: number, |
|
||||
public readonly y: number |
|
||||
) { |
|
||||
Object.freeze(this); |
|
||||
} |
|
||||
|
|
||||
public eq(v: Vec2): boolean { |
|
||||
return this.x === v.x && this.y === v.y; |
|
||||
} |
|
||||
|
|
||||
public ne(v: Vec2): boolean { |
|
||||
return this.x !== v.x || this.y !== v.y; |
|
||||
} |
|
||||
|
|
||||
public toString(): string { |
|
||||
return `(${this.x}, ${this.y})`; |
|
||||
} |
|
||||
|
|
||||
public add(v: Vec2): Vec2 { |
|
||||
return new Vec2(this.x + v.x, this.y + v.y); |
|
||||
} |
|
||||
|
|
||||
public addScalar(s: number): Vec2 { |
|
||||
return new Vec2(this.x + s, this.y + s); |
|
||||
} |
|
||||
|
|
||||
public sub(v: Vec2): Vec2 { |
|
||||
return new Vec2(this.x - v.x, this.y - v.y); |
|
||||
} |
|
||||
|
|
||||
public subScalar(s: number): Vec2 { |
|
||||
return new Vec2(this.x - s, this.y - s); |
|
||||
} |
|
||||
|
|
||||
public mul(v: Vec2): Vec2 { |
|
||||
return new Vec2(this.x * v.x, this.y * v.y); |
|
||||
} |
|
||||
|
|
||||
public mulScalar(s: number): Vec2 { |
|
||||
return new Vec2(this.x * s, this.y * s); |
|
||||
} |
|
||||
|
|
||||
public div(v: Vec2): Vec2 { |
|
||||
return new Vec2(this.x / v.x, this.y / v.y); |
|
||||
} |
|
||||
|
|
||||
public divScalar(s: number): Vec2 { |
|
||||
return new Vec2(this.x / s, this.y / s); |
|
||||
} |
|
||||
|
|
||||
public negate(): Vec2 { |
|
||||
return new Vec2(-this.x, -this.y); |
|
||||
} |
|
||||
|
|
||||
public static max(lhs: Vec2, rhs: Vec2): Vec2 { |
|
||||
return new Vec2(Math.max(lhs.x, rhs.x), Math.max(lhs.y, rhs.y)); |
|
||||
} |
|
||||
|
|
||||
public static min(lhs: Vec2, rhs: Vec2): Vec2 { |
|
||||
return new Vec2(Math.min(lhs.x, rhs.x), Math.min(lhs.y, rhs.y)); |
|
||||
} |
|
||||
|
|
||||
public round(factor = 1): Vec2 { |
|
||||
return new Vec2(MathHelper.roundToMultipleOf(this.x, factor), MathHelper.roundToMultipleOf(this.y, factor)); |
|
||||
} |
|
||||
|
|
||||
public roundToMultipleOfTwo(): Vec2 { |
|
||||
return new Vec2(MathHelper.roundToMultipleOf(this.x, 2), MathHelper.roundToMultipleOf(this.y, 2)); |
|
||||
} |
|
||||
|
|
||||
public static createRotated(vec: Vec2, center: Vec2, rotation: Rotation): Vec2 { |
|
||||
const x = vec.x - center.x; |
|
||||
const y = vec.y - center.y; |
|
||||
|
|
||||
const result = new Vec2( |
|
||||
(x * rotation.cos) - (y * rotation.sin) + center.x, |
|
||||
(x * rotation.sin) + (y * rotation.cos) + center.y); |
|
||||
|
|
||||
return result; |
|
||||
} |
|
||||
|
|
||||
public static createMedian(...vecs: Vec2[]) { |
|
||||
let medianX = 0; |
|
||||
let medianY = 0; |
|
||||
|
|
||||
for (let v of vecs) { |
|
||||
medianX += v.x; |
|
||||
medianY += v.y; |
|
||||
} |
|
||||
|
|
||||
return new Vec2(medianX / vecs.length, medianY / vecs.length); |
|
||||
} |
|
||||
|
|
||||
public static angleBetween(lhs: Vec2, rhs: Vec2): number { |
|
||||
const y = (lhs.x * rhs.y) - (rhs.x * lhs.y); |
|
||||
const x = (lhs.x * rhs.x) + (lhs.y * rhs.y); |
|
||||
|
|
||||
return MathHelper.toPositiveDegree(MathHelper.toDegree(Math.atan2(y, x))); |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue