feat(frontend): cinematic loading, page-load entrance, mobile polish
Browse filesWithin the existing amber/polaroid identity:
- loading cycles poetic phrases so the ~40s read feels like attention paid
- staggered rise of wordmark -> tagline -> rule -> dropzone on load
- stronger drag-over state (amber border + inner glow)
- mobile breakpoint for the polaroid stack and film cards
- prefers-reduced-motion guard
- frontend/src/Chalchitra.jsx +53 -7
frontend/src/Chalchitra.jsx
CHANGED
|
@@ -13,6 +13,16 @@ const STACK = [
|
|
| 13 |
|
| 14 |
const fanOffset = (i, n) => (i - (n - 1) / 2) * 36;
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
// Read file → resize via canvas → compressed JPEG data URL.
|
| 17 |
// Keeps the payload to the model small; the backend accepts data URLs directly.
|
| 18 |
const processImage = (file) => new Promise((res) => {
|
|
@@ -56,13 +66,16 @@ const CSS = `
|
|
| 56 |
.chi{max-width:700px;margin:0 auto;padding:60px 24px 80px;position:relative;z-index:2}
|
| 57 |
|
| 58 |
.ch-hd{text-align:center;margin-bottom:52px}
|
| 59 |
-
.ch-wm{font-family:var(--fd);font-size:clamp(56px,11vw,92px);font-weight:300;font-style:italic;letter-spacing:.05em;color:var(--tx);line-height:1;text-shadow:0 0 80px rgba(201,146,62,.14);display:block}
|
| 60 |
-
.ch-tg{font-family:var(--fd);font-size:10.5px;letter-spacing:.32em;text-transform:uppercase;color:rgba(201,146,62,.88);margin-top:14px}
|
| 61 |
-
.ch-rl{width:28px;height:1px;background:var(--bd);margin:18px auto 0}
|
|
|
|
| 62 |
|
| 63 |
-
.ch-dp{border:1px solid var(--bd);padding:56px 40px;text-align:center;cursor:pointer;transition:
|
| 64 |
.ch-dp::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse at 50% 0,rgba(201,146,62,.06) 0%,transparent 65%);pointer-events:none}
|
| 65 |
-
.ch-dp:hover
|
|
|
|
|
|
|
| 66 |
.ch-dp-ic{display:block;font-size:24px;color:var(--am);opacity:.45;margin-bottom:18px}
|
| 67 |
.ch-dp-mn{font-family:var(--fd);font-size:23px;font-style:italic;font-weight:300;color:var(--tx);opacity:.6}
|
| 68 |
.ch-dp-hn{font-size:11px;letter-spacing:.2em;text-transform:uppercase;color:var(--tx2);margin-top:8px}
|
|
@@ -108,7 +121,8 @@ const CSS = `
|
|
| 108 |
.ch-ld{text-align:center;padding:72px 0}
|
| 109 |
.ch-lic{display:block;font-size:30px;color:var(--am);animation:chP 2.2s ease-in-out infinite;margin-bottom:22px}
|
| 110 |
@keyframes chP{0%,100%{opacity:.2;transform:scale(.95)}50%{opacity:.9;transform:scale(1.05)}}
|
| 111 |
-
.ch-ltx{font-family:var(--fd);font-size:18px;font-style:italic;font-weight:300;color:var(--tx2);letter-spacing:.12em}
|
|
|
|
| 112 |
|
| 113 |
.ch-mini-stack{position:relative;height:116px;margin-bottom:40px}
|
| 114 |
.ch-mini-photo{position:absolute;width:112px;background:#f0ebe1;padding:3px 3px 14px;box-shadow:0 5px 18px rgba(0,0,0,.6)}
|
|
@@ -137,6 +151,26 @@ const CSS = `
|
|
| 137 |
.ch-ag button:hover{color:var(--am);border-bottom-color:var(--am2)}
|
| 138 |
|
| 139 |
.ch-ft-cr{margin-top:64px;text-align:center;font-family:var(--fm);font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--tx2);opacity:.5}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
`;
|
| 141 |
|
| 142 |
export default function Chalchitra() {
|
|
@@ -149,6 +183,7 @@ export default function Chalchitra() {
|
|
| 149 |
// Titles already offered this session — sent as `exclude` so refreshing or
|
| 150 |
// re-rolling brings different films. Stateless server; the memory lives here.
|
| 151 |
const [seen, setSeen] = useState([]);
|
|
|
|
| 152 |
const ref = useRef(null);
|
| 153 |
|
| 154 |
useEffect(() => {
|
|
@@ -158,6 +193,17 @@ export default function Chalchitra() {
|
|
| 158 |
document.head.appendChild(link);
|
| 159 |
}, []);
|
| 160 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
const addFiles = async (fs) => {
|
| 162 |
const fileArray = Array.from(fs)
|
| 163 |
.filter(f => f.type.startsWith("image/"))
|
|
@@ -302,7 +348,7 @@ export default function Chalchitra() {
|
|
| 302 |
{phase === "loading" && (
|
| 303 |
<div className="ch-ld">
|
| 304 |
<span className="ch-lic">✦</span>
|
| 305 |
-
<p className="ch-ltx"
|
| 306 |
</div>
|
| 307 |
)}
|
| 308 |
|
|
|
|
| 13 |
|
| 14 |
const fanOffset = (i, n) => (i - (n - 1) / 2) * 36;
|
| 15 |
|
| 16 |
+
// Shown one at a time while the oracle reads — turns a ~40s wait into something
|
| 17 |
+
// that feels like attention being paid, not a spinner.
|
| 18 |
+
const LOADING_PHRASES = [
|
| 19 |
+
"reading your moment",
|
| 20 |
+
"studying the quality of light",
|
| 21 |
+
"listening for the mood beneath",
|
| 22 |
+
"searching the reels",
|
| 23 |
+
"finding what belongs to you",
|
| 24 |
+
];
|
| 25 |
+
|
| 26 |
// Read file → resize via canvas → compressed JPEG data URL.
|
| 27 |
// Keeps the payload to the model small; the backend accepts data URLs directly.
|
| 28 |
const processImage = (file) => new Promise((res) => {
|
|
|
|
| 66 |
.chi{max-width:700px;margin:0 auto;padding:60px 24px 80px;position:relative;z-index:2}
|
| 67 |
|
| 68 |
.ch-hd{text-align:center;margin-bottom:52px}
|
| 69 |
+
.ch-wm{font-family:var(--fd);font-size:clamp(56px,11vw,92px);font-weight:300;font-style:italic;letter-spacing:.05em;color:var(--tx);line-height:1;text-shadow:0 0 80px rgba(201,146,62,.14);display:block;animation:chRise 1.2s cubic-bezier(.2,.7,.2,1) both}
|
| 70 |
+
.ch-tg{font-family:var(--fd);font-size:10.5px;letter-spacing:.32em;text-transform:uppercase;color:rgba(201,146,62,.88);margin-top:14px;animation:chRise 1.2s .14s cubic-bezier(.2,.7,.2,1) both}
|
| 71 |
+
.ch-rl{width:28px;height:1px;background:var(--bd);margin:18px auto 0;animation:chRise 1.2s .28s cubic-bezier(.2,.7,.2,1) both}
|
| 72 |
+
@keyframes chRise{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:none}}
|
| 73 |
|
| 74 |
+
.ch-dp{border:1px solid var(--bd);padding:56px 40px;text-align:center;cursor:pointer;transition:border-color .3s,box-shadow .3s,background .3s;background:var(--am3);position:relative;overflow:hidden;animation:chRise 1.2s .42s cubic-bezier(.2,.7,.2,1) both}
|
| 75 |
.ch-dp::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse at 50% 0,rgba(201,146,62,.06) 0%,transparent 65%);pointer-events:none}
|
| 76 |
+
.ch-dp:hover{border-color:var(--am2)}
|
| 77 |
+
.ch-dp-ov{border-color:var(--am);background:var(--am2);box-shadow:inset 0 0 70px rgba(201,146,62,.12)}
|
| 78 |
+
.ch-dp-ov .ch-dp-ic{opacity:.85}
|
| 79 |
.ch-dp-ic{display:block;font-size:24px;color:var(--am);opacity:.45;margin-bottom:18px}
|
| 80 |
.ch-dp-mn{font-family:var(--fd);font-size:23px;font-style:italic;font-weight:300;color:var(--tx);opacity:.6}
|
| 81 |
.ch-dp-hn{font-size:11px;letter-spacing:.2em;text-transform:uppercase;color:var(--tx2);margin-top:8px}
|
|
|
|
| 121 |
.ch-ld{text-align:center;padding:72px 0}
|
| 122 |
.ch-lic{display:block;font-size:30px;color:var(--am);animation:chP 2.2s ease-in-out infinite;margin-bottom:22px}
|
| 123 |
@keyframes chP{0%,100%{opacity:.2;transform:scale(.95)}50%{opacity:.9;transform:scale(1.05)}}
|
| 124 |
+
.ch-ltx{font-family:var(--fd);font-size:18px;font-style:italic;font-weight:300;color:var(--tx2);letter-spacing:.12em;animation:chPhrase .9s ease both}
|
| 125 |
+
@keyframes chPhrase{0%{opacity:0;transform:translateY(6px)}100%{opacity:.85;transform:none}}
|
| 126 |
|
| 127 |
.ch-mini-stack{position:relative;height:116px;margin-bottom:40px}
|
| 128 |
.ch-mini-photo{position:absolute;width:112px;background:#f0ebe1;padding:3px 3px 14px;box-shadow:0 5px 18px rgba(0,0,0,.6)}
|
|
|
|
| 151 |
.ch-ag button:hover{color:var(--am);border-bottom-color:var(--am2)}
|
| 152 |
|
| 153 |
.ch-ft-cr{margin-top:64px;text-align:center;font-family:var(--fm);font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--tx2);opacity:.5}
|
| 154 |
+
|
| 155 |
+
@media (max-width:600px){
|
| 156 |
+
.chi{padding:42px 18px 64px}
|
| 157 |
+
.ch-hd{margin-bottom:38px}
|
| 158 |
+
.ch-dp{padding:46px 26px}
|
| 159 |
+
.ch-stack{height:300px}
|
| 160 |
+
.ch-photo{width:74%;left:13%}
|
| 161 |
+
.ch-fi{font-size:18px}
|
| 162 |
+
.ch-ip{padding-left:18px;margin-bottom:34px}
|
| 163 |
+
.ch-ipt{font-size:19px;line-height:1.66}
|
| 164 |
+
.ch-fc{padding:22px 18px}
|
| 165 |
+
.ch-ft{font-size:23px}
|
| 166 |
+
.ch-fc:first-child .ch-ft{font-size:26px}
|
| 167 |
+
.ch-fw{font-size:16px}
|
| 168 |
+
.ch-bn{padding:13px 34px}
|
| 169 |
+
.ch-ag{gap:22px;margin-top:42px}
|
| 170 |
+
}
|
| 171 |
+
@media (prefers-reduced-motion:reduce){
|
| 172 |
+
*{animation-duration:.001ms!important;animation-iteration-count:1!important}
|
| 173 |
+
}
|
| 174 |
`;
|
| 175 |
|
| 176 |
export default function Chalchitra() {
|
|
|
|
| 183 |
// Titles already offered this session — sent as `exclude` so refreshing or
|
| 184 |
// re-rolling brings different films. Stateless server; the memory lives here.
|
| 185 |
const [seen, setSeen] = useState([]);
|
| 186 |
+
const [phraseI, setPhraseI] = useState(0);
|
| 187 |
const ref = useRef(null);
|
| 188 |
|
| 189 |
useEffect(() => {
|
|
|
|
| 193 |
document.head.appendChild(link);
|
| 194 |
}, []);
|
| 195 |
|
| 196 |
+
// Cycle the loading phrases only while reading.
|
| 197 |
+
useEffect(() => {
|
| 198 |
+
if (phase !== "loading") return;
|
| 199 |
+
setPhraseI(0);
|
| 200 |
+
const id = setInterval(
|
| 201 |
+
() => setPhraseI(i => (i + 1) % LOADING_PHRASES.length),
|
| 202 |
+
2800
|
| 203 |
+
);
|
| 204 |
+
return () => clearInterval(id);
|
| 205 |
+
}, [phase]);
|
| 206 |
+
|
| 207 |
const addFiles = async (fs) => {
|
| 208 |
const fileArray = Array.from(fs)
|
| 209 |
.filter(f => f.type.startsWith("image/"))
|
|
|
|
| 348 |
{phase === "loading" && (
|
| 349 |
<div className="ch-ld">
|
| 350 |
<span className="ch-lic">✦</span>
|
| 351 |
+
<p className="ch-ltx" key={phraseI}>{LOADING_PHRASES[phraseI]}…</p>
|
| 352 |
</div>
|
| 353 |
)}
|
| 354 |
|