diff --git a/web/public/locales/cs/expo-editor.json b/web/public/locales/cs/expo-editor.json index a4db1017..0e2b0398 100644 --- a/web/public/locales/cs/expo-editor.json +++ b/web/public/locales/cs/expo-editor.json @@ -278,7 +278,8 @@ "imageResultTooltip": "Z knihovny dokumentů vyberte výsledný kompletní obrázek.", "object": "Objekt", "objectSelectLabel": "Vybrat", - "objectTooltip": "Z knihovny dokumentů vyberte obrázek s částmi, ze kterých budou návštěvníci vybírat." + "objectTooltip": "Z knihovny dokumentů vyberte obrázek s částmi, ze kterých budou návštěvníci vybírat.", + "screenPreviewText": "Náhled obrazovky" }, "gameQuizScreen": { "nameLabel": "Název", diff --git a/web/public/locales/en/expo-editor.json b/web/public/locales/en/expo-editor.json index e5aa08bb..b5e0bd08 100644 --- a/web/public/locales/en/expo-editor.json +++ b/web/public/locales/en/expo-editor.json @@ -278,7 +278,8 @@ "imageResultTooltip": "Select the complete image from the document library.", "object": "Object", "objectSelectLabel": "Select", - "objectTooltip": "From the document library, select an image with parts for the user to select from." + "objectTooltip": "From the document library, select an image with parts for the user to select from.", + "screenPreviewText": "Screen preview" }, "gameQuizScreen": { "nameLabel": "Title", diff --git a/web/public/locales/sk/expo-editor.json b/web/public/locales/sk/expo-editor.json index 5d3f99af..258076e8 100644 --- a/web/public/locales/sk/expo-editor.json +++ b/web/public/locales/sk/expo-editor.json @@ -278,7 +278,8 @@ "imageResultTooltip": "Z knižnice dokumentov vyberte výsledný kompletný obrázok.", "object": "Objekt", "objectSelectLabel": "Vybrať", - "objectTooltip": "Z knižnice dokumentov vyberte obrázok s časťami, z ktorých budú návštevníci vyberať." + "objectTooltip": "Z knižnice dokumentov vyberte obrázok s časťami, z ktorých budú návštevníci vyberať.", + "screenPreviewText": "Náhľad obrazovky" }, "gameQuizScreen": { "nameLabel": "Názov", diff --git a/web/src/containers/expo-administration/expo-editor/screen-game-move/Images.tsx b/web/src/containers/expo-administration/expo-editor/screen-game-move/Images.tsx index 61fa8bca..c8c67207 100644 --- a/web/src/containers/expo-administration/expo-editor/screen-game-move/Images.tsx +++ b/web/src/containers/expo-administration/expo-editor/screen-game-move/Images.tsx @@ -8,6 +8,8 @@ import TextField from "react-md/lib/TextFields"; import ImageBox from "components/editors/ImageBox"; import HelpIcon from "components/help-icon"; +import ObjectImagePreview from "./ObjectImagePreview"; + // Models import { GameMoveScreen, File as IndihuFile } from "models"; import { AppDispatch } from "store/store"; @@ -89,6 +91,7 @@ const Images = ({ activeScreen }: ImagesProps) => { /> +
{t("object")} image @@ -139,6 +142,7 @@ const Images = ({ activeScreen }: ImagesProps) => {
+ {object && ( { alt="" /> )} + + {image1 && object && ( + + )} ); diff --git a/web/src/containers/expo-administration/expo-editor/screen-game-move/ObjectImagePreview.tsx b/web/src/containers/expo-administration/expo-editor/screen-game-move/ObjectImagePreview.tsx new file mode 100644 index 00000000..47dce988 --- /dev/null +++ b/web/src/containers/expo-administration/expo-editor/screen-game-move/ObjectImagePreview.tsx @@ -0,0 +1,142 @@ +import { useDispatch } from "react-redux"; +import { useTranslation } from "react-i18next"; +import { animated } from "react-spring"; + +import useResizeObserver from "hooks/use-resize-observer"; +import { useElementMove } from "hooks/spring-hooks/use-element-move"; +import { useElementResize } from "hooks/spring-hooks/use-element-resize"; + +// Models +import { AppDispatch } from "store/store"; +import { GameMoveScreen } from "models"; + +// Actions and utils +import { updateScreenData } from "actions/expoActions"; +import { calculateObjectFit } from "utils/object-fit"; + +// Assets +import expandImg from "../../../../assets/img/expand.png"; + +// - - - - + +type ObjectImagePreviewProps = { + activeScreen: GameMoveScreen; + image1Src: string; + objectImgSrc: string; +}; + +const ObjectImagePreview = ({ + activeScreen, + image1Src, + objectImgSrc, +}: ObjectImagePreviewProps) => { + const dispatch = useDispatch(); + const { t } = useTranslation("expo-editor", { + keyPrefix: "descFields.gameMoveScreen", + }); + + const image1OrigData = activeScreen.image1OrigData ?? { width: 0, height: 0 }; + const objectOrigData = activeScreen.objectOrigData ?? { width: 0, height: 0 }; + + const [containerRef, containerSize] = useResizeObserver(); + const [objectRef, objectSize] = useResizeObserver(); + + const { + width: containedImg1Width, + height: containedImg1Height, + left: fromLeft, + top: fromTop, + } = calculateObjectFit({ + type: "contain", + parent: containerSize, + child: image1OrigData, + }); + + const { moveSpring, bindMoveDrag } = useElementMove({ + containerSize: containerSize, + dragMovingObjectSize: objectSize, + initialPosition: activeScreen.objectPositionProps?.containerPosition, + additionalCallback: (left, top) => { + dispatch( + updateScreenData({ + objectPositionProps: { + containerPosition: { left: left, top: top }, + containedImgPosition: { + left: left - fromLeft, + top: top - fromTop, + }, + }, + }) + ); + }, + }); + + const { resizeSpring, bindResizeDrag } = useElementResize({ + containerSize: containerSize, + dragResizingImgOrigData: objectOrigData, + initialSize: activeScreen.objectSizeProps?.inContainerSize, + additionalCallback: (width, height) => { + dispatch( + updateScreenData({ + objectSizeProps: { + inContainerSize: { width: width, height: height }, + inContainedImgFractionSize: { + width: width / containedImg1Width, + height: height / containedImg1Height, + }, + }, + }) + ); + }, + }); + + return ( +
+
+ {t("screenPreviewText")} +
+ +
+ first img + + + object drag content + + expand image icon + +
+
+ ); +}; + +export default ObjectImagePreview; diff --git a/web/src/containers/views/games/game-move/game-move.tsx b/web/src/containers/views/games/game-move/game-move.tsx index 7f60707d..af2df325 100644 --- a/web/src/containers/views/games/game-move/game-move.tsx +++ b/web/src/containers/views/games/game-move/game-move.tsx @@ -1,13 +1,13 @@ import ReactDOM from "react-dom"; -import { useCallback, useState } from "react"; - +import { useState, useMemo, useCallback } from "react"; +import { animated, useTransition } from "react-spring"; import { useSelector } from "react-redux"; import { createSelector } from "reselect"; + +import { useTranslation } from "react-i18next"; import { useTutorial } from "context/tutorial-provider/use-tutorial"; -import { animated, useSpring, useTransition } from "react-spring"; -import { useDrag } from "@use-gesture/react"; import useResizeObserver from "hooks/use-resize-observer"; -import { useTranslation } from "react-i18next"; +import { useElementMove } from "../../../../hooks/spring-hooks/use-element-move"; // Components import { GameInfoPanel } from "../GameInfoPanel"; @@ -17,14 +17,17 @@ import { GameActionsPanel } from "../GameActionsPanel"; import { AppState } from "store/store"; import { ScreenProps, GameMoveScreen } from "models"; -// - - - +// Utils +import { calculateObjectInitialPosition, calculateObjectSize } from "./utils"; + +// - - - - - - const stateSelector = createSelector( ({ expo }: AppState) => expo.viewScreen as GameMoveScreen, (viewScreen) => ({ viewScreen }) ); -// - - - +// - - - - - - export const GameMove = ({ screenPreloadedFiles, @@ -32,56 +35,59 @@ export const GameMove = ({ actionsPanelRef, isMobileOverlay, }: ScreenProps) => { - const { viewScreen } = useSelector(stateSelector); const { t } = useTranslation("view-screen"); + const { viewScreen } = useSelector(stateSelector); - const [containerRef, { width: containerWidth, height: containerHeight }] = - useResizeObserver(); - const [dragRef, { width: dragWidth, height: dragHeight }] = - useResizeObserver(); // dragTarget as a container with the object img + const { + image1: assignmentImgSrc, + image2: resultingImgSrc, + object: objectImgSrc, + } = screenPreloadedFiles; - // - - + // - - Move functionality - - - const [isGameFinished, setIsGameFinished] = useState(false); + const [containerRef, containerSize] = useResizeObserver(); - // Initialize the position for drag object image, it will be reset back to [0, 0] whenever the width or height of the container changes - const [{ dragLeft, dragTop }, dragApi] = useSpring( - () => ({ - dragLeft: 0, - dragTop: 0, - }), - [containerWidth, containerHeight] + const [objectDragRef, objectDragSize] = useResizeObserver(); + + const { objInitialLeft, objInitialTop } = useMemo( + () => calculateObjectInitialPosition(viewScreen, containerSize), + [containerSize, viewScreen] ); + const { moveSpring, moveSpringApi, bindMoveDrag } = useElementMove({ + containerSize: containerSize, + dragMovingObjectSize: objectDragSize, + initialPosition: { left: objInitialLeft, top: objInitialTop }, + }); + + // - - Size calculation of object, based on administration settings - - + // once at mount assigned through CSS and then used by `objectDragSize` + + const { objectWidth, objectHeight } = useMemo( + () => calculateObjectSize(viewScreen, containerSize), + [containerSize, viewScreen] + ); + + // - - Tutorial - - + + const { bind: bindTutorial, TutorialTooltip } = useTutorial("gameMove", { + shouldOpen: !isMobileOverlay, + closeOnEsc: true, + }); + + // - - - - + + const [isGameFinished, setIsGameFinished] = useState(false); + const onGameFinish = useCallback(() => { setIsGameFinished(true); }, []); const onGameReset = useCallback(() => { setIsGameFinished(false); - dragApi.start({ dragLeft: 0, dragTop: 0 }); - }, [dragApi]); - - // - - - - const bind = useDrag( - ({ down, offset: [x, y] }) => { - if (!down) { - return; - } - - dragApi.start({ dragLeft: x, dragTop: y, immediate: true }); - }, - { - from: () => [dragLeft.get(), dragTop.get()], - bounds: { - left: 0, - top: 0, - right: containerWidth - dragWidth, - bottom: containerHeight - dragHeight, - }, - } - ); + moveSpringApi.start({ left: objInitialLeft, top: objInitialTop }); + }, [moveSpringApi, objInitialLeft, objInitialTop]); const transition = useTransition(isGameFinished, { initial: { opacity: 1 }, @@ -90,49 +96,42 @@ export const GameMove = ({ leave: { opacity: 0 }, }); - // - - - - const { bind: bindTutorial, TutorialTooltip } = useTutorial("gameMove", { - shouldOpen: !isMobileOverlay, - closeOnEsc: true, - }); - return (
{transition(({ opacity }, isGameFinished) => isGameFinished ? ( - // Image2 is result image ) : ( <> - {/* Image1 is background image (zadanie) */} - {/* Object */} drag content diff --git a/web/src/containers/views/games/game-move/utils.ts b/web/src/containers/views/games/game-move/utils.ts new file mode 100644 index 00000000..52cca2ae --- /dev/null +++ b/web/src/containers/views/games/game-move/utils.ts @@ -0,0 +1,76 @@ +import { GameMoveScreen, Size } from "models"; +import { calculateObjectFit } from "utils/object-fit"; + +export const calculateObjectInitialPosition = ( + viewScreen: GameMoveScreen, + containerSize: Size +) => { + const assignmentImgOrigData = viewScreen.image1OrigData ?? { + width: 0, + height: 0, + }; + + // Object position from administration against the contained image there + const objectPosition = viewScreen.objectPositionProps + ?.containedImgPosition ?? { + left: 0, + top: 0, + }; + + const { + width: assignmentImgWidth, + height: assignmentImgHeight, + left: assignmentImgLeftEdge, + top: assignmentImgTopEdge, + } = calculateObjectFit({ + type: "contain", + parent: containerSize, + child: assignmentImgOrigData, + }); + + // E.g. wFraction = 0.25 means that the object's left-top corner is located 25% left against contained img there + const wFraction = objectPosition.left / assignmentImgOrigData.width; + const hFraction = objectPosition.top / assignmentImgOrigData.height; + + const objInitialLeft = assignmentImgLeftEdge + wFraction * assignmentImgWidth; + const objInitialTop = assignmentImgTopEdge + hFraction * assignmentImgHeight; + + return { objInitialLeft, objInitialTop }; +}; + +export const calculateObjectSize = ( + viewScreen: GameMoveScreen, + containerSize: Size +) => { + const assignmentImgOrigData = viewScreen.image1OrigData ?? { + width: 0, + height: 0, + }; + + const { width: assignmentImgWidth, height: assignmentImgHeight } = + calculateObjectFit({ + type: "contain", + parent: containerSize, + child: assignmentImgOrigData, + }); + + const objectImgOrigData = viewScreen.objectOrigData ?? { + width: 0, + height: 0, + }; + + const inContainedImgFractionSize = + viewScreen.objectSizeProps?.inContainedImgFractionSize; + + if (inContainedImgFractionSize === undefined) { + return { + objectWidth: objectImgOrigData.width, + objectHeight: objectImgOrigData.height, + }; + } + + const objectWidth = inContainedImgFractionSize.width * assignmentImgWidth; + const objectHeight = inContainedImgFractionSize.height * assignmentImgHeight; + + return { objectWidth, objectHeight }; +}; diff --git a/web/src/containers/views/games/game-sizing/game-sizing.tsx b/web/src/containers/views/games/game-sizing/game-sizing.tsx index a1e6596c..87121125 100644 --- a/web/src/containers/views/games/game-sizing/game-sizing.tsx +++ b/web/src/containers/views/games/game-sizing/game-sizing.tsx @@ -1,126 +1,137 @@ import ReactDOM from "react-dom"; -import { useCallback, useMemo, useState } from "react"; -import { animated, useSpring, useTransition } from "react-spring"; -import { createSelector } from "reselect"; -import { useDrag } from "@use-gesture/react"; +import { useState, useCallback } from "react"; +import { animated, useTransition } from "react-spring"; import { useSelector } from "react-redux"; +import { createSelector } from "reselect"; + +import { useTranslation } from "react-i18next"; +import { useTutorial } from "context/tutorial-provider/use-tutorial"; +import useResizeObserver from "hooks/use-resize-observer"; +import { useElementResize } from "../../../../hooks/spring-hooks/use-element-resize"; +// Components +import { GameInfoPanel } from "../GameInfoPanel"; +import { GameActionsPanel } from "../GameActionsPanel"; + +// Models import { ScreenProps } from "models"; import { GameSizingScreen } from "models"; import { AppState } from "store/store"; -import useElementSize from "hooks/element-size-hook"; -import expand from "../../../../assets/img/expand.png"; -import { GameInfoPanel } from "../GameInfoPanel"; -import { GameActionsPanel } from "../GameActionsPanel"; -import { useTutorial } from "context/tutorial-provider/use-tutorial"; -import { useTranslation } from "react-i18next"; +// Assets +import expandImg from "../../../../assets/img/expand.png"; + +// - - - - - - const stateSelector = createSelector( ({ expo }: AppState) => expo.viewScreen as GameSizingScreen, (viewScreen) => ({ viewScreen }) ); +// - - - - - - + export const GameSizing = ({ screenPreloadedFiles, infoPanelRef, actionsPanelRef, isMobileOverlay, }: ScreenProps) => { - const { viewScreen } = useSelector(stateSelector); - const [finished, setFinished] = useState(false); - const [ref, containerSize] = useElementSize(); const { t } = useTranslation("view-screen"); + const { viewScreen } = useSelector(stateSelector); - const onFinish = useCallback(() => { - setFinished(true); - }, []); - - const onReset = useCallback(() => { - setFinished(false); - }, []); + const { + image1: referenceImgSrc, + image2: comparisonImgSrc, + image3: resultingImgSrc, + } = screenPreloadedFiles; - const { height: originalHeight = 0, width: originalWidth = 0 } = - viewScreen.image2OrigData ?? {}; + // - - Resizing functionality - - - const [{ width, height }, api] = useSpring(() => ({ - width: originalWidth, - height: originalHeight, - })); + const { + image1OrigData: referenceImgOrigData, + image2OrigData: comparisonImgOrigData, + } = viewScreen; - const ratio = useMemo( - () => originalWidth / originalHeight, - [originalHeight, originalWidth] - ); + const [rightContainerRef, rightContainerSize] = useResizeObserver(); + const [leftContainerRef, leftContainerSize] = useResizeObserver(); - const bind = useDrag( - ({ down, offset: [x, y], lastOffset: [xp, yp] }) => { - 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 * ratio; - - api.start({ - width: widthBased ? width : height * ratio, - height: widthBased ? width / ratio : height, - immediate: true, - }); - }, - { - from: () => [width.get(), 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 * ratio; - - return { - left: (50 + xp) / 2, - top: (50 + yp) / 2, - right: widthBased ? maxWidth : maxHeight * ratio, - bottom: widthBased ? maxWidth / ratio : maxHeight, - }; - }, - } - ); + const { + resizeSpring: comparisonImgResizeSpring, + bindResizeDrag: comparisongImgBindResizeDrag, + } = useElementResize({ + containerSize: rightContainerSize, + dragResizingImgOrigData: comparisonImgOrigData ?? { width: 0, height: 0 }, + }); - const transition = useTransition(finished, { - initial: { opacity: 1 }, - from: { opacity: 0 }, - enter: { opacity: 1 }, - leave: { opacity: 0 }, + const { + resizeSpring: referenceImgResizeSpring, + bindResizeDrag: referenceImgBindResizeDrag, + } = useElementResize({ + containerSize: leftContainerSize, + dragResizingImgOrigData: referenceImgOrigData ?? { width: 0, height: 0 }, }); - // - - + // - - Tutorial - - const { bind: bindTutorial, TutorialTooltip } = useTutorial("gameSizing", { shouldOpen: !isMobileOverlay, closeOnEsc: true, }); + // - - - - + + const [isGameFinished, setIsGameFinished] = useState(false); + + const onGameFinish = useCallback(() => { + setIsGameFinished(true); + }, []); + + const onGameReset = useCallback(() => { + setIsGameFinished(false); + }, []); + + const transition = useTransition(isGameFinished, { + initial: { opacity: 1 }, + from: { opacity: 0 }, + enter: { opacity: 1 }, + leave: { opacity: 0 }, + }); + return (
-
- + {/* Left container */} +
+
+ + expand image icon left +
+ + {/* Right container */}
- {transition(({ opacity }, finished) => - finished ? ( + {transition(({ opacity }, isGameFinished) => + isGameFinished ? ( ) : ( @@ -129,15 +140,18 @@ export const GameSizing = ({ style={{ opacity }} > expand icon ) @@ -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;