feat: KolayTercih lansman videosu (Remotion, v3)
64 saniyelik, kesintisiz döngü lansman filmi; 9:16 ve 16:9. - remocn.dev bileşenleri (sheen-slide-in, tracking-in, gradient-scale-cut-text, outline-fill-track-text, perspective-marquee, path-ride, zoom-blur, lens-zoom) KolayTercih piksel diline uyarlandı (src/kinetik.tsx) - Sahne içi imleç + imleci takip eden kamera, piksel iz ve tık patlaması - Sessizlik → BAM kurgusu (lensZoom), punch zoom ve hareket bulanıklığı - Müzik ve efektler numpy ile sentezlendi (ses/uret.py), zaman.json tek kaynak - Örnek taban sıralamaları gerçek YÖK Atlas 2026 kayıtları Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
out
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
58
README.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# KolayTercih lansman videosu (v3)
|
||||||
|
|
||||||
|
Remotion ile kodla üretilen 64 saniyelik, **kesintisiz döngü** lansman filmi. Müzik ve efektler
|
||||||
|
kodla sentezlendi. Bileşen ve geçişlerin bir kısmı [remocn.dev](https://remocn.dev) kayıt
|
||||||
|
defterinden alınıp (`src/remocn/`) KolayTercih'in piksel diline uyarlandı (`src/kinetik.tsx`).
|
||||||
|
|
||||||
|
## Kurgu
|
||||||
|
|
||||||
|
Her sahne tek bir fikir anlatır. Üç yerde müzik birden kesilir, ekran sessizleşir, sonra lens
|
||||||
|
patlamasıyla (remocn `lensZoom`) BAM gelir:
|
||||||
|
|
||||||
|
| Sn | Sahne | Metin |
|
||||||
|
|---|---|---|
|
||||||
|
| 0–3.5 | Açılış | "45.320." (Track Text) · "Sıralaman geldi. Tebrikler! Sırada küçük bir bulmaca var." |
|
||||||
|
| 3.5–7 | Seçenekler | Perspective Marquee'de akan bölümler · "21.000+ program var. Senin 24 hakkın var." (Sheen) |
|
||||||
|
| 7–10.5 | Kaos | "Kafanda yüz sekme açık, değil mi?" |
|
||||||
|
| 10.5–13 | **Sessizlik** | "Derin bir nefes al." |
|
||||||
|
| 13–16.5 | **BAM → Logo** | Dev piksel "KolayTercih" (Gradient Scale Cut) → logo · "Tercih listeni gerçek veriyle kur, panikle değil." |
|
||||||
|
| 16.5–22 | Adım 1 | "Sıralamanı yaz." Tam ekran sihirbaz; kamera imlece kilitlenir |
|
||||||
|
| 22–28 | Adım 2 | "Tabloları biz okuruz." Harita yukarıdan dökülür; imleç ısı fırçası, kamera takip eder · "Taranan: 21.000+ program" (Outline Fill Track) |
|
||||||
|
| 28–33.5 | Adım 3 | "Listen hazır." · "Hayal de var, akıl da." (Path Ride) · 24 kare, imleç her kareyi takip eder |
|
||||||
|
| 33.5–36 | **Sessizlik** | "Off. Kafan mı karıştı?" (imleç kararsız dolanır) |
|
||||||
|
| 36–38 | **BAM → Danışman** | DanışmanlaKonuş · "Aklına takılanı sor. Gece 3'te bile." |
|
||||||
|
| 38–48 | Sohbet | Tam ekran danışman: `program_ara`, `profil_guncelle` (gerçek YÖK Atlas verisi) |
|
||||||
|
| 48–52.5 | Paket | Apple hızlı kesmeleri: "6 yıllık trend." · "PDF rapor. Aile toplantısına hazır." · "2 revizyon. Fikrin değişir, listen de." |
|
||||||
|
| 52.5–56.5 | Dürüstlük | "Yerleşme garantisi vermiyoruz. Çünkü kimse veremez." (Outline Fill) |
|
||||||
|
| 56.5–58 | **Sessizlik** | "Hazır mısın?" |
|
||||||
|
| 58–64 | **BAM → Final** | Logo + "Listeni kur, panikle değil." + kolaytercih.com butonu → tık → çöküş → boş ekran (döngü) |
|
||||||
|
|
||||||
|
- İmleç yalnız bir şey yapılırken görünür; göründüğü sahnelerde kamera ona yaklaşıp takip eder
|
||||||
|
(`TakipKamera`, `src/imlec.tsx`). Geçtiği yerde piksel iz, tıkladığı yerde piksel patlaması.
|
||||||
|
- Telefon maketi yok; arayüz tüm kadrajı kullanır.
|
||||||
|
- Gren v2'nin üçte biri. Görseller `<Img>` ile yüklendiği için titreme yok.
|
||||||
|
|
||||||
|
## remocn bileşenleri
|
||||||
|
|
||||||
|
`sheen-slide-in`, `tracking-in`, `gradient-scale-cut-text`, `outline-fill-track-text`,
|
||||||
|
`perspective-marquee`, `path-ride` (yardımcı fonksiyonları), `zoom-blur`, `lens-zoom`.
|
||||||
|
İndirme: `https://remocn.dev/r/<ad>.json` (shadcn kayıt biçimi). Uyarlamada yumuşak degradeler
|
||||||
|
logonun üç rengiyle (mavi, şeftali, turuncu) basamaklı piksel bantlara, açılma kenarları kare
|
||||||
|
kare ilerleyen maskelere çevrildi; yazı tipleri Cal Sans / Bricolage.
|
||||||
|
|
||||||
|
## Değiştirmek
|
||||||
|
|
||||||
|
- Süreler, geçişler, imleç tıkları ve yazma aralıkları: `src/zaman.json` → sonra
|
||||||
|
`python3 ses/uret.py`.
|
||||||
|
- Punch zoomlar ve hareket bulanıklığı pencereleri: `src/vurus.ts`.
|
||||||
|
- Metinler: `src/sahneler-1.tsx`, `src/sahneler-2.tsx`, `src/sahneler-3.tsx`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
pnpm studio
|
||||||
|
python3 ses/uret.py
|
||||||
|
node scripts/kareler.mjs Dikey 0.3 400 540 740
|
||||||
|
pnpm render:dikey && pnpm render:yatay
|
||||||
|
```
|
||||||
|
|
||||||
|
v2 sahneleri `src/eski/` altında duruyor (derlemeye girmez).
|
||||||
27
package.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "kolaytercih-lansman",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"studio": "remotion studio",
|
||||||
|
"render:dikey": "remotion render Lansman-Dikey out/kolaytercih-lansman-9x16.mp4",
|
||||||
|
"render:yatay": "remotion render Lansman-Yatay out/kolaytercih-lansman-16x9.mp4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@remotion/cli": "^4.0.530",
|
||||||
|
"@remotion/fonts": "^4.0.530",
|
||||||
|
"@remotion/google-fonts": "4.0.530",
|
||||||
|
"@remotion/layout-utils": "4.0.530",
|
||||||
|
"@remotion/motion-blur": "4.0.530",
|
||||||
|
"@remotion/paths": "4.0.530",
|
||||||
|
"@remotion/transitions": "4.0.530",
|
||||||
|
"react": "19.2.4",
|
||||||
|
"react-dom": "19.2.4",
|
||||||
|
"remotion": "^4.0.530"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@remotion/bundler": "4.0.530",
|
||||||
|
"@remotion/renderer": "4.0.530",
|
||||||
|
"@types/react": "^19.3.0",
|
||||||
|
"typescript": "^7.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
2991
pnpm-lock.yaml
generated
Normal file
BIN
public/fonts/ClarityCity-Bold.woff2
Normal file
BIN
public/fonts/ClarityCity-Regular.woff2
Normal file
BIN
public/fonts/ClarityCity-SemiBold.woff2
Normal file
BIN
public/fonts/bricolage-grotesque-latin-300-normal.woff
Normal file
BIN
public/fonts/bricolage-grotesque-latin-600-normal.woff
Normal file
BIN
public/fonts/bricolage-grotesque-latin-800-normal.woff
Normal file
BIN
public/fonts/bricolage-grotesque-latin-ext-300-normal.woff
Normal file
BIN
public/fonts/bricolage-grotesque-latin-ext-600-normal.woff
Normal file
BIN
public/fonts/bricolage-grotesque-latin-ext-800-normal.woff
Normal file
BIN
public/fonts/cal-sans-latin-400-normal.woff
Normal file
BIN
public/fonts/cal-sans-latin-ext-400-normal.woff
Normal file
BIN
public/gren/g1.png
Normal file
|
After Width: | Height: | Size: 498 KiB |
BIN
public/gren/g2.png
Normal file
|
After Width: | Height: | Size: 498 KiB |
BIN
public/gren/g3.png
Normal file
|
After Width: | Height: | Size: 498 KiB |
BIN
public/gren/g4.png
Normal file
|
After Width: | Height: | Size: 498 KiB |
BIN
public/gren/g5.png
Normal file
|
After Width: | Height: | Size: 498 KiB |
BIN
public/gren/g6.png
Normal file
|
After Width: | Height: | Size: 498 KiB |
14
public/kolay-tercih-logo.svg
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 199 44" role="img" aria-labelledby="kt-title">
|
||||||
|
<title id="kt-title">KolayTercih</title>
|
||||||
|
<!-- Mark: public/kolay-tercih-mark.svg (viewBox 180 -> 44px) -->
|
||||||
|
<g transform="scale(0.244444)">
|
||||||
|
<rect x="27" y="56" width="57" height="57" rx="13" fill="#3b82f6"/>
|
||||||
|
<rect x="91" y="29" width="56" height="56" rx="13" fill="#fdc7a7"/>
|
||||||
|
<rect x="91" y="95" width="56" height="56" rx="13" fill="#ff5a00"/>
|
||||||
|
</g>
|
||||||
|
<!-- "Kolay" — Bricolage Grotesque 300, "Tercih" — 600, tracking -0.05em -->
|
||||||
|
<g fill="#0a0a0a">
|
||||||
|
<path d="M48.92 31.90L46.70 31.90L46.70 12.10L48.92 12.10L48.92 21.76Q50.66 21.19 52.30 20.23Q53.93 19.27 55.36 17.99Q56.78 16.72 57.84 15.22Q58.91 13.72 59.48 12.10L61.91 12.10Q61.25 13.93 60.23 15.47Q59.21 17.02 57.95 18.27Q56.69 19.51 55.34 20.44Q53.99 21.37 52.67 21.91L52.67 22.27Q54.17 22.30 55.38 22.70Q56.60 23.11 57.60 23.90Q58.61 24.70 59.42 25.91Q60.23 27.13 60.89 28.78L62.06 31.90L59.54 31.90L58.82 29.59Q58.07 27.55 57.08 26.21Q56.09 24.88 54.59 24.20Q53.09 23.53 50.72 23.53L48.92 23.53M70.91 32.26Q68.72 32.26 67.05 31.29Q65.39 30.31 64.47 28.49Q63.56 26.68 63.56 24.10Q63.56 21.40 64.53 19.63Q65.51 17.86 67.16 16.98Q68.81 16.09 70.82 16.09Q72.89 16.09 74.55 17.03Q76.22 17.98 77.19 19.79Q78.17 21.61 78.17 24.25Q78.17 26.89 77.21 28.67Q76.25 30.46 74.60 31.36Q72.95 32.26 70.91 32.26M71 30.34Q72.53 30.34 73.64 29.59Q74.75 28.84 75.35 27.47Q75.95 26.11 75.95 24.28Q75.95 22.39 75.32 20.99Q74.69 19.60 73.53 18.84Q72.38 18.07 70.76 18.07Q69.26 18.07 68.15 18.77Q67.04 19.48 66.41 20.81Q65.78 22.15 65.78 24.04Q65.78 26.95 67.19 28.64Q68.60 30.34 71 30.34M83.06 31.90L80.84 31.90L80.84 11.05L83.06 11.05M90.08 32.26Q88.82 32.26 87.80 31.79Q86.78 31.33 86.18 30.34Q85.58 29.35 85.58 27.88Q85.58 26.83 85.95 25.99Q86.33 25.15 87.14 24.54Q87.95 23.92 89.30 23.50Q90.65 23.08 92.60 22.84Q93.80 22.69 94.52 22.52Q95.24 22.36 95.58 22.02Q95.93 21.67 95.93 20.95Q95.93 19.72 95.09 18.86Q94.25 18.01 92.45 18.01Q91.58 18.01 90.62 18.31Q89.66 18.61 88.94 19.36Q88.22 20.11 88.04 21.49L85.97 20.74Q86.18 19.69 86.69 18.85Q87.20 18.01 88.04 17.38Q88.88 16.75 90 16.42Q91.13 16.09 92.51 16.09Q94.40 16.09 95.64 16.72Q96.89 17.35 97.50 18.66Q98.12 19.96 98.12 22L98.12 25.69Q98.12 26.59 98.16 27.68Q98.21 28.78 98.30 29.88Q98.39 30.97 98.48 31.90L96.41 31.90Q96.32 30.88 96.26 29.92Q96.20 28.96 96.17 28L95.81 28Q95.39 29.11 94.64 30.08Q93.89 31.06 92.77 31.66Q91.64 32.26 90.08 32.26M90.56 30.37Q91.22 30.37 91.94 30.14Q92.66 29.92 93.38 29.42Q94.10 28.93 94.75 28.10Q95.39 27.28 95.96 26.11L95.96 23.71Q95.75 23.80 95.51 23.86Q94.67 24.13 93.65 24.28Q92.63 24.43 91.59 24.61Q90.56 24.79 89.70 25.13Q88.85 25.48 88.33 26.09Q87.80 26.71 87.80 27.79Q87.80 29.08 88.56 29.72Q89.33 30.37 90.56 30.37M106.94 37.03Q105.71 37.03 104.54 36.73Q103.37 36.43 102.42 35.83Q101.48 35.23 100.88 34.27L102.29 32.86Q102.92 33.88 104.12 34.43Q105.32 34.99 106.82 34.99Q108.56 34.99 109.70 34.21Q110.84 33.43 111.42 31.84Q112.01 30.25 112.01 27.82L112.37 24.58L111.86 24.58Q111.50 26.83 110.78 28.15Q110.06 29.47 109.04 30.04Q108.02 30.61 106.67 30.61Q104.99 30.61 103.83 29.77Q102.68 28.93 102.09 27.27Q101.51 25.60 101.51 23.26L101.51 16.45L103.67 16.45L103.67 22.84Q103.67 25.75 104.58 27.16Q105.50 28.57 107.24 28.57Q108.20 28.57 108.98 28.12Q109.76 27.67 110.33 26.72Q110.90 25.78 111.24 24.37Q111.59 22.96 111.68 21.01L111.68 16.45L113.90 16.45L113.90 27.97Q113.90 29.77 113.64 31.22Q113.39 32.68 112.82 33.76Q112.25 34.84 111.42 35.57Q110.60 36.31 109.47 36.67Q108.35 37.03 106.94 37.03"/>
|
||||||
|
<path d="M125.18 31.90L121.37 31.90L121.37 15.31L115.49 15.31L115.49 12.10L131.09 12.10L131.09 15.31L125.18 15.31M139.43 32.29Q137.54 32.29 136.06 31.75Q134.57 31.21 133.55 30.17Q132.53 29.14 131.99 27.67Q131.45 26.20 131.45 24.34Q131.45 22.48 131.97 20.92Q132.50 19.36 133.49 18.22Q134.48 17.08 135.91 16.45Q137.33 15.82 139.16 15.82Q140.87 15.82 142.22 16.39Q143.57 16.96 144.50 18.09Q145.43 19.21 145.88 20.88Q146.33 22.54 146.18 24.73L135.11 24.82Q135.23 27.01 136.28 28.18Q137.42 29.47 139.43 29.47Q140.33 29.47 140.94 29.24Q141.56 29.02 141.98 28.64Q142.40 28.27 142.66 27.79Q142.91 27.31 143.06 26.77L146.36 27.52Q146.12 28.63 145.58 29.50Q145.04 30.37 144.19 31Q143.33 31.63 142.16 31.96Q140.99 32.29 139.43 32.29M135.23 22.63L142.73 22.54Q142.67 21.64 142.40 20.95Q141.95 19.87 141.11 19.34Q140.27 18.82 139.19 18.82Q137.96 18.82 137.03 19.46Q136.10 20.11 135.62 21.31Q135.38 21.91 135.23 22.63M151.91 31.90L148.13 31.90L148.13 16.21L151.31 16.21L151.25 21.61L151.79 21.61Q152.06 19.69 152.63 18.41Q153.20 17.14 154.22 16.49Q155.24 15.85 156.74 15.85Q157.04 15.85 157.38 15.88Q157.73 15.91 158.15 16.03L158 20.02Q157.52 19.84 157.03 19.77Q156.53 19.69 156.11 19.69Q154.94 19.69 154.09 20.29Q153.23 20.89 152.71 21.98Q152.18 23.08 151.91 24.58M166.34 32.29Q164.30 32.29 162.81 31.69Q161.33 31.09 160.35 29.99Q159.38 28.90 158.90 27.43Q158.42 25.96 158.42 24.25Q158.42 22.42 158.91 20.89Q159.41 19.36 160.40 18.22Q161.39 17.08 162.84 16.45Q164.30 15.82 166.19 15.82Q168.08 15.82 169.47 16.45Q170.87 17.08 171.74 18.20Q172.61 19.33 172.91 20.83L169.37 21.94Q169.31 21.04 168.89 20.34Q168.47 19.63 167.75 19.24Q167.03 18.85 166.04 18.85Q165.08 18.85 164.38 19.23Q163.67 19.60 163.20 20.29Q162.74 20.98 162.48 21.95Q162.23 22.93 162.23 24.13Q162.23 25.78 162.68 26.95Q163.13 28.12 164.06 28.77Q164.99 29.41 166.37 29.41Q167.60 29.41 168.37 28.95Q169.13 28.48 169.53 27.73Q169.94 26.98 170.03 26.08L173.39 26.83Q173.21 28.06 172.70 29.05Q172.19 30.04 171.31 30.77Q170.42 31.51 169.19 31.90Q167.96 32.29 166.34 32.29M178.37 31.90L174.59 31.90L174.59 16.21L178.37 16.21L178.37 31.90M176.48 13.75Q175.34 13.75 174.72 13.25Q174.11 12.76 174.11 11.83Q174.11 10.90 174.72 10.41Q175.34 9.91 176.48 9.91Q177.68 9.91 178.28 10.39Q178.88 10.87 178.88 11.83Q178.88 12.76 178.27 13.25Q177.65 13.75 176.48 13.75M184.91 31.90L181.13 31.90L181.13 10.60L184.94 10.60L184.94 15.49Q184.94 16.12 184.90 16.78Q184.85 17.44 184.76 18.13Q184.67 18.82 184.56 19.51Q184.46 20.20 184.34 20.89L184.97 20.89Q185.39 19.27 186.05 18.14Q186.71 17.02 187.75 16.42Q188.78 15.82 190.28 15.82Q193.01 15.82 194.36 17.73Q195.71 19.63 195.71 23.50L195.71 31.90L191.90 31.90L191.90 24.04Q191.90 21.46 191.13 20.23Q190.37 19 188.84 19Q187.58 19 186.74 19.77Q185.90 20.53 185.45 21.79Q185 23.05 184.91 24.64"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.2 KiB |
BIN
public/ses/efekt.wav
Normal file
BIN
public/ses/muzik.wav
Normal file
9
remotion.config.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Config } from "@remotion/cli/config";
|
||||||
|
|
||||||
|
Config.setVideoImageFormat("jpeg");
|
||||||
|
Config.setJpegQuality(95);
|
||||||
|
Config.setCodec("h264");
|
||||||
|
Config.setCrf(21);
|
||||||
|
Config.setX264Preset("slow");
|
||||||
|
Config.setDelayRenderTimeoutInMilliseconds(180000);
|
||||||
|
Config.setConcurrency(4);
|
||||||
13
scripts/kareler.mjs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// Kullanım: node scripts/kareler.mjs <Dikey|Yatay> <ölçek> kare1 kare2 ...
|
||||||
|
import { bundle } from "@remotion/bundler";
|
||||||
|
import { renderStill, selectComposition } from "@remotion/renderer";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const [bicim, olcek, ...kareler] = process.argv.slice(2);
|
||||||
|
const serveUrl = await bundle({ entryPoint: path.resolve("src/index.ts") });
|
||||||
|
const composition = await selectComposition({ serveUrl, id: `Lansman-${bicim}` });
|
||||||
|
for (const f of kareler) {
|
||||||
|
await renderStill({ composition, serveUrl, frame: Number(f), output: `out/kare/${bicim[0].toLowerCase()}-${f}.png`, scale: Number(olcek) });
|
||||||
|
process.stdout.write(`${f} `);
|
||||||
|
}
|
||||||
|
console.log("tamam");
|
||||||
678
ses/uret.py
Normal file
@@ -0,0 +1,678 @@
|
|||||||
|
"""
|
||||||
|
KolayTercih lansman videosunun müziği ve ses efektleri — tamamen sentez
|
||||||
|
(numpy), dışarıdan örnek/sample yok, lisans derdi yok.
|
||||||
|
|
||||||
|
Zaman çizelgesi src/zaman.json'dan okunur; video ile aynı kaynak.
|
||||||
|
120 BPM (1 vuruş = 0.5 sn = 15 kare), Do majör. Video döngüye girer: son kare ilk kareyle aynıdır, ses de öyle — SON'dan
|
||||||
|
taşan kuyruk dosyanın başına eklenir. Çıktılar:
|
||||||
|
public/ses/muzik.wav — müzik (stereo, 44.1 kHz, 16 bit)
|
||||||
|
public/ses/efekt.wav — ses efektleri
|
||||||
|
Çalıştırma: python3 ses/uret.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import wave
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
KOK = Path(__file__).resolve().parent.parent
|
||||||
|
SR = 44100
|
||||||
|
Z = json.loads((KOK / "src/zaman.json").read_text())
|
||||||
|
FPS = Z["fps"]
|
||||||
|
S = {s["id"]: s["bas"] / FPS for s in Z["sahneler"]}
|
||||||
|
SON = Z["son"] / FPS
|
||||||
|
# Döngü: SON'dan sonra taşan kuyruk (reverb, son akor) başa sarılır
|
||||||
|
KUYRUK = 3.5
|
||||||
|
N = int((SON + KUYRUK) * SR)
|
||||||
|
rng = np.random.default_rng(11)
|
||||||
|
B = 0.5 # vuruş (sn)
|
||||||
|
ZAMAN_TIK = Z["tiklar"]
|
||||||
|
|
||||||
|
|
||||||
|
def lf(sahne, kare):
|
||||||
|
"""Sahne içi kare → mutlak saniye."""
|
||||||
|
return S[sahne] + kare / FPS
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Temel araçlar #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
|
||||||
|
def zaman(d):
|
||||||
|
return np.arange(int(d * SR)) / SR
|
||||||
|
|
||||||
|
|
||||||
|
def mf(m):
|
||||||
|
return 440.0 * 2 ** ((m - 69) / 12)
|
||||||
|
|
||||||
|
|
||||||
|
def fftconv(x, h):
|
||||||
|
n = len(x) + len(h) - 1
|
||||||
|
boy = 1 << (n - 1).bit_length()
|
||||||
|
y = np.fft.irfft(np.fft.rfft(x, boy) * np.fft.rfft(h, boy), boy)[:n]
|
||||||
|
return y
|
||||||
|
|
||||||
|
|
||||||
|
def sinc_lp(fc, taps=255):
|
||||||
|
n = np.arange(taps) - (taps - 1) / 2
|
||||||
|
h = np.sinc(2 * fc / SR * n) * np.blackman(taps)
|
||||||
|
return h / h.sum()
|
||||||
|
|
||||||
|
|
||||||
|
def lp(x, fc, taps=255):
|
||||||
|
return fftconv(x, sinc_lp(fc, taps))[(taps - 1) // 2 : (taps - 1) // 2 + len(x)]
|
||||||
|
|
||||||
|
|
||||||
|
def hp(x, fc, taps=255):
|
||||||
|
return x - lp(x, fc, taps)
|
||||||
|
|
||||||
|
|
||||||
|
def bp(x, f1, f2, taps=255):
|
||||||
|
return lp(hp(x, f1, taps), f2, taps)
|
||||||
|
|
||||||
|
|
||||||
|
def gurultu(d):
|
||||||
|
return rng.standard_normal(int(d * SR))
|
||||||
|
|
||||||
|
|
||||||
|
def zarf(d, a=0.005, r=0.05):
|
||||||
|
n = int(d * SR)
|
||||||
|
e = np.ones(n)
|
||||||
|
na = max(1, min(n, int(a * SR)))
|
||||||
|
e[:na] = np.linspace(0, 1, na)
|
||||||
|
nr = max(1, min(n, int(r * SR)))
|
||||||
|
e[n - nr :] *= np.linspace(1, 0, nr)
|
||||||
|
return e
|
||||||
|
|
||||||
|
|
||||||
|
def bos():
|
||||||
|
return np.zeros((N, 2))
|
||||||
|
|
||||||
|
|
||||||
|
def ekle(tampon, sinyal, t0, kazanc=1.0, pan=0.0):
|
||||||
|
i = int(round(t0 * SR))
|
||||||
|
if i >= N or i + len(sinyal) <= 0:
|
||||||
|
return
|
||||||
|
if sinyal.ndim == 1:
|
||||||
|
a = (pan + 1) * np.pi / 4
|
||||||
|
sinyal = np.stack([sinyal * np.cos(a), sinyal * np.sin(a)], 1) * np.sqrt(2)
|
||||||
|
if i < 0:
|
||||||
|
sinyal = sinyal[-i:]
|
||||||
|
i = 0
|
||||||
|
n = min(len(sinyal), N - i)
|
||||||
|
tampon[i : i + n] += sinyal[:n] * kazanc
|
||||||
|
|
||||||
|
|
||||||
|
def reverb(tampon, sure=2.4, tau=0.55, karisim=1.0):
|
||||||
|
t = zaman(sure)
|
||||||
|
cikis = np.zeros_like(tampon)
|
||||||
|
for k in range(2):
|
||||||
|
ir = rng.standard_normal(len(t)) * np.exp(-t / tau)
|
||||||
|
ir[: int(0.012 * SR)] = 0
|
||||||
|
ir = lp(ir, 5500)
|
||||||
|
ir /= np.sqrt((ir**2).sum())
|
||||||
|
cikis[:, k] = fftconv(tampon[:, k], ir)[: len(tampon)]
|
||||||
|
return cikis * karisim
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Enstrümanlar #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
|
||||||
|
def kick(guc=1.0):
|
||||||
|
t = zaman(0.55)
|
||||||
|
f = 44 + 120 * np.exp(-t / 0.032)
|
||||||
|
faz = 2 * np.pi * np.cumsum(f) / SR
|
||||||
|
govde = np.sin(faz) * np.exp(-t / 0.26)
|
||||||
|
tik = hp(gurultu(0.55), 2500) * np.exp(-t / 0.003) * 0.35
|
||||||
|
return np.tanh((govde + tik) * 1.6 * guc) * 0.9
|
||||||
|
|
||||||
|
|
||||||
|
def clap():
|
||||||
|
d = 0.35
|
||||||
|
t = zaman(d)
|
||||||
|
g = bp(gurultu(d), 900, 6000)
|
||||||
|
e = np.zeros_like(t)
|
||||||
|
for o in (0.0, 0.011, 0.022):
|
||||||
|
e += np.where(t >= o, np.exp(-(t - o) / (0.006 if o < 0.02 else 0.13)), 0)
|
||||||
|
return g * e * 0.55
|
||||||
|
|
||||||
|
|
||||||
|
def hat(acik=False):
|
||||||
|
d = 0.35 if acik else 0.08
|
||||||
|
t = zaman(d)
|
||||||
|
return hp(gurultu(d), 7500) * np.exp(-t / (0.11 if acik else 0.022)) * 0.28
|
||||||
|
|
||||||
|
|
||||||
|
def bas(m, d):
|
||||||
|
t = zaman(d)
|
||||||
|
f = mf(m)
|
||||||
|
s = sum(np.sin(2 * np.pi * f * k * t + k) / k ** 1.4 for k in range(1, 7))
|
||||||
|
return s * zarf(d, 0.004, 0.06) * np.exp(-t / 0.9) * 0.55
|
||||||
|
|
||||||
|
|
||||||
|
def ped(notalar, d, parlak=1.0):
|
||||||
|
t = zaman(d)
|
||||||
|
sol = np.zeros(len(t))
|
||||||
|
sag = np.zeros(len(t))
|
||||||
|
for m in notalar:
|
||||||
|
for cent, pan in ((-8, -0.7), (0, 0.0), (8, 0.7)):
|
||||||
|
f = mf(m) * 2 ** (cent / 1200)
|
||||||
|
v = np.zeros(len(t))
|
||||||
|
for k in range(1, 11):
|
||||||
|
if f * k > 9000:
|
||||||
|
break
|
||||||
|
v += np.sin(2 * np.pi * f * k * t + rng.uniform(0, 6.28)) / k ** (1.9 - 0.4 * parlak)
|
||||||
|
a = (pan + 1) * np.pi / 4
|
||||||
|
sol += v * np.cos(a)
|
||||||
|
sag += v * np.sin(a)
|
||||||
|
e = zarf(d, 0.5, 0.9)
|
||||||
|
# yavaş nefes: pedin içinde hafif hareket
|
||||||
|
e *= 0.85 + 0.15 * np.sin(2 * np.pi * 0.25 * t)
|
||||||
|
return np.stack([sol * e, sag * e], 1) * (0.06 / max(1, len(notalar) / 3))
|
||||||
|
|
||||||
|
|
||||||
|
def pluck(m, d=0.5, parlak=1.0):
|
||||||
|
t = zaman(d)
|
||||||
|
f = mf(m)
|
||||||
|
s = (
|
||||||
|
np.sin(2 * np.pi * f * t) * np.exp(-t / 0.32)
|
||||||
|
+ 0.45 * parlak * np.sin(2 * np.pi * 2 * f * t) * np.exp(-t / 0.14)
|
||||||
|
+ 0.18 * parlak * np.sin(2 * np.pi * 3 * f * t) * np.exp(-t / 0.06)
|
||||||
|
)
|
||||||
|
return s * zarf(d, 0.002, 0.03) * 0.3
|
||||||
|
|
||||||
|
|
||||||
|
def piyano(m, d=1.8):
|
||||||
|
t = zaman(d)
|
||||||
|
f = mf(m)
|
||||||
|
s = (
|
||||||
|
np.sin(2 * np.pi * f * t) * np.exp(-t / 0.9)
|
||||||
|
+ 0.5 * np.sin(2 * np.pi * 2.001 * f * t) * np.exp(-t / 0.5)
|
||||||
|
+ 0.22 * np.sin(2 * np.pi * 3.004 * f * t) * np.exp(-t / 0.25)
|
||||||
|
+ 0.1 * np.sin(2 * np.pi * 4.01 * f * t) * np.exp(-t / 0.12)
|
||||||
|
)
|
||||||
|
return s * zarf(d, 0.003, 0.2) * 0.32
|
||||||
|
|
||||||
|
|
||||||
|
def zil(m, d=1.4):
|
||||||
|
t = zaman(d)
|
||||||
|
f = mf(m)
|
||||||
|
s = np.sin(2 * np.pi * f * t) * np.exp(-t / 0.5) + 0.35 * np.sin(2 * np.pi * 2.76 * f * t) * np.exp(-t / 0.25)
|
||||||
|
return s * zarf(d, 0.002, 0.2) * 0.25
|
||||||
|
|
||||||
|
|
||||||
|
def yukselen(d, f0=180, f1=1500):
|
||||||
|
t = zaman(d)
|
||||||
|
p = t / d
|
||||||
|
f = f0 * (f1 / f0) ** p
|
||||||
|
faz = 2 * np.pi * np.cumsum(f) / SR
|
||||||
|
ton = (np.sin(faz) + 0.5 * np.sin(faz * 1.005 + 1)) * 0.25
|
||||||
|
g = gurultu(d)
|
||||||
|
parlak = hp(g, 3000)
|
||||||
|
koyu = lp(g, 900)
|
||||||
|
gur = koyu * (1 - p) + parlak * p
|
||||||
|
return (ton + gur * 0.35) * p ** 2.2 * 0.6
|
||||||
|
|
||||||
|
|
||||||
|
def darbe():
|
||||||
|
"""Logo / final vuruşu: alt bas düşüşü + gövde + parlak patlama."""
|
||||||
|
d = 2.6
|
||||||
|
t = zaman(d)
|
||||||
|
f = 30 + 45 * np.exp(-t / 0.25)
|
||||||
|
faz = 2 * np.pi * np.cumsum(f) / SR
|
||||||
|
alt = np.sin(faz) * np.exp(-t / 1.1)
|
||||||
|
govde = lp(gurultu(d), 1400) * np.exp(-t / 0.18) * 0.8
|
||||||
|
parlak = hp(gurultu(d), 4000) * np.exp(-t / 0.35) * 0.25
|
||||||
|
return np.tanh((alt * 1.2 + govde + parlak) * 1.4) * 0.95
|
||||||
|
|
||||||
|
|
||||||
|
def ters_sisme(notalar, d=1.2):
|
||||||
|
"""Ters çevrilmiş ped + reverb: vuruştan önce çekilen nefes."""
|
||||||
|
p = ped(notalar, d, parlak=1.4)
|
||||||
|
p = p + reverb(p, 1.2, 0.35, 0.8)
|
||||||
|
t = zaman(d)[:, None]
|
||||||
|
return (p * (t / d) ** 2)[::-1] * 3.2
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Efektler #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
|
||||||
|
def whoosh(d=0.7, tur="zoom"):
|
||||||
|
t = zaman(d)
|
||||||
|
g = gurultu(d)
|
||||||
|
koyu = lp(g, 500 if tur == "zoom" else 900)
|
||||||
|
acik = lp(g, 7000)
|
||||||
|
p = t / d
|
||||||
|
tepe = 0.62 if tur != "yuksel" else 0.5
|
||||||
|
iz = np.exp(-((p - tepe) / 0.23) ** 2)
|
||||||
|
karisim = koyu * (1 - iz) + acik * iz
|
||||||
|
e = np.where(p < tepe, (p / tepe) ** 2.2, np.exp(-(p - tepe) / 0.12))
|
||||||
|
s = karisim * e * 0.4
|
||||||
|
if tur == "kaydir":
|
||||||
|
pan = np.linspace(-0.8, 0.8, len(t))
|
||||||
|
else:
|
||||||
|
pan = np.zeros(len(t))
|
||||||
|
a = (pan + 1) * np.pi / 4
|
||||||
|
return np.stack([s * np.cos(a), s * np.sin(a)], 1) * np.sqrt(2)
|
||||||
|
|
||||||
|
|
||||||
|
def pop(f=700, guc=1.0):
|
||||||
|
t = zaman(0.12)
|
||||||
|
fr = f * (1 + 0.9 * np.exp(-t / 0.012))
|
||||||
|
faz = 2 * np.pi * np.cumsum(fr) / SR
|
||||||
|
return np.sin(faz) * np.exp(-t / 0.045) * zarf(0.12, 0.001, 0.02) * 0.4 * guc
|
||||||
|
|
||||||
|
|
||||||
|
def tik(f=2600, guc=1.0):
|
||||||
|
t = zaman(0.03)
|
||||||
|
s = hp(gurultu(0.03), 3000) * np.exp(-t / 0.0025) * 0.5 + np.sin(2 * np.pi * f * t) * np.exp(-t / 0.008) * 0.3
|
||||||
|
return s * guc
|
||||||
|
|
||||||
|
|
||||||
|
def tus():
|
||||||
|
"""Klavye tuşu: kısa, gövdeli tık."""
|
||||||
|
t = zaman(0.05)
|
||||||
|
g = bp(gurultu(0.05), 1200, 6000) * np.exp(-t / 0.006)
|
||||||
|
return (g * 0.5 + np.sin(2 * np.pi * rng.uniform(180, 260) * t) * np.exp(-t / 0.01) * 0.3) * rng.uniform(0.6, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def tiklama():
|
||||||
|
"""Buton basışı."""
|
||||||
|
t = zaman(0.08)
|
||||||
|
return (bp(gurultu(0.08), 800, 5000) * np.exp(-t / 0.008) * 0.6 + np.sin(2 * np.pi * 160 * t) * np.exp(-t / 0.02) * 0.5)
|
||||||
|
|
||||||
|
|
||||||
|
def pirilti(sure, yogun=40, f0=2200, f1=5200, guc=0.12):
|
||||||
|
"""Pikseller dökülürken minik ışıltılar."""
|
||||||
|
s = np.zeros((int((sure + 0.2) * SR), 2))
|
||||||
|
for _ in range(int(yogun * sure)):
|
||||||
|
t0 = rng.uniform(0, sure)
|
||||||
|
b = pop(rng.uniform(f0, f1), guc=rng.uniform(0.3, 1.0))
|
||||||
|
i = int(t0 * SR)
|
||||||
|
a = (rng.uniform(-0.9, 0.9) + 1) * np.pi / 4
|
||||||
|
s[i : i + len(b), 0] += b * np.cos(a) * guc * 1.4
|
||||||
|
s[i : i + len(b), 1] += b * np.sin(a) * guc * 1.4
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def gumbur(d=1.0):
|
||||||
|
t = zaman(d)
|
||||||
|
return lp(gurultu(d), 180) * np.sin(np.pi * t / d) ** 2 * 1.2
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def tok_whoosh(d=0.9, tur="zoom"):
|
||||||
|
"""Geçiş sesi: tok, gövdeli. Alçak "vuuş" + varışta alt bas darbesi."""
|
||||||
|
t = zaman(d)
|
||||||
|
p = t / d
|
||||||
|
g = gurultu(d)
|
||||||
|
koyu = lp(g, 260)
|
||||||
|
orta = lp(g, 1300)
|
||||||
|
tepe = 0.68
|
||||||
|
iz = np.clip((p / tepe), 0, 1) ** 1.5
|
||||||
|
karisim = koyu * (1 - iz) + orta * iz
|
||||||
|
e = np.where(p < tepe, (p / tepe) ** 2.4, np.exp(-(p - tepe) / 0.07))
|
||||||
|
vuus = karisim * e * 0.9
|
||||||
|
# Varış darbesi: 95 → 42 Hz inen sinüs
|
||||||
|
d0 = int(tepe * d * SR)
|
||||||
|
tt = zaman(d)[: len(t) - d0]
|
||||||
|
f = 42 + 55 * np.exp(-tt / 0.06)
|
||||||
|
faz = 2 * np.pi * np.cumsum(f) / SR
|
||||||
|
darbe_ = np.zeros(len(t))
|
||||||
|
darbe_[d0:] = np.sin(faz) * np.exp(-tt / 0.22) * 0.9
|
||||||
|
govde = np.zeros(len(t))
|
||||||
|
govde[d0:] = lp(gurultu(len(tt) / SR), 400)[: len(tt)] * np.exp(-tt / 0.05) * 0.5
|
||||||
|
s = np.tanh((vuus + darbe_ + govde) * 1.3)
|
||||||
|
pan = np.linspace(-0.6, 0.6, len(t)) if tur == "kaydir" else np.zeros(len(t))
|
||||||
|
a = (pan + 1) * np.pi / 4
|
||||||
|
return np.stack([s * np.cos(a), s * np.sin(a)], 1) * np.sqrt(2)
|
||||||
|
|
||||||
|
|
||||||
|
def tok_vurus():
|
||||||
|
"""Sert kesmelerin (bölüm kartları) vuruşu: tok davul + alt bas."""
|
||||||
|
d = 0.9
|
||||||
|
t = zaman(d)
|
||||||
|
f = 40 + 90 * np.exp(-t / 0.045)
|
||||||
|
faz = 2 * np.pi * np.cumsum(f) / SR
|
||||||
|
alt = np.sin(faz) * np.exp(-t / 0.35)
|
||||||
|
tom = np.sin(2 * np.pi * 92 * t) * np.exp(-t / 0.16) * 0.5
|
||||||
|
tok = lp(gurultu(d), 700) * np.exp(-t / 0.035) * 0.8
|
||||||
|
return np.tanh((alt + tom + tok) * 1.5) * 0.95
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Müzik #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
C9 = ([60, 67, 74, 76], 36)
|
||||||
|
G6 = ([59, 67, 71, 74], 43)
|
||||||
|
AM7 = ([57, 64, 67, 72], 45)
|
||||||
|
FM9 = ([53, 60, 64, 67, 69], 41)
|
||||||
|
ILERLEYIS = [C9, G6, AM7, FM9]
|
||||||
|
ILERLEYIS_YZ = [AM7, FM9, C9, G6]
|
||||||
|
|
||||||
|
|
||||||
|
def bam_ani(sahne):
|
||||||
|
"""lensZoom geçişi 30 kare; kesme yarısında (15. kare) olur: BAM anı."""
|
||||||
|
return S[sahne] + 15 / FPS
|
||||||
|
|
||||||
|
|
||||||
|
def bwomp():
|
||||||
|
"""'Off.' için esprili inen iki nota (tuba gibi yumuşak)."""
|
||||||
|
d = 0.7
|
||||||
|
t = zaman(d)
|
||||||
|
f = np.where(t < 0.28, mf(52), mf(47)) * (1 - 0.04 * t)
|
||||||
|
faz = 2 * np.pi * np.cumsum(f) / SR
|
||||||
|
s = (np.sin(faz) + 0.3 * np.sin(2 * faz) + 0.1 * np.sin(3 * faz)) * zarf(d, 0.02, 0.2)
|
||||||
|
s *= np.where(t < 0.28, 1, 0.9) * np.exp(-np.maximum(0, t - 0.28) / 0.35)
|
||||||
|
return lp(s, 1800) * 0.5
|
||||||
|
|
||||||
|
|
||||||
|
def soru_plink():
|
||||||
|
"""'Kafan mı karıştı?' için yükselen soru tınısı."""
|
||||||
|
a = zil(79, 0.5) * 0.8
|
||||||
|
b = zil(84, 0.9)
|
||||||
|
out = np.zeros(len(a) + int(0.16 * SR) + len(b))
|
||||||
|
out[: len(a)] += a
|
||||||
|
out[int(0.16 * SR) : int(0.16 * SR) + len(b)] += b
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def muzik():
|
||||||
|
davul = bos()
|
||||||
|
ahenk = bos()
|
||||||
|
parlak = bos()
|
||||||
|
kickler = []
|
||||||
|
|
||||||
|
def groove(bas_t, bitis_t, ilerleyis, arp=True, arp_parlak=1.0, davul_var=True, hafif=False):
|
||||||
|
bar = 0
|
||||||
|
t = bas_t
|
||||||
|
while t < bitis_t - 0.01:
|
||||||
|
notalar, kok = ilerleyis[bar % 4]
|
||||||
|
d = min(2.0, bitis_t - t)
|
||||||
|
ekle(ahenk, ped(notalar, d + 0.05) * zarf(d + 0.05, 0.4, 0.05)[:, None], t, 1.0)
|
||||||
|
for k in range(8):
|
||||||
|
tt = t + k * B / 2
|
||||||
|
if tt >= bitis_t - 0.02:
|
||||||
|
break
|
||||||
|
ekle(ahenk, bas(kok + (12 if k % 4 == 3 else 0), B / 2 * 0.9), tt, 0.85)
|
||||||
|
if arp:
|
||||||
|
ton = sorted(set(notalar + [n + 12 for n in notalar]))
|
||||||
|
desen = [0, 2, 1, 3, 2, 4, 3, 5, 4, 3, 2, 4, 1, 3, 2, 5]
|
||||||
|
for k in range(16):
|
||||||
|
tt = t + k * B / 4
|
||||||
|
if tt >= bitis_t - 0.02:
|
||||||
|
break
|
||||||
|
ekle(ahenk, pluck(ton[desen[k] % len(ton)] + 12, 0.35, arp_parlak), tt, 0.5 if k % 4 else 0.7, pan=np.sin(k * 0.8) * 0.5)
|
||||||
|
if davul_var:
|
||||||
|
for k in range(4):
|
||||||
|
tt = t + k * B
|
||||||
|
if tt >= bitis_t - 0.02:
|
||||||
|
break
|
||||||
|
ekle(davul, kick(), tt, 0.8 if hafif else 0.95)
|
||||||
|
kickler.append(tt)
|
||||||
|
if k % 2 == 1 and not hafif:
|
||||||
|
ekle(davul, clap(), tt, 0.6)
|
||||||
|
ekle(davul, hat(), tt + B / 2, 0.5, pan=0.25)
|
||||||
|
ekle(davul, hat(), tt + B / 4, 0.18, pan=-0.2)
|
||||||
|
ekle(davul, hat(), tt + 3 * B / 4, 0.18, pan=-0.2)
|
||||||
|
t += 2.0
|
||||||
|
bar += 1
|
||||||
|
|
||||||
|
def saat(bas_t, bitis_t, guc=0.6):
|
||||||
|
t = bas_t
|
||||||
|
while t < bitis_t - 0.01:
|
||||||
|
ekle(davul, tik(3400, 0.25), t, guc, pan=0.4)
|
||||||
|
t += B / 2
|
||||||
|
|
||||||
|
def bam(t0, akor=C9):
|
||||||
|
ekle(davul, darbe(), t0, 1.0)
|
||||||
|
ekle(parlak, zil(84, 2.2), t0, 0.6)
|
||||||
|
ekle(parlak, zil(79, 2.2), t0, 0.5)
|
||||||
|
for j, m in enumerate(akor[0]):
|
||||||
|
ekle(parlak, piyano(m + 12, 1.8), t0 + j * 0.02, 0.4)
|
||||||
|
|
||||||
|
def sessizlik_sonu(t_bam):
|
||||||
|
"""Sessizliğin sonunda BAM'e çekilen ters nefes."""
|
||||||
|
ekle(parlak, ters_sisme(C9[0], 0.7), t_bam - 0.7, 0.9)
|
||||||
|
|
||||||
|
b_logo, b_yz, b_final = bam_ani("logo"), bam_ani("danisman"), bam_ani("final")
|
||||||
|
tik_final = S["final"] + ZAMAN_TIK["final"][0] / FPS
|
||||||
|
|
||||||
|
# --- Giriş: saat + ped + piyano; seçeneklerde kalp atışı
|
||||||
|
saat(0, S["kaos"])
|
||||||
|
ekle(ahenk, ped(AM7[0], S["secenek"] + 0.6), 0, 0.8)
|
||||||
|
ekle(ahenk, ped(FM9[0], S["kaos"] - S["secenek"] + 0.3), S["secenek"], 0.9)
|
||||||
|
for i, m in enumerate([76, 72, 69, 72, 76, 79, 76, 72, 69]):
|
||||||
|
ekle(parlak, piyano(m), i * 0.75, 0.5, pan=0.3 if i % 2 else -0.3)
|
||||||
|
t = S["secenek"]
|
||||||
|
while t < S["kaos"]:
|
||||||
|
ekle(davul, kick(0.5), t, 0.45)
|
||||||
|
ekle(davul, kick(0.35), t + 0.18, 0.25)
|
||||||
|
t += 1.0
|
||||||
|
|
||||||
|
# --- Kaos: gerilim → SERT DURUŞ (nefes sahnesi sessiz)
|
||||||
|
dur1 = S["nefes"]
|
||||||
|
ekle(ahenk, ped(FM9[0], 2.0) , S["kaos"], 1.0)
|
||||||
|
ekle(ahenk, ped([55, 62, 67, 71, 74], dur1 - S["kaos"] - 2.0), S["kaos"] + 2.0, 1.1)
|
||||||
|
ekle(parlak, yukselen(dur1 - S["kaos"] - 0.4), S["kaos"] + 0.4, 0.9)
|
||||||
|
t = S["kaos"] + 1.0
|
||||||
|
while t < dur1 - 1.0:
|
||||||
|
ekle(davul, kick(0.8), t, 0.7)
|
||||||
|
kickler.append(t)
|
||||||
|
t += B
|
||||||
|
adim = B / 2
|
||||||
|
t = dur1 - 1.0
|
||||||
|
while t < dur1 - 0.05:
|
||||||
|
ekle(davul, clap(), t, 0.35 + 0.45 * (t - (dur1 - 1.0)))
|
||||||
|
t += adim
|
||||||
|
adim = max(B / 8, adim * 0.8)
|
||||||
|
# Sessizlik: tek piyano notası, sonra ters nefes → BAM
|
||||||
|
ekle(parlak, piyano(76, 2.4), dur1 + 0.45, 0.35)
|
||||||
|
sessizlik_sonu(b_logo)
|
||||||
|
bam(b_logo)
|
||||||
|
|
||||||
|
# --- Groove: logo (arp'sız) → üç adım (arp'lı), Off'ta SERT DURUŞ
|
||||||
|
dur2 = S["off"]
|
||||||
|
groove(b_logo, S["adim1"], ILERLEYIS, arp=False)
|
||||||
|
groove(S["adim1"], dur2, ILERLEYIS, arp_parlak=1.2)
|
||||||
|
# Off: esprili bwomp + soru tınısı; ters nefes → BAM (DanışmanlaKonuş)
|
||||||
|
sessizlik_sonu(b_yz)
|
||||||
|
bam(b_yz, AM7)
|
||||||
|
|
||||||
|
# --- Yapay zekâ: parlak groove; paket kartlarında vurgular
|
||||||
|
groove(b_yz, S["paket"], ILERLEYIS_YZ, arp_parlak=1.6, hafif=True)
|
||||||
|
groove(S["paket"], S["durust"], ILERLEYIS, arp_parlak=1.4)
|
||||||
|
for k in range(3):
|
||||||
|
ekle(davul, tok_vurus() * 0.6, S["paket"] + k * 1.5, 0.8)
|
||||||
|
|
||||||
|
# --- Dürüstlük: kırılma (piyano + ped) → Hazır'da SERT DURUŞ
|
||||||
|
dur3 = S["hazir"]
|
||||||
|
for i, (notalar, kok) in enumerate([AM7, FM9]):
|
||||||
|
tt = S["durust"] + i * 2.0
|
||||||
|
ekle(ahenk, ped(notalar, 2.0) * zarf(2.0, 0.3, 0.1)[:, None], tt, 1.1)
|
||||||
|
for j, m in enumerate(notalar[1:]):
|
||||||
|
ekle(parlak, piyano(m + 12, 1.8), tt + j * 0.18, 0.5)
|
||||||
|
ekle(ahenk, bas(kok, 1.8), tt, 0.6)
|
||||||
|
# Hazır: iki yumuşak tık, ters nefes → BAM (final)
|
||||||
|
ekle(davul, tik(2400, 0.5), dur3 + 0.5, 0.6)
|
||||||
|
ekle(davul, tik(2600, 0.5), dur3 + 1.0, 0.6)
|
||||||
|
sessizlik_sonu(b_final)
|
||||||
|
bam(b_final)
|
||||||
|
|
||||||
|
# --- Final: groove → butona tık = son akor → saat tıkırtısı döngüye
|
||||||
|
groove(b_final, tik_final, ILERLEYIS, arp_parlak=1.3)
|
||||||
|
ekle(davul, darbe(), tik_final, 0.7)
|
||||||
|
ekle(ahenk, ped(C9[0] + [48], 2.8), tik_final, 1.2)
|
||||||
|
for j, m in enumerate([72, 76, 79, 84, 88]):
|
||||||
|
ekle(parlak, piyano(m, 2.6), tik_final + j * 0.06, 0.45)
|
||||||
|
ekle(parlak, zil(96, 2.5), tik_final, 0.3)
|
||||||
|
saat(SON - 1.0, SON + 0.01, 0.45)
|
||||||
|
|
||||||
|
zarf_sc = np.ones(N)
|
||||||
|
tt = np.arange(N) / SR
|
||||||
|
for k in kickler:
|
||||||
|
i = int(k * SR)
|
||||||
|
n = min(int(0.35 * SR), N - i)
|
||||||
|
if n > 0:
|
||||||
|
zarf_sc[i : i + n] = np.minimum(zarf_sc[i : i + n], 1 - 0.45 * np.exp(-tt[:n] / 0.11))
|
||||||
|
ahenk *= zarf_sc[:, None]
|
||||||
|
|
||||||
|
karisim = davul * 0.9 + ahenk + parlak
|
||||||
|
yanki = reverb(ahenk * 0.35 + parlak * 0.6 + davul * 0.08, 2.6, 0.6)
|
||||||
|
# Sert duruşlar gerçekten sessiz olsun: yankı kuyruğu sessizlik başında hızla kısılır
|
||||||
|
for d in (S["nefes"], S["off"], S["hazir"]):
|
||||||
|
i0 = int(d * SR)
|
||||||
|
n = int(0.35 * SR)
|
||||||
|
z = np.ones(N)
|
||||||
|
z[i0 : i0 + n] = np.linspace(1, 0.08, n)
|
||||||
|
z[i0 + n : int((d + 1.7) * SR)] = 0.08
|
||||||
|
yanki *= z[:, None]
|
||||||
|
return karisim + yanki
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Efekt izi #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
ZAMAN_TIK = Z["tiklar"]
|
||||||
|
ZAMAN_YAZ = Z["yazma"]
|
||||||
|
|
||||||
|
|
||||||
|
def efektler():
|
||||||
|
e = bos()
|
||||||
|
|
||||||
|
# Geçişler: zoomBlur → tok vuuş; lensZoom → emiş + alt bas düşüşü (BAM müzikte)
|
||||||
|
for s in Z["sahneler"][1:]:
|
||||||
|
g = s.get("gecis", {})
|
||||||
|
tur = g.get("tur")
|
||||||
|
if tur == "zoomBlur":
|
||||||
|
ekle(e, tok_whoosh(0.9), S[s["id"]] + 0.3 - 0.9 * 0.68, 0.9)
|
||||||
|
elif tur == "lensZoom":
|
||||||
|
t_bam = bam_ani(s["id"])
|
||||||
|
ekle(e, whoosh(0.5, "zoom")[::-1] * 0.8, t_bam - 0.5, 0.9)
|
||||||
|
ekle(e, tok_vurus(), t_bam, 0.9)
|
||||||
|
|
||||||
|
# İmleç tıkları ve yazma (tek kaynak: zaman.json)
|
||||||
|
for sahne, kareler in ZAMAN_TIK.items():
|
||||||
|
for f in kareler:
|
||||||
|
ekle(e, tiklama(), lf(sahne, f), 0.75)
|
||||||
|
ekle(e, pop(1300, 0.35), lf(sahne, f) + 0.01)
|
||||||
|
for sahne, araliklar in ZAMAN_YAZ.items():
|
||||||
|
for a, b in araliklar:
|
||||||
|
f = a
|
||||||
|
while f < b:
|
||||||
|
ekle(e, tus(), lf(sahne, f), 0.7)
|
||||||
|
f += 4 if sahne == "adim1" else rng.choice([2, 3])
|
||||||
|
|
||||||
|
# Açılış: rozet, iz açılışı
|
||||||
|
ekle(e, pop(900, 0.5), lf("acilis", 8))
|
||||||
|
ekle(e, whoosh(0.6, "yuksel") * 0.4, lf("acilis", 14), 0.6)
|
||||||
|
# Seçenekler: sayaç, sheen
|
||||||
|
for k in range(16):
|
||||||
|
ekle(e, tik(2000 + k * 70, 0.4), lf("secenek", 4 + 36 * (k / 15) ** 1.9))
|
||||||
|
ekle(e, pop(1100, 0.8), lf("secenek", 38))
|
||||||
|
ekle(e, pirilti(0.5, 60, 3000, 6000), lf("secenek", 50), 0.7)
|
||||||
|
# Kaos: kartlar
|
||||||
|
for i in range(8):
|
||||||
|
ekle(e, pop(rng.uniform(420, 880), 0.7), lf("kaos", 6 + i * 5), pan=rng.uniform(-0.7, 0.7))
|
||||||
|
# Off: esprili ses
|
||||||
|
ekle(e, bwomp(), lf("off", 3), 0.9)
|
||||||
|
ekle(e, soru_plink() * 0.8, lf("off", 20), 0.8)
|
||||||
|
# Logo: dev kelime → kesme, rota, sheen
|
||||||
|
ekle(e, whoosh(0.5, "kaydir") * 0.6, lf("logo", 18), 0.8)
|
||||||
|
ekle(e, pop(700, 0.9), lf("logo", 26))
|
||||||
|
ekle(e, pirilti(0.5, 60, 3000, 6000), lf("logo", 46), 0.7)
|
||||||
|
# Adımlar: başlık sheen
|
||||||
|
for a in ("adim1", "adim2", "adim3"):
|
||||||
|
ekle(e, pop(620, 0.8), lf(a, 2))
|
||||||
|
ekle(e, pirilti(0.45, 50, 3000, 6000), lf(a, 4), 0.6)
|
||||||
|
ekle(e, pop(600, 0.8), lf("adim1", 104))
|
||||||
|
for i in range(24):
|
||||||
|
ekle(e, pop(1800 + i * 40, 0.25), lf("adim1", ZAMAN_TIK["adim1"][-1] + 10 + i * 1.1))
|
||||||
|
# Adım 2: harita yağmuru, ısı fırçası, kontur dolgu
|
||||||
|
ekle(e, pirilti(1.4, 90), lf("adim2", 4), 0.9)
|
||||||
|
ekle(e, pirilti(2.3, 32, 1500, 3000, 0.08), lf("adim2", 50), 0.7)
|
||||||
|
ekle(e, whoosh(1.2, "yuksel") * 0.35, lf("adim2", 146), 0.7)
|
||||||
|
# Adım 3: yol çizimi, 24 kare pentatonik
|
||||||
|
ekle(e, pirilti(1.3, 40, 2400, 4800, 0.1), lf("adim3", 6), 0.7)
|
||||||
|
penta = [60, 62, 64, 67, 69]
|
||||||
|
for i in range(24):
|
||||||
|
m = penta[i % 5] + 12 * (i // 5)
|
||||||
|
ekle(e, pop(min(mf(m + 12), 5000), 0.55), lf("adim3", 62 + i * 2), pan=((i % 8) / 7 - 0.5) * 0.8)
|
||||||
|
for f in (112, 117, 122):
|
||||||
|
ekle(e, pop(800, 0.5), lf("adim3", f))
|
||||||
|
# Danışman: piksel patlaması, sheen
|
||||||
|
ekle(e, pirilti(0.8, 90, 1800, 5200, 0.14), lf("danisman", 15), 0.9)
|
||||||
|
ekle(e, pirilti(0.5, 60, 3000, 6000), lf("danisman", 28), 0.7)
|
||||||
|
# Sohbet: araç, cevaplar
|
||||||
|
ts = ZAMAN_TIK["sohbet"]
|
||||||
|
for bas_f, bit_f, cev_f in [(ts[1] + 6, ts[1] + 32, 90), (ts[3] + 6, ts[3] + 22, 224)]:
|
||||||
|
ekle(e, pop(1200, 0.4), lf("sohbet", bas_f))
|
||||||
|
f = bas_f + 6
|
||||||
|
while f < bit_f:
|
||||||
|
ekle(e, tik(4200, 0.15), lf("sohbet", f), 0.6)
|
||||||
|
f += 6
|
||||||
|
ekle(e, zil(88, 0.9), lf("sohbet", cev_f), 0.3)
|
||||||
|
ekle(e, zil(95, 0.9), lf("sohbet", cev_f + 3), 0.22)
|
||||||
|
for f in (100, 107, 114):
|
||||||
|
ekle(e, pop(900, 0.5), lf("sohbet", f))
|
||||||
|
ekle(e, pop(1000, 0.8), lf("sohbet", 242))
|
||||||
|
# Paket: kart sheen'leri, sürükle-bırak
|
||||||
|
for f in (2, 47, 92):
|
||||||
|
ekle(e, pirilti(0.45, 50, 3000, 6000), lf("paket", f), 0.6)
|
||||||
|
tp = ZAMAN_TIK["paket"]
|
||||||
|
ekle(e, whoosh(0.8, "kaydir") * 0.4, lf("paket", tp[0]), 0.6)
|
||||||
|
ekle(e, tok_vurus() * 0.3, lf("paket", tp[1]), 0.8)
|
||||||
|
# Dürüstlük
|
||||||
|
ekle(e, pop(330, 1.0), lf("durust", 0))
|
||||||
|
ekle(e, pirilti(1.2, 36, 1600, 3200, 0.09), lf("durust", 56), 0.7)
|
||||||
|
for k, f in enumerate([94, 99, 104]):
|
||||||
|
ekle(e, pop([784, 659, 523][k], 0.7), lf("durust", f))
|
||||||
|
# Hazır: üç kare
|
||||||
|
for i in range(3):
|
||||||
|
ekle(e, pop([660, 880, 990][i], 0.6), lf("hazir", 4 + i * 6))
|
||||||
|
# Final: buton, çöküş
|
||||||
|
ekle(e, pop(700, 0.9), lf("final", 38))
|
||||||
|
tf = ZAMAN_TIK["final"][0]
|
||||||
|
ekle(e, tok_whoosh(0.9)[::-1] * 0.8, lf("final", tf + 30) - 0.9, 0.8)
|
||||||
|
|
||||||
|
e += reverb(e, 1.4, 0.3, 0.25)
|
||||||
|
return e
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Yazım #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
|
||||||
|
def yaz(yol, x, hedef_tepe=0.89, hedef_rms=None):
|
||||||
|
n = int(SON * SR)
|
||||||
|
# Döngü: SON'dan taşan kuyruğu başa ekle ki son → ilk kare geçişi kesintisiz olsun
|
||||||
|
kuyruk = x[n:]
|
||||||
|
x = x[:n].copy()
|
||||||
|
x[: len(kuyruk)] += kuyruk
|
||||||
|
if hedef_rms:
|
||||||
|
rms = np.sqrt((x**2).mean())
|
||||||
|
x = x * (hedef_rms / rms)
|
||||||
|
x = np.tanh(x * 1.1) / np.tanh(1.1) # yumuşak limitleyici
|
||||||
|
x = x / np.abs(x).max() * hedef_tepe
|
||||||
|
veri = (x * 32767).astype("<i2")
|
||||||
|
yol.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with wave.open(str(yol), "wb") as w:
|
||||||
|
w.setnchannels(2)
|
||||||
|
w.setsampwidth(2)
|
||||||
|
w.setframerate(SR)
|
||||||
|
w.writeframes(veri.tobytes())
|
||||||
|
print(f"{yol.relative_to(KOK)} {len(x) / SR:.1f} sn")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
yaz(KOK / "public/ses/muzik.wav", muzik(), 0.89, hedef_rms=0.16)
|
||||||
|
yaz(KOK / "public/ses/efekt.wav", efektler(), 0.8)
|
||||||
101
src/Lansman.tsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { CameraMotionBlur } from "@remotion/motion-blur";
|
||||||
|
import { TransitionSeries, linearTiming } from "@remotion/transitions";
|
||||||
|
import { Fragment, type ComponentType, type ReactNode } from "react";
|
||||||
|
import { AbsoluteFill, Audio, staticFile, useCurrentFrame } from "remotion";
|
||||||
|
import { lensZoom } from "./remocn/lens-zoom";
|
||||||
|
import { zoomBlur } from "./remocn/zoom-blur";
|
||||||
|
import { Acilis, Kaos, LogoSahnesi, Nefes, Secenek } from "./sahneler-1";
|
||||||
|
import { Adim1, Adim2, Adim3 } from "./sahneler-2";
|
||||||
|
import { Danisman, Durust, Final, Hazir, Off, Paket, Sohbet } from "./sahneler-3";
|
||||||
|
import { Gren, Vinyet } from "./sinema";
|
||||||
|
import { R } from "./tema";
|
||||||
|
import { BULANIK, VURUSLAR } from "./vurus";
|
||||||
|
import ZAMAN from "./zaman.json";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* v3 zaman çizelgesi — `zaman.json` (ses üreticisi de aynı dosyayı okur).
|
||||||
|
* Geçişler remocn.dev'den: zoomBlur (normal), lensZoom (sessizlikten sonraki
|
||||||
|
* BAM anları: lens punch-in + zoom blur + kromatik ayrışma + sarsıntı).
|
||||||
|
* "kesme": vuruşa oturan sert kesme (sessizliğe giriş).
|
||||||
|
*
|
||||||
|
* Döngü: ilk kare ve son kare aynı boş zemin; imleç o anlarda ekranda yok.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Gecis = { tur: string; sure?: number };
|
||||||
|
type SahneTanim = { id: string; bas: number; gecis?: Gecis };
|
||||||
|
|
||||||
|
const BILESENLER: Record<string, ComponentType> = {
|
||||||
|
acilis: Acilis,
|
||||||
|
secenek: Secenek,
|
||||||
|
kaos: Kaos,
|
||||||
|
nefes: Nefes,
|
||||||
|
logo: LogoSahnesi,
|
||||||
|
adim1: Adim1,
|
||||||
|
adim2: Adim2,
|
||||||
|
adim3: Adim3,
|
||||||
|
off: Off,
|
||||||
|
danisman: Danisman,
|
||||||
|
sohbet: Sohbet,
|
||||||
|
paket: Paket,
|
||||||
|
durust: Durust,
|
||||||
|
hazir: Hazir,
|
||||||
|
final: Final,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SURE = ZAMAN.son;
|
||||||
|
|
||||||
|
/** Punch zoomlarda ve hızlı kamera hareketlerinde alt-kare hareket bulanıklığı. */
|
||||||
|
function Bulaniklik({ children, pencereler }: { children: ReactNode; pencereler: [number, number][] }) {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
if (!pencereler.some(([a, b]) => frame >= a && frame <= b)) return <>{children}</>;
|
||||||
|
return (
|
||||||
|
<CameraMotionBlur shutterAngle={200} samples={5}>
|
||||||
|
{children}
|
||||||
|
</CameraMotionBlur>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const gecisSuresi = (g?: Gecis) => (g && g.tur !== "kesme" ? (g.sure ?? 18) : 0);
|
||||||
|
|
||||||
|
export function Lansman() {
|
||||||
|
const sahneler = ZAMAN.sahneler as SahneTanim[];
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ background: R.slate50 }}>
|
||||||
|
<TransitionSeries>
|
||||||
|
{sahneler.map((s, i) => {
|
||||||
|
const sonraki = sahneler[i + 1];
|
||||||
|
const uzunluk = (sonraki ? sonraki.bas : ZAMAN.son) - s.bas + gecisSuresi(sonraki?.gecis);
|
||||||
|
const Bilesen = BILESENLER[s.id];
|
||||||
|
// Lens geçişi zaten kendi zoom blur'unu taşır ve sahneyi çok kez çizer;
|
||||||
|
// üstüne alt-kare bulanıklığı binerse kare başına maliyet katlanır.
|
||||||
|
const lensGiris = s.gecis?.tur === "lensZoom" ? gecisSuresi(s.gecis) + 4 : -1;
|
||||||
|
const lensCikis = sonraki?.gecis?.tur === "lensZoom" ? uzunluk - gecisSuresi(sonraki.gecis) - 4 : Infinity;
|
||||||
|
const pencereler: [number, number][] = [
|
||||||
|
...(VURUSLAR[s.id] ?? []).map((v): [number, number] => [v.f - 1, v.f + 7]),
|
||||||
|
...(BULANIK[s.id] ?? []),
|
||||||
|
].filter(([a, b]) => a > lensGiris && b < lensCikis);
|
||||||
|
const g = i > 0 ? s.gecis : undefined;
|
||||||
|
return (
|
||||||
|
<Fragment key={s.id}>
|
||||||
|
{g && g.tur === "lensZoom" ? (
|
||||||
|
<TransitionSeries.Transition presentation={lensZoom({ shakeAmount: 40, lensSteps: 5, blurSamples: 4 })} timing={linearTiming({ durationInFrames: gecisSuresi(g) })} />
|
||||||
|
) : null}
|
||||||
|
{g && g.tur === "zoomBlur" ? (
|
||||||
|
<TransitionSeries.Transition presentation={zoomBlur({ blur: 18 })} timing={linearTiming({ durationInFrames: gecisSuresi(g) })} />
|
||||||
|
) : null}
|
||||||
|
<TransitionSeries.Sequence durationInFrames={uzunluk} name={s.id}>
|
||||||
|
<Bulaniklik pencereler={pencereler}>
|
||||||
|
<Bilesen />
|
||||||
|
</Bulaniklik>
|
||||||
|
</TransitionSeries.Sequence>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TransitionSeries>
|
||||||
|
<Vinyet />
|
||||||
|
<Gren />
|
||||||
|
<Audio src={staticFile("ses/muzik.wav")} volume={0.72} />
|
||||||
|
<Audio src={staticFile("ses/efekt.wav")} />
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
}
|
||||||
15
src/Root.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Composition } from "remotion";
|
||||||
|
import "./fontlar";
|
||||||
|
import { Lansman, SURE } from "./Lansman";
|
||||||
|
import { FPS } from "./tema";
|
||||||
|
|
||||||
|
export function Root() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Reels / TikTok / Shorts — öğrenci kitlesinin ana kanalı */}
|
||||||
|
<Composition id="Lansman-Dikey" component={Lansman} durationInFrames={SURE} fps={FPS} width={1080} height={1920} />
|
||||||
|
{/* YouTube, site, sunum */}
|
||||||
|
<Composition id="Lansman-Yatay" component={Lansman} durationInFrames={SURE} fps={FPS} width={1920} height={1080} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
162
src/eski/ara.tsx
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { AbsoluteFill, Easing, interpolate, interpolateColors, useCurrentFrame } from "remotion";
|
||||||
|
import { Sahne, useDuzen } from "./ortak";
|
||||||
|
import { Eyebrow } from "./piksel";
|
||||||
|
import { Kamera, MaskeSatir, OdakDisiPikseller } from "./sinema";
|
||||||
|
import { FONT, R, hash, opaklik } from "./tema";
|
||||||
|
import { VURUSLAR } from "./vurus";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bölüm arası (Apple'ın "chapter" kartı): hero dokusundan yapılmış iri
|
||||||
|
* piksel rakam yukarıdan dökülür, imleç üstünden geçip pikselleri ısıtır,
|
||||||
|
* rakama tıklar ve kamera o noktadan bir sonraki sahneye dalar (geçiş
|
||||||
|
* kökeni zaman.json'daki `koken`, rakamın merkezi).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const RAKAMLAR: Record<number, string[]> = {
|
||||||
|
1: ["..#..", ".##..", "#.#..", "..#..", "..#..", "..#..", "#####"],
|
||||||
|
2: [".###.", "#...#", "....#", "...#.", "..#..", ".#...", "#####"],
|
||||||
|
3: ["####.", "....#", "....#", ".###.", "....#", "....#", "####."],
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Rakamın ekran konumu — zaman.json `koken` ile aynı olmalı. */
|
||||||
|
export const RAKAM_MERKEZ = { dikey: [0.5, 0.36], yatay: [0.3, 0.5] } as const;
|
||||||
|
|
||||||
|
export function AraSahne({
|
||||||
|
id,
|
||||||
|
no,
|
||||||
|
rozet,
|
||||||
|
baslik,
|
||||||
|
alt,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
no: number;
|
||||||
|
rozet: string;
|
||||||
|
baslik: ReactNode;
|
||||||
|
alt: string;
|
||||||
|
}) {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey, w, h } = useDuzen();
|
||||||
|
const hucre = dikey ? 66 : 72;
|
||||||
|
const desen = RAKAMLAR[no];
|
||||||
|
const [mx, my] = dikey ? RAKAM_MERKEZ.dikey : RAKAM_MERKEZ.yatay;
|
||||||
|
const cx = mx * w;
|
||||||
|
const cy = my * h;
|
||||||
|
// Izgara: rakamın çevresine 2 hücre serpinti payı
|
||||||
|
const SUT = 9;
|
||||||
|
const SAT = 11;
|
||||||
|
const ox = cx - (SUT * hucre) / 2;
|
||||||
|
const oy = cy - (SAT * hucre) / 2;
|
||||||
|
|
||||||
|
// Isı fırçası: sol üstten sağ alta, rakamın üstünden
|
||||||
|
const FB = 14;
|
||||||
|
const FS = 22;
|
||||||
|
const yol = (p: number): [number, number] => [ox + hucre * (0.8 + p * 7.4), oy + hucre * (1.2 + p * 8.4) + Math.sin(p * Math.PI) * -hucre * 1.4];
|
||||||
|
const fircaNokta = (f: number) => {
|
||||||
|
const p = (f - FB) / FS;
|
||||||
|
if (p < 0 || p > 1) return null;
|
||||||
|
return yol(Easing.inOut(Easing.sin)(p));
|
||||||
|
};
|
||||||
|
const gecmis = Array.from({ length: 40 }, (_, k) => fircaNokta(frame - k));
|
||||||
|
const isaret = yol(Easing.inOut(Easing.sin)(Math.min(1, Math.max(0, (frame - FB) / FS))));
|
||||||
|
// Tık anında rakam nabız atar
|
||||||
|
const tik = frame - 40;
|
||||||
|
const nabiz = tik >= 0 && tik < 12 ? 1 + 0.12 * Math.sin((tik / 12) * Math.PI) : 1;
|
||||||
|
|
||||||
|
const kareler: ReactNode[] = [];
|
||||||
|
for (let y = 0; y < SAT; y++) {
|
||||||
|
for (let x = 0; x < SUT; x++) {
|
||||||
|
const rx = x - 2;
|
||||||
|
const ry = y - 2;
|
||||||
|
const dolu = ry >= 0 && ry < 7 && rx >= 0 && rx < 5 && desen[ry][rx] === "#";
|
||||||
|
// Serpinti: rakamın etrafında seyrek, silik pikseller (hero dokusu)
|
||||||
|
if (!dolu && hash(x, y, no * 7) > 0.22) continue;
|
||||||
|
const t0 = (y / SAT) * 14 + hash(x, y, no) * 6;
|
||||||
|
const ilerle = interpolate(frame, [t0, t0 + 14], [0, 1], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
easing: Easing.bezier(0.23, 1, 0.32, 1),
|
||||||
|
});
|
||||||
|
if (ilerle <= 0) continue;
|
||||||
|
const px = ox + x * hucre + hucre / 2;
|
||||||
|
const py = oy + y * hucre + hucre / 2;
|
||||||
|
let isi = 0;
|
||||||
|
for (let k = 0; k < gecmis.length; k++) {
|
||||||
|
const b = gecmis[k];
|
||||||
|
if (!b) continue;
|
||||||
|
if ((b[0] - px) ** 2 + (b[1] - py) ** 2 > (hucre * 1.5) ** 2) continue;
|
||||||
|
isi = k < 4 ? 1 : Math.pow(1 - Math.min(1, (k - 4) / 34), 2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (tik >= 0 && dolu) isi = Math.max(isi, Math.max(0, 1 - tik / 16));
|
||||||
|
const tabanOp = dolu ? 0.55 + opaklik(x, y) * 1.3 : opaklik(x, y) * 0.8;
|
||||||
|
const op = (tabanOp + (0.95 - tabanOp) * isi) * Math.min(1, ilerle * 2);
|
||||||
|
const dot = hucre * 0.8;
|
||||||
|
const hh = dot * (1 + (1 - ilerle) * 1.1);
|
||||||
|
kareler.push(
|
||||||
|
<rect
|
||||||
|
key={`${x}-${y}`}
|
||||||
|
x={px - dot / 2}
|
||||||
|
y={py - hh / 2 - (1 - ilerle) * hucre * 5}
|
||||||
|
width={dot}
|
||||||
|
height={hh}
|
||||||
|
rx={dot * 0.26}
|
||||||
|
fill={isi > 0 ? interpolateColors(isi, [0, 1], [R.primary, R.turuncu]) : R.primary}
|
||||||
|
fillOpacity={op}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const metin = (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
...(dikey
|
||||||
|
? { left: 70, right: 70, top: cy + (SAT * hucre) / 2 + 10, textAlign: "center" as const, alignItems: "center" }
|
||||||
|
: { left: w * 0.5, right: 120, top: 0, bottom: 0, justifyContent: "center", alignItems: "flex-start" }),
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 30,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MaskeSatir gecikme={4} sure={18}>
|
||||||
|
<Eyebrow olcek={dikey ? 2.6 : 2.3}>{rozet}</Eyebrow>
|
||||||
|
</MaskeSatir>
|
||||||
|
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 118 : 124, lineHeight: 1.02, letterSpacing: "0.01em", color: R.slate900 }}>
|
||||||
|
<MaskeSatir gecikme={8} sure={20}>
|
||||||
|
{baslik}
|
||||||
|
</MaskeSatir>
|
||||||
|
</div>
|
||||||
|
<MaskeSatir gecikme={16} sure={20}>
|
||||||
|
<span style={{ display: "block", fontSize: dikey ? 42 : 40, lineHeight: 1.4, color: R.slate600, maxWidth: dikey ? 920 : 760 }}>{alt}</span>
|
||||||
|
</MaskeSatir>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sahne zemin={R.beyaz}>
|
||||||
|
<OdakDisiPikseller seed={no * 5} adet={7} guc={0.8} />
|
||||||
|
<Kamera anahtarlar={[{ f: 0, s: 1.04 }, { f: 45, s: 1.12 }]} vuruslar={VURUSLAR[id]}>
|
||||||
|
<AbsoluteFill>
|
||||||
|
<svg width={w} height={h} style={{ position: "absolute", inset: 0, overflow: "visible" }}>
|
||||||
|
<g style={{ transformBox: "view-box", transformOrigin: `${cx}px ${cy}px`, transform: `scale(${nabiz})` }}>{kareler}</g>
|
||||||
|
</svg>
|
||||||
|
<div data-iz={`${id}-firca`} style={{ position: "absolute", left: isaret[0], top: isaret[1], width: 2, height: 2 }} />
|
||||||
|
<div data-iz={`${id}-rakam`} style={{ position: "absolute", left: cx + hucre * 0.3, top: cy + hucre * 0.4, width: 2, height: 2 }} />
|
||||||
|
{metin}
|
||||||
|
</AbsoluteFill>
|
||||||
|
</Kamera>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Ara1 = () => (
|
||||||
|
<AraSahne id="ara1" no={1} rozet="Adım 1" baslik="Sıralamanı yaz." alt="Bir de neyi sevdiğini, nerede okumak istediğini. O kadar." />
|
||||||
|
);
|
||||||
|
export const Ara2 = () => (
|
||||||
|
<AraSahne id="ara2" no={2} rozet="Adım 2" baslik="Tabloları biz okuyalım." alt="6 yılın resmî YÖK Atlas verisi taranır. Gözlerin sana lazım." />
|
||||||
|
);
|
||||||
|
export const Ara3 = () => (
|
||||||
|
<AraSahne id="ara3" no={3} rozet="Adım 3" baslik="Listen hazır." alt="5 hayal, 13 dengeli, 6 güvenli. Hayal de var, akıl da." />
|
||||||
|
);
|
||||||
236
src/eski/imlec.tsx
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
import { useLayoutEffect, useRef } from "react";
|
||||||
|
import { Easing, continueRender, delayRender, interpolateColors, useCurrentFrame, useVideoConfig } from "remotion";
|
||||||
|
import { R, hash } from "./tema";
|
||||||
|
import ZAMAN from "./zaman.json";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filmin baş karakteri: tek bir imleç. Sahneler arasında kesintisiz dolaşır
|
||||||
|
* ve hedeflerine **motion tracking** ile kilitlenir: hedef, sahnedeki bir
|
||||||
|
* öğenin `data-iz` adıdır ve konumu her karede DOM'dan ölçülür
|
||||||
|
* (getBoundingClientRect). Böylece kamera yaklaşsa, telefon 3B dönse, kart
|
||||||
|
* uçuşsa da imleç öğenin üstünde kalır.
|
||||||
|
*
|
||||||
|
* Geçtiği yerde sitedeki ısı fırçası gibi turuncu pikseller bırakır (hızla
|
||||||
|
* orantılı, 0.7 sn'de soğur); tıkladığı yerde halka + piksel patlaması.
|
||||||
|
* Konum hesabı ölçüme bağlı olduğu için çizim, React render'ı yerine
|
||||||
|
* useLayoutEffect içinde doğrudan DOM'a yazılır (ekran görüntüsünden önce).
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Hedef = string | [number, number] | { dikey: [number, number]; yatay: [number, number] };
|
||||||
|
type Nokta = { s: string; f: number; h: Hedef; dx?: number; dy?: number; g?: number; tik?: boolean };
|
||||||
|
type Durak = { F: number; h: Hedef; dx: number; dy: number; g: number; tik: boolean };
|
||||||
|
|
||||||
|
const BAS: Record<string, number> = Object.fromEntries(ZAMAN.sahneler.map((s) => [s.id, s.bas]));
|
||||||
|
export const DURAKLAR: Durak[] = (ZAMAN.imlec as Nokta[])
|
||||||
|
.map((n) => ({ F: BAS[n.s] + n.f, h: n.h, dx: n.dx ?? 0, dy: n.dy ?? 0, g: n.g ?? 14, tik: !!n.tik }))
|
||||||
|
.sort((a, b) => a.F - b.F);
|
||||||
|
const TIKLAR = DURAKLAR.filter((d) => d.tik);
|
||||||
|
|
||||||
|
const yolEgri = Easing.bezier(0.33, 0, 0.12, 1); // insan eli: hızlı kalkış, yumuşak varış
|
||||||
|
|
||||||
|
export function IzleyenImlec() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { width: W, height: H } = useVideoConfig();
|
||||||
|
const dikey = H > W;
|
||||||
|
const kokRef = useRef<HTMLDivElement>(null);
|
||||||
|
const svgRef = useRef<SVGSVGElement>(null);
|
||||||
|
const imlecRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||||
|
const BOY = dikey ? 72 : 60;
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
// Remotion kompozisyonu önce ekran dışındaki sıfır genişlikli bir kapta
|
||||||
|
// çizer, sonra görünür tuvale taşır. Ölçüm ancak taşındıktan sonra
|
||||||
|
// doğrudur; o ana kadar kare tutulur (delayRender).
|
||||||
|
if (ciz()) return;
|
||||||
|
const tutamac = delayRender("İmleç: yerleşim bekleniyor");
|
||||||
|
let iptal = false;
|
||||||
|
const dene = () => {
|
||||||
|
if (iptal) return;
|
||||||
|
if (ciz()) continueRender(tutamac);
|
||||||
|
else requestAnimationFrame(dene);
|
||||||
|
};
|
||||||
|
requestAnimationFrame(dene);
|
||||||
|
return () => {
|
||||||
|
iptal = true;
|
||||||
|
continueRender(tutamac);
|
||||||
|
};
|
||||||
|
|
||||||
|
function ciz(): boolean {
|
||||||
|
const kok = kokRef.current?.parentElement;
|
||||||
|
if (!kok) return false;
|
||||||
|
const kr = kok.getBoundingClientRect();
|
||||||
|
if (kr.width < 1) return false;
|
||||||
|
const olcek = kr.width / W;
|
||||||
|
const onbellek = new Map<string, [number, number] | null>();
|
||||||
|
|
||||||
|
const olc = (h: Hedef): [number, number] | null => {
|
||||||
|
if (Array.isArray(h)) return [h[0] * W, h[1] * H];
|
||||||
|
if (typeof h === "object") {
|
||||||
|
const p = dikey ? h.dikey : h.yatay;
|
||||||
|
return [p[0] * W, p[1] * H];
|
||||||
|
}
|
||||||
|
if (onbellek.has(h)) return onbellek.get(h)!;
|
||||||
|
const el = kok.querySelector(`[data-iz="${h}"]`);
|
||||||
|
let sonuc: [number, number] | null = null;
|
||||||
|
if (el) {
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
if (r.width > 0 || r.height > 0) {
|
||||||
|
sonuc = [(r.left + r.width / 2 - kr.left) / olcek, (r.top + r.height / 2 - kr.top) / olcek];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onbellek.set(h, sonuc);
|
||||||
|
return sonuc;
|
||||||
|
};
|
||||||
|
const durakNoktasi = (i: number): [number, number] | null => {
|
||||||
|
const d = DURAKLAR[i];
|
||||||
|
const p = olc(d.h);
|
||||||
|
return p ? [p[0] + d.dx, p[1] + d.dy] : null;
|
||||||
|
};
|
||||||
|
// Hedef ölçülemezse (öğe henüz sahnede yoksa) bir önceki durağa düş
|
||||||
|
const guvenli = (i: number): [number, number] => {
|
||||||
|
for (let k = i; k >= 0; k--) {
|
||||||
|
const p = durakNoktasi(k);
|
||||||
|
if (p) return p;
|
||||||
|
}
|
||||||
|
return [W / 2, H / 2];
|
||||||
|
};
|
||||||
|
|
||||||
|
const konum = (t: number): [number, number] => {
|
||||||
|
let j = DURAKLAR.findIndex((d) => d.F > t);
|
||||||
|
if (j === -1) return guvenli(DURAKLAR.length - 1);
|
||||||
|
if (j === 0) return guvenli(0);
|
||||||
|
const i = j - 1;
|
||||||
|
const a = guvenli(i);
|
||||||
|
const d = DURAKLAR[j];
|
||||||
|
const kalkis = Math.max(DURAKLAR[i].F, d.F - d.g);
|
||||||
|
if (t <= kalkis) return a;
|
||||||
|
const b = durakNoktasi(j) ?? a;
|
||||||
|
const p = yolEgri(Math.min(1, (t - kalkis) / (d.F - kalkis)));
|
||||||
|
// Hafif yay: düz çizgi değil, el hareketi gibi kavis
|
||||||
|
const mx = b[0] - a[0];
|
||||||
|
const my = b[1] - a[1];
|
||||||
|
const uz = Math.hypot(mx, my);
|
||||||
|
const kavis = Math.sin(Math.PI * p) * Math.min(90, uz * 0.12);
|
||||||
|
const nx = uz ? -my / uz : 0;
|
||||||
|
const ny = uz ? mx / uz : 0;
|
||||||
|
return [a[0] + mx * p + nx * kavis, a[1] + my * p + ny * kavis];
|
||||||
|
};
|
||||||
|
|
||||||
|
const [x, y] = konum(frame);
|
||||||
|
// Durağan anlarda nefes gibi minik salınım
|
||||||
|
const sx = Math.sin(frame / 13) * 1.6;
|
||||||
|
const sy = Math.cos(frame / 17) * 1.4;
|
||||||
|
|
||||||
|
// Basış
|
||||||
|
let bas = 1;
|
||||||
|
for (const t of TIKLAR) {
|
||||||
|
const d = frame - t.F;
|
||||||
|
if (d >= -2 && d <= 2) bas = Math.min(bas, 1 - 0.16 * ((d + 2) / 4));
|
||||||
|
else if (d > 2 && d <= 9) bas = Math.min(bas, 0.84 + 0.16 * ((d - 2) / 7));
|
||||||
|
}
|
||||||
|
|
||||||
|
// İmleç + hareket bulanıklığı hayaletleri
|
||||||
|
const [px, py] = konum(frame - 1);
|
||||||
|
const hiz = Math.hypot(x - px, y - py);
|
||||||
|
const tipX = (4.04 / 24) * BOY;
|
||||||
|
const tipY = (4.04 / 24) * BOY;
|
||||||
|
imlecRefs.current.forEach((el, k) => {
|
||||||
|
if (!el) return;
|
||||||
|
if (k === 0) {
|
||||||
|
el.style.transform = `translate(${x + sx - tipX}px, ${y + sy - tipY}px) scale(${bas})`;
|
||||||
|
el.style.opacity = "1";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [gx, gy] = konum(frame - k * 0.33);
|
||||||
|
const op = hiz > 22 ? [0, 0.28, 0.16, 0.08][k] * Math.min(1, (hiz - 22) / 30) : 0;
|
||||||
|
el.style.transform = `translate(${gx + sx - tipX}px, ${gy + sy - tipY}px)`;
|
||||||
|
el.style.opacity = String(op);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Piksel izi (ısı fırçası): hızla orantılı, ızgaraya oturan kareler
|
||||||
|
const G = dikey ? 26 : 24;
|
||||||
|
const hucreler = new Map<string, number>();
|
||||||
|
const IZ = 21;
|
||||||
|
for (let k = 1; k <= IZ; k++) {
|
||||||
|
const [ax, ay] = konum(frame - k);
|
||||||
|
const [bx, by] = konum(frame - k - 1);
|
||||||
|
const v = Math.hypot(ax - bx, ay - by);
|
||||||
|
if (v < 5) continue;
|
||||||
|
const isi = (1 - k / IZ) * Math.min(1, v / 24);
|
||||||
|
// Hızlı harekette ara noktaları da doldur ki iz kopuk olmasın
|
||||||
|
const adim = Math.max(1, Math.ceil(v / G));
|
||||||
|
for (let s = 0; s < adim; s++) {
|
||||||
|
const qx = ax + ((bx - ax) * s) / adim;
|
||||||
|
const qy = ay + ((by - ay) * s) / adim;
|
||||||
|
const anahtar = `${Math.floor(qx / G)},${Math.floor(qy / G)}`;
|
||||||
|
hucreler.set(anahtar, Math.max(hucreler.get(anahtar) ?? 0, isi));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const dot = G * 0.74;
|
||||||
|
let svg = "";
|
||||||
|
hucreler.forEach((isi, anahtar) => {
|
||||||
|
const [cx, cy] = anahtar.split(",").map(Number);
|
||||||
|
const renk = interpolateColors(isi, [0, 1], [R.primary, R.turuncu]);
|
||||||
|
svg += `<rect x="${cx * G + (G - dot) / 2}" y="${cy * G + (G - dot) / 2}" width="${dot}" height="${dot}" rx="${dot * 0.27}" fill="${renk}" fill-opacity="${(0.2 + 0.65 * isi).toFixed(3)}"/>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tık: halka + piksel patlaması
|
||||||
|
for (const t of TIKLAR) {
|
||||||
|
const d = frame - t.F;
|
||||||
|
if (d < 0 || d > 24) continue;
|
||||||
|
const [cx, cy] = konum(t.F);
|
||||||
|
const p = Easing.out(Easing.cubic)(Math.min(1, d / 18));
|
||||||
|
const r = 12 + (dikey ? 78 : 64) * p;
|
||||||
|
svg += `<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="${R.turuncu}" stroke-width="${(5 * (1 - p)).toFixed(2)}" stroke-opacity="${(0.8 * (1 - p)).toFixed(3)}"/>`;
|
||||||
|
const KARE = 12;
|
||||||
|
const Gp = dikey ? 14 : 12;
|
||||||
|
for (let n = 0; n < 10; n++) {
|
||||||
|
const aci = (n / 10) * Math.PI * 2 + hash(t.F, n, 3) * 0.6;
|
||||||
|
const uz = (18 + (dikey ? 96 : 80) * (0.6 + 0.4 * hash(t.F, n, 5))) * p;
|
||||||
|
// Konum ızgaraya oturur: piksel adımlarıyla dağılan kareler
|
||||||
|
const qx = Math.round((cx + Math.cos(aci) * uz) / Gp) * Gp;
|
||||||
|
const qy = Math.round((cy + Math.sin(aci) * uz) / Gp) * Gp;
|
||||||
|
const boy = KARE * (1 - d / 26);
|
||||||
|
const renk = [R.turuncu, R.primary, R.logoSeftali][n % 3];
|
||||||
|
svg += `<rect x="${qx - boy / 2}" y="${qy - boy / 2}" width="${boy}" height="${boy}" rx="${boy * 0.27}" fill="${renk}" fill-opacity="${(1 - d / 24).toFixed(3)}"/>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (svgRef.current) svgRef.current.innerHTML = svg;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}, [frame, W, H, dikey, BOY]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={kokRef} style={{ position: "absolute", inset: 0, pointerEvents: "none", overflow: "hidden" }}>
|
||||||
|
<svg ref={svgRef} width={W} height={H} style={{ position: "absolute", inset: 0 }} />
|
||||||
|
{[0, 1, 2, 3].map((k) => (
|
||||||
|
<div
|
||||||
|
key={k}
|
||||||
|
ref={(el) => {
|
||||||
|
imlecRefs.current[k] = el;
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
width: BOY,
|
||||||
|
height: BOY,
|
||||||
|
transformOrigin: `${(4.04 / 24) * 100}% ${(4.04 / 24) * 100}%`,
|
||||||
|
filter: k === 0 ? "drop-shadow(0 8px 14px rgba(15,23,42,0.28))" : undefined,
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg width={BOY} height={BOY} viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
d="M4.037 4.688a.495.495 0 0 1 .651-.651l16 6.5a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063z"
|
||||||
|
fill={R.slate900}
|
||||||
|
stroke="#fff"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
277
src/eski/sahneler-giris.tsx
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
import { measureText } from "@remotion/layout-utils";
|
||||||
|
import { AbsoluteFill, Easing, interpolate, useCurrentFrame } from "remotion";
|
||||||
|
import { YagmurAlani } from "./harita";
|
||||||
|
import { LogoMark } from "./logo";
|
||||||
|
import { Sahne, useDuzen } from "./ortak";
|
||||||
|
import { Eyebrow, KelimeKelime, Wordmark } from "./piksel";
|
||||||
|
import { Kamera, MaskeSatir, OdakDisiPikseller, type Anahtar } from "./sinema";
|
||||||
|
import { FONT, R, ara, hash, trSayi, zipla } from "./tema";
|
||||||
|
import { VURUSLAR } from "./vurus";
|
||||||
|
|
||||||
|
const vurguStil = {
|
||||||
|
fontFamily: FONT.vurgu,
|
||||||
|
fontStyle: "italic" as const,
|
||||||
|
fontWeight: 800,
|
||||||
|
letterSpacing: "-0.04em",
|
||||||
|
color: R.primary,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Açılış — boş ekran, imleç tıklar, sıralama belirir */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
export function Acilis() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey } = useDuzen();
|
||||||
|
// Tık (10. kare) noktasından büyüyerek belirir: kt-unblur
|
||||||
|
const ac = interpolate(frame, [10, 30], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.bezier(0.16, 1, 0.3, 1) });
|
||||||
|
const sayi = ara(frame, [12, 46], [0, 45320], Easing.bezier(0.16, 1, 0.3, 1));
|
||||||
|
return (
|
||||||
|
<Sahne>
|
||||||
|
<Kamera anahtarlar={[{ f: 0, s: 1.1 }, { f: 90, s: 1 }, { f: 140, s: 0.97 }]} vuruslar={VURUSLAR.acilis}>
|
||||||
|
<AbsoluteFill
|
||||||
|
style={{
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 40,
|
||||||
|
textAlign: "center",
|
||||||
|
opacity: ac,
|
||||||
|
transform: `scale(${0.6 + 0.4 * ac})`,
|
||||||
|
filter: `blur(${(1 - ac) * 12}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Eyebrow olcek={dikey ? 2.8 : 2.4}>YKS sonuçları açıklandı</Eyebrow>
|
||||||
|
<div
|
||||||
|
data-iz="ac-sayi"
|
||||||
|
style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 250 : 230, lineHeight: 1, letterSpacing: "-0.01em", color: R.slate900 }}
|
||||||
|
>
|
||||||
|
{trSayi(sayi)}
|
||||||
|
<span style={{ color: R.primary, opacity: ara(frame, [44, 48], [0, 1]) }}>.</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 92 : 88, lineHeight: 1.1, maxWidth: dikey ? 940 : 1600 }}>
|
||||||
|
<MaskeSatir gecikme={36}>Sıralaman geldi.</MaskeSatir>
|
||||||
|
<MaskeSatir gecikme={52}>
|
||||||
|
Asıl <span data-iz="ac-simdi" style={vurguStil}>bulmaca</span> şimdi başlıyor.
|
||||||
|
</MaskeSatir>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
</Kamera>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* İhtimal — 21.000+ program, 24 hak; imleç pikselleri ısıtarak geçer */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
export function Ihtimal() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey, w, h } = useDuzen();
|
||||||
|
const sayi = ara(frame, [0, 40], [0, 21000], Easing.bezier(0.16, 1, 0.3, 1));
|
||||||
|
const arti = zipla(frame, 38);
|
||||||
|
const firca = {
|
||||||
|
bas: 8,
|
||||||
|
sure: 66,
|
||||||
|
yol: (p: number): [number, number] =>
|
||||||
|
dikey
|
||||||
|
? [w * (0.08 + p * 0.84), h * (0.1 + p * 0.8) + Math.sin(p * Math.PI * 2) * h * 0.08]
|
||||||
|
: [w * (0.03 + p * 0.94), h * (0.16 + Math.sin(p * Math.PI) * 0.7)],
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Sahne>
|
||||||
|
<Kamera anahtarlar={[{ f: 0, s: 1.04 }, { f: 125, s: 1.16 }]} vuruslar={VURUSLAR.ihtimal}>
|
||||||
|
<YagmurAlani
|
||||||
|
baslangic={0}
|
||||||
|
dokulme={46}
|
||||||
|
hucre={dikey ? 44 : 40}
|
||||||
|
bosluk={{ rx: dikey ? 520 : 760, ry: dikey ? 520 : 330 }}
|
||||||
|
firca={firca}
|
||||||
|
iz="ih-firca"
|
||||||
|
/>
|
||||||
|
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center", textAlign: "center" }}>
|
||||||
|
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 230 : 250, lineHeight: 1, color: R.slate900, letterSpacing: "-0.01em" }}>
|
||||||
|
{trSayi(sayi)}
|
||||||
|
<span style={{ display: "inline-block", color: R.primary, transform: `scale(${Math.max(0, arti)})` }}>+</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 20, fontSize: dikey ? 44 : 40, fontWeight: 600, color: R.slate500 }}>lisans ve önlisans programı</div>
|
||||||
|
<div style={{ marginTop: dikey ? 80 : 56, fontFamily: FONT.baslik, fontWeight: 700, fontSize: 80, lineHeight: 1.12, maxWidth: dikey ? 1000 : 1500 }}>
|
||||||
|
<MaskeSatir gecikme={48}>
|
||||||
|
Senin sadece <span data-iz="ih-24" style={{ color: R.primary }}>24</span> hakkın var.
|
||||||
|
</MaskeSatir>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 18, fontSize: dikey ? 40 : 38, color: R.slate500 }}>
|
||||||
|
<MaskeSatir gecikme={62}>Matematik biraz karışık.</MaskeSatir>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
</Kamera>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Kaos — soru kartları uçuşur; imleç kartları tek tek takip eder, */
|
||||||
|
/* sonra hepsi imlecin tıkladığı noktaya emilir */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
const SORULAR = [
|
||||||
|
"Geçen yılın tabanı yeter mi?",
|
||||||
|
"Kontenjan düştü mü?",
|
||||||
|
"Hangi şehir?",
|
||||||
|
"Devlet mi, vakıf mı?",
|
||||||
|
"Danışman 3.000 TL mi?!",
|
||||||
|
"Teyzem “hukuk yaz” dedi",
|
||||||
|
"Ya boşta kalırsam?",
|
||||||
|
"Hangisi kaçıncı sıraya?",
|
||||||
|
];
|
||||||
|
const YER_YATAY = [
|
||||||
|
[-0.62, -0.62], [0.55, -0.7], [-0.78, -0.08], [0.74, -0.18],
|
||||||
|
[-0.6, 0.55], [0.58, 0.52], [-0.12, 0.8], [0.1, -0.86],
|
||||||
|
];
|
||||||
|
const YER_DIKEY = [
|
||||||
|
[-0.38, -0.74], [0.36, -0.6], [-0.42, -0.42], [0.4, -0.3],
|
||||||
|
[-0.36, 0.34], [0.38, 0.48], [-0.3, 0.66], [0.32, 0.8],
|
||||||
|
];
|
||||||
|
// İmlecin üzerinde durduğu kart ve kareler (zaman.json imleç senaryosuyla aynı)
|
||||||
|
const HOVER: Record<number, [number, number]> = { 1: [12, 30], 3: [30, 48], 5: [48, 66], 6: [66, 82] };
|
||||||
|
const KAOS_KAMERA: Anahtar[] = [
|
||||||
|
{ f: 0, s: 0.96, r: -1.2 },
|
||||||
|
{ f: 86, s: 1.1, r: 0.8 },
|
||||||
|
{ f: 112, s: 1.3, r: 0 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Kaos() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey, w, h } = useDuzen();
|
||||||
|
const yerler = dikey ? YER_DIKEY : YER_YATAY;
|
||||||
|
const cek = ara(frame, [89, 112], [0, 1], Easing.in(Easing.cubic));
|
||||||
|
return (
|
||||||
|
<Sahne>
|
||||||
|
<Kamera anahtarlar={KAOS_KAMERA} vuruslar={VURUSLAR.kaos}>
|
||||||
|
<AbsoluteFill style={{ opacity: 0.5 * (1 - cek) }}>
|
||||||
|
{Array.from({ length: 16 }, (_, i) => {
|
||||||
|
const y = ((i * 150 - frame * (6 + (i % 3) * 3)) % (h + 300) + h + 300) % (h + 300) - 150;
|
||||||
|
return (
|
||||||
|
<div key={i} style={{ position: "absolute", left: (i % 2) * (w * 0.5) + 40, top: y, width: w * 0.45, height: 34, display: "flex", gap: 16 }}>
|
||||||
|
{[0.5, 0.2, 0.15, 0.15].map((oran, j) => (
|
||||||
|
<div key={j} style={{ flex: oran, borderRadius: 8, background: R.slate200, opacity: 0.35 + hash(i, j, 3) * 0.5 }} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</AbsoluteFill>
|
||||||
|
|
||||||
|
{SORULAR.map((s, i) => {
|
||||||
|
const [nx, ny] = yerler[i];
|
||||||
|
const g = zipla(frame, 6 + i * 5);
|
||||||
|
const yuzX = Math.sin((frame + i * 20) / 18) * 16;
|
||||||
|
const yuzY = Math.cos((frame + i * 13) / 15) * 14;
|
||||||
|
const x = nx * (w / 2) * (1 - cek) + yuzX * (1 - cek);
|
||||||
|
const y = ny * (h / 2) * (1 - cek) + yuzY * (1 - cek);
|
||||||
|
const donus = (hash(i, 1, 2) - 0.5) * 10 + cek * 90 * (i % 2 ? 1 : -1);
|
||||||
|
const hv = HOVER[i];
|
||||||
|
const ust = hv
|
||||||
|
? interpolate(frame, [hv[0], hv[0] + 4, hv[1] - 2, hv[1] + 2], [0, 1, 1, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" })
|
||||||
|
: 0;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={s}
|
||||||
|
data-iz={`kart-${i}`}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: w / 2 + x,
|
||||||
|
top: h / 2 + y,
|
||||||
|
transform: `translate(-50%, -50%) scale(${Math.max(0, g) * (1 - cek) * (1 + 0.07 * ust)}) rotate(${donus * (1 - ust)}deg)`,
|
||||||
|
background: ust > 0.5 ? "#fff7ed" : R.beyaz,
|
||||||
|
border: `3px solid ${ust > 0.5 ? R.turuncu : R.slate200}`,
|
||||||
|
borderRadius: 30,
|
||||||
|
padding: dikey ? "24px 32px" : "22px 30px",
|
||||||
|
fontSize: dikey ? 38 : 34,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: ust > 0.5 ? R.turuncu : R.slate700,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
boxShadow: `0 ${12 + ust * 16}px ${30 + ust * 20}px rgba(15,23,42,${0.08 + ust * 0.06})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{s}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<h2
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
textAlign: "center",
|
||||||
|
fontFamily: FONT.baslik,
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: 104,
|
||||||
|
lineHeight: 1.08,
|
||||||
|
maxWidth: dikey ? 800 : 1200,
|
||||||
|
background: `radial-gradient(closest-side, ${R.slate50} 60%, rgba(248,250,252,0))`,
|
||||||
|
padding: "40px 60px",
|
||||||
|
opacity: 1 - cek,
|
||||||
|
filter: `blur(${cek * 12}px)`,
|
||||||
|
transform: `scale(${1 - cek * 0.15})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<KelimeKelime metin="Kafanda yüz sekme açık, değil mi?" gecikme={4} aralik={3} vurgu={["yüz", "sekme"]} />
|
||||||
|
</h2>
|
||||||
|
</AbsoluteFill>
|
||||||
|
</Kamera>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Logo — imlecin tıkladığı noktadan kareler iner, rota, slogan */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
export function LogoSahnesi() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey } = useDuzen();
|
||||||
|
const markBoy = dikey ? 300 : 280;
|
||||||
|
const yaziBoy = dikey ? 150 : 170;
|
||||||
|
const olc = (metin: string, agirlik: string) =>
|
||||||
|
measureText({ text: metin, fontFamily: "Bricolage", fontSize: yaziBoy, fontWeight: agirlik, letterSpacing: `${-0.05 * yaziBoy}px` }).width;
|
||||||
|
const yaziGen = olc("Kolay", "300") + olc("Tercih", "600");
|
||||||
|
const ac = ara(frame, [26, 50], [0, 1]);
|
||||||
|
const bosluk = 30;
|
||||||
|
const kaydirX = dikey ? 0 : interpolate(ac, [0, 1], [0, -(yaziGen + bosluk) / 2]);
|
||||||
|
const kaydirY = dikey ? interpolate(ac, [0, 1], [0, -110]) : 0;
|
||||||
|
return (
|
||||||
|
<Sahne zemin={R.beyaz}>
|
||||||
|
<OdakDisiPikseller seed={3} adet={8} guc={ac} />
|
||||||
|
<Kamera anahtarlar={[{ f: 0, s: 1.3 }, { f: 70, s: 1.02 }, { f: 160, s: 0.98 }]} vuruslar={VURUSLAR.logo}>
|
||||||
|
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<div style={{ transform: `translate(${kaydirX}px, ${kaydirY - (dikey ? 80 : 90)}px)`, position: "relative" }}>
|
||||||
|
<LogoMark boy={markBoy} giris={2} rotaBas={26} />
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
...(dikey
|
||||||
|
? { left: "50%", top: markBoy + 10, transform: `translateX(-50%) translateY(${(1 - ac) * 30}px)` }
|
||||||
|
: { left: markBoy * 0.86 + bosluk, top: "50%", transform: "translateY(-50%)" }),
|
||||||
|
clipPath: dikey ? undefined : `inset(-20% ${(1 - ac) * 100}% -20% 0)`,
|
||||||
|
opacity: dikey ? ac : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Wordmark boy={yaziBoy} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: dikey ? 620 : 470,
|
||||||
|
textAlign: "center",
|
||||||
|
fontFamily: FONT.baslik,
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: dikey ? 80 : 76,
|
||||||
|
lineHeight: 1.15,
|
||||||
|
letterSpacing: "0.015em",
|
||||||
|
maxWidth: dikey ? 860 : 1600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<KelimeKelime metin="Tercih listeni gerçek veriyle kur, panikle değil." gecikme={58} aralik={3} vurgu={["gerçek", "veriyle"]} />
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
</Kamera>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
742
src/eski/sahneler-urun.tsx
Normal file
@@ -0,0 +1,742 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { AbsoluteFill, Easing, useCurrentFrame } from "remotion";
|
||||||
|
import { YagmurHaritasi } from "./harita";
|
||||||
|
import { Ikon, type IkonAdi } from "./ikonlar";
|
||||||
|
import { LogoMark } from "./logo";
|
||||||
|
import { BaslikBlogu, Kutu, RiskEtiketi, Sahne, TuruncuCta, useDuzen } from "./ortak";
|
||||||
|
import { Eyebrow, PixelCorner, Wordmark } from "./piksel";
|
||||||
|
import { Kamera, MaskeSatir, OdakDisiPikseller, degerAl, type Anahtar } from "./sinema";
|
||||||
|
import { Telefon } from "./telefon";
|
||||||
|
import { FONT, R, ara, trSayi, yay, zipla } from "./tema";
|
||||||
|
import { VURUSLAR } from "./vurus";
|
||||||
|
|
||||||
|
const vurguStil = {
|
||||||
|
fontFamily: FONT.vurgu,
|
||||||
|
fontStyle: "italic" as const,
|
||||||
|
fontWeight: 800,
|
||||||
|
letterSpacing: "-0.04em",
|
||||||
|
color: R.primary,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Adım 1 — 3B telefonda sihirbaz: sıralama, çipler, CTA */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
const CIPLER = ["Mühendislik", "Bilgisayar", "İstanbul", "Ankara", "İzmir", "Devlet"];
|
||||||
|
// Seçim kareleri imleç tıklarıyla aynı (zaman.json)
|
||||||
|
const SECIM: Record<number, number> = { 0: 58, 1: 66, 2: 74, 3: 82, 5: 92 };
|
||||||
|
|
||||||
|
function SihirbazEkrani() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const metin = "45.320";
|
||||||
|
const yazilan = metin.slice(0, Math.max(0, Math.floor((frame - 22) / 4)));
|
||||||
|
const odak = frame >= 20;
|
||||||
|
const imlecGorunur = Math.floor(frame / 8) % 2 === 0 || yazilan.length < metin.length;
|
||||||
|
const cta = zipla(frame, 96);
|
||||||
|
const bas = ara(frame, [122, 125], [0, 1]) * (1 - ara(frame, [129, 134], [0, 1]));
|
||||||
|
const sayfa = yay(frame, 128, 22);
|
||||||
|
return (
|
||||||
|
<div style={{ padding: "0 20px", fontFamily: FONT.govde, color: R.slate900 }}>
|
||||||
|
{/* Site üst çubuğu */}
|
||||||
|
<div style={{ height: 56, display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
||||||
|
<span style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
|
||||||
|
<LogoMark boy={28} varyant="duruk" girisYok />
|
||||||
|
<Wordmark boy={21} />
|
||||||
|
</span>
|
||||||
|
<span style={{ width: 40, height: 40, borderRadius: 999, border: `1px solid ${R.slate200}`, display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<Ikon ad="kullanici" boy={16} renk={R.slate600} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 18 }}>
|
||||||
|
<Eyebrow olcek={1}>Adım 1 / 3</Eyebrow>
|
||||||
|
<div style={{ marginTop: 14, fontFamily: FONT.baslik, fontWeight: 700, fontSize: 30, lineHeight: 1.1 }}>Sıralamanı gir</div>
|
||||||
|
<div style={{ marginTop: 8, fontSize: 16, lineHeight: 1.5, color: R.slate600 }}>Sana uyan programları bununla bulacağız.</div>
|
||||||
|
<div style={{ marginTop: 22, fontSize: 14, fontWeight: 600, color: R.slate700 }}>Başarı sıralaman</div>
|
||||||
|
<div
|
||||||
|
data-iz="a1-girdi"
|
||||||
|
style={{
|
||||||
|
marginTop: 8,
|
||||||
|
height: 48,
|
||||||
|
borderRadius: 999,
|
||||||
|
border: `1px solid ${odak ? R.primary : R.slate200}`,
|
||||||
|
boxShadow: odak ? "0 0 0 3px rgba(59,130,246,0.25)" : undefined,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 12,
|
||||||
|
padding: "0 20px",
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ikon ad="kupa" boy={16} renk={R.slate400} />
|
||||||
|
<span style={{ fontWeight: 600 }}>
|
||||||
|
{yazilan}
|
||||||
|
{odak && imlecGorunur ? <span style={{ color: R.primary, fontWeight: 400 }}>|</span> : null}
|
||||||
|
{!yazilan && !odak ? <span style={{ color: R.slate400, fontWeight: 400 }}>Örn. 45.320</span> : null}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 20, fontSize: 14, fontWeight: 600, color: R.slate700 }}>Neyi seviyorsun, nerede okumak istersin?</div>
|
||||||
|
<div style={{ marginTop: 10, display: "flex", flexWrap: "wrap", gap: 8 }}>
|
||||||
|
{CIPLER.map((c, i) => {
|
||||||
|
const an = SECIM[i];
|
||||||
|
const secili = an != null && frame >= an;
|
||||||
|
const tik = an != null ? ara(frame, [an, an + 2], [0, 1]) * (1 - ara(frame, [an + 3, an + 7], [0, 1])) : 0;
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={c}
|
||||||
|
data-iz={`a1-cip-${i}`}
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
height: 40,
|
||||||
|
padding: "0 14px",
|
||||||
|
borderRadius: 999,
|
||||||
|
border: `1px solid ${secili ? R.primary : R.slate200}`,
|
||||||
|
background: secili ? R.primary : R.beyaz,
|
||||||
|
color: secili ? R.beyaz : R.slate700,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 600,
|
||||||
|
transform: `scale(${1 - 0.04 * tik})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i >= 2 && i <= 4 ? <Ikon ad="konum" boy={14} /> : i === 5 ? <Ikon ad="devlet" boy={14} /> : null}
|
||||||
|
{c}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 26, opacity: Math.min(1, Math.max(0, cta)), transform: `scale(${0.85 + 0.15 * cta})` }}>
|
||||||
|
<span data-iz="a1-cta" style={{ display: "block" }}>
|
||||||
|
<TuruncuCta metin="Listemi oluştur" boy={16 / 40} basili={bas} style={{ width: "100%", height: 48 }} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* CTA'dan sonra: liste hazırlanıyor paneli (3. adıma köprü) */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 12,
|
||||||
|
right: 12,
|
||||||
|
bottom: 16,
|
||||||
|
padding: 18,
|
||||||
|
borderRadius: 24,
|
||||||
|
background: R.beyaz,
|
||||||
|
border: `1px solid ${R.slate200}`,
|
||||||
|
boxShadow: "0 -10px 30px rgba(15,23,42,0.10)",
|
||||||
|
transform: `translateY(${(1 - sayfa) * 260}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: 20 }}>Listen hazırlanıyor</div>
|
||||||
|
<div style={{ marginTop: 4, fontSize: 14, color: R.slate500 }}>6 yılın verisi taranıyor…</div>
|
||||||
|
<div style={{ marginTop: 12, display: "grid", gridTemplateColumns: "repeat(12, 1fr)", gap: 5 }}>
|
||||||
|
{Array.from({ length: 24 }, (_, i) => {
|
||||||
|
const on = frame >= 134 + i * 1.2;
|
||||||
|
const renk = i < 5 ? R.riskli : i < 18 ? R.azRiskli : R.guvenli;
|
||||||
|
return <span key={i} style={{ aspectRatio: "1", borderRadius: 5, background: on ? renk : R.slate100 }} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Adim1() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey, w, h } = useDuzen();
|
||||||
|
const telGen = dikey ? 640 : 470;
|
||||||
|
const telDon = [
|
||||||
|
{ f: 0, s: -28, x: 12, r: -3 },
|
||||||
|
{ f: 34, s: -10, x: 5, r: -1 },
|
||||||
|
{ f: 70, s: 0, x: 0, r: 0 },
|
||||||
|
{ f: 185, s: 9, x: 4, r: 1 },
|
||||||
|
].map((a) => ({ f: a.f, s: a.s, x: a.x, r: a.r }));
|
||||||
|
const donY = degerAl(telDon as Anahtar[], frame, "s", 0);
|
||||||
|
const donX = degerAl(telDon as Anahtar[], frame, "x", 0);
|
||||||
|
const donZ = degerAl(telDon as Anahtar[], frame, "r", 0);
|
||||||
|
const kamera: Anahtar[] = dikey
|
||||||
|
? [
|
||||||
|
{ f: 0, s: 0.95, x: 0, y: 60 },
|
||||||
|
{ f: 24, s: 1.4, x: 0, y: -40 },
|
||||||
|
{ f: 50, s: 1.4, x: 0, y: -40 },
|
||||||
|
{ f: 64, s: 1.3, x: 0, y: 140 },
|
||||||
|
{ f: 100, s: 1.3, x: 0, y: 150 },
|
||||||
|
{ f: 118, s: 1.42, x: 0, y: 290 },
|
||||||
|
{ f: 140, s: 1.3, x: 0, y: 420 },
|
||||||
|
{ f: 185, s: 1, x: 0, y: 60 },
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{ f: 0, s: 1, x: 0, y: 0 },
|
||||||
|
{ f: 24, s: 1.6, x: 384, y: -140 },
|
||||||
|
{ f: 50, s: 1.6, x: 384, y: -140 },
|
||||||
|
{ f: 64, s: 1.55, x: 384, y: -10 },
|
||||||
|
{ f: 100, s: 1.55, x: 384, y: 0 },
|
||||||
|
{ f: 118, s: 1.7, x: 384, y: 90 },
|
||||||
|
{ f: 140, s: 1.5, x: 384, y: 250 },
|
||||||
|
{ f: 185, s: 1, x: 0, y: 0 },
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<Sahne>
|
||||||
|
<OdakDisiPikseller seed={11} adet={8} />
|
||||||
|
<Kamera anahtarlar={kamera} vuruslar={VURUSLAR.adim1}>
|
||||||
|
<AbsoluteFill>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
...(dikey
|
||||||
|
? { left: 70, right: 70, top: 130, textAlign: "center" as const, alignItems: "center" }
|
||||||
|
: { left: 140, width: 760, top: 0, bottom: 0, justifyContent: "center", alignItems: "flex-start" }),
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 28,
|
||||||
|
// Kamera telefona dalınca kenarda yarım yazı kalmasın
|
||||||
|
opacity: dikey ? 1 : 1 - ara(frame, [14, 26], [0, 1]) + ara(frame, [150, 172], [0, 1]),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!dikey ? (
|
||||||
|
<MaskeSatir gecikme={4}>
|
||||||
|
<Eyebrow olcek={2.3}>Adım 1</Eyebrow>
|
||||||
|
</MaskeSatir>
|
||||||
|
) : null}
|
||||||
|
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 84 : 96, lineHeight: 1.05 }}>
|
||||||
|
<MaskeSatir gecikme={6}>Form doldurmaktan</MaskeSatir>
|
||||||
|
<MaskeSatir gecikme={12}>
|
||||||
|
<span style={vurguStil}>kısa</span> sürer.
|
||||||
|
</MaskeSatir>
|
||||||
|
</div>
|
||||||
|
{!dikey ? (
|
||||||
|
<MaskeSatir gecikme={20}>
|
||||||
|
<span style={{ display: "block", fontSize: 36, lineHeight: 1.45, color: R.slate600 }}>
|
||||||
|
Sıralamanı yaz, sevdiğin alanları ve şehirleri seç. Gerisini bir butona bırak.
|
||||||
|
</span>
|
||||||
|
</MaskeSatir>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: (dikey ? w / 2 : w * 0.7) - telGen / 2,
|
||||||
|
top: (dikey ? h * 0.585 : h / 2) - (telGen * 2.05) / 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Telefon genislik={telGen} donX={donX} donY={donY} donZ={donZ}>
|
||||||
|
<SihirbazEkrani />
|
||||||
|
</Telefon>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
</Kamera>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Adım 2 — hero haritası dökülür, imleç ısı fırçasıyla tarar */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
const ISTATISTIK = [
|
||||||
|
{ deger: "21.000+", etiket: "Taranan program" },
|
||||||
|
{ deger: "6 yıl", etiket: "Taban sıralaması trendi" },
|
||||||
|
{ deger: "%100", etiket: "Resmî YÖK Atlas kaynağı" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Adim2() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey } = useDuzen();
|
||||||
|
const kilit = yay(frame, 150, 20);
|
||||||
|
const haritaY = dikey ? -70 : -40;
|
||||||
|
const kamera: Anahtar[] = [
|
||||||
|
{ f: 0, s: 1.42, x: 0, y: haritaY },
|
||||||
|
{ f: 58, s: 1.3, x: 0, y: haritaY },
|
||||||
|
{ f: 118, s: 1.22, x: 0, y: haritaY },
|
||||||
|
{ f: 138, s: 1, x: 0, y: 0 },
|
||||||
|
{ f: 200, s: 1.04, x: 0, y: 0 },
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<Sahne zemin={R.beyaz}>
|
||||||
|
<OdakDisiPikseller seed={21} adet={6} guc={0.7} />
|
||||||
|
<Kamera anahtarlar={kamera} vuruslar={VURUSLAR.adim2}>
|
||||||
|
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center", gap: dikey ? 64 : 30 }}>
|
||||||
|
<BaslikBlogu
|
||||||
|
rozet="Adım 2"
|
||||||
|
baslik="6 yılın verisi, tek taramada"
|
||||||
|
aciklama={dikey ? "Taban sıralaması, kontenjan, doluluk. Hepsi resmî YÖK Atlas'tan." : undefined}
|
||||||
|
genis
|
||||||
|
/>
|
||||||
|
<YagmurHaritasi genislik={dikey ? 1000 : 860} baslangic={4} dokulme={42} fircaBas={56} fircaSure={64} iz="a2-firca" />
|
||||||
|
<div style={{ display: "flex", gap: dikey ? 18 : 28 }}>
|
||||||
|
{ISTATISTIK.map((s, i) => {
|
||||||
|
const g = yay(frame, 124 + i * 8, 20);
|
||||||
|
return (
|
||||||
|
<Kutu
|
||||||
|
key={s.etiket}
|
||||||
|
style={{ width: dikey ? 300 : 380, padding: dikey ? "30px 18px" : "22px 24px", textAlign: "center", opacity: g, transform: `translateY(${(1 - g) * 40}px)` }}
|
||||||
|
>
|
||||||
|
<div data-iz={`a2-stat-${i}`} style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 74 : 64, lineHeight: 1, color: R.primary }}>
|
||||||
|
{s.deger}
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 14, fontSize: 26, fontWeight: 600, color: R.slate500, lineHeight: 1.3 }}>{s.etiket}</div>
|
||||||
|
</Kutu>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 22, opacity: kilit, transform: `translateY(${(1 - kilit) * 20}px)`, maxWidth: dikey ? 920 : 1400 }}>
|
||||||
|
<div style={{ width: 84, height: 84, borderRadius: 24, background: R.primary, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
|
||||||
|
<Ikon ad="kalkan" boy={44} renk={R.beyaz} />
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 36, fontWeight: 600, color: R.slate800, lineHeight: 1.3 }}>
|
||||||
|
Yapay zekâ bu havuzun dışına çıkamaz. <span style={{ color: R.primary }}>Uydurmak yok.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
</Kamera>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Adım 3 — 24 kare dolar, imleç her yeni kareyi takip eder */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
const DILIMLER = [
|
||||||
|
{ ad: "Hayal", adet: 5, renk: R.riskli },
|
||||||
|
{ ad: "Dengeli", adet: 13, renk: R.azRiskli },
|
||||||
|
{ ad: "Güvenli", adet: 6, renk: R.guvenli },
|
||||||
|
] as const;
|
||||||
|
const KARELER = DILIMLER.flatMap((d) => Array.from({ length: d.adet }, () => d));
|
||||||
|
|
||||||
|
// Gerçek YÖK Atlas 2026 taban sıralamaları (data/yokatlas.db, SAYISAL);
|
||||||
|
// örnek aday 45.320. — risk riskHesapla() eşikleriyle birebir.
|
||||||
|
const SATIRLAR = [
|
||||||
|
{ sira: 3, ad: "Elektrik-Elektronik Mühendisliği", uni: "Dokuz Eylül Üniversitesi", il: "İzmir", taban: 38626, risk: "riskli" as const },
|
||||||
|
{ sira: 11, ad: "Bilgisayar Mühendisliği", uni: "Ankara Yıldırım Beyazıt Üniversitesi", il: "Ankara", taban: 55158, risk: "az-riskli" as const },
|
||||||
|
{ sira: 20, ad: "Bilgisayar Mühendisliği", uni: "Kocaeli Üniversitesi", il: "Kocaeli", taban: 70832, risk: "guvenli" as const },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Adim3() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey } = useDuzen();
|
||||||
|
const kare = dikey ? 112 : 92;
|
||||||
|
const arali = dikey ? 16 : 12;
|
||||||
|
const sut = dikey ? 6 : 8;
|
||||||
|
const BAS = 24;
|
||||||
|
// İmlecin takip ettiği nokta: dolmakta olan kare
|
||||||
|
const t = Math.min(23, Math.max(0, (frame - BAS) / 2));
|
||||||
|
const kareMerkez = (i: number) => [(i % sut) * (kare + arali) + kare / 2, Math.floor(i / sut) * (kare + arali) + kare / 2];
|
||||||
|
const a = kareMerkez(Math.floor(t));
|
||||||
|
const b = kareMerkez(Math.min(23, Math.floor(t) + 1));
|
||||||
|
const f = t - Math.floor(t);
|
||||||
|
const takip = [a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f];
|
||||||
|
const kamera: Anahtar[] = dikey
|
||||||
|
? [
|
||||||
|
{ f: 0, s: 1.16, x: 0, y: -190 },
|
||||||
|
{ f: 80, s: 1.1, x: 0, y: -160 },
|
||||||
|
{ f: 112, s: 1.15, x: 0, y: 500 },
|
||||||
|
{ f: 150, s: 1.1, x: 0, y: 520 },
|
||||||
|
{ f: 185, s: 1, x: 0, y: 0 },
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{ f: 0, s: 1.45, x: -435, y: 90 },
|
||||||
|
{ f: 80, s: 1.3, x: -420, y: 110 },
|
||||||
|
{ f: 112, s: 1.45, x: 455, y: 120 },
|
||||||
|
{ f: 150, s: 1.35, x: 455, y: 130 },
|
||||||
|
{ f: 185, s: 1, x: 0, y: 0 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const izgara = (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 36 }}>
|
||||||
|
<div style={{ position: "relative", display: "grid", gridTemplateColumns: `repeat(${sut}, ${kare}px)`, gap: arali }}>
|
||||||
|
{KARELER.map((d, i) => {
|
||||||
|
const s = zipla(frame, BAS + i * 2);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
width: kare,
|
||||||
|
height: kare,
|
||||||
|
borderRadius: kare * 0.26,
|
||||||
|
background: d.renk,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontFamily: FONT.baslik,
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: kare * 0.38,
|
||||||
|
color: R.beyaz,
|
||||||
|
transform: `scale(${Math.max(0, s)})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i + 1}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div data-iz="a3-takip" style={{ position: "absolute", left: takip[0], top: takip[1], width: 2, height: 2 }} />
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", gap: 22 }}>
|
||||||
|
{DILIMLER.map((d, i) => {
|
||||||
|
const g = yay(frame, 78 + i * 6, 16);
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={d.ad}
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 14,
|
||||||
|
height: 76,
|
||||||
|
padding: "0 28px",
|
||||||
|
borderRadius: 999,
|
||||||
|
border: `3px solid ${R.slate200}`,
|
||||||
|
background: R.beyaz,
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: R.slate800,
|
||||||
|
opacity: g,
|
||||||
|
transform: `translateY(${(1 - g) * 16}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ width: 20, height: 20, borderRadius: 6, background: d.renk }} />
|
||||||
|
{d.ad}
|
||||||
|
<span style={{ fontFamily: FONT.baslik, fontWeight: 700, color: R.slate900 }}>{d.adet}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const satirlar = (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 18, width: dikey ? 940 : 780 }}>
|
||||||
|
{SATIRLAR.map((s, i) => {
|
||||||
|
const g = yay(frame, 96 + i * 9, 20);
|
||||||
|
const hover = (i === 1 && frame >= 100 && frame < 132) || (i === 2 && frame >= 130 && frame < 158);
|
||||||
|
return (
|
||||||
|
<Kutu
|
||||||
|
key={s.uni}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 26,
|
||||||
|
padding: "26px 30px",
|
||||||
|
opacity: g,
|
||||||
|
transform: `translateX(${(1 - g) * 60}px) scale(${hover ? 1.02 : 1})`,
|
||||||
|
borderColor: hover ? R.turuncu : R.slate200,
|
||||||
|
background: hover ? "#fff7ed" : R.beyaz,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ width: 70, fontFamily: FONT.baslik, fontWeight: 700, fontSize: 44, color: hover ? R.turuncu : R.slate400, textAlign: "center", flexShrink: 0 }}>
|
||||||
|
{s.sira}
|
||||||
|
</div>
|
||||||
|
<div data-iz={`a3-satir-${i}`} style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<div style={{ fontSize: 34, fontWeight: 600, lineHeight: 1.2, color: R.slate900 }}>{s.ad}</div>
|
||||||
|
<div style={{ marginTop: 6, fontSize: 26, color: R.slate500, lineHeight: 1.3 }}>
|
||||||
|
{s.uni} · {s.il}
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 12, display: "flex", alignItems: "center", gap: 20 }}>
|
||||||
|
<RiskEtiketi risk={s.risk} olcek={2.3} />
|
||||||
|
<span style={{ fontSize: 26, color: R.slate500 }}>
|
||||||
|
Taban <b style={{ color: R.slate900, fontWeight: 600 }}>{trSayi(s.taban)}</b>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Kutu>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div style={{ fontSize: 22, color: R.slate400, textAlign: dikey ? "center" : "left", opacity: yay(frame, 128, 16) }}>
|
||||||
|
Örnek: sayısal, 45.320. sıra · YÖK Atlas 2026 taban sıralamaları
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sahne>
|
||||||
|
<OdakDisiPikseller seed={31} adet={6} guc={0.7} />
|
||||||
|
<Kamera anahtarlar={kamera} vuruslar={VURUSLAR.adim3}>
|
||||||
|
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center", gap: dikey ? 60 : 56, padding: dikey ? "0 60px" : "0 100px" }}>
|
||||||
|
<BaslikBlogu rozet="Adım 3" baslik="Dengeli 24'lük listen hazır" genis />
|
||||||
|
<div style={{ display: "flex", flexDirection: dikey ? "column" : "row", alignItems: "center", gap: dikey ? 56 : 90 }}>
|
||||||
|
{izgara}
|
||||||
|
{satirlar}
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
</Kamera>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Paket içinde — trend, PDF, revizyon (satır imleçle sürüklenir) */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
// Ankara Yıldırım Beyazıt Ü. Bilgisayar Müh. taban sırası, 2021–2026
|
||||||
|
const TREND = [37831, 27534, 21602, 30797, 42108, 55158];
|
||||||
|
const REVIZYON = [
|
||||||
|
{ ad: "Bilgisayar Müh. · Ege", renk: R.riskli },
|
||||||
|
{ ad: "Bilgisayar Müh. · Dokuz Eylül", renk: R.azRiskli },
|
||||||
|
{ ad: "Bilgisayar Müh. · Katip Çelebi", renk: R.guvenli },
|
||||||
|
];
|
||||||
|
|
||||||
|
function OzellikKarti({
|
||||||
|
ikon,
|
||||||
|
baslik,
|
||||||
|
aciklama,
|
||||||
|
gecikme,
|
||||||
|
children,
|
||||||
|
dikey,
|
||||||
|
}: {
|
||||||
|
ikon: IkonAdi;
|
||||||
|
baslik: string;
|
||||||
|
aciklama: string;
|
||||||
|
gecikme: number;
|
||||||
|
children: ReactNode;
|
||||||
|
dikey: boolean;
|
||||||
|
}) {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const g = yay(frame, gecikme, 22);
|
||||||
|
return (
|
||||||
|
<Kutu
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
overflow: "visible",
|
||||||
|
width: dikey ? 940 : 520,
|
||||||
|
height: dikey ? 390 : 600,
|
||||||
|
padding: dikey ? "36px 40px" : "44px 40px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: dikey ? "row" : "column",
|
||||||
|
gap: dikey ? 36 : 0,
|
||||||
|
opacity: g,
|
||||||
|
transform: `translateY(${(1 - g) * 80}px)`,
|
||||||
|
borderColor: "rgba(226,232,240,0.7)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ position: "absolute", right: 30, top: 30 }}>
|
||||||
|
<PixelCorner boy={60} />
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: dikey ? 1 : undefined }}>
|
||||||
|
<div style={{ width: 96, height: 96, borderRadius: 26, background: "rgba(59,130,246,0.1)", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<Ikon ad={ikon} boy={46} renk={R.primary} />
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 26, fontFamily: FONT.baslik, fontWeight: 700, fontSize: 48, lineHeight: 1.1 }}>{baslik}</div>
|
||||||
|
<div style={{ marginTop: 12, fontSize: 30, lineHeight: 1.4, color: R.slate600 }}>{aciklama}</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1, display: "flex", alignItems: dikey ? "center" : "flex-end", justifyContent: "center", marginTop: dikey ? 0 : 24 }}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Kutu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Ozellikler() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey } = useDuzen();
|
||||||
|
const gW = dikey ? 380 : 420;
|
||||||
|
const gH = dikey ? 200 : 170;
|
||||||
|
const min = 15000;
|
||||||
|
const max = 60000;
|
||||||
|
const noktalar = TREND.map((v, i) => [(i / (TREND.length - 1)) * gW, ((v - min) / (max - min)) * gH]);
|
||||||
|
const cizgi = ara(frame, [26, 58], [0, 1]);
|
||||||
|
const yol = noktalar.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join(" ");
|
||||||
|
// Sürükle-bırak: imleç 58'de ilk satırı tutar, 92'de en alta bırakır
|
||||||
|
const tut = ara(frame, [58, 92], [0, 1], Easing.inOut(Easing.cubic));
|
||||||
|
const tutuldu = frame >= 58 && frame < 94;
|
||||||
|
const SATIR = 74;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sahne>
|
||||||
|
<OdakDisiPikseller seed={41} adet={7} guc={0.8} />
|
||||||
|
<Kamera anahtarlar={[{ f: 0, s: 1.18, x: 0, y: 60 }, { f: 50, s: 1.04, x: 0, y: 0 }, { f: 140, s: 1.0, x: 0, y: 0 }]} vuruslar={VURUSLAR.ozellikler}>
|
||||||
|
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center", gap: dikey ? 60 : 64 }}>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 36, textAlign: "center" }}>
|
||||||
|
<MaskeSatir gecikme={2}>
|
||||||
|
<Eyebrow olcek={dikey ? 2.6 : 2.3}>Paket içinde</Eyebrow>
|
||||||
|
</MaskeSatir>
|
||||||
|
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 88 : 88, lineHeight: 1.08, maxWidth: dikey ? 940 : 1600 }}>
|
||||||
|
<MaskeSatir gecikme={6}>
|
||||||
|
Her tercihin bir <span style={vurguStil}>“neden”i</span> var.
|
||||||
|
</MaskeSatir>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", flexDirection: dikey ? "column" : "row", gap: dikey ? 28 : 36 }}>
|
||||||
|
<OzellikKarti dikey={dikey} ikon="trend" baslik="6 yıllık trend" aciklama="Her tercihin taban seyri tek bakışta." gecikme={8}>
|
||||||
|
<svg data-iz="oz-trend" width={gW + 20} height={gH + 20} viewBox={`-10 -10 ${gW + 20} ${gH + 20}`}>
|
||||||
|
<path d={yol} fill="none" stroke={R.primary} strokeWidth={6} strokeLinecap="round" strokeLinejoin="round" pathLength={1} strokeDasharray={1} strokeDashoffset={1 - cizgi} />
|
||||||
|
{noktalar.map((p, i) => (
|
||||||
|
<rect key={i} x={p[0] - 9} y={p[1] - 9} width={18} height={18} rx={5} fill={i === TREND.length - 1 ? R.turuncu : R.primary} opacity={cizgi >= i / (TREND.length - 1) ? 1 : 0} />
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
</OzellikKarti>
|
||||||
|
<OzellikKarti dikey={dikey} ikon="belge" baslik="PDF rapor" aciklama="Aile toplantısına hazır." gecikme={14}>
|
||||||
|
<div style={{ position: "relative", width: 230, height: 230 }}>
|
||||||
|
{[2, 1, 0].map((k) => {
|
||||||
|
const s = yay(frame, 34 + (2 - k) * 6, 18);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={k}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 40 + k * 18,
|
||||||
|
top: 10 + k * 14,
|
||||||
|
width: 150,
|
||||||
|
height: 196,
|
||||||
|
borderRadius: 20,
|
||||||
|
background: R.beyaz,
|
||||||
|
border: `3px solid ${R.slate200}`,
|
||||||
|
boxShadow: "0 10px 24px rgba(15,23,42,0.06)",
|
||||||
|
transform: `rotate(${(k - 1) * 6 * s}deg)`,
|
||||||
|
padding: 20,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{k === 0 ? (
|
||||||
|
<>
|
||||||
|
<div style={{ display: "flex", gap: 6 }}>
|
||||||
|
{[R.riskli, R.azRiskli, R.guvenli].map((c) => (
|
||||||
|
<span key={c} style={{ width: 18, height: 18, borderRadius: 5, background: c }} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{[1, 0.8, 0.9, 0.6, 0.75].map((wd, j) => (
|
||||||
|
<span key={j} style={{ height: 10, width: `${wd * 100}%`, borderRadius: 5, background: R.slate200 }} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</OzellikKarti>
|
||||||
|
<OzellikKarti dikey={dikey} ikon="yenile" baslik="2 revizyon hakkı" aciklama="Fikrin değişir, listen de." gecikme={20}>
|
||||||
|
<div style={{ position: "relative", width: dikey ? 420 : 400, height: 3 * SATIR }}>
|
||||||
|
{REVIZYON.map((r, i) => {
|
||||||
|
// 0. satır sürüklenir; diğerleri yukarı kayarak yer açar
|
||||||
|
// Sürüklenen satır i'nin üstünden geçince o satır bir basamak yukarı kayar
|
||||||
|
const gecis = i === 1 ? (2 * tut - 0.5) / 0.8 : (2 * tut - 1.3) / 0.7;
|
||||||
|
const kay = Easing.inOut(Easing.cubic)(Math.min(1, Math.max(0, gecis)));
|
||||||
|
const y = i === 0 ? tut * 2 * SATIR : (i - kay) * SATIR;
|
||||||
|
const kalkis = i === 0 && tutuldu ? 1 : 0;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={r.ad}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: kalkis * 18,
|
||||||
|
top: y,
|
||||||
|
width: "100%",
|
||||||
|
height: 62,
|
||||||
|
borderRadius: 18,
|
||||||
|
border: `3px solid ${kalkis ? R.turuncu : R.slate200}`,
|
||||||
|
background: kalkis ? "#fff7ed" : R.beyaz,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 16,
|
||||||
|
padding: "0 20px",
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: R.slate800,
|
||||||
|
boxShadow: kalkis ? "0 18px 36px rgba(15,23,42,0.16)" : undefined,
|
||||||
|
transform: `scale(${1 + kalkis * 0.04}) rotate(${kalkis * -1.5}deg)`,
|
||||||
|
zIndex: i === 0 ? 2 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ width: 14, height: 14, borderRadius: 4, background: r.renk, flexShrink: 0 }} />
|
||||||
|
<span style={{ flex: 1, whiteSpace: "nowrap", overflow: "hidden" }}>{r.ad}</span>
|
||||||
|
{i === 0 ? (
|
||||||
|
<span data-iz="oz-rev-tut" style={{ display: "grid", gridTemplateColumns: "repeat(2, 6px)", gap: 4 }}>
|
||||||
|
{Array.from({ length: 6 }, (_, k) => (
|
||||||
|
<span key={k} style={{ width: 6, height: 6, borderRadius: 2, background: R.slate400 }} />
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</OzellikKarti>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
</Kamera>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Dürüstlük notu */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
export function Durustluk() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey } = useDuzen();
|
||||||
|
const kalkan = zipla(frame, 0);
|
||||||
|
const alt = yay(frame, 64, 18);
|
||||||
|
return (
|
||||||
|
<Sahne zemin={R.beyaz}>
|
||||||
|
<OdakDisiPikseller seed={51} adet={6} guc={0.6} />
|
||||||
|
<Kamera anahtarlar={[{ f: 0, s: 1.02 }, { f: 140, s: 1.16 }]} vuruslar={VURUSLAR.durustluk}>
|
||||||
|
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center", textAlign: "center", gap: 46, padding: "0 70px" }}>
|
||||||
|
<div style={{ width: 150, height: 150, borderRadius: 40, background: R.primary, display: "flex", alignItems: "center", justifyContent: "center", transform: `scale(${Math.max(0, kalkan)})` }}>
|
||||||
|
<Ikon ad="kalkan" boy={80} renk={R.beyaz} />
|
||||||
|
</div>
|
||||||
|
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 104 : 112, lineHeight: 1.06, maxWidth: dikey ? 920 : 1600 }}>
|
||||||
|
<MaskeSatir gecikme={8}>Yerleşme garantisi</MaskeSatir>
|
||||||
|
<MaskeSatir gecikme={16}>vermiyoruz.</MaskeSatir>
|
||||||
|
</div>
|
||||||
|
<div style={{ ...vurguStil, fontSize: dikey ? 92 : 96, lineHeight: 1.1 }}>
|
||||||
|
<MaskeSatir gecikme={36}>Çünkü kimse veremez.</MaskeSatir>
|
||||||
|
</div>
|
||||||
|
<div style={{ opacity: alt, transform: `translateY(${(1 - alt) * 24}px)`, display: "flex", flexDirection: "column", alignItems: "center", gap: 26, marginTop: 16 }}>
|
||||||
|
<div style={{ fontSize: dikey ? 40 : 40, color: R.slate600, fontWeight: 600, maxWidth: dikey ? 900 : 1400, lineHeight: 1.4 }}>
|
||||||
|
Veren olursa, uzak dur. Biz riski satır satır gösteririz.
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", gap: 44 }}>
|
||||||
|
{(["guvenli", "az-riskli", "riskli"] as const).map((r, i) => (
|
||||||
|
<span key={r} data-iz={`du-risk-${i}`} style={{ transform: `scale(${Math.max(0, zipla(frame, 76 + i * 5))})` }}>
|
||||||
|
<RiskEtiketi risk={r} olcek={3.2} />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
</Kamera>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Kapanış — yalnız logo, tipografi, buton. Tıkla → her şey imlecin */
|
||||||
|
/* içine çöker → ilk kare: döngü */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
export function Kapanis() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey } = useDuzen();
|
||||||
|
const buton = zipla(frame, 40);
|
||||||
|
const bas = ara(frame, [118, 121], [0, 1]) * (1 - ara(frame, [125, 130], [0, 1]));
|
||||||
|
// Çöküş: tıktan sonra içerik butonun olduğu noktaya büzülür
|
||||||
|
const cok = ara(frame, [124, 150], [0, 1], Easing.bezier(0.7, 0, 0.84, 0));
|
||||||
|
return (
|
||||||
|
<Sahne>
|
||||||
|
<Kamera anahtarlar={[{ f: 0, s: 1.12 }, { f: 100, s: 1.0 }]} vuruslar={VURUSLAR.kapanis}>
|
||||||
|
<AbsoluteFill
|
||||||
|
style={{
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
textAlign: "center",
|
||||||
|
gap: dikey ? 70 : 56,
|
||||||
|
transformOrigin: "50% 66%",
|
||||||
|
transform: `scale(${1 - cok})`,
|
||||||
|
opacity: 1 - ara(frame, [140, 150], [0, 1]),
|
||||||
|
filter: `blur(${cok * 10}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex", flexDirection: dikey ? "column" : "row", alignItems: "center", gap: dikey ? 18 : 28 }}>
|
||||||
|
<LogoMark boy={dikey ? 220 : 180} giris={4} rotaBas={22} />
|
||||||
|
<div style={{ opacity: yay(frame, 12, 16) }}>
|
||||||
|
<Wordmark boy={dikey ? 120 : 120} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 100 : 108, lineHeight: 1.06, maxWidth: dikey ? 920 : 1600 }}>
|
||||||
|
<MaskeSatir gecikme={20}>Listeni kur,</MaskeSatir>
|
||||||
|
<MaskeSatir gecikme={28}>
|
||||||
|
<span style={vurguStil}>panikle</span> değil.
|
||||||
|
</MaskeSatir>
|
||||||
|
</div>
|
||||||
|
<div data-iz="ka-buton" style={{ transform: `scale(${Math.max(0, buton)})` }}>
|
||||||
|
<TuruncuCta metin="kolaytercih.com" boy={dikey ? 1.2 : 1.1} basili={bas} />
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
</Kamera>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
441
src/eski/sahneler-yz.tsx
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { AbsoluteFill, useCurrentFrame } from "remotion";
|
||||||
|
import { Ikon } from "./ikonlar";
|
||||||
|
import { LogoMark } from "./logo";
|
||||||
|
import { RiskEtiketi, Sahne, useDuzen } from "./ortak";
|
||||||
|
import { Eyebrow } from "./piksel";
|
||||||
|
import { Kamera, MaskeSatir, OdakDisiPikseller, degerAl, type Anahtar } from "./sinema";
|
||||||
|
import { Telefon } from "./telefon";
|
||||||
|
import { FONT, R, ara, trSayi, yay, zipla } from "./tema";
|
||||||
|
import { VURUSLAR } from "./vurus";
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Danışman başlığı — sitedeki DanismanBaslik (sohbet-client.tsx) */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
function DanismanlaKonus({ boy }: { boy: number }) {
|
||||||
|
return (
|
||||||
|
<span style={{ display: "inline-flex", alignItems: "center", gap: boy * 0.08, fontFamily: FONT.vurgu, fontSize: boy, letterSpacing: "-0.05em", color: R.slate900, whiteSpace: "nowrap" }}>
|
||||||
|
<LogoMark boy={boy * 1.25} varyant="nabiz" turuncu girisYok />
|
||||||
|
<span>
|
||||||
|
<span style={{ fontWeight: 300 }}>Danışmanla</span>
|
||||||
|
<span style={{ fontWeight: 600 }}>Konuş</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sitedeki ORNEK_SORULAR (sohbet-client.tsx) + danışman araçlarının gerçek kullanımları
|
||||||
|
const ORNEK_SORULAR = [
|
||||||
|
"Listemdeki riskli tercihler ne kadar riskli?",
|
||||||
|
"İlk 5 tercihimin taban trendi nasıl?",
|
||||||
|
"Güvenli dilimden bir tercihi üste alsam mantıklı mı?",
|
||||||
|
"X bölümü ile Y arasında kalırsam neye bakmalıyım?",
|
||||||
|
"İstanbul'da %50 burslu vakıf var mı?",
|
||||||
|
"Bu üniversite nasıl bir yer?",
|
||||||
|
];
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Yapay zekâ bölüm açılışı */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
export function YzBaslik() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey, w, h } = useDuzen();
|
||||||
|
const mark = zipla(frame, 2);
|
||||||
|
return (
|
||||||
|
<Sahne zemin={R.beyaz}>
|
||||||
|
<AbsoluteFill>
|
||||||
|
{ORNEK_SORULAR.map((q, i) => {
|
||||||
|
const x = (dikey ? [0.08, 0.52, 0.18, 0.6, 0.05, 0.45] : [0.05, 0.62, 0.2, 0.7, 0.1, 0.55])[i] * w;
|
||||||
|
const hiz = 3.2 + (i % 3) * 0.9;
|
||||||
|
const y = h * (0.95 - ((i * 0.19 + (frame * hiz) / h) % 1.15));
|
||||||
|
const derin = i % 2 === 0;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={q}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: x,
|
||||||
|
top: y,
|
||||||
|
padding: "18px 28px",
|
||||||
|
borderRadius: 999,
|
||||||
|
border: `3px solid ${R.slate200}`,
|
||||||
|
background: R.beyaz,
|
||||||
|
fontSize: derin ? 28 : 34,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: R.slate500,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
opacity: derin ? 0.35 : 0.55,
|
||||||
|
filter: `blur(${derin ? 5 : 1.5}px)`,
|
||||||
|
transform: `scale(${derin ? 0.85 : 1})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{q}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</AbsoluteFill>
|
||||||
|
<Kamera anahtarlar={[{ f: 0, s: 1.06 }, { f: 110, s: 1.2 }]} vuruslar={VURUSLAR.yzBaslik}>
|
||||||
|
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center", textAlign: "center" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: `radial-gradient(closest-side, ${R.beyaz} 70%, rgba(255,255,255,0))`,
|
||||||
|
padding: "80px 120px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 40,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ transform: `scale(${Math.max(0, mark)})` }}>
|
||||||
|
<DanismanlaKonus boy={dikey ? 84 : 80} />
|
||||||
|
</div>
|
||||||
|
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 124 : 130, lineHeight: 1.05, maxWidth: dikey ? 900 : 1600 }}>
|
||||||
|
<MaskeSatir gecikme={10}>
|
||||||
|
Aklına takılanı{" "}
|
||||||
|
<span data-iz="yz-sor" style={{ fontFamily: FONT.vurgu, fontStyle: "italic", fontWeight: 800, letterSpacing: "-0.04em", color: R.primary }}>
|
||||||
|
sor.
|
||||||
|
</span>
|
||||||
|
</MaskeSatir>
|
||||||
|
</div>
|
||||||
|
<MaskeSatir gecikme={26}>
|
||||||
|
<span style={{ display: "block", fontSize: dikey ? 42 : 40, lineHeight: 1.4, color: R.slate600, maxWidth: dikey ? 860 : 1100 }}>
|
||||||
|
Gece 3'te bile cevap verir. Sıkılmaz, yargılamaz, “bu da soru mu” demez.
|
||||||
|
</span>
|
||||||
|
</MaskeSatir>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
</Kamera>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Sohbet — 3B telefonda üç gerçek kullanım senaryosu */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
// Gerçek YÖK Atlas 2026 verisi (data/yokatlas.db): İzmir'de İngilizce
|
||||||
|
// Bilgisayar Mühendisliği, devlet; örnek aday 45.320. (riskHesapla eşikleri)
|
||||||
|
const SONUCLAR = [
|
||||||
|
{ uni: "Ege Üniversitesi", taban: 44589, risk: "riskli" as const },
|
||||||
|
{ uni: "Dokuz Eylül Üniversitesi", taban: 52071, risk: "az-riskli" as const },
|
||||||
|
{ uni: "İzmir Katip Çelebi Üniversitesi", taban: 71833, risk: "guvenli" as const },
|
||||||
|
];
|
||||||
|
|
||||||
|
type Mesaj =
|
||||||
|
| { tur: "kullanici"; metin: string; yaz: [number, number]; gonder: number }
|
||||||
|
| { tur: "arac"; ad: string; detay: string; bas: number; bitis: number }
|
||||||
|
| { tur: "cevap"; bas: number; Icerik: () => ReactNode; tahmin: number; iz?: string };
|
||||||
|
|
||||||
|
function SonucListesi() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||||
|
<div>Sıralamana göre İzmir'de üç devlet seçeneği öne çıkıyor:</div>
|
||||||
|
{SONUCLAR.map((s, i) => {
|
||||||
|
const g = yay(frame, 102 + i * 7, 16);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={s.uni}
|
||||||
|
data-iz={`yz-sonuc-${i}`}
|
||||||
|
style={{ background: R.beyaz, border: `1px solid ${R.slate200}`, borderRadius: 14, padding: "9px 12px", opacity: g, transform: `translateY(${(1 - g) * 10}px)` }}
|
||||||
|
>
|
||||||
|
<div style={{ fontWeight: 600, color: R.slate900, lineHeight: 1.25, fontSize: 14 }}>Bilgisayar Mühendisliği</div>
|
||||||
|
<div style={{ fontSize: 12, color: R.slate500, marginTop: 2 }}>{s.uni} · Devlet</div>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 5 }}>
|
||||||
|
<RiskEtiketi risk={s.risk} olcek={1} />
|
||||||
|
<span style={{ fontSize: 12, color: R.slate500 }}>
|
||||||
|
Taban <b style={{ color: R.slate900, fontWeight: 600 }}>{trSayi(s.taban)}</b>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProfilCevabi() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const cip = zipla(frame, 292);
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 10, alignItems: "flex-start" }}>
|
||||||
|
<div>Profilini güncelledim. Yeni listende havuz yalnız İzmir'deki programlardan süzülecek.</div>
|
||||||
|
<span
|
||||||
|
data-iz="yz-profil"
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
height: 32,
|
||||||
|
padding: "0 12px",
|
||||||
|
borderRadius: 999,
|
||||||
|
background: R.primary,
|
||||||
|
color: R.beyaz,
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: 13,
|
||||||
|
transform: `scale(${Math.max(0, cip)})`,
|
||||||
|
transformOrigin: "left center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ikon ad="konum" boy={14} />
|
||||||
|
Şehir: İzmir
|
||||||
|
<Ikon ad="tik" boy={14} kalinlik={3} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SENARYO: Mesaj[] = [
|
||||||
|
{ tur: "kullanici", metin: "İzmir'de İngilizce bilgisayar mühendisliği var mı?", yaz: [22, 52], gonder: 58 },
|
||||||
|
{ tur: "arac", ad: "program_ara", detay: "İzmir · İngilizce", bas: 64, bitis: 90 },
|
||||||
|
{ tur: "cevap", bas: 92, tahmin: 330, Icerik: SonucListesi },
|
||||||
|
{ tur: "kullanici", metin: "Dokuz Eylül'deki akreditasyonlu mu?", yaz: [142, 158], gonder: 164 },
|
||||||
|
{ tur: "arac", ad: "program_detay", detay: "Dokuz Eylül", bas: 170, bitis: 188 },
|
||||||
|
{
|
||||||
|
tur: "cevap",
|
||||||
|
bas: 190,
|
||||||
|
tahmin: 120,
|
||||||
|
iz: "yz-cevap-2",
|
||||||
|
Icerik: () => (
|
||||||
|
<>
|
||||||
|
Evet, <b>MÜDEK akreditasyonlu</b> ve eğitim dili İngilizce. 2026'da son yerleşen <b>52.071.</b> oldu; sen 45.320.'sin.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ tur: "kullanici", metin: "Aslında İzmir dışına çıkmak istemiyorum.", yaz: [226, 246], gonder: 250 },
|
||||||
|
{ tur: "arac", ad: "profil_guncelle", detay: "şehir: İzmir", bas: 256, bitis: 272 },
|
||||||
|
{ tur: "cevap", bas: 274, tahmin: 140, Icerik: ProfilCevabi },
|
||||||
|
];
|
||||||
|
|
||||||
|
function Avatar({ canli }: { canli: boolean }) {
|
||||||
|
return (
|
||||||
|
<span style={{ width: 30, height: 30, borderRadius: 999, border: `1px solid ${R.slate200}`, background: R.beyaz, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
|
||||||
|
<LogoMark boy={19} varyant={canli ? "nabiz" : "duruk"} girisYok />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Telefonun içindeki sohbet ekranı (site px, 390 genişlik). */
|
||||||
|
function SohbetEkrani() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const yazilan = (() => {
|
||||||
|
for (const m of SENARYO) {
|
||||||
|
if (m.tur !== "kullanici") continue;
|
||||||
|
if (frame >= m.yaz[0] && frame < m.gonder) {
|
||||||
|
const n = Math.floor(ara(frame, m.yaz, [0, m.metin.length], (t) => t));
|
||||||
|
return m.metin.slice(0, n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
})();
|
||||||
|
const basis = SENARYO.some((m) => m.tur === "kullanici" && frame >= m.gonder - 2 && frame < m.gonder + 3);
|
||||||
|
const onerilerGit = ara(frame, [52, 60], [1, 0]);
|
||||||
|
const kredi = frame >= 250 ? 57 : frame >= 164 ? 58 : frame >= 58 ? 59 : 60;
|
||||||
|
return (
|
||||||
|
<div style={{ position: "absolute", top: 50, left: 0, right: 0, bottom: 0, display: "flex", flexDirection: "column", fontFamily: FONT.govde }}>
|
||||||
|
<div style={{ height: 56, display: "flex", alignItems: "center", justifyContent: "space-between", padding: "0 16px", borderBottom: `1px solid ${R.slate100}` }}>
|
||||||
|
<DanismanlaKonus boy={18} />
|
||||||
|
<span style={{ display: "inline-flex", alignItems: "center", height: 30, padding: "0 12px", borderRadius: 999, border: `1px solid ${R.slate200}`, fontSize: 12, fontWeight: 600, color: R.slate700 }}>
|
||||||
|
{kredi} soru hakkı
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1, display: "flex", flexDirection: "column", justifyContent: "flex-end", gap: 12, padding: "0 14px 10px", overflow: "hidden", fontSize: 15, lineHeight: 1.45 }}>
|
||||||
|
{onerilerGit > 0 ? (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 8, alignItems: "flex-start", opacity: onerilerGit, marginBottom: 6 }}>
|
||||||
|
<div style={{ fontSize: 12, fontWeight: 600, color: R.slate500 }}>Örnek sorular</div>
|
||||||
|
{ORNEK_SORULAR.slice(0, 3).map((q, i) => {
|
||||||
|
const g = yay(frame, 6 + i * 4, 14);
|
||||||
|
return (
|
||||||
|
<span key={q} style={{ padding: "8px 12px", borderRadius: 999, border: `1px solid ${R.slate200}`, fontSize: 12, fontWeight: 600, color: R.slate700, opacity: g, transform: `translateY(${(1 - g) * 8}px)` }}>
|
||||||
|
{q}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{SENARYO.map((m, i) => {
|
||||||
|
const bas = m.tur === "kullanici" ? m.gonder : m.bas;
|
||||||
|
if (frame < bas) return null;
|
||||||
|
const g = yay(frame, bas, 16);
|
||||||
|
const tahmin = m.tur === "kullanici" ? 80 : m.tur === "arac" ? 40 : m.tahmin;
|
||||||
|
const kap = { maxHeight: g >= 0.999 ? undefined : g * tahmin, overflow: "hidden" as const, flexShrink: 0 };
|
||||||
|
if (m.tur === "kullanici") {
|
||||||
|
return (
|
||||||
|
<div key={i} style={{ ...kap, display: "flex", justifyContent: "flex-end" }}>
|
||||||
|
<div style={{ maxWidth: "85%", background: R.primary, color: R.beyaz, padding: "10px 14px", borderRadius: 18, fontWeight: 600, opacity: g, transform: `translateY(${(1 - g) * 12}px)` }}>
|
||||||
|
{m.metin}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (m.tur === "arac") {
|
||||||
|
const bitti = frame >= m.bitis;
|
||||||
|
return (
|
||||||
|
<div key={i} style={{ ...kap, display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
|
<Avatar canli={!bitti} />
|
||||||
|
<span style={{ display: "inline-flex", alignItems: "center", gap: 6, height: 28, padding: "0 10px", borderRadius: 999, background: R.slate50, border: `1px solid ${R.slate200}`, fontSize: 11, fontWeight: 600, color: R.slate600, opacity: g, whiteSpace: "nowrap" }}>
|
||||||
|
{bitti ? <Ikon ad="tik" boy={12} renk={R.guvenliYazi} kalinlik={3} /> : <Ikon ad="arama" boy={12} renk={R.primary} />}
|
||||||
|
<span style={{ fontFamily: FONT.vurgu, color: R.primary }}>{m.ad}</span>
|
||||||
|
<span>· {m.detay}</span>
|
||||||
|
{!bitti ? <span style={{ color: R.slate400 }}>· arıyor{".".repeat(1 + (Math.floor(frame / 6) % 3))}</span> : null}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div key={i} style={{ ...kap, paddingLeft: 38 }}>
|
||||||
|
<div data-iz={m.iz} style={{ maxWidth: "94%", background: R.slate100, color: R.slate800, padding: "11px 14px", borderRadius: 18, opacity: g, transform: `translateY(${(1 - g) * 12}px)` }}>
|
||||||
|
<m.Icerik />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: "8px 12px 22px" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
minHeight: 50,
|
||||||
|
borderRadius: 16,
|
||||||
|
border: `1px solid ${yazilan ? R.primary : R.slate200}`,
|
||||||
|
boxShadow: yazilan ? "0 0 0 3px rgba(59,130,246,0.2)" : undefined,
|
||||||
|
padding: "0 7px 0 14px",
|
||||||
|
fontSize: 15,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span data-iz="yz-girdi" style={{ flex: 1, color: yazilan ? R.slate900 : R.slate400, whiteSpace: "nowrap", overflow: "hidden" }}>
|
||||||
|
{yazilan || "Listen hakkında bir şey sor…"}
|
||||||
|
{yazilan && Math.floor(frame / 8) % 2 === 0 ? <span style={{ color: R.primary }}>|</span> : null}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
data-iz="yz-gonder"
|
||||||
|
style={{ width: 36, height: 36, borderRadius: 999, background: basis ? R.turuncuKoyu : R.turuncu, display: "flex", alignItems: "center", justifyContent: "center", transform: `scale(${basis ? 0.92 : 1})`, flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
<Ikon ad="ok" boy={16} renk={R.beyaz} kalinlik={2.6} style={{ transform: "rotate(-90deg)" }} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Olan biteni anlatan altyazılar (Apple tarzı), kameradan bağımsız
|
||||||
|
const ALTYAZI: { bas: number; son: number; metin: ReactNode }[] = [
|
||||||
|
{ bas: 4, son: 60, metin: "Sen soruyorsun." },
|
||||||
|
{ bas: 64, son: 94, metin: "O, YÖK Atlas'ta arıyor." },
|
||||||
|
{ bas: 98, son: 158, metin: "Gerçek kayıtla cevaplıyor." },
|
||||||
|
{ bas: 164, son: 244, metin: "Ayrıntıya da iniyor." },
|
||||||
|
{ bas: 252, son: 345, metin: "Fikrin değişirse, profilini güncelliyor." },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function YzSohbet() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey, w, h } = useDuzen();
|
||||||
|
const telGen = dikey ? 700 : 480;
|
||||||
|
const don = [
|
||||||
|
{ f: 0, s: -22, x: 8 },
|
||||||
|
{ f: 40, s: -5, x: 2 },
|
||||||
|
{ f: 300, s: 3, x: 1 },
|
||||||
|
{ f: 350, s: 10, x: 4 },
|
||||||
|
] as Anahtar[];
|
||||||
|
const kamera: Anahtar[] = dikey
|
||||||
|
? [
|
||||||
|
{ f: 0, s: 1, y: 60 },
|
||||||
|
{ f: 18, s: 1.3, y: 650 },
|
||||||
|
{ f: 52, s: 1.3, y: 650 },
|
||||||
|
{ f: 70, s: 1.2, y: 420 },
|
||||||
|
{ f: 112, s: 1.14, y: 330 },
|
||||||
|
{ f: 134, s: 1.3, y: 650 },
|
||||||
|
{ f: 166, s: 1.3, y: 620 },
|
||||||
|
{ f: 196, s: 1.22, y: 440 },
|
||||||
|
{ f: 218, s: 1.3, y: 650 },
|
||||||
|
{ f: 252, s: 1.3, y: 620 },
|
||||||
|
{ f: 280, s: 1.22, y: 440 },
|
||||||
|
{ f: 305, s: 1.2, y: 420 },
|
||||||
|
{ f: 334, s: 1, y: 60 },
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{ f: 0, s: 1, x: 0, y: 0 },
|
||||||
|
{ f: 18, s: 1.35, x: 150, y: 330 },
|
||||||
|
{ f: 52, s: 1.35, x: 150, y: 330 },
|
||||||
|
{ f: 70, s: 1.25, x: 150, y: 180 },
|
||||||
|
{ f: 112, s: 1.2, x: 150, y: 120 },
|
||||||
|
{ f: 134, s: 1.35, x: 150, y: 330 },
|
||||||
|
{ f: 166, s: 1.35, x: 150, y: 310 },
|
||||||
|
{ f: 196, s: 1.28, x: 150, y: 200 },
|
||||||
|
{ f: 218, s: 1.35, x: 150, y: 330 },
|
||||||
|
{ f: 252, s: 1.35, x: 150, y: 310 },
|
||||||
|
{ f: 280, s: 1.28, x: 150, y: 200 },
|
||||||
|
{ f: 305, s: 1.25, x: 150, y: 180 },
|
||||||
|
{ f: 334, s: 1, x: 0, y: 0 },
|
||||||
|
];
|
||||||
|
const dipnot = yay(frame, 312, 20);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sahne>
|
||||||
|
<OdakDisiPikseller seed={61} adet={8} />
|
||||||
|
<Kamera anahtarlar={kamera} vuruslar={VURUSLAR.yzSohbet}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: (dikey ? w / 2 : w * 0.66) - telGen / 2,
|
||||||
|
top: (dikey ? h * 0.56 : h / 2) - (telGen * 2.05) / 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Telefon genislik={telGen} donX={degerAl(don, frame, "x", 0)} donY={degerAl(don, frame, "s", 0)}>
|
||||||
|
<SohbetEkrani />
|
||||||
|
</Telefon>
|
||||||
|
</div>
|
||||||
|
</Kamera>
|
||||||
|
{/* Altyazılar: kameradan bağımsız, hep okunur */}
|
||||||
|
<AbsoluteFill style={{ pointerEvents: "none" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
...(dikey
|
||||||
|
? { left: 0, right: 0, top: 0, height: 380, alignItems: "center", textAlign: "center" as const, paddingTop: 140, background: "linear-gradient(to bottom, rgba(248,250,252,0.96) 55%, rgba(248,250,252,0))" }
|
||||||
|
: { left: 0, width: w * 0.46, top: 0, bottom: 0, paddingLeft: 140, justifyContent: "center", background: "linear-gradient(to right, rgba(248,250,252,0.96) 70%, rgba(248,250,252,0))" }),
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 26,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!dikey ? (
|
||||||
|
<MaskeSatir gecikme={2}>
|
||||||
|
<Eyebrow olcek={2.2}>Yapay zekâ danışman</Eyebrow>
|
||||||
|
</MaskeSatir>
|
||||||
|
) : null}
|
||||||
|
<div style={{ position: "relative", height: dikey ? 110 : 260, width: "100%" }}>
|
||||||
|
{ALTYAZI.map((a) =>
|
||||||
|
frame >= a.bas - 1 && frame < a.son + 16 ? (
|
||||||
|
<div
|
||||||
|
key={a.bas}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: dikey ? 60 : 0,
|
||||||
|
right: dikey ? 60 : 40,
|
||||||
|
top: 0,
|
||||||
|
fontFamily: FONT.baslik,
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: dikey ? 72 : 84,
|
||||||
|
lineHeight: 1.08,
|
||||||
|
color: R.slate900,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MaskeSatir gecikme={a.bas} sure={18} cikis={a.son}>
|
||||||
|
{a.metin}
|
||||||
|
</MaskeSatir>
|
||||||
|
</div>
|
||||||
|
) : null,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ position: "absolute", left: 0, right: dikey ? 0 : undefined, width: dikey ? undefined : w * 0.46, paddingLeft: dikey ? 0 : 140, bottom: dikey ? 60 : 120, display: "flex", justifyContent: dikey ? "center" : "flex-start", opacity: dipnot, transform: `translateY(${(1 - dipnot) * 16}px)` }}>
|
||||||
|
<Eyebrow olcek={dikey ? 2.3 : 2.1}>Havuz dışına çıkamaz, uydurma bölüm önermez</Eyebrow>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
129
src/eski/telefon.tsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { R } from "./tema";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3B telefon maketi. Ekranın içi **sitenin kendi px ölçüsüyle** (390 px
|
||||||
|
* genişlik) yazılır ve ekrana ölçeklenir; böylece h-12, text-base gibi
|
||||||
|
* değerler DESIGN.md'deki gibi birebir kullanılır. Marka/logo taşımayan
|
||||||
|
* genel bir cihaz çerçevesi.
|
||||||
|
*/
|
||||||
|
export function Telefon({
|
||||||
|
genislik,
|
||||||
|
donX = 0,
|
||||||
|
donY = 0,
|
||||||
|
donZ = 0,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
genislik: number;
|
||||||
|
donX?: number;
|
||||||
|
donY?: number;
|
||||||
|
donZ?: number;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
const yukseklik = genislik * 2.05;
|
||||||
|
const cerceve = genislik * 0.034;
|
||||||
|
const yaricap = genislik * 0.16;
|
||||||
|
const ekranW = genislik - 2 * cerceve;
|
||||||
|
const ekranH = yukseklik - 2 * cerceve;
|
||||||
|
const k = ekranW / 390;
|
||||||
|
// Işık açıyla kayar: telefon döndükçe camdan geçen parlama
|
||||||
|
const isikKonum = 50 + donY * 2.2 - donX * 1.2;
|
||||||
|
return (
|
||||||
|
<div style={{ perspective: 2800, width: genislik, height: yukseklik }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
width: genislik,
|
||||||
|
height: yukseklik,
|
||||||
|
transform: `rotateX(${donX}deg) rotateY(${donY}deg) rotateZ(${donZ}deg)`,
|
||||||
|
transformStyle: "preserve-3d",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Gövde */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
borderRadius: yaricap,
|
||||||
|
background: R.slate900,
|
||||||
|
boxShadow: `inset 0 0 0 ${genislik * 0.007}px #334155, 0 ${genislik * 0.09}px ${genislik * 0.2}px rgba(15,23,42,0.22), 0 ${genislik * 0.02}px ${genislik * 0.04}px rgba(15,23,42,0.18)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* Ekran */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: cerceve,
|
||||||
|
top: cerceve,
|
||||||
|
width: ekranW,
|
||||||
|
height: ekranH,
|
||||||
|
borderRadius: yaricap - cerceve,
|
||||||
|
overflow: "hidden",
|
||||||
|
background: R.beyaz,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ width: 390, height: ekranH / k, transform: `scale(${k})`, transformOrigin: "0 0", position: "relative" }}>
|
||||||
|
<DurumCubugu />
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Kamera adası */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: genislik / 2 - genislik * 0.15,
|
||||||
|
top: cerceve + genislik * 0.028,
|
||||||
|
width: genislik * 0.3,
|
||||||
|
height: genislik * 0.082,
|
||||||
|
borderRadius: 999,
|
||||||
|
background: "#000",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* Cam parlaması */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: cerceve,
|
||||||
|
top: cerceve,
|
||||||
|
width: ekranW,
|
||||||
|
height: ekranH,
|
||||||
|
borderRadius: yaricap - cerceve,
|
||||||
|
background: "linear-gradient(115deg, rgba(255,255,255,0) 38%, rgba(255,255,255,0.28) 48%, rgba(255,255,255,0) 58%)",
|
||||||
|
backgroundSize: "260% 260%",
|
||||||
|
backgroundPosition: `${isikKonum}% 50%`,
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** iOS benzeri durum çubuğu (site px). */
|
||||||
|
function DurumCubugu() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: 50,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
padding: "6px 30px 0 34px",
|
||||||
|
fontFamily: "'Clarity City', sans-serif",
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: 16,
|
||||||
|
color: R.slate900,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>09:41</span>
|
||||||
|
<span style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||||||
|
{[5, 7, 9, 11].map((h, i) => (
|
||||||
|
<span key={i} style={{ width: 3.5, height: h, borderRadius: 2, background: R.slate900, alignSelf: "flex-end" }} />
|
||||||
|
))}
|
||||||
|
<span style={{ marginLeft: 6, width: 25, height: 12, borderRadius: 4, border: `1.5px solid ${R.slate900}`, padding: 1.5 }}>
|
||||||
|
<span style={{ display: "block", width: "78%", height: "100%", borderRadius: 2, background: R.slate900 }} />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
32
src/fontlar.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { loadFont } from "@remotion/fonts";
|
||||||
|
import { staticFile } from "remotion";
|
||||||
|
|
||||||
|
// fontsource'un latin / latin-ext ayrımı: Türkçe harfler (ğ, ş, İ) latin-ext'te
|
||||||
|
const LATIN =
|
||||||
|
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD";
|
||||||
|
const LATIN_EXT =
|
||||||
|
"U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF";
|
||||||
|
|
||||||
|
const f = (dosya: string) => staticFile(`fonts/${dosya}`);
|
||||||
|
|
||||||
|
export const fontlarHazir = Promise.all([
|
||||||
|
loadFont({ family: "Cal Sans", url: f("cal-sans-latin-400-normal.woff"), unicodeRange: LATIN }),
|
||||||
|
loadFont({ family: "Cal Sans", url: f("cal-sans-latin-ext-400-normal.woff"), unicodeRange: LATIN_EXT }),
|
||||||
|
...(["300", "600", "800"] as const).flatMap((w) => [
|
||||||
|
loadFont({
|
||||||
|
family: "Bricolage",
|
||||||
|
weight: w,
|
||||||
|
url: f(`bricolage-grotesque-latin-${w}-normal.woff`),
|
||||||
|
unicodeRange: LATIN,
|
||||||
|
}),
|
||||||
|
loadFont({
|
||||||
|
family: "Bricolage",
|
||||||
|
weight: w,
|
||||||
|
url: f(`bricolage-grotesque-latin-ext-${w}-normal.woff`),
|
||||||
|
unicodeRange: LATIN_EXT,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
loadFont({ family: "Clarity City", weight: "400", url: f("ClarityCity-Regular.woff2") }),
|
||||||
|
loadFont({ family: "Clarity City", weight: "600", url: f("ClarityCity-SemiBold.woff2") }),
|
||||||
|
loadFont({ family: "Clarity City", weight: "700", url: f("ClarityCity-Bold.woff2") }),
|
||||||
|
]);
|
||||||
302
src/harita.tsx
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import {
|
||||||
|
AbsoluteFill,
|
||||||
|
Easing,
|
||||||
|
interpolate,
|
||||||
|
interpolateColors,
|
||||||
|
useCurrentFrame,
|
||||||
|
useVideoConfig,
|
||||||
|
} from "remotion";
|
||||||
|
import { R, hash, opaklik } from "./tema";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hero'daki dot-matrix Türkiye (turkey-pixel-map.tsx): aynı 64×26 ızgara,
|
||||||
|
* aynı şehirler, kıyının altına doğru seyrelen serpinti ve imleç ısı
|
||||||
|
* fırçası (use-pixel-heat.ts: ısınma 0.15 sn, soğuma 1.4 sn). Videoda
|
||||||
|
* pikseller yukarıdan dökülerek yerine oturur; ardından bir imleç haritanın
|
||||||
|
* üzerinden geçip pikselleri turuncuya ısıtır.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const GRID = [
|
||||||
|
"..######..............##########................................",
|
||||||
|
"..#######...........##############..............................",
|
||||||
|
"..#######.........#################................#####........",
|
||||||
|
".########...###########################...........########......",
|
||||||
|
".######.....###############################################.....",
|
||||||
|
".###........###############################################.....",
|
||||||
|
".##.......##################################################....",
|
||||||
|
".###########################################################....",
|
||||||
|
".#############################################################..",
|
||||||
|
"..#############################################################.",
|
||||||
|
"...############################################################.",
|
||||||
|
"...##########################################################...",
|
||||||
|
"...##########################################################...",
|
||||||
|
"...###########################################################..",
|
||||||
|
"..############################################################..",
|
||||||
|
"..############################################################..",
|
||||||
|
"...###########################################################..",
|
||||||
|
"....##########################################################..",
|
||||||
|
"....##########################################################..",
|
||||||
|
".....#########################################################..",
|
||||||
|
".....###################################################..##....",
|
||||||
|
".....############################################...............",
|
||||||
|
"......##########..######################...###..................",
|
||||||
|
"...........#####....#########....#####..........................",
|
||||||
|
"............###.......#####......####...........................",
|
||||||
|
".................................###............................",
|
||||||
|
];
|
||||||
|
const SUTUN = GRID[0].length;
|
||||||
|
const SATIR = GRID.length;
|
||||||
|
const SERPINTI = 9;
|
||||||
|
|
||||||
|
const SEHIRLER = [
|
||||||
|
{ x: 8, y: 4 },
|
||||||
|
{ x: 23, y: 9 },
|
||||||
|
{ x: 4, y: 16 },
|
||||||
|
{ x: 16, y: 22 },
|
||||||
|
{ x: 32, y: 14 },
|
||||||
|
{ x: 46, y: 5 },
|
||||||
|
{ x: 47, y: 18 },
|
||||||
|
{ x: 58, y: 15 },
|
||||||
|
];
|
||||||
|
const sehirIndeks = new Map(SEHIRLER.map((c, i) => [`${c.x},${c.y}`, i]));
|
||||||
|
|
||||||
|
type Piksel = { x: number; y: number; op: number; tur: "kara" | "sehir" | "serpinti" | "vurgu"; sira: number };
|
||||||
|
|
||||||
|
// Modül yüklenirken tek sefer: hero'daki MAP_MARKUP ile aynı üretim
|
||||||
|
const PIKSELLER: Piksel[] = (() => {
|
||||||
|
const p: Piksel[] = [];
|
||||||
|
GRID.forEach((satir, y) => {
|
||||||
|
for (let x = 0; x < satir.length; x++) {
|
||||||
|
if (satir[x] !== "#") continue;
|
||||||
|
const si = sehirIndeks.get(`${x},${y}`);
|
||||||
|
p.push({ x, y, op: si == null ? opaklik(x, y) : 0.7, tur: si == null ? "kara" : "sehir", sira: si ?? 0 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const sonKara: number[] = Array(SUTUN).fill(-1);
|
||||||
|
GRID.forEach((satir, y) => {
|
||||||
|
for (let x = 0; x < satir.length; x++) if (satir[x] === "#") sonKara[x] = y;
|
||||||
|
});
|
||||||
|
let vurgu = 0;
|
||||||
|
for (let x = 0; x < SUTUN; x++) {
|
||||||
|
if (sonKara[x] < 0) continue;
|
||||||
|
for (let d = 1; d <= SERPINTI; d++) {
|
||||||
|
const y = sonKara[x] + d;
|
||||||
|
if (y >= SATIR + SERPINTI) break;
|
||||||
|
const yogun = 0.05 + 0.4 * (1 - (d - 1) / SERPINTI) ** 2;
|
||||||
|
if (hash(x, y) > yogun) continue;
|
||||||
|
if (hash(x, y, 99) > 0.96) {
|
||||||
|
p.push({ x, y, op: 0.7, tur: "vurgu", sira: vurgu++ % 8 });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const op = Math.round(opaklik(x, y) * (1 - d / (SERPINTI + 1)) * 100) / 100;
|
||||||
|
p.push({ x, y, op, tur: "serpinti", sira: 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const belirEgri = Easing.bezier(0.23, 1, 0.32, 1); // kt-piksel-belir
|
||||||
|
|
||||||
|
export const HARITA_SUTUN = SUTUN;
|
||||||
|
export const HARITA_SATIR = SATIR + SERPINTI;
|
||||||
|
const varsayilanYol = (p: number) => {
|
||||||
|
const e = Easing.inOut(Easing.sin)(p);
|
||||||
|
return { x: -4 + e * (SUTUN + 8), y: 11 + Math.sin(p * Math.PI * 2.2) * 6 };
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Tailwind animate-pulse: opaklık 1 → 0.5 → 1, burada 3.6 sn (hero şehirleri). */
|
||||||
|
const nabiz = (frame: number, gecikmeSn: number) => {
|
||||||
|
const t = ((frame / 30 - gecikmeSn) % 3.6 + 3.6) % 3.6 / 3.6;
|
||||||
|
return 0.75 + 0.25 * Math.cos(t * Math.PI * 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function YagmurHaritasi({
|
||||||
|
genislik,
|
||||||
|
baslangic = 0,
|
||||||
|
dokulme = 40,
|
||||||
|
fircaBas,
|
||||||
|
fircaSure = 70,
|
||||||
|
iz,
|
||||||
|
fircaYol,
|
||||||
|
}: {
|
||||||
|
genislik: number;
|
||||||
|
baslangic?: number;
|
||||||
|
/** Üst satırdan alt satıra dökülme süresi (kare) */
|
||||||
|
dokulme?: number;
|
||||||
|
/** Isı fırçasının (imleç) haritaya girdiği kare */
|
||||||
|
fircaBas?: number;
|
||||||
|
fircaSure?: number;
|
||||||
|
/** Fırçanın ucuna konan görünmez işaretin adı */
|
||||||
|
iz?: string;
|
||||||
|
/** Fırça yolu, ızgara biriminde (p: 0..1, sin eğrili) */
|
||||||
|
fircaYol?: (p: number) => { x: number; y: number };
|
||||||
|
}) {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const hucre = genislik / SUTUN;
|
||||||
|
const dot = hucre * 0.74;
|
||||||
|
const ic = (hucre - dot) / 2;
|
||||||
|
const yukseklik = (SATIR + SERPINTI) * hucre;
|
||||||
|
|
||||||
|
// Fırça yolu (ızgara biriminde): soldan sağa hafif dalgalı bir geçiş
|
||||||
|
const firca = (f: number) => {
|
||||||
|
if (fircaBas == null) return null;
|
||||||
|
const p = (f - fircaBas) / fircaSure;
|
||||||
|
if (p < 0 || p > 1) return null;
|
||||||
|
return (fircaYol ?? varsayilanYol)(p);
|
||||||
|
};
|
||||||
|
const YARICAP = 3.4; // 34 viewBox birimi / 10'luk hücre
|
||||||
|
const isiHesapla = (x: number, y: number) => {
|
||||||
|
if (fircaBas == null || frame < fircaBas) return 0;
|
||||||
|
let isi = 0;
|
||||||
|
for (let k = 0; k < 48; k++) {
|
||||||
|
const b = firca(frame - k);
|
||||||
|
if (!b) continue;
|
||||||
|
if ((b.x - x - 0.5) ** 2 + (b.y - y - 0.5) ** 2 > YARICAP * YARICAP) continue;
|
||||||
|
// 0.15 sn'de ısın, 1.4 sn'de soğu (easeOut)
|
||||||
|
const h = k < 4.5 ? 1 : Math.pow(1 - Math.min(1, (k - 4.5) / 42), 2.2);
|
||||||
|
if (h > isi) isi = h;
|
||||||
|
break; // en yakın geçmiş temas en sıcak olandır
|
||||||
|
}
|
||||||
|
return isi;
|
||||||
|
};
|
||||||
|
|
||||||
|
const rectler: ReactNode[] = [];
|
||||||
|
for (const p of PIKSELLER) {
|
||||||
|
// Dökülme: üst satır önce; serpinti en son, kıyıdan aşağı damlar gibi
|
||||||
|
const t0 = baslangic + (p.y / (SATIR + SERPINTI)) * dokulme + hash(p.x, p.y, 3) * 10;
|
||||||
|
const ilerle = interpolate(frame, [t0, t0 + 16], [0, 1], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
easing: belirEgri,
|
||||||
|
});
|
||||||
|
if (ilerle <= 0) continue;
|
||||||
|
const dusme = (1 - ilerle) * (hucre * (7 + hash(p.x, p.y, 8) * 9));
|
||||||
|
const uzama = 1 + (1 - ilerle) * 0.9; // düşerken dikine uzar
|
||||||
|
const gorunur = Math.min(1, ilerle * 2.2);
|
||||||
|
const w = dot;
|
||||||
|
const h = dot * uzama;
|
||||||
|
const x = p.x * hucre + ic;
|
||||||
|
const y = p.y * hucre + ic - dusme - (h - dot) / 2;
|
||||||
|
|
||||||
|
let fill: string = R.primary;
|
||||||
|
let op = p.op;
|
||||||
|
if (p.tur === "sehir" || p.tur === "vurgu") {
|
||||||
|
fill = R.turuncu;
|
||||||
|
op = 0.7 * nabiz(frame, p.sira * 0.45);
|
||||||
|
} else {
|
||||||
|
const isi = isiHesapla(p.x, p.y);
|
||||||
|
if (isi > 0) {
|
||||||
|
fill = interpolateColors(isi, [0, 1], [R.primary, R.turuncu]);
|
||||||
|
op = p.op + (0.85 - p.op) * isi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rectler.push(
|
||||||
|
<rect key={`${p.x}-${p.y}`} x={x} y={y} width={w} height={h} rx={dot * 0.27} fill={fill} fillOpacity={op * gorunur} />,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// İşaret, fırça yolunun o anki noktasında durur (öncesinde başta, sonrasında sonda)
|
||||||
|
const isaret = (() => {
|
||||||
|
if (fircaBas == null) return null;
|
||||||
|
const p = Math.min(1, Math.max(0, (frame - fircaBas) / fircaSure));
|
||||||
|
return (fircaYol ?? varsayilanYol)(p);
|
||||||
|
})();
|
||||||
|
return (
|
||||||
|
<div style={{ position: "relative", width: genislik, height: yukseklik }}>
|
||||||
|
<svg width={genislik} height={yukseklik} style={{ overflow: "visible" }}>
|
||||||
|
{rectler}
|
||||||
|
</svg>
|
||||||
|
{iz && isaret ? (
|
||||||
|
<div data-iz={iz} style={{ position: "absolute", left: isaret.x * hucre, top: isaret.y * hucre, width: 2, height: 2 }} />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ekranı dolduran piksel alanı — hero dokusu gibi yukarıdan aşağı dökülür. */
|
||||||
|
export function YagmurAlani({
|
||||||
|
baslangic,
|
||||||
|
dokulme,
|
||||||
|
hucre = 40,
|
||||||
|
bosluk,
|
||||||
|
firca,
|
||||||
|
iz,
|
||||||
|
}: {
|
||||||
|
baslangic: number;
|
||||||
|
dokulme: number;
|
||||||
|
hucre?: number;
|
||||||
|
bosluk?: { rx: number; ry: number };
|
||||||
|
/** İmleç yolu: kare → ekran noktası (px). Geçtiği pikseller ısınır. */
|
||||||
|
firca?: { bas: number; sure: number; yol: (p: number) => [number, number] };
|
||||||
|
iz?: string;
|
||||||
|
}) {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { width, height } = useVideoConfig();
|
||||||
|
const sut = Math.ceil(width / hucre);
|
||||||
|
const sat = Math.ceil(height / hucre);
|
||||||
|
const dot = hucre * 0.74;
|
||||||
|
const kareler: ReactNode[] = [];
|
||||||
|
const fircaNokta = (f: number): [number, number] | null => {
|
||||||
|
if (!firca) return null;
|
||||||
|
const p = (f - firca.bas) / firca.sure;
|
||||||
|
if (p < 0 || p > 1) return null;
|
||||||
|
return firca.yol(Easing.inOut(Easing.sin)(p));
|
||||||
|
};
|
||||||
|
const gecmis: ([number, number] | null)[] = Array.from({ length: 48 }, (_, k) => fircaNokta(frame - k));
|
||||||
|
const YARICAP = hucre * 3.2;
|
||||||
|
for (let y = 0; y < sat; y++) {
|
||||||
|
for (let x = 0; x < sut; x++) {
|
||||||
|
const cx = x * hucre + hucre / 2 - width / 2;
|
||||||
|
const cy = y * hucre + hucre / 2 - height / 2;
|
||||||
|
if (bosluk && (cx / bosluk.rx) ** 2 + (cy / bosluk.ry) ** 2 < 1) continue;
|
||||||
|
// Yoğunluk kenarlara doğru artar (hero serpintisi gibi seyrek başlar)
|
||||||
|
if (hash(x, y, 21) > 0.82) continue;
|
||||||
|
const t0 = baslangic + (y / sat) * dokulme + hash(x, y, 5) * 12;
|
||||||
|
const ilerle = interpolate(frame, [t0, t0 + 14], [0, 1], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
easing: belirEgri,
|
||||||
|
});
|
||||||
|
if (ilerle <= 0) continue;
|
||||||
|
const vurgu = hash(x, y, 77) > 0.975;
|
||||||
|
let op = (vurgu ? 0.8 : opaklik(x, y) + 0.04) * Math.min(1, ilerle * 2);
|
||||||
|
const h = dot * (1 + (1 - ilerle) * 1.2);
|
||||||
|
// Isı fırçası: imlecin son 1.6 sn'de değdiği pikseller turuncuya ısınır
|
||||||
|
let isi = 0;
|
||||||
|
if (!vurgu && firca) {
|
||||||
|
const px = x * hucre + hucre / 2;
|
||||||
|
const py = y * hucre + hucre / 2;
|
||||||
|
for (let k = 0; k < gecmis.length; k++) {
|
||||||
|
const b = gecmis[k];
|
||||||
|
if (!b) continue;
|
||||||
|
if ((b[0] - px) ** 2 + (b[1] - py) ** 2 > YARICAP * YARICAP) continue;
|
||||||
|
isi = k < 4.5 ? 1 : Math.pow(1 - Math.min(1, (k - 4.5) / 42), 2.2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (isi > 0) op = op + (0.85 - op) * isi;
|
||||||
|
}
|
||||||
|
kareler.push(
|
||||||
|
<rect
|
||||||
|
key={`${x}-${y}`}
|
||||||
|
x={x * hucre + (hucre - dot) / 2}
|
||||||
|
y={y * hucre + (hucre - dot) / 2 - (1 - ilerle) * hucre * 5 - (h - dot) / 2}
|
||||||
|
width={dot}
|
||||||
|
height={h}
|
||||||
|
rx={dot * 0.27}
|
||||||
|
fill={vurgu ? R.turuncu : isi > 0 ? interpolateColors(isi, [0, 1], [R.primary, R.turuncu]) : R.primary}
|
||||||
|
fillOpacity={op}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const isaret = firca ? firca.yol(Easing.inOut(Easing.sin)(Math.min(1, Math.max(0, (frame - firca.bas) / firca.sure)))) : null;
|
||||||
|
return (
|
||||||
|
<AbsoluteFill>
|
||||||
|
<svg width={width} height={height}>
|
||||||
|
{kareler}
|
||||||
|
</svg>
|
||||||
|
{iz && isaret ? <div data-iz={iz} style={{ position: "absolute", left: isaret[0], top: isaret[1], width: 2, height: 2 }} /> : null}
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
}
|
||||||
102
src/ikonlar.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import type { CSSProperties } from "react";
|
||||||
|
|
||||||
|
// lucide-react yollarının kopyası (sitedeki ikon seti; DESIGN.md §4.7)
|
||||||
|
const YOLLAR: Record<string, string[]> = {
|
||||||
|
ok: ["M5 12h14", "m12 5 7 7-7 7"],
|
||||||
|
kupa: [
|
||||||
|
"M10 14.66v1.626a2 2 0 0 1-.976 1.696A5 5 0 0 0 7 21.978",
|
||||||
|
"M14 14.66v1.626a2 2 0 0 0 .976 1.696A5 5 0 0 1 17 21.978",
|
||||||
|
"M18 9h1.5a1 1 0 0 0 0-5H18",
|
||||||
|
"M4 22h16",
|
||||||
|
"M6 9a6 6 0 0 0 12 0V3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1z",
|
||||||
|
"M6 9H4.5a1 1 0 0 1 0-5H6",
|
||||||
|
],
|
||||||
|
kalkan: [
|
||||||
|
"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z",
|
||||||
|
"m9 12 2 2 4-4",
|
||||||
|
],
|
||||||
|
belge: [
|
||||||
|
"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z",
|
||||||
|
"M14 2v5a1 1 0 0 0 1 1h5",
|
||||||
|
"M10 9H8",
|
||||||
|
"M16 13H8",
|
||||||
|
"M16 17H8",
|
||||||
|
],
|
||||||
|
soru: [
|
||||||
|
"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719",
|
||||||
|
"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3",
|
||||||
|
"M12 17h.01",
|
||||||
|
],
|
||||||
|
trend: ["M16 7h6v6", "m22 7-8.5 8.5-5-5L2 17"],
|
||||||
|
konum: [
|
||||||
|
"M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0",
|
||||||
|
"M15 10a3 3 0 1 1-6 0 3 3 0 0 1 6 0",
|
||||||
|
],
|
||||||
|
devlet: [
|
||||||
|
"M10 18v-7",
|
||||||
|
"M11.119 2.205a2 2 0 0 1 1.762 0l7.84 3.846A.5.5 0 0 1 20.5 7h-17a.5.5 0 0 1-.22-.949z",
|
||||||
|
"M14 18v-7",
|
||||||
|
"M18 18v-7",
|
||||||
|
"M3 22h18",
|
||||||
|
"M6 18v-7",
|
||||||
|
],
|
||||||
|
tik: ["M20 6 9 17l-5-5"],
|
||||||
|
yenile: [
|
||||||
|
"M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",
|
||||||
|
"M3 3v5h5",
|
||||||
|
"M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16",
|
||||||
|
"M16 16h5v5",
|
||||||
|
],
|
||||||
|
gonder: ["M5 12h14", "m12 5 7 7-7 7"],
|
||||||
|
arama: ["m21 21-4.34-4.34", "M19 11a8 8 0 1 1-16 0 8 8 0 0 1 16 0"],
|
||||||
|
kullanici: ["M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2", "M16 7a4 4 0 1 1-8 0 4 4 0 0 1 8 0"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IkonAdi = keyof typeof YOLLAR;
|
||||||
|
|
||||||
|
export function Ikon({
|
||||||
|
ad,
|
||||||
|
boy,
|
||||||
|
renk = "currentColor",
|
||||||
|
kalinlik = 2,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
ad: IkonAdi;
|
||||||
|
boy: number;
|
||||||
|
renk?: string;
|
||||||
|
kalinlik?: number;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={boy}
|
||||||
|
height={boy}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke={renk}
|
||||||
|
strokeWidth={kalinlik}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
style={{ flexShrink: 0, ...style }}
|
||||||
|
>
|
||||||
|
{YOLLAR[ad].map((d) => (
|
||||||
|
<path key={d} d={d} />
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** İmleç (mouse-pointer-2) — dolu, beyaz kenarlı. */
|
||||||
|
export function Imlec({ boy, style }: { boy: number; style?: CSSProperties }) {
|
||||||
|
return (
|
||||||
|
<svg width={boy} height={boy} viewBox="0 0 24 24" style={style}>
|
||||||
|
<path
|
||||||
|
d="M4.037 4.688a.495.495 0 0 1 .651-.651l16 6.5a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063z"
|
||||||
|
fill="#0f172a"
|
||||||
|
stroke="#fff"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
216
src/imlec.tsx
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { AbsoluteFill, Easing, interpolate, interpolateColors, useCurrentFrame, useVideoConfig } from "remotion";
|
||||||
|
import { R, hash } from "./tema";
|
||||||
|
import { vurusCarpani, type Vurus } from "./vurus";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sahne içi imleç. v3'te imleç sürekli ekranda değil: yalnız bir şey
|
||||||
|
* yapılırken (yazı kutusuna tıklama, çip seçme, harita tarama, gönderme)
|
||||||
|
* belirir. Konumları sahnenin kendi koordinatlarında verilir; aynı yol
|
||||||
|
* fonksiyonunu kamera da okur ve imleci takip eder (cursor tracking).
|
||||||
|
*
|
||||||
|
* Geçtiği yerde sitenin ısı fırçası gibi piksel iz bırakır, tıkladığı yerde
|
||||||
|
* halka + piksel patlaması.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type Durak = { f: number; x: number; y: number; g?: number; /** Bir önceki duraktan düz ve sabit hızla (takip edilen yol örnekleri için) */ dogrusal?: boolean };
|
||||||
|
|
||||||
|
const yolEgri = Easing.bezier(0.33, 0, 0.12, 1);
|
||||||
|
|
||||||
|
/** Duraklar arasında el hareketi gibi kavisli yol; `f` kare. */
|
||||||
|
export function imlecKonumu(duraklar: Durak[], f: number): [number, number] {
|
||||||
|
if (duraklar.length === 0) return [0, 0];
|
||||||
|
const j = duraklar.findIndex((d) => d.f > f);
|
||||||
|
if (j === -1) {
|
||||||
|
const s = duraklar[duraklar.length - 1];
|
||||||
|
return [s.x, s.y];
|
||||||
|
}
|
||||||
|
if (j === 0) return [duraklar[0].x, duraklar[0].y];
|
||||||
|
const a = duraklar[j - 1];
|
||||||
|
const b = duraklar[j];
|
||||||
|
if (b.dogrusal) {
|
||||||
|
const p = (f - a.f) / (b.f - a.f);
|
||||||
|
return [a.x + (b.x - a.x) * p, a.y + (b.y - a.y) * p];
|
||||||
|
}
|
||||||
|
const g = b.g ?? 14;
|
||||||
|
const kalkis = Math.max(a.f, b.f - g);
|
||||||
|
if (f <= kalkis) return [a.x, a.y];
|
||||||
|
const p = yolEgri(Math.min(1, (f - kalkis) / (b.f - kalkis)));
|
||||||
|
const mx = b.x - a.x;
|
||||||
|
const my = b.y - a.y;
|
||||||
|
const uz = Math.hypot(mx, my);
|
||||||
|
const kavis = Math.sin(Math.PI * p) * Math.min(90, uz * 0.12);
|
||||||
|
const nx = uz ? -my / uz : 0;
|
||||||
|
const ny = uz ? mx / uz : 0;
|
||||||
|
return [a.x + mx * p + nx * kavis, a.y + my * p + ny * kavis];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SahneImleci({
|
||||||
|
duraklar,
|
||||||
|
tiklar = [],
|
||||||
|
gorunur,
|
||||||
|
boy = 72,
|
||||||
|
}: {
|
||||||
|
duraklar: Durak[];
|
||||||
|
tiklar?: number[];
|
||||||
|
/** [belirme, kaybolma] kareleri */
|
||||||
|
gorunur: [number, number];
|
||||||
|
boy?: number;
|
||||||
|
}) {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { width: W, height: H } = useVideoConfig();
|
||||||
|
const op = interpolate(frame, [gorunur[0], gorunur[0] + 8, gorunur[1] - 8, gorunur[1]], [0, 1, 1, 0], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
});
|
||||||
|
const belir = interpolate(frame, [gorunur[0], gorunur[0] + 10], [0.6, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.back(2)) });
|
||||||
|
const [x, y] = imlecKonumu(duraklar, frame);
|
||||||
|
const sx = Math.sin(frame / 13) * 1.6;
|
||||||
|
const sy = Math.cos(frame / 17) * 1.4;
|
||||||
|
|
||||||
|
let bas = 1;
|
||||||
|
for (const t of tiklar) {
|
||||||
|
const d = frame - t;
|
||||||
|
if (d >= -2 && d <= 2) bas = Math.min(bas, 1 - 0.16 * ((d + 2) / 4));
|
||||||
|
else if (d > 2 && d <= 9) bas = Math.min(bas, 0.84 + 0.16 * ((d - 2) / 7));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Piksel iz: hızla orantılı, ızgaraya oturan kareler (0.7 sn'de soğur)
|
||||||
|
const G = boy * 0.36;
|
||||||
|
const hucreler = new Map<string, number>();
|
||||||
|
if (op > 0) {
|
||||||
|
for (let k = 1; k <= 21; k++) {
|
||||||
|
if (frame - k < gorunur[0]) break;
|
||||||
|
const [ax, ay] = imlecKonumu(duraklar, frame - k);
|
||||||
|
const [bx, by] = imlecKonumu(duraklar, frame - k - 1);
|
||||||
|
const v = Math.hypot(ax - bx, ay - by);
|
||||||
|
if (v < 5) continue;
|
||||||
|
const isi = (1 - k / 21) * Math.min(1, v / 24);
|
||||||
|
const adim = Math.max(1, Math.ceil(v / G));
|
||||||
|
for (let s = 0; s < adim; s++) {
|
||||||
|
const qx = ax + ((bx - ax) * s) / adim;
|
||||||
|
const qy = ay + ((by - ay) * s) / adim;
|
||||||
|
const an = `${Math.floor(qx / G)},${Math.floor(qy / G)}`;
|
||||||
|
hucreler.set(an, Math.max(hucreler.get(an) ?? 0, isi));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const dot = G * 0.74;
|
||||||
|
const iz: ReactNode[] = [];
|
||||||
|
hucreler.forEach((isi, an) => {
|
||||||
|
const [cx, cy] = an.split(",").map(Number);
|
||||||
|
iz.push(
|
||||||
|
<rect key={an} x={cx * G + (G - dot) / 2} y={cy * G + (G - dot) / 2} width={dot} height={dot} rx={dot * 0.27} fill={interpolateColors(isi, [0, 1], [R.primary, R.turuncu])} fillOpacity={(0.2 + 0.65 * isi) * op} />,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const patlama: ReactNode[] = [];
|
||||||
|
for (const t of tiklar) {
|
||||||
|
const d = frame - t;
|
||||||
|
if (d < 0 || d > 24) continue;
|
||||||
|
const [cx, cy] = imlecKonumu(duraklar, t);
|
||||||
|
const p = Easing.out(Easing.cubic)(Math.min(1, d / 18));
|
||||||
|
patlama.push(<circle key={`h${t}`} cx={cx} cy={cy} r={12 + boy * 1.1 * p} fill="none" stroke={R.turuncu} strokeWidth={5 * (1 - p)} strokeOpacity={0.8 * (1 - p)} />);
|
||||||
|
const Gp = boy * 0.2;
|
||||||
|
for (let n = 0; n < 10; n++) {
|
||||||
|
const aci = (n / 10) * Math.PI * 2 + hash(t, n, 3) * 0.6;
|
||||||
|
const uz = (18 + boy * 1.3 * (0.6 + 0.4 * hash(t, n, 5))) * p;
|
||||||
|
const qx = Math.round((cx + Math.cos(aci) * uz) / Gp) * Gp;
|
||||||
|
const qy = Math.round((cy + Math.sin(aci) * uz) / Gp) * Gp;
|
||||||
|
const b = boy * 0.17 * (1 - d / 26);
|
||||||
|
patlama.push(<rect key={`p${t}-${n}`} x={qx - b / 2} y={qy - b / 2} width={b} height={b} rx={b * 0.27} fill={[R.turuncu, R.primary, R.logoSeftali][n % 3]} fillOpacity={1 - d / 24} />);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tip = (4.04 / 24) * boy;
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ pointerEvents: "none" }}>
|
||||||
|
<svg width={W} height={H} style={{ position: "absolute", inset: 0, overflow: "visible" }}>
|
||||||
|
{iz}
|
||||||
|
{patlama}
|
||||||
|
</svg>
|
||||||
|
{op > 0 ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: x + sx - tip,
|
||||||
|
top: y + sy - tip,
|
||||||
|
width: boy,
|
||||||
|
height: boy,
|
||||||
|
opacity: op,
|
||||||
|
transform: `scale(${bas * belir})`,
|
||||||
|
transformOrigin: `${(4.04 / 24) * 100}% ${(4.04 / 24) * 100}%`,
|
||||||
|
filter: "drop-shadow(0 8px 14px rgba(15,23,42,0.28))",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg width={boy} height={boy} viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
d="M4.037 4.688a.495.495 0 0 1 .651-.651l16 6.5a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063z"
|
||||||
|
fill={R.slate900}
|
||||||
|
stroke="#fff"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* İmleci takip eden kamera */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
export type TakipAnahtar = { f: number; s: number; t: number };
|
||||||
|
|
||||||
|
function ara(list: TakipAnahtar[], f: number, k: "s" | "t") {
|
||||||
|
if (f <= list[0].f) return list[0][k];
|
||||||
|
for (let i = 0; i < list.length - 1; i++) {
|
||||||
|
const a = list[i];
|
||||||
|
const b = list[i + 1];
|
||||||
|
if (f <= b.f) return interpolate(f, [a.f, b.f], [a[k], b[k]], { easing: Easing.bezier(0.65, 0, 0.35, 1) });
|
||||||
|
}
|
||||||
|
return list[list.length - 1][k];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kamera `t` oranında imlece kilitlenir (0 = kadraj ortası, 1 = imleç tam
|
||||||
|
* ortada) ve `s` kadar yaklaşır. İmleç konumu birkaç karelik gecikmeyle
|
||||||
|
* yumuşatılır: kamera operatörü eli gibi hafif geriden gelir.
|
||||||
|
*/
|
||||||
|
export function TakipKamera({
|
||||||
|
nokta,
|
||||||
|
anahtarlar,
|
||||||
|
vuruslar,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
nokta: (f: number) => [number, number];
|
||||||
|
anahtarlar: TakipAnahtar[];
|
||||||
|
vuruslar?: Vurus[];
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { width: W, height: H } = useVideoConfig();
|
||||||
|
const s = ara(anahtarlar, frame, "s") * vurusCarpani(frame, vuruslar);
|
||||||
|
const t = ara(anahtarlar, frame, "t");
|
||||||
|
let px = 0;
|
||||||
|
let py = 0;
|
||||||
|
let ag = 0;
|
||||||
|
for (let k = 0; k < 8; k++) {
|
||||||
|
const w = 1 - k / 8;
|
||||||
|
const [qx, qy] = nokta(frame - k);
|
||||||
|
px += qx * w;
|
||||||
|
py += qy * w;
|
||||||
|
ag += w;
|
||||||
|
}
|
||||||
|
px /= ag;
|
||||||
|
py /= ag;
|
||||||
|
const x = (px - W / 2) * t;
|
||||||
|
const y = (py - H / 2) * t;
|
||||||
|
const sx = Math.sin(frame / 47) * 4 + Math.sin(frame / 19) * 1.2;
|
||||||
|
const sy = Math.cos(frame / 53) * 3;
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ transform: `scale(${s}) translate(${-x + sx}px, ${-y + sy}px)`, transformOrigin: "50% 50%" }}>{children}</AbsoluteFill>
|
||||||
|
);
|
||||||
|
}
|
||||||
4
src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { registerRoot } from "remotion";
|
||||||
|
import { Root } from "./Root";
|
||||||
|
|
||||||
|
registerRoot(Root);
|
||||||
397
src/kinetik.tsx
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
import { measureText } from "@remotion/layout-utils";
|
||||||
|
import type { CSSProperties, ReactNode } from "react";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { Easing, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
|
||||||
|
import { getPathRideFade, getPathRidePoint, samplePath } from "./remocn/path-ride";
|
||||||
|
import { FONT, R } from "./tema";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* remocn.dev kinetik yazı efektlerinin KolayTercih uyarlamaları. Hareket
|
||||||
|
* matematiği remocn bileşenlerinden (src/remocn/*) alındı; görünüm sitenin
|
||||||
|
* diline çevrildi:
|
||||||
|
* - yumuşak degrade yerine logodaki üç karenin renkleriyle **basamaklı
|
||||||
|
* (piksel) bantlar** — mavi #3b82f6, şeftali #fdc7a7, turuncu #ff5a00;
|
||||||
|
* - açılma kenarları kare kare ilerler (kuantize), düzgün kaymaz;
|
||||||
|
* - yazı tipleri Cal Sans / Bricolage, zemin slate-50.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const PIKSEL_BANT = (aci = 100, bas = 38) =>
|
||||||
|
`linear-gradient(${aci}deg, transparent ${bas}%, ${R.logoSeftali} ${bas}% ${bas + 6}%, ${R.logoTuruncu} ${bas + 6}% ${bas + 12}%, ${R.logoMavi} ${bas + 12}% ${bas + 18}%, transparent ${bas + 18}%)`;
|
||||||
|
|
||||||
|
/** 0..1 ilerlemeyi n basamağa böler: kenar pürüzsüz değil, piksel piksel ilerler. */
|
||||||
|
const basamak = (p: number, n: number) => Math.floor(p * n) / n;
|
||||||
|
|
||||||
|
const clamp = { extrapolateLeft: "clamp" as const, extrapolateRight: "clamp" as const };
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Sheen Slide In — remocn/sheen-slide-in */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/**
|
||||||
|
* Başlık sağdan üstel bir oturuşla kayar (remocn'un travelEm eğrisi), üstünden
|
||||||
|
* logodaki üç renkten oluşan piksel bir parlama bandı geçer.
|
||||||
|
*/
|
||||||
|
export function PikselSheen({
|
||||||
|
children,
|
||||||
|
gecikme = 0,
|
||||||
|
fontSize = 96,
|
||||||
|
renk = R.slate900,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
gecikme?: number;
|
||||||
|
fontSize?: number;
|
||||||
|
renk?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}) {
|
||||||
|
const frame = Math.max(0, useCurrentFrame() - gecikme);
|
||||||
|
const oturus = 1.8 * Math.exp(-frame / 2.5) + 0.28 * Math.exp(-frame / 12); // remocn SETTLE/CREEP
|
||||||
|
const opak = interpolate(frame, [0, 4], [0, 1], clamp);
|
||||||
|
const bantOpak = interpolate(frame, [0, 4], [0, 1], clamp) * interpolate(frame, [44, 54], [1, 0], clamp);
|
||||||
|
const bantKonum = basamak(interpolate(frame, [0, 44], [0, 1], { ...clamp, easing: Easing.bezier(0.42, 0, 0.58, 1) }), 14);
|
||||||
|
const yazi: CSSProperties = { fontFamily: FONT.baslik, fontWeight: 700, fontSize, lineHeight: 1.08, letterSpacing: "0.005em", whiteSpace: "nowrap" };
|
||||||
|
return (
|
||||||
|
<span style={{ position: "relative", display: "inline-block", opacity: opak, transform: `translateX(${fontSize * oturus}px)`, ...style }}>
|
||||||
|
<span style={{ ...yazi, color: renk }}>{children}</span>
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
style={{
|
||||||
|
...yazi,
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
color: "transparent",
|
||||||
|
backgroundImage: PIKSEL_BANT(),
|
||||||
|
backgroundSize: "260% 100%",
|
||||||
|
backgroundPosition: `${160 - bantKonum * 220}% 50%`,
|
||||||
|
WebkitBackgroundClip: "text",
|
||||||
|
backgroundClip: "text",
|
||||||
|
opacity: bantOpak,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Tracking In — remocn/tracking-in */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/** Harf aralığı açık başlar, yaylanarak kapanır; bulanıklık çözülür. */
|
||||||
|
export function IzAc({
|
||||||
|
children,
|
||||||
|
gecikme = 0,
|
||||||
|
fontSize = 200,
|
||||||
|
renk = R.slate900,
|
||||||
|
baslangicAralik = 0.5,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
gecikme?: number;
|
||||||
|
fontSize?: number;
|
||||||
|
renk?: string;
|
||||||
|
baslangicAralik?: number;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}) {
|
||||||
|
const frame = useCurrentFrame() - gecikme;
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
const t = spring({ frame, fps, config: { damping: 18, stiffness: 90 } });
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
fontFamily: FONT.baslik,
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize,
|
||||||
|
lineHeight: 1,
|
||||||
|
color: renk,
|
||||||
|
letterSpacing: `${interpolate(t, [0, 1], [baslangicAralik, -0.01])}em`,
|
||||||
|
filter: `blur(${interpolate(t, [0, 1], [12, 0])}px)`,
|
||||||
|
opacity: interpolate(frame, [0, 12], [0, 1], clamp),
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Gradient Scale Cut Text — remocn/gradient-scale-cut-text */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/**
|
||||||
|
* Dev bir kelime, piksel basamaklarla açılan bir maskeyle ve logo renklerinde
|
||||||
|
* sert geçişli bir bantla ekranı süpürür; `kesme` karesinde sert bir kesmeyle
|
||||||
|
* kompakt içeriğe (ör. logo) döner, kompakt olan bulanıklıktan oturur.
|
||||||
|
*/
|
||||||
|
export function PikselDevKesme({
|
||||||
|
metin,
|
||||||
|
devBoy,
|
||||||
|
kesme = 14,
|
||||||
|
gecikme = 0,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
metin: string;
|
||||||
|
devBoy: number;
|
||||||
|
kesme?: number;
|
||||||
|
gecikme?: number;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
const frame = useCurrentFrame() - gecikme;
|
||||||
|
const { width } = useVideoConfig();
|
||||||
|
const acilma = basamak(interpolate(frame, [0, 30], [0.02, 1], { ...clamp, easing: Easing.bezier(0.22, 0.74, 0.24, 1) }), 16);
|
||||||
|
const kenar = acilma * 104;
|
||||||
|
const maske = `linear-gradient(90deg, #000 0%, #000 ${kenar - 2}%, transparent ${kenar}%)`;
|
||||||
|
const dev = interpolate(frame, [0, kesme - 1], [0, 1], { ...clamp, easing: Easing.out(Easing.cubic) });
|
||||||
|
const otur = interpolate(frame, [kesme, kesme + 8], [0, 1], { ...clamp, easing: Easing.out(Easing.cubic) });
|
||||||
|
const katman: CSSProperties = { fontFamily: FONT.vurgu, fontWeight: 600, letterSpacing: "-0.05em", fontSize: devBoy, lineHeight: 1, whiteSpace: "nowrap" };
|
||||||
|
if (frame < kesme) {
|
||||||
|
return (
|
||||||
|
<div style={{ position: "absolute", top: "50%", left: width * 0.52 - dev * width * 0.35, transform: "translateY(-54%)" }}>
|
||||||
|
<div style={{ position: "relative" }}>
|
||||||
|
<span style={{ ...katman, color: R.slate200 }}>{metin}</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
...katman,
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
color: "transparent",
|
||||||
|
backgroundImage: `linear-gradient(90deg, ${R.logoMavi} 0% 34%, ${R.logoSeftali} 34% 50%, ${R.logoTuruncu} 50% 100%)`,
|
||||||
|
WebkitBackgroundClip: "text",
|
||||||
|
backgroundClip: "text",
|
||||||
|
WebkitMaskImage: maske,
|
||||||
|
maskImage: maske,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{metin}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
transform: `translateX(${(1 - otur) * 140}px)`,
|
||||||
|
filter: `blur(${(1 - otur) * 12}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Outline Fill Track Text — remocn/outline-fill-track-text */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/**
|
||||||
|
* Öndeki kelime maskeden yükselir; ikisi birlikte tek bir kaydırma çizgisinde
|
||||||
|
* ilerlerken değer önce kontur olarak gelir, sonra soldan sağa **piksel
|
||||||
|
* basamaklarla** dolar.
|
||||||
|
*/
|
||||||
|
export function PikselKonturDolgu({
|
||||||
|
on,
|
||||||
|
deger,
|
||||||
|
fontSize,
|
||||||
|
gecikme = 0,
|
||||||
|
genislik,
|
||||||
|
dolguRenk = R.primary,
|
||||||
|
}: {
|
||||||
|
on: string;
|
||||||
|
deger: string;
|
||||||
|
fontSize: number;
|
||||||
|
gecikme?: number;
|
||||||
|
genislik: number;
|
||||||
|
dolguRenk?: string;
|
||||||
|
}) {
|
||||||
|
const frame = useCurrentFrame() - gecikme;
|
||||||
|
const olc = (m: string) => measureText({ text: m, fontFamily: "Cal Sans", fontSize, fontWeight: "700" }).width;
|
||||||
|
const onW = useMemo(() => olc(on), [on, fontSize]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
const degerW = useMemo(() => olc(deger), [deger, fontSize]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
const bosluk = fontSize * 0.35;
|
||||||
|
const giris = interpolate(frame, [0, 16], [0, 1], { ...clamp, easing: Easing.bezier(0.16, 0.84, 0.24, 1) });
|
||||||
|
const iz = interpolate(frame, [16, 60], [0, 1], { ...clamp, easing: Easing.bezier(0.42, 0, 0.58, 1) });
|
||||||
|
const dolgu = basamak(interpolate(frame, [26, 62], [0, 1], clamp), 12);
|
||||||
|
const toplam = onW + bosluk + degerW;
|
||||||
|
// Başta öndeki kelime ortada; sonda ikisi birlikte ortalanır
|
||||||
|
const basX = genislik / 2 - onW / 2;
|
||||||
|
const sonX = genislik / 2 - toplam / 2;
|
||||||
|
const x = basX + (sonX - basX) * iz;
|
||||||
|
const kenar = giris * 112 - 8;
|
||||||
|
const maske = `linear-gradient(180deg, #000 0%, #000 ${Math.max(0, kenar - 10)}%, transparent ${Math.min(100, kenar + 10)}%)`;
|
||||||
|
const yazi: CSSProperties = { fontFamily: FONT.baslik, fontWeight: 700, fontSize, lineHeight: 1, whiteSpace: "nowrap", position: "absolute", top: 0 };
|
||||||
|
return (
|
||||||
|
<div style={{ position: "relative", width: genislik, height: fontSize * 1.1 }}>
|
||||||
|
<span style={{ ...yazi, left: x, color: R.slate900, transform: `translateY(${(1 - giris) * fontSize * 0.7}px)`, WebkitMaskImage: maske, maskImage: maske }}>{on}</span>
|
||||||
|
<span style={{ ...yazi, left: x + onW + bosluk, visibility: frame < 22 ? "hidden" : "visible" }}>
|
||||||
|
<span style={{ color: "transparent", WebkitTextStroke: `${Math.max(2, fontSize / 60)}px ${R.slate300}` }}>{deger}</span>
|
||||||
|
<span style={{ position: "absolute", inset: 0, color: dolguRenk, clipPath: `inset(0 ${100 - dolgu * 100}% 0 0)` }}>{deger}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Perspective Marquee — remocn/perspective-marquee */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/**
|
||||||
|
* 3B eğik, sonsuz akan şerit; ufka doğru kayan öğeler alan derinliğiyle
|
||||||
|
* bulanıklaşır. Öğelerin arasında logodaki gibi üç kare durur.
|
||||||
|
*/
|
||||||
|
export function PikselMarquee({
|
||||||
|
ogeler,
|
||||||
|
fontSize = 84,
|
||||||
|
hiz = 3,
|
||||||
|
donY = -26,
|
||||||
|
donX = 8,
|
||||||
|
renk = R.slate900,
|
||||||
|
soluk = R.slate50,
|
||||||
|
ters = false,
|
||||||
|
kaydir = 0,
|
||||||
|
}: {
|
||||||
|
ogeler: string[];
|
||||||
|
fontSize?: number;
|
||||||
|
hiz?: number;
|
||||||
|
donY?: number;
|
||||||
|
donX?: number;
|
||||||
|
renk?: string;
|
||||||
|
soluk?: string;
|
||||||
|
ters?: boolean;
|
||||||
|
kaydir?: number;
|
||||||
|
}) {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { width } = useVideoConfig();
|
||||||
|
const ara = fontSize * 1.4;
|
||||||
|
const genislikler = useMemo(
|
||||||
|
() => ogeler.map((o) => measureText({ text: o, fontFamily: "Cal Sans", fontSize, fontWeight: "700" }).width + ara),
|
||||||
|
[ogeler, fontSize, ara],
|
||||||
|
);
|
||||||
|
const set = genislikler.reduce((a, b) => a + b, 0);
|
||||||
|
const kayma = (((frame * hiz + kaydir) % set) + set) % set;
|
||||||
|
const offset = ters ? -set + kayma : -kayma;
|
||||||
|
const dizi = [...ogeler, ...ogeler, ...ogeler];
|
||||||
|
let x = offset;
|
||||||
|
return (
|
||||||
|
<div style={{ position: "relative", width: "100%", perspective: 1400, overflow: "visible" }}>
|
||||||
|
<div style={{ transform: `rotateX(${donX}deg) rotateY(${donY}deg)`, transformStyle: "preserve-3d", whiteSpace: "nowrap", display: "flex" }}>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", transform: `translateX(${offset}px)` }}>
|
||||||
|
{dizi.map((o, i) => {
|
||||||
|
const w = genislikler[i % ogeler.length];
|
||||||
|
const merkez = x + w / 2;
|
||||||
|
x += w;
|
||||||
|
const uzak = Math.min(1, Math.abs((merkez - width / 2) / (width / 2)));
|
||||||
|
return (
|
||||||
|
<span key={i} style={{ display: "inline-flex", alignItems: "center", width: w, filter: `blur(${uzak * 5}px)`, opacity: 1 - uzak * 0.45 }}>
|
||||||
|
<span style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize, color: renk, letterSpacing: "-0.01em" }}>{o}</span>
|
||||||
|
<span style={{ display: "inline-flex", gap: fontSize * 0.08, marginLeft: ara * 0.3 }}>
|
||||||
|
{[R.logoMavi, R.logoSeftali, R.logoTuruncu].map((c) => (
|
||||||
|
<span key={c} style={{ width: fontSize * 0.16, height: fontSize * 0.16, borderRadius: fontSize * 0.04, background: c }} />
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ position: "absolute", inset: "-20% 0", pointerEvents: "none", background: `linear-gradient(90deg, ${soluk} 0%, transparent 16%, transparent 84%, ${soluk} 100%)` }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Path Ride — remocn/path-ride */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/**
|
||||||
|
* Bir eğri kendini piksel noktalarla çizer, yazı başının arkasından eğriye
|
||||||
|
* binerek gelir; sonra eğri gerilip düz bir taban çizgisine döner. Noktaların
|
||||||
|
* rengi yol boyunca risk diline göre değişir (hayal → dengeli → güvenli);
|
||||||
|
* uçlarda metin etiketi durur, renk tek başına anlam taşımaz.
|
||||||
|
*/
|
||||||
|
export function PikselYolSurus({
|
||||||
|
metin,
|
||||||
|
yol,
|
||||||
|
fontSize,
|
||||||
|
genislik,
|
||||||
|
yukseklik,
|
||||||
|
gecikme = 0,
|
||||||
|
taban,
|
||||||
|
}: {
|
||||||
|
metin: string;
|
||||||
|
/** SVG yol verisi, kabın px koordinatlarında */
|
||||||
|
yol: string;
|
||||||
|
fontSize: number;
|
||||||
|
genislik: number;
|
||||||
|
yukseklik: number;
|
||||||
|
gecikme?: number;
|
||||||
|
/** Düzleşince yazının oturacağı taban çizgisi (y, px) */
|
||||||
|
taban: number;
|
||||||
|
}) {
|
||||||
|
const frame = useCurrentFrame() - gecikme;
|
||||||
|
const ornek = useMemo(() => samplePath(yol, 400), [yol]);
|
||||||
|
const harfler = Array.from(metin);
|
||||||
|
const ilerleme = harfler.map((c) => measureText({ text: c, fontFamily: "Cal Sans", fontSize, fontWeight: "700" }).width);
|
||||||
|
// remocn zamanlaması: çiz 0–40, düzleş 50–68, sil 52–70
|
||||||
|
const ease = Easing.inOut(Easing.cubic);
|
||||||
|
const L = ornek.length;
|
||||||
|
const bas = interpolate(frame, [0, 40], [0, L], { ...clamp, easing: ease });
|
||||||
|
const kuyruk = interpolate(frame, [52, 70], [0, L], { ...clamp, easing: ease });
|
||||||
|
const k = interpolate(frame, [50, 68], [0, 1], { ...clamp, easing: ease });
|
||||||
|
let acc = 0;
|
||||||
|
const merkezler = ilerleme.map((a) => {
|
||||||
|
const c = acc + a / 2;
|
||||||
|
acc += a;
|
||||||
|
return c;
|
||||||
|
});
|
||||||
|
const yaziW = acc;
|
||||||
|
const ONDE = 0.3 * fontSize;
|
||||||
|
const duz = { start: L - ONDE - yaziW, left: genislik / 2 - yaziW / 2, baseline: taban };
|
||||||
|
const RENKLER = [R.riskli, R.azRiskli, R.guvenli];
|
||||||
|
const noktalar: ReactNode[] = [];
|
||||||
|
const ADIM = fontSize * 0.28;
|
||||||
|
for (let s = kuyruk; s <= bas; s += ADIM) {
|
||||||
|
const p = getPathRidePoint(ornek, s, k, duz);
|
||||||
|
const u = s / L;
|
||||||
|
const renk = RENKLER[Math.min(2, Math.floor(u * 3))];
|
||||||
|
const boy = fontSize * 0.16;
|
||||||
|
noktalar.push(<rect key={s} x={p.x - boy / 2} y={p.y - boy / 2} width={boy} height={boy} rx={boy * 0.27} fill={renk} />);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div style={{ position: "relative", width: genislik, height: yukseklik }}>
|
||||||
|
<svg width={genislik} height={yukseklik} style={{ position: "absolute", inset: 0, overflow: "visible" }}>
|
||||||
|
{noktalar}
|
||||||
|
</svg>
|
||||||
|
{harfler.map((c, j) => {
|
||||||
|
const arc = bas - ONDE - (yaziW - merkezler[j]);
|
||||||
|
const fade = getPathRideFade(arc, fontSize);
|
||||||
|
if (fade <= 0) return null;
|
||||||
|
const p = getPathRidePoint(ornek, arc, k, duz, fontSize * 0.12);
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={j}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
fontFamily: FONT.baslik,
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize,
|
||||||
|
lineHeight: 1,
|
||||||
|
color: R.slate900,
|
||||||
|
whiteSpace: "pre",
|
||||||
|
opacity: fade,
|
||||||
|
transformOrigin: "0 0",
|
||||||
|
transform: `translate(${p.x}px, ${p.y}px) rotate(${p.angle}rad) translate(-50%, -82%)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{c}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
125
src/logo.tsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { Easing, useCurrentFrame } from "remotion";
|
||||||
|
import { FPS, R, zipla } from "./tema";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sitedeki AnimasyonluMark'ın (src/components/animasyonlu-mark.tsx) kare
|
||||||
|
* kare karşılığı. Motion'ın keyframe dizileri burada zaman → değer
|
||||||
|
* fonksiyonuna çevrildi; süreler, duraklar ve eğriler birebir aynı.
|
||||||
|
*
|
||||||
|
* - "rota": her kare sıradakinin yerine süzülür (mavi → şeftalinin yeri,
|
||||||
|
* şeftali → turuncunun, turuncu → mavinin), yolun ortasında 0.84'e küçülüp
|
||||||
|
* 0.6'ya solar; kalkışlar 0.2 sn kademeli, tam tur 3.6 sn.
|
||||||
|
* - "nabiz": kareler yerinde, 120° faz farkıyla 0.82'ye küçülüp büyür (1.8 sn).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const POZISYONLAR = [
|
||||||
|
[27, 56],
|
||||||
|
[91, 29],
|
||||||
|
[91, 95],
|
||||||
|
] as const;
|
||||||
|
const KARELER = [
|
||||||
|
{ boyut: 57, renk: R.logoMavi, baslangic: 0 },
|
||||||
|
{ boyut: 56, renk: R.logoSeftali, baslangic: 1 },
|
||||||
|
{ boyut: 56, renk: R.logoTuruncu, baslangic: 2 },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const ROTA_SURE = 3.6;
|
||||||
|
const ROTA_GECIKME_ADIMI = 0.2;
|
||||||
|
const KONUM_ZAMANLARI = [0, 0.25, 1 / 3, 0.583, 2 / 3, 0.917, 1];
|
||||||
|
const NABIZ_ZAMANLARI = [0, 0.125, 0.25, 1 / 3, 0.458, 0.583, 2 / 3, 0.792, 0.917, 1];
|
||||||
|
const NABIZ_OLCEK = [1, 0.84, 1, 1, 0.84, 1, 1, 0.84, 1, 1];
|
||||||
|
const NABIZ_OPAKLIK = [1, 0.6, 1, 1, 0.6, 1, 1, 0.6, 1, 1];
|
||||||
|
|
||||||
|
const easeInOut = Easing.bezier(0.42, 0, 0.58, 1); // Motion "easeInOut"
|
||||||
|
const sine = Easing.bezier(0.37, 0, 0.63, 1);
|
||||||
|
|
||||||
|
/** Motion keyframe dizisi: segment başına easing ile ara değer. */
|
||||||
|
function kf(p: number, zamanlar: number[], degerler: number[], ease = easeInOut) {
|
||||||
|
if (p <= 0) return degerler[0];
|
||||||
|
if (p >= 1) return degerler[degerler.length - 1];
|
||||||
|
let i = 0;
|
||||||
|
while (i < zamanlar.length - 2 && p > zamanlar[i + 1]) i++;
|
||||||
|
const t = (p - zamanlar[i]) / (zamanlar[i + 1] - zamanlar[i] || 1);
|
||||||
|
return degerler[i] + (degerler[i + 1] - degerler[i]) * ease(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotaDegerleri(baslangic: number, eksen: 0 | 1) {
|
||||||
|
const [p0, p1, p2] = [0, 1, 2].map((adim) => POZISYONLAR[(baslangic + adim) % 3][eksen]);
|
||||||
|
return [p0, p1, p1, p2, p2, p0, p0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogoMark({
|
||||||
|
boy,
|
||||||
|
giris = 0,
|
||||||
|
beyaz = false,
|
||||||
|
varyant = "rota",
|
||||||
|
rotaBas,
|
||||||
|
rotaTur = 1,
|
||||||
|
turuncu = false,
|
||||||
|
girisYok = false,
|
||||||
|
}: {
|
||||||
|
boy: number;
|
||||||
|
/** Karelerin sahneye iniş karesi */
|
||||||
|
giris?: number;
|
||||||
|
/** Mavi zeminde mavi kare beyaza döner */
|
||||||
|
beyaz?: boolean;
|
||||||
|
varyant?: "rota" | "nabiz" | "duruk";
|
||||||
|
/** Rota koreografisinin başladığı kare; verilmezse iniş bitince başlar */
|
||||||
|
rotaBas?: number;
|
||||||
|
/** Kaç tam tur dönsün; sonra kareler yerinde durur */
|
||||||
|
rotaTur?: number;
|
||||||
|
/** Danışman başlığındaki gibi tüm kareler turuncu */
|
||||||
|
turuncu?: boolean;
|
||||||
|
girisYok?: boolean;
|
||||||
|
}) {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const bas = rotaBas ?? giris + 22;
|
||||||
|
return (
|
||||||
|
<svg width={boy} height={boy} viewBox="0 0 180 180" style={{ overflow: "visible" }}>
|
||||||
|
{KARELER.map((k, i) => {
|
||||||
|
// İniş: sitede yok; sahneye giriş için kare kendi yerinde yaylanarak belirir
|
||||||
|
const inis = girisYok ? 1 : zipla(frame, giris + i * 5);
|
||||||
|
let [x, y] = POZISYONLAR[k.baslangic] as unknown as [number, number];
|
||||||
|
let olcek = 1;
|
||||||
|
let opak = 1;
|
||||||
|
const t = (frame - bas) / FPS;
|
||||||
|
if (varyant === "rota") {
|
||||||
|
const gecikme = i * ROTA_GECIKME_ADIMI;
|
||||||
|
const yerel = t - gecikme;
|
||||||
|
if (yerel > 0 && yerel < ROTA_SURE * rotaTur) {
|
||||||
|
const p = (yerel % ROTA_SURE) / ROTA_SURE;
|
||||||
|
x = kf(p, KONUM_ZAMANLARI, rotaDegerleri(k.baslangic, 0));
|
||||||
|
y = kf(p, KONUM_ZAMANLARI, rotaDegerleri(k.baslangic, 1));
|
||||||
|
olcek = kf(p, NABIZ_ZAMANLARI, NABIZ_OLCEK);
|
||||||
|
opak = kf(p, NABIZ_ZAMANLARI, NABIZ_OPAKLIK);
|
||||||
|
}
|
||||||
|
} else if (varyant === "nabiz") {
|
||||||
|
const yerel = frame / FPS - [0, 0.6, 1.2][i];
|
||||||
|
if (yerel > 0) {
|
||||||
|
const p = (yerel % 1.8) / 1.8;
|
||||||
|
olcek = kf(p, [0, 0.5, 1], [1, 0.82, 1], sine);
|
||||||
|
opak = kf(p, [0, 0.5, 1], [1, 0.55, 1], sine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const renk = turuncu ? R.turuncu : beyaz && i === 0 ? R.beyaz : k.renk;
|
||||||
|
return (
|
||||||
|
<rect
|
||||||
|
key={i}
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
width={k.boyut}
|
||||||
|
height={k.boyut}
|
||||||
|
rx={13}
|
||||||
|
fill={renk}
|
||||||
|
opacity={opak}
|
||||||
|
style={{
|
||||||
|
transformBox: "fill-box",
|
||||||
|
transformOrigin: "center",
|
||||||
|
transform: `scale(${Math.max(0, inis) * olcek})`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
190
src/ortak.tsx
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import type { CSSProperties, ReactNode } from "react";
|
||||||
|
import { AbsoluteFill, useCurrentFrame, useVideoConfig } from "remotion";
|
||||||
|
import { Ikon } from "./ikonlar";
|
||||||
|
import { Eyebrow, KelimeKelime } from "./piksel";
|
||||||
|
import { FONT, R, yay } from "./tema";
|
||||||
|
|
||||||
|
export const useDuzen = () => {
|
||||||
|
const { width, height } = useVideoConfig();
|
||||||
|
return { dikey: height > width, w: width, h: height };
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Sahne zemini: ana sayfanın slate-50'si. */
|
||||||
|
export function Sahne({
|
||||||
|
children,
|
||||||
|
zemin = R.slate50,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
zemin?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<AbsoluteFill
|
||||||
|
style={{
|
||||||
|
background: zemin,
|
||||||
|
fontFamily: FONT.govde,
|
||||||
|
color: R.slate900,
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bölüm başlık bloğu (DESIGN.md §5.1): rozet → h2 → giriş paragrafı. */
|
||||||
|
export function BaslikBlogu({
|
||||||
|
rozet,
|
||||||
|
baslik,
|
||||||
|
aciklama,
|
||||||
|
vurgu,
|
||||||
|
hiza = "center",
|
||||||
|
gecikme = 0,
|
||||||
|
genis = false,
|
||||||
|
}: {
|
||||||
|
rozet: string;
|
||||||
|
baslik: string;
|
||||||
|
aciklama?: string;
|
||||||
|
vurgu?: string[];
|
||||||
|
hiza?: "center" | "left";
|
||||||
|
gecikme?: number;
|
||||||
|
/** Başlık tek satıra sığsın diye geniş sütun (yatayda dar sahneler için) */
|
||||||
|
genis?: boolean;
|
||||||
|
}) {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey } = useDuzen();
|
||||||
|
const r = yay(frame, gecikme, 16);
|
||||||
|
const p = yay(frame, gecikme + 16, 20);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: hiza === "center" ? "center" : "flex-start",
|
||||||
|
textAlign: hiza,
|
||||||
|
gap: 36,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ opacity: r, transform: `scale(${0.9 + 0.1 * r})` }}>
|
||||||
|
<Eyebrow olcek={dikey ? 2.6 : 2.3}>{rozet}</Eyebrow>
|
||||||
|
</div>
|
||||||
|
<h2
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontFamily: FONT.baslik,
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: dikey ? 92 : 88,
|
||||||
|
lineHeight: 1.08,
|
||||||
|
letterSpacing: "0.01em",
|
||||||
|
color: R.slate900,
|
||||||
|
maxWidth: dikey ? 940 : genis ? 1600 : 900,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<KelimeKelime metin={baslik} gecikme={gecikme + 4} vurgu={vurgu} />
|
||||||
|
</h2>
|
||||||
|
{aciklama ? (
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontSize: dikey ? 40 : 36,
|
||||||
|
lineHeight: 1.45,
|
||||||
|
color: R.slate600,
|
||||||
|
maxWidth: dikey ? 900 : 760,
|
||||||
|
opacity: p,
|
||||||
|
transform: `translateY(${(1 - p) * 20}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{aciklama}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sade kutu: rounded-2xl border border-slate-200 bg-white (×2.2). */
|
||||||
|
export function Kutu({ children, style }: { children: ReactNode; style?: CSSProperties }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: R.beyaz,
|
||||||
|
border: `3px solid ${R.slate200}`,
|
||||||
|
borderRadius: 36,
|
||||||
|
boxShadow: "0 2px 6px rgba(15,23,42,0.05)",
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Turuncu CTA — metin önce, ArrowRight sonda; Sparkles yok (AGENTS.md). */
|
||||||
|
export function TuruncuCta({
|
||||||
|
metin,
|
||||||
|
boy = 1,
|
||||||
|
basili = 0,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
metin: string;
|
||||||
|
boy?: number;
|
||||||
|
/** 0→1: basılma anı (active:scale-[0.97] + hover:bg-orange-600) */
|
||||||
|
basili?: number;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 18 * boy,
|
||||||
|
height: 112 * boy,
|
||||||
|
padding: `0 ${64 * boy}px`,
|
||||||
|
borderRadius: 999,
|
||||||
|
background: basili > 0.5 ? R.turuncuKoyu : R.turuncu,
|
||||||
|
color: R.beyaz,
|
||||||
|
fontFamily: FONT.govde,
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: 40 * boy,
|
||||||
|
transform: `scale(${1 - 0.03 * basili})`,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{metin}
|
||||||
|
<Ikon ad="ok" boy={40 * boy} kalinlik={2.4} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Risk etiketi (RiskEtiketi): nokta + metin, renk asla tek başına. */
|
||||||
|
export function RiskEtiketi({
|
||||||
|
risk,
|
||||||
|
olcek = 2.4,
|
||||||
|
}: {
|
||||||
|
risk: "guvenli" | "az-riskli" | "riskli";
|
||||||
|
olcek?: number;
|
||||||
|
}) {
|
||||||
|
const t = {
|
||||||
|
guvenli: { nokta: R.guvenli, yazi: R.guvenliYazi, etiket: "Güvenli" },
|
||||||
|
"az-riskli": { nokta: R.azRiskli, yazi: R.azRiskliYazi, etiket: "Az riskli" },
|
||||||
|
riskli: { nokta: R.riskli, yazi: R.riskliYazi, etiket: "Riskli" },
|
||||||
|
}[risk];
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6 * olcek,
|
||||||
|
fontSize: 12 * olcek,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: t.yazi,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ width: 8 * olcek, height: 8 * olcek, borderRadius: 99, background: t.nokta }} />
|
||||||
|
{t.etiket}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
218
src/piksel.tsx
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
import type { CSSProperties, ReactNode } from "react";
|
||||||
|
import {
|
||||||
|
AbsoluteFill,
|
||||||
|
interpolateColors,
|
||||||
|
useCurrentFrame,
|
||||||
|
useVideoConfig,
|
||||||
|
} from "remotion";
|
||||||
|
import { FONT, R, ara, hash, opaklik, yay } from "./tema";
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* PixelMark + SectionEyebrow — sitedeki rozetin video karşılığı */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/** 2×2 piksel imzası; pikseller saat yönünde sırayla ısınıp söner (animate-pixel-isi). */
|
||||||
|
export function PixelMark({ boy }: { boy: number }) {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const isi = (gecikme: number) => {
|
||||||
|
const t = ((frame / 30 - gecikme) % 2.4 + 2.4) % 2.4;
|
||||||
|
return t < 0.6 ? Math.sin((t / 0.6) * Math.PI) : 0;
|
||||||
|
};
|
||||||
|
const px = (x: number, y: number, op: number, gecikme: number) => {
|
||||||
|
const h = isi(gecikme);
|
||||||
|
return (
|
||||||
|
<rect
|
||||||
|
x={x + 1.3}
|
||||||
|
y={y + 1.3}
|
||||||
|
width={7.4}
|
||||||
|
height={7.4}
|
||||||
|
rx={2}
|
||||||
|
fill={interpolateColors(h, [0, 1], [R.primary, R.turuncu])}
|
||||||
|
fillOpacity={op + (0.85 - op) * h}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<svg width={boy} height={boy} viewBox="0 0 20 20">
|
||||||
|
{px(0, 0, 0.35, 0)}
|
||||||
|
<rect x={11.3} y={1.3} width={7.4} height={7.4} rx={2} fill={R.turuncu} fillOpacity={0.85} />
|
||||||
|
{px(10, 10, 0.18, 1.2)}
|
||||||
|
{px(0, 10, 0.6, 1.8)}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Rozet: rounded-full border-slate-200 bg-white px-3.5 py-1.5 text-xs font-semibold text-primary (×ölçek). */
|
||||||
|
export function Eyebrow({ children, olcek = 2.4 }: { children: ReactNode; olcek?: number }) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8 * olcek,
|
||||||
|
borderRadius: 999,
|
||||||
|
border: `${olcek}px solid ${R.slate200}`,
|
||||||
|
background: R.beyaz,
|
||||||
|
padding: `${6 * olcek}px ${14 * olcek}px`,
|
||||||
|
fontFamily: FONT.govde,
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: 12 * olcek,
|
||||||
|
color: R.primary,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PixelMark boy={14 * olcek} />
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "Kolay" Bricolage 300 + "Tercih" 600, tracking -0.05em (logo yazısı). */
|
||||||
|
export function Wordmark({ boy, renk = R.murekkep }: { boy: number; renk?: string }) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontFamily: FONT.vurgu,
|
||||||
|
fontSize: boy,
|
||||||
|
letterSpacing: "-0.05em",
|
||||||
|
color: renk,
|
||||||
|
lineHeight: 1,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontWeight: 300 }}>Kolay</span>
|
||||||
|
<span style={{ fontWeight: 600 }}>Tercih</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CTA bandının zemini (PixelMasonry): büyüklü küçüklü, makro hücre içinde
|
||||||
|
* kaymış kareler, yalnız kenar çerçevesinde; orta boş.
|
||||||
|
*/
|
||||||
|
export function PikselMasonry({
|
||||||
|
seed = 8,
|
||||||
|
renk = R.beyaz,
|
||||||
|
giris = 0,
|
||||||
|
kenar = 0.3,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
seed?: number;
|
||||||
|
renk?: string;
|
||||||
|
giris?: number;
|
||||||
|
kenar?: number;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}) {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { width: w, height: h } = useVideoConfig();
|
||||||
|
const CELL = 22;
|
||||||
|
const MAKRO = 4;
|
||||||
|
const kisa = Math.min(w, h);
|
||||||
|
const kareler: ReactNode[] = [];
|
||||||
|
for (let my = 0; my < h / CELL / MAKRO; my++) {
|
||||||
|
for (let mx = 0; mx < w / CELL / MAKRO; mx++) {
|
||||||
|
const ox = mx * MAKRO;
|
||||||
|
const oy = my * MAKRO;
|
||||||
|
const cx = (ox + MAKRO / 2) * CELL;
|
||||||
|
const cy = (oy + MAKRO / 2) * CELL;
|
||||||
|
const k = Math.min(cx, w - cx, cy, h - cy) / kisa;
|
||||||
|
if (k >= kenar) continue;
|
||||||
|
const yogun = 0.06 + 0.5 * (1 - k / kenar) ** 1.6;
|
||||||
|
if (hash(mx, my, seed) > yogun) continue;
|
||||||
|
const r = hash(mx, my, seed + 7);
|
||||||
|
const n = r < 0.5 ? 2 : r < 0.85 ? 3 : 4;
|
||||||
|
const serbest = MAKRO - n;
|
||||||
|
const kx = ox + Math.floor(hash(mx, my, seed + 11) * (serbest + 1));
|
||||||
|
const ky = oy + Math.floor(hash(mx, my, seed + 13) * (serbest + 1));
|
||||||
|
const vurgu = n === 2 && hash(mx, my, seed + 99) > 0.9;
|
||||||
|
const ins = CELL * 0.13;
|
||||||
|
const t0 = giris + k * 40 + hash(mx, my, seed + 3) * 10;
|
||||||
|
const s = ara(frame, [t0, t0 + 12], [0, 1]);
|
||||||
|
const boy = (n * CELL - 2 * ins) * s;
|
||||||
|
const nabiz = vurgu ? 0.55 + 0.3 * Math.sin(frame / 9 + mx) : 1;
|
||||||
|
kareler.push(
|
||||||
|
<rect
|
||||||
|
key={`${mx}-${my}`}
|
||||||
|
x={kx * CELL + ins + ((n * CELL - 2 * ins) - boy) / 2}
|
||||||
|
y={ky * CELL + ins + ((n * CELL - 2 * ins) - boy) / 2}
|
||||||
|
width={boy}
|
||||||
|
height={boy}
|
||||||
|
rx={3 * n}
|
||||||
|
fill={vurgu ? R.turuncu : renk}
|
||||||
|
fillOpacity={vurgu ? nabiz : opaklik(kx, ky) * (n === 2 ? 0.9 : n === 3 ? 0.7 : 0.55)}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={style}>
|
||||||
|
<svg width={w} height={h}>
|
||||||
|
{kareler}
|
||||||
|
</svg>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kart köşesindeki üç piksel (PixelCorner). */
|
||||||
|
export function PixelCorner({ boy }: { boy: number }) {
|
||||||
|
const p = (x: number, y: number, op: number) => (
|
||||||
|
<rect x={x + 1.3} y={y + 1.3} width={7.4} height={7.4} rx={2} fill={R.primary} fillOpacity={op} />
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<svg width={boy} height={boy} viewBox="0 0 30 30">
|
||||||
|
{p(20, 0, 0.5)}
|
||||||
|
{p(10, 0, 0.14)}
|
||||||
|
{p(20, 10, 0.26)}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kelime kelime beliren başlık — her kelime kendi yayıyla yukarı oturur. */
|
||||||
|
export function KelimeKelime({
|
||||||
|
metin,
|
||||||
|
gecikme = 0,
|
||||||
|
aralik = 3,
|
||||||
|
style,
|
||||||
|
vurgu,
|
||||||
|
}: {
|
||||||
|
metin: string;
|
||||||
|
gecikme?: number;
|
||||||
|
aralik?: number;
|
||||||
|
style?: CSSProperties;
|
||||||
|
/** Vurgulanacak kelimeler → italik mavi Bricolage (hero'daki span) */
|
||||||
|
vurgu?: string[];
|
||||||
|
}) {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const kelimeler = metin.split(" ");
|
||||||
|
return (
|
||||||
|
<span style={{ display: "inline", ...style }}>
|
||||||
|
{kelimeler.map((k, i) => {
|
||||||
|
const s = yay(frame, gecikme + i * aralik, 18);
|
||||||
|
const vurgulu = vurgu?.includes(k.replace(/[.,!?]/g, ""));
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
opacity: s,
|
||||||
|
transform: `translateY(${(1 - s) * 0.45}em)`,
|
||||||
|
filter: `blur(${(1 - s) * 8}px)`,
|
||||||
|
marginRight: i < kelimeler.length - 1 ? "0.25em" : 0,
|
||||||
|
...(vurgulu
|
||||||
|
? {
|
||||||
|
fontFamily: FONT.vurgu,
|
||||||
|
fontStyle: "italic",
|
||||||
|
fontWeight: 800,
|
||||||
|
letterSpacing: "-0.04em",
|
||||||
|
color: R.primary,
|
||||||
|
}
|
||||||
|
: null),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{k}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
215
src/remocn/gradient-scale-cut-text.tsx
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Easing, interpolate, useCurrentFrame } from "remotion";
|
||||||
|
|
||||||
|
export interface GradientScaleCutTextProps {
|
||||||
|
text?: string;
|
||||||
|
giantFontSize?: number;
|
||||||
|
compactFontSize?: number;
|
||||||
|
fontWeight?: number;
|
||||||
|
gradientStart?: string;
|
||||||
|
gradientEnd?: string;
|
||||||
|
ghostColor?: string;
|
||||||
|
backgroundColor?: string;
|
||||||
|
anchorOffsetX?: number;
|
||||||
|
giantTravel?: number;
|
||||||
|
settleTravel?: number;
|
||||||
|
cutFrame?: number;
|
||||||
|
revealSoftness?: number;
|
||||||
|
compactBlur?: number;
|
||||||
|
speed?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const gradientScaleCutTextLength = 36;
|
||||||
|
|
||||||
|
const REVEAL_END = 33;
|
||||||
|
const SETTLE_FRAMES = 8;
|
||||||
|
const GIANT_Y = -28;
|
||||||
|
const GIANT_X_CORRECTION = -48;
|
||||||
|
const COMPACT_Y = 5;
|
||||||
|
const COMPACT_X_SCALE = 0.87;
|
||||||
|
const COMPACT_START_X_CORRECTION = 28;
|
||||||
|
const FONT_FAMILY = "Arial, Helvetica, sans-serif";
|
||||||
|
const clamp = {
|
||||||
|
extrapolateLeft: "clamp" as const,
|
||||||
|
extrapolateRight: "clamp" as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TextLayerProps {
|
||||||
|
text: string;
|
||||||
|
fontSize: number;
|
||||||
|
fontWeight: number;
|
||||||
|
gradientStart: string;
|
||||||
|
gradientEnd: string;
|
||||||
|
ghostColor: string;
|
||||||
|
maskImage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TextLayer({
|
||||||
|
text,
|
||||||
|
fontSize,
|
||||||
|
fontWeight,
|
||||||
|
gradientStart,
|
||||||
|
gradientEnd,
|
||||||
|
ghostColor,
|
||||||
|
maskImage,
|
||||||
|
}: TextLayerProps) {
|
||||||
|
const textStyle = {
|
||||||
|
display: "block",
|
||||||
|
fontFamily: FONT_FAMILY,
|
||||||
|
fontSize,
|
||||||
|
fontWeight,
|
||||||
|
letterSpacing: 0,
|
||||||
|
lineHeight: 1.12,
|
||||||
|
whiteSpace: "nowrap" as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: "relative" }}>
|
||||||
|
<span style={{ ...textStyle, color: ghostColor }}>{text}</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
...textStyle,
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
color: "transparent",
|
||||||
|
backgroundImage: `linear-gradient(90deg, ${gradientStart} 0%, ${gradientStart} 4%, ${gradientEnd} 26%, ${gradientEnd} 100%)`,
|
||||||
|
backgroundClip: "text",
|
||||||
|
maskImage,
|
||||||
|
maskRepeat: "no-repeat",
|
||||||
|
opacity: 1,
|
||||||
|
WebkitBackgroundClip: "text",
|
||||||
|
WebkitMaskImage: maskImage,
|
||||||
|
WebkitMaskRepeat: "no-repeat",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GradientScaleCutText({
|
||||||
|
text = "Introducing",
|
||||||
|
giantFontSize = 520,
|
||||||
|
compactFontSize = 132,
|
||||||
|
fontWeight = 700,
|
||||||
|
gradientStart = "#f04a14",
|
||||||
|
gradientEnd = "#f3f1f1",
|
||||||
|
ghostColor = "#170b0a",
|
||||||
|
backgroundColor = "#000000",
|
||||||
|
anchorOffsetX = -16,
|
||||||
|
giantTravel = 400,
|
||||||
|
settleTravel = 160,
|
||||||
|
cutFrame = 13,
|
||||||
|
revealSoftness = 14,
|
||||||
|
compactBlur = 12,
|
||||||
|
speed = 1,
|
||||||
|
className,
|
||||||
|
}: GradientScaleCutTextProps) {
|
||||||
|
const frame = useCurrentFrame() * Math.max(speed, 0);
|
||||||
|
const safeCutFrame = Math.max(cutFrame, 1);
|
||||||
|
const safeGiantFontSize = Math.max(giantFontSize, 1);
|
||||||
|
const safeCompactFontSize = Math.max(compactFontSize, 1);
|
||||||
|
const safeSoftness = Math.max(revealSoftness, 0);
|
||||||
|
const safeBlur = Math.max(compactBlur, 0);
|
||||||
|
const revealProgress = interpolate(frame, [0, REVEAL_END], [0.02, 1], {
|
||||||
|
...clamp,
|
||||||
|
easing: Easing.bezier(0.22, 0.74, 0.24, 1),
|
||||||
|
});
|
||||||
|
const revealEdge = revealProgress * (100 + safeSoftness / 2);
|
||||||
|
const revealSolid = Math.max(0, revealEdge - safeSoftness / 2);
|
||||||
|
const revealFade = revealEdge + safeSoftness / 2;
|
||||||
|
const maskImage = `linear-gradient(90deg, #000 0%, #000 ${revealSolid}%, transparent ${revealFade}%, transparent ${revealFade}%)`;
|
||||||
|
const giantProgress = interpolate(
|
||||||
|
frame,
|
||||||
|
[0, Math.max(safeCutFrame - 1, 1)],
|
||||||
|
[0, 1],
|
||||||
|
{
|
||||||
|
...clamp,
|
||||||
|
easing: Easing.out(Easing.cubic),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const settleProgress = interpolate(
|
||||||
|
frame,
|
||||||
|
[safeCutFrame, safeCutFrame + SETTLE_FRAMES],
|
||||||
|
[0, 1],
|
||||||
|
{
|
||||||
|
...clamp,
|
||||||
|
easing: Easing.out(Easing.cubic),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const moveProgress = interpolate(
|
||||||
|
frame,
|
||||||
|
[
|
||||||
|
safeCutFrame + 1,
|
||||||
|
safeCutFrame + 2,
|
||||||
|
safeCutFrame + 4,
|
||||||
|
safeCutFrame + SETTLE_FRAMES,
|
||||||
|
],
|
||||||
|
[0, 0.58, 0.875, 1],
|
||||||
|
clamp,
|
||||||
|
);
|
||||||
|
const showGiant = frame < safeCutFrame;
|
||||||
|
const compactX =
|
||||||
|
interpolate(moveProgress, [0, 1], [settleTravel, 0], clamp) +
|
||||||
|
COMPACT_START_X_CORRECTION * (1 - moveProgress);
|
||||||
|
const compactBlurValue = safeBlur * (1 - settleProgress);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
overflow: "hidden",
|
||||||
|
backgroundColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showGiant ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: `calc(50% + ${GIANT_Y}px)`,
|
||||||
|
left: `calc(50% + ${anchorOffsetX + GIANT_X_CORRECTION - giantTravel * giantProgress}px)`,
|
||||||
|
opacity: 1,
|
||||||
|
transform: "translate3d(0, -50%, 0)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextLayer
|
||||||
|
text={text}
|
||||||
|
fontSize={safeGiantFontSize}
|
||||||
|
fontWeight={fontWeight}
|
||||||
|
gradientStart={gradientStart}
|
||||||
|
gradientEnd={gradientEnd}
|
||||||
|
ghostColor={ghostColor}
|
||||||
|
maskImage={maskImage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: `calc(50% + ${COMPACT_Y}px)`,
|
||||||
|
left: `calc(50% + ${anchorOffsetX + compactX}px)`,
|
||||||
|
filter: `blur(${compactBlurValue}px)`,
|
||||||
|
opacity: 1,
|
||||||
|
transform: `translate3d(-50%, -50%, 0) scaleX(${COMPACT_X_SCALE})`,
|
||||||
|
transformOrigin: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextLayer
|
||||||
|
text={text}
|
||||||
|
fontSize={safeCompactFontSize}
|
||||||
|
fontWeight={fontWeight}
|
||||||
|
gradientStart={gradientStart}
|
||||||
|
gradientEnd={gradientEnd}
|
||||||
|
ghostColor={ghostColor}
|
||||||
|
maskImage={maskImage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
535
src/remocn/lens-zoom.tsx
Normal file
@@ -0,0 +1,535 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
TransitionPresentation,
|
||||||
|
TransitionPresentationComponentProps,
|
||||||
|
} from "@remotion/transitions";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { useId } from "react";
|
||||||
|
import { Easing, interpolate, useCurrentFrame, useVideoConfig } from "remotion";
|
||||||
|
|
||||||
|
export const LENS_ZOOM_DURATION_IN_FRAMES = 28;
|
||||||
|
|
||||||
|
export const SHAKE_STOPS = [-9 / 11, -3 / 11, 4 / 11, 8 / 11] as const;
|
||||||
|
|
||||||
|
export const LENS_ZOOM_EASE_IN = Easing.bezier(0.54, 0, 0.82, 0.8);
|
||||||
|
export const LENS_ZOOM_EASE_OUT = Easing.bezier(0.18, 0.2, 0.46, 1);
|
||||||
|
|
||||||
|
export type LensZoomProps = {
|
||||||
|
inScaleFrom?: number;
|
||||||
|
inScaleTo?: number;
|
||||||
|
inFovFrom?: number;
|
||||||
|
inFovTo?: number;
|
||||||
|
inBlurFrom?: number;
|
||||||
|
inBlurTo?: number;
|
||||||
|
inRotationFrom?: number;
|
||||||
|
inRotationTo?: number;
|
||||||
|
outScaleFrom?: number;
|
||||||
|
outScaleTo?: number;
|
||||||
|
outFovFrom?: number;
|
||||||
|
outFovTo?: number;
|
||||||
|
outBlurFrom?: number;
|
||||||
|
outBlurTo?: number;
|
||||||
|
outRotationFrom?: number;
|
||||||
|
outRotationTo?: number;
|
||||||
|
anticipateScale?: number;
|
||||||
|
anticipatePortion?: number;
|
||||||
|
lensSteps?: number;
|
||||||
|
blurSamples?: number;
|
||||||
|
blurScaleGain?: number;
|
||||||
|
blurFade?: number;
|
||||||
|
shakeAmount?: number;
|
||||||
|
shakeFrequency?: number;
|
||||||
|
shakeCyclesPerUnit?: number;
|
||||||
|
shakeTranslatePx?: number;
|
||||||
|
shakeRotateDeg?: number;
|
||||||
|
redScale?: number;
|
||||||
|
greenScale?: number;
|
||||||
|
blueScale?: number;
|
||||||
|
letterboxAspect?: number;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
distortionCenterX?: number;
|
||||||
|
distortionCenterY?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function hashed(seed: number, i: number): number {
|
||||||
|
let t = Math.imul(i ^ seed, 0x27d4eb2d);
|
||||||
|
t ^= t >>> 15;
|
||||||
|
t = Math.imul(t, 0x85ebca6b);
|
||||||
|
t ^= t >>> 13;
|
||||||
|
return ((t >>> 0) / 4294967296) * 2 - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function valueNoise(seed: number, t: number): number {
|
||||||
|
const i = Math.floor(t);
|
||||||
|
const f = t - i;
|
||||||
|
const u = f * f * (3 - 2 * f);
|
||||||
|
return hashed(seed, i) * (1 - u) + hashed(seed, i + 1) * u;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fovHalf(fov: number): number {
|
||||||
|
return (Math.min(fov, 178) / 2) * (Math.PI / 180);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lensMagnification(r: number, fov: number): number {
|
||||||
|
if (fov < 0.01) return 1;
|
||||||
|
const theta = fovHalf(fov);
|
||||||
|
const peak = Math.tan(theta) / theta;
|
||||||
|
if (r <= 0) return peak;
|
||||||
|
const a = r * theta;
|
||||||
|
if (a >= Math.PI / 2 - 1e-3) return 1;
|
||||||
|
const rSource = Math.tan(a) / Math.tan(theta);
|
||||||
|
if (rSource <= 1e-6) return peak;
|
||||||
|
return Math.max(1, r / rSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
function radiusForMagnification(target: number, fov: number): number {
|
||||||
|
let lo = 0;
|
||||||
|
let hi = 1;
|
||||||
|
for (let i = 0; i < 24; i++) {
|
||||||
|
const mid = (lo + hi) / 2;
|
||||||
|
if (lensMagnification(mid, fov) > target) lo = mid;
|
||||||
|
else hi = mid;
|
||||||
|
}
|
||||||
|
return (lo + hi) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ringClip(
|
||||||
|
rInner: number,
|
||||||
|
rOuter: number | null,
|
||||||
|
w: number,
|
||||||
|
h: number,
|
||||||
|
cx: number,
|
||||||
|
cy: number,
|
||||||
|
): string {
|
||||||
|
const circle = (r: number, clockwise: boolean) => {
|
||||||
|
const s = clockwise ? 1 : 0;
|
||||||
|
return `M ${cx + r} ${cy} A ${r} ${r} 0 1 ${s} ${cx - r} ${cy} A ${r} ${r} 0 1 ${s} ${cx + r} ${cy} Z`;
|
||||||
|
};
|
||||||
|
const outer =
|
||||||
|
rOuter === null
|
||||||
|
? `M 0 0 L ${w} 0 L ${w} ${h} L 0 ${h} Z`
|
||||||
|
: circle(rOuter, true);
|
||||||
|
return rInner > 0 ? `${outer} ${circle(rInner, false)}` : outer;
|
||||||
|
}
|
||||||
|
|
||||||
|
type HalfProps = {
|
||||||
|
scene: ReactNode;
|
||||||
|
scale: number;
|
||||||
|
fov: number;
|
||||||
|
blur: number;
|
||||||
|
rotation: number;
|
||||||
|
centerX: number;
|
||||||
|
centerY: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
lensSteps: number;
|
||||||
|
blurSamples: number;
|
||||||
|
blurScaleGain: number;
|
||||||
|
blurFade: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function TransitionHalf({
|
||||||
|
scene,
|
||||||
|
scale,
|
||||||
|
fov,
|
||||||
|
blur,
|
||||||
|
rotation,
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
lensSteps,
|
||||||
|
blurSamples,
|
||||||
|
blurScaleGain,
|
||||||
|
blurFade,
|
||||||
|
}: HalfProps) {
|
||||||
|
const origin = `${centerX}px ${centerY}px`;
|
||||||
|
const base = scale / 100;
|
||||||
|
|
||||||
|
const peak = lensMagnification(0, fov);
|
||||||
|
const steps = peak <= 1.001 ? 0 : lensSteps;
|
||||||
|
const rings = Array.from({ length: steps }, (_, i) => {
|
||||||
|
const s = 1 + ((peak - 1) * (i + 0.5)) / steps;
|
||||||
|
return { s, r: radiusForMagnification(s, fov) };
|
||||||
|
});
|
||||||
|
|
||||||
|
const samples =
|
||||||
|
blur <= 0.05 ? 1 : Math.max(2, Math.round((blurSamples * blur) / 60));
|
||||||
|
const weights = Array.from(
|
||||||
|
{ length: samples },
|
||||||
|
(_, j) => (1 - j / samples) ** blurFade,
|
||||||
|
);
|
||||||
|
|
||||||
|
const groups: {
|
||||||
|
key: number;
|
||||||
|
alpha: number;
|
||||||
|
copies: { key: string; clip: string; k: number }[];
|
||||||
|
}[] = [];
|
||||||
|
let running = 0;
|
||||||
|
for (let j = samples - 1; j >= 0; j--) {
|
||||||
|
running += weights[j];
|
||||||
|
const g =
|
||||||
|
samples === 1
|
||||||
|
? 1
|
||||||
|
: 1 + (blur / 100) * blurScaleGain * (j / (samples - 1));
|
||||||
|
const halfWidth = (width / 2) * g;
|
||||||
|
const copies: { key: string; clip: string; k: number }[] = [];
|
||||||
|
|
||||||
|
copies.push({
|
||||||
|
key: "base",
|
||||||
|
clip: ringClip(
|
||||||
|
rings.length ? rings[0].r * halfWidth : 0,
|
||||||
|
null,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
),
|
||||||
|
k: g * base,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = 0; i < rings.length; i++) {
|
||||||
|
const inner = i + 1 < rings.length ? rings[i + 1].r * halfWidth : 0;
|
||||||
|
copies.push({
|
||||||
|
key: `${i}`,
|
||||||
|
clip: ringClip(
|
||||||
|
inner,
|
||||||
|
rings[i].r * halfWidth,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
),
|
||||||
|
k: g * rings[i].s * base,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
groups.push({ key: j, alpha: weights[j] / running, copies });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: "absolute", inset: 0 }}>
|
||||||
|
{groups.map(({ key, alpha, copies }) => (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
style={{ position: "absolute", inset: 0, opacity: alpha }}
|
||||||
|
>
|
||||||
|
{copies.map(({ key: ck, clip, k }) => (
|
||||||
|
<div
|
||||||
|
key={ck}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
clipPath: `path("${clip}")`,
|
||||||
|
rotate: `${rotation}deg`,
|
||||||
|
scale: `${k}`,
|
||||||
|
transformOrigin: origin,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{scene}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LensZoomStageProps extends HalfProps {
|
||||||
|
shakeEnvelope: number;
|
||||||
|
shakeFrequency: number;
|
||||||
|
shakeCyclesPerUnit: number;
|
||||||
|
shakeTranslatePx: number;
|
||||||
|
shakeRotateDeg: number;
|
||||||
|
redScale: number;
|
||||||
|
greenScale: number;
|
||||||
|
blueScale: number;
|
||||||
|
letterboxAspect: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LensZoomStage({
|
||||||
|
shakeEnvelope,
|
||||||
|
shakeFrequency,
|
||||||
|
shakeCyclesPerUnit,
|
||||||
|
shakeTranslatePx,
|
||||||
|
shakeRotateDeg,
|
||||||
|
redScale,
|
||||||
|
greenScale,
|
||||||
|
blueScale,
|
||||||
|
letterboxAspect,
|
||||||
|
...half
|
||||||
|
}: LensZoomStageProps) {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
const filterId = useId();
|
||||||
|
const { width, height, centerX, centerY } = half;
|
||||||
|
|
||||||
|
const core = <TransitionHalf {...half} />;
|
||||||
|
|
||||||
|
const split = redScale !== greenScale || blueScale !== greenScale;
|
||||||
|
const channels: { id: string; scale: number; matrix: string }[] = [
|
||||||
|
{
|
||||||
|
id: "r",
|
||||||
|
scale: redScale,
|
||||||
|
matrix: "1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "g",
|
||||||
|
scale: greenScale,
|
||||||
|
matrix: "0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b",
|
||||||
|
scale: blueScale,
|
||||||
|
matrix: "0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const composited = split ? (
|
||||||
|
<div style={{ position: "absolute", inset: 0, backgroundColor: "#000000" }}>
|
||||||
|
{channels.map((c) => (
|
||||||
|
<div
|
||||||
|
key={c.id}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
mixBlendMode: "screen",
|
||||||
|
filter: `url(#${filterId}-${c.id})`,
|
||||||
|
scale: `${c.scale}`,
|
||||||
|
transformOrigin: `${centerX}px ${centerY}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{core}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
core
|
||||||
|
);
|
||||||
|
|
||||||
|
const t = (frame / fps) * shakeFrequency * shakeCyclesPerUnit;
|
||||||
|
const octave = (seed: number) =>
|
||||||
|
valueNoise(seed, t) + 0.45 * valueNoise(seed + 977, t * 2.6);
|
||||||
|
const shakeX = octave(11) * shakeTranslatePx * shakeEnvelope;
|
||||||
|
const shakeY = octave(23) * shakeTranslatePx * shakeEnvelope;
|
||||||
|
const shakeR = octave(41) * shakeRotateDeg * shakeEnvelope;
|
||||||
|
|
||||||
|
const barHeight =
|
||||||
|
letterboxAspect > 0
|
||||||
|
? Math.max(0, (height - width / letterboxAspect) / 2)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
overflow: "hidden",
|
||||||
|
backgroundColor: "#000000",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{split ? (
|
||||||
|
<svg width={0} height={0} style={{ position: "absolute" }} aria-hidden>
|
||||||
|
<defs>
|
||||||
|
{channels.map((c) => (
|
||||||
|
<filter
|
||||||
|
key={c.id}
|
||||||
|
id={`${filterId}-${c.id}`}
|
||||||
|
colorInterpolationFilters="sRGB"
|
||||||
|
>
|
||||||
|
<feColorMatrix type="matrix" values={c.matrix} />
|
||||||
|
</filter>
|
||||||
|
))}
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
translate: `${shakeX}px ${shakeY}px`,
|
||||||
|
rotate: `${shakeR}deg`,
|
||||||
|
transformOrigin: `${centerX}px ${centerY}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{composited}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{barHeight > 0 ? (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
height: barHeight,
|
||||||
|
backgroundColor: "#000000",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
height: barHeight,
|
||||||
|
backgroundColor: "#000000",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const LensZoomPresentation: React.FC<
|
||||||
|
TransitionPresentationComponentProps<LensZoomProps>
|
||||||
|
> = ({
|
||||||
|
children,
|
||||||
|
presentationDirection,
|
||||||
|
presentationProgress,
|
||||||
|
passedProps,
|
||||||
|
}) => {
|
||||||
|
const config = useVideoConfig();
|
||||||
|
const {
|
||||||
|
inScaleFrom = 100,
|
||||||
|
inScaleTo = 150,
|
||||||
|
inFovFrom = 0,
|
||||||
|
inFovTo = 160,
|
||||||
|
inBlurFrom = 0,
|
||||||
|
inBlurTo = 60,
|
||||||
|
inRotationFrom = 0,
|
||||||
|
inRotationTo = -15,
|
||||||
|
|
||||||
|
outScaleFrom = 135,
|
||||||
|
outScaleTo = 100,
|
||||||
|
outFovFrom = 135,
|
||||||
|
outFovTo = 0,
|
||||||
|
outBlurFrom = 45,
|
||||||
|
outBlurTo = 0,
|
||||||
|
outRotationFrom = 345,
|
||||||
|
outRotationTo = 360,
|
||||||
|
|
||||||
|
anticipateScale = 100,
|
||||||
|
anticipatePortion = 0.35,
|
||||||
|
|
||||||
|
lensSteps = 9,
|
||||||
|
blurSamples = 7,
|
||||||
|
blurScaleGain = 1,
|
||||||
|
blurFade = 1.5,
|
||||||
|
|
||||||
|
shakeAmount = 50,
|
||||||
|
shakeFrequency = 1.4,
|
||||||
|
shakeCyclesPerUnit = 8,
|
||||||
|
shakeTranslatePx = 73,
|
||||||
|
shakeRotateDeg = 1.1,
|
||||||
|
|
||||||
|
redScale = 1.01,
|
||||||
|
greenScale = 1.0,
|
||||||
|
blueScale = 0.99,
|
||||||
|
|
||||||
|
letterboxAspect = 0,
|
||||||
|
|
||||||
|
width = config.width,
|
||||||
|
height = config.height,
|
||||||
|
distortionCenterX = width / 2,
|
||||||
|
distortionCenterY = height / 2,
|
||||||
|
} = passedProps;
|
||||||
|
|
||||||
|
const exiting = presentationDirection === "exiting";
|
||||||
|
|
||||||
|
if (exiting ? presentationProgress >= 0.5 : presentationProgress < 0.5)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const p = exiting
|
||||||
|
? interpolate(presentationProgress, [0, 0.5], [0, 1], {
|
||||||
|
easing: LENS_ZOOM_EASE_IN,
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
})
|
||||||
|
: interpolate(presentationProgress, [0.5, 1], [0, 1], {
|
||||||
|
easing: LENS_ZOOM_EASE_OUT,
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
});
|
||||||
|
|
||||||
|
const anticipating =
|
||||||
|
exiting && anticipatePortion > 0 && anticipateScale < inScaleFrom;
|
||||||
|
|
||||||
|
const pFx =
|
||||||
|
anticipating && exiting
|
||||||
|
? interpolate(p, [anticipatePortion, 1], [0, 1], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
})
|
||||||
|
: p;
|
||||||
|
|
||||||
|
const lerp = (from: number, to: number) => from + (to - from) * pFx;
|
||||||
|
|
||||||
|
const exitScale = anticipating
|
||||||
|
? interpolate(
|
||||||
|
p,
|
||||||
|
[0, anticipatePortion, 1],
|
||||||
|
[inScaleFrom, anticipateScale, inScaleTo],
|
||||||
|
{
|
||||||
|
easing: Easing.inOut(Easing.ease),
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: lerp(inScaleFrom, inScaleTo);
|
||||||
|
|
||||||
|
const shakeEnvelope =
|
||||||
|
interpolate(
|
||||||
|
presentationProgress,
|
||||||
|
SHAKE_STOPS.map((s) => 0.5 + s / 2),
|
||||||
|
[0, shakeAmount, shakeAmount, 0],
|
||||||
|
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" },
|
||||||
|
) / 100;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LensZoomStage
|
||||||
|
scene={children}
|
||||||
|
scale={exiting ? exitScale : lerp(outScaleFrom, outScaleTo)}
|
||||||
|
fov={exiting ? lerp(inFovFrom, inFovTo) : lerp(outFovFrom, outFovTo)}
|
||||||
|
blur={exiting ? lerp(inBlurFrom, inBlurTo) : lerp(outBlurFrom, outBlurTo)}
|
||||||
|
rotation={
|
||||||
|
exiting
|
||||||
|
? lerp(inRotationFrom, inRotationTo)
|
||||||
|
: lerp(outRotationFrom, outRotationTo)
|
||||||
|
}
|
||||||
|
centerX={distortionCenterX}
|
||||||
|
centerY={distortionCenterY}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
lensSteps={lensSteps}
|
||||||
|
blurSamples={blurSamples}
|
||||||
|
blurScaleGain={blurScaleGain}
|
||||||
|
blurFade={blurFade}
|
||||||
|
shakeEnvelope={shakeEnvelope}
|
||||||
|
shakeFrequency={shakeFrequency}
|
||||||
|
shakeCyclesPerUnit={shakeCyclesPerUnit}
|
||||||
|
shakeTranslatePx={shakeTranslatePx}
|
||||||
|
shakeRotateDeg={shakeRotateDeg}
|
||||||
|
redScale={redScale}
|
||||||
|
greenScale={greenScale}
|
||||||
|
blueScale={blueScale}
|
||||||
|
letterboxAspect={letterboxAspect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function lensZoom(
|
||||||
|
props: LensZoomProps = {},
|
||||||
|
): TransitionPresentation<LensZoomProps> {
|
||||||
|
return {
|
||||||
|
component: LensZoomPresentation,
|
||||||
|
props,
|
||||||
|
};
|
||||||
|
}
|
||||||
236
src/remocn/outline-fill-track-text.tsx
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { Easing, interpolate, useCurrentFrame, useVideoConfig } from "remotion";
|
||||||
|
|
||||||
|
export interface OutlineFillTrackTextProps {
|
||||||
|
leadText?: string;
|
||||||
|
valueText?: string;
|
||||||
|
fontSize?: number;
|
||||||
|
fontWeight?: number;
|
||||||
|
color?: string;
|
||||||
|
outlineColor?: string;
|
||||||
|
outlineWidth?: number;
|
||||||
|
backgroundColor?: string;
|
||||||
|
glowColor?: string;
|
||||||
|
enterOffset?: number;
|
||||||
|
anchorOffsetX?: number;
|
||||||
|
trackDistance?: number;
|
||||||
|
wordGap?: number;
|
||||||
|
endPadding?: number;
|
||||||
|
fillDuration?: number;
|
||||||
|
speed?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const outlineFillTrackTextLength = 80;
|
||||||
|
|
||||||
|
const ENTER_END = 16;
|
||||||
|
const TRACK_START = 16;
|
||||||
|
const TRACK_END = 72;
|
||||||
|
const FILL_START = 34;
|
||||||
|
const FILL_PREFILL = 10;
|
||||||
|
const FONT_FAMILY = "Arial, Helvetica, sans-serif";
|
||||||
|
const clamp = {
|
||||||
|
extrapolateLeft: "clamp" as const,
|
||||||
|
extrapolateRight: "clamp" as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
function estimateTextWidth(
|
||||||
|
text: string,
|
||||||
|
fontSize: number,
|
||||||
|
fontWeight: number,
|
||||||
|
): number {
|
||||||
|
if (typeof document === "undefined") {
|
||||||
|
return text.length * fontSize * 0.56;
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = document.createElement("canvas").getContext("2d");
|
||||||
|
if (!context) return text.length * fontSize * 0.56;
|
||||||
|
context.font = `${fontWeight} ${fontSize}px Arial`;
|
||||||
|
return context.measureText(text).width;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OutlineFillTrackText({
|
||||||
|
leadText = "Keep",
|
||||||
|
valueText = "1000000000%",
|
||||||
|
fontSize = 368,
|
||||||
|
fontWeight = 700,
|
||||||
|
color = "#f4f3f6",
|
||||||
|
outlineColor = "#e6e2ed",
|
||||||
|
outlineWidth = 3,
|
||||||
|
backgroundColor = "#030012",
|
||||||
|
glowColor = "#64109a",
|
||||||
|
enterOffset = 240,
|
||||||
|
anchorOffsetX = -84,
|
||||||
|
trackDistance = 0,
|
||||||
|
wordGap = 220,
|
||||||
|
endPadding = 96,
|
||||||
|
fillDuration = 38,
|
||||||
|
speed = 1,
|
||||||
|
className,
|
||||||
|
}: OutlineFillTrackTextProps) {
|
||||||
|
const frame = useCurrentFrame() * Math.max(speed, 0);
|
||||||
|
const { width: compositionWidth } = useVideoConfig();
|
||||||
|
const safeFontSize = Math.max(fontSize, 1);
|
||||||
|
const safeFontWeight = Math.max(fontWeight, 1);
|
||||||
|
const safeOutlineWidth = Math.max(outlineWidth, 0);
|
||||||
|
const safeGap = Math.max(wordGap, 0);
|
||||||
|
const safeEndPadding = Math.max(endPadding, 0);
|
||||||
|
const safeFillDuration = Math.max(fillDuration, 1);
|
||||||
|
|
||||||
|
const measurements = useMemo(() => {
|
||||||
|
const leadWidth = estimateTextWidth(leadText, safeFontSize, safeFontWeight);
|
||||||
|
const valueWidth = estimateTextWidth(
|
||||||
|
valueText,
|
||||||
|
safeFontSize,
|
||||||
|
safeFontWeight,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
valueWidth,
|
||||||
|
distance: leadWidth / 2 + safeGap + valueWidth / 2,
|
||||||
|
};
|
||||||
|
}, [leadText, safeFontSize, safeFontWeight, safeGap, valueText]);
|
||||||
|
|
||||||
|
const valueOffset = trackDistance > 0 ? trackDistance : measurements.distance;
|
||||||
|
const anchorX = compositionWidth / 2 + anchorOffsetX;
|
||||||
|
const trailingOverflow = Math.max(
|
||||||
|
0,
|
||||||
|
anchorX + measurements.valueWidth / 2 - (compositionWidth - safeEndPadding),
|
||||||
|
);
|
||||||
|
const travelDistance = valueOffset + trailingOverflow;
|
||||||
|
const enterProgress = interpolate(frame, [0, ENTER_END], [0, 1], {
|
||||||
|
...clamp,
|
||||||
|
easing: Easing.bezier(0.16, 0.84, 0.24, 1),
|
||||||
|
});
|
||||||
|
const trackProgress = interpolate(frame, [TRACK_START, TRACK_END], [0, 1], {
|
||||||
|
...clamp,
|
||||||
|
easing: Easing.bezier(0.42, 0, 0.58, 1),
|
||||||
|
});
|
||||||
|
const fillProgress = interpolate(
|
||||||
|
frame,
|
||||||
|
[FILL_START - FILL_PREFILL, FILL_START + safeFillDuration],
|
||||||
|
[0, 1],
|
||||||
|
clamp,
|
||||||
|
);
|
||||||
|
const leadY = enterOffset * (1 - enterProgress);
|
||||||
|
const revealEdge = enterProgress * 112 - 8;
|
||||||
|
const revealSolid = Math.max(0, Math.min(100, revealEdge - 10));
|
||||||
|
const revealFade = Math.max(0, Math.min(100, revealEdge + 10));
|
||||||
|
const leadMask = `linear-gradient(180deg, #000 0%, #000 ${revealSolid}%, transparent ${revealFade}%, transparent 100%)`;
|
||||||
|
const glowOpacity = interpolate(
|
||||||
|
enterProgress,
|
||||||
|
[0, 0.18, 0.72, 1],
|
||||||
|
[0.92, 1, 0.52, 0],
|
||||||
|
clamp,
|
||||||
|
);
|
||||||
|
const fieldY = interpolate(enterProgress, [0, 1], [-42, -210], clamp);
|
||||||
|
const textStyle = {
|
||||||
|
display: "block",
|
||||||
|
fontFamily: FONT_FAMILY,
|
||||||
|
fontSize: safeFontSize,
|
||||||
|
fontWeight: safeFontWeight,
|
||||||
|
letterSpacing: 0,
|
||||||
|
lineHeight: 0.86,
|
||||||
|
whiteSpace: "nowrap" as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
overflow: "hidden",
|
||||||
|
backgroundColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
background: `radial-gradient(ellipse 118% 66% at 50% -12%, ${glowColor} 0%, ${glowColor} 14%, transparent 76%)`,
|
||||||
|
filter: `blur(${safeFontSize * 0.15}px)`,
|
||||||
|
opacity: glowOpacity,
|
||||||
|
pointerEvents: "none",
|
||||||
|
transform: "translateZ(0)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "50%",
|
||||||
|
left: `calc(50% + ${anchorOffsetX}px)`,
|
||||||
|
transform: `translate3d(${-travelDistance * trackProgress}px, -50%, 0)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
color,
|
||||||
|
maskImage: leadMask,
|
||||||
|
maskRepeat: "no-repeat",
|
||||||
|
opacity: 1,
|
||||||
|
transform: `translate3d(-50%, calc(-50% + ${leadY}px), 0)`,
|
||||||
|
WebkitMaskImage: leadMask,
|
||||||
|
WebkitMaskRepeat: "no-repeat",
|
||||||
|
...textStyle,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{leadText}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: valueOffset,
|
||||||
|
transform: "translate3d(-50%, -50%, 0)",
|
||||||
|
visibility: frame < FILL_START ? "hidden" : "visible",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
...textStyle,
|
||||||
|
color: "transparent",
|
||||||
|
paintOrder: "stroke fill",
|
||||||
|
WebkitTextStroke: `${safeOutlineWidth}px ${outlineColor}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{valueText}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
...textStyle,
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
clipPath: `inset(0 ${100 - fillProgress * 100}% 0 0)`,
|
||||||
|
color,
|
||||||
|
opacity: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{valueText}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: fieldY,
|
||||||
|
left: 0,
|
||||||
|
width: safeFontSize * 2.8,
|
||||||
|
height: safeFontSize * 0.72,
|
||||||
|
background: `radial-gradient(ellipse at center, ${backgroundColor} 0%, ${backgroundColor} 18%, transparent 74%)`,
|
||||||
|
filter: `blur(${safeFontSize * 0.07}px)`,
|
||||||
|
opacity: glowOpacity,
|
||||||
|
pointerEvents: "none",
|
||||||
|
transform: "translate3d(-50%, -50%, 0)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
430
src/remocn/path-ride.tsx
Normal file
@@ -0,0 +1,430 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getLength,
|
||||||
|
getPointAtLength,
|
||||||
|
getTangentAtLength,
|
||||||
|
} from "@remotion/paths";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
continueRender,
|
||||||
|
delayRender,
|
||||||
|
Easing,
|
||||||
|
interpolate,
|
||||||
|
useCurrentFrame,
|
||||||
|
useVideoConfig,
|
||||||
|
} from "remotion";
|
||||||
|
|
||||||
|
// KolayTercih: Google Fonts yerine projenin kendi Cal Sans'ı (fontlar.ts)
|
||||||
|
const INTER = "'Cal Sans', sans-serif";
|
||||||
|
|
||||||
|
export interface PathRideProps {
|
||||||
|
text?: string;
|
||||||
|
path?: string;
|
||||||
|
fontSize?: number;
|
||||||
|
fontWeight?: number;
|
||||||
|
fontFamily?: string;
|
||||||
|
color?: string;
|
||||||
|
lineColor?: string;
|
||||||
|
lineWidth?: number;
|
||||||
|
speed?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An S-curve across a 1280×720 frame. */
|
||||||
|
export const pathRideDefaultPath =
|
||||||
|
"M 120 560 C 360 120 560 120 640 360 S 920 600 1160 160";
|
||||||
|
|
||||||
|
export const pathRideDefaults = {
|
||||||
|
text: "Follow the line",
|
||||||
|
path: pathRideDefaultPath,
|
||||||
|
fontSize: 44,
|
||||||
|
fontWeight: 700,
|
||||||
|
fontFamily: INTER,
|
||||||
|
color: "#fafafa",
|
||||||
|
lineColor: "#D97757",
|
||||||
|
lineWidth: 3,
|
||||||
|
speed: 1,
|
||||||
|
} satisfies Required<Omit<PathRideProps, "className">>;
|
||||||
|
|
||||||
|
/** Beats on the 30 fps clock. */
|
||||||
|
export const pathRideTiming = {
|
||||||
|
draw: [0, 40],
|
||||||
|
straighten: [50, 68],
|
||||||
|
erase: [52, 70],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** Gap between the head and the last letter, in em. */
|
||||||
|
const LEAD = 0.3;
|
||||||
|
/** Letters fade in over this much arc length, in em. */
|
||||||
|
const FADE_IN = 0.5;
|
||||||
|
/** Extra lift of the baseline above the line, in em. */
|
||||||
|
const LIFT = 0.08;
|
||||||
|
/** Samples along the path; the line and the letters read positions from them. */
|
||||||
|
const SAMPLES = 400;
|
||||||
|
const MEASURE_SIZE = 100;
|
||||||
|
/** Inter's baseline and cap height in a `line-height: 1` box, until measured. */
|
||||||
|
const INTER_METRICS = { baseline: 0.8637, capHeight: 0.7275 };
|
||||||
|
|
||||||
|
const CLAMP = {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const easeInOutCubic = Easing.inOut(Easing.cubic);
|
||||||
|
|
||||||
|
function finite(value: number | undefined, fallback: number) {
|
||||||
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Maps a composition frame onto the 30 fps timeline, scaled by `speed`. */
|
||||||
|
export function getPathRideTime(
|
||||||
|
frame: number,
|
||||||
|
options: { fps?: number; speed?: number } = {},
|
||||||
|
) {
|
||||||
|
const fps = Math.max(1, finite(options.fps, 30));
|
||||||
|
const speed = Math.max(0, finite(options.speed, 1));
|
||||||
|
return (Math.max(0, finite(frame, 0)) * 30 * speed) / fps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Arc length drawn so far. */
|
||||||
|
export function getPathRideHead(t: number, length: number) {
|
||||||
|
return interpolate(t, [...pathRideTiming.draw], [0, length], {
|
||||||
|
...CLAMP,
|
||||||
|
easing: easeInOutCubic,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Arc length erased from the start. */
|
||||||
|
export function getPathRideTail(t: number, length: number) {
|
||||||
|
return interpolate(t, [...pathRideTiming.erase], [0, length], {
|
||||||
|
...CLAMP,
|
||||||
|
easing: easeInOutCubic,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** How far the curve has pulled straight, 0..1. */
|
||||||
|
export function getPathRideStraighten(t: number) {
|
||||||
|
return interpolate(t, [...pathRideTiming.straighten], [0, 1], {
|
||||||
|
...CLAMP,
|
||||||
|
easing: easeInOutCubic,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Frame where the line has erased and the text rests on its baseline. */
|
||||||
|
export const pathRideLength = Math.max(
|
||||||
|
pathRideTiming.straighten[1],
|
||||||
|
pathRideTiming.erase[1],
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Letter centers from the text start, and the total width. */
|
||||||
|
export function getPathRideCenters(advances: readonly number[]) {
|
||||||
|
let x = 0;
|
||||||
|
const centers = advances.map((a) => {
|
||||||
|
const c = x + a / 2;
|
||||||
|
x += a;
|
||||||
|
return c;
|
||||||
|
});
|
||||||
|
return { centers, width: x };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Arc length of a letter's center: it trails the head by the lead plus the
|
||||||
|
* text after it, so the last letter follows the head most closely.
|
||||||
|
*/
|
||||||
|
export function getPathRideArc(
|
||||||
|
head: number,
|
||||||
|
center: number,
|
||||||
|
width: number,
|
||||||
|
fontSize: number,
|
||||||
|
) {
|
||||||
|
return head - LEAD * fontSize - (width - center);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Opacity of a letter from how far onto the curve it has come. */
|
||||||
|
export function getPathRideFade(arc: number, fontSize: number) {
|
||||||
|
return Math.min(1, Math.max(0, arc / (FADE_IN * fontSize)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Point {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PathSamples {
|
||||||
|
length: number;
|
||||||
|
points: Point[];
|
||||||
|
tangents: Point[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Points and unit tangents at evenly spaced arc lengths along `path`. */
|
||||||
|
export function samplePath(path: string, count = SAMPLES): PathSamples {
|
||||||
|
const length = getLength(path);
|
||||||
|
const n = Math.max(2, Math.round(count));
|
||||||
|
const points: Point[] = [];
|
||||||
|
const tangents: Point[] = [];
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const at = (length * i) / (n - 1);
|
||||||
|
// Both can come back null on a degenerate path; reuse the last sample.
|
||||||
|
const p = getPointAtLength(path, at) ?? points[i - 1] ?? { x: 0, y: 0 };
|
||||||
|
const tangent = getTangentAtLength(path, at) ??
|
||||||
|
tangents[i - 1] ?? { x: 1, y: 0 };
|
||||||
|
points.push({ x: p.x, y: p.y });
|
||||||
|
tangents.push({ x: tangent.x, y: tangent.y });
|
||||||
|
}
|
||||||
|
return { length, points, tangents };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Position and unit tangent at arc length `s`, clamped to the path. */
|
||||||
|
export function getPathSample(samples: PathSamples, s: number) {
|
||||||
|
const n = samples.points.length;
|
||||||
|
const u = samples.length > 0 ? (s / samples.length) * (n - 1) : 0;
|
||||||
|
const i = Math.min(n - 2, Math.max(0, Math.floor(u)));
|
||||||
|
const f = Math.min(1, Math.max(0, u - i));
|
||||||
|
const a = samples.points[i];
|
||||||
|
const b = samples.points[i + 1];
|
||||||
|
const ta = samples.tangents[i];
|
||||||
|
const tb = samples.tangents[i + 1];
|
||||||
|
const tx = ta.x + (tb.x - ta.x) * f;
|
||||||
|
const ty = ta.y + (tb.y - ta.y) * f;
|
||||||
|
const norm = Math.hypot(tx, ty) || 1;
|
||||||
|
return {
|
||||||
|
x: a.x + (b.x - a.x) * f,
|
||||||
|
y: a.y + (b.y - a.y) * f,
|
||||||
|
tx: tx / norm,
|
||||||
|
ty: ty / norm,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PathRideStraight {
|
||||||
|
/** Arc length that lands on the text's left edge. */
|
||||||
|
start: number;
|
||||||
|
/** Reference x of the text's left edge on the straight baseline. */
|
||||||
|
left: number;
|
||||||
|
/** Reference y of the straight baseline. */
|
||||||
|
baseline: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PathRidePose {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
angle: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A point of the path pulled `k` of the way from the curve to the straight
|
||||||
|
* baseline, lifted `lift` along its normal. Arc length maps to x one to one,
|
||||||
|
* so the text keeps its spacing while the curve pulls taut.
|
||||||
|
*/
|
||||||
|
export function getPathRidePoint(
|
||||||
|
samples: PathSamples,
|
||||||
|
s: number,
|
||||||
|
k: number,
|
||||||
|
straight: PathRideStraight,
|
||||||
|
lift = 0,
|
||||||
|
): PathRidePose {
|
||||||
|
const p = getPathSample(samples, s);
|
||||||
|
const x = p.x + (straight.left + (s - straight.start) - p.x) * k;
|
||||||
|
const y = p.y + (straight.baseline - p.y) * k;
|
||||||
|
const tx = p.tx + (1 - p.tx) * k;
|
||||||
|
const ty = p.ty * (1 - k);
|
||||||
|
const norm = Math.hypot(tx, ty) || 1;
|
||||||
|
return {
|
||||||
|
x: x + (ty / norm) * lift,
|
||||||
|
y: y - (tx / norm) * lift,
|
||||||
|
angle: Math.atan2(ty, tx),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Polyline of the pulled path between two arc lengths. */
|
||||||
|
export function getPathRideLine(
|
||||||
|
samples: PathSamples,
|
||||||
|
from: number,
|
||||||
|
to: number,
|
||||||
|
k: number,
|
||||||
|
straight: PathRideStraight,
|
||||||
|
) {
|
||||||
|
if (to <= from) return "";
|
||||||
|
const step = samples.length / (samples.points.length - 1);
|
||||||
|
const stops = [from];
|
||||||
|
for (let s = Math.ceil(from / step) * step; s < to; s += step) {
|
||||||
|
if (s > from) stops.push(s);
|
||||||
|
}
|
||||||
|
stops.push(to);
|
||||||
|
return stops
|
||||||
|
.map((s, i) => {
|
||||||
|
const p = getPathRidePoint(samples, s, k, straight);
|
||||||
|
return `${i === 0 ? "M" : "L"}${p.x.toFixed(2)} ${p.y.toFixed(2)}`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function useMetrics(text: string, font: string, fallback: number) {
|
||||||
|
const chars = Array.from(text);
|
||||||
|
const key = chars.join("");
|
||||||
|
const [metrics, setMetrics] = useState<{
|
||||||
|
advances: number[];
|
||||||
|
baseline: number;
|
||||||
|
capHeight: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handle = delayRender("path-ride: measuring the text");
|
||||||
|
let released = false;
|
||||||
|
const release = () => {
|
||||||
|
if (released) return;
|
||||||
|
released = true;
|
||||||
|
continueRender(handle);
|
||||||
|
};
|
||||||
|
const measure = () => {
|
||||||
|
if (released) return;
|
||||||
|
const ctx = document.createElement("canvas").getContext("2d");
|
||||||
|
if (ctx) {
|
||||||
|
ctx.font = font;
|
||||||
|
const m = ctx.measureText("H");
|
||||||
|
const ascent = m.fontBoundingBoxAscent / MEASURE_SIZE;
|
||||||
|
const descent = m.fontBoundingBoxDescent / MEASURE_SIZE;
|
||||||
|
setMetrics({
|
||||||
|
advances: Array.from(key).map(
|
||||||
|
(c) => ctx.measureText(c).width / MEASURE_SIZE,
|
||||||
|
),
|
||||||
|
baseline:
|
||||||
|
ascent > 0
|
||||||
|
? (1 - (ascent + descent)) / 2 + ascent
|
||||||
|
: INTER_METRICS.baseline,
|
||||||
|
capHeight:
|
||||||
|
m.actualBoundingBoxAscent > 0
|
||||||
|
? m.actualBoundingBoxAscent / MEASURE_SIZE
|
||||||
|
: INTER_METRICS.capHeight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
release();
|
||||||
|
};
|
||||||
|
document.fonts.load(font, key).then(measure, measure);
|
||||||
|
return release;
|
||||||
|
}, [key, font]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
metrics ?? {
|
||||||
|
advances: chars.map(() => fallback),
|
||||||
|
baseline: INTER_METRICS.baseline,
|
||||||
|
capHeight: INTER_METRICS.capHeight,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PathRide({
|
||||||
|
text = pathRideDefaults.text,
|
||||||
|
path = pathRideDefaults.path,
|
||||||
|
fontSize = pathRideDefaults.fontSize,
|
||||||
|
fontWeight = pathRideDefaults.fontWeight,
|
||||||
|
fontFamily = pathRideDefaults.fontFamily,
|
||||||
|
color = pathRideDefaults.color,
|
||||||
|
lineColor = pathRideDefaults.lineColor,
|
||||||
|
lineWidth = pathRideDefaults.lineWidth,
|
||||||
|
speed = pathRideDefaults.speed,
|
||||||
|
className,
|
||||||
|
}: PathRideProps) {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { fps, width, height } = useVideoConfig();
|
||||||
|
const unit = height / 720;
|
||||||
|
const t = getPathRideTime(frame, { fps, speed });
|
||||||
|
const metrics = useMetrics(
|
||||||
|
text,
|
||||||
|
`${fontWeight} ${MEASURE_SIZE}px ${fontFamily}`,
|
||||||
|
0.55,
|
||||||
|
);
|
||||||
|
const samples = useMemo(() => samplePath(path), [path]);
|
||||||
|
const chars = Array.from(text);
|
||||||
|
const advances = metrics.advances.map((a) => a * fontSize);
|
||||||
|
const { centers, width: textWidth } = getPathRideCenters(advances);
|
||||||
|
const length = samples.length;
|
||||||
|
const head = getPathRideHead(t, length);
|
||||||
|
const tail = getPathRideTail(t, length);
|
||||||
|
const k = getPathRideStraighten(t);
|
||||||
|
const lift = lineWidth / 2 + LIFT * fontSize;
|
||||||
|
// The line settles `lift` below the text so the capitals center on the frame.
|
||||||
|
const straight: PathRideStraight = {
|
||||||
|
start: getPathRideArc(length, 0, textWidth, fontSize),
|
||||||
|
left: 640 - textWidth / 2,
|
||||||
|
baseline: 360 + (metrics.capHeight * fontSize) / 2 + lift,
|
||||||
|
};
|
||||||
|
const tip = getPathRidePoint(samples, head, k, straight);
|
||||||
|
const tipOpacity = interpolate(
|
||||||
|
head,
|
||||||
|
[0, length * 0.02, length * 0.98, length],
|
||||||
|
[0, 1, 1, 0],
|
||||||
|
CLAMP,
|
||||||
|
);
|
||||||
|
// Reference space (1280×720, y down) mapped onto the composition.
|
||||||
|
const toX = (x: number) => width / 2 + (x - 640) * unit;
|
||||||
|
const toY = (y: number) => height / 2 + (y - 360) * unit;
|
||||||
|
const size = fontSize * unit;
|
||||||
|
const line = getPathRideLine(samples, tail, head, k, straight);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={className}
|
||||||
|
style={{ position: "absolute", inset: 0, overflow: "hidden" }}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
viewBox={`0 0 ${width} ${height}`}
|
||||||
|
style={{ position: "absolute", inset: 0 }}
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
transform={`translate(${toX(0)} ${toY(0)}) scale(${unit})`}
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
{line ? (
|
||||||
|
<path
|
||||||
|
d={line}
|
||||||
|
stroke={lineColor}
|
||||||
|
strokeWidth={lineWidth}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{tipOpacity > 0 ? (
|
||||||
|
<circle
|
||||||
|
cx={tip.x}
|
||||||
|
cy={tip.y}
|
||||||
|
r={lineWidth * 1.6}
|
||||||
|
fill={lineColor}
|
||||||
|
opacity={tipOpacity}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
{chars.map((char, j) => {
|
||||||
|
const arc = getPathRideArc(head, centers[j], textWidth, fontSize);
|
||||||
|
const fade = getPathRideFade(arc, fontSize);
|
||||||
|
if (fade <= 0) return null;
|
||||||
|
const pose = getPathRidePoint(samples, arc, k, straight, lift);
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={j}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
fontFamily,
|
||||||
|
fontSize: size,
|
||||||
|
lineHeight: 1,
|
||||||
|
fontWeight,
|
||||||
|
fontVariationSettings: `"wght" ${fontWeight}`,
|
||||||
|
color,
|
||||||
|
whiteSpace: "pre",
|
||||||
|
opacity: fade,
|
||||||
|
transformOrigin: "0 0",
|
||||||
|
transform: `translate(${toX(pose.x)}px, ${toY(pose.y)}px) rotate(${pose.angle}rad) translate(-50%, ${-metrics.baseline * size}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{char}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
145
src/remocn/perspective-marquee.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCurrentFrame } from "remotion";
|
||||||
|
|
||||||
|
export interface PerspectiveMarqueeProps {
|
||||||
|
items?: string[];
|
||||||
|
fontSize?: number;
|
||||||
|
color?: string;
|
||||||
|
fontWeight?: number;
|
||||||
|
pixelsPerFrame?: number;
|
||||||
|
rotateY?: number;
|
||||||
|
rotateX?: number;
|
||||||
|
perspective?: number;
|
||||||
|
fadeColor?: string;
|
||||||
|
speed?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FONT_FAMILY =
|
||||||
|
"var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif";
|
||||||
|
|
||||||
|
const DEFAULT_ITEMS = [
|
||||||
|
"Vercel",
|
||||||
|
"Linear",
|
||||||
|
"Stripe",
|
||||||
|
"Figma",
|
||||||
|
"Notion",
|
||||||
|
"Raycast",
|
||||||
|
"Arc",
|
||||||
|
"Cursor",
|
||||||
|
];
|
||||||
|
|
||||||
|
export function PerspectiveMarquee({
|
||||||
|
items = DEFAULT_ITEMS,
|
||||||
|
fontSize = 84,
|
||||||
|
color = "#fafafa",
|
||||||
|
fontWeight = 700,
|
||||||
|
pixelsPerFrame = 2,
|
||||||
|
rotateY = -28,
|
||||||
|
rotateX = 8,
|
||||||
|
perspective = 1200,
|
||||||
|
fadeColor = "#050505",
|
||||||
|
speed = 1,
|
||||||
|
className,
|
||||||
|
}: PerspectiveMarqueeProps) {
|
||||||
|
const frame = useCurrentFrame() * speed;
|
||||||
|
|
||||||
|
// Approximate width of one full item set so the modulo wraps cleanly.
|
||||||
|
const itemPadding = fontSize * 0.9;
|
||||||
|
const approxItemWidth = items.reduce(
|
||||||
|
(acc, item) => acc + item.length * fontSize * 0.6 + itemPadding,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const offset = -((frame * pixelsPerFrame) % approxItemWidth);
|
||||||
|
|
||||||
|
// Render the row 3x so the loop is seamless even when rotated in 3D.
|
||||||
|
const rendered = [...items, ...items, ...items];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
overflow: "hidden",
|
||||||
|
perspective: `${perspective}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
transform: `rotateX(${rotateX}deg) rotateY(${rotateY}deg)`,
|
||||||
|
transformStyle: "preserve-3d",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
translate: `${offset}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{rendered.map((item, i) => {
|
||||||
|
// Approximate this item's center X (relative to the marquee row,
|
||||||
|
// including the rolling offset). Used to dial in depth-of-field blur
|
||||||
|
// and opacity by horizontal position on screen.
|
||||||
|
const itemCenter =
|
||||||
|
i * (approxItemWidth / items.length) +
|
||||||
|
approxItemWidth / items.length / 2 +
|
||||||
|
offset;
|
||||||
|
// Normalize roughly to [-1, 1] across a 1280px viewport.
|
||||||
|
const norm = (itemCenter - 640) / 640;
|
||||||
|
const distance = Math.min(1, Math.abs(norm));
|
||||||
|
const blurPx = distance * 6;
|
||||||
|
const opacity = 1 - distance * 0.4;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
fontFamily: FONT_FAMILY,
|
||||||
|
fontSize,
|
||||||
|
fontWeight,
|
||||||
|
color,
|
||||||
|
letterSpacing: "-0.03em",
|
||||||
|
paddingRight: itemPadding,
|
||||||
|
filter: `blur(${blurPx}px)`,
|
||||||
|
opacity,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Vignette to fade marquee edges into background */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
pointerEvents: "none",
|
||||||
|
background: `linear-gradient(90deg, ${fadeColor} 0%, transparent 18%, transparent 82%, ${fadeColor} 100%)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
pointerEvents: "none",
|
||||||
|
background: `linear-gradient(180deg, ${fadeColor} 0%, transparent 25%, transparent 75%, ${fadeColor} 100%)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
145
src/remocn/sheen-slide-in.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Easing, interpolate, useCurrentFrame } from "remotion";
|
||||||
|
|
||||||
|
export interface SheenSlideInProps {
|
||||||
|
text: string;
|
||||||
|
exitAt?: number;
|
||||||
|
fontSize?: number;
|
||||||
|
baseColor?: string;
|
||||||
|
sheenColor?: string;
|
||||||
|
fontWeight?: number;
|
||||||
|
speed?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SETTLE_AMP_EM = 1.8;
|
||||||
|
const SETTLE_TAU = 2.5;
|
||||||
|
const CREEP_AMP_EM = 0.28;
|
||||||
|
const CREEP_TAU = 12;
|
||||||
|
const FADE_FRAMES = 4;
|
||||||
|
const SHEEN_IN_START = 0;
|
||||||
|
const SHEEN_IN_END = 4;
|
||||||
|
const SHEEN_SWEEP_END = 48;
|
||||||
|
const SHEEN_OUT_START = 48;
|
||||||
|
const SHEEN_OUT_END = 58;
|
||||||
|
const EXIT_FRAMES = 4;
|
||||||
|
const EXIT_SCALE = 0.9;
|
||||||
|
|
||||||
|
export function sheenSlideInLength(exitAt = 60): number {
|
||||||
|
return exitAt + EXIT_FRAMES;
|
||||||
|
}
|
||||||
|
|
||||||
|
function travelEm(frame: number): number {
|
||||||
|
return (
|
||||||
|
SETTLE_AMP_EM * Math.exp(-frame / SETTLE_TAU) +
|
||||||
|
CREEP_AMP_EM * Math.exp(-frame / CREEP_TAU)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const exitEasing = Easing.out(Easing.quad);
|
||||||
|
|
||||||
|
export function SheenSlideIn({
|
||||||
|
text,
|
||||||
|
exitAt = 60,
|
||||||
|
fontSize = 72,
|
||||||
|
baseColor = "#18181b",
|
||||||
|
sheenColor = "#4f8ef7",
|
||||||
|
fontWeight = 400,
|
||||||
|
speed = 1,
|
||||||
|
className,
|
||||||
|
}: SheenSlideInProps) {
|
||||||
|
const frame = useCurrentFrame() * speed;
|
||||||
|
|
||||||
|
const x = fontSize * travelEm(frame);
|
||||||
|
|
||||||
|
const exit = interpolate(frame, [exitAt, exitAt + EXIT_FRAMES], [0, 1], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
easing: exitEasing,
|
||||||
|
});
|
||||||
|
|
||||||
|
const opacity = interpolate(frame, [0, FADE_FRAMES], [0, 1], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
});
|
||||||
|
|
||||||
|
const sheenOpacity =
|
||||||
|
interpolate(frame, [SHEEN_IN_START, SHEEN_IN_END], [0, 1], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
}) *
|
||||||
|
interpolate(frame, [SHEEN_OUT_START, SHEEN_OUT_END], [1, 0], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
});
|
||||||
|
|
||||||
|
const sheenPosition = interpolate(frame, [0, SHEEN_SWEEP_END], [160, -60], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
easing: Easing.bezier(0.42, 0, 0.58, 1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const textStyle: React.CSSProperties = {
|
||||||
|
fontSize,
|
||||||
|
fontWeight,
|
||||||
|
letterSpacing: "-0.02em",
|
||||||
|
lineHeight: 1.1,
|
||||||
|
margin: 0,
|
||||||
|
fontFamily:
|
||||||
|
"var(--font-geist-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif)",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (exit >= 1) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ position: "absolute", inset: 0, background: "transparent" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
background: "transparent",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
display: "inline-block",
|
||||||
|
opacity,
|
||||||
|
translate: `${x}px 0`,
|
||||||
|
scale: `${1 - (1 - EXIT_SCALE) * exit}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className={className} style={{ ...textStyle, color: baseColor }}>
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
style={{
|
||||||
|
...textStyle,
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
color: "transparent",
|
||||||
|
backgroundClip: "text",
|
||||||
|
WebkitBackgroundClip: "text",
|
||||||
|
backgroundImage: `linear-gradient(100deg, transparent 25%, ${sheenColor} 50%, transparent 75%)`,
|
||||||
|
backgroundSize: "250% 100%",
|
||||||
|
backgroundPosition: `${sheenPosition}% 50%`,
|
||||||
|
opacity: sheenOpacity,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
92
src/remocn/soft-blur-in.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Easing, interpolate, useCurrentFrame } from "remotion";
|
||||||
|
|
||||||
|
export interface SoftBlurInProps {
|
||||||
|
text: string;
|
||||||
|
blur?: number;
|
||||||
|
fontSize?: number;
|
||||||
|
color?: string;
|
||||||
|
fontWeight?: number;
|
||||||
|
speed?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SoftBlurIn({
|
||||||
|
text,
|
||||||
|
blur = 12,
|
||||||
|
fontSize = 72,
|
||||||
|
color = "#171717",
|
||||||
|
fontWeight = 600,
|
||||||
|
speed = 1,
|
||||||
|
className,
|
||||||
|
}: SoftBlurInProps) {
|
||||||
|
const frame = useCurrentFrame() * speed;
|
||||||
|
|
||||||
|
const chars = Array.from(text);
|
||||||
|
const charDurationFrames = 27;
|
||||||
|
const charTravelFrames = 9;
|
||||||
|
const staggerFrames = 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
background: "transparent",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
fontSize,
|
||||||
|
fontWeight,
|
||||||
|
color,
|
||||||
|
letterSpacing: "-0.05em",
|
||||||
|
fontFamily:
|
||||||
|
"var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{chars.map((char, i) => {
|
||||||
|
const local = frame - i * staggerFrames;
|
||||||
|
const easing = Easing.bezier(0.22, 1, 0.36, 1);
|
||||||
|
const opacity = interpolate(local, [0, charDurationFrames], [0, 1], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
easing,
|
||||||
|
});
|
||||||
|
const y = interpolate(local, [0, charTravelFrames], [16, 0], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
easing,
|
||||||
|
});
|
||||||
|
const blurAmount = interpolate(
|
||||||
|
local,
|
||||||
|
[0, charDurationFrames],
|
||||||
|
[blur, 0],
|
||||||
|
{ extrapolateLeft: "clamp", extrapolateRight: "clamp", easing },
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
whiteSpace: "pre",
|
||||||
|
backfaceVisibility: "hidden",
|
||||||
|
transformOrigin: "50% 55%",
|
||||||
|
opacity,
|
||||||
|
translate: `0 ${y}px`,
|
||||||
|
filter: `blur(${blurAmount}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{char}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
71
src/remocn/tracking-in.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
|
||||||
|
|
||||||
|
export interface TrackingInProps {
|
||||||
|
text: string;
|
||||||
|
startTracking?: number;
|
||||||
|
startBlur?: number;
|
||||||
|
fontSize?: number;
|
||||||
|
color?: string;
|
||||||
|
fontWeight?: number;
|
||||||
|
speed?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TrackingIn({
|
||||||
|
text,
|
||||||
|
startTracking = 0.5,
|
||||||
|
startBlur = 12,
|
||||||
|
fontSize = 96,
|
||||||
|
color = "#171717",
|
||||||
|
fontWeight = 700,
|
||||||
|
speed = 1,
|
||||||
|
className,
|
||||||
|
}: TrackingInProps) {
|
||||||
|
const frame = useCurrentFrame() * speed;
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
|
||||||
|
const t = spring({
|
||||||
|
frame,
|
||||||
|
fps,
|
||||||
|
config: { damping: 18, stiffness: 90 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const letterSpacing = `${interpolate(t, [0, 1], [startTracking, -0.03])}em`;
|
||||||
|
const blurAmount = interpolate(t, [0, 1], [startBlur, 0]);
|
||||||
|
const opacity = interpolate(frame, [0, 15], [0, 1], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
background: "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
fontSize,
|
||||||
|
fontWeight,
|
||||||
|
color,
|
||||||
|
letterSpacing,
|
||||||
|
opacity,
|
||||||
|
filter: `blur(${blurAmount}px)`,
|
||||||
|
fontFamily:
|
||||||
|
"var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
src/remocn/zoom-blur.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
TransitionPresentation,
|
||||||
|
TransitionPresentationComponentProps,
|
||||||
|
} from "@remotion/transitions";
|
||||||
|
import type React from "react";
|
||||||
|
import { AbsoluteFill } from "remotion";
|
||||||
|
|
||||||
|
export type ZoomBlurProps = {
|
||||||
|
blur?: number;
|
||||||
|
rise?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ZoomBlurPresentation: React.FC<
|
||||||
|
TransitionPresentationComponentProps<ZoomBlurProps>
|
||||||
|
> = ({
|
||||||
|
children,
|
||||||
|
presentationProgress,
|
||||||
|
presentationDirection,
|
||||||
|
passedProps,
|
||||||
|
}) => {
|
||||||
|
const { blur = 16, rise = 0 } = passedProps;
|
||||||
|
const entering = presentationDirection === "entering";
|
||||||
|
const p = presentationProgress;
|
||||||
|
|
||||||
|
const style: React.CSSProperties = entering
|
||||||
|
? {
|
||||||
|
opacity: p,
|
||||||
|
transform: `scale(${0.9 + p * 0.1})${
|
||||||
|
rise > 0 ? ` translateY(${(1 - p) * rise}px)` : ""
|
||||||
|
}`,
|
||||||
|
filter: p < 1 ? `blur(${(1 - p) * blur}px)` : undefined,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
opacity: 1 - p,
|
||||||
|
transform: `scale(${1 + p * 0.12})${
|
||||||
|
rise > 0 ? ` translateY(${-p * rise}px)` : ""
|
||||||
|
}`,
|
||||||
|
filter: p > 0 ? `blur(${p * blur}px)` : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <AbsoluteFill style={style}>{children}</AbsoluteFill>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function zoomBlur(
|
||||||
|
props: ZoomBlurProps = {},
|
||||||
|
): TransitionPresentation<ZoomBlurProps> {
|
||||||
|
return {
|
||||||
|
component: ZoomBlurPresentation,
|
||||||
|
props,
|
||||||
|
};
|
||||||
|
}
|
||||||
252
src/sahneler-1.tsx
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
import { measureText } from "@remotion/layout-utils";
|
||||||
|
import { AbsoluteFill, Easing, interpolate, useCurrentFrame } from "remotion";
|
||||||
|
import { IzAc, PikselDevKesme, PikselMarquee, PikselSheen } from "./kinetik";
|
||||||
|
import { LogoMark } from "./logo";
|
||||||
|
import { Sahne, useDuzen } from "./ortak";
|
||||||
|
import { Eyebrow, KelimeKelime, Wordmark } from "./piksel";
|
||||||
|
import { Kamera, MaskeSatir, OdakDisiPikseller } from "./sinema";
|
||||||
|
import { FONT, R, ara, hash, trSayi, zipla } from "./tema";
|
||||||
|
import { VURUSLAR } from "./vurus";
|
||||||
|
|
||||||
|
export const vurguStil = {
|
||||||
|
fontFamily: FONT.vurgu,
|
||||||
|
fontStyle: "italic" as const,
|
||||||
|
fontWeight: 800,
|
||||||
|
letterSpacing: "-0.04em",
|
||||||
|
color: R.primary,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Açılış — boş ekran → "45.320." (Track Text) → tebrik */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
export function Acilis() {
|
||||||
|
const { dikey } = useDuzen();
|
||||||
|
return (
|
||||||
|
<Sahne>
|
||||||
|
<Kamera anahtarlar={[{ f: 0, s: 1.08 }, { f: 110, s: 1 }]} vuruslar={VURUSLAR.acilis}>
|
||||||
|
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center", gap: 44, textAlign: "center" }}>
|
||||||
|
<MaskeSatir gecikme={8} sure={18}>
|
||||||
|
<Eyebrow olcek={dikey ? 2.8 : 2.4}>YKS sonuçları açıklandı</Eyebrow>
|
||||||
|
</MaskeSatir>
|
||||||
|
<IzAc gecikme={14} fontSize={dikey ? 250 : 240}>
|
||||||
|
45.320<span style={{ color: R.primary }}>.</span>
|
||||||
|
</IzAc>
|
||||||
|
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 88 : 84, lineHeight: 1.1, maxWidth: dikey ? 960 : 1600 }}>
|
||||||
|
<MaskeSatir gecikme={42}>Sıralaman geldi. Tebrikler!</MaskeSatir>
|
||||||
|
<MaskeSatir gecikme={56} style={{ color: R.slate500 }}>
|
||||||
|
Sırada küçük bir <span style={vurguStil}>bulmaca</span> var.
|
||||||
|
</MaskeSatir>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
</Kamera>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Seçenekler — Perspective Marquee: akıp giden bölümler, 21.000+ / 24 */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
const UST_SIRA = ["Bilgisayar Mühendisliği", "Hukuk", "Tıp", "Psikoloji", "Mimarlık", "İşletme", "Hemşirelik"];
|
||||||
|
const ALT_SIRA = ["Diş Hekimliği", "Endüstri Mühendisliği", "Öğretmenlik", "Uluslararası İlişkiler", "Yazılım Mühendisliği", "Eczacılık"];
|
||||||
|
|
||||||
|
export function Secenek() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey, h } = useDuzen();
|
||||||
|
const sayi = ara(frame, [4, 40], [0, 21000], Easing.bezier(0.16, 1, 0.3, 1));
|
||||||
|
const arti = zipla(frame, 38);
|
||||||
|
return (
|
||||||
|
<Sahne>
|
||||||
|
<Kamera anahtarlar={[{ f: 0, s: 1.02 }, { f: 125, s: 1.12 }]} vuruslar={VURUSLAR.secenek}>
|
||||||
|
<div style={{ position: "absolute", left: 0, right: 0, top: h * (dikey ? 0.2 : 0.14) - 60 }}>
|
||||||
|
<PikselMarquee ogeler={UST_SIRA} fontSize={dikey ? 100 : 96} hiz={6} renk={R.slate400} donY={-22} donX={10} />
|
||||||
|
</div>
|
||||||
|
<div style={{ position: "absolute", left: 0, right: 0, top: h * (dikey ? 0.8 : 0.86) - 60 }}>
|
||||||
|
<PikselMarquee ogeler={ALT_SIRA} fontSize={dikey ? 100 : 96} hiz={5} renk={R.slate400} donY={22} donX={-10} ters />
|
||||||
|
</div>
|
||||||
|
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center", textAlign: "center" }}>
|
||||||
|
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 230 : 240, lineHeight: 1, color: R.slate900, letterSpacing: "-0.01em" }}>
|
||||||
|
{trSayi(sayi)}
|
||||||
|
<span style={{ display: "inline-block", color: R.primary, transform: `scale(${Math.max(0, arti)})` }}>+</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 16, fontSize: dikey ? 46 : 42, fontWeight: 600, color: R.slate500 }}>program var.</div>
|
||||||
|
<div style={{ marginTop: dikey ? 70 : 50 }}>
|
||||||
|
<PikselSheen gecikme={50} fontSize={dikey ? 86 : 88}>
|
||||||
|
Senin <span style={{ color: R.primary }}>24</span> hakkın var.
|
||||||
|
</PikselSheen>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
</Kamera>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Kaos — sekmeler gibi açılan sorular, sonra ani sessizlik */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
const SORULAR = [
|
||||||
|
"Geçen yılın tabanı yeter mi?",
|
||||||
|
"Kontenjan düştü mü?",
|
||||||
|
"Hangi şehir?",
|
||||||
|
"Devlet mi, vakıf mı?",
|
||||||
|
"Danışman 3.000 TL mi?!",
|
||||||
|
"Teyzem “hukuk yaz” dedi",
|
||||||
|
"Ya boşta kalırsam?",
|
||||||
|
"Hangisi kaçıncı sıraya?",
|
||||||
|
];
|
||||||
|
const YER_YATAY = [
|
||||||
|
[-0.62, -0.62], [0.55, -0.7], [-0.78, -0.08], [0.74, -0.18],
|
||||||
|
[-0.6, 0.55], [0.58, 0.52], [-0.12, 0.8], [0.1, -0.86],
|
||||||
|
];
|
||||||
|
const YER_DIKEY = [
|
||||||
|
[-0.38, -0.74], [0.36, -0.6], [-0.42, -0.42], [0.4, -0.3],
|
||||||
|
[-0.36, 0.34], [0.38, 0.48], [-0.3, 0.66], [0.32, 0.8],
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Kaos() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey, w, h } = useDuzen();
|
||||||
|
const yerler = dikey ? YER_DIKEY : YER_YATAY;
|
||||||
|
// Sonda kartlar hızlanır, ekran doluyor hissi; sonra sessizliğe sert kesme
|
||||||
|
const telas = ara(frame, [60, 104], [0, 1], Easing.in(Easing.quad));
|
||||||
|
return (
|
||||||
|
<Sahne>
|
||||||
|
<Kamera anahtarlar={[{ f: 0, s: 0.96, r: -1.2 }, { f: 104, s: 1.14, r: 0.8 }]} vuruslar={VURUSLAR.kaos}>
|
||||||
|
{SORULAR.map((s, i) => {
|
||||||
|
const [nx, ny] = yerler[i];
|
||||||
|
const g = zipla(frame, 6 + i * 5);
|
||||||
|
const hizli = 1 + telas * 3;
|
||||||
|
const yuzX = Math.sin(((frame + i * 20) / 18) * hizli) * (16 + telas * 20);
|
||||||
|
const yuzY = Math.cos(((frame + i * 13) / 15) * hizli) * (14 + telas * 18);
|
||||||
|
// Sırayla "sekme değiştirme" vurgusu
|
||||||
|
const aktif = Math.floor(frame / 9) % SORULAR.length === i && frame > 20;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={s}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: w / 2 + nx * (w / 2) + yuzX,
|
||||||
|
top: h / 2 + ny * (h / 2) + yuzY,
|
||||||
|
transform: `translate(-50%, -50%) scale(${Math.max(0, g) * (aktif ? 1.07 : 1)}) rotate(${(hash(i, 1, 2) - 0.5) * 10}deg)`,
|
||||||
|
background: aktif ? "#fff7ed" : R.beyaz,
|
||||||
|
border: `3px solid ${aktif ? R.turuncu : R.slate200}`,
|
||||||
|
borderRadius: 30,
|
||||||
|
padding: dikey ? "24px 32px" : "22px 30px",
|
||||||
|
fontSize: dikey ? 38 : 34,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: aktif ? R.turuncu : R.slate700,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
boxShadow: "0 12px 30px rgba(15,23,42,0.08)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{s}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<h2
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
textAlign: "center",
|
||||||
|
fontFamily: FONT.baslik,
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: 104,
|
||||||
|
lineHeight: 1.08,
|
||||||
|
maxWidth: dikey ? 800 : 1200,
|
||||||
|
background: `radial-gradient(closest-side, ${R.slate50} 60%, rgba(248,250,252,0))`,
|
||||||
|
padding: "40px 60px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<KelimeKelime metin="Kafanda yüz sekme açık, değil mi?" gecikme={4} aralik={3} vurgu={["yüz", "sekme"]} />
|
||||||
|
</h2>
|
||||||
|
</AbsoluteFill>
|
||||||
|
</Kamera>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Nefes — sessizlik. Tek bir turuncu piksel nefes alır. */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
export function Nefes() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey } = useDuzen();
|
||||||
|
const nefes = 1 + 0.12 * Math.sin((frame / 30) * Math.PI * 1.1);
|
||||||
|
const kare = dikey ? 64 : 56;
|
||||||
|
return (
|
||||||
|
<Sahne zemin={R.beyaz}>
|
||||||
|
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center", gap: 56 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: kare,
|
||||||
|
height: kare,
|
||||||
|
borderRadius: kare * 0.23,
|
||||||
|
background: R.logoTuruncu,
|
||||||
|
opacity: interpolate(frame, [2, 12], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }),
|
||||||
|
transform: `scale(${nefes})`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 64 : 60, color: R.slate400 }}>
|
||||||
|
<MaskeSatir gecikme={10} sure={24}>
|
||||||
|
Derin bir nefes al.
|
||||||
|
</MaskeSatir>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Logo — BAM: dev piksel kelime süpürür, logoya kesilir, slogan sheen */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
export function LogoSahnesi() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey } = useDuzen();
|
||||||
|
const markBoy = dikey ? 250 : 230;
|
||||||
|
const yaziBoy = dikey ? 130 : 150;
|
||||||
|
// Kilit: işaret + yazı (yatayda yan yana, dikeyde alt alta)
|
||||||
|
const yaziGen =
|
||||||
|
measureText({ text: "Kolay", fontFamily: "Bricolage", fontSize: yaziBoy, fontWeight: "300", letterSpacing: `${-0.05 * yaziBoy}px` }).width +
|
||||||
|
measureText({ text: "Tercih", fontFamily: "Bricolage", fontSize: yaziBoy, fontWeight: "600", letterSpacing: `${-0.05 * yaziBoy}px` }).width;
|
||||||
|
const kilit = (
|
||||||
|
<div style={{ display: "flex", flexDirection: dikey ? "column" : "row", alignItems: "center", gap: dikey ? 10 : 24, transform: `translateY(${dikey ? -170 : -120}px)` }}>
|
||||||
|
<LogoMark boy={markBoy} girisYok rotaBas={32} />
|
||||||
|
<div style={{ width: dikey ? undefined : yaziGen }}>
|
||||||
|
<Wordmark boy={yaziBoy} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Sahne zemin={R.beyaz}>
|
||||||
|
<OdakDisiPikseller seed={3} adet={8} guc={ara(frame, [30, 60], [0, 1])} />
|
||||||
|
<Kamera anahtarlar={[{ f: 0, s: 1.1 }, { f: 130, s: 1 }]} vuruslar={VURUSLAR.logo}>
|
||||||
|
<PikselDevKesme metin="KolayTercih" devBoy={dikey ? 420 : 520} gecikme={8} kesme={18}>
|
||||||
|
{kilit}
|
||||||
|
</PikselDevKesme>
|
||||||
|
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center", textAlign: "center" }}>
|
||||||
|
<div style={{ marginTop: dikey ? 560 : 400, display: "flex", flexDirection: "column", alignItems: "center", gap: 6 }}>
|
||||||
|
{dikey ? (
|
||||||
|
<>
|
||||||
|
<PikselSheen gecikme={46} fontSize={76}>
|
||||||
|
Tercih listeni
|
||||||
|
</PikselSheen>
|
||||||
|
<PikselSheen gecikme={52} fontSize={76}>
|
||||||
|
gerçek veriyle kur,
|
||||||
|
</PikselSheen>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<PikselSheen gecikme={46} fontSize={80}>
|
||||||
|
Tercih listeni gerçek veriyle kur,
|
||||||
|
</PikselSheen>
|
||||||
|
)}
|
||||||
|
<div style={{ fontSize: dikey ? 76 : 80, lineHeight: 1.1 }}>
|
||||||
|
<MaskeSatir gecikme={62}>
|
||||||
|
<span style={vurguStil}>panikle</span>
|
||||||
|
<span style={{ fontFamily: FONT.baslik, fontWeight: 700, color: R.slate900 }}> değil.</span>
|
||||||
|
</MaskeSatir>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
</Kamera>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
418
src/sahneler-2.tsx
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
import { Easing, useCurrentFrame } from "remotion";
|
||||||
|
import { HARITA_SATIR, HARITA_SUTUN, YagmurHaritasi } from "./harita";
|
||||||
|
import { Ikon } from "./ikonlar";
|
||||||
|
import { SahneImleci, TakipKamera, imlecKonumu, type Durak } from "./imlec";
|
||||||
|
import { PikselKonturDolgu, PikselSheen, PikselYolSurus } from "./kinetik";
|
||||||
|
import { RiskEtiketi, Sahne, TuruncuCta, useDuzen } from "./ortak";
|
||||||
|
import { OdakDisiPikseller } from "./sinema";
|
||||||
|
import { FONT, R, ara, trSayi, yay, zipla } from "./tema";
|
||||||
|
import { VURUSLAR } from "./vurus";
|
||||||
|
import ZAMAN from "./zaman.json";
|
||||||
|
|
||||||
|
/** Adım başlığı: turuncu piksel rozetinde numara + Sheen Slide In başlık. */
|
||||||
|
function AdimBasligi({ no, metin, alt, dikey }: { no: number; metin: string; alt?: string; dikey: boolean }) {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const r = zipla(frame, 2);
|
||||||
|
const kare = dikey ? 96 : 88;
|
||||||
|
return (
|
||||||
|
<div style={{ position: "absolute", left: dikey ? 70 : 140, top: dikey ? 130 : 70, display: "flex", flexDirection: "column", gap: 14 }}>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 26 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: kare,
|
||||||
|
height: kare,
|
||||||
|
borderRadius: kare * 0.26,
|
||||||
|
background: R.turuncu,
|
||||||
|
color: R.beyaz,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontFamily: FONT.baslik,
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: kare * 0.56,
|
||||||
|
transform: `scale(${Math.max(0, r)})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{no}
|
||||||
|
</div>
|
||||||
|
<PikselSheen gecikme={4} fontSize={dikey ? 76 : 80}>
|
||||||
|
{metin}
|
||||||
|
</PikselSheen>
|
||||||
|
</div>
|
||||||
|
{alt ? (
|
||||||
|
<div style={{ marginLeft: kare + 26, fontSize: dikey ? 34 : 32, color: R.slate500, opacity: yay(frame, 14, 14) }}>{alt}</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Adım 1 — tam ekran sihirbaz; kamera imlece kilitlenir */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
const CIPLER = ["Mühendislik", "Bilgisayar", "İstanbul", "Ankara", "İzmir", "Devlet"];
|
||||||
|
const T1 = ZAMAN.tiklar.adim1; // [girdi, çip0, çip1, çip2, çip5, CTA]
|
||||||
|
const SECIM: Record<number, number> = { 0: T1[1], 1: T1[2], 2: T1[3], 5: T1[4] };
|
||||||
|
|
||||||
|
export function Adim1() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey } = useDuzen();
|
||||||
|
// Yerleşim (px): dikey 1080×1920, yatay 1920×1080
|
||||||
|
const L = dikey
|
||||||
|
? { x: 70, w: 940, etiket1: 380, girdi: [440, 160], etiket2: 700, cip: { y: 770, h: 110, w: 455, ara: 30, sut: 2 }, cta: [1240, 140], kare: [1480, 62] }
|
||||||
|
: { x: 140, w: 1640, etiket1: 250, girdi: [300, 140], etiket2: 500, cip: { y: 560, h: 100, w: 255, ara: 22, sut: 6 }, cta: [730, 130], kare: [920, 52] };
|
||||||
|
const cipMerkez = (i: number): [number, number] => {
|
||||||
|
const c = i % L.cip.sut;
|
||||||
|
const r = Math.floor(i / L.cip.sut);
|
||||||
|
return [L.x + c * (L.cip.w + L.cip.ara) + L.cip.w / 2, L.cip.y + r * (L.cip.h + L.cip.ara) + L.cip.h / 2];
|
||||||
|
};
|
||||||
|
const girdiY = L.girdi[0] + L.girdi[1] / 2;
|
||||||
|
const ctaY = L.cta[0] + L.cta[1] / 2;
|
||||||
|
const duraklar: Durak[] = [
|
||||||
|
{ f: 16, x: dikey ? 820 : 1500, y: dikey ? 1520 : 900 },
|
||||||
|
{ f: T1[0], x: L.x + 230, y: girdiY + 20, g: 14 },
|
||||||
|
{ f: T1[1], x: cipMerkez(0)[0], y: cipMerkez(0)[1], g: 12 },
|
||||||
|
{ f: T1[2], x: cipMerkez(1)[0], y: cipMerkez(1)[1], g: 8 },
|
||||||
|
{ f: T1[3], x: cipMerkez(2)[0], y: cipMerkez(2)[1], g: 8 },
|
||||||
|
{ f: T1[4], x: cipMerkez(5)[0], y: cipMerkez(5)[1], g: 8 },
|
||||||
|
{ f: T1[5], x: L.x + L.w * 0.6, y: ctaY, g: 16 },
|
||||||
|
{ f: 152, x: dikey ? 780 : 1420, y: dikey ? 1600 : 980, g: 20 },
|
||||||
|
];
|
||||||
|
const nokta = (f: number) => imlecKonumu(duraklar, f);
|
||||||
|
|
||||||
|
const metin = "45.320";
|
||||||
|
const [yb, ys] = ZAMAN.yazma.adim1[0];
|
||||||
|
const yazilan = metin.slice(0, Math.max(0, Math.floor(((frame - yb) / (ys - yb)) * metin.length + 0.5)));
|
||||||
|
const odak = frame >= T1[0];
|
||||||
|
const imlecGor = Math.floor(frame / 8) % 2 === 0 || (frame >= yb && frame <= ys);
|
||||||
|
const cta = zipla(frame, 104);
|
||||||
|
const bas = ara(frame, [T1[5] - 2, T1[5] + 1], [0, 1]) * (1 - ara(frame, [T1[5] + 5, T1[5] + 10], [0, 1]));
|
||||||
|
const u = dikey ? 1 : 0.92; // yazı ölçeği
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sahne>
|
||||||
|
<OdakDisiPikseller seed={11} adet={6} guc={0.6} />
|
||||||
|
<TakipKamera
|
||||||
|
nokta={nokta}
|
||||||
|
vuruslar={VURUSLAR.adim1}
|
||||||
|
anahtarlar={[
|
||||||
|
{ f: 0, s: 1, t: 0 },
|
||||||
|
{ f: 18, s: 1.02, t: 0 },
|
||||||
|
{ f: T1[0], s: 1.5, t: 1 },
|
||||||
|
{ f: ys + 2, s: 1.5, t: 1 },
|
||||||
|
{ f: T1[1] - 4, s: 1.32, t: 0.9 },
|
||||||
|
{ f: T1[4] + 4, s: 1.32, t: 0.9 },
|
||||||
|
{ f: T1[5] - 10, s: 1.1, t: 0.5 },
|
||||||
|
{ f: T1[5] + 4, s: 1.18, t: 0.6 },
|
||||||
|
{ f: 142, s: 1, t: 0 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<AdimBasligi no={1} metin="Sıralamanı yaz." alt="Bir de neyi sevdiğini. O kadar." dikey={dikey} />
|
||||||
|
<div style={{ position: "absolute", left: L.x, top: L.etiket1, fontSize: 36 * u, fontWeight: 600, color: R.slate700 }}>Başarı sıralaman</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: L.x,
|
||||||
|
top: L.girdi[0],
|
||||||
|
width: L.w,
|
||||||
|
height: L.girdi[1],
|
||||||
|
borderRadius: 999,
|
||||||
|
background: R.beyaz,
|
||||||
|
border: `4px solid ${odak ? R.primary : R.slate200}`,
|
||||||
|
boxShadow: odak ? "0 0 0 10px rgba(59,130,246,0.18)" : "0 2px 6px rgba(15,23,42,0.04)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 30,
|
||||||
|
padding: "0 56px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ikon ad="kupa" boy={54 * u} renk={R.slate400} />
|
||||||
|
<span style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: 80 * u, color: R.slate900 }}>
|
||||||
|
{yazilan}
|
||||||
|
{odak && imlecGor ? <span style={{ color: R.primary, fontFamily: FONT.govde, fontWeight: 400 }}>|</span> : null}
|
||||||
|
{!odak ? <span style={{ color: R.slate300, fontFamily: FONT.govde, fontWeight: 400, fontSize: 56 * u }}>Örn. 45.320</span> : null}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ position: "absolute", left: L.x, top: L.etiket2, fontSize: 36 * u, fontWeight: 600, color: R.slate700 }}>
|
||||||
|
Neyi seviyorsun, nerede okumak istersin?
|
||||||
|
</div>
|
||||||
|
{CIPLER.map((c, i) => {
|
||||||
|
const [cx, cy] = cipMerkez(i);
|
||||||
|
const an = SECIM[i];
|
||||||
|
const secili = an != null && frame >= an;
|
||||||
|
const tik = an != null ? ara(frame, [an, an + 2], [0, 1]) * (1 - ara(frame, [an + 3, an + 8], [0, 1])) : 0;
|
||||||
|
const gir = yay(frame, 8 + i * 2, 14);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={c}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: cx - L.cip.w / 2,
|
||||||
|
top: cy - L.cip.h / 2,
|
||||||
|
width: L.cip.w,
|
||||||
|
height: L.cip.h,
|
||||||
|
borderRadius: 999,
|
||||||
|
border: `4px solid ${secili ? R.primary : R.slate200}`,
|
||||||
|
background: secili ? R.primary : R.beyaz,
|
||||||
|
color: secili ? R.beyaz : R.slate700,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 14,
|
||||||
|
fontSize: (dikey ? 42 : 34) * u,
|
||||||
|
fontWeight: 600,
|
||||||
|
opacity: gir,
|
||||||
|
transform: `scale(${(0.92 + 0.08 * gir) * (1 - 0.05 * tik)})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i >= 2 && i <= 4 ? <Ikon ad="konum" boy={34 * u} /> : i === 5 ? <Ikon ad="devlet" boy={34 * u} /> : null}
|
||||||
|
{c}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div style={{ position: "absolute", left: L.x, top: L.cta[0], width: L.w, opacity: Math.min(1, Math.max(0, cta)), transform: `scale(${0.85 + 0.15 * cta})` }}>
|
||||||
|
<TuruncuCta metin="Listemi oluştur" boy={dikey ? 1.3 : 1.2} basili={bas} style={{ width: "100%", height: L.cta[1] }} />
|
||||||
|
</div>
|
||||||
|
{/* CTA'dan sonra: 24 piksel dolar (liste hazırlanıyor) */}
|
||||||
|
<div style={{ position: "absolute", left: L.x, top: L.kare[0], width: L.w, display: "flex", flexDirection: "column", gap: 18, opacity: yay(frame, T1[5] + 6, 12) }}>
|
||||||
|
<div style={{ fontSize: 34 * u, fontWeight: 600, color: R.slate500 }}>Listen hazırlanıyor…</div>
|
||||||
|
<div style={{ display: "grid", gridTemplateColumns: `repeat(${dikey ? 12 : 24}, ${L.kare[1]}px)`, gap: dikey ? 16 : 14 }}>
|
||||||
|
{Array.from({ length: 24 }, (_, i) => {
|
||||||
|
const on = frame >= T1[5] + 10 + i * 1.1;
|
||||||
|
const renk = i < 5 ? R.riskli : i < 18 ? R.azRiskli : R.guvenli;
|
||||||
|
return <span key={i} style={{ width: L.kare[1], height: L.kare[1], borderRadius: L.kare[1] * 0.26, background: on ? renk : R.slate100 }} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SahneImleci duraklar={duraklar} tiklar={T1} gorunur={[16, 152]} boy={dikey ? 76 : 64} />
|
||||||
|
</TakipKamera>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Adım 2 — harita dökülür; imleç ısı fırçası, kamera ona kilitlenir */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
const FB = 50; // fırça başlangıcı
|
||||||
|
const FS = 72; // fırça süresi
|
||||||
|
const fircaYol = (p: number) => {
|
||||||
|
const e = Easing.inOut(Easing.sin)(p);
|
||||||
|
return { x: 3 + e * (HARITA_SUTUN - 7), y: 12 + Math.sin(p * Math.PI * 2) * 6 };
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Adim2() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey, w } = useDuzen();
|
||||||
|
const haritaW = dikey ? 1040 : 1100;
|
||||||
|
const hucre = haritaW / HARITA_SUTUN;
|
||||||
|
const hx = (w - haritaW) / 2;
|
||||||
|
const hy = dikey ? 470 : 150;
|
||||||
|
const ekran = (p: number): [number, number] => {
|
||||||
|
const g = fircaYol(Math.min(1, Math.max(0, p)));
|
||||||
|
return [hx + g.x * hucre, hy + g.y * hucre];
|
||||||
|
};
|
||||||
|
// İmleç fırçanın ucunda: yolun örnekleri (2 karede bir) doğrusal duraklar
|
||||||
|
const duraklar: Durak[] = [{ f: 38, x: ekran(0)[0] - 80, y: ekran(0)[1] + 120 }, { f: FB, x: ekran(0)[0], y: ekran(0)[1], g: 12 }];
|
||||||
|
for (let f = FB + 2; f <= FB + FS; f += 2) {
|
||||||
|
const [x, y] = ekran((f - FB) / FS);
|
||||||
|
duraklar.push({ f, x, y, dogrusal: true });
|
||||||
|
}
|
||||||
|
duraklar.push({ f: FB + FS + 16, x: ekran(1)[0] + 60, y: ekran(1)[1] + 140, g: 14 });
|
||||||
|
const nokta = (f: number) => imlecKonumu(duraklar, f);
|
||||||
|
const kilit = yay(frame, 150, 18);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sahne zemin={R.beyaz}>
|
||||||
|
<TakipKamera
|
||||||
|
nokta={nokta}
|
||||||
|
vuruslar={VURUSLAR.adim2}
|
||||||
|
anahtarlar={[
|
||||||
|
{ f: 0, s: 1.08, t: 0 },
|
||||||
|
{ f: 40, s: 1.1, t: 0.3 },
|
||||||
|
{ f: FB + 6, s: 1.75, t: 1 },
|
||||||
|
{ f: FB + FS - 4, s: 1.75, t: 1 },
|
||||||
|
{ f: FB + FS + 18, s: 1, t: 0 },
|
||||||
|
{ f: 200, s: 1.03, t: 0 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<AdimBasligi no={2} metin="Tabloları biz okuruz." alt="6 yılın resmî YÖK Atlas verisi, tek taramada." dikey={dikey} />
|
||||||
|
<div style={{ position: "absolute", left: hx, top: hy }}>
|
||||||
|
<YagmurHaritasi genislik={haritaW} baslangic={4} dokulme={40} fircaBas={FB} fircaSure={FS} fircaYol={fircaYol} />
|
||||||
|
</div>
|
||||||
|
<div style={{ position: "absolute", left: 0, width: w, top: hy + HARITA_SATIR * hucre + (dikey ? 30 : 0) }}>
|
||||||
|
<PikselKonturDolgu on="Taranan:" deger="21.000+ program" fontSize={dikey ? 74 : 88} gecikme={FB + FS + 4} genislik={w} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
width: w,
|
||||||
|
top: hy + HARITA_SATIR * hucre + (dikey ? 180 : 130),
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 22,
|
||||||
|
opacity: kilit,
|
||||||
|
transform: `translateY(${(1 - kilit) * 20}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ width: 80, height: 80, borderRadius: 22, background: R.primary, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
|
||||||
|
<Ikon ad="kalkan" boy={42} renk={R.beyaz} />
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: dikey ? 36 : 38, fontWeight: 600, color: R.slate800, lineHeight: 1.3, maxWidth: dikey ? 820 : 1400 }}>
|
||||||
|
Yapay zekâ bu havuzun dışına çıkamaz. <span style={{ color: R.primary }}>Uydurmak yok.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SahneImleci duraklar={duraklar} gorunur={[38, FB + FS + 16]} boy={dikey ? 76 : 64} />
|
||||||
|
</TakipKamera>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Adım 3 — Path Ride + 24 kare; imleç her yeni kareyi takip eder */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
const DILIMLER = [
|
||||||
|
{ ad: "Hayal", adet: 5, renk: R.riskli },
|
||||||
|
{ ad: "Dengeli", adet: 13, renk: R.azRiskli },
|
||||||
|
{ ad: "Güvenli", adet: 6, renk: R.guvenli },
|
||||||
|
] as const;
|
||||||
|
const KARELER = DILIMLER.flatMap((d) => Array.from({ length: d.adet }, () => d));
|
||||||
|
const DOLUM = 62; // ilk karenin karesi
|
||||||
|
|
||||||
|
export function Adim3() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey, w } = useDuzen();
|
||||||
|
const G = dikey ? { sut: 6, kare: 130, ara: 18, y: 820 } : { sut: 12, kare: 108, ara: 16, y: 560 };
|
||||||
|
const izW = G.sut * G.kare + (G.sut - 1) * G.ara;
|
||||||
|
const gx = (w - izW) / 2;
|
||||||
|
const kareMerkez = (i: number): [number, number] => [
|
||||||
|
gx + (i % G.sut) * (G.kare + G.ara) + G.kare / 2,
|
||||||
|
G.y + Math.floor(i / G.sut) * (G.kare + G.ara) + G.kare / 2,
|
||||||
|
];
|
||||||
|
const satirSayisi = Math.ceil(24 / G.sut);
|
||||||
|
const lejantY = G.y + satirSayisi * (G.kare + G.ara) + 20;
|
||||||
|
const ornekY = lejantY + (dikey ? 130 : 110);
|
||||||
|
const duraklar: Durak[] = [{ f: DOLUM - 8, x: kareMerkez(0)[0] - 60, y: kareMerkez(0)[1] + 150 }];
|
||||||
|
for (let i = 0; i < 24; i++) {
|
||||||
|
const [x, y] = kareMerkez(i);
|
||||||
|
duraklar.push({ f: DOLUM + i * 2, x: x + 10, y: y + 20, ...(i === 0 ? { g: 8 } : { dogrusal: true }) });
|
||||||
|
}
|
||||||
|
duraklar.push({ f: 124, x: w / 2 + 40, y: lejantY + 50, g: 14 });
|
||||||
|
duraklar.push({ f: 146, x: w / 2 + 140, y: ornekY + 60, g: 14 });
|
||||||
|
const nokta = (f: number) => imlecKonumu(duraklar, f);
|
||||||
|
const yolKap = dikey
|
||||||
|
? { y: 250, w: 1080, h: 520, yol: "M 80 420 C 300 60 520 60 560 260 S 820 470 1000 120", taban: 320, font: 84 }
|
||||||
|
: { y: 150, w: 1920, h: 360, yol: "M 260 300 C 560 40 860 40 960 180 S 1360 330 1660 60", taban: 230, font: 96 };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sahne>
|
||||||
|
<OdakDisiPikseller seed={31} adet={6} guc={0.6} />
|
||||||
|
<TakipKamera
|
||||||
|
nokta={nokta}
|
||||||
|
vuruslar={VURUSLAR.adim3}
|
||||||
|
anahtarlar={[
|
||||||
|
{ f: 0, s: 1, t: 0 },
|
||||||
|
{ f: DOLUM - 6, s: 1.02, t: 0 },
|
||||||
|
{ f: DOLUM + 6, s: 1.5, t: 0.85 },
|
||||||
|
{ f: DOLUM + 46, s: 1.5, t: 0.85 },
|
||||||
|
{ f: 128, s: 1.05, t: 0.2 },
|
||||||
|
{ f: 160, s: 1, t: 0 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<AdimBasligi no={3} metin="Listen hazır." dikey={dikey} />
|
||||||
|
<div style={{ position: "absolute", left: (w - yolKap.w) / 2, top: yolKap.y }}>
|
||||||
|
<PikselYolSurus metin="Hayal de var, akıl da." yol={yolKap.yol} fontSize={yolKap.font} genislik={yolKap.w} yukseklik={yolKap.h} gecikme={6} taban={yolKap.taban} />
|
||||||
|
</div>
|
||||||
|
{KARELER.map((d, i) => {
|
||||||
|
const [cx, cy] = kareMerkez(i);
|
||||||
|
const s = zipla(frame, DOLUM + i * 2);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: cx - G.kare / 2,
|
||||||
|
top: cy - G.kare / 2,
|
||||||
|
width: G.kare,
|
||||||
|
height: G.kare,
|
||||||
|
borderRadius: G.kare * 0.26,
|
||||||
|
background: d.renk,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontFamily: FONT.baslik,
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: G.kare * 0.38,
|
||||||
|
color: R.beyaz,
|
||||||
|
transform: `scale(${Math.max(0, s)})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i + 1}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div style={{ position: "absolute", left: 0, width: w, top: lejantY, display: "flex", justifyContent: "center", gap: 22 }}>
|
||||||
|
{DILIMLER.map((d, i) => {
|
||||||
|
const g = yay(frame, 112 + i * 5, 14);
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={d.ad}
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 14,
|
||||||
|
height: 84,
|
||||||
|
padding: "0 30px",
|
||||||
|
borderRadius: 999,
|
||||||
|
border: `3px solid ${R.slate200}`,
|
||||||
|
background: R.beyaz,
|
||||||
|
fontSize: 34,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: R.slate800,
|
||||||
|
opacity: g,
|
||||||
|
transform: `translateY(${(1 - g) * 16}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ width: 22, height: 22, borderRadius: 6, background: d.renk }} />
|
||||||
|
{d.ad}
|
||||||
|
<span style={{ fontFamily: FONT.baslik, fontWeight: 700, color: R.slate900 }}>{d.adet}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{/* Gerçek örnek satır: YÖK Atlas 2026, sayısal, 45.320. sıra */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: (w - (dikey ? 940 : 1100)) / 2,
|
||||||
|
width: dikey ? 940 : 1100,
|
||||||
|
top: ornekY,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 26,
|
||||||
|
padding: dikey ? "26px 32px" : "20px 32px",
|
||||||
|
borderRadius: 32,
|
||||||
|
background: R.beyaz,
|
||||||
|
border: `3px solid ${frame >= 144 ? R.turuncu : R.slate200}`,
|
||||||
|
opacity: yay(frame, 128, 16),
|
||||||
|
transform: `translateY(${(1 - yay(frame, 128, 16)) * 30}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: 48, color: R.slate400, width: 64, textAlign: "center" }}>11</div>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div style={{ fontSize: 34, fontWeight: 600, color: R.slate900 }}>Bilgisayar Mühendisliği</div>
|
||||||
|
<div style={{ marginTop: 4, fontSize: 26, color: R.slate500 }}>Ankara Yıldırım Beyazıt Üniversitesi · Ankara</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 6 }}>
|
||||||
|
<RiskEtiketi risk="az-riskli" olcek={2.4} />
|
||||||
|
<span style={{ fontSize: 26, color: R.slate500 }}>
|
||||||
|
Taban <b style={{ color: R.slate900 }}>{trSayi(55158)}</b>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SahneImleci duraklar={duraklar} gorunur={[DOLUM - 8, 162]} boy={dikey ? 76 : 64} />
|
||||||
|
</TakipKamera>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
656
src/sahneler-3.tsx
Normal file
@@ -0,0 +1,656 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { AbsoluteFill, Easing, Sequence, interpolate, useCurrentFrame } from "remotion";
|
||||||
|
import { Ikon, type IkonAdi } from "./ikonlar";
|
||||||
|
import { SahneImleci, TakipKamera, imlecKonumu, type Durak } from "./imlec";
|
||||||
|
import { PikselKonturDolgu, PikselSheen } from "./kinetik";
|
||||||
|
import { LogoMark } from "./logo";
|
||||||
|
import { RiskEtiketi, Sahne, TuruncuCta, useDuzen } from "./ortak";
|
||||||
|
import { Eyebrow, Wordmark } from "./piksel";
|
||||||
|
import { vurguStil } from "./sahneler-1";
|
||||||
|
import { Kamera, MaskeSatir, OdakDisiPikseller } from "./sinema";
|
||||||
|
import { FONT, R, ara, hash, trSayi, yay, zipla } from "./tema";
|
||||||
|
import { VURUSLAR } from "./vurus";
|
||||||
|
import ZAMAN from "./zaman.json";
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Off — sessizlik: "Off. Kafan mı karıştı?" imleç kararsız dolanır */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
export function Off() {
|
||||||
|
const { dikey, w, h } = useDuzen();
|
||||||
|
const cx = w / 2 + (dikey ? 170 : 330);
|
||||||
|
const cy = h / 2 + (dikey ? 160 : 90);
|
||||||
|
// Kararsız imleç: küçük kıvrımlar, sonra kadrajın ortasına (lens buradan patlar)
|
||||||
|
const duraklar: Durak[] = [
|
||||||
|
{ f: 14, x: cx, y: cy },
|
||||||
|
{ f: 22, x: cx - 40, y: cy + 20, g: 8 },
|
||||||
|
{ f: 30, x: cx + 26, y: cy - 24, g: 8 },
|
||||||
|
{ f: 38, x: cx - 18, y: cy + 30, g: 8 },
|
||||||
|
{ f: 58, x: w / 2, y: h / 2, g: 14 },
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<Sahne zemin={R.beyaz}>
|
||||||
|
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center", textAlign: "center", gap: 20 }}>
|
||||||
|
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 170 : 180, color: R.slate300, lineHeight: 1 }}>
|
||||||
|
<MaskeSatir gecikme={2} sure={16}>
|
||||||
|
Off.
|
||||||
|
</MaskeSatir>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 76 : 80, color: R.slate500 }}>
|
||||||
|
<MaskeSatir gecikme={18} sure={18}>
|
||||||
|
Kafan mı karıştı?
|
||||||
|
</MaskeSatir>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
<SahneImleci duraklar={duraklar} gorunur={[14, 64]} boy={dikey ? 76 : 64} />
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Danışman — BAM: DanışmanlaKonuş */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
function DanismanlaKonus({ boy }: { boy: number }) {
|
||||||
|
return (
|
||||||
|
<span style={{ display: "inline-flex", alignItems: "center", gap: boy * 0.08, fontFamily: FONT.vurgu, fontSize: boy, letterSpacing: "-0.05em", color: R.slate900, whiteSpace: "nowrap" }}>
|
||||||
|
<LogoMark boy={boy * 1.25} varyant="nabiz" turuncu girisYok />
|
||||||
|
<span>
|
||||||
|
<span style={{ fontWeight: 300 }}>Danışmanla</span>
|
||||||
|
<span style={{ fontWeight: 600 }}>Konuş</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Danisman() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey, w, h } = useDuzen();
|
||||||
|
// BAM (15. kare, lens kesmesi) anında büyükten oturur
|
||||||
|
const otur = interpolate(frame, [15, 26], [1.3, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.cubic) });
|
||||||
|
// Patlayan pikseller
|
||||||
|
const kareler: ReactNode[] = [];
|
||||||
|
const d = frame - 15;
|
||||||
|
if (d >= 0 && d < 40) {
|
||||||
|
for (let n = 0; n < 28; n++) {
|
||||||
|
const aci = (n / 28) * Math.PI * 2 + hash(n, 2, 7);
|
||||||
|
const uz = (dikey ? 260 : 300) + Easing.out(Easing.cubic)(Math.min(1, d / 30)) * (dikey ? 420 : 620) * (0.5 + hash(n, 3, 1) * 0.5);
|
||||||
|
const G = 24;
|
||||||
|
const x = Math.round((w / 2 + Math.cos(aci) * uz) / G) * G;
|
||||||
|
const y = Math.round((h / 2 - 40 + Math.sin(aci) * uz * 0.7) / G) * G;
|
||||||
|
const b = 30 * (1 - d / 42);
|
||||||
|
kareler.push(<rect key={n} x={x - b / 2} y={y - b / 2} width={b} height={b} rx={b * 0.27} fill={[R.turuncu, R.logoSeftali, R.primary][n % 3]} fillOpacity={1 - d / 40} />);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Sahne zemin={R.beyaz}>
|
||||||
|
<OdakDisiPikseller seed={61} adet={8} />
|
||||||
|
<Kamera anahtarlar={[{ f: 0, s: 1.05 }, { f: 80, s: 1.12 }]} vuruslar={VURUSLAR.danisman}>
|
||||||
|
<svg width={w} height={h} style={{ position: "absolute", inset: 0 }}>
|
||||||
|
{kareler}
|
||||||
|
</svg>
|
||||||
|
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center", textAlign: "center", gap: 46 }}>
|
||||||
|
<div style={{ transform: `scale(${otur})` }}>
|
||||||
|
<DanismanlaKonus boy={dikey ? 110 : 150} />
|
||||||
|
</div>
|
||||||
|
<PikselSheen gecikme={28} fontSize={dikey ? 90 : 100}>
|
||||||
|
Aklına takılanı sor.
|
||||||
|
</PikselSheen>
|
||||||
|
<div style={{ fontSize: dikey ? 40 : 40, color: R.slate500 }}>
|
||||||
|
<MaskeSatir gecikme={40}>Gece 3'te bile. Sıkılmaz, yargılamaz.</MaskeSatir>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
</Kamera>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Sohbet — tam ekran danışman; imleç yalnız yazarken/gönderirken */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
// Gerçek YÖK Atlas 2026 verisi: İzmir, İngilizce Bilgisayar Mühendisliği, devlet
|
||||||
|
const SONUCLAR = [
|
||||||
|
{ uni: "Ege Üniversitesi", taban: 44589, risk: "riskli" as const },
|
||||||
|
{ uni: "Dokuz Eylül Üniversitesi", taban: 52071, risk: "az-riskli" as const },
|
||||||
|
{ uni: "İzmir Katip Çelebi Üniversitesi", taban: 71833, risk: "guvenli" as const },
|
||||||
|
];
|
||||||
|
const TS = ZAMAN.tiklar.sohbet; // [girdi1, gönder1, girdi2, gönder2]
|
||||||
|
const YS = ZAMAN.yazma.sohbet;
|
||||||
|
|
||||||
|
type Mesaj =
|
||||||
|
| { tur: "kullanici"; metin: string; yaz: number[]; gonder: number }
|
||||||
|
| { tur: "arac"; ad: string; detay: string; bas: number; bitis: number }
|
||||||
|
| { tur: "cevap"; bas: number; tahmin: number; Icerik: () => ReactNode };
|
||||||
|
|
||||||
|
function SonucListesi({ olcek }: { olcek: number }) {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 14 * olcek }}>
|
||||||
|
<div>Sıralamana göre İzmir'de üç devlet seçeneği var:</div>
|
||||||
|
{SONUCLAR.map((s, i) => {
|
||||||
|
const g = yay(frame, 100 + i * 7, 16);
|
||||||
|
return (
|
||||||
|
<div key={s.uni} style={{ display: "flex", alignItems: "center", gap: 20 * olcek, background: R.beyaz, border: `3px solid ${R.slate200}`, borderRadius: 24 * olcek, padding: `${16 * olcek}px ${22 * olcek}px`, opacity: g, transform: `translateY(${(1 - g) * 14}px)` }}>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div style={{ fontWeight: 600, color: R.slate900 }}>{s.uni}</div>
|
||||||
|
<div style={{ fontSize: "0.72em", color: R.slate500 }}>Bilgisayar Mühendisliği · Devlet</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 4 }}>
|
||||||
|
<RiskEtiketi risk={s.risk} olcek={2 * olcek} />
|
||||||
|
<span style={{ fontSize: "0.72em", color: R.slate500 }}>
|
||||||
|
Taban <b style={{ color: R.slate900 }}>{trSayi(s.taban)}</b>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Sohbet() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey, w, h } = useDuzen();
|
||||||
|
const o = dikey ? 1 : 0.86; // ölçek
|
||||||
|
const L = dikey
|
||||||
|
? { x: 50, w: 980, ustY: 80, ustH: 130, altY: 1630, altH: 170 }
|
||||||
|
: { x: 260, w: 1400, ustY: 30, ustH: 110, altY: 900, altH: 130 };
|
||||||
|
const gonderX = L.x + L.w - L.altH / 2 - 10;
|
||||||
|
const altMerkez = L.altY + L.altH / 2;
|
||||||
|
|
||||||
|
const SENARYO: Mesaj[] = [
|
||||||
|
{ tur: "kullanici", metin: "İzmir'de İngilizce bilgisayar mühendisliği var mı?", yaz: YS[0], gonder: TS[1] },
|
||||||
|
{ tur: "arac", ad: "program_ara", detay: "İzmir · İngilizce", bas: TS[1] + 6, bitis: TS[1] + 32 },
|
||||||
|
{ tur: "cevap", bas: 90, tahmin: 620 * o, Icerik: () => <SonucListesi olcek={o} /> },
|
||||||
|
{ tur: "kullanici", metin: "Aslında İzmir dışına çıkmak istemiyorum.", yaz: YS[1], gonder: TS[3] },
|
||||||
|
{ tur: "arac", ad: "profil_guncelle", detay: "şehir: İzmir", bas: TS[3] + 6, bitis: TS[3] + 22 },
|
||||||
|
{
|
||||||
|
tur: "cevap",
|
||||||
|
bas: 224,
|
||||||
|
tahmin: 260 * o,
|
||||||
|
Icerik: () => (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 16, alignItems: "flex-start" }}>
|
||||||
|
<div>Tamam, profilini güncelledim. Yeni listende yalnız İzmir'deki programlar olacak.</div>
|
||||||
|
<span style={{ display: "inline-flex", alignItems: "center", gap: 10, height: 64 * o, padding: `0 ${24 * o}px`, borderRadius: 999, background: R.primary, color: R.beyaz, fontWeight: 600, fontSize: "0.85em", transform: `scale(${Math.max(0, zipla(frame, 242))})`, transformOrigin: "left center" }}>
|
||||||
|
<Ikon ad="konum" boy={28 * o} />
|
||||||
|
Şehir: İzmir
|
||||||
|
<Ikon ad="tik" boy={28 * o} kalinlik={3} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const yazilan = (() => {
|
||||||
|
for (const m of SENARYO) {
|
||||||
|
if (m.tur !== "kullanici") continue;
|
||||||
|
if (frame >= m.yaz[0] && frame < m.gonder) {
|
||||||
|
const n = Math.floor(ara(frame, [m.yaz[0], m.yaz[1]], [0, m.metin.length], (t) => t));
|
||||||
|
return m.metin.slice(0, n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
})();
|
||||||
|
const basis = TS.some((t, i) => i % 2 === 1 && frame >= t - 2 && frame < t + 3);
|
||||||
|
|
||||||
|
const girdiX = L.x + 260;
|
||||||
|
const imlec1: Durak[] = [
|
||||||
|
{ f: 8, x: w * 0.72, y: altMerkez - 260 },
|
||||||
|
{ f: TS[0], x: girdiX, y: altMerkez + 10, g: 12 },
|
||||||
|
{ f: TS[1], x: gonderX, y: altMerkez, g: 12 },
|
||||||
|
{ f: 72, x: gonderX - 60, y: altMerkez - 220, g: 12 },
|
||||||
|
];
|
||||||
|
const imlec2: Durak[] = [
|
||||||
|
{ f: 156, x: w * 0.7, y: altMerkez - 300 },
|
||||||
|
{ f: TS[2], x: girdiX, y: altMerkez + 10, g: 10 },
|
||||||
|
{ f: TS[3], x: gonderX, y: altMerkez, g: 12 },
|
||||||
|
{ f: 216, x: gonderX - 80, y: altMerkez - 240, g: 12 },
|
||||||
|
];
|
||||||
|
const nokta = (f: number) => (f < 120 ? imlecKonumu(imlec1, f) : imlecKonumu(imlec2, f));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sahne>
|
||||||
|
<TakipKamera
|
||||||
|
nokta={nokta}
|
||||||
|
vuruslar={VURUSLAR.sohbet}
|
||||||
|
anahtarlar={[
|
||||||
|
{ f: 0, s: 1, t: 0 },
|
||||||
|
{ f: TS[0], s: 1.3, t: 0.8 },
|
||||||
|
{ f: TS[1] + 4, s: 1.3, t: 0.8 },
|
||||||
|
{ f: 80, s: 1.02, t: 0 },
|
||||||
|
{ f: 150, s: 1.04, t: 0 },
|
||||||
|
{ f: TS[2], s: 1.3, t: 0.8 },
|
||||||
|
{ f: TS[3] + 4, s: 1.3, t: 0.8 },
|
||||||
|
{ f: 214, s: 1.02, t: 0 },
|
||||||
|
{ f: 300, s: 1.06, t: 0 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{/* Üst şerit */}
|
||||||
|
<div style={{ position: "absolute", left: L.x, top: L.ustY, width: L.w, height: L.ustH, display: "flex", alignItems: "center", justifyContent: "space-between", borderBottom: `3px solid ${R.slate200}` }}>
|
||||||
|
<DanismanlaKonus boy={dikey ? 52 : 50} />
|
||||||
|
<span style={{ display: "inline-flex", alignItems: "center", height: 64 * o, padding: `0 ${26 * o}px`, borderRadius: 999, border: `3px solid ${R.slate200}`, fontSize: 28 * o, fontWeight: 600, color: R.slate700 }}>
|
||||||
|
{frame >= TS[3] ? 58 : frame >= TS[1] ? 59 : 60} soru hakkı
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/* Mesajlar */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: L.x,
|
||||||
|
width: L.w,
|
||||||
|
top: L.ustY + L.ustH + 20,
|
||||||
|
height: L.altY - (L.ustY + L.ustH + 20) - 20,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
gap: 26 * o,
|
||||||
|
overflow: "hidden",
|
||||||
|
fontSize: 40 * o,
|
||||||
|
lineHeight: 1.4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{SENARYO.map((m, i) => {
|
||||||
|
const bas = m.tur === "kullanici" ? m.gonder : m.bas;
|
||||||
|
if (frame < bas) return null;
|
||||||
|
const g = yay(frame, bas, 16);
|
||||||
|
const tahmin = m.tur === "kullanici" ? 200 * o : m.tur === "arac" ? 90 * o : m.tahmin;
|
||||||
|
const kap = { maxHeight: g >= 0.999 ? undefined : g * tahmin, overflow: "hidden" as const, flexShrink: 0 };
|
||||||
|
if (m.tur === "kullanici") {
|
||||||
|
return (
|
||||||
|
<div key={i} style={{ ...kap, display: "flex", justifyContent: "flex-end" }}>
|
||||||
|
<div style={{ maxWidth: "82%", background: R.primary, color: R.beyaz, padding: `${22 * o}px ${30 * o}px`, borderRadius: 34 * o, fontWeight: 600, opacity: g, transform: `translateY(${(1 - g) * 20}px)` }}>{m.metin}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (m.tur === "arac") {
|
||||||
|
const bitti = frame >= m.bitis;
|
||||||
|
return (
|
||||||
|
<div key={i} style={{ ...kap, display: "flex", alignItems: "center", gap: 16 }}>
|
||||||
|
<span style={{ width: 64 * o, height: 64 * o, borderRadius: 999, border: `3px solid ${R.slate200}`, background: R.beyaz, display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<LogoMark boy={40 * o} varyant={bitti ? "duruk" : "nabiz"} girisYok />
|
||||||
|
</span>
|
||||||
|
<span style={{ display: "inline-flex", alignItems: "center", gap: 12, height: 60 * o, padding: `0 ${24 * o}px`, borderRadius: 999, background: R.slate50, border: `3px solid ${R.slate200}`, fontSize: 26 * o, fontWeight: 600, color: R.slate600, opacity: g, whiteSpace: "nowrap" }}>
|
||||||
|
{bitti ? <Ikon ad="tik" boy={26 * o} renk={R.guvenliYazi} kalinlik={3} /> : <Ikon ad="arama" boy={26 * o} renk={R.primary} />}
|
||||||
|
<span style={{ fontFamily: FONT.vurgu, color: R.primary }}>{m.ad}</span>
|
||||||
|
<span>· {m.detay}</span>
|
||||||
|
{!bitti ? <span style={{ color: R.slate400 }}>· YÖK Atlas'ta arıyor{".".repeat(1 + (Math.floor(frame / 6) % 3))}</span> : null}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div key={i} style={{ ...kap, paddingLeft: 80 * o }}>
|
||||||
|
<div style={{ maxWidth: "94%", background: R.slate100, color: R.slate800, padding: `${24 * o}px ${30 * o}px`, borderRadius: 34 * o, opacity: g, transform: `translateY(${(1 - g) * 20}px)` }}>
|
||||||
|
<m.Icerik />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{/* Yazma kutusu */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: L.x,
|
||||||
|
top: L.altY,
|
||||||
|
width: L.w,
|
||||||
|
height: L.altH,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 20,
|
||||||
|
borderRadius: 48 * o,
|
||||||
|
border: `4px solid ${yazilan ? R.primary : R.slate200}`,
|
||||||
|
boxShadow: yazilan ? "0 0 0 10px rgba(59,130,246,0.16)" : undefined,
|
||||||
|
background: R.beyaz,
|
||||||
|
padding: `0 16px 0 ${44 * o}px`,
|
||||||
|
fontSize: 42 * o,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ flex: 1, color: yazilan ? R.slate900 : R.slate400, whiteSpace: "nowrap", overflow: "hidden" }}>
|
||||||
|
{yazilan || "Listen hakkında bir şey sor…"}
|
||||||
|
{yazilan && Math.floor(frame / 8) % 2 === 0 ? <span style={{ color: R.primary }}>|</span> : null}
|
||||||
|
</span>
|
||||||
|
<span style={{ width: L.altH - 36, height: L.altH - 36, borderRadius: 999, background: basis ? R.turuncuKoyu : R.turuncu, display: "flex", alignItems: "center", justifyContent: "center", transform: `scale(${basis ? 0.93 : 1})`, flexShrink: 0 }}>
|
||||||
|
<Ikon ad="ok" boy={56 * o} renk={R.beyaz} kalinlik={2.6} style={{ transform: "rotate(-90deg)" }} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ position: "absolute", left: 0, width: w, top: dikey ? h - 100 : h - 50, display: "flex", justifyContent: "center", opacity: yay(frame, 262, 16) }}>
|
||||||
|
<Eyebrow olcek={dikey ? 2.3 : 2}>Havuz dışına çıkamaz, uydurma bölüm önermez</Eyebrow>
|
||||||
|
</div>
|
||||||
|
<SahneImleci duraklar={imlec1} tiklar={[TS[0], TS[1]]} gorunur={[8, 74]} boy={dikey ? 76 : 64} />
|
||||||
|
<SahneImleci duraklar={imlec2} tiklar={[TS[2], TS[3]]} gorunur={[156, 218]} boy={dikey ? 76 : 64} />
|
||||||
|
</TakipKamera>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Paket — Apple hızlı kesmeleri: trend, PDF, revizyon */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
const TREND = [37831, 27534, 21602, 30797, 42108, 55158]; // AYBÜ Bilgisayar, 2021–2026
|
||||||
|
|
||||||
|
function PaketKarti({ ikon, baslik, alt, children }: { ikon: IkonAdi; baslik: string; alt: string; children: ReactNode }) {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey } = useDuzen();
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center", gap: dikey ? 60 : 40, flexDirection: dikey ? "column" : "row", padding: dikey ? "0 70px" : "0 160px" }}>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", alignItems: dikey ? "center" : "flex-start", textAlign: dikey ? "center" : "left", gap: 24, flex: dikey ? undefined : 1 }}>
|
||||||
|
<div style={{ width: 120, height: 120, borderRadius: 32, background: "rgba(59,130,246,0.1)", display: "flex", alignItems: "center", justifyContent: "center", transform: `scale(${Math.max(0, zipla(frame, 0))})` }}>
|
||||||
|
<Ikon ad={ikon} boy={58} renk={R.primary} />
|
||||||
|
</div>
|
||||||
|
<PikselSheen gecikme={2} fontSize={dikey ? 110 : 120}>
|
||||||
|
{baslik}
|
||||||
|
</PikselSheen>
|
||||||
|
<div style={{ fontSize: dikey ? 46 : 46, color: R.slate500, lineHeight: 1.3 }}>
|
||||||
|
<MaskeSatir gecikme={10}>{alt}</MaskeSatir>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: dikey ? undefined : 1, display: "flex", justifyContent: "center" }}>{children}</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TrendGorseli() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey } = useDuzen();
|
||||||
|
const gW = dikey ? 860 : 760;
|
||||||
|
const gH = dikey ? 420 : 440;
|
||||||
|
const noktalar = TREND.map((v, i) => [(i / (TREND.length - 1)) * gW, ((v - 15000) / 45000) * gH]);
|
||||||
|
const c = ara(frame, [6, 34], [0, 1]);
|
||||||
|
const yol = noktalar.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join(" ");
|
||||||
|
return (
|
||||||
|
<svg width={gW + 40} height={gH + 90} viewBox={`-20 -20 ${gW + 40} ${gH + 90}`}>
|
||||||
|
<path d={yol} fill="none" stroke={R.primary} strokeWidth={10} strokeLinecap="round" strokeLinejoin="round" pathLength={1} strokeDasharray={1} strokeDashoffset={1 - c} />
|
||||||
|
{noktalar.map((p, i) => (
|
||||||
|
<g key={i} opacity={c >= i / (TREND.length - 1) ? 1 : 0}>
|
||||||
|
<rect x={p[0] - 16} y={p[1] - 16} width={32} height={32} rx={9} fill={i === TREND.length - 1 ? R.turuncu : R.primary} />
|
||||||
|
<text x={p[0]} y={gH + 50} textAnchor="middle" fontSize={28} fontFamily="Clarity City" fontWeight={600} fill={R.slate400}>
|
||||||
|
{2021 + i}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PdfGorseli() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey } = useDuzen();
|
||||||
|
const k = dikey ? 1.6 : 1.5;
|
||||||
|
return (
|
||||||
|
<div style={{ position: "relative", width: 330 * k, height: 380 * k }}>
|
||||||
|
{[2, 1, 0].map((i) => {
|
||||||
|
const s = yay(frame, 4 + (2 - i) * 5, 18);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: (60 + i * 30) * k,
|
||||||
|
top: (20 + i * 22) * k,
|
||||||
|
width: 220 * k,
|
||||||
|
height: 290 * k,
|
||||||
|
borderRadius: 26 * k,
|
||||||
|
background: R.beyaz,
|
||||||
|
border: `3px solid ${R.slate200}`,
|
||||||
|
boxShadow: "0 18px 40px rgba(15,23,42,0.08)",
|
||||||
|
transform: `rotate(${(i - 1) * 7 * s}deg)`,
|
||||||
|
padding: 26 * k,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 16 * k,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i === 0 ? (
|
||||||
|
<>
|
||||||
|
<div style={{ display: "flex", gap: 8 * k }}>
|
||||||
|
{[R.riskli, R.azRiskli, R.guvenli].map((c) => (
|
||||||
|
<span key={c} style={{ width: 24 * k, height: 24 * k, borderRadius: 7 * k, background: c }} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{[1, 0.8, 0.9, 0.6, 0.75, 0.5].map((wd, j) => (
|
||||||
|
<span key={j} style={{ height: 14 * k, width: `${wd * 100}%`, borderRadius: 7 * k, background: R.slate200 }} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const REVIZYON = [
|
||||||
|
{ ad: "Ege · Bilgisayar Müh.", renk: R.riskli },
|
||||||
|
{ ad: "Dokuz Eylül · Bilgisayar Müh.", renk: R.azRiskli },
|
||||||
|
{ ad: "Katip Çelebi · Bilgisayar Müh.", renk: R.guvenli },
|
||||||
|
];
|
||||||
|
|
||||||
|
function RevizyonGorseli() {
|
||||||
|
const frame = useCurrentFrame(); // alt dizi içi kare (0–45)
|
||||||
|
const { dikey } = useDuzen();
|
||||||
|
const [tut, birak] = ZAMAN.tiklar.paket.map((t) => t - 90);
|
||||||
|
const SAT = dikey ? 130 : 120;
|
||||||
|
const g = dikey ? 880 : 760;
|
||||||
|
const ilerle = ara(frame, [tut, birak], [0, 1], Easing.inOut(Easing.cubic));
|
||||||
|
const tutuldu = frame >= tut && frame < birak + 2;
|
||||||
|
return (
|
||||||
|
<div style={{ position: "relative", width: g, height: 3 * SAT }}>
|
||||||
|
{REVIZYON.map((r, i) => {
|
||||||
|
const gecis = i === 1 ? (2 * ilerle - 0.5) / 0.8 : (2 * ilerle - 1.3) / 0.7;
|
||||||
|
const kay = Easing.inOut(Easing.cubic)(Math.min(1, Math.max(0, gecis)));
|
||||||
|
const y = i === 0 ? ilerle * 2 * SAT : (i - kay) * SAT;
|
||||||
|
const kalk = i === 0 && tutuldu ? 1 : 0;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={r.ad}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: kalk * 24,
|
||||||
|
top: y,
|
||||||
|
width: "100%",
|
||||||
|
height: SAT - 20,
|
||||||
|
borderRadius: 30,
|
||||||
|
border: `4px solid ${kalk ? R.turuncu : R.slate200}`,
|
||||||
|
background: kalk ? "#fff7ed" : R.beyaz,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 24,
|
||||||
|
padding: "0 32px",
|
||||||
|
fontSize: dikey ? 40 : 38,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: R.slate800,
|
||||||
|
boxShadow: kalk ? "0 24px 48px rgba(15,23,42,0.16)" : undefined,
|
||||||
|
transform: `scale(${1 + kalk * 0.04}) rotate(${kalk * -1.5}deg)`,
|
||||||
|
zIndex: i === 0 ? 2 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ width: 24, height: 24, borderRadius: 7, background: r.renk }} />
|
||||||
|
<span style={{ flex: 1 }}>{r.ad}</span>
|
||||||
|
<span style={{ display: "grid", gridTemplateColumns: "repeat(2, 9px)", gap: 6 }}>
|
||||||
|
{Array.from({ length: 6 }, (_, k) => (
|
||||||
|
<span key={k} style={{ width: 9, height: 9, borderRadius: 3, background: R.slate400 }} />
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Paket() {
|
||||||
|
const { dikey, w, h } = useDuzen();
|
||||||
|
const [tut, birak] = ZAMAN.tiklar.paket;
|
||||||
|
// Revizyon kartında imleç ilk satırı tutup en alta sürükler (kamera takip eder)
|
||||||
|
const SAT = dikey ? 130 : 120;
|
||||||
|
const gW = dikey ? 880 : 760;
|
||||||
|
const kartX = dikey ? (w - gW) / 2 : w * 0.5 + 60 + (w * 0.5 - 220 - gW) / 2;
|
||||||
|
const kartY = dikey ? h / 2 + 230 : h / 2 - (3 * SAT) / 2;
|
||||||
|
const tutX = kartX + gW - 50;
|
||||||
|
const duraklar: Durak[] = [
|
||||||
|
{ f: 92, x: tutX + 120, y: kartY + 360 },
|
||||||
|
{ f: tut, x: tutX, y: kartY + (SAT - 20) / 2, g: 8 },
|
||||||
|
{ f: birak, x: tutX + 24, y: kartY + 2 * SAT + (SAT - 20) / 2, g: birak - tut },
|
||||||
|
{ f: 134, x: tutX + 90, y: kartY + 2 * SAT + 120, g: 10 },
|
||||||
|
];
|
||||||
|
const nokta = (f: number) => imlecKonumu(duraklar, f);
|
||||||
|
return (
|
||||||
|
<Sahne>
|
||||||
|
<OdakDisiPikseller seed={71} adet={6} guc={0.6} />
|
||||||
|
<TakipKamera
|
||||||
|
nokta={nokta}
|
||||||
|
vuruslar={VURUSLAR.paket}
|
||||||
|
anahtarlar={[
|
||||||
|
{ f: 0, s: 1.06, t: 0 },
|
||||||
|
{ f: 44, s: 1.0, t: 0 },
|
||||||
|
{ f: 45, s: 1.06, t: 0 },
|
||||||
|
{ f: 89, s: 1.0, t: 0 },
|
||||||
|
{ f: 90, s: 1.04, t: 0 },
|
||||||
|
{ f: tut, s: dikey ? 1.15 : 1.08, t: dikey ? 0.35 : 0.12 },
|
||||||
|
{ f: birak, s: dikey ? 1.15 : 1.08, t: dikey ? 0.35 : 0.12 },
|
||||||
|
{ f: 135, s: 1.05, t: 0.1 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Sequence durationInFrames={45}>
|
||||||
|
<PaketKarti ikon="trend" baslik="6 yıllık trend." alt="Her tercihin geçmişi, tek bakışta.">
|
||||||
|
<TrendGorseli />
|
||||||
|
</PaketKarti>
|
||||||
|
</Sequence>
|
||||||
|
<Sequence from={45} durationInFrames={45}>
|
||||||
|
<PaketKarti ikon="belge" baslik="PDF rapor." alt="Aile toplantısına hazır.">
|
||||||
|
<PdfGorseli />
|
||||||
|
</PaketKarti>
|
||||||
|
</Sequence>
|
||||||
|
<Sequence from={90}>
|
||||||
|
<PaketKarti ikon="yenile" baslik="2 revizyon." alt="Fikrin değişir, listen de.">
|
||||||
|
<RevizyonGorseli />
|
||||||
|
</PaketKarti>
|
||||||
|
</Sequence>
|
||||||
|
<SahneImleci duraklar={duraklar} tiklar={[tut, birak]} gorunur={[92, 136]} boy={dikey ? 76 : 64} />
|
||||||
|
</TakipKamera>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Dürüstlük — Outline Fill Track: "Çünkü kimse veremez." */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
export function Durust() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey, w } = useDuzen();
|
||||||
|
const alt = yay(frame, 86, 18);
|
||||||
|
return (
|
||||||
|
<Sahne zemin={R.beyaz}>
|
||||||
|
<Kamera anahtarlar={[{ f: 0, s: 1.02 }, { f: 140, s: 1.12 }]} vuruslar={VURUSLAR.durust}>
|
||||||
|
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center", textAlign: "center", gap: 40 }}>
|
||||||
|
<div style={{ width: 140, height: 140, borderRadius: 38, background: R.primary, display: "flex", alignItems: "center", justifyContent: "center", transform: `scale(${Math.max(0, zipla(frame, 0))})` }}>
|
||||||
|
<Ikon ad="kalkan" boy={76} renk={R.beyaz} />
|
||||||
|
</div>
|
||||||
|
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 104 : 116, lineHeight: 1.06 }}>
|
||||||
|
<MaskeSatir gecikme={6}>Yerleşme garantisi</MaskeSatir>
|
||||||
|
<MaskeSatir gecikme={14}>vermiyoruz.</MaskeSatir>
|
||||||
|
</div>
|
||||||
|
<PikselKonturDolgu on="Çünkü" deger="kimse veremez." fontSize={dikey ? 84 : 100} gecikme={30} genislik={w} />
|
||||||
|
<div style={{ opacity: alt, transform: `translateY(${(1 - alt) * 20}px)`, display: "flex", flexDirection: "column", alignItems: "center", gap: 24, marginTop: 10 }}>
|
||||||
|
<div style={{ fontSize: 40, color: R.slate600, fontWeight: 600, maxWidth: dikey ? 900 : 1500, lineHeight: 1.4 }}>
|
||||||
|
Veren olursa, uzak dur. Biz riski satır satır gösteririz.
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", gap: 44 }}>
|
||||||
|
{(["guvenli", "az-riskli", "riskli"] as const).map((r, i) => (
|
||||||
|
<span key={r} style={{ transform: `scale(${Math.max(0, zipla(frame, 94 + i * 5))})` }}>
|
||||||
|
<RiskEtiketi risk={r} olcek={3.2} />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
</Kamera>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Hazır — sessizlik */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
export function Hazir() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey } = useDuzen();
|
||||||
|
const kare = dikey ? 64 : 56;
|
||||||
|
return (
|
||||||
|
<Sahne zemin={R.beyaz}>
|
||||||
|
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center", gap: 50 }}>
|
||||||
|
<div style={{ display: "flex", gap: kare * 0.22 }}>
|
||||||
|
{[R.logoMavi, R.logoSeftali, R.logoTuruncu].map((c, i) => (
|
||||||
|
<div key={c} style={{ width: kare, height: kare, borderRadius: kare * 0.23, background: c, transform: `scale(${Math.max(0, zipla(frame, 4 + i * 6))})` }} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 70 : 66, color: R.slate500 }}>
|
||||||
|
<MaskeSatir gecikme={8}>Hazır mısın?</MaskeSatir>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Final — logo + tipografi + buton; tık → çöküş → boş ekran (döngü) */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
export function Final() {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { dikey, w, h } = useDuzen();
|
||||||
|
const [tik] = ZAMAN.tiklar.final;
|
||||||
|
const otur = interpolate(frame, [15, 26], [1.2, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.cubic) });
|
||||||
|
const bas = ara(frame, [tik - 2, tik + 1], [0, 1]) * (1 - ara(frame, [tik + 5, tik + 10], [0, 1]));
|
||||||
|
const cok = ara(frame, [tik + 4, tik + 30], [0, 1], Easing.bezier(0.7, 0, 0.84, 0));
|
||||||
|
// Buton kadrajda sabit bir noktada (imleç ve çöküş kökeni)
|
||||||
|
const butonY = dikey ? h / 2 + 350 : h / 2 + 260;
|
||||||
|
const duraklar: Durak[] = [
|
||||||
|
{ f: 84, x: w / 2 + (dikey ? 300 : 520), y: butonY + (dikey ? 380 : 200) },
|
||||||
|
{ f: tik - 4, x: w / 2 + 60, y: butonY + 8, g: 26 },
|
||||||
|
{ f: tik, x: w / 2 + 60, y: butonY + 8 },
|
||||||
|
{ f: 170, x: w / 2, y: h / 2, g: 30 },
|
||||||
|
];
|
||||||
|
const nokta = (f: number) => imlecKonumu(duraklar, f);
|
||||||
|
return (
|
||||||
|
<Sahne>
|
||||||
|
<TakipKamera
|
||||||
|
nokta={nokta}
|
||||||
|
vuruslar={VURUSLAR.final}
|
||||||
|
anahtarlar={[
|
||||||
|
{ f: 0, s: 1.06, t: 0 },
|
||||||
|
{ f: 90, s: 1, t: 0 },
|
||||||
|
{ f: tik - 6, s: 1.4, t: 1 },
|
||||||
|
{ f: tik + 10, s: 1.4, t: 1 },
|
||||||
|
{ f: tik + 36, s: 1, t: 0 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<AbsoluteFill
|
||||||
|
style={{
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
textAlign: "center",
|
||||||
|
gap: dikey ? 70 : 54,
|
||||||
|
transformOrigin: `50% ${(butonY / h) * 100}%`,
|
||||||
|
transform: `scale(${otur * (1 - cok)})`,
|
||||||
|
opacity: 1 - ara(frame, [tik + 18, tik + 30], [0, 1]),
|
||||||
|
filter: `blur(${cok * 10}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex", flexDirection: dikey ? "column" : "row", alignItems: "center", gap: dikey ? 14 : 26 }}>
|
||||||
|
<LogoMark boy={dikey ? 220 : 180} girisYok rotaBas={24} />
|
||||||
|
<Wordmark boy={dikey ? 116 : 116} />
|
||||||
|
</div>
|
||||||
|
<div style={{ fontFamily: FONT.baslik, fontWeight: 700, fontSize: dikey ? 100 : 108, lineHeight: 1.06 }}>
|
||||||
|
<MaskeSatir gecikme={20}>Listeni kur,</MaskeSatir>
|
||||||
|
<MaskeSatir gecikme={28}>
|
||||||
|
<span style={vurguStil}>panikle</span> değil.
|
||||||
|
</MaskeSatir>
|
||||||
|
</div>
|
||||||
|
<div style={{ transform: `scale(${Math.max(0, zipla(frame, 38))})` }}>
|
||||||
|
<TuruncuCta metin="kolaytercih.com" boy={dikey ? 1.25 : 1.15} basili={bas} />
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
<SahneImleci duraklar={duraklar} tiklar={[tik]} gorunur={[84, 176]} boy={dikey ? 80 : 68} />
|
||||||
|
</TakipKamera>
|
||||||
|
</Sahne>
|
||||||
|
);
|
||||||
|
}
|
||||||
201
src/sinema.tsx
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import type { CSSProperties, ReactNode } from "react";
|
||||||
|
import { AbsoluteFill, Easing, Img, interpolate, staticFile, useCurrentFrame, useVideoConfig } from "remotion";
|
||||||
|
import { R, hash } from "./tema";
|
||||||
|
import { vurusCarpani, type Vurus } from "./vurus";
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Kamera — sahneyi takip eden sanal kamera */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
export type Anahtar = {
|
||||||
|
/** Sahne içi kare */
|
||||||
|
f: number;
|
||||||
|
/** Yakınlık (1 = tam kadraj) */
|
||||||
|
s?: number;
|
||||||
|
/** Kadrajın merkezine getirilecek noktanın merkeze uzaklığı (px, ölçeksiz uzayda) */
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
/** Hafif yatış (derece) */
|
||||||
|
r?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const kameraEgri = Easing.bezier(0.65, 0, 0.35, 1);
|
||||||
|
|
||||||
|
export function degerAl(anahtarlar: Anahtar[], frame: number, alan: "s" | "x" | "y" | "r", varsayilan: number): number {
|
||||||
|
const noktalar = anahtarlar.filter((a) => a[alan] != null).map((a) => ({ f: a.f, v: a[alan] as number }));
|
||||||
|
if (noktalar.length === 0) return varsayilan;
|
||||||
|
if (frame <= noktalar[0].f) return noktalar[0].v;
|
||||||
|
for (let i = 0; i < noktalar.length - 1; i++) {
|
||||||
|
const a = noktalar[i];
|
||||||
|
const b = noktalar[i + 1];
|
||||||
|
if (frame <= b.f) return interpolate(frame, [a.f, b.f], [a.v, b.v], { easing: kameraEgri });
|
||||||
|
}
|
||||||
|
return noktalar[noktalar.length - 1].v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Anahtar kareler arasında yumuşak (ease-in-out) kamera: yakınlaşır,
|
||||||
|
* uzaklaşır, kaydırır. Üstüne çok hafif bir el kamerası salınımı biner.
|
||||||
|
*/
|
||||||
|
export function Kamera({
|
||||||
|
anahtarlar,
|
||||||
|
children,
|
||||||
|
salinim = 1,
|
||||||
|
vuruslar,
|
||||||
|
}: {
|
||||||
|
anahtarlar: Anahtar[];
|
||||||
|
children: ReactNode;
|
||||||
|
salinim?: number;
|
||||||
|
/** Müziğe oturan ani zoom in / zoom out'lar (vurus.ts) */
|
||||||
|
vuruslar?: Vurus[];
|
||||||
|
}) {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const s = degerAl(anahtarlar, frame, "s", 1) * vurusCarpani(frame, vuruslar);
|
||||||
|
const x = degerAl(anahtarlar, frame, "x", 0);
|
||||||
|
const y = degerAl(anahtarlar, frame, "y", 0);
|
||||||
|
const r = degerAl(anahtarlar, frame, "r", 0);
|
||||||
|
const sx = Math.sin(frame / 47) * 5 * salinim + Math.sin(frame / 19) * 1.5 * salinim;
|
||||||
|
const sy = Math.cos(frame / 53) * 4 * salinim;
|
||||||
|
const sr = Math.sin(frame / 71) * 0.15 * salinim;
|
||||||
|
return (
|
||||||
|
<AbsoluteFill
|
||||||
|
style={{
|
||||||
|
transform: `scale(${s}) translate(${-x + sx}px, ${-y + sy}px) rotate(${r + sr}deg)`,
|
||||||
|
transformOrigin: "50% 50%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Film dokusu: gren + vinyet */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
const KARO = 768;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Film greni. Karolar Remotion'ın <Img>'i ile çizilir: her kare görselin
|
||||||
|
* yüklenmesini bekler. (CSS background-image beklenmiyordu; bazı karelerde
|
||||||
|
* gren hiç çizilmeyince ekran bir anlığına açılıp "siyah-beyaz" titriyordu.)
|
||||||
|
*/
|
||||||
|
export function Gren({ guc = 1 }: { guc?: number }) {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { width, height } = useVideoConfig();
|
||||||
|
const n = (frame % 6) + 1;
|
||||||
|
const ox = -Math.floor(hash(frame, 1, 4) * KARO);
|
||||||
|
const oy = -Math.floor(hash(frame, 2, 9) * KARO);
|
||||||
|
const sut = Math.ceil((width - ox) / KARO);
|
||||||
|
const sat = Math.ceil((height - oy) / KARO);
|
||||||
|
const karolar: ReactNode[] = [];
|
||||||
|
for (let j = 0; j < sat; j++) {
|
||||||
|
for (let i = 0; i < sut; i++) {
|
||||||
|
karolar.push(
|
||||||
|
<Img
|
||||||
|
key={`${i}-${j}`}
|
||||||
|
src={staticFile(`gren/g${n}.png`)}
|
||||||
|
style={{ position: "absolute", left: ox + i * KARO, top: oy + j * KARO, width: KARO, height: KARO }}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ mixBlendMode: "multiply", opacity: 0.1 * guc, pointerEvents: "none", overflow: "hidden" }}>
|
||||||
|
{karolar}
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Vinyet() {
|
||||||
|
return (
|
||||||
|
<AbsoluteFill
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"radial-gradient(ellipse 78% 72% at 50% 48%, rgba(15,23,42,0) 58%, rgba(15,23,42,0.05) 80%, rgba(15,23,42,0.13) 100%)",
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Odak dışı pikseller — alan derinliği */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kameranın odağı dışında kalan iri, bulanık piksel kareleri: sahneye
|
||||||
|
* derinlik verir, markanın kare dilini "bokeh" olarak taşır. Yavaşça süzülür.
|
||||||
|
*/
|
||||||
|
export function OdakDisiPikseller({ seed = 1, adet = 9, guc = 1 }: { seed?: number; adet?: number; guc?: number }) {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { width, height } = useVideoConfig();
|
||||||
|
const renkler = [R.primary, R.turuncu, R.logoSeftali, R.primary];
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ pointerEvents: "none" }}>
|
||||||
|
{Array.from({ length: adet }, (_, i) => {
|
||||||
|
const boy = 90 + hash(i, seed, 1) * 220;
|
||||||
|
const yakin = hash(i, seed, 2) > 0.5;
|
||||||
|
const x = hash(i, seed, 3) * width - boy / 2 + Math.sin((frame + i * 40) / (90 + i * 7)) * 40;
|
||||||
|
const y = hash(i, seed, 4) * height - boy / 2 - frame * (0.4 + hash(i, seed, 5) * 0.8) * (yakin ? 1.4 : 0.7);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: x,
|
||||||
|
top: ((y % (height + 400)) + height + 400) % (height + 400) - 200,
|
||||||
|
width: boy,
|
||||||
|
height: boy,
|
||||||
|
borderRadius: boy * 0.27,
|
||||||
|
background: renkler[i % renkler.length],
|
||||||
|
opacity: (yakin ? 0.1 : 0.14) * guc,
|
||||||
|
filter: `blur(${yakin ? 46 : 24}px)`,
|
||||||
|
transform: `rotate(${(hash(i, seed, 6) - 0.5) * 16}deg)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Maskeli satır — Apple'ın metin açılışı */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/** Satır, görünmez bir çizginin arkasından yükselir (overflow maskesi). */
|
||||||
|
export function MaskeSatir({
|
||||||
|
children,
|
||||||
|
gecikme = 0,
|
||||||
|
sure = 22,
|
||||||
|
cikis,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
gecikme?: number;
|
||||||
|
sure?: number;
|
||||||
|
/** Bu karede satır yukarı kayarak maskenin arkasına çekilir */
|
||||||
|
cikis?: number;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}) {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const g = interpolate(frame, [gecikme, gecikme + sure], [0, 1], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
easing: Easing.bezier(0.16, 1, 0.3, 1),
|
||||||
|
});
|
||||||
|
const c =
|
||||||
|
cikis == null
|
||||||
|
? 0
|
||||||
|
: interpolate(frame, [cikis, cikis + 14], [0, 1], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
easing: Easing.bezier(0.7, 0, 0.84, 0),
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<span style={{ display: "block", overflow: "hidden", paddingBottom: "0.12em", marginBottom: "-0.12em", ...style }}>
|
||||||
|
<span style={{ display: "block", transform: `translateY(${(1 - g) * 110 - c * 110}%)` }}>{children}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
87
src/tema.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { Easing, interpolate, spring } from "remotion";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KolayTercih marka değerleri — kaynak: kolaytercih/docs/tasarim/DESIGN.md
|
||||||
|
* ve src/app/globals.css. Video tarayıcı dışı bir ortam olduğu için Tailwind
|
||||||
|
* sınıflarının hex karşılıkları burada tek yerde durur.
|
||||||
|
*/
|
||||||
|
export const R = {
|
||||||
|
primary: "#3b82f6", // --primary oklch(0.623 0.188 259.8)
|
||||||
|
turuncu: "#ff6900", // orange-500 (v4)
|
||||||
|
turuncuKoyu: "#f54900", // orange-600 — yalnız CTA zemin hover'ı
|
||||||
|
logoTuruncu: "#ff5a00", // logodaki turuncu kare
|
||||||
|
logoSeftali: "#fdc7a7", // logodaki şeftali kare
|
||||||
|
logoMavi: "#3b82f6",
|
||||||
|
slate900: "#0f172a",
|
||||||
|
slate800: "#1e293b",
|
||||||
|
slate700: "#334155",
|
||||||
|
slate600: "#475569",
|
||||||
|
slate500: "#64748b",
|
||||||
|
slate400: "#94a3b8",
|
||||||
|
slate300: "#cbd5e1",
|
||||||
|
slate200: "#e2e8f0",
|
||||||
|
slate100: "#f1f5f9",
|
||||||
|
slate50: "#f8fafc",
|
||||||
|
beyaz: "#ffffff",
|
||||||
|
murekkep: "#0a0a0a", // logo yazısı
|
||||||
|
// Risk dili — her zaman metin etiketiyle (DESIGN.md §2.2)
|
||||||
|
guvenli: "#00bc7d", // emerald-500
|
||||||
|
guvenliYazi: "#009966", // emerald-600
|
||||||
|
azRiskli: "#fe9a00", // amber-500
|
||||||
|
azRiskliYazi: "#e17100", // amber-600
|
||||||
|
riskli: "#fb2c36", // red-500
|
||||||
|
riskliYazi: "#e7000b", // red-600
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FONT = {
|
||||||
|
baslik: "'Cal Sans', 'Clarity City', sans-serif",
|
||||||
|
govde: "'Clarity City', sans-serif",
|
||||||
|
vurgu: "'Bricolage', 'Clarity City', sans-serif",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FPS = 30;
|
||||||
|
/** 120 BPM ızgara: bir vuruş 15 kare, bir ölçü 60 kare. Kesmeler vuruşa oturur. */
|
||||||
|
export const VURUS = 15;
|
||||||
|
|
||||||
|
/** Sıçramasız yay — Apple'ın damping 1'i (DESIGN.md §4.17 yakınlaşma). */
|
||||||
|
export const yay = (frame: number, gecikme = 0, sure = 24) =>
|
||||||
|
spring({
|
||||||
|
frame: frame - gecikme,
|
||||||
|
fps: FPS,
|
||||||
|
config: { damping: 200 },
|
||||||
|
durationInFrames: sure,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Logo kareleri ve CTA gibi "dokunulabilir" şeyler için hafif tepkili yay. */
|
||||||
|
export const zipla = (frame: number, gecikme = 0) =>
|
||||||
|
spring({
|
||||||
|
frame: frame - gecikme,
|
||||||
|
fps: FPS,
|
||||||
|
config: { damping: 13, stiffness: 170, mass: 0.7 },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ara = (
|
||||||
|
frame: number,
|
||||||
|
giris: [number, number],
|
||||||
|
cikis: [number, number],
|
||||||
|
ease: (t: number) => number = Easing.bezier(0.22, 1, 0.36, 1),
|
||||||
|
) =>
|
||||||
|
interpolate(frame, giris, cikis, {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
easing: ease,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Deterministik sözde-rastgele — sitedeki pixel-decor ile aynı hash. */
|
||||||
|
export const hash = (x: number, y: number, seed = 0) => {
|
||||||
|
const n = Math.sin(x * 127.1 + y * 311.7 + seed * 74.7) * 43758.5453;
|
||||||
|
return n - Math.floor(n);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Hero dokusunun opaklık paleti (pixel-decor OPACITIES). */
|
||||||
|
export const OPAKLIKLAR = [0.3, 0.16, 0.24, 0.12, 0.27, 0.19];
|
||||||
|
export const opaklik = (x: number, y: number) =>
|
||||||
|
OPAKLIKLAR[(x * 7 + y * 11) % OPAKLIKLAR.length];
|
||||||
|
|
||||||
|
export const trSayi = (n: number) =>
|
||||||
|
Math.round(n).toLocaleString("tr-TR", { maximumFractionDigits: 0 });
|
||||||
45
src/vurus.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* Sahne içi "punch" zoomları: müziğin vurgularına oturan ani yakınlaşma
|
||||||
|
* (g > 0) ya da geri tepme (g < 0). Kamera bu listeyi okur; Lansman da aynı
|
||||||
|
* kareleri hareket bulanıklığı (motion blur) pencereleri olarak kullanır.
|
||||||
|
* Kareler sahne içidir.
|
||||||
|
*/
|
||||||
|
export type Vurus = { f: number; g: number };
|
||||||
|
|
||||||
|
export const VURUSLAR: Record<string, Vurus[]> = {
|
||||||
|
acilis: [{ f: 16, g: 0.06 }],
|
||||||
|
secenek: [{ f: 52, g: 0.06 }],
|
||||||
|
kaos: [{ f: 45, g: -0.05 }, { f: 75, g: 0.05 }],
|
||||||
|
logo: [{ f: 15, g: 0.1 }, { f: 52, g: 0.04 }],
|
||||||
|
adim1: [{ f: 124, g: 0.07 }],
|
||||||
|
adim2: [{ f: 140, g: 0.06 }],
|
||||||
|
adim3: [{ f: 90, g: 0.07 }],
|
||||||
|
danisman: [{ f: 15, g: 0.12 }],
|
||||||
|
sohbet: [{ f: 90, g: 0.04 }, { f: 224, g: 0.04 }],
|
||||||
|
paket: [{ f: 0, g: 0.05 }, { f: 45, g: 0.05 }, { f: 90, g: 0.05 }],
|
||||||
|
durust: [{ f: 36, g: 0.08 }],
|
||||||
|
final: [{ f: 15, g: 0.1 }, { f: 120, g: 0.08 }],
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Punch eğrisi: 5 karede sert yaklaş, ~10 karede yumuşakça geri gel. */
|
||||||
|
export function vurusCarpani(frame: number, vuruslar: Vurus[] | undefined) {
|
||||||
|
if (!vuruslar) return 1;
|
||||||
|
let c = 1;
|
||||||
|
for (const v of vuruslar) {
|
||||||
|
const d = frame - v.f;
|
||||||
|
if (d < 0 || d > 30) continue;
|
||||||
|
const e = d < 5 ? 1 - (1 - d / 5) ** 3 : Math.exp(-(d - 5) / 8);
|
||||||
|
c *= 1 + v.g * e;
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kameranın imlece hızla kilitlendiği / bıraktığı anlar: hareket bulanıklığı pencereleri. */
|
||||||
|
export const BULANIK: Record<string, [number, number][]> = {
|
||||||
|
adim1: [[18, 32], [128, 142]],
|
||||||
|
adim2: [[46, 58], [118, 140]],
|
||||||
|
adim3: [[54, 68], [104, 128]],
|
||||||
|
sohbet: [[8, 20], [60, 80], [156, 168], [204, 214]],
|
||||||
|
paket: [[92, 102]],
|
||||||
|
final: [[108, 116], [124, 156]],
|
||||||
|
};
|
||||||
31
src/zaman.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"fps": 30,
|
||||||
|
"sahneler": [
|
||||||
|
{ "id": "acilis", "bas": 0 },
|
||||||
|
{ "id": "secenek", "bas": 105, "gecis": { "tur": "zoomBlur", "sure": 18 } },
|
||||||
|
{ "id": "kaos", "bas": 210, "gecis": { "tur": "zoomBlur", "sure": 18 } },
|
||||||
|
{ "id": "nefes", "bas": 315, "gecis": { "tur": "kesme" }, "sessiz": true },
|
||||||
|
{ "id": "logo", "bas": 375, "gecis": { "tur": "lensZoom", "sure": 30 }, "bam": true },
|
||||||
|
{ "id": "adim1", "bas": 495, "gecis": { "tur": "zoomBlur", "sure": 18 } },
|
||||||
|
{ "id": "adim2", "bas": 660, "gecis": { "tur": "zoomBlur", "sure": 18 } },
|
||||||
|
{ "id": "adim3", "bas": 840, "gecis": { "tur": "zoomBlur", "sure": 18 } },
|
||||||
|
{ "id": "off", "bas": 1005, "gecis": { "tur": "kesme" }, "sessiz": true },
|
||||||
|
{ "id": "danisman", "bas": 1065, "gecis": { "tur": "lensZoom", "sure": 30 }, "bam": true },
|
||||||
|
{ "id": "sohbet", "bas": 1140, "gecis": { "tur": "zoomBlur", "sure": 18 } },
|
||||||
|
{ "id": "paket", "bas": 1440, "gecis": { "tur": "zoomBlur", "sure": 18 } },
|
||||||
|
{ "id": "durust", "bas": 1575, "gecis": { "tur": "zoomBlur", "sure": 18 } },
|
||||||
|
{ "id": "hazir", "bas": 1695, "gecis": { "tur": "kesme" }, "sessiz": true },
|
||||||
|
{ "id": "final", "bas": 1740, "gecis": { "tur": "lensZoom", "sure": 30 }, "bam": true }
|
||||||
|
],
|
||||||
|
"son": 1920,
|
||||||
|
"tiklar": {
|
||||||
|
"adim1": [30, 70, 80, 90, 100, 124],
|
||||||
|
"sohbet": [18, 56, 166, 200],
|
||||||
|
"paket": [100, 122],
|
||||||
|
"final": [120]
|
||||||
|
},
|
||||||
|
"yazma": {
|
||||||
|
"adim1": [[34, 58]],
|
||||||
|
"sohbet": [[22, 50], [170, 194]]
|
||||||
|
}
|
||||||
|
}
|
||||||
16
tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"lib": ["DOM", "ES2022"],
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["src/eski"]
|
||||||
|
}
|
||||||