background gallery + many other styling and animation transition updates

This commit is contained in:
bflatastic committed 2025-03-20 22:11:12 +07:00
1 parent 3602f8fc2b
commit e83389a874
28 files changed
+315 -54

No files matched your search

+18 -6
View File
@@ -8,6 +8,7 @@
"name": "storyteller-board",
"version": "0.0.0",
"dependencies": {
"@devnomic/marquee": "^1.0.3",
"@fortawesome/free-brands-svg-icons": "^6.6.0",
"@headlessui/react": "^2.1.3",
"@preact/signals-react": "^2.2.0",
@@ -381,6 +382,21 @@
"node": ">=6.9.0"
}
},
"node_modules/@devnomic/marquee": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@devnomic/marquee/-/marquee-1.0.3.tgz",
"integrity": "sha512-q8QUtNO8ff+UvfrKJsHBtxh4fbOfhGk6SSdK0Uu/XZFdEzrmiGJ/l0FuVHRYsPAmzgnhVVnz1dGNB5CWGLMVvw==",
"dependencies": {
"clsx": "^2.1.0",
"tailwind-merge": "^2.2.0"
},
"peerDependencies": {
"@types/react": "^18.2.47",
"@types/react-dom": "^18.2.18",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
},
"node_modules/@emotion/is-prop-valid": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.0.tgz",
@@ -1510,14 +1526,12 @@
"node_modules/@types/prop-types": {
"version": "15.7.12",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
"dev": true
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q=="
},
"node_modules/@types/react": {
"version": "18.3.4",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz",
"integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==",
"dev": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -1527,7 +1541,6 @@
"version": "18.3.0",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz",
"integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
@@ -2364,8 +2377,7 @@
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/data-view-buffer": {
"version": "1.0.1",
+1
View File
@@ -11,6 +11,7 @@
"preview": "vite preview"
},
"dependencies": {
"@devnomic/marquee": "^1.0.3",
"@fortawesome/free-brands-svg-icons": "^6.6.0",
"@headlessui/react": "^2.1.3",
"@preact/signals-react": "^2.2.0",
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

@@ -0,0 +1,145 @@
import { useEffect, useState } from "react";
import { Marquee } from "@devnomic/marquee";
import "@devnomic/marquee/dist/index.css";
function getRandomImages(count: number) {
const images = Array.from({ length: count }, (_, i) => ({
id: i + 1,
src: `/images/generate/image${i + 1}.png`,
alt: `Generated image ${i + 1}`,
}));
// Shuffle the array
return [...images].sort(() => Math.random() - 0.5);
}
interface ImageColumnProps {
images: Array<{ id: number; src: string; alt: string }>;
delay: number;
reverse?: boolean;
isLoaded: boolean;
duration?: number;
}
function ImageColumn({
images,
delay,
reverse = false,
duration = 100,
isLoaded,
}: ImageColumnProps) {
return (
<div className="flex h-screen flex-col gap-6">
<Marquee
fade={false}
direction="up"
reverse={reverse}
pauseOnHover={false}
className={`h-full`}
style={{ "--duration": `${duration}s` } as React.CSSProperties}
innerClassName="gap-6 [--gap:24px]"
numberOfCopies={2}
>
{images.map((image, i) => (
<div
key={`${image.id}-${i}`}
className={`relative aspect-[4/5] w-full overflow-hidden rounded-lg shadow-lg transition-all duration-700 ease-out ${
isLoaded
? "translate-y-0 opacity-100"
: "translate-y-12 opacity-0"
}`}
style={{
transitionDelay: `${delay + i * 50}ms`,
}}
>
<img
src={image.src}
alt={image.alt}
className="w-full object-cover"
/>
</div>
))}
</Marquee>
</div>
);
}
export default function GalleryBackground() {
const [images, _setImages] = useState(() => getRandomImages(20));
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
// delay for smooth animation
const timer = setTimeout(() => {
setIsLoaded(true);
}, 100);
return () => clearTimeout(timer);
}, []);
// Columns for masonry layout
const columns = {
col1: images.filter((_, i) => i % 5 === 0),
col2: images.filter((_, i) => i % 5 === 1),
col3: images.filter((_, i) => i % 5 === 2),
col4: images.filter((_, i) => i % 5 === 3),
col5: images.filter((_, i) => i % 5 === 4),
};
return (
<>
<div className="fixed inset-0 z-[1] overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,rgba(0,0,0,0.00)_49%,rgba(0,0,0,0.60)_100%)]" />
<div className="fixed inset-0 z-0 overflow-hidden">
<div
className={`h-full w-full transition-opacity duration-1000 ${isLoaded ? "opacity-50" : "opacity-0"}`}
>
<div className="absolute inset-0 grid grid-cols-5 gap-6 px-6 opacity-35">
<ImageColumn
images={columns.col1}
delay={0}
isLoaded={isLoaded}
duration={100}
/>
<ImageColumn
images={columns.col2}
delay={columns.col1.length * 50}
isLoaded={isLoaded}
reverse={true}
duration={100}
/>
<ImageColumn
images={columns.col3}
delay={(columns.col1.length + columns.col2.length) * 50}
isLoaded={isLoaded}
duration={125}
/>
<ImageColumn
images={columns.col4}
delay={
(columns.col1.length +
columns.col2.length +
columns.col3.length) *
50
}
isLoaded={isLoaded}
reverse={true}
duration={160}
/>
<ImageColumn
images={columns.col5}
delay={
(columns.col1.length +
columns.col2.length +
columns.col3.length +
columns.col4.length) *
50
}
isLoaded={isLoaded}
duration={110}
/>
</div>
</div>
</div>
</>
);
}
@@ -3,7 +3,10 @@ import { useSignals } from "@preact/signals-react/runtime";
import { Button, LoadingSpinner } from "~/components/ui";
import { useRenderCounter } from "~/hooks/useRenderCounter";
import { GenerationLoadingState, generationSignal } from "~/signals";
import { useRef } from "react";
import { useRef, useState } from "react";
import BackgroundGallery from "./BackgroundGallery";
import { twMerge } from "tailwind-merge";
import { Transition } from "@headlessui/react";
export const GenerationRootComponent = () => {
// This is a hook that will log the number of times the component has rerendered
@@ -12,22 +15,29 @@ export const GenerationRootComponent = () => {
useSignals();
const generationState = generationSignal.value;
const inputRef = useRef<HTMLInputElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
const [isInputFocused, setIsInputFocused] = useState(false);
const handleGenerate = () => {
if (!inputRef.current) {
return
};
return;
}
// TODO: For testing only!!! Remove when finished
if (generationState.loadingState === GenerationLoadingState.GENERATING) {
generationSignal.value = { loadingState: GenerationLoadingState.INIT, prompt: "" };
generationSignal.value = {
loadingState: GenerationLoadingState.INIT,
prompt: "",
};
return;
}
const prompt = inputRef.current.value;
generationSignal.value = { loadingState: GenerationLoadingState.GENERATING, prompt };
generationSignal.value = {
loadingState: GenerationLoadingState.GENERATING,
prompt,
};
// TODO: Write the signal change effect in generationSignals.ts
// TODO: Call the server to generate the image
};
@@ -35,15 +45,17 @@ export const GenerationRootComponent = () => {
let contentElement;
switch (generationState.loadingState) {
case GenerationLoadingState.INIT:
contentElement =
<div className="flex flex-col items-center justify-center mb-32">
<span className="text-9xl font-bold">Generate Image</span>
<span className="text-3xl pt-2">Imagine, Describe, Generate.</span>
</div>
break;
contentElement = null;
break;
case GenerationLoadingState.GENERATING:
case GenerationLoadingState.GENERATED:
contentElement = <GenerationContent loadingState={generationState.loadingState} imageB64={generationState.imageB64} prompt={generationState.prompt} />
contentElement = (
<GenerationContent
loadingState={generationState.loadingState}
imageB64={generationState.imageB64}
prompt={generationState.prompt}
/>
);
break;
}
@@ -52,50 +64,129 @@ export const GenerationRootComponent = () => {
return (
<>
<div className="fixed flex flex-col items-center justify-center w-full h-full gap-y-12 transition-all duration-1000">
<div className="fixed z-10 flex h-full w-full flex-col items-center justify-center transition-all duration-500">
<Transition
show={generationState.loadingState !== GenerationLoadingState.INIT}
enter="transition-opacity duration-500"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity duration-500"
leaveFrom="opacity-100"
leaveTo="opacity-0"
as="div"
>
{contentElement}
<div className="glass p-3 rounded-xl flex w-1/2 border-ui-panel border-2">
<input ref={inputRef} type="text" placeholder="Describe what you want to see..." className="flex-1 text-lg rounded-md bg-transparent focus:outline-none" />
<Button icon={faSparkles} variant="primary" className="text-lg" onClick={handleGenerate} disabled={isButtonDisabled}>Generate</Button>
</Transition>
<div
className={twMerge(
"absolute flex w-full flex-col items-center justify-center gap-16 transition-all duration-500",
generationState.loadingState === GenerationLoadingState.INIT
? "bottom-1/2 translate-y-1/2 transform"
: "bottom-8",
)}
>
<Transition
show={generationState.loadingState === GenerationLoadingState.INIT}
enter="transition-opacity duration-200"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
as="div"
>
<div className="flex flex-col items-center justify-center text-center drop-shadow-xl">
<span className="text-8xl font-bold">Generate Image</span>
<span className="pt-2 text-2xl opacity-80">
Imagine, Describe, Generate.
</span>
</div>
</Transition>
<div
className={twMerge(
"glass flex min-h-[56px] w-full max-w-[900px] items-end rounded-xl border-2 p-3 shadow-xl transition-all duration-[400ms] ease-in-out",
isInputFocused ? "border-primary-400/60" : "border-ui-panel",
)}
>
<textarea
ref={inputRef}
rows={1}
placeholder="Describe what you want to see..."
className="max-h-[120px] min-h-[40px] flex-1 resize-none overflow-y-auto rounded-md bg-transparent px-3 py-2 text-lg leading-normal focus:outline-none"
style={{ lineHeight: "24px" }}
onInput={(e) => {
const target = e.target as HTMLTextAreaElement;
target.style.height = "auto";
target.style.height = `${target.scrollHeight}px`;
}}
onFocus={() => setIsInputFocused(true)}
onBlur={() => setIsInputFocused(false)}
/>
<Button
icon={faSparkles}
variant="primary"
className="text-md ml-2 self-end"
onClick={handleGenerate}
disabled={isButtonDisabled}
>
Generate
</Button>
</div>
</div>
</div>
<Transition
show={generationState.loadingState === GenerationLoadingState.INIT}
enter="transition-opacity duration-700"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity duration-700"
leaveFrom="opacity-100"
leaveTo="opacity-0"
as="div"
>
<BackgroundGallery />
</Transition>
</>
);
};
const GenerationContent = (generationState : {
const GenerationContent = (generationState: {
loadingState: GenerationLoadingState;
imageB64?: string;
prompt: string;
}) => {
const imgBoxStyle = {
width: "1024px",
height: "1024px",
}
let imgBoxContent;
if (generationState.loadingState === GenerationLoadingState.GENERATING) {
imgBoxContent = (
<div className="w-full h-full flex items-center justify-center">
<div className="flex h-full w-full items-center justify-center">
<LoadingSpinner isShowing={true} message="Generating image..." />
</div>
)
} else if (generationState.loadingState === GenerationLoadingState.GENERATED) {
);
} else if (
generationState.loadingState === GenerationLoadingState.GENERATED
) {
imgBoxContent = (
<img src={generationState.imageB64} alt="Generated Image" />
)
<img
src={generationState.imageB64}
alt="Generated Image"
className="h-full w-full object-contain"
/>
);
}
return (
<div className="flex flex-col items-center justify-center" >
<div style={imgBoxStyle} className="bg-ui-border rounded-lg">
{imgBoxContent}
<>
<div className="absolute inset-0 flex h-full w-full flex-col items-center justify-center">
<div className="aspect-[1/1] w-[min(80vw,68vh)] rounded-t-lg bg-[#29292D]/60">
{imgBoxContent}
</div>
<div className="mb-16 flex w-[min(80vw,68vh)] rounded-b-lg bg-[#29292D]">
<span className="p-3.5 text-sm opacity-50">
{generationState.prompt || "No prompt provided"}
</span>
</div>
</div>
<div className="bg-ui-panel w-full min-h-12 flex items-center justify-center">
<span className="text-lg p-4">{generationState.prompt}</span>
</div>
</div>
)
}
</>
);
};
@@ -27,7 +27,9 @@ export const ToolbarTopLeft = () => {
return (
<div
className={twMerge("z-20 flex h-fit w-fit items-center gap-8 pl-1 pr-4")}
className={twMerge(
"relative z-50 flex h-fit w-fit items-center gap-8 pl-1 pr-4",
)}
>
<img
src="/brand/mira-logo.png"
@@ -22,7 +22,9 @@ export const ToolbarTopRight = () => {
const [aspectRatio, setAspectRatio] = useState("1:1");
return (
<div className={twMerge("z-20 flex h-fit w-fit items-center gap-2")}>
<div
className={twMerge("relative z-50 flex h-fit w-fit items-center gap-2")}
>
{import.meta.env.DEV && ( // only show in dev mode for now
<ToggleButton
label="Ratio"
+14 -6
View File
@@ -200,6 +200,14 @@
"@babel/helper-validator-identifier" "^7.24.7"
to-fast-properties "^2.0.0"
"@devnomic/marquee@^1.0.3":
version "1.0.3"
resolved "https://registry.npmjs.org/@devnomic/marquee/-/marquee-1.0.3.tgz"
integrity sha512-q8QUtNO8ff+UvfrKJsHBtxh4fbOfhGk6SSdK0Uu/XZFdEzrmiGJ/l0FuVHRYsPAmzgnhVVnz1dGNB5CWGLMVvw==
dependencies:
clsx "^2.1.0"
tailwind-merge "^2.2.0"
"@emotion/is-prop-valid@^1.1.0":
version "1.3.0"
resolved "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.0.tgz"
@@ -910,14 +918,14 @@
resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz"
integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==
"@types/react-dom@^18.3.0":
"@types/react-dom@^18.2.18", "@types/react-dom@^18.3.0":
version "18.3.0"
resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz"
integrity sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^18.3.3":
"@types/react@*", "@types/react@^18.2.47", "@types/react@^18.3.3":
version "18.3.4"
resolved "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz"
integrity sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==
@@ -1416,7 +1424,7 @@ cli-columns@^4.0.0:
string-width "^4.2.3"
strip-ansi "^6.0.1"
clsx@^2.0.0:
clsx@^2.0.0, clsx@^2.1.0:
version "2.1.1"
resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
@@ -3635,7 +3643,7 @@ react-colorful@^5.6.1:
resolved "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz"
integrity sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==
"react-dom@^16.8.0 || ^17.0.0 || ^18.0.0", react-dom@^18, react-dom@^18.0.0, "react-dom@^18.0.0 || ^19.0.0 || ^19.0.0-rc", react-dom@^18.3.1, "react-dom@>= 16.8.0", react-dom@>=16.4.0, react-dom@>=16.8, react-dom@>=16.8.0:
"react-dom@^16.8.0 || ^17.0.0 || ^18.0.0", react-dom@^18, react-dom@^18.0.0, "react-dom@^18.0.0 || ^19.0.0 || ^19.0.0-rc", react-dom@^18.2.0, react-dom@^18.3.1, "react-dom@>= 16.8.0", react-dom@>=16.4.0, react-dom@>=16.8, react-dom@>=16.8.0:
version "18.3.1"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz"
integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==
@@ -3684,7 +3692,7 @@ react-router@6.26.1:
dependencies:
"@remix-run/router" "1.19.1"
"react@^16.14.0 || 17.x || 18.x", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0", react@^18, react@^18.0.0, "react@^18.0.0 || ^19.0.0 || ^19.0.0-rc", react@^18.3.1, "react@>= 16.8.0", react@>=16.3, react@>=16.4.0, react@>=16.8, react@>=16.8.0:
"react@^16.14.0 || 17.x || 18.x", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0", react@^18, react@^18.0.0, "react@^18.0.0 || ^19.0.0 || ^19.0.0-rc", react@^18.2.0, react@^18.3.1, "react@>= 16.8.0", react@>=16.3, react@>=16.4.0, react@>=16.8, react@>=16.8.0:
version "18.3.1"
resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz"
integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
@@ -4180,7 +4188,7 @@ tabbable@^6.0.0:
resolved "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz"
integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==
tailwind-merge@^2.5.2:
tailwind-merge@^2.2.0, tailwind-merge@^2.5.2:
version "2.5.2"
resolved "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.2.tgz"
integrity sha512-kjEBm+pvD+6eAwzJL2Bi+02/9LFLal1Gs61+QB7HvTfQQ0aXwC5LGT8PEt1gS0CWKktKe6ysPTAy3cBC5MeiIg==