From 1e983d7b914dfcf2dcf3ef1a73b5a57928429dbb Mon Sep 17 00:00:00 2001 From: Victor Date: Tue, 23 Jul 2024 18:05:04 -0700 Subject: [PATCH] added cropping to EntityInput, and ZoomSlider --- package.json | 1 + .../src/api/workflows/EnqueueActingFace.ts | 24 --- .../src/api/workflows/EnqueueFaceMirror.ts | 33 ++++ .../common/ZoomSlider/ZoomSlider.tsx | 71 +++++++ .../components/common/ZoomSlider/index.tsx | 2 + .../src/components/common/index.tsx | 10 +- .../entities/EntityInput/EntityInput.scss | 185 ++++++++++++++++-- .../entities/EntityInput/EntityInput.tsx | 103 ++++++---- .../EntityInputImageVideoPreview.tsx | 101 ++++++++++ .../EntityInput/EntityInputSidePanel.tsx | 46 +++++ .../src/hooks/useVideo/useVideo.tsx | 23 ++- packages/fakeyou.com/src/scss/colours.scss | 3 + .../pages/ai_face_mirror/AIFaceMirror.tsx | 25 ++- yarn.lock | 28 +++ 14 files changed, 562 insertions(+), 93 deletions(-) delete mode 100644 packages/components/src/api/workflows/EnqueueActingFace.ts create mode 100644 packages/components/src/api/workflows/EnqueueFaceMirror.ts create mode 100644 packages/fakeyou.com/src/components/common/ZoomSlider/ZoomSlider.tsx create mode 100644 packages/fakeyou.com/src/components/common/ZoomSlider/index.tsx create mode 100644 packages/fakeyou.com/src/components/entities/EntityInput/EntityInputImageVideoPreview.tsx create mode 100644 packages/fakeyou.com/src/components/entities/EntityInput/EntityInputSidePanel.tsx diff --git a/package.json b/package.json index f86a190e30..beb3a80233 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "framer-motion": "6.5.1", "local-storage": "^2.0.0", "moment": "^2.29.4", + "react-easy-crop": "^5.0.7", "universal-cookie": "^6.1.1", "yarn": "^1.22.19" }, diff --git a/packages/components/src/api/workflows/EnqueueActingFace.ts b/packages/components/src/api/workflows/EnqueueActingFace.ts deleted file mode 100644 index 0c742c3d9e..0000000000 --- a/packages/components/src/api/workflows/EnqueueActingFace.ts +++ /dev/null @@ -1,24 +0,0 @@ -import MakeRequest from "../MakeRequest"; - -export interface EnqueueActingFaceRequest { - creator_set_visibility: "public" | "private"; - face_driver_media_file_token: string; - remove_watermark: boolean; - source_media_file_token: string; - uuid_idempotency_token: string; -} - -export interface EnqueueActingFaceResponse { - inference_job_token?: string; - success: boolean; -} - -export const EnqueueActingFace = MakeRequest< - string, - EnqueueActingFaceRequest, - EnqueueActingFaceResponse, - {} ->({ - method: "POST", - routingFunction: () => `/v1/workflows/enqueue_acting_face`, -}); diff --git a/packages/components/src/api/workflows/EnqueueFaceMirror.ts b/packages/components/src/api/workflows/EnqueueFaceMirror.ts new file mode 100644 index 0000000000..3d092d8952 --- /dev/null +++ b/packages/components/src/api/workflows/EnqueueFaceMirror.ts @@ -0,0 +1,33 @@ +import MakeRequest from "../MakeRequest"; + +// this type is eqivelent to react-easy-crops Area type + +export interface MediaFileCropArea { + height: number; + width: number; + x: number; + y: number; +} +export interface EnqueueFaceMirrorRequest { + creator_set_visibility: "public" | "private"; + face_driver_media_file_token: string; + maybe_crop: MediaFileCropArea; + remove_watermark: boolean; + source_media_file_token: string; + uuid_idempotency_token: string; +} + +export interface EnqueueFaceMirrorResponse { + inference_job_token?: string; + success: boolean; +} + +export const EnqueueFaceMirror = MakeRequest< + string, + EnqueueFaceMirrorRequest, + EnqueueFaceMirrorResponse, + {} +>({ + method: "POST", + routingFunction: () => `/v1/workflows/enqueue_face_mirror`, +}); diff --git a/packages/fakeyou.com/src/components/common/ZoomSlider/ZoomSlider.tsx b/packages/fakeyou.com/src/components/common/ZoomSlider/ZoomSlider.tsx new file mode 100644 index 0000000000..ed851aada8 --- /dev/null +++ b/packages/fakeyou.com/src/components/common/ZoomSlider/ZoomSlider.tsx @@ -0,0 +1,71 @@ +import React from "react"; +import { Direction, Range } from "react-range"; +import { FontAwesomeIcon as Icon } from "@fortawesome/react-fontawesome"; +import { + faMagnifyingGlassPlus, + faMagnifyingGlassMinus, +} from "@fortawesome/pro-solid-svg-icons"; + +export interface ZoomSliderOnChangeEvent { + target: { name?: string; type: "zoom"; value: number }; +} + +interface Props { + name?: string; + onChange: (event: ZoomSliderOnChangeEvent) => void; + value: number; +} + +const renderTrack = ({ props: { style, ...props }, children }: any) => ( +
+ {children} +
+
+); + +const thumb = + (thumbTip = "") => + ({ props: { style, ...props } }: any) => { + return ( +
+ {thumbTip} +
+ ); + }; + +export default function ZoomSlider({ + name = "", + onChange: inChange, + value, +}: Props) { + const onChange = (newVal: number[]) => + inChange({ + target: { ...(name && { name }), type: "zoom", value: newVal[0] }, + }); + + return ( +
+ +
+ +
+ +
+ ); +} diff --git a/packages/fakeyou.com/src/components/common/ZoomSlider/index.tsx b/packages/fakeyou.com/src/components/common/ZoomSlider/index.tsx new file mode 100644 index 0000000000..24f0b0c7a2 --- /dev/null +++ b/packages/fakeyou.com/src/components/common/ZoomSlider/index.tsx @@ -0,0 +1,2 @@ +export { default } from "./ZoomSlider"; +export type { ZoomSliderOnChangeEvent } from "./ZoomSlider"; diff --git a/packages/fakeyou.com/src/components/common/index.tsx b/packages/fakeyou.com/src/components/common/index.tsx index 9574e7f657..1a534830eb 100644 --- a/packages/fakeyou.com/src/components/common/index.tsx +++ b/packages/fakeyou.com/src/components/common/index.tsx @@ -59,6 +59,7 @@ import VideoBasic from "./VideoBasic"; import VideoFakeyou from "./VideoFakeyou"; import VideoInput from "./VideoInput"; import WeightCoverImage from "./WeightCoverImage"; +import ZoomSlider, { ZoomSliderOnChangeEvent } from "./ZoomSlider"; export { Accordion, @@ -127,6 +128,13 @@ export { VideoFakeyou, VideoInput, WeightCoverImage, + ZoomSlider, }; -export type { ActionButtonProps, ModalUtilities, SelectModalData, Socials }; +export type { + ActionButtonProps, + ModalUtilities, + SelectModalData, + Socials, + ZoomSliderOnChangeEvent, +}; diff --git a/packages/fakeyou.com/src/components/entities/EntityInput/EntityInput.scss b/packages/fakeyou.com/src/components/entities/EntityInput/EntityInput.scss index 01e5888209..a7f6486e27 100644 --- a/packages/fakeyou.com/src/components/entities/EntityInput/EntityInput.scss +++ b/packages/fakeyou.com/src/components/entities/EntityInput/EntityInput.scss @@ -13,6 +13,47 @@ overflow: hidden; position: relative; // background-color: $panel; + + .fy-entity-input-crop-container-hidden { + cursor: default; + background-color: rgba($secondary-very-dark, 0.75); + + .fy-entity-input-crop-area { + color: rgba(#f00, 0); + } + } + + .fy-entity-input-crop-area { + color: rgba($secondary-very-dark, 0.75); + } + + .fy-entity-input-crop-media { + box-shadow: + // 0 0 0 1px rgba(#000, 0.16), + 0 0 15px rgb(#000, 0.5); + } + + .fy-entity-input-play-button { + border: none; + background-color: rgba($secondary-very-dark, 0.5); + backdrop-filter: blur(5px); + left: 0; + right: 0; + bottom: 1rem; + margin: auto; + width: fit-content; + // background-color: red; + position: absolute; + } + + .fy-entity-input-crop-tip { + display: none; + position: absolute; + background-color: rgba($secondary-very-dark, 0.5); + padding: 0.5rem; + border-radius: 0.5rem; + color: rgba(#fff, 0.5); + } } .fy-entity-input-square { @@ -31,7 +72,8 @@ .fy-entity-input-empty, .fy-entity-input-full { - // position: relative; + // background-color: red; + position: relative; // overflow: hidden; justify-content: center; @@ -49,6 +91,9 @@ } .fy-entity-input-file-details { + padding: 0 0.5rem; + // background-color: red; + // display: inline-flex; div { font-size: 0.75rem; } @@ -72,33 +117,67 @@ } .fy-entity-input-full { - display: flex; + display: grid; + grid-template-columns: 1fr fit-content(50%); + // grid-template-rows: 1fr 1fr; + // display: flex; - video { - width: 100%; - } + // video { + // width: 100%; + // } } -.fy-entity-input-full-controls { - display: flex; - align-items: center; - justify-content: space-between; - position: absolute; +.fy-entity-input-media-preview { + // display: inline-block; + position: relative; + // width: 66%; + // height: 100%; + // width: 100%; + // aspect-ratio: 1; + padding: 0.5rem; +} + +.fy-entity-input-preview-controls { + display: grid; // background-color: red; - background-color: rgba(#000, 0.5); - background: linear-gradient( - 0deg, - rgba(0, 0, 0, 0) 0%, - rgba(0, 0, 0, 0.1) 6.25%, - rgba(0, 0, 0, 0.25) 12.5%, - rgba(0, 0, 0, 0.4) 18.75%, - rgba(0, 0, 0, 0.5) 25% - ); + grid-template-rows: 1fr auto; + gap: 0.5rem; padding: 1rem; +} + +.fy-entity-input-preview-tools { + display: grid; + grid-template-columns: auto 1fr; + // grid-template-rows: 1fr auto; + gap: 0.5rem; + // display: inline-block; + // width: 33%; + // height: 100%; + background-color: $secondary-dark; + + // display: flex; + // align-items: center; + // justify-content: space-between; + // position: absolute; + // background-color: red; + // background-color: rgba(#000, 0.5); + // background: linear-gradient( + // 0deg, + // rgba(0, 0, 0, 0) 0%, + // rgba(0, 0, 0, 0.1) 6.25%, + // rgba(0, 0, 0, 0.25) 12.5%, + // rgba(0, 0, 0, 0.4) 18.75%, + // rgba(0, 0, 0, 0.5) 25% + // ); + // padding: 1rem; // backdrop-filter: blur(2px); overflow: hidden; border-radius: 0.5rem 0.5rem 0 0; - width: 100%; + // width: 100%; + + .fy-zoom-slider { + padding: 0.5rem 0; + } } .fy-entity-input-empty-controls { @@ -146,3 +225,69 @@ } } } + +.fy-zoom-slider { + display: flex; + flex-direction: column; + height: 100%; + // padding: 1rem 0; + + svg { + color: rgba(#fff, 0.25); + } +} + +.fy-zoom-slider-container { + height: 100%; + // background-color: red; + margin: 1.5rem 0; + // width: 3rem; +} + +.fy-zoom-slider-track { + display: inline-flex; + justify-content: center; + height: 100%; + width: 2.25rem; + // background-color: green; + + .fy-zoom-slider-track-bar { + background-color: $secondary-darker; + border-radius: 0.25rem; + // margin: 1rem 0; + // flex-shrink: 1; + box-shadow: + // inset 0 0 1px 1px rgba(#000, 0.16), + inset 0 5px 10px rgb(#000, 0.25); + border-bottom: 1px solid rgba(#fff, 0.07); + // border-left: none; + // border-right: none; + width: 0.5rem; + height: 100%; + } +} + +.fy-zoom-slider-thumb { + color: rgba(#fff, 0.6); + border: 1px solid rgba(#fff, 0.04); + border-left: none; + border-right: none; + box-shadow: + // 0 0 0 1px rgba(#000, 0.16), + 0 5px 10px rgb(#000, 0.25); + // @include panel; + display: flex; + justify-content: center; + align-items: center; + font-size: 0.75rem; + background-color: $secondary; + border-radius: 0.75rem; + height: 2.25rem; + width: 2.25rem; + + &:focus-visible { + // color: $secondary; + background-color: $secondary-light; + outline: none; + } +} diff --git a/packages/fakeyou.com/src/components/entities/EntityInput/EntityInput.tsx b/packages/fakeyou.com/src/components/entities/EntityInput/EntityInput.tsx index 4acef55c1c..b95146ec6b 100644 --- a/packages/fakeyou.com/src/components/entities/EntityInput/EntityInput.tsx +++ b/packages/fakeyou.com/src/components/entities/EntityInput/EntityInput.tsx @@ -3,7 +3,8 @@ import { useLocation } from "react-router-dom"; import { a, useTransition } from "@react-spring/web"; import { MediaFile } from "@storyteller/components/src/api/media_files/GetMedia"; import Iframe from "react-iframe"; -import { Button, Label, Spinner } from "components/common"; +import { Area, Point } from "react-easy-crop"; +import { Label, Spinner, ZoomSliderOnChangeEvent } from "components/common"; import { AcceptTypes, EntityModeProp, @@ -15,12 +16,25 @@ import { useMedia, useMediaUploader } from "hooks"; import { BucketConfig } from "@storyteller/components/src/api/BucketConfig"; import { Prompt } from "@storyteller/components/src/api/prompts/GetPrompts"; import EntityInputEmpty from "./EntityInputEmpty"; +import EntityInputSidePanel from "./EntityInputSidePanel"; +import EntityInputImageVideoPreview from "./EntityInputImageVideoPreview"; import "./EntityInput.scss"; +export type OnCropComplete = ( + croppedArea: Area, + croppedAreaPixels: Area +) => void; + +export interface CropProps { + aspect: number; + onCropComplete?: OnCropComplete; +} + interface EntityInputProps { accept?: AcceptTypes | AcceptTypes[]; aspectRatio?: "square" | "landscape" | "portrait"; className?: string; + cropProps?: CropProps; debug?: string; label?: string; name?: string; @@ -31,14 +45,16 @@ interface EntityInputProps { } export interface SlideProps { - media?: MediaFile; clear: () => void; + cropProps?: CropProps; + media?: MediaFile; } interface AniProps { animating: boolean; className: string; isLeaving: boolean; + onCropComplete?: OnCropComplete; render: any; style: any; } @@ -47,53 +63,68 @@ const MediaBusy = () => { return ; }; -const EntityInputFull = ({ media, clear }: SlideProps) => { +const EntityInputFull = ({ clear, cropProps, media }: SlideProps) => { const bucketConfig = new BucketConfig(); const mediaUrl = media?.public_bucket_path ? bucketConfig.getGcsUrl(media.public_bucket_path) : ""; const mediaType = mediaCategoryfromString(media?.media_type || ""); - const uploader = `Uploaded by ${ - media?.maybe_creator_user?.display_name || "User" - }`; + + const [crop, cropSet] = useState({ x: 0, y: 0 }); + const [zoom, zoomSet] = useState(1); + + const zoomSliderChange = ({ target }: ZoomSliderOnChangeEvent) => + zoomSet(target.value); switch (mediaType) { case MediaFilters.image: return ( <> - -
-
- {media?.maybe_title || "Untitled image"} -
{uploader}
-
-
+ + ); case MediaFilters.video: return ( <> -