aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/utilities
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2023-09-20 20:26:42 +0800
committerGitHub <noreply@github.com>2023-09-20 20:26:42 +0800
commitf836d77e73f3ea0af45c5f71dae7268143d6d86f (patch)
tree573cfafd972106d69bef0d41ff5f270ec3c43ec2 /FrontEnd/src/utilities
parent4a069bf1268f393d5467166356f691eb89963152 (diff)
parent901fe3d7c032d284da5c9bce24c4aaee9054c7ac (diff)
downloadtimeline-f836d77e73f3ea0af45c5f71dae7268143d6d86f.tar.gz
timeline-f836d77e73f3ea0af45c5f71dae7268143d6d86f.tar.bz2
timeline-f836d77e73f3ea0af45c5f71dae7268143d6d86f.zip
Merge pull request #1395 from crupest/dev
Refector 2023 v0.1
Diffstat (limited to 'FrontEnd/src/utilities')
-rw-r--r--FrontEnd/src/utilities/array.ts41
-rw-r--r--FrontEnd/src/utilities/base64.ts21
-rw-r--r--FrontEnd/src/utilities/geometry.ts292
-rw-r--r--FrontEnd/src/utilities/hooks.ts5
-rw-r--r--FrontEnd/src/utilities/hooks/mediaQuery.ts5
-rw-r--r--FrontEnd/src/utilities/hooks/useClickOutside.ts38
-rw-r--r--FrontEnd/src/utilities/hooks/useScrollToBottom.ts45
-rw-r--r--FrontEnd/src/utilities/index.ts27
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;
+}