diff options
Diffstat (limited to 'FrontEnd/src/utilities')
-rw-r--r-- | FrontEnd/src/utilities/array.ts | 41 | ||||
-rw-r--r-- | FrontEnd/src/utilities/base64.ts | 21 | ||||
-rw-r--r-- | FrontEnd/src/utilities/geometry.ts | 292 | ||||
-rw-r--r-- | FrontEnd/src/utilities/hooks.ts | 5 | ||||
-rw-r--r-- | FrontEnd/src/utilities/hooks/mediaQuery.ts | 5 | ||||
-rw-r--r-- | FrontEnd/src/utilities/hooks/useClickOutside.ts | 38 | ||||
-rw-r--r-- | FrontEnd/src/utilities/hooks/useScrollToBottom.ts | 45 | ||||
-rw-r--r-- | FrontEnd/src/utilities/index.ts | 27 |
8 files changed, 376 insertions, 98 deletions
diff --git a/FrontEnd/src/utilities/array.ts b/FrontEnd/src/utilities/array.ts new file mode 100644 index 00000000..838e8744 --- /dev/null +++ b/FrontEnd/src/utilities/array.ts @@ -0,0 +1,41 @@ +export function copy_move<T>( + array: T[], + oldIndex: number, + newIndex: number, +): T[] { + if (oldIndex < 0 || oldIndex >= array.length) { + throw new Error("Old index out of range."); + } + + if (newIndex < 0) { + newIndex = 0; + } + + if (newIndex >= array.length) { + newIndex = array.length - 1; + } + + const result = array.slice(); + const [element] = result.splice(oldIndex, 1); + result.splice(newIndex, 0, element); + + return result; +} + +export function copy_insert<T>(array: T[], index: number, element: T): T[] { + const result = array.slice(); + result.splice(index, 0, element); + return result; +} + +export function copy_push<T>(array: T[], element: T): T[] { + const result = array.slice(); + result.push(element); + return result; +} + +export function copy_delete<T>(array: T[], index: number): T[] { + const result = array.slice(); + result.splice(index, 1); + return array; +} diff --git a/FrontEnd/src/utilities/base64.ts b/FrontEnd/src/utilities/base64.ts index 59de7512..6eece979 100644 --- a/FrontEnd/src/utilities/base64.ts +++ b/FrontEnd/src/utilities/base64.ts @@ -1,8 +1,19 @@ -import { Base64 } from "js-base64"; +function bytesToBase64(bytes: Uint8Array): string { + const binString = Array.from(bytes, (x) => String.fromCodePoint(x)).join(""); + return btoa(binString); +} + +export default function base64( + data: Blob | Uint8Array | string, +): Promise<string> { + if (typeof data === "string") { + // From https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem + const binString = new TextEncoder().encode(data); + return Promise.resolve(bytesToBase64(binString)); + } -export default function base64(blob: Blob | string): Promise<string> { - if (typeof blob === "string") { - return Promise.resolve(Base64.encode(blob)); + if (data instanceof Uint8Array) { + return Promise.resolve(bytesToBase64(data)); } return new Promise<string>((resolve) => { @@ -10,6 +21,6 @@ export default function base64(blob: Blob | string): Promise<string> { reader.onload = function () { resolve((reader.result as string).replace(/^data:.*;base64,/, "")); }; - reader.readAsDataURL(blob); + reader.readAsDataURL(data); }); } diff --git a/FrontEnd/src/utilities/geometry.ts b/FrontEnd/src/utilities/geometry.ts new file mode 100644 index 00000000..60a8d3d4 --- /dev/null +++ b/FrontEnd/src/utilities/geometry.ts @@ -0,0 +1,292 @@ +function clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max); +} + +export interface Point { + x: number; + y: number; +} + +export type Movement = Point; + +export interface Size { + width: number; + height: number; +} + +export class Rect { + static empty = new Rect(0, 0, 0, 0); + static max = new Rect( + Number.MIN_VALUE, + Number.MIN_VALUE, + Number.MAX_VALUE, + Number.MAX_VALUE, + ); + + static from({ + left, + top, + width, + height, + }: { + left: number; + top: number; + width: number; + height: number; + }): Rect { + return new Rect(left, top, width, height); + } + + constructor( + public left: number, + public top: number, + public width: number, + public height: number, + ) {} + + get right(): number { + return this.left + this.width; + } + + set right(value: number) { + this.width = value - this.left; + } + + get bottom(): number { + return this.top + this.height; + } + + set bottom(value: number) { + this.height = value - this.top; + } + + get ratio(): number { + return this.height / this.width; + } + + get position(): Point { + return { + x: this.left, + y: this.top, + }; + } + + set position(value: Point) { + this.left = value.x; + this.top = value.y; + } + + get size(): Size { + return { + width: this.width, + height: this.height, + }; + } + + set size(value: Size) { + this.width = value.width; + this.height = value.height; + } + + get normalizedLeft(): number { + return this.width >= 0 ? this.left : this.right; + } + + get normalizedTop(): number { + return this.height >= 0 ? this.top : this.bottom; + } + + get normalizedRight(): number { + return this.width >= 0 ? this.right : this.left; + } + + get normalizedBottom(): number { + return this.height >= 0 ? this.bottom : this.top; + } + + get normalizedWidth(): number { + return Math.abs(this.width); + } + + get normalizedHeight(): number { + return Math.abs(this.height); + } + + get normalizedSize(): Size { + return { + width: this.normalizedWidth, + height: this.normalizedHeight, + }; + } + + get normalizedRatio(): number { + return Math.abs(this.ratio); + } + + normalize(): Rect { + if (this.width < 0) { + this.width = -this.width; + this.left -= this.width; + } + if (this.height < 0) { + this.height = -this.height; + this.top -= this.height; + } + return this; + } + + move(movement: Movement): Rect { + this.left += movement.x; + this.top += movement.y; + return this; + } + + expand(size: Size | Point): Rect { + if ("x" in size) { + this.width += size.x; + this.height += size.y; + } else { + this.width += size.width; + this.height += size.height; + } + return this; + } + + copy(): Rect { + return new Rect(this.left, this.top, this.width, this.height); + } +} + +export function adjustRectToContainer( + rect: Rect, + container: Rect, + mode: "move" | "resize" | "both", + options?: { + targetRatio?: number; + resizeNoFlip?: boolean; + ratioCorrectBasedOn?: "bigger" | "smaller" | "width" | "height"; + }, +): Rect { + rect = rect.copy(); + container = container.copy().normalize(); + + if (process.env.NODE_ENV === "development") { + if (mode === "move") { + if (rect.normalizedWidth > container.width) { + console.warn( + "adjust rect (move): rect.normalizedWidth > container.normalizedWidth", + ); + } + if (rect.normalizedHeight > container.height) { + console.warn( + "adjust rect (move): rect.normalizedHeight > container.normalizedHeight", + ); + } + } + if (mode === "resize") { + if (rect.left < container.left) { + console.warn( + "adjust rect (resize): rect.left < container.normalizedLeft", + ); + } + if (rect.left > container.right) { + console.warn( + "adjust rect (resize): rect.left > container.normalizedRight", + ); + } + if (rect.top < container.top) { + console.warn( + "adjust rect (resize): rect.top < container.normalizedTop", + ); + } + if (rect.top > container.bottom) { + console.warn( + "adjust rect (resize): rect.top > container.normalizedBottom", + ); + } + } + } + + if (mode === "move") { + rect.left = + rect.width >= 0 + ? clamp(rect.left, container.left, container.right - rect.width) + : clamp(rect.left, container.left - rect.width, container.right); + rect.top = + rect.height >= 0 + ? clamp(rect.top, container.top, container.bottom - rect.height) + : clamp(rect.top, container.top - rect.height, container.bottom); + } else if (mode === "resize") { + const noFlip = options?.resizeNoFlip; + const newRight = clamp( + rect.right, + rect.width > 0 && noFlip ? rect.left : container.left, + rect.width < 0 && noFlip ? rect.left : container.right, + ); + rect.right = newRight; + rect.bottom = clamp( + rect.bottom, + rect.height > 0 && noFlip ? rect.top : container.top, + rect.height < 0 && noFlip ? rect.top : container.bottom, + ); + } else { + rect.left = clamp(rect.left, container.left, container.right); + rect.top = clamp(rect.top, container.top, container.bottom); + rect.right = clamp(rect.right, container.left, container.right); + rect.bottom = clamp(rect.bottom, container.top, container.bottom); + } + + // Now correct ratio + const currentRatio = rect.normalizedRatio; + let targetRatio = options?.targetRatio; + if (targetRatio != null) targetRatio = Math.abs(targetRatio); + if (targetRatio != null && currentRatio !== targetRatio) { + const { ratioCorrectBasedOn } = options ?? {}; + + const newWidth = + (Math.sign(rect.width) * rect.normalizedHeight) / targetRatio; + const newHeight = + Math.sign(rect.height) * rect.normalizedWidth * targetRatio; + + const newBottom = rect.top + newHeight; + const newRight = rect.left + newWidth; + + if (ratioCorrectBasedOn === "width") { + if (newBottom >= container.top && newBottom <= container.bottom) { + rect.height = newHeight; + } else { + rect.bottom = clamp(newBottom, container.top, container.bottom); + rect.width = + (Math.sign(rect.width) * rect.normalizedHeight) / targetRatio; + } + } else if (ratioCorrectBasedOn === "height") { + if (newRight >= container.left && newRight <= container.right) { + rect.width = newWidth; + } else { + rect.right = clamp(newRight, container.left, container.right); + rect.height = + Math.sign(rect.height) * rect.normalizedWidth * targetRatio; + } + } else if (ratioCorrectBasedOn === "smaller") { + if (currentRatio > targetRatio) { + // too tall + rect.width = + (Math.sign(rect.width) * rect.normalizedHeight) / targetRatio; + } else { + rect.height = + Math.sign(rect.height) * rect.normalizedWidth * targetRatio; + } + } else { + if (currentRatio < targetRatio) { + // too wide + rect.width = + (Math.sign(rect.width) * rect.normalizedHeight) / targetRatio; + } else { + rect.height = + Math.sign(rect.height) * rect.normalizedWidth * targetRatio; + } + } + } + + return rect; +} diff --git a/FrontEnd/src/utilities/hooks.ts b/FrontEnd/src/utilities/hooks.ts deleted file mode 100644 index a59f7167..00000000 --- a/FrontEnd/src/utilities/hooks.ts +++ /dev/null @@ -1,5 +0,0 @@ -import useClickOutside from "./hooks/useClickOutside"; -import useScrollToBottom from "./hooks/useScrollToBottom"; -import { useIsSmallScreen } from "./hooks/mediaQuery"; - -export { useClickOutside, useScrollToBottom, useIsSmallScreen }; diff --git a/FrontEnd/src/utilities/hooks/mediaQuery.ts b/FrontEnd/src/utilities/hooks/mediaQuery.ts deleted file mode 100644 index ad55c3c0..00000000 --- a/FrontEnd/src/utilities/hooks/mediaQuery.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { useMediaQuery } from "react-responsive"; - -export function useIsSmallScreen(): boolean { - return useMediaQuery({ maxWidth: 576 }); -} diff --git a/FrontEnd/src/utilities/hooks/useClickOutside.ts b/FrontEnd/src/utilities/hooks/useClickOutside.ts deleted file mode 100644 index 6dcbf7b3..00000000 --- a/FrontEnd/src/utilities/hooks/useClickOutside.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useRef, useEffect } from "react"; - -export default function useClickOutside( - element: HTMLElement | null | undefined, - onClickOutside: () => void, - nextTick?: boolean -): void { - const onClickOutsideRef = useRef<() => void>(onClickOutside); - - useEffect(() => { - onClickOutsideRef.current = onClickOutside; - }, [onClickOutside]); - - useEffect(() => { - if (element != null) { - const handler = (event: MouseEvent): void => { - let e: HTMLElement | null = event.target as HTMLElement; - while (e) { - if (e == element) { - return; - } - e = e.parentElement; - } - onClickOutsideRef.current(); - }; - if (nextTick) { - setTimeout(() => { - document.addEventListener("click", handler); - }); - } else { - document.addEventListener("click", handler); - } - return () => { - document.removeEventListener("click", handler); - }; - } - }, [element, nextTick]); -} diff --git a/FrontEnd/src/utilities/hooks/useScrollToBottom.ts b/FrontEnd/src/utilities/hooks/useScrollToBottom.ts deleted file mode 100644 index 216746f4..00000000 --- a/FrontEnd/src/utilities/hooks/useScrollToBottom.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { useRef, useEffect } from "react"; -import { fromEvent } from "rxjs"; -import { filter, throttleTime } from "rxjs/operators"; - -function useScrollToBottom( - handler: () => void, - enable = true, - option = { - maxOffset: 5, - throttle: 1000, - } -): void { - const handlerRef = useRef<(() => void) | null>(null); - - useEffect(() => { - handlerRef.current = handler; - - return () => { - handlerRef.current = null; - }; - }, [handler]); - - useEffect(() => { - const subscription = fromEvent(window, "scroll") - .pipe( - filter( - () => - window.scrollY >= - document.body.scrollHeight - window.innerHeight - option.maxOffset - ), - throttleTime(option.throttle) - ) - .subscribe(() => { - if (enable) { - handlerRef.current?.(); - } - }); - - return () => { - subscription.unsubscribe(); - }; - }, [enable, option.maxOffset, option.throttle]); -} - -export default useScrollToBottom; diff --git a/FrontEnd/src/utilities/index.ts b/FrontEnd/src/utilities/index.ts new file mode 100644 index 00000000..7659a8aa --- /dev/null +++ b/FrontEnd/src/utilities/index.ts @@ -0,0 +1,27 @@ +export { default as base64 } from "./base64"; +export { withQuery } from "./url"; + +export function delay(milliseconds: number): Promise<void> { + return new Promise<void>((resolve) => { + setTimeout(() => { + resolve(); + }, milliseconds); + }); +} + +export function range(stop: number): number[]; +export function range(start: number, stop: number, step?: number): number[]; +export function range(start: number, stop?: number, step?: number): number[] { + if (stop == undefined) { + stop = start; + start = 0; + } + if (step == undefined) { + step = 1; + } + const result: number[] = []; + for (let i = start; i < stop; i += step) { + result.push(i); + } + return result; +} |