video/ ayrı bir Remotion projesi; Docker imajı, ESLint ve tsc kapsamı dışında. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
138 lines
4.4 KiB
TypeScript
138 lines
4.4 KiB
TypeScript
import type {
|
||
TransitionPresentation,
|
||
TransitionPresentationComponentProps,
|
||
} from "@remotion/transitions";
|
||
import { AbsoluteFill, Easing, interpolate } from "remotion";
|
||
import { H, LOGO_KARELER, PIKSEL, W } from "../marka";
|
||
import { clamp01, hash } from "../yardimci";
|
||
|
||
/**
|
||
* Piksel dalgası: yeni sahne, bir noktadan (çoğunlukla imlecin tıkladığı
|
||
* yer) yayılan iri piksellerle açılır. Dalganın önünde logonun üç rengi
|
||
* (mavi, şeftali, turuncu) yanıp söner — hero haritasındaki ısınan
|
||
* piksellerin sahne ölçeğindeki hâli. Hücreler sitenin nokta oranını
|
||
* (%74, rx %20) taşır; dalga geçince birleşip kesintisiz yüzeye döner.
|
||
*/
|
||
export type PikselDalgaProps = {
|
||
origin?: { x: number; y: number };
|
||
/** Hücre kenarı (px) */
|
||
hucre?: number;
|
||
/** Dalga önünün kalınlığı (px) */
|
||
bant?: number;
|
||
/** Dalga önündeki piksellerin renkleri */
|
||
renkler?: string[];
|
||
/** Çıkan sahne hafifçe geri çekilsin (derinlik) */
|
||
geriCekil?: number;
|
||
tohum?: number;
|
||
};
|
||
|
||
const VARSAYILAN_RENKLER = LOGO_KARELER.map((k) => k.renk);
|
||
|
||
const koseUzakligi = (o: { x: number; y: number }) =>
|
||
Math.max(
|
||
Math.hypot(o.x, o.y),
|
||
Math.hypot(W - o.x, o.y),
|
||
Math.hypot(o.x, H - o.y),
|
||
Math.hypot(W - o.x, H - o.y),
|
||
);
|
||
|
||
/**
|
||
* Dalga önünün yarıçapı. Gelen sahne kendi öğelerini dalgayla eş zamanlı
|
||
* belirtmek isterse (ör. haritanın noktaları) aynı fonksiyonu kullanır.
|
||
*/
|
||
export const dalgaYaricapi = (
|
||
p: number,
|
||
origin: { x: number; y: number } = { x: W / 2, y: H / 2 },
|
||
bant = 420,
|
||
) =>
|
||
interpolate(clamp01(p), [0, 1], [0, koseUzakligi(origin) + bant * 1.25], {
|
||
easing: Easing.bezier(0.45, 0.05, 0.4, 1),
|
||
});
|
||
|
||
const PikselDalgaSunum: React.FC<
|
||
TransitionPresentationComponentProps<PikselDalgaProps>
|
||
> = ({ children, presentationProgress, presentationDirection, passedProps }) => {
|
||
const {
|
||
origin = { x: W / 2, y: H / 2 },
|
||
hucre = 64,
|
||
bant = 420,
|
||
renkler = VARSAYILAN_RENKLER,
|
||
geriCekil = 0.05,
|
||
tohum = 3,
|
||
} = passedProps;
|
||
const p = presentationProgress;
|
||
|
||
if (presentationDirection === "exiting") {
|
||
return (
|
||
<AbsoluteFill
|
||
style={{
|
||
transformOrigin: `${origin.x}px ${origin.y}px`,
|
||
scale: `${1 - geriCekil * Easing.in(Easing.cubic)(p)}`,
|
||
}}
|
||
>
|
||
{children}
|
||
</AbsoluteFill>
|
||
);
|
||
}
|
||
|
||
const sutun = Math.ceil(W / hucre);
|
||
const satir = Math.ceil(H / hucre);
|
||
const r = dalgaYaricapi(p, origin, bant);
|
||
|
||
let maske = "";
|
||
let renkli = "";
|
||
for (let j = 0; j < satir; j++) {
|
||
for (let i = 0; i < sutun; i++) {
|
||
const cx = i * hucre + hucre / 2;
|
||
const cy = j * hucre + hucre / 2;
|
||
const d = Math.hypot(cx - origin.x, cy - origin.y) + (hash(i, j, tohum) - 0.5) * bant * 0.55;
|
||
const t = clamp01((r - d) / bant);
|
||
if (t <= 0) continue;
|
||
// Önde renkli piksel belirip söner…
|
||
if (t < 0.72) {
|
||
const c = Math.sin(Math.PI * (t / 0.72));
|
||
const boy = hucre * PIKSEL.nokta * c;
|
||
const renk = renkler[Math.floor(hash(i, j, tohum + 9) * renkler.length)];
|
||
renkli += `<rect x="${cx - boy / 2}" y="${cy - boy / 2}" width="${boy}" height="${boy}" rx="${boy * PIKSEL.rx}" fill="${renk}"/>`;
|
||
}
|
||
// …arkasından yeni sahne piksel piksel açılır
|
||
const m = Easing.bezier(0.22, 1, 0.36, 1)(clamp01((t - 0.38) / 0.62));
|
||
if (m > 0) {
|
||
const boy = hucre * (PIKSEL.nokta + 0.3 * m);
|
||
const rx = hucre * PIKSEL.rx * (1 - m);
|
||
maske += `<rect x="${cx - boy / 2}" y="${cy - boy / 2}" width="${boy}" height="${boy}" rx="${rx}" fill="white"/>`;
|
||
}
|
||
}
|
||
}
|
||
|
||
const maskeUrl = `url("data:image/svg+xml;utf8,${encodeURIComponent(
|
||
`<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">${maske}</svg>`,
|
||
)}")`;
|
||
|
||
return (
|
||
<AbsoluteFill>
|
||
<AbsoluteFill
|
||
style={{
|
||
maskImage: maskeUrl,
|
||
WebkitMaskImage: maskeUrl,
|
||
maskSize: "100% 100%",
|
||
WebkitMaskSize: "100% 100%",
|
||
maskRepeat: "no-repeat",
|
||
WebkitMaskRepeat: "no-repeat",
|
||
}}
|
||
>
|
||
{children}
|
||
</AbsoluteFill>
|
||
<AbsoluteFill style={{ pointerEvents: "none" }}>
|
||
<svg width={W} height={H} dangerouslySetInnerHTML={{ __html: renkli }} />
|
||
</AbsoluteFill>
|
||
</AbsoluteFill>
|
||
);
|
||
};
|
||
|
||
export function pikselDalga(
|
||
props: PikselDalgaProps = {},
|
||
): TransitionPresentation<PikselDalgaProps> {
|
||
return { component: PikselDalgaSunum, props };
|
||
}
|