- {transition(({ opacity }, finished) =>
- finished ? (
+ {transition(({ opacity }, isGameFinished) =>
+ isGameFinished ? (
) : (
@@ -129,15 +140,18 @@ export const GameSizing = ({
style={{ opacity }}
>
)
@@ -148,7 +162,7 @@ export const GameSizing = ({
ReactDOM.createPortal(
,
@@ -159,9 +173,9 @@ export const GameSizing = ({
ReactDOM.createPortal(
,
actionsPanelRef.current
)}
diff --git a/web/src/hooks/spring-hooks/use-element-move.ts b/web/src/hooks/spring-hooks/use-element-move.ts
new file mode 100644
index 00000000..a30918cd
--- /dev/null
+++ b/web/src/hooks/spring-hooks/use-element-move.ts
@@ -0,0 +1,73 @@
+import { CSSProperties } from "react";
+import { useSpring } from "react-spring";
+import { useDrag } from "@use-gesture/react";
+
+import { Position, Size } from "models";
+
+type UseElementMoveProps = {
+ containerSize: Size;
+ dragMovingObjectSize: Size;
+ initialPosition?: Position;
+ additionalCallback?: (left: number, top: number) => void;
+};
+
+/**
+ * This hook enables an element (e.g. image) to be moved within a defined container boundary.
+ * Moving object should be positioned 'absolute', while its container should be positioned 'relative'.
+ * NOTE: Add `draggable={false}` html property to the element being moved
+ *
+ * @param containerSize size of the container which acts as a boundary when moving our element
+ * @param dragMovingObjectSize size of the object which is being moved
+ * @param initialPosition position of moved object inside container boundary, defaults [0, 0]
+ * @param additionalCallback function which is called when new left and top position is being assigned
+ */
+export const useElementMove = ({
+ containerSize,
+ dragMovingObjectSize,
+ initialPosition,
+ additionalCallback,
+}: UseElementMoveProps) => {
+ // Initial position of image being dragged
+ // It will be also reset back to [0, 0] whenever the container size changes (because of the deps array)
+ const [moveSpring, moveSpringApi] = useSpring(
+ () => ({
+ left: initialPosition?.left ?? 0,
+ top: initialPosition?.top ?? 0,
+ }),
+ [containerSize.width, containerSize.height]
+ );
+
+ const bindMoveDrag = useDrag(
+ ({ down, offset: [x, y] }) => {
+ if (!down) {
+ return;
+ }
+
+ moveSpringApi.start({ left: x, top: y, immediate: true });
+ additionalCallback?.(x, y);
+ },
+ {
+ from: () => [moveSpring.left.get(), moveSpring.top.get()],
+ bounds: {
+ left: 0,
+ top: 0,
+ right: containerSize.width - dragMovingObjectSize.width,
+ bottom: containerSize.height - dragMovingObjectSize.height,
+ },
+ }
+ );
+
+ return { moveSpring, moveSpringApi, bindMoveDrag };
+};
+
+export const moveContainerStyle: CSSProperties = {
+ position: "relative",
+};
+
+// NOTE: also add 'hover:cursor-move'
+export const dragMovingObjectStyle: CSSProperties = {
+ position: "absolute",
+ touchAction: "none",
+ WebkitUserSelect: "none",
+ WebkitTouchCallout: "none",
+};
diff --git a/web/src/hooks/spring-hooks/use-element-resize.ts b/web/src/hooks/spring-hooks/use-element-resize.ts
new file mode 100644
index 00000000..73440146
--- /dev/null
+++ b/web/src/hooks/spring-hooks/use-element-resize.ts
@@ -0,0 +1,108 @@
+import { useMemo, CSSProperties } from "react";
+import { useSpring } from "react-spring";
+import { useDrag } from "@use-gesture/react";
+
+import { ImageOrigData, Size } from "models";
+
+type UseElementResizeProps = {
+ containerSize: Size;
+ dragResizingImgOrigData: ImageOrigData;
+ initialSize?: Size;
+ additionalCallback?: (width: number, height: number) => void;
+};
+
+/**
+ * This hook enables an element (e.g. image) to be resized within a defined container boundary.
+ * Object being resized should be positioned 'absolute', while its container should be positioned 'relative'.
+ * NOTE: Add `draggable={false}` html property to the element being resized
+ *
+ * @param containerSize size of the container which acts as a boundary when resizing our element
+ * @param dragResizingImgOrigData needed for aspect ratio of the element which is being resized
+ * @param initialSize first size of the object which is being resized inside container boundary
+ * @param additionalCallback function which is called when new width and height size is being assigned
+ */
+export const useElementResize = ({
+ containerSize,
+ dragResizingImgOrigData,
+ initialSize,
+ additionalCallback,
+}: UseElementResizeProps) => {
+ const { width: origImgWidth, height: origImgHeight } =
+ dragResizingImgOrigData;
+
+ const origImgRatio = useMemo(
+ () => origImgWidth / origImgHeight,
+ [origImgHeight, origImgWidth]
+ );
+
+ const [resizeSpring, resizeSpringApi] = useSpring(() => ({
+ width: initialSize?.width ?? origImgWidth,
+ height: initialSize?.height ?? origImgHeight,
+ }));
+
+ const bindResizeDrag = useDrag(
+ (state) => {
+ const { down, offset, lastOffset } = state;
+ const [x, y] = offset;
+ const [xp, yp] = lastOffset;
+
+ if (!down) {
+ return;
+ }
+
+ // we double the increments since the container is centered (grows on both sides)
+ const width = 2 * x - xp;
+ const height = 2 * y - yp;
+
+ const widthBased = width > height * origImgRatio;
+
+ const finalWidth = widthBased ? width : height * origImgRatio;
+ const finalHeight = widthBased ? width / origImgRatio : height;
+
+ resizeSpringApi.start({
+ width: finalWidth,
+ height: finalHeight,
+ immediate: true,
+ });
+
+ additionalCallback?.(finalWidth, finalHeight);
+ },
+ {
+ from: () => [resizeSpring.width.get(), resizeSpring.height.get()],
+ bounds: (state) => {
+ const [xp = 0, yp = 0] = state?.lastOffset ?? [];
+
+ const maxWidth = (containerSize.width - 100 + xp) / 2;
+ const maxHeight = (containerSize.height - 100 + yp) / 2;
+
+ const widthBased =
+ containerSize.width < containerSize.height * origImgRatio;
+
+ return {
+ left: (50 + xp) / 2,
+ top: (50 + yp) / 2,
+ right: widthBased ? maxWidth : maxHeight * origImgRatio,
+ bottom: widthBased ? maxWidth / origImgRatio : maxHeight,
+ };
+ },
+ }
+ );
+
+ return {
+ resizeSpring,
+ resizeSpringApi,
+ bindResizeDrag,
+ };
+};
+
+export const resizeContainerStyle: CSSProperties = {
+ position: "relative",
+};
+
+// NOTE: also add 'hover:cursor-se-resize'
+export const dragResizingObjectStyle: CSSProperties = {
+ position: "absolute",
+ touchAction: "none",
+ // WebkitUserSelect: "none",
+ // WebkitTouchCallout: "none",
+};
diff --git a/web/src/models/screen.ts b/web/src/models/screen.ts
index 1a1d345b..b533a50e 100644
--- a/web/src/models/screen.ts
+++ b/web/src/models/screen.ts
@@ -8,6 +8,7 @@ import { Document } from "./document";
import { Infopoint } from "./infopoint";
import { ScreenPreloadedFiles } from "context/file-preloader/file-preloader-provider";
import { ActiveExpo } from "./exposition";
+import { Position, Size } from "models";
// NEW
import {
@@ -468,6 +469,14 @@ export type GameMoveScreen = {
image1OrigData?: ImageOrigData;
image2OrigData?: ImageOrigData;
objectOrigData?: ImageOrigData;
+ objectPositionProps?: {
+ containerPosition: Position;
+ containedImgPosition: Position;
+ };
+ objectSizeProps?: {
+ inContainerSize: Size;
+ inContainedImgFractionSize: Size;
+ };
aloneScreen: boolean;
music?: string;
muteChapterMusic: boolean;