Compare commits

..
11 Commits
36 changed files with 4043 additions and 105 deletions
@@ -0,0 +1,115 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { AnimationSourceDetailContent } from "@/components/animation-source-detail-content";
import { ArticleJsonLd } from "@/components/json-ld";
import {
getAnimationSourceBySlug,
listAnimationSources,
} from "@/data/animation-sources";
const SITE_URL =
process.env.NEXT_PUBLIC_SITE_URL || "https://poyrazavsever.com";
type AnimationSourceDetailPageProps = {
params: Promise<{ locale: string; slug: string }>;
};
function getLocalizedPath(locale: string, slug: string) {
const localePrefix = locale === "tr" ? "" : `/${locale}`;
return `${localePrefix}/animation-sources/${slug}`;
}
export async function generateStaticParams() {
const sources = await listAnimationSources();
return sources.map((source) => ({
locale: source.lang,
slug: source.slug,
}));
}
export const dynamicParams = false;
export async function generateMetadata({
params,
}: AnimationSourceDetailPageProps): Promise<Metadata> {
const { locale, slug } = await params;
const source = await getAnimationSourceBySlug(slug, locale);
if (!source) {
return { title: locale === "en" ? "Source not found" : "Kaynak bulunamadı" };
}
const url = `${SITE_URL}${getLocalizedPath(locale, source.slug)}`;
const socialImageUrl = new URL(source.coverImage, SITE_URL).toString();
const turkishUrl = `${SITE_URL}${getLocalizedPath("tr", source.slug)}`;
const englishUrl = `${SITE_URL}${getLocalizedPath("en", source.slug)}`;
return {
title: source.title,
description: source.excerpt,
alternates: {
canonical: url,
languages: {
"tr-TR": turkishUrl,
"en-US": englishUrl,
},
},
openGraph: {
title: source.title,
description: source.excerpt,
url,
siteName: "Poyraz Avsever",
type: "article",
locale: locale === "en" ? "en_US" : "tr_TR",
alternateLocale: locale === "en" ? ["tr_TR"] : ["en_US"],
publishedTime: source.date,
authors: [source.author],
images: [
{
url: socialImageUrl,
type: "image/gif",
width: 480,
height: 480,
alt: source.title,
},
],
},
twitter: {
card: "summary_large_image",
title: source.title,
description: source.excerpt,
creator: "@poyrazavsever",
images: [
{
url: socialImageUrl,
alt: source.title,
},
],
},
};
}
export default async function AnimationSourceDetailPage({
params,
}: AnimationSourceDetailPageProps) {
const { locale, slug } = await params;
const source = await getAnimationSourceBySlug(slug, locale);
if (!source) notFound();
const url = `${SITE_URL}${getLocalizedPath(locale, source.slug)}`;
return (
<>
<ArticleJsonLd
title={source.title}
description={source.excerpt}
url={url}
image={source.coverImage}
datePublished={source.date}
authorName={source.author}
/>
<AnimationSourceDetailContent source={source} />
</>
);
}
+79
View File
@@ -0,0 +1,79 @@
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { AnimationSourcesContent } from "@/components/animation-sources-content";
import { listAnimationSources } from "@/data/animation-sources";
const SITE_URL =
process.env.NEXT_PUBLIC_SITE_URL || "https://poyrazavsever.com";
type AnimationSourcesPageProps = {
params: Promise<{ locale: string }>;
};
export async function generateMetadata({
params,
}: AnimationSourcesPageProps): Promise<Metadata> {
const { locale } = await params;
const [t, sources] = await Promise.all([
getTranslations({ locale, namespace: "AnimationSources" }),
listAnimationSources(locale),
]);
const localizedPath = locale === "en" ? "/en/animation-sources" : "/animation-sources";
const url = `${SITE_URL}${localizedPath}`;
const socialImagePath = sources[0]?.coverImage ?? "/logo/logo.png";
const socialImageUrl = new URL(socialImagePath, SITE_URL).toString();
const isGif = socialImagePath.toLowerCase().endsWith(".gif");
return {
title: t("title"),
description: t("description"),
alternates: {
canonical: url,
languages: {
"tr-TR": `${SITE_URL}/animation-sources`,
"en-US": `${SITE_URL}/en/animation-sources`,
},
},
openGraph: {
title: t("title"),
description: t("description"),
url,
siteName: "Poyraz Avsever",
type: "website",
locale: locale === "en" ? "en_US" : "tr_TR",
images: [
{
url: socialImageUrl,
type: isGif ? "image/gif" : "image/png",
width: isGif ? 480 : 1200,
height: isGif ? 480 : 1200,
alt: t("title"),
},
],
},
twitter: {
card: "summary_large_image",
title: t("title"),
description: t("description"),
creator: "@poyrazavsever",
images: [{ url: socialImageUrl, alt: t("title") }],
},
};
}
export default async function AnimationSourcesPage({
params,
}: AnimationSourcesPageProps) {
const { locale } = await params;
const [sources, t] = await Promise.all([
listAnimationSources(locale),
getTranslations({ locale, namespace: "AnimationSources" }),
]);
return (
<AnimationSourcesContent
sources={sources}
emptyLabel={t("empty")}
/>
);
}
+17 -2
View File
@@ -7,6 +7,8 @@ import "../globals.css";
import { AppShell } from "@/components/app-shell"; import { AppShell } from "@/components/app-shell";
import { GoogleAnalytics } from "@/components/google-analytics"; import { GoogleAnalytics } from "@/components/google-analytics";
import { PoyrazBottomRightFollower } from "@/components/poyraz-bottom-right-follower";
import { listAnimationSources } from "@/data/animation-sources";
export async function generateMetadata({ export async function generateMetadata({
params, params,
@@ -107,14 +109,27 @@ export default async function LocaleLayout({
} }
// Provide messages for NextIntlClientProvider // Provide messages for NextIntlClientProvider
const messages = await getMessages(); const [messages, animationSources] = await Promise.all([
getMessages(),
listAnimationSources(locale),
]);
const animationSourceSearchItems = animationSources.map((source) => ({
slug: source.slug,
title: source.title,
excerpt: source.excerpt,
platform: source.platform,
tools: source.tools,
}));
return ( return (
<html lang={locale}> <html lang={locale}>
<body className="min-h-dvh bg-background text-foreground antialiased"> <body className="min-h-dvh bg-background text-foreground antialiased">
<NextIntlClientProvider messages={messages}> <NextIntlClientProvider messages={messages}>
<GoogleAnalytics /> <GoogleAnalytics />
<AppShell>{children}</AppShell> <AppShell animationSources={animationSourceSearchItems}>
{children}
</AppShell>
<PoyrazBottomRightFollower />
</NextIntlClientProvider> </NextIntlClientProvider>
</body> </body>
</html> </html>
+23 -2
View File
@@ -1,6 +1,13 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { LinksContent } from "@/components/links-content"; import { LinksContent } from "@/components/links-content";
type LinksPageProps = {
searchParams?: Promise<{
category?: string | string[];
query?: string | string[];
}>;
};
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Links", title: "Links",
description: description:
@@ -31,6 +38,20 @@ export const metadata: Metadata = {
}, },
}; };
export default function LinksPage() { function firstParam(value?: string | string[]) {
return <LinksContent />; return Array.isArray(value) ? value[0] : value;
}
export default async function LinksPage({ searchParams }: LinksPageProps) {
const resolvedSearchParams = searchParams ? await searchParams : undefined;
const initialCategory = firstParam(resolvedSearchParams?.category);
const initialQuery = firstParam(resolvedSearchParams?.query);
return (
<LinksContent
key={`${initialCategory ?? "all"}-${initialQuery ?? ""}`}
initialCategory={initialCategory}
initialQuery={initialQuery}
/>
);
} }
+13
View File
@@ -13,6 +13,19 @@
} }
} }
html[data-poyraz-theme="light"] {
--poyraz-background: #ffffff;
background-color: #ffffff;
}
html[data-poyraz-theme="light"] body {
background-color: #ffffff;
}
html[data-poyraz-theme="dark"] [data-cursor-portrait] {
display: none;
}
@layer utilities { @layer utilities {
@keyframes marquee { @keyframes marquee {
0% { transform: translateX(0%); } 0% { transform: translateX(0%); }
+21 -2
View File
@@ -1,11 +1,15 @@
import { listBlogDetails } from "@/data/blog-detail"; import { listBlogDetails } from "@/data/blog-detail";
import { listAnimationSources } from "@/data/animation-sources";
import type { MetadataRoute } from "next"; import type { MetadataRoute } from "next";
const SITE_URL = const SITE_URL =
process.env.NEXT_PUBLIC_SITE_URL || "https://poyrazavsever.com"; process.env.NEXT_PUBLIC_SITE_URL || "https://poyrazavsever.com";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> { export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await listBlogDetails(); const [posts, animationSources] = await Promise.all([
listBlogDetails(),
listAnimationSources(),
]);
const staticRoutes: MetadataRoute.Sitemap = [ const staticRoutes: MetadataRoute.Sitemap = [
{ {
@@ -62,6 +66,12 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
changeFrequency: "monthly", changeFrequency: "monthly",
priority: 0.4, priority: 0.4,
}, },
{
url: `${SITE_URL}/animation-sources`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.7,
},
]; ];
const blogRoutes: MetadataRoute.Sitemap = posts.map((post) => ({ const blogRoutes: MetadataRoute.Sitemap = posts.map((post) => ({
@@ -71,5 +81,14 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
priority: 0.7, priority: 0.7,
})); }));
return [...staticRoutes, ...blogRoutes]; const animationSourceRoutes: MetadataRoute.Sitemap = animationSources.map(
(source) => ({
url: `${SITE_URL}${source.lang === "en" ? "/en" : ""}/animation-sources/${source.slug}`,
lastModified: source.date ? new Date(source.date) : new Date(),
changeFrequency: "monthly",
priority: 0.6,
}),
);
return [...staticRoutes, ...blogRoutes, ...animationSourceRoutes];
} }
@@ -0,0 +1,404 @@
"use client";
import Image from "next/image";
import { Icon } from "@iconify/react";
import { useTranslations } from "next-intl";
import React, { useCallback, useEffect, useRef, useState } from "react";
import ReactMarkdown from "react-markdown";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
import remarkGfm from "remark-gfm";
import { Badge, Card, Typography } from "poyraz-ui/atoms";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "poyraz-ui/molecules";
import { ArticleToc } from "@/components/article-toc";
import type { AnimationSource } from "@/data/animation-sources";
import { Link } from "@/i18n/routing";
import { slugifyMarkdownHeading } from "@/lib/markdown-headings";
type AnimationSourceDetailContentProps = {
source: AnimationSource;
};
function extractText(children: React.ReactNode): string {
if (typeof children === "string" || typeof children === "number") {
return String(children);
}
if (Array.isArray(children)) return children.map(extractText).join("");
if (children && typeof children === "object" && "props" in children) {
return extractText(
(children as React.ReactElement<{ children?: React.ReactNode }>).props
.children,
);
}
return "";
}
async function copyToClipboard(value: string) {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(value);
return;
}
const textarea = document.createElement("textarea");
textarea.value = value;
textarea.setAttribute("readonly", "");
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.select();
document.execCommand("copy");
textarea.remove();
}
function CopyableCodeBlock({
code,
language,
}: {
code: string;
language: string;
}) {
const t = useTranslations("AnimationSources");
const [copied, setCopied] = useState(false);
const resetTimerRef = useRef<number | null>(null);
useEffect(() => {
return () => {
if (resetTimerRef.current !== null) {
window.clearTimeout(resetTimerRef.current);
}
};
}, []);
const handleCopy = async () => {
try {
await copyToClipboard(code);
setCopied(true);
if (resetTimerRef.current !== null) {
window.clearTimeout(resetTimerRef.current);
}
resetTimerRef.current = window.setTimeout(() => setCopied(false), 1800);
} catch {
setCopied(false);
}
};
const displayLanguage = language || "text";
const highlighterLanguage = language === "prompt" ? "text" : displayLanguage;
return (
<Card className="my-4 overflow-hidden rounded-sm border-border">
<div className="flex h-9 items-center justify-between border-b border-border bg-muted/40 px-3">
<span className="text-[11px] font-medium uppercase text-muted-foreground">
{displayLanguage}
</span>
<TooltipProvider delayDuration={150}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleCopy}
className="inline-flex h-7 w-7 cursor-pointer items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
aria-label={copied ? t("copied") : t("copy")}
>
<Icon
icon={copied ? "mdi:check" : "mdi:content-copy"}
width={15}
height={15}
/>
</button>
</TooltipTrigger>
<TooltipContent>{copied ? t("copied") : t("copy")}</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<SyntaxHighlighter
language={highlighterLanguage}
style={oneDark}
customStyle={{
margin: 0,
borderRadius: 0,
fontSize: "13px",
lineHeight: "1.65",
}}
wrapLongLines={false}
>
{code}
</SyntaxHighlighter>
</Card>
);
}
function MarkdownImage({ src, alt }: { src?: string; alt?: string }) {
if (!src || !src.startsWith("/")) return null;
return (
<Card className="my-4 overflow-hidden rounded-sm border-border">
<Image
src={src}
alt={alt ?? ""}
width={1200}
height={1200}
unoptimized={src.toLowerCase().endsWith(".gif")}
sizes="(max-width: 768px) 100vw, 720px"
className="h-auto w-full object-contain"
/>
{alt ? (
<Typography
variant="small"
className="block border-t border-border px-3 py-2 text-center text-muted-foreground"
>
{alt}
</Typography>
) : null}
</Card>
);
}
export function AnimationSourceDetailContent({
source,
}: AnimationSourceDetailContentProps) {
const t = useTranslations("AnimationSources");
const progressBarRef = useRef<HTMLDivElement>(null);
const [tocOpen, setTocOpen] = useState(false);
const closeToc = useCallback(() => setTocOpen(false), []);
useEffect(() => {
let rafId = 0;
const update = () => {
rafId = 0;
const bar = progressBarRef.current;
if (!bar) return;
const scrollableHeight =
document.documentElement.scrollHeight -
document.documentElement.clientHeight;
const progress =
scrollableHeight <= 0 ? 100 : (window.scrollY / scrollableHeight) * 100;
bar.style.width = `${Math.min(100, Math.max(0, progress))}%`;
};
const handleScroll = () => {
if (rafId !== 0) return;
rafId = window.requestAnimationFrame(update);
};
window.addEventListener("scroll", handleScroll, { passive: true });
update();
return () => {
window.removeEventListener("scroll", handleScroll);
if (rafId !== 0) window.cancelAnimationFrame(rafId);
};
}, []);
return (
<>
<div className="fixed top-0 left-0 z-50 h-1 w-full bg-border/70">
<div ref={progressBarRef} className="h-full w-0 bg-red-600" />
</div>
<div className="flex gap-4">
<div className="min-w-0 flex-1">
<article className="space-y-6 rounded-sm border border-border p-5 md:p-8">
<Link
href="/animation-sources"
className="inline-flex items-center gap-1.5 rounded-sm border border-border px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground"
>
<Icon icon="mdi:arrow-left" width={15} height={15} />
{t("back")}
</Link>
<header className="space-y-3 border-b border-border pb-5">
<div className="flex flex-wrap items-center gap-2">
<Badge className="rounded-sm">{source.platform}</Badge>
{source.tools.map((tool) => (
<Badge key={tool} variant="outline" className="rounded-sm">
{tool}
</Badge>
))}
</div>
<Typography variant="h2">{source.title}</Typography>
<Typography variant="p" className="text-sm text-muted-foreground">
{source.excerpt}
</Typography>
<Typography variant="small" className="block text-muted-foreground">
{source.author} · {source.date}
</Typography>
</header>
<section className="min-w-0 space-y-5">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
h1: ({ children }) => (
<Typography
id={slugifyMarkdownHeading(extractText(children))}
variant="h2"
className="mt-10 mb-3 border-b border-border pb-3"
>
{children}
</Typography>
),
h2: ({ children }) => (
<Typography
id={slugifyMarkdownHeading(extractText(children))}
variant="h3"
className="mt-8 mb-2 scroll-mt-24 border-b border-border pb-2"
>
{children}
</Typography>
),
h3: ({ children }) => (
<Typography
id={slugifyMarkdownHeading(extractText(children))}
variant="large"
className="mt-6 mb-1 scroll-mt-24 text-foreground"
>
{children}
</Typography>
),
p: ({ node, children }) => {
const containsImage = node?.children.some(
(child) =>
child.type === "element" && child.tagName === "img",
);
if (containsImage) {
return <div className="my-4">{children}</div>;
}
return (
<Typography
variant="p"
className="text-sm leading-7 text-foreground/85"
>
{children}
</Typography>
);
},
ul: ({ children }) => (
<ul className="my-3 list-disc space-y-1.5 pl-5 text-sm leading-7 text-foreground/85">
{children}
</ul>
),
ol: ({ children }) => (
<ol className="my-3 list-decimal space-y-1.5 pl-5 text-sm leading-7 text-foreground/85">
{children}
</ol>
),
a: ({ href, children }) => (
<a
href={href}
target={href?.startsWith("http") ? "_blank" : undefined}
rel={href?.startsWith("http") ? "noopener noreferrer" : undefined}
className="text-red-600 underline decoration-red-600/30 underline-offset-2 transition-colors hover:decoration-red-600"
>
{children}
</a>
),
blockquote: ({ children }) => (
<Card className="my-4 rounded-sm border-border border-l-red-600 bg-muted/30 px-4 py-3">
<div className="text-sm text-muted-foreground">{children}</div>
</Card>
),
hr: () => <div className="my-6 border-t border-border" />,
table: ({ children }) => (
<Card className="my-4 overflow-x-auto rounded-sm border-border">
<table className="min-w-full border-collapse text-sm">
{children}
</table>
</Card>
),
th: ({ children }) => (
<th className="border-b border-border bg-muted/50 px-4 py-2.5 text-left text-xs font-semibold text-muted-foreground">
{children}
</th>
),
td: ({ children }) => (
<td className="border-b border-border px-4 py-2.5 align-top text-sm">
{children}
</td>
),
img: ({ src, alt }) => (
<MarkdownImage
src={typeof src === "string" ? src : undefined}
alt={typeof alt === "string" ? alt : undefined}
/>
),
pre: ({ children }) => <>{children}</>,
code: ({ className, children }) => {
const match = /language-([\w-]+)/.exec(className ?? "");
if (!match) {
return (
<code className="rounded-sm border border-border/60 bg-muted/60 px-1.5 py-0.5 text-[13px] text-red-600">
{children}
</code>
);
}
return (
<CopyableCodeBlock
language={match[1]}
code={String(children).replace(/\n$/, "")}
/>
);
},
}}
>
{source.markdown}
</ReactMarkdown>
</section>
</article>
</div>
<aside className="hidden w-52 shrink-0 lg:block">
<div className="sticky top-24">
<ArticleToc markdown={source.markdown} title={t("toc")} />
</div>
</aside>
</div>
<button
type="button"
onClick={() => setTocOpen(true)}
className="fixed right-4 bottom-6 z-40 flex h-11 w-11 cursor-pointer items-center justify-center rounded-full border border-border bg-background shadow-lg transition-transform hover:scale-105 active:scale-95 lg:hidden"
aria-label={t("toc")}
>
<Icon icon="mdi:table-of-contents" width={22} height={22} className="text-red-600" />
</button>
{tocOpen ? (
<div className="fixed inset-0 z-50 lg:hidden" onClick={closeToc}>
<div className="absolute inset-0 bg-black/40" />
<div
className="absolute right-0 bottom-0 left-0 max-h-[60vh] overflow-y-auto rounded-t-lg border-t border-border bg-background p-5 shadow-2xl"
onClick={(event) => event.stopPropagation()}
>
<div className="mb-3 flex items-center justify-between">
<Typography variant="large">{t("toc")}</Typography>
<button
type="button"
onClick={closeToc}
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
aria-label={t("closeToc")}
>
<Icon icon="mdi:close" width={18} height={18} />
</button>
</div>
<ArticleToc
markdown={source.markdown}
title={t("toc")}
onNavigate={closeToc}
/>
</div>
</div>
) : null}
</>
);
}
+45
View File
@@ -0,0 +1,45 @@
import { Card, Typography } from "poyraz-ui/atoms";
import { NewsCard } from "poyraz-ui/molecules";
import { Link } from "@/i18n/routing";
import type { AnimationSource } from "@/data/animation-sources";
type AnimationSourcesContentProps = {
sources: AnimationSource[];
emptyLabel: string;
};
export function AnimationSourcesContent({
sources,
emptyLabel,
}: AnimationSourcesContentProps) {
return (
<section>
{sources.length > 0 ? (
<div className="space-y-3">
{sources.map((source) => (
<Link
key={`${source.lang}-${source.slug}`}
href={`/animation-sources/${source.slug}`}
data-animation-source-card
className="block min-w-0"
>
<NewsCard
image={source.coverImage}
category={source.platform}
title={source.title}
date={source.date}
className="w-full rounded-sm border-border [&>div]:min-h-32 [&>div>div:first-child]:w-32 sm:[&>div>div:first-child]:w-40 [&_h3]:text-base"
/>
</Link>
))}
</div>
) : (
<Card className="rounded-sm border-border p-6 text-center">
<Typography variant="p" className="text-muted-foreground">
{emptyLabel}
</Typography>
</Card>
)}
</section>
);
}
+8 -2
View File
@@ -9,6 +9,7 @@ import { ANNOUNCEMENT_ITEMS, ENABLE_NEKO_FOLLOWER } from "@/data/site-settings";
import { useLocale } from "next-intl"; import { useLocale } from "next-intl";
import { getLocalizedValue } from "@/lib/locale"; import { getLocalizedValue } from "@/lib/locale";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import type { AnimationSourceSearchItem } from "@/lib/command-palette-links";
const AtaturkWidgetModal = dynamic( const AtaturkWidgetModal = dynamic(
() => import("@/components/ataturk-widget-modal").then((mod) => mod.AtaturkWidgetModal), () => import("@/components/ataturk-widget-modal").then((mod) => mod.AtaturkWidgetModal),
@@ -17,6 +18,7 @@ const AtaturkWidgetModal = dynamic(
type AppShellProps = { type AppShellProps = {
children: React.ReactNode; children: React.ReactNode;
animationSources: AnimationSourceSearchItem[];
}; };
export type ThemeMode = "light" | "dark"; export type ThemeMode = "light" | "dark";
@@ -30,7 +32,7 @@ function getInitialTheme(): ThemeMode {
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
} }
export function AppShell({ children }: AppShellProps) { export function AppShell({ children, animationSources }: AppShellProps) {
const pathname = usePathname(); const pathname = usePathname();
const locale = useLocale(); const locale = useLocale();
const announcement = ANNOUNCEMENT_ITEMS[0]; const announcement = ANNOUNCEMENT_ITEMS[0];
@@ -57,7 +59,11 @@ export function AppShell({ children }: AppShellProps) {
<AtaturkWidgetModal theme={theme} /> <AtaturkWidgetModal theme={theme} />
{ENABLE_NEKO_FOLLOWER ? <NekoFollower /> : null} {ENABLE_NEKO_FOLLOWER ? <NekoFollower /> : null}
<div className="mx-auto flex w-full max-w-4xl flex-col px-4 py-4 "> <div className="mx-auto flex w-full max-w-4xl flex-col px-4 py-4 ">
<SiteNavbar theme={theme} onThemeChange={setTheme} /> <SiteNavbar
theme={theme}
onThemeChange={setTheme}
animationSources={animationSources}
/>
{announcement ? ( {announcement ? (
<AnnouncementBar <AnnouncementBar
variant="branded" variant="branded"
+110
View File
@@ -0,0 +1,110 @@
"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Typography } from "poyraz-ui/atoms";
import { parseMarkdownHeadings } from "@/lib/markdown-headings";
type ArticleTocProps = {
markdown: string;
title: string;
onNavigate?: () => void;
};
export function ArticleToc({ markdown, title, onNavigate }: ArticleTocProps) {
const headings = useMemo(() => parseMarkdownHeadings(markdown), [markdown]);
const [activeId, setActiveId] = useState("");
const rafRef = useRef(0);
const handleClick = useCallback(
(id: string) => {
const target = document.getElementById(id);
if (!target) return;
target.scrollIntoView({ behavior: "smooth", block: "start" });
setActiveId(id);
onNavigate?.();
},
[onNavigate],
);
useEffect(() => {
if (headings.length === 0) return;
const headingElements = headings
.map((heading) => ({
id: heading.id,
element: document.getElementById(heading.id),
}))
.filter((item): item is { id: string; element: HTMLElement } =>
Boolean(item.element),
);
if (headingElements.length === 0) return;
const updateActive = () => {
let current = headingElements[0].id;
for (const item of headingElements) {
if (item.element.getBoundingClientRect().top <= 120) {
current = item.id;
continue;
}
break;
}
setActiveId(current);
};
const observer = new IntersectionObserver(
() => {
cancelAnimationFrame(rafRef.current);
rafRef.current = requestAnimationFrame(updateActive);
},
{
rootMargin: "-120px 0px -65% 0px",
threshold: [0, 1],
},
);
for (const item of headingElements) {
observer.observe(item.element);
}
const timer = window.setTimeout(updateActive, 200);
return () => {
observer.disconnect();
cancelAnimationFrame(rafRef.current);
window.clearTimeout(timer);
};
}, [headings]);
if (headings.length < 2) return null;
return (
<nav
aria-label={title}
className="max-h-[calc(100vh-7rem)] space-y-1 overflow-y-auto overscroll-contain pr-1 [scrollbar-width:thin]"
>
<Typography variant="small" className="mb-2 font-semibold text-foreground">
{title}
</Typography>
{headings.map((heading, index) => (
<button
key={`${heading.id}-${index}`}
type="button"
onClick={() => handleClick(heading.id)}
className={[
"block w-full cursor-pointer truncate border-l-2 text-left text-xs leading-relaxed transition-colors",
heading.level === 3 ? "pl-5" : "pl-3",
activeId === heading.id
? "border-red-600 text-red-600"
: "border-transparent text-muted-foreground hover:text-foreground",
].join(" ")}
>
{heading.text}
</button>
))}
</nav>
);
}
+21 -3
View File
@@ -24,6 +24,11 @@ import {
type CategoryFilter = "all" | LinkDirectoryCategory; type CategoryFilter = "all" | LinkDirectoryCategory;
type LinksContentProps = {
initialCategory?: string;
initialQuery?: string;
};
const CATEGORY_ORDER = { const CATEGORY_ORDER = {
resources: 0, resources: 0,
navigation: 1, navigation: 1,
@@ -58,13 +63,26 @@ function formatHref(href: string) {
return href.replace(/^https?:\/\//, "").replace(/\/$/, ""); return href.replace(/^https?:\/\//, "").replace(/\/$/, "");
} }
export function LinksContent() { function parseCategoryFilter(value?: string): CategoryFilter {
if (value === "navigation" || value === "social" || value === "resources") {
return value;
}
return "all";
}
export function LinksContent({
initialCategory,
initialQuery = "",
}: LinksContentProps) {
const t = useTranslations("Links"); const t = useTranslations("Links");
const tNav = useTranslations("Nav"); const tNav = useTranslations("Nav");
const locale = useLocale(); const locale = useLocale();
const [activeCategory, setActiveCategory] = useState<CategoryFilter>("all"); const [activeCategory, setActiveCategory] = useState<CategoryFilter>(() =>
const [query, setQuery] = useState(""); parseCategoryFilter(initialCategory),
);
const [query, setQuery] = useState(initialQuery);
const filterItems = useMemo(() => [ const filterItems = useMemo(() => [
{ id: "all" as const, label: t("allCategories") }, { id: "all" as const, label: t("allCategories") },
+295
View File
@@ -0,0 +1,295 @@
"use client";
import Image from "next/image";
import { useEffect, useRef, useState } from "react";
import {
clamp,
DEFAULT_TIME,
mapPointerYToTime,
SMOOTHING,
TOTAL_DURATION,
} from "@/lib/cursor-portrait";
const VIDEO_SRC = "/media/cursor-portrait/poyraz-bottom-right.mp4";
const POSTER_SRC = "/media/cursor-portrait/poyraz-bottom-right-poster.webp";
const SEEK_INTERVAL_MS = 1000 / 60;
const MIN_TIME_DELTA = 0.002;
const MIN_SEEK_DELTA = 1 / 120;
type DisplayMode =
| "pending"
| "interactive"
| "poster-reduced"
| "hidden-mobile"
| "hidden-dark";
function subscribeToMediaQuery(query: MediaQueryList, listener: () => void) {
if (typeof query.addEventListener === "function") {
query.addEventListener("change", listener);
return () => query.removeEventListener("change", listener);
}
query.addListener(listener);
return () => query.removeListener(listener);
}
export function PoyrazBottomRightFollower() {
const videoRef = useRef<HTMLVideoElement>(null);
const pointerYRef = useRef<number | null>(null);
const targetTimeRef = useRef(DEFAULT_TIME);
const currentTimeRef = useRef(DEFAULT_TIME);
const rafIdRef = useRef<number | null>(null);
const lastSeekTimestampRef = useRef(0);
const metadataReadyRef = useRef(false);
const primedRef = useRef(false);
const [displayMode, setDisplayMode] = useState<DisplayMode>("pending");
const [videoFailed, setVideoFailed] = useState(false);
useEffect(() => {
const finePointerQuery = window.matchMedia("(pointer: fine)");
const desktopQuery = window.matchMedia("(min-width: 840px)");
const reducedMotionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
const updateDisplayMode = () => {
if (document.documentElement.dataset.poyrazTheme === "dark") {
setDisplayMode("hidden-dark");
return;
}
if (!desktopQuery.matches || !finePointerQuery.matches) {
setDisplayMode("hidden-mobile");
return;
}
setDisplayMode(reducedMotionQuery.matches ? "poster-reduced" : "interactive");
};
updateDisplayMode();
const unsubscribeFinePointer = subscribeToMediaQuery(
finePointerQuery,
updateDisplayMode,
);
const unsubscribeDesktop = subscribeToMediaQuery(desktopQuery, updateDisplayMode);
const unsubscribeReducedMotion = subscribeToMediaQuery(
reducedMotionQuery,
updateDisplayMode,
);
const themeObserver = new MutationObserver(updateDisplayMode);
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-poyraz-theme"],
});
return () => {
unsubscribeFinePointer();
unsubscribeDesktop();
unsubscribeReducedMotion();
themeObserver.disconnect();
};
}, []);
useEffect(() => {
if (displayMode !== "interactive" || videoFailed) return;
const video = videoRef.current;
if (!video) return;
let disposed = false;
const stopAnimationLoop = () => {
if (rafIdRef.current === null) return;
window.cancelAnimationFrame(rafIdRef.current);
rafIdRef.current = null;
};
const seekVideo = (timestamp: number) => {
if (
!metadataReadyRef.current ||
video.seeking ||
timestamp - lastSeekTimestampRef.current < SEEK_INTERVAL_MS
) {
return;
}
const duration = Number.isFinite(video.duration)
? Math.min(video.duration, TOTAL_DURATION)
: TOTAL_DURATION;
const nextTime = clamp(currentTimeRef.current, 0, duration);
if (Math.abs(video.currentTime - nextTime) < MIN_SEEK_DELTA) return;
try {
video.currentTime = nextTime;
lastSeekTimestampRef.current = timestamp;
} catch {
// The poster remains visible until the browser exposes seekable metadata.
}
};
const runAnimationFrame = (timestamp: number) => {
rafIdRef.current = null;
if (disposed || document.hidden) return;
const difference = targetTimeRef.current - currentTimeRef.current;
const settled = Math.abs(difference) <= MIN_TIME_DELTA;
currentTimeRef.current = settled
? targetTimeRef.current
: currentTimeRef.current + difference * SMOOTHING;
seekVideo(timestamp);
const videoNeedsSeek =
metadataReadyRef.current &&
(video.seeking ||
Math.abs(video.currentTime - targetTimeRef.current) >= MIN_SEEK_DELTA);
if (!settled || videoNeedsSeek) {
rafIdRef.current = window.requestAnimationFrame(runAnimationFrame);
}
};
const startAnimationLoop = () => {
if (
disposed ||
document.hidden ||
!metadataReadyRef.current ||
rafIdRef.current !== null
) {
return;
}
rafIdRef.current = window.requestAnimationFrame(runAnimationFrame);
};
const returnToDefault = () => {
pointerYRef.current = null;
targetTimeRef.current = DEFAULT_TIME;
startAnimationLoop();
};
const primeVideo = () => {
if (primedRef.current || !metadataReadyRef.current) return;
primedRef.current = true;
const resumeTime = currentTimeRef.current;
video.muted = true;
void video
.play()
.then(() => {
if (disposed) return;
video.pause();
video.currentTime = resumeTime;
})
.catch(() => {
video.pause();
});
};
const handlePointerMove = (event: PointerEvent) => {
if (event.pointerType !== "mouse") return;
pointerYRef.current = event.clientY;
targetTimeRef.current = mapPointerYToTime(event.clientY, window.innerHeight);
primeVideo();
startAnimationLoop();
};
const handleResize = () => {
if (pointerYRef.current === null) return;
targetTimeRef.current = mapPointerYToTime(
pointerYRef.current,
window.innerHeight,
);
startAnimationLoop();
};
const handleVisibilityChange = () => {
if (document.hidden) {
stopAnimationLoop();
return;
}
startAnimationLoop();
};
const handleLoadedMetadata = () => {
video.pause();
metadataReadyRef.current = true;
targetTimeRef.current = DEFAULT_TIME;
currentTimeRef.current = DEFAULT_TIME;
video.currentTime = clamp(DEFAULT_TIME, 0, video.duration);
};
if (video.readyState >= HTMLMediaElement.HAVE_METADATA) {
handleLoadedMetadata();
}
video.addEventListener("loadedmetadata", handleLoadedMetadata);
window.addEventListener("pointermove", handlePointerMove, { passive: true });
document.documentElement.addEventListener("pointerleave", returnToDefault);
window.addEventListener("blur", returnToDefault);
window.addEventListener("resize", handleResize, { passive: true });
document.addEventListener("visibilitychange", handleVisibilityChange);
return () => {
disposed = true;
stopAnimationLoop();
video.pause();
metadataReadyRef.current = false;
primedRef.current = false;
pointerYRef.current = null;
targetTimeRef.current = DEFAULT_TIME;
currentTimeRef.current = DEFAULT_TIME;
lastSeekTimestampRef.current = 0;
video.removeEventListener("loadedmetadata", handleLoadedMetadata);
window.removeEventListener("pointermove", handlePointerMove);
document.documentElement.removeEventListener("pointerleave", returnToDefault);
window.removeEventListener("blur", returnToDefault);
window.removeEventListener("resize", handleResize);
document.removeEventListener("visibilitychange", handleVisibilityChange);
};
}, [displayMode, videoFailed]);
const showVideo = displayMode === "interactive" && !videoFailed;
if (
displayMode === "pending" ||
displayMode === "hidden-mobile" ||
displayMode === "hidden-dark"
) {
return null;
}
return (
<div
aria-hidden="true"
data-cursor-portrait
className="pointer-events-none fixed right-6 bottom-0 z-40 aspect-square w-[clamp(110px,11vw,170px)] select-none bg-white"
>
{showVideo ? (
<video
ref={videoRef}
src={VIDEO_SRC}
muted
playsInline
preload="auto"
poster={POSTER_SRC}
draggable={false}
disablePictureInPicture
className="h-full w-full bg-white object-contain"
onError={() => setVideoFailed(true)}
/>
) : (
<Image
src={POSTER_SRC}
alt=""
fill
sizes="(max-width: 1545px) 11vw, 170px"
draggable={false}
className="object-contain"
/>
)}
</div>
);
}
+9 -3
View File
@@ -17,6 +17,7 @@ import {
} from "poyraz-ui/molecules"; } from "poyraz-ui/molecules";
import { import {
getCommandPaletteGroups, getCommandPaletteGroups,
type AnimationSourceSearchItem,
type CommandPaletteItem as PaletteItem, type CommandPaletteItem as PaletteItem,
} from "@/lib/command-palette-links"; } from "@/lib/command-palette-links";
import { useKeyboardShortcutLabel } from "@/lib/use-keyboard-shortcut-label"; import { useKeyboardShortcutLabel } from "@/lib/use-keyboard-shortcut-label";
@@ -24,9 +25,14 @@ import { useKeyboardShortcutLabel } from "@/lib/use-keyboard-shortcut-label";
type SearchCommandProps = { type SearchCommandProps = {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
animationSources: AnimationSourceSearchItem[];
}; };
export function SearchCommand({ open, onOpenChange }: SearchCommandProps) { export function SearchCommand({
open,
onOpenChange,
animationSources,
}: SearchCommandProps) {
const router = useRouter(); const router = useRouter();
const locale = useLocale(); const locale = useLocale();
const tLinks = useTranslations("Links"); const tLinks = useTranslations("Links");
@@ -36,8 +42,8 @@ export function SearchCommand({ open, onOpenChange }: SearchCommandProps) {
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const groups = useMemo(() => { const groups = useMemo(() => {
return getCommandPaletteGroups(locale, tLinks, tNav); return getCommandPaletteGroups(locale, tLinks, tNav, animationSources);
}, [locale, tLinks, tNav]); }, [animationSources, locale, tLinks, tNav]);
const handleOpenChange = useCallback((nextOpen: boolean) => { const handleOpenChange = useCallback((nextOpen: boolean) => {
if (!nextOpen) { if (!nextOpen) {
+209 -18
View File
@@ -5,7 +5,7 @@ import dynamic from "next/dynamic";
import { Link, usePathname } from "@/i18n/routing"; import { Link, usePathname } from "@/i18n/routing";
import { useLocale, useTranslations } from "next-intl"; import { useLocale, useTranslations } from "next-intl";
import { LanguageSwitcher } from "@/components/language-switcher"; import { LanguageSwitcher } from "@/components/language-switcher";
import { useState } from "react"; import { Fragment, useState } from "react";
import { import {
Button, Button,
ButtonIcon, ButtonIcon,
@@ -35,8 +35,15 @@ import {
} from "poyraz-ui/molecules"; } from "poyraz-ui/molecules";
import { NavbarTopBar, NavbarTopBarSection } from "poyraz-ui/organisms"; import { NavbarTopBar, NavbarTopBarSection } from "poyraz-ui/organisms";
import { useKeyboardShortcutLabel } from "@/lib/use-keyboard-shortcut-label"; import { useKeyboardShortcutLabel } from "@/lib/use-keyboard-shortcut-label";
import { getResumeHref, NAV_LINKS, SOCIAL_LINKS, TOP_ICON_LINKS } from "@/lib/links"; import {
getResumeHref,
NAV_DROPDOWN_GROUPS,
NAV_LINKS,
SOCIAL_LINKS,
TOP_ICON_LINKS,
} from "@/lib/links";
import type { ThemeMode } from "@/components/app-shell"; import type { ThemeMode } from "@/components/app-shell";
import type { AnimationSourceSearchItem } from "@/lib/command-palette-links";
const SearchCommand = dynamic( const SearchCommand = dynamic(
() => import("@/components/search-command").then((mod) => mod.SearchCommand), () => import("@/components/search-command").then((mod) => mod.SearchCommand),
@@ -54,6 +61,15 @@ function getNavLinkClass(isActive: boolean) {
].join(" "); ].join(" ");
} }
function getDesktopDropdownTriggerClass(isActive: boolean) {
return [
"relative inline-flex h-7 shrink-0 cursor-pointer items-center justify-center gap-1 whitespace-nowrap rounded-sm px-2.5 text-xs font-medium outline-none",
"transition-[color,background-color] duration-[var(--poyraz-motion-duration-fast)] ease-[var(--poyraz-motion-ease-out)]",
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
isActive ? "text-foreground" : "text-muted-foreground hover:text-foreground",
].join(" ");
}
type ThemeToggleProps = { type ThemeToggleProps = {
theme: ThemeMode; theme: ThemeMode;
onThemeChange: (theme: ThemeMode) => void; onThemeChange: (theme: ThemeMode) => void;
@@ -83,11 +99,19 @@ function ThemeToggle({ theme, onThemeChange }: ThemeToggleProps) {
); );
} }
type SiteNavbarProps = ThemeToggleProps; type SiteNavbarProps = ThemeToggleProps & {
animationSources: AnimationSourceSearchItem[];
};
export function SiteNavbar({ theme, onThemeChange }: SiteNavbarProps) { export function SiteNavbar({
theme,
onThemeChange,
animationSources,
}: SiteNavbarProps) {
const pathname = usePathname(); const pathname = usePathname();
const [searchOpen, setSearchOpen] = useState(false); const [searchOpen, setSearchOpen] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [mobileMenuGroupId, setMobileMenuGroupId] = useState<string | null>(null);
const shortcut = useKeyboardShortcutLabel(); const shortcut = useKeyboardShortcutLabel();
const t = useTranslations("Nav"); const t = useTranslations("Nav");
const locale = useLocale(); const locale = useLocale();
@@ -98,14 +122,22 @@ export function SiteNavbar({ theme, onThemeChange }: SiteNavbarProps) {
}; };
const activeTab = NAV_LINKS.find((item) => isActiveLink(item.href))?.id; const activeTab = NAV_LINKS.find((item) => isActiveLink(item.href))?.id;
const activeMobileMenuGroup = NAV_DROPDOWN_GROUPS.find(
(group) => group.id === mobileMenuGroupId,
);
const handleMobileMenuOpenChange = (open: boolean) => {
setMobileMenuOpen(open);
if (!open) setMobileMenuGroupId(null);
};
const languageLabel = locale === "tr" ? "Switch to English" : "Türkçe'ye geç"; const languageLabel = locale === "tr" ? "Switch to English" : "Türkçe'ye geç";
return ( return (
<div className="space-y-3"> <div className="min-w-0 space-y-3">
<TooltipProvider delayDuration={180}> <TooltipProvider delayDuration={180}>
<NavbarTopBar <NavbarTopBar
variant="secondary" variant="secondary"
className="border-0 bg-transparent p-0 shadow-none" className="border-0 bg-transparent p-0 shadow-none [&>div]:max-w-none [&>div]:px-0"
> >
<NavbarTopBarSection align="end" className="gap-2"> <NavbarTopBarSection align="end" className="gap-2">
{TOP_ICON_LINKS.map((item) => { {TOP_ICON_LINKS.map((item) => {
@@ -161,11 +193,11 @@ export function SiteNavbar({ theme, onThemeChange }: SiteNavbarProps) {
</NavbarTopBar> </NavbarTopBar>
</TooltipProvider> </TooltipProvider>
<header className="flex items-center justify-between gap-3 border-b border-border pb-4"> <header className="flex min-w-0 items-center justify-between gap-3 border-b border-border pb-4">
<Link <Link
href="/" href="/"
aria-label="Ana sayfaya git" aria-label="Ana sayfaya git"
className="inline-flex items-center" className="inline-flex shrink-0 items-center"
> >
<Logo <Logo
src="/logo/logo.png" src="/logo/logo.png"
@@ -179,8 +211,8 @@ export function SiteNavbar({ theme, onThemeChange }: SiteNavbarProps) {
/> />
</Link> </Link>
<div className="hidden items-center gap-3 md:flex"> <div className="hidden min-w-0 flex-1 items-center justify-end gap-2 min-[840px]:flex">
<Tabs value={activeTab ?? ""} className="w-auto"> <Tabs value={activeTab ?? ""} className="w-auto shrink-0">
<TabsList <TabsList
variant="line" variant="line"
radius="sm" radius="sm"
@@ -188,7 +220,8 @@ export function SiteNavbar({ theme, onThemeChange }: SiteNavbarProps) {
aria-label="Ana navigasyon" aria-label="Ana navigasyon"
> >
{NAV_LINKS.map((item, index) => ( {NAV_LINKS.map((item, index) => (
<div key={item.id} className="flex items-center gap-1.5"> <Fragment key={item.id}>
<div className="flex items-center gap-1.5">
{index > 0 && ( {index > 0 && (
<Separator <Separator
orientation="vertical" orientation="vertical"
@@ -206,13 +239,75 @@ export function SiteNavbar({ theme, onThemeChange }: SiteNavbarProps) {
<Link href={item.href}>{t(item.id)}</Link> <Link href={item.href}>{t(item.id)}</Link>
</TabsTrigger> </TabsTrigger>
</div> </div>
{NAV_DROPDOWN_GROUPS.filter(
(group) => group.insertAfter === item.id,
).map((group) => (
<div key={group.id} className="flex items-center gap-1.5">
<Separator
orientation="vertical"
className="h-4 bg-border/70"
decorative
/>
<DropdownMenu interaction="click">
<DropdownMenuTrigger asChild>
<button
type="button"
className={getDesktopDropdownTriggerClass(
group.items.some(
(groupItem) =>
!groupItem.external &&
isActiveLink(groupItem.href.split("?")[0]),
),
)}
>
<span>{t(group.id)}</span>
<Icon icon="mdi:chevron-down" width={14} height={14} />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
surface="solid"
radius="md"
itemSize="md"
itemRadius="sm"
className="w-56 bg-popover"
>
{group.items.map((groupItem) => (
<DropdownMenuItem key={groupItem.id} asChild>
{groupItem.external ? (
<a
href={groupItem.href}
target="_blank"
rel="noreferrer"
className="flex items-center gap-2"
>
<Icon icon={groupItem.icon} width={16} height={16} />
<span>{t(groupItem.id)}</span>
</a>
) : (
<Link
href={groupItem.href}
className="flex items-center gap-2"
>
<Icon icon={groupItem.icon} width={16} height={16} />
<span>{t(groupItem.id)}</span>
</Link>
)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
))}
</Fragment>
))} ))}
</TabsList> </TabsList>
</Tabs> </Tabs>
<Separator <Separator
orientation="vertical" orientation="vertical"
className="h-5 bg-border" className="h-5 shrink-0 bg-border"
decorative decorative
/> />
<Button <Button
@@ -221,7 +316,7 @@ export function SiteNavbar({ theme, onThemeChange }: SiteNavbarProps) {
radius="sm" radius="sm"
effect="shine" effect="shine"
onClick={() => setSearchOpen(true)} onClick={() => setSearchOpen(true)}
className={`h-9 w-44 cursor-pointer justify-between px-3 text-sm sm:w-52 ${slowShineClassName}`} className={`h-9 w-auto cursor-pointer px-3 text-sm ${slowShineClassName}`}
aria-label={t("search")} aria-label={t("search")}
> >
<ButtonIcon> <ButtonIcon>
@@ -273,7 +368,7 @@ export function SiteNavbar({ theme, onThemeChange }: SiteNavbarProps) {
</DropdownMenu> </DropdownMenu>
</div> </div>
<div className="flex items-center gap-2 md:hidden"> <div className="flex items-center gap-2 min-[840px]:hidden">
<Button <Button
type="button" type="button"
variant="secondary" variant="secondary"
@@ -287,7 +382,7 @@ export function SiteNavbar({ theme, onThemeChange }: SiteNavbarProps) {
<Icon icon="mdi:magnify" width={16} height={16} /> <Icon icon="mdi:magnify" width={16} height={16} />
</Button> </Button>
<Sheet> <Sheet open={mobileMenuOpen} onOpenChange={handleMobileMenuOpenChange}>
<SheetTrigger asChild> <SheetTrigger asChild>
<Button <Button
type="button" type="button"
@@ -303,8 +398,76 @@ export function SiteNavbar({ theme, onThemeChange }: SiteNavbarProps) {
</Button> </Button>
</SheetTrigger> </SheetTrigger>
<SheetContent side="right" className="w-72 p-4"> <SheetContent side="right" className="w-72 p-4">
<SheetTitle className="sr-only">{t("mobileMenu")}</SheetTitle> {activeMobileMenuGroup ? (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex min-h-9 items-center gap-2 pr-9">
<Button
type="button"
variant="secondary"
size="icon-sm"
radius="sm"
effect="shine"
className={`shrink-0 cursor-pointer ${slowShineClassName}`}
aria-label={t("backToMenu")}
onClick={() => setMobileMenuGroupId(null)}
>
<Icon icon="mdi:chevron-left" width={18} height={18} />
</Button>
<SheetTitle className="truncate text-base font-medium">
{t(activeMobileMenuGroup.id)}
</SheetTitle>
</div>
<Separator className="bg-border" decorative />
<nav aria-label={t(activeMobileMenuGroup.id)}>
<ul className="space-y-1">
{activeMobileMenuGroup.items.map((groupItem) => (
<li key={groupItem.id}>
{groupItem.external ? (
<SheetClose asChild>
<a
href={groupItem.href}
target="_blank"
rel="noreferrer"
className="flex min-h-10 w-full items-center gap-3 rounded-sm px-2 py-2 text-sm text-foreground/70 transition-colors hover:bg-muted hover:text-foreground"
>
<Icon icon={groupItem.icon} width={18} height={18} />
<span>{t(groupItem.id)}</span>
<Icon
icon="mdi:open-in-new"
width={14}
height={14}
className="ml-auto text-muted-foreground"
/>
</a>
</SheetClose>
) : (
<SheetClose asChild>
<Link
href={groupItem.href}
className="flex min-h-10 w-full items-center gap-3 rounded-sm px-2 py-2 text-sm text-foreground/70 transition-colors hover:bg-muted hover:text-foreground"
>
<Icon icon={groupItem.icon} width={18} height={18} />
<span>{t(groupItem.id)}</span>
<Icon
icon="mdi:chevron-right"
width={16}
height={16}
className="ml-auto text-muted-foreground"
/>
</Link>
</SheetClose>
)}
</li>
))}
</ul>
</nav>
</div>
) : (
<div className="flex flex-col gap-4">
<SheetTitle className="sr-only">{t("mobileMenu")}</SheetTitle>
<SheetClose asChild> <SheetClose asChild>
<Button <Button
type="button" type="button"
@@ -330,7 +493,8 @@ export function SiteNavbar({ theme, onThemeChange }: SiteNavbarProps) {
<nav aria-label="Mobil navigasyon"> <nav aria-label="Mobil navigasyon">
<ul className="space-y-2"> <ul className="space-y-2">
{NAV_LINKS.map((item) => ( {NAV_LINKS.map((item) => (
<li key={item.id}> <Fragment key={item.id}>
<li>
<SheetClose asChild> <SheetClose asChild>
<Link <Link
href={item.href} href={item.href}
@@ -340,6 +504,28 @@ export function SiteNavbar({ theme, onThemeChange }: SiteNavbarProps) {
</Link> </Link>
</SheetClose> </SheetClose>
</li> </li>
{NAV_DROPDOWN_GROUPS.filter(
(group) => group.insertAfter === item.id,
).map((group) => (
<li key={group.id}>
<button
type="button"
className={`${getNavLinkClass(
group.items.some(
(groupItem) =>
!groupItem.external &&
isActiveLink(groupItem.href.split("?")[0]),
),
)} w-full cursor-pointer items-center justify-between gap-3 text-left`}
onClick={() => setMobileMenuGroupId(group.id)}
>
<span>{t(group.id)}</span>
<Icon icon="mdi:chevron-right" width={16} height={16} />
</button>
</li>
))}
</Fragment>
))} ))}
</ul> </ul>
</nav> </nav>
@@ -361,11 +547,16 @@ export function SiteNavbar({ theme, onThemeChange }: SiteNavbarProps) {
))} ))}
</div> </div>
</div> </div>
)}
</SheetContent> </SheetContent>
</Sheet> </Sheet>
</div> </div>
<SearchCommand open={searchOpen} onOpenChange={setSearchOpen} /> <SearchCommand
open={searchOpen}
onOpenChange={setSearchOpen}
animationSources={animationSources}
/>
</header> </header>
</div> </div>
); );
@@ -0,0 +1,879 @@
---
title: "How to Build an AI Avatar That Follows the Pointer"
slug: "poyraz-cursor-portrait"
excerpt: "Build the effect from start to finish with the prompts, Wiro AI and MiniMax H3 video workflow, FFmpeg preparation, and React integration I used."
coverImage: "/animation-sources/poyraz-cursor-portrait/avatar-mouse-follow.gif"
platform: "Web"
tools:
- "Wiro AI"
- "MiniMax H3"
- "FFmpeg"
- "React"
date: "2026-08-29"
author: "Poyraz Avsever"
lang: "en"
---
The portrait at the bottom-right is not playing like a normal video. It remains paused while the pointer's vertical position controls the video timeline. At the bottom of the screen the portrait looks down-left, in the middle it looks horizontally left, and at the top it looks up-left.
I generated the video with the **MiniMax H3** model through **Wiro AI** in a **1:1 square format**. I then prepared it for frequent seeking with FFmpeg and connected its `currentTime` to the pointer's Y position in React.
Every prompt on this page is copyable and can be adapted to your portrait, avatar, or brand character.
## How to use the `[[...]]` fields
Double square brackets mark values you must replace. Do not leave `[[OUTFIT]]` in the final prompt; replace it with a concrete value such as `plain red polo shirt`.
| Variable | What it means | Value in this project |
| --- | --- | --- |
| `[[SUBJECT]]` | Person or character | young male software creator |
| `[[OUTFIT]]` | Clothing | plain red polo shirt |
| `[[BACKGROUND_COLOR]]` | Flat background | pure white, `#FFFFFF` |
| `[[EXPRESSION]]` | Fixed expression | calm, natural, neutral |
| `[[ASPECT_RATIO]]` | Generation ratio | `1:1` |
| `[[HEAD_DIRECTION]]` | Fixed horizontal angle | about 60 degrees left |
| `[[VIDEO_PATH]]` | Public video path | `/media/cursor-portrait/poyraz-bottom-right.mp4` |
| `[[POSTER_PATH]]` | Public poster path | `/media/cursor-portrait/poyraz-bottom-right-poster.webp` |
| `[[FRAMEWORK]]` | Application stack | Next.js, React, TypeScript |
| `[[STYLING_SYSTEM]]` | Styling stack | Tailwind CSS |
Search for every `[[...]]` field before submitting a prompt and make sure no unresolved variable remains.
## How the effect works
The reliable way to control a single video in real time is to treat it as a short **motion-control plate**, not as an autoplaying clip.
The four-second timeline in this project:
1. `0.000.25`: hold the down-left pose.
2. `0.253.75`: move from down-left to up-left.
3. Around `2.00`: reach the neutral horizontal-left pose.
4. `3.754.00`: hold the up-left pose.
Moving the pointer vertically scrubs this active range forward or backward. Pointer X is intentionally ignored because the generated video contains only one controlled motion axis.
> A single video is reliable only along the motion axis it contains. For true horizontal and vertical tracking, use a consistent 3×3 set of directional stills instead of inventing a second axis in code.
## Production workflow
1. Select a clear, front-facing identity reference.
2. Prepare a consistent 1:1 master frame with fixed clothing, light, and background.
3. Upload the master frame to Wiro AI and generate the motion with MiniMax H3.
4. Regenerate with the repair prompt if the face, camera, or background drifts.
5. Convert the result into a seek-friendly 720×720 H.264 web asset.
6. Map pointer Y to the video's active time range.
7. Test desktop, reduced-motion, dark-theme, and mobile behavior separately.
## 1. Master frame prompt
Upload a clear identity reference to your image-generation tool and replace every `[[...]]` field first.
```prompt
Use the uploaded image only as the identity reference for [[SUBJECT]].
Create a new photorealistic, production-ready 1:1 studio portrait for an
interactive website animation. Preserve the exact recognizable identity,
facial proportions, skin tone, hairstyle, hairline, eyebrows, eye shape,
nose, lips, jawline, age, and overall appearance.
Composition:
- Square [[ASPECT_RATIO]] frame.
- Medium close-up from [[CROP_POINT]] upward.
- Keep the full head, hair, ears, neck, shoulders, and visible upper torso
safely inside the frame.
- Keep comfortable negative space around the hair and shoulders.
- The shoulders remain stable and the head is turned approximately
[[HEAD_DIRECTION]].
- Expression: [[EXPRESSION]].
- Outfit: [[OUTFIT]].
Background and light:
- Perfectly flat, seamless [[BACKGROUND_COLOR]] background.
- No gradient, texture, horizon line, furniture, props, text, watermark,
logo, border, or visible cast shadow.
- Soft, bright studio lighting with natural skin texture.
- Keep hair, ears, face, shoulders, and clothing edges clean.
Continuity constraints:
- Do not beautify, age, de-age, stylize, or reinterpret the person.
- Do not change facial hair, outfit, accessories, body proportions, or light.
- Do not crop the hair, ears, shoulders, or upper torso.
- Generate one person and one clean master frame only.
```
Values used for this implementation:
```text
[[SUBJECT]] = a young male software creator
[[ASPECT_RATIO]] = 1:1
[[CROP_POINT]] = mid-torso
[[HEAD_DIRECTION]] = 60 degrees toward screen-left
[[EXPRESSION]] = calm, natural, neutral expression
[[OUTFIT]] = plain red polo shirt
[[BACKGROUND_COLOR]] = pure white (#FFFFFF)
```
## 2. Wiro AI / MiniMax H3 video prompt
Use the master frame as the image reference in Wiro AI with the MiniMax H3 model. The goal is a technical plate that works frame by frame, not a cinematic scene.
```prompt
Animate the uploaded 1:1 master frame into a precise four-second motion-control
plate for an interactive website portrait. Preserve the exact identity, face,
hairstyle, red polo shirt, body proportions, lighting, colors, square framing,
and pure white background from the reference image.
Output:
- Duration: exactly 4.0 seconds.
- Aspect ratio: 1:1.
- One continuous shot with a completely locked, eye-level camera.
- No zoom, crop change, pan, tilt, dolly, reframing, or camera shake.
- No speech and no audio-dependent movement.
Head direction:
- Keep the subject turned approximately 60 degrees toward screen-left for
the entire video.
- The horizontal head angle must not change.
- Never turn toward the camera and never rotate into a full side profile.
Exact motion timeline:
- 0.000.25 seconds: hold a clean down-left gaze and head-tilt pose.
- 0.253.75 seconds: move smoothly and continuously from down-left to up-left.
- At exactly 2.00 seconds: reach a neutral horizontal-left gaze.
- 3.754.00 seconds: hold the final up-left pose perfectly still.
Movement rules:
- Only the eyes and the minimum natural head/neck tilt required for the
vertical gaze may move.
- Shoulders, torso, arms, clothing, body position, head scale, and horizontal
head angle remain fixed.
- Keep the mouth closed and motionless.
- No talking, smiling, eyebrow movement, nodding, leaning, body sway,
breathing motion, or secondary gesture.
- Movement must be slow, linear, anatomically coherent, and usable when
scrubbed both forward and backward.
Continuity:
- Preserve the same recognizable face in every frame.
- Keep hair volume, hairline, ears, nose, jaw, skin texture, clothing folds,
and lighting stable.
- No face drift, morphing, warped anatomy, flicker, or changing expression.
- Keep the background perfectly uniform pure white (#FFFFFF) in every frame.
This is not a cinematic scene. It is a deterministic frame-scrubbing asset
for a website and every intermediate frame must work as a clean still image.
```
Inspect the middle frames as carefully as the endpoints. Face shape, ears, hairline, and clothing edges must remain stable throughout the MiniMax H3 output.
## 3. Repair prompt
Describe the failed generation precisely in `[[OBSERVED_PROBLEMS]]`.
```prompt
Regenerate this clip as a strict technical motion plate. The previous result
is unusable because: [[OBSERVED_PROBLEMS]].
Lock every property except the intended vertical gaze and head-tilt movement:
- preserve the exact identity and facial proportions in every frame;
- keep the horizontal head angle fixed at approximately 60 degrees left;
- fixed camera, crop, focal length, scale, head position, shoulders, torso,
arms, outfit, expression, lighting, and background;
- one slow linear movement from down-left to up-left;
- neutral horizontal-left pose at exactly two seconds;
- closed and motionless mouth;
- no speech, smile, blink during movement, eyebrow motion, body sway,
zoom, parallax, lighting shift, background flicker, face morphing,
hair change, ear deformation, or new objects;
- perfectly uniform pure white (#FFFFFF) background.
This clip will be paused and scrubbed frame by frame. Every intermediate frame
must remain anatomically coherent and visually consistent with the reference.
```
Example problem description:
```text
[[OBSERVED_PROBLEMS]] = the face changes near the final pose, the shoulders
move with the head, and the white background flickers between frames
```
## 4. Preparing the video for the web
AI video can play directly in a browser, but codec and keyframe interval matter when `currentTime` changes frequently. I prepared a 720×720, 30 FPS, silent H.264 file with every frame encoded as a keyframe.
```bash
ffmpeg -i INPUT.mp4 \
-vf "scale=720:720:force_original_aspect_ratio=decrease,pad=720:720:(ow-iw)/2:(oh-ih)/2:color=white,fps=30" \
-an -c:v libx264 -preset slow -crf 20 -pix_fmt yuv420p \
-g 1 -keyint_min 1 -sc_threshold 0 -movflags +faststart \
public/media/cursor-portrait/poyraz-bottom-right.mp4
```
Key decisions:
- `-an` removes audio completely.
- `yuv420p` improves Safari and Chromium compatibility.
- `faststart` moves MP4 metadata to the beginning.
- `-g 1` makes every frame independently seekable.
- `scale + pad` preserves proportions on a square white surface.
### Media optimization agent prompt
```prompt
Prepare [[INPUT_VIDEO_PATH]] as a web motion-control plate that will be scrubbed
forward and backward from pointer movement. Never overwrite the source file.
Outputs:
- Video: [[VIDEO_PATH]]
- Poster: [[POSTER_PATH]]
- Exact duration: 4.00 seconds
- Starting hold: 0.000.25
- Active motion: 0.253.75
- Final hold: 3.754.00
- Resolution: 720×720
- Frame rate: 30 FPS
- Codec: H.264 MP4, libx264, yuv420p
- Settings: preset slow, CRF 20, faststart, no audio
- Every frame, or at most every second frame, must be a keyframe
Do not distort the aspect ratio. Use #FFFFFF padding when needed. Do not crop
hair, face, ears, shoulders, or clothing. Verify duration, resolution, FPS,
codec, and file size. Visually inspect the first, middle, and final frames.
Do not modify unrelated project files.
```
## 5. Mapping pointer Y to video time
Pointer Y is `0` at the viewport top and `window.innerHeight` at the bottom. Invert and clamp it to `01`, then map it to the active video range.
```ts
const TOTAL_DURATION = 4;
const ACTIVE_START = 0.25;
const ACTIVE_END = 3.75;
const DEFAULT_TIME = 2;
const SMOOTHING = 0.12;
function clamp(value: number, minimum: number, maximum: number) {
return Math.min(Math.max(value, minimum), maximum);
}
function mapPointerYToTime(pointerY: number, viewportHeight: number) {
if (viewportHeight <= 0) return DEFAULT_TIME;
const progress = clamp(1 - pointerY / viewportHeight, 0, 1);
return ACTIVE_START + progress * (ACTIVE_END - ACTIVE_START);
}
```
| Pointer position | Progress | Video time | Gaze |
| --- | ---: | ---: | --- |
| Viewport bottom | `0` | `0.25` | down-left |
| Viewport middle | `0.5` | `2.00` | horizontal-left |
| Viewport top | `1` | `3.75` | up-left |
`pointerX` never enters this formula, so horizontal pointer movement does not change the frame.
## 6. Smooth scrubbing in React
Keep high-frequency values in refs instead of updating React state for every pointer event. A single `requestAnimationFrame` loop damps the current value toward the target.
```ts
const pointerYRef = useRef<number | null>(null);
const targetTimeRef = useRef(DEFAULT_TIME);
const currentTimeRef = useRef(DEFAULT_TIME);
const rafIdRef = useRef<number | null>(null);
function animate() {
const difference = targetTimeRef.current - currentTimeRef.current;
currentTimeRef.current += difference * SMOOTHING;
const video = videoRef.current;
if (video && Math.abs(video.currentTime - currentTimeRef.current) > 1 / 120) {
video.currentTime = currentTimeRef.current;
}
if (Math.abs(difference) > 0.002) {
rafIdRef.current = requestAnimationFrame(animate);
} else {
rafIdRef.current = null;
}
}
function handlePointerMove(event: PointerEvent) {
if (event.pointerType !== "mouse") return;
pointerYRef.current = event.clientY;
targetTimeRef.current = mapPointerYToTime(
event.clientY,
window.innerHeight,
);
if (rafIdRef.current === null) {
rafIdRef.current = requestAnimationFrame(animate);
}
}
```
Production code must also handle metadata readiness, optional video priming, tab visibility, and complete listener cleanup.
## 7. Ready-to-use coding-agent prompt
Replace the bracketed values and use this with a coding agent in an existing frontend project.
```prompt
Add a reusable `PointerPortraitFollower` component to the existing [[FRAMEWORK]]
project. It must stay at the bottom-right of the viewport and react only to the
pointer's Y position. Styling system: [[STYLING_SYSTEM]].
Assets:
- Video: [[VIDEO_PATH]]
- Poster: [[POSTER_PATH]]
System constants:
- TOTAL_DURATION = 4
- ACTIVE_START = 0.25
- ACTIVE_END = 3.75
- DEFAULT_TIME = 2
- SMOOTHING = 0.12
Behavior:
- Keep the video paused; never autoplay it normally.
- Use pointerY only. pointerX must never affect video timing.
- progress = clamp(1 - pointerY / window.innerHeight, 0, 1)
- targetTime = ACTIVE_START + progress * (ACTIVE_END - ACTIVE_START)
- Use a global pointermove listener without React state updates per event.
- Store pointerY, targetTime, and currentTime in refs.
- Apply damping in one requestAnimationFrame loop.
- Limit seeks to about 3060 Hz and skip tiny time differences.
- Return smoothly to the neutral 2.00-second pose when the pointer leaves the
window or the window loses focus.
Video element:
- muted, playsInline, preload="auto", no controls, no autoplay
- seek to 2.00 after loadedmetadata
- prime muted playback briefly on the first real pointer move only if required
- show the poster instead of a broken media icon after an asset error
Placement:
- position: fixed; right: [[RIGHT_OFFSET]]; bottom: [[BOTTOM_OFFSET]]
- width: [[DESKTOP_WIDTH]]; aspect-ratio: 1 / 1; z-index: [[Z_INDEX]]
- object-fit: contain; background: [[BACKGROUND_COLOR]]
- pointer-events: none; user-select: none; aria-hidden: true
- no border, radius, shadow, or horizontal mirroring
Responsive and lifecycle:
- disable animation on pointer: coarse and narrow viewports
- never interpret touch as mouse tracking
- honor prefers-reduced-motion
- do not block CTA, link, or menu interaction
- never access window/document during SSR
- stop RAF and seeking while the tab is hidden
- clean pointermove, pointerleave, blur, resize, visibilitychange, and RAF on
unmount; never start multiple RAF loops
Separate mapping and clamp into pure typed helpers. Add boundary tests when a
test setup exists. Do not add a heavy animation dependency. Run build,
typecheck, lint, and existing tests after implementation.
```
Values from this implementation:
```text
[[FRAMEWORK]] = Next.js App Router, React, TypeScript
[[STYLING_SYSTEM]] = Tailwind CSS and Poyraz UI
[[VIDEO_PATH]] = /media/cursor-portrait/poyraz-bottom-right.mp4
[[POSTER_PATH]] = /media/cursor-portrait/poyraz-bottom-right-poster.webp
[[RIGHT_OFFSET]] = 24px
[[BOTTOM_OFFSET]] = 0px
[[DESKTOP_WIDTH]] = clamp(110px, 11vw, 170px)
[[Z_INDEX]] = 40
[[BACKGROUND_COLOR]] = #FFFFFF
```
## 8. Mobile, accessibility, and fallback
This effect is meaningful on desktop with a mouse. Treating touch movement as pointer tracking hurts usability and wastes decoding work.
My choices:
- Do not render the component on `pointer: coarse` devices.
- Hide it completely below `840px`.
- Honor `prefers-reduced-motion`.
- Hide the white-background asset in dark mode.
- Keep it decorative with `pointer-events: none` and `aria-hidden="true"`.
- Show the poster if video loading fails.
## 9. Quality checklist
### Video
- Is it the same person in the first, middle, and final frames?
- Does the head stay turned about 60 degrees left?
- Are the poses down-left, horizontal-left, and up-left in the correct order?
- Do hair, ears, jaw, and facial features remain stable?
- Do shoulders and clothing stay still?
- Does the camera, light, or white background flicker?
- Does the motion remain natural when scrubbed backward?
### Web
- Does the video remain paused before pointer input?
- Does it scrub in the correct direction on vertical movement?
- Does horizontal-only movement leave the frame unchanged?
- Is there a seek queue or visible lag during fast movement?
- Does the portrait return to neutral after leaving the window?
- Are links and CTA controls still clickable?
- Is animation disabled on mobile and reduced-motion?
- Does the poster appear after a video error?
- Are listeners and RAF cleaned up after navigation?
## Adapt it to your project
Five steps are enough to reuse the system:
1. Replace every `[[...]]` variable for your character.
2. Produce a consistent 1:1 master frame on a flat background.
3. Describe only one intended motion axis in the MiniMax H3 prompt.
4. Map that same axis to the active video range.
5. Connect the optimized video and poster to the component.
The main rule is simple: do not invent motion in code that does not exist in the generated video. Treating the AI output as a controlled motion plate makes the effect more natural, deterministic, and testable.
## 10. Master-frame prompt for avatars
When the source is a 2D, 3D, or stylized avatar, prevent the model from reinterpreting its design language. This prompt locks the original medium and character identity.
```prompt
Use the uploaded avatar as the strict character-design reference. Recreate the
exact same character in a clean, production-ready master frame for a
mouse-following website animation.
Preserve exactly:
- the character's face design, head shape, hairstyle, colors, outfit,
accessories, proportions, material style, line style, shading language,
and overall visual identity;
- the original medium and aesthetic: [[AVATAR_STYLE]];
- all intentional asymmetries and recognizable features.
Do not turn a 2D avatar into 3D, do not turn a stylized avatar into a real
person, and do not redesign or improve the character.
Composition:
- [[ASPECT_RATIO]] frame.
- Medium close-up from [[CROP_POINT]] upward.
- Center the character, leaving enough space for the head to rotate up to
[[MAX_HEAD_ROTATION]] degrees toward [[TURN_DIRECTION]].
- Shoulders remain stable and mostly facing forward.
- Neutral starting pose, only [[STARTING_HEAD_ANGLE]] degrees toward
[[STARTING_DIRECTION]].
- Expression: [[EXPRESSION]].
Background:
- Completely flat, seamless [[BACKGROUND_COLOR]].
- No texture, gradient, cast shadow, props, text, logo, border, scenery, or UI.
Continuity requirements:
- Clean silhouette and stable edges.
- Consistent eyes and facial features according to the reference design.
- No additional accessories or design changes.
- Produce one character and one clean master frame only.
```
## 11. Motion prompts for every placement
The live example in this guide maps vertical pointer movement to a vertical gaze. The alternatives below produce frontal-to-profile clips driven by horizontal pointer movement. Reverse the turn for left-side placements so that the subject looks into the page instead of away from it.
### Bottom-right: portrait turning left
```prompt
Animate the uploaded master frame into a precise motion-control plate for an
interactive website portrait. Preserve the exact identity, face, hairstyle,
outfit, body proportions, lighting, colors, framing, and background.
Output:
- Duration: exactly 4.0 seconds.
- Aspect ratio: [[ASPECT_RATIO]].
- One continuous shot with a locked, eye-level camera.
- No zoom, crop change, pan, tilt, dolly, camera shake, speech, or audio motion.
The person will appear in the bottom-right corner. Website content and the
pointer will usually be to the person's left, so the motion must progress from
an almost frontal pose to a clear screen-left profile.
Timeline:
- 0.00-0.25: hold the reference pose, only [[STARTING_HEAD_ANGLE]] degrees left.
- 0.25-3.75: rotate smoothly and linearly toward screen-left.
- Reach approximately [[MAX_HEAD_ROTATION]] degrees in a clean left profile.
- 3.75-4.00: hold the final pose perfectly still.
The eyes lead slightly. Only eyes, head, and neck move. Shoulders, torso, arms,
clothing, scale, body position, and expression remain fixed. Keep the mouth
closed. No talking, smiling, eyebrow movement, nodding, leaning, breathing
motion, blinking during the turn, or secondary gestures.
Preserve identity and anatomy in every frame. No morphing, face drift, hair or
ear deformation, lighting change, clothing change, or background flicker.
Keep a perfectly flat [[BACKGROUND_COLOR]] background with no gradient, shadow,
texture, object, text, or logo.
This is a deterministic website animation plate intended to be paused and
scrubbed frame by frame, not a cinematic video.
```
Start with `[[MAX_HEAD_ROTATION]] = 85-90` and `[[STARTING_HEAD_ANGLE]] = 5-10` for a corner portrait.
### Bottom-left: portrait turning right
```prompt
Animate the uploaded master frame into a precise motion-control plate for an
interactive website portrait. Preserve the exact identity, face, hairstyle,
outfit, proportions, lighting, framing, and background.
The person will appear in the bottom-left corner, while most content and pointer
movement will be to the person's right.
Create exactly 4.0 seconds of one continuous, locked-off motion:
- Start almost facing the camera, only [[STARTING_HEAD_ANGLE]] degrees right.
- Hold the starting pose from 0.00 to 0.25 seconds.
- From 0.25 to 3.75 seconds, rotate smoothly and linearly toward screen-right.
- End at approximately [[MAX_HEAD_ROTATION]] degrees in a clean right profile.
- Hold that final pose from 3.75 to 4.00 seconds.
Only the eyes, head, and neck move. The eyes lead slightly and stay focused
toward screen-right. Shoulders, torso, arms, clothing, head scale, and body
position remain fixed. The camera is completely locked.
Keep the mouth closed and [[EXPRESSION]] unchanged. No speech, smile, lip or
eyebrow motion, nodding, leaning, blinking during the turn, breathing motion,
or gestures. No identity drift, morphing, hair change, warped profile, ear
deformation, lighting change, clothing change, or background flicker.
The background must remain perfectly flat [[BACKGROUND_COLOR]], without
shadows, gradients, props, text, logos, textures, or color variation. This is a
frame-scrubbable website plate, not a cinematic video.
```
### Hero-right: looking at the headline and CTA on the left
```prompt
Animate the uploaded identity-locked master frame for a website hero section.
The subject will be positioned on the right side; headline, copy, CTA buttons,
and pointer will be primarily on the left.
Create an exact 4.0-second locked-off motion-control clip. Start almost facing
the viewer at [[STARTING_HEAD_ANGLE]] degrees left and hold from 0.00 to 0.25.
From 0.25 to 3.75, smoothly rotate the eyes and head toward screen-left, ending
at [[MAX_HEAD_ROTATION]] degrees. Hold the final pose from 3.75 to 4.00.
The final pose must feel like the subject is looking at the hero headline and
CTA, not outside the page. Eyes lead slightly; the head follows in one slow,
continuous, linear movement.
Only eyes, head, and neck move. Keep shoulders, torso, arms, clothing, position,
scale, expression, and silhouette fixed. Mouth closed. No talking, smiling,
blinking during the turn, nodding, leaning, gestures, body sway, or breathing.
Preserve the exact person or avatar design. No face drift, morphing, hair or
outfit changes, lighting shifts, framing changes, or warped profile. Use a fixed
eye-level camera and a perfectly flat [[BACKGROUND_COLOR]] background.
Aspect ratio: [[ASPECT_RATIO]]. The result must be frame-scrubbable.
```
For hero layouts, `[[MAX_HEAD_ROTATION]] = 65-75` usually looks more natural.
### Hero-left: looking at the headline and CTA on the right
```prompt
Animate the uploaded identity-locked master frame for a website hero section.
The subject will be positioned on the left side; headline, copy, CTA buttons,
and pointer will be primarily on the right.
Create an exact 4.0-second locked-off motion-control clip. Start almost facing
the viewer at [[STARTING_HEAD_ANGLE]] degrees right and hold from 0.00 to 0.25.
From 0.25 to 3.75, smoothly rotate the eyes and head toward screen-right, ending
at [[MAX_HEAD_ROTATION]] degrees. Hold the final pose from 3.75 to 4.00.
The final pose must feel like the subject is looking at the hero headline and
CTA, not outside the page. Eyes lead slightly; the head follows in one slow,
continuous, linear movement.
Only eyes, head, and neck move. Keep shoulders, torso, arms, clothing, position,
scale, expression, and silhouette fixed. Mouth closed. No talking, smiling,
blinking during the turn, nodding, leaning, gestures, body sway, or breathing.
Preserve the exact person or avatar design. No identity drift, morphing, hair or
outfit changes, lighting shifts, framing changes, or warped profile. Use a fixed
eye-level camera and a perfectly flat [[BACKGROUND_COLOR]] background.
Aspect ratio: [[ASPECT_RATIO]]. The result must be frame-scrubbable.
```
### Hero-center: complete left-to-right scan
```prompt
Animate the uploaded identity-locked master frame into a symmetrical
left-to-right head-turn calibration clip for an interactive centered hero.
Output one continuous 4.0-second shot in [[ASPECT_RATIO]] with a locked,
eye-level camera. Preserve identity or avatar design, outfit, expression,
lighting, framing, scale, and [[BACKGROUND_COLOR]] background.
Timeline:
- 0.00-0.25: hold approximately [[LEFT_ANGLE]] degrees toward screen-left.
- 0.25-3.75: perform the complete symmetrical left-to-right rotation.
- Reach the exact front-facing pose at 50% of the active motion interval.
- Continue at the same speed to [[RIGHT_ANGLE]] degrees toward screen-right.
- 3.75-4.00: hold the final right-facing pose.
- Keep path, speed, scale, and head height symmetrical on both sides.
The eyes lead only slightly. Only eyes, head, and neck move. Shoulders, torso,
arms, clothing, body position, scale, and expression remain fixed. Mouth closed.
No speech, smile, blink during movement, eyebrow motion, nod, lean, gesture,
body sway, or breathing motion.
No identity drift, morphing, hairstyle change, ear deformation, warped profile,
lighting shift, background flicker, camera movement, zoom, crop, or reframing.
The background remains perfectly uniform [[BACKGROUND_COLOR]]. This must stay
clean when paused and scrubbed in either direction.
```
Use `[[LEFT_ANGLE]] = 75` and `[[RIGHT_ANGLE]] = 75` as a symmetric starting point.
## 12. Generic repair prompt
If the camera, shoulders, or mouth move, or if the profile loses identity, describe the defect precisely in `[[OBSERVED_PROBLEMS]]`.
```prompt
Regenerate this clip as a strict technical motion plate. The previous result is
unusable because it contains: [[OBSERVED_PROBLEMS]].
Lock every property except the intended head rotation:
- exact same identity and facial proportions in every frame;
- fixed camera, crop, focal length, scale, head position, shoulders, torso,
arms, outfit, expression, lighting, and background;
- only the eyes, head, and neck may move;
- one slow, linear rotation from [[STARTING_DIRECTION_AND_ANGLE]] to
[[ENDING_DIRECTION_AND_ANGLE]];
- closed and motionless mouth;
- no speech, smile, blink during the turn, eyebrow movement, nod, lean, body
sway, breathing, camera motion, zoom, parallax, lighting shift, background
flicker, face morphing, hair change, ear deformation, or new objects;
- perfectly uniform [[BACKGROUND_COLOR]] background;
- preserve the reference identity exactly, especially in the final profile.
This is a frame-scrubbing website asset, so every intermediate frame must be
anatomically coherent and usable as a still image.
```
## 13. Coding-agent prompt for horizontal variants
This prompt builds one reusable component for bottom-right, bottom-left, and hero placements. It uses `pointerX`, so treat it as an alternative to the vertical `pointerY` implementation earlier in this guide.
```prompt
Add a reusable mouse-following video portrait component to the existing
[[FRAMEWORK]] project. Styling system: [[STYLING_SYSTEM]]. Inspect the project's
structure, responsive rules, dependencies, and code conventions first.
Assets:
- Video: [[VIDEO_PATH]]
- Poster: [[POSTER_PATH]]
- Active motion: 0.25-3.75 seconds
- Video motion: [[VIDEO_MOTION_DESCRIPTION]]
- Placement: [[PLACEMENT]]
Behavior:
- Keep the video muted, playsInline, preload auto, paused, and without autoplay.
- Listen to global pointermove and measure the portrait anchor when needed.
- Map pointer position to 0-1 targetProgress, then to currentTime 0.25-3.75.
- Use RAF with lerp/damping. Do not update React state per pointer event.
- Limit seeks to 30-60 Hz and skip negligible time differences.
Direction mapping:
- For bottom-right or hero-right clips turning left: progress 0 near the
portrait and progress 1 as the pointer moves farther left.
- For bottom-left or hero-left clips turning right: progress 0 near the
portrait and progress 1 as the pointer moves farther right.
- For a hero-center clip scanning left-to-right, use pointerX / viewportWidth.
- Clamp progress to 0-1 and prevent anatomically invalid reverse turns.
Typed API:
- src, poster
- placement: bottom-right | bottom-left | hero-right | hero-left | hero-center
- defaultProgress, smoothing, desktopWidth, mobileWidth
- offsetX, offsetY, zIndex, className, decorative, invertProgress
Layout and lifecycle:
- Use fixed positioning for bottom-* and absolute positioning inside the hero
for hero-* variants.
- Use object-fit contain, a reserved aspect-ratio, and [[BACKGROUND_COLOR]].
- If decorative, use pointer-events none, user-select none, draggable false,
and aria-hidden true. Do not cover CTA controls or copy.
- Run client-side. Seek to [[DEFAULT_PROGRESS]] after loadedmetadata.
- If decoding needs it, prime muted playback on first real interaction and
immediately pause.
- Do not read layout every frame. Re-measure on resize/scroll at low cost.
- Stop RAF/seeking in hidden tabs and clean every listener and RAF on unmount.
- Disable tracking for coarse pointers and reduced motion; use
[[MOBILE_BEHAVIOR]]. Show the poster after video errors.
Use named constants TOTAL_DURATION=4, ACTIVE_START=0.25, ACTIVE_END=3.75.
Do not add a heavy animation library. Add complete TypeScript types and focused
mapping/clamp tests without refactoring unrelated files.
Report changed files, direction formula, build/typecheck/lint/test results, and
a four-item manual test checklist.
```
### Add only one new placement
```prompt
Do not break the behavior or public API of `CursorFollowerPortrait`. Add only a
new [[NEW_PLACEMENT]] variant.
Asset:
- Video: [[NEW_VIDEO_PATH]]
- Poster: [[NEW_POSTER_PATH]]
- Active motion: 0.25-3.75 seconds
- Motion: [[NEW_VIDEO_MOTION_DESCRIPTION]]
Placement and mapping:
- Placement: [[NEW_PLACEMENT]]
- Offset: [[HORIZONTAL_OFFSET]] horizontal, [[VERTICAL_OFFSET]] vertical
- Width: [[DESKTOP_WIDTH]] / mobile [[MOBILE_WIDTH]]
- Anatomical direction rule: [[DIRECTION_MAPPING_RULE]]
Do not change existing variants. Add a working usage example, run build,
typecheck, and lint, then report only changed files and verification results.
```
### Debugging prompt
```prompt
`CursorFollowerPortrait` has this problem: [[BUG_DESCRIPTION]].
Reproduce it first and identify the root cause with evidence. Check:
- assigning currentTime before metadata loads;
- slow seeking caused by codec or keyframe distance;
- incorrect progress direction or invertProgress;
- React renders on every pointer event;
- duplicate RAF loops or event listeners;
- getBoundingClientRect layout thrashing on every frame;
- Safari/iOS video priming behavior;
- asset path, CORS, preload, and poster fallback;
- incorrect reduced-motion or coarse-pointer detection;
- fixed/absolute containers and stacking contexts.
Do not refactor randomly before explaining the root cause. Apply the smallest
safe fix, preserve the public API, and report build/typecheck/lint/test results.
```
## 14. True two-axis tracking with a 3x3 grid
A single video can reliably follow only the axis it contains. For horizontal and vertical gaze, generate nine aligned poses from one master frame. For a real person, start with yaw values of `-35° / 0° / +35°` and pitch values of `-18° / 0° / +18°`.
| Pose | Yaw | Pitch |
| --- | ---: | ---: |
| Top-left | `[[YAW_LEFT]]` | `[[PITCH_UP]]` |
| Top-center | `0` | `[[PITCH_UP]]` |
| Top-right | `[[YAW_RIGHT]]` | `[[PITCH_UP]]` |
| Middle-left | `[[YAW_LEFT]]` | `0` |
| Center | `0` | `0` |
| Middle-right | `[[YAW_RIGHT]]` | `0` |
| Bottom-left | `[[YAW_LEFT]]` | `[[PITCH_DOWN]]` |
| Bottom-center | `0` | `[[PITCH_DOWN]]` |
| Bottom-right | `[[YAW_RIGHT]]` | `[[PITCH_DOWN]]` |
### Nine-direction image prompt
```prompt
Using the uploaded identity-locked master frame, create one exact directional
calibration pose for a 3x3 mouse-tracking portrait system.
Preserve the exact same identity, face, hairstyle, outfit, body, framing,
camera, focal length, lighting, scale, background, crop, and expression. This
image must align pixel-for-pixel as closely as possible with all other poses.
Change only:
- head yaw: [[TARGET_YAW]] degrees;
- head pitch: [[TARGET_PITCH]] degrees;
- eye gaze: [[GAZE_DIRECTION]], aligned naturally with the head direction.
Keep shoulders, torso, arms, clothing, body position, head center, and head
scale fixed. Mouth closed. No smile, speech, blink, eyebrow movement, body
turn, lean, camera motion, crop change, zoom, or lighting change.
Maintain anatomically correct neck, ears, eyes, jaw, and facial profile. No
morphing or identity drift. Use a perfectly flat [[BACKGROUND_COLOR]] identical
to the master frame. Output one image only at [[OUTPUT_DIMENSIONS]].
```
### Coding-agent prompt for the nine-direction system
```prompt
Add a `DirectionalPortraitGrid` component with true two-axis pointer tracking
to the existing [[FRAMEWORK]] project.
Nine equal-size, pixel-aligned assets:
- top-left: [[TOP_LEFT_PATH]]
- top-center: [[TOP_CENTER_PATH]]
- top-right: [[TOP_RIGHT_PATH]]
- middle-left: [[MIDDLE_LEFT_PATH]]
- center: [[CENTER_PATH]]
- middle-right: [[MIDDLE_RIGHT_PATH]]
- bottom-left: [[BOTTOM_LEFT_PATH]]
- bottom-center: [[BOTTOM_CENTER_PATH]]
- bottom-right: [[BOTTOM_RIGHT_PATH]]
Normalize the pointer relative to the portrait center and clamp both axes to
-1..1. Do not hard-switch to the nearest image. Find the surrounding four grid
cells, calculate bilinear interpolation weights, and blend those four aligned
images with opacity.
Use RAF and damping without React renders per pointer event. Preload assets
without blocking the page's LCP. Show the center image for reduced motion,
coarse pointers, or loading failures. If decorative, use pointer-events none
and aria-hidden true.
Typed props: sources, placement, desktopWidth, mobileWidth, smoothing,
maxTrackingDistance, offsetX, offsetY, className, decorative. Extract grid math
into pure functions and test corners, center, and intermediate values. Do not
add a heavy animation library. Add a usage example and report
build/typecheck/lint/test results.
```
## 15. Reels planning prompt
For a 55-second walkthrough, use this sequence: `0-3s` result, `3-7s` hook, `7-16s` master frame, `16-26s` motion video, `26-42s` coding, `42-51s` before/after and variant, `51-58s` CTA.
```prompt
I am a [[CREATOR_PROFILE]] software content creator. Plan an Instagram Reel for
this interactive website effect:
Effect: [[EFFECT_DESCRIPTION]]
Character: [[REAL_PERSON_OR_AVATAR]]
Placement: [[PLACEMENT]]
Generation tool: [[GENERATION_TOOL]]
Coding agent: [[CODING_AGENT]]
Audience: [[TARGET_AUDIENCE]]
Target duration: 55 seconds
Tone: [[TONE]]
Use this structure:
1. Show the result in the first two seconds as a visual hook.
2. A spoken hook of no more than ten words.
3. A concrete promise for what the viewer will build.
4. The reference-to-master-frame step.
5. Only the critical lines of the placement-specific motion prompt.
6. Only the critical technical logic from the coding prompt.
7. One beginner-friendly sentence explaining pointer-to-currentTime mapping.
8. A before/after scene.
9. An open loop into the next placement variant.
10. A short, natural CTA.
For every segment, provide its time range, spoken line, screen recording,
large on-screen text, and editing transition. Avoid exaggerated marketing,
unnecessary jargon, long intros, claims that AI did everything, or unrealistic
time promises. Highlight prompt fragments instead of displaying entire prompts.
Keep the total spoken script under 120 words.
```
This library lets you produce bottom-right, bottom-left, hero-right, hero-left, hero-center, and true 3x3 tracking assets from the same master design. Name each asset together with its motion direction, placement formula, and fallback to prevent direction mistakes as the system grows.
@@ -0,0 +1,884 @@
---
title: "Fareyi Takip Eden AI Avatar Nasıl Yapılır?"
slug: "poyraz-cursor-portrait"
excerpt: "Wiro AI üzerinde MiniMax H3 ile ürettiğim 1:1 avatar videosunu, promptlardan FFmpeg optimizasyonuna ve React entegrasyonuna kadar adım adım oluşturun."
coverImage: "/animation-sources/poyraz-cursor-portrait/avatar-mouse-follow.gif"
platform: "Web"
tools:
- "Wiro AI"
- "MiniMax H3"
- "FFmpeg"
- "React"
date: "2026-08-29"
author: "Poyraz Avsever"
lang: "tr"
---
Bu efektte sağ alttaki portre normal bir video gibi oynatılmıyor. Video duraklatılmış halde tutuluyor; farenin ekrandaki dikey konumu videonun zaman çizelgesine bağlanıyor. Fare aşağıdayken portre aşağı-sola, ortadayken yatay-sola, yukarıdayken yukarı-sola bakıyor.
Videoyu **Wiro AI** üzerinden **MiniMax H3** modeliyle, **1:1 kare formatta** ürettim. Sonrasında videoyu FFmpeg ile sık ileri-geri sarılmaya uygun hale getirip React içinde `currentTime` üzerinden kontrol ettim.
Bu sayfadaki promptları doğrudan kopyalayabilir ve kendi portreniz, avatarınız veya marka karakteriniz için uyarlayabilirsiniz.
## Promptlardaki `[[...]]` alanları nasıl kullanılır?
Promptlarda gördüğünüz çift köşeli parantezler doldurulması gereken değişken alanlardır. Örneğin `[[OUTFIT]]` ifadesini promptta bırakmak yerine `plain red polo shirt` gibi kendi değerinizi yazmalısınız.
| Değişken | Ne yazılmalı? | Bu projedeki değer |
| --- | --- | --- |
| `[[SUBJECT]]` | Kişi veya karakter tanımı | 20 yaşında erkek içerik üreticisi |
| `[[OUTFIT]]` | Kıyafet | düz kırmızı polo tişört |
| `[[BACKGROUND_COLOR]]` | Düz arka plan | saf beyaz, `#FFFFFF` |
| `[[EXPRESSION]]` | Sabit yüz ifadesi | doğal ve nötr |
| `[[ASPECT_RATIO]]` | Üretim oranı | `1:1` |
| `[[HEAD_DIRECTION]]` | Başın sabit yatay yönü | yaklaşık 60 derece sola |
| `[[VIDEO_PATH]]` | Web video yolu | `/media/cursor-portrait/poyraz-bottom-right.mp4` |
| `[[POSTER_PATH]]` | Poster yolu | `/media/cursor-portrait/poyraz-bottom-right-poster.webp` |
| `[[FRAMEWORK]]` | Kullanılan teknoloji | Next.js, React, TypeScript |
| `[[STYLING_SYSTEM]]` | Stil sistemi | Tailwind CSS |
Bir promptu kullanmadan önce içindeki tüm `[[...]]` alanlarını aratın. Projeniz için karşılığı olmayan bir değişken kalmamalı.
## Sistem nasıl çalışıyor?
Tek bir videoyu gerçek zamanlı kontrol etmenin en stabil yolu videoyu sürekli oynatmak değil, onu kısa bir **hareket plakası** olarak kullanmaktır.
Bu uygulamadaki dört saniyelik zaman çizelgesi:
1. `0.000.25`: aşağı-sola bakış pozu sabit tutulur.
2. `0.253.75`: bakış aşağıdan yukarıya doğru ilerler.
3. Yaklaşık `2.00`: sola doğru nötr ve yatay bakış oluşur.
4. `3.754.00`: yukarı-sola bakış pozu sabit tutulur.
Fare aşağı-yukarı hareket ettikçe video bu aktif aralıkta ileri veya geri sarılır. Yatay fare konumu bu sürümde kullanılmaz; böylece yapay zeka videosunda bulunmayan ikinci bir hareket ekseni uydurulmaz.
> Tek video yalnızca üretilmiş hareket ekseninde güvenilir sonuç verir. Gerçek yatay ve dikey takip gerekiyorsa farklı yönlere ait tutarlı karelerden oluşan 3×3 bir sistem daha doğru yaklaşımdır.
## Üretim akışı
1. Net, önden çekilmiş bir referans fotoğraf seçin.
2. Kimliği, kıyafeti, ışığı ve beyaz arka planı sabitleyen 1:1 master kareyi hazırlayın.
3. Master kareyi Wiro AI'a yükleyip MiniMax H3 ile hareket videosunu üretin.
4. Kimlik kayması, kamera hareketi veya arka plan titreşimi varsa onarım promptuyla yeniden üretin.
5. Videoyu 720×720 H.264 web asset'ine dönüştürün.
6. Videonun `currentTime` değerini farenin Y konumuna bağlayın.
7. Masaüstü, reduced-motion, koyu tema ve mobil davranışlarını ayrı ayrı test edin.
## 1. Master kare promptu
Referans fotoğrafı kullandığınız görsel üretim aracına yükleyin. Aşağıdaki promptta önce tüm `[[...]]` alanlarını değiştirin.
```prompt
Use the uploaded image only as the identity reference for [[SUBJECT]].
Create a new photorealistic, production-ready 1:1 studio portrait for an
interactive website animation. Preserve the exact recognizable identity,
facial proportions, skin tone, hairstyle, hairline, eyebrows, eye shape,
nose, lips, jawline, age, and overall appearance.
Composition:
- Square [[ASPECT_RATIO]] frame.
- Medium close-up from [[CROP_POINT]] upward.
- Keep the full head, hair, ears, neck, shoulders, and visible upper torso
safely inside the frame.
- Keep comfortable negative space around the hair and shoulders.
- The shoulders remain stable and the head is turned approximately
[[HEAD_DIRECTION]].
- Expression: [[EXPRESSION]].
- Outfit: [[OUTFIT]].
Background and light:
- Perfectly flat, seamless [[BACKGROUND_COLOR]] background.
- No gradient, texture, horizon line, furniture, props, text, watermark,
logo, border, or visible cast shadow.
- Soft, bright studio lighting with natural skin texture.
- Keep hair, ears, face, shoulders, and clothing edges clean.
Continuity constraints:
- Do not beautify, age, de-age, stylize, or reinterpret the person.
- Do not change facial hair, outfit, accessories, body proportions, or light.
- Do not crop the hair, ears, shoulders, or upper torso.
- Generate one person and one clean master frame only.
```
Bu uygulama için kullandığım değerler:
```text
[[SUBJECT]] = a young male software creator
[[ASPECT_RATIO]] = 1:1
[[CROP_POINT]] = mid-torso
[[HEAD_DIRECTION]] = 60 degrees toward screen-left
[[EXPRESSION]] = calm, natural, neutral expression
[[OUTFIT]] = plain red polo shirt
[[BACKGROUND_COLOR]] = pure white (#FFFFFF)
```
## 2. Wiro AI / MiniMax H3 video promptu
Master kareyi Wiro AI üzerinde MiniMax H3 modeline referans olarak verin. Bu promptun amacı sinematik bir sahne değil, kare kare durdurulup sarılabilecek teknik bir hareket üretmektir.
```prompt
Animate the uploaded 1:1 master frame into a precise four-second motion-control
plate for an interactive website portrait. Preserve the exact identity, face,
hairstyle, red polo shirt, body proportions, lighting, colors, square framing,
and pure white background from the reference image.
Output:
- Duration: exactly 4.0 seconds.
- Aspect ratio: 1:1.
- One continuous shot with a completely locked, eye-level camera.
- No zoom, crop change, pan, tilt, dolly, reframing, or camera shake.
- No speech and no audio-dependent movement.
Head direction:
- Keep the subject turned approximately 60 degrees toward screen-left for
the entire video.
- The horizontal head angle must not change.
- Never turn toward the camera and never rotate into a full side profile.
Exact motion timeline:
- 0.000.25 seconds: hold a clean down-left gaze and head-tilt pose.
- 0.253.75 seconds: move smoothly and continuously from down-left to up-left.
- At exactly 2.00 seconds: reach a neutral horizontal-left gaze.
- 3.754.00 seconds: hold the final up-left pose perfectly still.
Movement rules:
- Only the eyes and the minimum natural head/neck tilt required for the
vertical gaze may move.
- Shoulders, torso, arms, clothing, body position, head scale, and horizontal
head angle remain fixed.
- Keep the mouth closed and motionless.
- No talking, smiling, eyebrow movement, nodding, leaning, body sway,
breathing motion, or secondary gesture.
- Movement must be slow, linear, anatomically coherent, and usable when
scrubbed both forward and backward.
Continuity:
- Preserve the same recognizable face in every frame.
- Keep hair volume, hairline, ears, nose, jaw, skin texture, clothing folds,
and lighting stable.
- No face drift, morphing, warped anatomy, flicker, or changing expression.
- Keep the background perfectly uniform pure white (#FFFFFF) in every frame.
This is not a cinematic scene. It is a deterministic frame-scrubbing asset
for a website and every intermediate frame must work as a clean still image.
```
MiniMax H3 çıktısını değerlendirirken yalnızca ilk ve son kareye bakmayın. Orta karede yüzün, kulağın, saç çizgisinin ve tişört kenarlarının bozulmadığını da kontrol edin.
## 3. Sorunlu videoyu yeniden üretme promptu
İlk üretimde yüz kayması veya kamera hareketi varsa problemi `[[OBSERVED_PROBLEMS]]` alanına açıkça yazın.
```prompt
Regenerate this clip as a strict technical motion plate. The previous result
is unusable because: [[OBSERVED_PROBLEMS]].
Lock every property except the intended vertical gaze and head-tilt movement:
- preserve the exact identity and facial proportions in every frame;
- keep the horizontal head angle fixed at approximately 60 degrees left;
- fixed camera, crop, focal length, scale, head position, shoulders, torso,
arms, outfit, expression, lighting, and background;
- one slow linear movement from down-left to up-left;
- neutral horizontal-left pose at exactly two seconds;
- closed and motionless mouth;
- no speech, smile, blink during movement, eyebrow motion, body sway,
zoom, parallax, lighting shift, background flicker, face morphing,
hair change, ear deformation, or new objects;
- perfectly uniform pure white (#FFFFFF) background.
This clip will be paused and scrubbed frame by frame. Every intermediate frame
must remain anatomically coherent and visually consistent with the reference.
```
Örnek problem tanımı:
```text
[[OBSERVED_PROBLEMS]] = the face changes near the final pose, the shoulders
move with the head, and the white background flickers between frames
```
## 4. Videoyu web için hazırlama
Yapay zeka videosu doğrudan tarayıcıya konabilir; ancak sık `currentTime` güncellemelerinde codec ve keyframe aralığı büyük fark yaratır. Ben çıktıyı 720×720, 30 FPS, sessiz H.264 ve her kare keyframe olacak şekilde hazırladım.
```bash
ffmpeg -i INPUT.mp4 \
-vf "scale=720:720:force_original_aspect_ratio=decrease,pad=720:720:(ow-iw)/2:(oh-ih)/2:color=white,fps=30" \
-an -c:v libx264 -preset slow -crf 20 -pix_fmt yuv420p \
-g 1 -keyint_min 1 -sc_threshold 0 -movflags +faststart \
public/media/cursor-portrait/poyraz-bottom-right.mp4
```
Buradaki kritik tercihler:
- `-an`: sesi tamamen kaldırır.
- `yuv420p`: Safari ve Chromium uyumluluğunu artırır.
- `faststart`: MP4 metadata'sını dosyanın başına taşır.
- `-g 1`: her kareyi keyframe yaparak sık seek işlemini hızlandırır.
- `scale + pad`: görüntüyü esnetmeden 1:1 beyaz yüzeyde tutar.
### Medya optimizasyonu için coding-agent promptu
```prompt
Projeye eklediğim [[INPUT_VIDEO_PATH]] videosunu fare konumuyla ileri ve geri
sarılacak bir web hareket plakası olarak hazırla.
Kaynak dosyaya dokunma veya üzerine yazma.
Hedefler:
- Video: [[VIDEO_PATH]]
- Poster: [[POSTER_PATH]]
- Tam süre: 4.00 saniye
- Başlangıç pozu: 0.000.25
- Aktif hareket: 0.253.75
- Son poz: 3.754.00
- Çözünürlük: 720×720
- FPS: 30
- Codec: H.264 MP4, libx264, yuv420p
- Ayarlar: preset slow, CRF 20, faststart, ses yok
- Sık currentTime değişimi için her kare veya en fazla iki karede bir keyframe
En-boy oranını bozma. Gerekirse #FFFFFF padding kullan. Saç, yüz, kulak,
omuz veya kıyafeti kesme. İşlemden sonra süre, çözünürlük, FPS, codec ve dosya
boyutunu doğrula; ilk, orta ve son kareyi görsel olarak kontrol et. Alakasız
proje dosyalarına dokunma.
```
## 5. Mouse Y değerini video zamanına eşleme
Farenin Y konumu ekranın üstünde `0`, altında `window.innerHeight` değerindedir. Önce bu değeri ters çevirip `01` aralığına sıkıştırıyorum, ardından videonun aktif zaman aralığına map ediyorum.
```ts
const TOTAL_DURATION = 4;
const ACTIVE_START = 0.25;
const ACTIVE_END = 3.75;
const DEFAULT_TIME = 2;
const SMOOTHING = 0.12;
function clamp(value: number, minimum: number, maximum: number) {
return Math.min(Math.max(value, minimum), maximum);
}
function mapPointerYToTime(pointerY: number, viewportHeight: number) {
if (viewportHeight <= 0) return DEFAULT_TIME;
const progress = clamp(1 - pointerY / viewportHeight, 0, 1);
return ACTIVE_START + progress * (ACTIVE_END - ACTIVE_START);
}
```
Eşleme sonucu:
| Fare konumu | Progress | Video zamanı | Bakış |
| --- | ---: | ---: | --- |
| Ekranın altı | `0` | `0.25` | aşağı-sola |
| Ekranın ortası | `0.5` | `2.00` | yatay-sola |
| Ekranın üstü | `1` | `3.75` | yukarı-sola |
`pointerX` bu hesaplamaya hiç girmez. Fare yalnızca sağa veya sola hareket ettiğinde video karesi değişmez.
## 6. Akıcı scrub için React yaklaşımı
Her pointer event'inde React state güncellemek yerine yüksek frekanslı değerleri ref içinde tutun. Tek bir `requestAnimationFrame` döngüsü mevcut zamanı hedef zamana yaklaştırsın.
```ts
const pointerYRef = useRef<number | null>(null);
const targetTimeRef = useRef(DEFAULT_TIME);
const currentTimeRef = useRef(DEFAULT_TIME);
const rafIdRef = useRef<number | null>(null);
function animate() {
const difference = targetTimeRef.current - currentTimeRef.current;
currentTimeRef.current += difference * SMOOTHING;
const video = videoRef.current;
if (video && Math.abs(video.currentTime - currentTimeRef.current) > 1 / 120) {
video.currentTime = currentTimeRef.current;
}
if (Math.abs(difference) > 0.002) {
rafIdRef.current = requestAnimationFrame(animate);
} else {
rafIdRef.current = null;
}
}
function handlePointerMove(event: PointerEvent) {
if (event.pointerType !== "mouse") return;
pointerYRef.current = event.clientY;
targetTimeRef.current = mapPointerYToTime(
event.clientY,
window.innerHeight,
);
if (rafIdRef.current === null) {
rafIdRef.current = requestAnimationFrame(animate);
}
}
```
Üretim kodunda ayrıca metadata yüklenmesini, video priming ihtiyacını, sekme görünürlüğünü ve listener temizliğini yönetmek gerekir.
## 7. Component'i kodlatmak için hazır prompt
Aşağıdaki prompt Next.js, React veya benzer bir frontend projesinde coding agent ile kullanılabilir. Köşeli alanları kendi projenize göre doldurun.
```prompt
Mevcut [[FRAMEWORK]] projesine, ekranın sağ altında duran ve farenin yalnızca
Y konumunu takip eden tekrar kullanılabilir `PointerPortraitFollower` component'i
ekle. Stil sistemi: [[STYLING_SYSTEM]].
Asset'ler:
- Video: [[VIDEO_PATH]]
- Poster: [[POSTER_PATH]]
Sistem sabitleri:
- TOTAL_DURATION = 4
- ACTIVE_START = 0.25
- ACTIVE_END = 3.75
- DEFAULT_TIME = 2
- SMOOTHING = 0.12
Davranış:
- Video normal şekilde oynatılmayacak; paused tutulacak.
- Yalnızca pointerY kullan. pointerX zamanlamayı hiçbir şekilde etkilemesin.
- progress = clamp(1 - pointerY / window.innerHeight, 0, 1)
- targetTime = ACTIVE_START + progress * (ACTIVE_END - ACTIVE_START)
- Global pointermove listener kullan fakat pointer event başına React state
güncelleme.
- pointerY, targetTime ve currentTime değerlerini ref içinde tut.
- Tek requestAnimationFrame döngüsünde damping uygula.
- Seek işlemlerini yaklaşık 3060 Hz ile sınırla ve çok küçük farklarda atla.
- Fare pencere dışına çıktığında veya pencere odağı kaybolduğunda 2.00 saniyelik
nötr poza yumuşakça dön.
Video:
- muted, playsInline, preload="auto", controls yok, autoplay yok
- loadedmetadata sonrasında 2.00 saniyeye getir
- İlk gerçek pointer hareketinde gerekiyorsa muted olarak kısa prime et ve pause et
- Asset hatasında kırık video ikonu yerine poster göster
Yerleşim:
- position: fixed; right: [[RIGHT_OFFSET]]; bottom: [[BOTTOM_OFFSET]]
- width: [[DESKTOP_WIDTH]]; aspect-ratio: 1 / 1; z-index: [[Z_INDEX]]
- object-fit: contain; background: [[BACKGROUND_COLOR]]
- pointer-events: none; user-select: none; aria-hidden: true
- Border, radius, shadow veya yatay aynalama ekleme
Responsive:
- pointer: coarse veya dar ekranda animasyonu kapat
- Touch hareketlerini fare gibi yorumlama
- prefers-reduced-motion durumunda poster göster veya component'i gizle
- CTA, link ve menülerin tıklanmasını engelleme
Yaşam döngüsü:
- SSR sırasında window/document kullanma
- visibilitychange ile görünmeyen sekmede RAF ve seek'i durdur
- pointermove, pointerleave, blur, resize, visibilitychange ve RAF temizliğini
unmount sırasında eksiksiz yap
- Aynı anda birden fazla RAF döngüsü başlatma
Mapping ve clamp işlemlerini saf TypeScript yardımcılarına ayır. Projede test
altyapısı varsa alt, orta, üst ve clamp sınırları için test ekle. Ağır animasyon
kütüphanesi ekleme. Build, typecheck, lint ve mevcut testleri çalıştır.
```
Bu uygulamadaki örnek değişkenler:
```text
[[FRAMEWORK]] = Next.js App Router, React, TypeScript
[[STYLING_SYSTEM]] = Tailwind CSS and Poyraz UI
[[VIDEO_PATH]] = /media/cursor-portrait/poyraz-bottom-right.mp4
[[POSTER_PATH]] = /media/cursor-portrait/poyraz-bottom-right-poster.webp
[[RIGHT_OFFSET]] = 24px
[[BOTTOM_OFFSET]] = 0px
[[DESKTOP_WIDTH]] = clamp(110px, 11vw, 170px)
[[Z_INDEX]] = 40
[[BACKGROUND_COLOR]] = #FFFFFF
```
## 8. Mobil, erişilebilirlik ve fallback
Bu efekt masaüstünde fare ile anlam kazanıyor. Touch hareketlerini pointer takibi gibi yorumlamak sayfayı kullanmayı zorlaştırır ve gereksiz video decode maliyeti oluşturur.
Benim tercihlerim:
- `pointer: coarse` cihazlarda component'i render etmemek.
- `840px` altındaki ekranlarda tamamen gizlemek.
- `prefers-reduced-motion` tercihine saygı göstermek.
- Koyu temada beyaz arka planlı asset'i gizlemek.
- Portreyi `pointer-events: none` ve `aria-hidden="true"` ile dekoratif tutmak.
- Video yüklenmezse poster göstermek.
## 9. Kalite kontrol listesi
### Video
- İlk, orta ve son karede aynı kişi görünüyor mu?
- Baş video boyunca yaklaşık 60 derece sola dönük kalıyor mu?
- İlk kare aşağı-sola, orta kare yatay-sola, son kare yukarı-sola mı bakıyor?
- Saç, kulak, çene ve yüz orta karelerde bozuluyor mu?
- Omuzlar veya tişört istemeden hareket ediyor mu?
- Kamera, ışık veya beyaz arka plan titreşiyor mu?
- Video tersine sarıldığında hareket doğal görünüyor mu?
### Web
- Video fare hareket etmeden kendi kendine oynuyor mu? Oynamamalı.
- Fare yukarı ve aşağı giderken doğru yönde sarılıyor mu?
- Yalnızca sağa-sola harekette video zamanı sabit kalıyor mu?
- Hızlı harekette seek kuyruğu veya gecikme oluşuyor mu?
- Fare pencere dışına çıktığında nötr poza dönüyor mu?
- Portre linklerin ve CTA'ların tıklanmasını engelliyor mu?
- Mobilde ve reduced-motion modunda animasyon kapanıyor mu?
- Video yüklenmezse poster görünüyor mu?
- Sayfa değişiminden sonra listener veya RAF ikiye katlanıyor mu?
## Kendi projenize uyarlayın
Bu sistemi farklı bir kişi, çizim veya marka maskotuna taşımak için beş şey yeterli:
1. `[[...]]` değişkenlerini karakterinize göre doldurun.
2. 1:1 ve düz arka planlı tutarlı bir master kare üretin.
3. MiniMax H3 video promptunda yalnızca istediğiniz hareket eksenini tarif edin.
4. Aktif video aralığını farenin aynı eksenine map edin.
5. Optimize video ve poster yollarını component'e bağlayın.
En kritik karar, videoda olmayan bir hareketi kod tarafında taklit etmeye çalışmamaktır. Yapay zeka videosunu kontrollü bir hareket plakası olarak tasarladığınızda efekt hem daha doğal hem de daha kolay test edilebilir hale gelir.
## 10. Avatar için master kare promptu
Gerçek fotoğraf yerine 2D, 3D veya stilize bir avatar kullanıyorsanız aracın karakteri yeniden yorumlamasını engellemeniz gerekir. Bu prompt, avatarın mevcut tasarım dilini kilitler.
```prompt
Use the uploaded avatar as the strict character-design reference. Recreate the
exact same character in a clean, production-ready master frame for a
mouse-following website animation.
Preserve exactly:
- the character's face design, head shape, hairstyle, colors, outfit,
accessories, proportions, material style, line style, shading language,
and overall visual identity;
- the original medium and aesthetic: [[AVATAR_STYLE]];
- all intentional asymmetries and recognizable features.
Do not turn a 2D avatar into 3D, do not turn a stylized avatar into a real
person, and do not redesign or improve the character.
Composition:
- [[ASPECT_RATIO]] frame.
- Medium close-up from [[CROP_POINT]] upward.
- Center the character, leaving enough space for the head to rotate up to
[[MAX_HEAD_ROTATION]] degrees toward [[TURN_DIRECTION]].
- Shoulders remain stable and mostly facing forward.
- Neutral starting pose, only [[STARTING_HEAD_ANGLE]] degrees toward
[[STARTING_DIRECTION]].
- Expression: [[EXPRESSION]].
Background:
- Completely flat, seamless [[BACKGROUND_COLOR]].
- No texture, gradient, cast shadow, props, text, logo, border, scenery, or UI.
Continuity requirements:
- Clean silhouette and stable edges.
- Consistent eyes and facial features according to the reference design.
- No additional accessories or design changes.
- Produce one character and one clean master frame only.
```
## 11. Konuma özel hareket promptları
Bu rehberdeki gerçek uygulama dikey fare hareketini kullanıyor. Aşağıdaki alternatifler ise farenin yatay konumuna bağlanacak, önden profile dönen videolar içindir. Kişinin sayfa içeriğine baktığından emin olmak için hareket yönünü yerleşime göre ters çevirin.
### Sağ alt: sola dönen portre
```prompt
Animate the uploaded master frame into a precise motion-control plate for an
interactive website portrait. Preserve the exact identity, face, hairstyle,
outfit, body proportions, lighting, colors, framing, and background.
Output:
- Duration: exactly 4.0 seconds.
- Aspect ratio: [[ASPECT_RATIO]].
- One continuous shot with a locked, eye-level camera.
- No zoom, crop change, pan, tilt, dolly, camera shake, speech, or audio motion.
The person will appear in the bottom-right corner. Website content and the
pointer will usually be to the person's left, so the motion must progress from
an almost frontal pose to a clear screen-left profile.
Timeline:
- 0.00-0.25: hold the reference pose, only [[STARTING_HEAD_ANGLE]] degrees left.
- 0.25-3.75: rotate smoothly and linearly toward screen-left.
- Reach approximately [[MAX_HEAD_ROTATION]] degrees in a clean left profile.
- 3.75-4.00: hold the final pose perfectly still.
The eyes lead slightly. Only eyes, head, and neck move. Shoulders, torso, arms,
clothing, scale, body position, and expression remain fixed. Keep the mouth
closed. No talking, smiling, eyebrow movement, nodding, leaning, breathing
motion, blinking during the turn, or secondary gestures.
Preserve identity and anatomy in every frame. No morphing, face drift, hair or
ear deformation, lighting change, clothing change, or background flicker.
Keep a perfectly flat [[BACKGROUND_COLOR]] background with no gradient, shadow,
texture, object, text, or logo.
This is a deterministic website animation plate intended to be paused and
scrubbed frame by frame, not a cinematic video.
```
Bu yerleşimde `[[MAX_HEAD_ROTATION]] = 85-90` ve `[[STARTING_HEAD_ANGLE]] = 5-10` iyi bir başlangıçtır.
### Sol alt: sağa dönen portre
```prompt
Animate the uploaded master frame into a precise motion-control plate for an
interactive website portrait. Preserve the exact identity, face, hairstyle,
outfit, proportions, lighting, framing, and background.
The person will appear in the bottom-left corner, while most content and pointer
movement will be to the person's right.
Create exactly 4.0 seconds of one continuous, locked-off motion:
- Start almost facing the camera, only [[STARTING_HEAD_ANGLE]] degrees right.
- Hold the starting pose from 0.00 to 0.25 seconds.
- From 0.25 to 3.75 seconds, rotate smoothly and linearly toward screen-right.
- End at approximately [[MAX_HEAD_ROTATION]] degrees in a clean right profile.
- Hold that final pose from 3.75 to 4.00 seconds.
Only the eyes, head, and neck move. The eyes lead slightly and stay focused
toward screen-right. Shoulders, torso, arms, clothing, head scale, and body
position remain fixed. The camera is completely locked.
Keep the mouth closed and [[EXPRESSION]] unchanged. No speech, smile, lip or
eyebrow motion, nodding, leaning, blinking during the turn, breathing motion,
or gestures. No identity drift, morphing, hair change, warped profile, ear
deformation, lighting change, clothing change, or background flicker.
The background must remain perfectly flat [[BACKGROUND_COLOR]], without
shadows, gradients, props, text, logos, textures, or color variation. This is a
frame-scrubbable website plate, not a cinematic video.
```
### Hero sağı: soldaki başlık ve CTA'ya bakan portre
```prompt
Animate the uploaded identity-locked master frame for a website hero section.
The subject will be positioned on the right side; headline, copy, CTA buttons,
and pointer will be primarily on the left.
Create an exact 4.0-second locked-off motion-control clip. Start almost facing
the viewer at [[STARTING_HEAD_ANGLE]] degrees left and hold from 0.00 to 0.25.
From 0.25 to 3.75, smoothly rotate the eyes and head toward screen-left, ending
at [[MAX_HEAD_ROTATION]] degrees. Hold the final pose from 3.75 to 4.00.
The final pose must feel like the subject is looking at the hero headline and
CTA, not outside the page. Eyes lead slightly; the head follows in one slow,
continuous, linear movement.
Only eyes, head, and neck move. Keep shoulders, torso, arms, clothing, position,
scale, expression, and silhouette fixed. Mouth closed. No talking, smiling,
blinking during the turn, nodding, leaning, gestures, body sway, or breathing.
Preserve the exact person or avatar design. No face drift, morphing, hair or
outfit changes, lighting shifts, framing changes, or warped profile. Use a fixed
eye-level camera and a perfectly flat [[BACKGROUND_COLOR]] background.
Aspect ratio: [[ASPECT_RATIO]]. The result must be frame-scrubbable.
```
Hero içinde daha doğal bir bakış için `[[MAX_HEAD_ROTATION]] = 65-75` genellikle yeterlidir.
### Hero solu: sağdaki başlık ve CTA'ya bakan portre
```prompt
Animate the uploaded identity-locked master frame for a website hero section.
The subject will be positioned on the left side; headline, copy, CTA buttons,
and pointer will be primarily on the right.
Create an exact 4.0-second locked-off motion-control clip. Start almost facing
the viewer at [[STARTING_HEAD_ANGLE]] degrees right and hold from 0.00 to 0.25.
From 0.25 to 3.75, smoothly rotate the eyes and head toward screen-right, ending
at [[MAX_HEAD_ROTATION]] degrees. Hold the final pose from 3.75 to 4.00.
The final pose must feel like the subject is looking at the hero headline and
CTA, not outside the page. Eyes lead slightly; the head follows in one slow,
continuous, linear movement.
Only eyes, head, and neck move. Keep shoulders, torso, arms, clothing, position,
scale, expression, and silhouette fixed. Mouth closed. No talking, smiling,
blinking during the turn, nodding, leaning, gestures, body sway, or breathing.
Preserve the exact person or avatar design. No identity drift, morphing, hair or
outfit changes, lighting shifts, framing changes, or warped profile. Use a fixed
eye-level camera and a perfectly flat [[BACKGROUND_COLOR]] background.
Aspect ratio: [[ASPECT_RATIO]]. The result must be frame-scrubbable.
```
### Hero ortası: soldan sağa tam tarama
```prompt
Animate the uploaded identity-locked master frame into a symmetrical
left-to-right head-turn calibration clip for an interactive centered hero.
Output one continuous 4.0-second shot in [[ASPECT_RATIO]] with a locked,
eye-level camera. Preserve identity or avatar design, outfit, expression,
lighting, framing, scale, and [[BACKGROUND_COLOR]] background.
Timeline:
- 0.00-0.25: hold approximately [[LEFT_ANGLE]] degrees toward screen-left.
- 0.25-3.75: perform the complete symmetrical left-to-right rotation.
- Reach the exact front-facing pose at 50% of the active motion interval.
- Continue at the same speed to [[RIGHT_ANGLE]] degrees toward screen-right.
- 3.75-4.00: hold the final right-facing pose.
- Keep path, speed, scale, and head height symmetrical on both sides.
The eyes lead only slightly. Only eyes, head, and neck move. Shoulders, torso,
arms, clothing, body position, scale, and expression remain fixed. Mouth closed.
No speech, smile, blink during movement, eyebrow motion, nod, lean, gesture,
body sway, or breathing motion.
No identity drift, morphing, hairstyle change, ear deformation, warped profile,
lighting shift, background flicker, camera movement, zoom, crop, or reframing.
The background remains perfectly uniform [[BACKGROUND_COLOR]]. This must stay
clean when paused and scrubbed in either direction.
```
Simetrik başlangıç için `[[LEFT_ANGLE]] = 75` ve `[[RIGHT_ANGLE]] = 75` kullanabilirsiniz.
## 12. Her varyantta kullanılabilen onarım promptu
Kamera, omuz veya ağız hareket ediyorsa; profil bozuluyor ya da yüz başka birine dönüşüyorsa sorunu `[[OBSERVED_PROBLEMS]]` alanında somut biçimde yazın.
```prompt
Regenerate this clip as a strict technical motion plate. The previous result is
unusable because it contains: [[OBSERVED_PROBLEMS]].
Lock every property except the intended head rotation:
- exact same identity and facial proportions in every frame;
- fixed camera, crop, focal length, scale, head position, shoulders, torso,
arms, outfit, expression, lighting, and background;
- only the eyes, head, and neck may move;
- one slow, linear rotation from [[STARTING_DIRECTION_AND_ANGLE]] to
[[ENDING_DIRECTION_AND_ANGLE]];
- closed and motionless mouth;
- no speech, smile, blink during the turn, eyebrow movement, nod, lean, body
sway, breathing, camera motion, zoom, parallax, lighting shift, background
flicker, face morphing, hair change, ear deformation, or new objects;
- perfectly uniform [[BACKGROUND_COLOR]] background;
- preserve the reference identity exactly, especially in the final profile.
This is a frame-scrubbing website asset, so every intermediate frame must be
anatomically coherent and usable as a still image.
```
## 13. Tüm yatay varyantlar için kodlama agent'ı promptu
Bu prompt, tek bir tekrar kullanılabilir component ile sağ alt, sol alt ve hero yerleşimlerini destekletmek içindir. `pointerX` kullanır; bu rehberin başındaki dikey `pointerY` uygulamasının alternatifi olarak düşünün.
```prompt
Mevcut [[FRAMEWORK]] projesine, fareyi başıyla takip ediyormuş gibi görünen
tekrar kullanılabilir bir video portre bileşeni ekle. Stil sistemi:
[[STYLING_SYSTEM]]. Önce proje yapısını ve kod standartlarını incele.
Asset'ler:
- Video: [[VIDEO_PATH]]
- Poster: [[POSTER_PATH]]
- Aktif hareket: 0.25-3.75 saniye
- Video hareketi: [[VIDEO_MOTION_DESCRIPTION]]
- Yerleşim: [[PLACEMENT]]
Davranış:
- Video muted, playsInline, preload auto, paused ve kontrolsüz autoplay olmadan çalışsın.
- Global pointermove dinle; portre ankrajını getBoundingClientRect ile ölç.
- Pointer konumunu 0-1 targetProgress değerine, sonra 0.25-3.75
currentTime aralığına map et.
- requestAnimationFrame içinde lerp/damping kullan; pointer event başına React
state güncelleme.
- Seek'i 30-60 Hz ile sınırla ve çok küçük farkları atla.
Yön eşlemesi:
- bottom-right veya hero-right sola dönüyorsa, fare portreye yakınken progress 0;
ekranın soluna uzaklaştıkça progress 1.
- bottom-left veya hero-left sağa dönüyorsa, fare portreye yakınken progress 0;
ekranın sağına uzaklaştıkça progress 1.
- hero-center soldan sağa dönüyorsa pointerX / viewportWidth progress olsun.
- Progress'i 0-1 arasında clamp et; anatomik olarak anlamsız ters dönüş üretme.
Typed API:
- src, poster
- placement: bottom-right | bottom-left | hero-right | hero-left | hero-center
- defaultProgress, smoothing, desktopWidth, mobileWidth
- offsetX, offsetY, zIndex, className, decorative, invertProgress
Görsel ve yaşam döngüsü:
- bottom-* fixed; hero-* ilgili hero container'ına absolute olsun.
- object-fit contain, sabit aspect-ratio ve [[BACKGROUND_COLOR]] kullan.
- Dekoratifse pointer-events none, user-select none, draggable false ve
aria-hidden true kullan; CTA ve metinleri kapatmasın.
- Client-side çalışsın. loadedmetadata sonrası [[DEFAULT_PROGRESS]] karesine git.
- Gerekirse ilk gerçek etkileşimde muted prime et ve hemen pause et.
- Her frame layout okuma; resize/scroll ile düşük maliyetli yeniden ölçüm yap.
- Sekme gizlenince RAF/seek'i durdur; tüm listener ve RAF'ları unmount'ta temizle.
- Coarse pointer ve reduced-motion'da takip etme; [[MOBILE_BEHAVIOR]] uygula.
- Video hata verirse poster göster, kırık medya ikonu gösterme.
Sistem sabitleri TOTAL_DURATION=4, ACTIVE_START=0.25 ve ACTIVE_END=3.75
olsun. Ağır animasyon kütüphanesi ekleme. TypeScript tiplerini tamamla,
mapping/clamp testlerini ekle ve alakasız dosyaları refactor etme.
Tamamlandığında değişen dosyaları, yön formülünü, build/typecheck/lint/test
sonuçlarını ve dört maddelik manuel test listesini raporla.
```
### Yalnızca yeni bir konum ekletme
```prompt
Mevcut `CursorFollowerPortrait` bileşeninin davranışını ve API'sini bozma.
Sadece yeni bir [[NEW_PLACEMENT]] varyantı ekle.
Yeni asset:
- Video: [[NEW_VIDEO_PATH]]
- Poster: [[NEW_POSTER_PATH]]
- Aktif hareket: 0.25-3.75 saniye
- Hareket: [[NEW_VIDEO_MOTION_DESCRIPTION]]
Konum ve eşleme:
- Yerleşim: [[NEW_PLACEMENT]]
- Offset: [[HORIZONTAL_OFFSET]] yatay, [[VERTICAL_OFFSET]] dikey
- Genişlik: [[DESKTOP_WIDTH]] / mobil [[MOBILE_WIDTH]]
- Anatomik yön kuralı: [[DIRECTION_MAPPING_RULE]]
Diğer varyantları değiştirme. Çalışan kullanım örneği ekle; build,
typecheck ve lint çalıştır. Yalnızca değişen dosyaları ve sonuçları raporla.
```
### Hata ayıklama promptu
```prompt
`CursorFollowerPortrait` efektinde şu sorun var: [[BUG_DESCRIPTION]].
Önce sorunu yeniden üret ve kök nedeni ölçerek belirle. Kontrol et:
- metadata yüklenmeden currentTime atanması;
- codec/keyframe aralığı nedeniyle yavaş seeking;
- yanlış progress yönü veya invertProgress;
- pointer event başına React render;
- biriken RAF veya event listener;
- her frame getBoundingClientRect ile layout thrashing;
- Safari/iOS video priming;
- asset yolu, CORS, preload ve poster fallback;
- reduced-motion veya coarse pointer algısı;
- fixed/absolute container ve stacking context.
Kök nedeni açıklamadan rastgele refactor yapma. En küçük güvenli düzeltmeyi
uygula, public API'yi koru ve build/typecheck/lint/test sonuçlarını raporla.
```
## 14. Gerçek iki eksenli takip: 3x3 yön sistemi
Tek video yalnızca içerdiği ekseni güvenilir biçimde takip eder. Fareye hem yatay hem dikey bakılması gerekiyorsa aynı master kareden dokuz hizalı poz üretin. Gerçek kişi için başlangıç değeri olarak yaw'da `-35° / 0° / +35°`, pitch'te `-18° / 0° / +18°` kullanılabilir.
| Poz | Yaw | Pitch |
| --- | ---: | ---: |
| Üst sol | `[[YAW_LEFT]]` | `[[PITCH_UP]]` |
| Üst orta | `0` | `[[PITCH_UP]]` |
| Üst sağ | `[[YAW_RIGHT]]` | `[[PITCH_UP]]` |
| Orta sol | `[[YAW_LEFT]]` | `0` |
| Orta | `0` | `0` |
| Orta sağ | `[[YAW_RIGHT]]` | `0` |
| Alt sol | `[[YAW_LEFT]]` | `[[PITCH_DOWN]]` |
| Alt orta | `0` | `[[PITCH_DOWN]]` |
| Alt sağ | `[[YAW_RIGHT]]` | `[[PITCH_DOWN]]` |
### Dokuz yönlü kare üretim promptu
```prompt
Using the uploaded identity-locked master frame, create one exact directional
calibration pose for a 3x3 mouse-tracking portrait system.
Preserve the exact same identity, face, hairstyle, outfit, body, framing,
camera, focal length, lighting, scale, background, crop, and expression. This
image must align pixel-for-pixel as closely as possible with all other poses.
Change only:
- head yaw: [[TARGET_YAW]] degrees;
- head pitch: [[TARGET_PITCH]] degrees;
- eye gaze: [[GAZE_DIRECTION]], aligned naturally with the head direction.
Keep shoulders, torso, arms, clothing, body position, head center, and head
scale fixed. Mouth closed. No smile, speech, blink, eyebrow movement, body
turn, lean, camera motion, crop change, zoom, or lighting change.
Maintain anatomically correct neck, ears, eyes, jaw, and facial profile. No
morphing or identity drift. Use a perfectly flat [[BACKGROUND_COLOR]] identical
to the master frame. Output one image only at [[OUTPUT_DIMENSIONS]].
```
### Dokuz yönlü sistemi kodlatma promptu
```prompt
Mevcut [[FRAMEWORK]] projesine gerçek iki eksenli fare takibi yapan
`DirectionalPortraitGrid` bileşeni ekle.
Dokuz aynı boyutlu ve hizalı asset:
- top-left: [[TOP_LEFT_PATH]]
- top-center: [[TOP_CENTER_PATH]]
- top-right: [[TOP_RIGHT_PATH]]
- middle-left: [[MIDDLE_LEFT_PATH]]
- center: [[CENTER_PATH]]
- middle-right: [[MIDDLE_RIGHT_PATH]]
- bottom-left: [[BOTTOM_LEFT_PATH]]
- bottom-center: [[BOTTOM_CENTER_PATH]]
- bottom-right: [[BOTTOM_RIGHT_PATH]]
Pointer konumunu portre merkezine göre normalize et ve iki ekseni -1 ile 1
arasında clamp et. En yakın resmi sertçe değiştirme. Çevredeki dört grid
hücresini bul, bilinear interpolation ağırlıklarını hesapla ve görselleri aynı
koordinatlarda opacity ile karıştır.
RAF ve damping kullan; pointer event başına React render yapma. Asset'leri LCP'yi
bloke etmeden önceden yükle. Reduced-motion, coarse pointer veya yükleme hatasında
center görselini göster. Dekoratifse pointer-events none ve aria-hidden true kullan.
Typed props: sources, placement, desktopWidth, mobileWidth, smoothing,
maxTrackingDistance, offsetX, offsetY, className, decorative. Grid matematiğini
saf fonksiyonlara ayır; köşe, merkez ve ara değer testleri ekle. Ağır animasyon
kütüphanesi ekleme. Örnek kullanım ile build/typecheck/lint/test sonuçlarını ver.
```
## 15. Reels için içerik üretim promptu
Bu efekti anlatan 55 saniyelik bir video için kullanabileceğiniz akış: `0-3 sn` sonuç, `3-7 sn` hook, `7-16 sn` master kare, `16-26 sn` hareket videosu, `26-42 sn` kodlama, `42-51 sn` önce/sonra ve varyant, `51-58 sn` CTA.
```prompt
Ben yazılım alanında içerik üreten [[CREATOR_PROFILE]] bir içerik üreticisiyim.
Aşağıdaki interaktif web efekti için Instagram Reels içeriği hazırla:
Efekt: [[EFFECT_DESCRIPTION]]
Karakter: [[REAL_PERSON_OR_AVATAR]]
Yerleşim: [[PLACEMENT]]
Üretim aracı: [[GENERATION_TOOL]]
Kodlama agent'ı: [[CODING_AGENT]]
Hedef kitle: [[TARGET_AUDIENCE]]
Süre: 55 saniye
Ton: [[TONE]]
Şu yapıyı kullan:
1. İlk 2 saniyede sonucu gösteren görsel hook.
2. En fazla 10 kelimelik konuşma hook'u.
3. Videonun sonunda elde edilecek net sonuç.
4. Referanstan master frame üretme adımı.
5. Konuma özel hareket promptunun kritik satırları.
6. Kodlama promptunun kritik teknik mantığı.
7. currentTime ile fare eşlemesini yeni başlayanın anlayacağı tek cümle.
8. Before/after sahnesi.
9. Sonraki varyanta açık döngü.
10. Doğal ve kısa CTA.
Her bölüm için zaman aralığı, konuşma, ekran görüntüsü, büyük ekran
yazısı ve kurgu geçişi ver. Abartılı pazarlama, gereksiz jargon, uzun giriş,
"AI her şeyi yaptı" söylemi veya gerçek dışı süre vaadi kullanma. Promptların
tamamını ekranda okutma; kritik satırları vurgula. Konuşma 120 kelimeyi geçmesin.
```
Bu paketle aynı temel master kareden sağ alt, sol alt, hero sağı, hero solu, hero ortası ve gerçek 3x3 takip varyantlarını ayrı asset'ler olarak üretebilirsiniz. Her asset'in video hareketini, yerleşim formülünü ve fallback'ini birlikte isimlendirmek sistem büyüdüğünde yön hatalarını önler.
+85
View File
@@ -0,0 +1,85 @@
---
title: "Software with Poyraz #2702082026"
category: "Newsletter"
date: "2026-08-02"
readTime: "5 min read"
author: "Poyraz Avsever"
slug: "newsletter2702082026-en"
excerpt: "This week, we have a packed agenda, from autonomous AI models and data center water consumption to major shifts in design tools, next-generation batteries, and chip technologies."
coverImage: "/blog/images/newsletter2702082026-cover.png"
lang: "en"
---
# Software with Poyraz #2702082026
Greetings,
I am back with a new issue of Software with Poyraz. In this edition, covering the week of July 27 - August 2, 2026, we take a closer look at several developments that are shaking up the technology and software world.
From AI models that act on their own and lock up systems, to the massive water crisis behind data centers, major changes in design tools, and next-generation battery and chip technologies, we have a full agenda this week.
Without further ado, let's move quickly into the week's highlights.
## Artificial Intelligence Developments
### The White House's $5 Billion Genesis Mission and the AI Race with China
Sources: [Washington Post](https://www.washingtonpost.com/business/technology/), [Nextgov](https://www.nextgov.com/)
The White House allocated a massive budget of more than $5 billion to the Genesis Mission project to accelerate the use of artificial intelligence in scientific research, with more than 15 federal agencies involved. Led by the Department of Energy, the project selected more than 270 AI initiatives across areas such as health, energy, and national security. On the other side, Silicon Valley CEOs published a joint letter opposing more restrictive AI policies. The real reason behind this letter is the rising cost of US-based labs, which is pushing companies toward China-based models, while companies such as DeepSeek and Moonshot AI continue to move quickly.
Thought: The confusion governments have around artificial intelligence is very clear. On one hand, billions of dollars are being distributed to preserve technological superiority against China; on the other hand, governments are trying to introduce strict regulations because they are afraid of autonomous hacking incidents. As software teams, the biggest lesson we should take from this geopolitical tension is that we should not lock our systems into the ecosystem of a single country or company. We have to design our infrastructure in an agnostic way that can switch between different APIs instantly.
## Software Developments
### The GitHub Models Era Is Over: Developers Are Looking for Alternative Routes
[Source link](https://www.developersdigest.tech/blog/github-models-retired-2026)
GitHub, under Microsoft, permanently shut down GitHub Models as of July 30, 2026. The service included a model catalog, playground, inference API, and bring-your-own-key (BYOK) features. After planned outages throughout July, the system's full shutdown put engineering teams in a difficult position, especially those testing models in CI workflows and relying on BYOK configurations.
Thought: This is one of the classic examples of PaaS providers moving toward cost optimization. These proxy-style services that offer API management with almost no friction have very high compute costs behind the scenes. The biggest architectural lesson here is that we should not tightly couple LLM integrations, which are now at the heart of many systems, to a single platform's interface or authentication model. Teams should manage their own LLM gateways and build modular routing solutions that can distribute requests across different models.
### AI Spam Split GitHub's Bug Bounty Program in Two
[Source link](https://www.techradar.com/pro/security/github-restructures-bug-bounty-program-following-flood-of-ai-generated-reports)
The ability of large language models to analyze source code created a serious crisis for GitHub's security department. Thousands of low-effort and hallucinated vulnerability reports generated with AI overwhelmed the platform. In response, GitHub divided its bug bounty program into two tiers: a Public Program that requires a HackerOne track record, and a VIP Program with increased rewards. Linus Torvalds similarly noted that Linux security mailing lists had become nearly unusable because of AI-driven hunters.
Thought: We can clearly see that DoS attacks have changed form and turned into "Cognitive DoS." In the past, server resources were exhausted; now, the attention and time of cybersecurity analysts are being consumed directly. Inexperienced users who paste code into ChatGPT and generate fake vulnerability reports are creating a serious cost for defenders. DevSecOps workflows will absolutely need intermediary agents that check whether a report was written with AI before it reaches human review.
### Open Source Security: Dependabot Updates and npm Supply Chain Defense
[Source link](https://github.blog/)
GitHub published new techniques to prevent supply chain attacks on npm and GitHub Actions, especially typosquatting attempts. Immediately afterward, strategies were introduced to reduce one of developers' biggest pain points: Dependabot noise. Dependabot pull requests can now be grouped, and update frequency can be slowed down, helping projects avoid unnecessary notification overload.
Thought: Dependency management is truly the Achilles' heel of software engineering. However, waiting for PR approval for every small package update clogs CI/CD pipelines and creates "Alert Fatigue," which can lead teams to approve warnings blindly. PR grouping is a strong solution from an engineering psychology perspective; testing updates in packages should seriously reduce integration risk.
## Design Developments
### 2026 Design Tools Report: Figma's Monopoly and Conversion-Focused Interface Metrics
[Source link](https://linkupst.com/design/blog-design/top-ui-ux-agencies)
According to an independent report published in July 2026, Figma was selected as the clear market leader with a score of 9.1 out of 10, thanks to its real-time multiplayer architecture and AI that can generate wireframes in 30 seconds. Adobe XD fell into legacy status because it no longer receives updates, while Canva positioned itself for marketing teams and Sketch for macOS performance enthusiasts. Meanwhile, reports from UI/UX agencies showed that a good interface can increase conversion rates by up to 200%, while deeper UX interventions with well-designed flow and interaction logic can increase them by up to 400%. Google data also confirms that more than half of mobile users leave sites that take longer than 3 seconds to load.
Thought: Figma's success is not just about being a good drawing tool; it comes from a radical change in data structure architecture. Older software kept files in the operating system, while Figma transformed interface design into a browser-based database problem and made the URL itself the source. Interface design has evolved from artistic aesthetics into an engineering discipline backed by behavioral economics and data analytics. Reducing cognitive load is now a much more strategic decision than nudging pixels around.
## Technology News
### A Cyber-Physical Revolution in the Oceans: RIMPAC 2026 and Additive Manufacturing
[Source link](https://www.eurasiareview.com/26072026-exercise-rimpac-2026-features-uncrewed-vessels-other-emerging-technologies/)
The RIMPAC 2026 exercise showcased an impressive integration of naval operations and technology. Autonomous uncrewed surface and underwater vehicles used for intelligence and surveillance played the leading role. But the most striking development was the use of uncrewed drones to deliver 3D printers to ships, allowing critical parts to be printed directly in the middle of the ocean instead of waiting for intercontinental supply chains.
Thought: The concept of contested logistics sits exactly at the intersection of digital software and physical manufacturing. Instead of waiting for a damaged sensor to be shipped, downloading its CAD file via satellite and printing it immediately turns the supply chain entirely into data transfer. The fact that uncrewed submarines can calculate physical factors such as ocean currents, pressure, and wind in real time proves that Physical AI is moving down to the hardware level, not only simulating the world but directly commanding it.
### The Industrial Technology Arena: Physical AI Events and the Asian Market
[Source link](https://www.iiot-world.com/industrial-iot/connected-industry/july-2026-industrial-ai-events-global-conference-guide/)
Events held in July, such as the Farnborough Airshow in the United Kingdom and Asia's massive Automation Expo Mumbai, put the intersection of hardware and software on display. The most notable trend was that Physical AI became an independent category of its own through conferences such as MACHINA and AUTONOMOUS.
Thought: Physical AI is no longer only in theoretical papers; it has become a commercial product sold directly on trade show floors. Our software code now controls not just digital pixels, but steel arms and servo motors that weigh tons. We can clearly see innovation shifting out of Silicon Valley and into production lines across Asia and Europe. This growing data load in automation will also increase edge computing investment in IIoT dramatically.
+85
View File
@@ -0,0 +1,85 @@
---
title: "Poyraz ile Yazılıma Dair #2702082026"
category: "Newsletter"
date: "2026-08-02"
readTime: "5 min read"
author: "Poyraz Avsever"
slug: "newsletter2702082026"
excerpt: "Bu hafta otonom hareket eden yapay zeka modellerinden veri merkezlerinin su tüketimine, tasarım araçlarındaki dönüşümden yeni nesil batarya ve çip teknolojilerine kadar yoğun bir gündemimiz var."
coverImage: "/blog/images/newsletter2702082026-cover.png"
lang: "tr"
---
# Poyraz ile Yazılıma Dair #2702082026
Selamlar,
Poyraz ile Yazılıma Dair serimizin yeni yazısıyla karşınızdayım. 27 Temmuz - 2 Ağustos 2026 haftasını kapsayan bu bölümümüzde, teknoloji ve yazılım dünyasında taşları yerinden oynatan oldukça ilginç gelişmeleri masaya yatırıyoruz.
Kendi başına hareket edip sistemleri kilitleyen yapay zeka modellerinden, veri merkezlerinin arkasında yatan inanılmaz su krizine, tasarım araçlarındaki devasa değişimlerden yeni nesil batarya ve çip teknolojilerine kadar dopdolu bir gündemimiz var.
Lafı hiç uzatmadan haftanın öne çıkan başlıklarına hızlıca geçelim.
## Yapay Zeka Gelişmeleri
### Beyaz Saray'dan 5 Milyar Dolarlık Genesis Mission ve Çin ile Yapay Zeka Rekabeti
Kaynaklar: [Washington Post](https://www.washingtonpost.com/business/technology/), [Nextgov](https://www.nextgov.com/)
Beyaz Saray, yapay zekanın bilimsel araştırmalardaki kullanımını hızlandırmak için 15'ten fazla federal kurumun katıldığı Genesis Mission projesine 5 milyar doların üzerinde devasa bir bütçe ayırdı. Enerji Bakanlığı önderliğinde yürütülen projede sağlık, enerji, ulusal güvenlik gibi alanlarda 270'ten fazla yapay zeka projesi seçildi. Diğer yanda ise Silikon Vadisi CEO'ları daha kısıtlayıcı yapay zeka politikalarına karşı çıkan ortak bir bildiri yayınladı. Amerika merkezli laboratuvarların artan maliyetleri nedeniyle şirketlerin Çin merkezli modellere yönelmesi ve DeepSeek, Moonshot AI gibi şirketlerin hızla ilerlemesi bu mektubun asıl sebebini oluşturuyor.
Düşüncem: Devletlerin yapay zeka konusundaki kafa karışıklığı çok net ortada. Bir yandan Çin'e karşı teknolojik üstünlüğü korumak için milyarlarca dolar fon dağıtılırken, diğer yandan otonom hack vakalarından korkup katı regülasyonlar getirmeye çalışıyorlar. Yazılım ekipleri olarak bu jeopolitik çekişmelerden çıkaracağımız en büyük ders, sistemlerimizi tek bir ülkenin veya şirketin ekosistemine kilitlememek olmalı. Altyapılarımızı farklı API'ler arasında anında geçiş yapabilecek şekilde agnostik tasarlamak zorundayız.
## Yazılım Gelişmeleri
### GitHub Models Dönemi Kapandı: Geliştiriciler Alternatif Rotalara Yöneliyor
[Kaynak linki](https://www.developersdigest.tech/blog/github-models-retired-2026)
Microsoft bünyesindeki GitHub, model kataloğunu, playground alanını, çıkarım API'sini ve kendi anahtarını getir (BYOK) özelliklerini barındıran GitHub Models hizmetini 30 Temmuz 2026 itibarıyla kalıcı olarak kapattı. Temmuz ayında uygulanan planlı kesintiler sonrası tamamen kapanan bu sistem, özellikle CI süreçlerinde model test edenleri ve BYOK konfigürasyonlarını kullanan mühendislik ekiplerini zor durumda bıraktı.
Düşüncem: PaaS sağlayıcılarının maliyet optimizasyonuna gitmesinin en klasik örneklerinden birini yaşıyoruz. Sıfır sürtünmeyle API yönetimi sunan bu tarz proxy hizmetlerinin arka plandaki compute maliyeti çok yüksektir. Buradan alacağımız en büyük mimari ders, sistemin kalbi olan LLM entegrasyonlarını tek bir platformun arayüzüne veya kimlik doğrulamasına sıkı sıkıya bağlamamaktır. Ekipler olarak kendi LLM gateway'lerimizi yönetmeli ve istekleri farklı modeller arasında dağıtabilen modüler yönlendirici çözümler kurmalıyız.
### Yapay Zeka Spam'i, GitHub'ın Bug Bounty Programını İkiye Böldü
[Kaynak linki](https://www.techradar.com/pro/security/github-restructures-bug-bounty-program-following-flood-of-ai-generated-reports)
Büyük dil modellerinin kaynak kod analizindeki yetenekleri GitHub'ın güvenlik departmanında ciddi bir kriz yarattı. İnsanların yapay zeka kullanarak ürettiği binlerce düşük eforlu ve halüsinasyon içeren sahte zafiyet raporu platformu kilitledi. Bunun üzerine GitHub, hata ödül programını iki kademeli hale getirerek HackerOne geçmişi aranan Public Program ve ödüllerin katlandığı VIP Program olmak üzere ayırdı. Linus Torvalds da benzer şekilde Linux güvenlik e-posta listelerinin yapay zeka avcıları yüzünden kullanılamaz hale geldiğini belirtti.
Düşüncem: DoS saldırılarının form değiştirip "Cognitive DoS" halini aldığını çok net görüyoruz. Eskiden sunucu kaynakları tüketilirken, şimdi doğrudan siber güvenlik analistlerinin dikkati ve zamanı tüketiliyor. ChatGPT'ye kod kopyalatıp sahte zafiyet raporları üreten deneyimsiz kullanıcılar, savunma tarafında ciddi bir maliyet yaratıyor. DevSecOps süreçlerinde artık insan incelemesinden önce raporun yapay zeka ile yazılıp yazılmadığını test edecek ara ajanlara kesinlikle ihtiyacımız olacak.
### Açık Kaynak Güvenliğinde Dependabot Güncellemeleri ve npm Tedarik Zinciri Savunması
[Kaynak linki](https://github.blog/)
GitHub, npm ve GitHub Actions üzerindeki tedarik zinciri saldırılarını, özellikle typosquatting girişimlerini, engellemek amacıyla yeni teknikler yayınladı. Hemen ardından geliştiricilerin en büyük dertlerinden biri olan Dependabot gürültüsünü azaltacak stratejiler devreye girdi. Artık Dependabot'un açtığı PR'lar gruplandırılabilecek ve güncelleme hızı yavaşlatılarak projeler gereksiz bildirim yağmurundan kurtarılacak.
Düşüncem: Bağımlılık yönetimi yazılım mühendisliğinin gerçekten Aşil topuğu. Ancak sürekli gelen ufak paket güncellemeleri için PR onayı beklemek, CI/CD hatlarını tıkayıp geliştiricilerde "Alert Fatigue" yarattığı için uyarıları körlemesine onaylama refleksine yol açıyordu. PR gruplandırma hamlesi mühendislik psikolojisi açısından harika bir çözüm; güncellemelerin paketler halinde test edilmesi entegrasyon riskini ciddi şekilde düşürecektir.
## Tasarım Gelişmeleri
### 2026 Tasarım Araçları Raporu: Figma'nın Monopolü ve Arayüzün Dönüşüm Odaklı Metrikleri
[Kaynak linki](https://linkupst.com/design/blog-design/top-ui-ux-agencies)
Temmuz 2026'da yayımlanan bağımsız rapora göre Figma; gerçek zamanlı multiplayer mimarisi ve 30 saniyede wireframe üreten yapay zekasıyla 10 üzerinden 9.1 puan alarak pazarın açık ara lideri seçildi. Adobe XD güncelleme almadığı için legacy statüsüne gerilerken, Canva pazarlama ekipleri, Sketch ise macOS performans tutkunları için konumlandı. Diğer yandan UI/UX ajanslarının raporları, iyi bir arayüzün dönüşüm oranlarını %200'e, akış ve etkileşim mantığı çözülmüş derin UX müdahalelerinin ise %400'e kadar artırabildiğini ortaya koydu. Google verileri de 3 saniyeden geç yüklenen mobil sitelerde kullanıcıların yarısından fazlasının kaçtığını doğruluyor.
Düşüncem: Figma'nın bu zaferi sadece iyi bir çizim aracı olmasından değil, veri yapısı mimarisindeki radikal değişimden geliyor. Eski yazılımlar dosyaları işletim sisteminde tutarken Figma arayüz tasarımını tarayıcı tabanlı bir veritabanı problemine dönüştürdü ve URL'in kendisini kaynak haline getirdi. Arayüz tasarımı artık sanatsal bir estetikten tamamen davranışsal ekonomi ve veri analitiğiyle desteklenen bir mühendislik disiplinine dönüştü. Bilişsel yükü azaltmak artık piksel kaydırmaktan çok daha stratejik bir karar.
## Teknoloji Haberleri
### Okyanuslarda Siber-Fiziksel Devrim: RIMPAC 2026 ve Katmanlı İmalat
[Kaynak linki](https://www.eurasiareview.com/26072026-exercise-rimpac-2026-features-uncrewed-vessels-other-emerging-technologies/)
RIMPAC 2026 tatbikatı donanma ve teknolojinin inanılmaz entegrasyonuna sahne oldu. İstihbarat ve gözetleme yapan otonom insansız su üstü ve su altı araçları ana roldeydi. Ancak en çarpıcı gelişme, kıtalararası tedarik zincirlerini beklemek yerine insansız drone'larla gemilere 3D yazıcılar ulaştırılarak hayati parçaların doğrudan okyanus ortasında basılması oldu.
Düşüncem: Contested Logistics kavramı dijital yazılım ile fiziksel üretimin tam uç noktasında birleşmesi demek. Hasarlı bir sensörün kargosunu beklemek yerine CAD dosyasını uyduyla indirip anında basmak, tedarik zincirini tamamen veri transferine dönüştürüyor. İnsansız denizaltıların okyanus akıntıları, basınç ve rüzgar gibi fiziksel faktörleri eşzamanlı hesaplaması, Physical AI kavramının donanım seviyesine inerek dünyayı sadece simüle etmediğini, doğrudan komuta ettiğini kanıtlıyor.
### Endüstriyel Teknoloji Arenası: Fiziksel Yapay Zeka Etkinlikleri ve Asya Piyasası
[Kaynak linki](https://www.iiot-world.com/industrial-iot/connected-industry/july-2026-industrial-ai-events-global-conference-guide/)
Temmuz ayında İngiltere'de düzenlenen Farnborough Airshow ve Asya'nın devasa otomasyon fuarı Automation Expo Mumbai gibi etkinlikler donanım ve yazılımın kesişimini vitrine çıkardı. En dikkat çeken trend ise Physical AI kavramının MACHINA ve AUTONOMOUS gibi konferanslarla kendi başına bağımsız bir kategori haline gelmesi oldu.
Düşüncem: Physical AI artık sadece teorik makalelerde değil, doğrudan fuar salonlarında satılan ticari bir ürüne dönüştü. Yazılım kodlarımız artık sadece dijital pikselleri değil, tonlarca ağırlıktaki çelik kolları ve servo motorları yönetiyor. İnovasyonun Silikon Vadisi'nden çıkıp Asya ve Avrupa'daki üretim bantlarına kaydığını çok net görüyoruz. Otomasyondaki bu veri yığını, IIoT tarafında edge computing yatırımlarını da astronomik şekilde artıracaktır.
@@ -0,0 +1,179 @@
---
title: "Poyraz ile Yazılıma Dair #0309082026"
category: "Newsletter"
date: "2026-08-09"
readTime: "7 min read"
author: "Poyraz Avsever"
slug: "poyraz-ile-yazilima-dair-0309082026"
excerpt: "Bu hafta yapay zeka maliyet savaşlarından DeepMind liderlik değişimine, 100x geliştirici tartışmasından Figma dosya mimarisine ve teknoloji sektöründeki güç dengelerine kadar yoğun bir gündemimiz var."
coverImage: "/blog/images/poyraz-ile-yazilima-dair-0309082026-cover.png"
lang: "tr"
---
# Poyraz ile Yazılıma Dair #0309082026
3-9 Ağustos 2026 aralığından selamlar :)
Bu haftanın teknoloji gündeminde ortak bir tema öne çıkıyor: ölçek büyürken maliyetler, organizasyonlar ve güç dengeleri yeniden şekilleniyor.
Çinli yapay zeka laboratuvarları fiyat sınırlarını aşağı çekerken Google DeepMind tarihi bir liderlik değişimine gitti. Yazılım dünyası "100x geliştirici" kavramını tartışırken güvenlik ekipleri kritik ağ açıklarıyla uğraştı.
Figma dosya mimarisini değiştirdi, teknoloji sektöründeki işten çıkarmalar geçen yılın toplamını aştı ve oyun endüstrisinin en büyük markalarından biri 55 milyar dolarlık bir işlemle el değiştirdi.
Haftanın öne çıkan gelişmelerini ve bu gelişmelerin bize ne anlattığını birlikte inceleyelim.
## Yapay Zeka Gelişmeleri
### Çinli Yapay Zeka Laboratuvarlarından Fiyat ve Performans Hamlesi
Kaynak: Pakistan Today - Qwen3.8-Max ve DeepSeek V4-Flash | Alibaba Cloud
Alibaba, 2,4 trilyon parametreli Qwen3.8-Max modelini tanıttı. Mixture of Experts, yani "uzmanların karışımı" mimarisiyle çalışan model, her istekte toplam parametrelerinin yalnızca 95 milyarını etkinleştiriyor.
Bir milyon token bağlam penceresine sahip modelin, şirket içindeki bir yazılım projesinde 16 gün boyunca otonom çalıştığı belirtiliyor.
Aynı dönemde DeepSeek, V4-Flash modelinin fiyatını bir milyon girdi tokenı için 0,14 dolar, çıktı için ise 0,28 dolar olarak açıkladı. Artificial Analysis verilerine dayanan habere göre bu fiyat, bazı Amerikalı rakiplerin maliyetinin yüzde birine kadar düşüyor.
Yapay zeka yarışındaki en önemli değişim artık yalnızca "en güçlü modeli kim geliştirdi?" sorusuyla ilgili değil. Aynı muhakeme kapasitesini kimin daha ucuza sunduğu da belirleyici hale geliyor.
Model kullanım maliyetlerinin düşmesi; büyük doküman koleksiyonlarının işlenmesi, uzun süre çalışan yazılım ajanları ve daha önce ekonomik olmayan ürün fikirleri için önemli bir alan açacak. Ancak ucuz token tek başına yeterli değil. Güvenilirlik, değerlendirme sistemleri, veri güvenliği ve model yönetimi yeni rekabet alanları olacak.
### Google DeepMind'da Tarihi Liderlik Değişimi
Kaynak: The Guardian - Google DeepMind liderlik değişimi
DeepMind'ın kurucusu ve 16 yıllık CEO'su Demis Hassabis, günlük operasyonel sorumluluklarını bırakarak DeepMind Başkanı ve Alphabet Baş Bilim İnsanı görevlerine geçti. Operasyonel liderlik ise CTO Koray Kavukcuoğlu'na devredildi.
Aynı süreçte Google'ın deneyimli mühendislerinden Jeff Dean ve Sanjay Ghemawat, makine öğrenmesi, bilim ve mühendislik alanlarına odaklanacak Discovery Loop isimli yeni bir girişim kurmak üzere şirketten ayrıldı. Alphabet hisseleri gelişmelerin açıklandığı günü yüzde 4 düşüşle kapattı.
Bu değişimi yalnızca bir "beyin göçü" olarak okumak eksik kalır. Alphabet, bilimsel vizyon ile Gemini gibi büyük ölçekli ürünlerin operasyonel ihtiyaçlarını farklı liderlik katmanlarına ayırıyor olabilir.
Yine de Jeff Dean ve Sanjay Ghemawat gibi isimlerin ayrılması önemli bir sinyal. Yapay zeka çağında büyük şirketlerin en ciddi rakipleri yalnızca diğer teknoloji devleri değil; kendi içlerinden çıkabilecek küçük, hızlı ve araştırma odaklı ekipler olacak.
## Yazılım Gelişmeleri
### Yapay Zeka Çağında "100x Geliştirici" Efsanesi
Kaynak: Stack Overflow - Explorers, exploiters, and the myth of the 100x engineer
Stack Overflow'un analizinde, yapay zeka araçlarını erkenden benimseyerek sıra dışı üretkenlik artışları yakalayan geliştiriciler "kaşifler" olarak tanımlanıyor. Ekiplerin büyük bölümü ise yeni yöntemleri kendisi keşfetmek yerine, daha önce denenmiş ve güvenilir hale getirilmiş süreçleri kullanmayı tercih ediyor.
Yazıdaki temel fikir, "100x geliştirici" olarak görülen kişilerin doğuştan farklı olmadığı. Merak, uyum sağlama isteği ve deney yapma özgürlüğü, yapay zekayla birlikte daha görünür hale geliyor. Liderlerin görevi birkaç istisnai çalışan bulmak değil, onların keşiflerini bütün ekibin kullanabileceği yöntemlere dönüştürmek.
Kod üretim hızının tek başına başarı göstergesi olduğu dönem sona eriyor. Bir geliştiricinin yüz kat daha fazla kod yazması, ekibin yüz kat daha fazla değer ürettiği anlamına gelmiyor.
Asıl mesele; üretilen kodun test edilmesi, güvenliğinin doğrulanması ve sürdürülebilir şekilde canlıya alınması. Geleceğin güçlü mühendislik organizasyonları, birkaç "süper geliştiriciye" bağımlı olanlar değil; kaşiflerin öğrendiklerini standartlara, değerlendirme sistemlerine ve otomatik kalite kapılarına dönüştürebilenler olacak.
### Cisco'dan Kritik IOS XE Güvenlik Güncellemesi
Kaynak: Cisco Security Advisory - IOS XE Security Hardening Release
Cisco, IOS XE yazılımında şirket içi testlerle tespit edilen yedi güvenlik açığını kapatan kritik bir güvenlik sıkılaştırma sürümü yayımladı. Açıklar arasında yetkisiz komut çalıştırılmasına yol açabilecek ve CVSS puanı 9,8 olarak açıklanan CVE-2026-20272 de bulunuyor.
Cisco, güvenlik açıklarının aktif olarak kullanıldığına dair bir bulgu olmadığını belirtiyor. Bununla birlikte açıklar için geçici bir çözüm bulunmuyor; etkilenen sistemlerin düzeltilmiş yazılım sürümlerine geçirilmesi gerekiyor.
"Güncellemeyi gelecek bakım dönemine bırakalım" yaklaşımı, kritik altyapılar için giderek daha tehlikeli hale geliyor. Özellikle yönlendirici ve anahtar gibi ağın merkezindeki cihazlarda tek bir gecikme, bütün organizasyonun saldırı yüzeyini etkileyebilir.
Yama yönetimi artık yalnızca BT ekiplerinin manuel olarak takip ettiği bir operasyon olmamalı. Envanter çıkarma, sürüm kontrolü, risk önceliklendirme ve kademeli dağıtım süreçlerinin mümkün olduğunca otomatikleştirilmesi gerekiyor.
### MVP'nizin Bir Kubernetes Kümesine İhtiyacı Olmayabilir
Kaynak: Stack Overflow - Your MVP doesn't need a Kubernetes cluster
Stack Overflow Podcast'in Render CEO'su Anurag Goel'i ağırladığı bölümde, erken aşama girişimlerin neden Kubernetes ve karmaşık bulut altyapıları yöneterek başlamaması gerektiği tartışıldı.
Temel mesaj oldukça net: Henüz ürün-pazar uyumu bulunmamış bir girişimin sınırlı mühendislik kapasitesini altyapı yönetimine ayırması, asıl ürünün gelişimini yavaşlatabilir. Yönetilen servisler, çoğu MVP için daha hızlı ve ekonomik bir başlangıç sunuyor.
Yazılım dünyasının en pahalı alışkanlıklarından biri, bugün var olmayan ölçek problemlerini çözmeye çalışmak. Bir ürünün henüz yüz kullanıcısı yokken milyonlarca kullanıcıya göre mikroservis mimarisi tasarlamak, teknik hazırlık değil; çoğu zaman ertelenmiş ürün geliştirmedir.
Başlangıçta basit bir uygulama, yönetilen veritabanı ve güvenilir bir dağıtım hattı yeterli olabilir. Kubernetes bir başarı rozeti değil, belirli ölçekte ortaya çıkan ihtiyaçlara verilen güçlü ama maliyetli bir cevaptır.
## Tasarım Gelişmeleri
### Figma Dosya Mimarisini ve Yapay Zeka Harcamalarını Yeniden Düzenliyor
Kaynak: Figma - File management updates | Figma - Manage AI credits
Figma, 3 Ağustos itibarıyla "Projects" adını "Folders" olarak değiştirmeye başladı. Ücretli planlarda klasörler artık on seviyeye kadar iç içe oluşturulabiliyor. Klasör izinleri de üst klasörden devralınacak veya belirli kişilerle sınırlandırılacak şekilde sadeleştiriliyor.
Platform ayrıca yöneticilere çalışanların ücretli yapay zeka kredilerine erişimini yönetme imkanı sunuyor. Yöneticiler kullanıcı bazında tam erişim, özel aylık limit veya erişim kapatma seçeneklerini belirleyebiliyor; kredi taleplerini inceleyebiliyor ve kullanım miktarlarını takip edebiliyor.
Bu iki gelişme birlikte değerlendirildiğinde tasarım araçlarının geldiği nokta daha net görünüyor. Kurumsal tasarım dosyaları artık basit görsel çalışmalar değil; izinleri, hiyerarşisi ve bağımlılıkları bulunan büyük yazılım depolarına benziyor.
Yapay zeka kredilerinin kullanıcı bazında yönetilmesi ise tasarım süreçlerine bir tür "AI FinOps" yaklaşımının geldiğini gösteriyor. Ekipler yakında yalnızca hangi tasarımın daha iyi olduğunu değil, hangi yapay zeka işleminin maliyetine değdiğini de tartışacak.
### Tasarım Sistemlerinde Yapay Zeka Sapması: AI'ı Doğru Anda Döngüden Çıkarmak
Kaynak: TJ Pitre - Use AI to Need Less AI
Smashing Magazine'in haftalık seçkisinde de öne çıkarılan TJ Pitre imzalı analiz, yapay zekanın tasarım sistemlerini yorumlarken oluşturduğu "drift", yani tasarım ile kod arasındaki sapma problemine odaklanıyor.
Yazının önerisi, her kontrolü tekrar yapay zekaya bırakmak yerine tasarım kurallarını makine tarafından okunabilir sözleşmelere dönüştürmek. Renk tokenları, bileşen özellikleri ve izin verilen varyasyonlar gibi kesin kuralların her seferinde model tarafından yeniden yorumlanması yerine deterministik sistemlerle uygulanması savunuluyor.
Yapay zeka yaratıcı seçenekler üretmekte güçlü; ancak her seferinde aynı kurala eksiksiz uyması gereken alanlarda hala kırılgan. Piksel hassasiyetine dayanan ürünlerde küçük bir sapma bile bütün bileşen sistemine yayılan tutarsızlıklar yaratabiliyor.
Tasarımcıların gelecekteki rolü yalnızca iyi komut yazmak olmayacak. Hangi kararların yapay zekaya bırakılabileceğini, hangilerinin ise değişmez sistem kurallarıyla korunması gerektiğini belirlemek çok daha değerli bir yetkinlik olacak.
## Teknoloji Haberleri
### 2026'daki Teknoloji İşten Çıkarmaları Geçen Yılın Toplamını Aştı
Kaynak: Fast Company - Tech layoffs August 2026 update
Ağustos ayının ilk haftasında Zillow 500'den fazla, TikTok 250, Etsy yaklaşık 220 ve Google 52 kişiyi etkileyen işten çıkarma kararları açıkladı.
Layoffs.fyi verilerine göre 6 Ağustos itibarıyla 2026'daki teknoloji sektörü iş kaybı 125.759'a ulaştı. Böylece 2025'in tamamında kaydedilen 122.606 kişilik toplam henüz yıl bitmeden aşılmış oldu.
Bu tabloyu yalnızca "yapay zeka insanların işini alıyor" şeklinde okumak fazla basit. Bence daha büyük değişim, şirket sermayesinin yön değiştirmesi. Kurumlar veri merkezlerine, GPU'lara ve yapay zeka altyapısına milyarlarca dolar ayırırken operasyonel giderlerini daha sert biçimde sorguluyor.
Bu durum, yapay zekanın iş kayıplarıyla ilgisiz olduğu anlamına gelmiyor. Etki her zaman bir çalışanın doğrudan bir modelle değiştirilmesi şeklinde ortaya çıkmıyor; yatırım bütçelerinin insan kaynağından altyapıya kaydırılması da aynı dönüşümün parçası.
### Suudi Arabistan Öncülüğündeki Konsorsiyum EA'i 55 Milyar Dolara Satın Aldı
Kaynak: SEPE - Saudi-led group completes $55bn purchase of EA
Suudi Arabistan Kamu Yatırım Fonu öncülüğündeki konsorsiyum, Electronic Arts'ın 55 milyar dolarlık satın alma işlemini tamamladı. The Sims, Battlefield ve EA Sports FC gibi markaların sahibi olan şirket, işlem sonucunda borsadan çıkarılarak özel mülkiyete geçti.
Satın alma, EA'in bilançosuna önemli miktarda borç yükleyen kaldıraçlı bir işlem niteliği taşıyor.
Oyun sektörü artık yalnızca eğlence üreten bir pazar değil. Küresel kültüre, genç kitlelere ve dijital dağıtım kanallarına erişim sağlayan stratejik bir güç alanı.
Suudi Arabistan'ın Vizyon 2030 kapsamında oyun sektörüne yaptığı yatırımlar, petrol dışı ekonomiye geçişin yanında uzun vadeli bir kültürel etki stratejisi olarak da okunmalı. Bundan sonraki kritik konu, yeni mülkiyet yapısının EA'in yaratıcı kararlarına ve sahip olduğu küresel markalara nasıl yansıyacağı olacak.
### Türkiye, Suudi Arabistan ve Pakistan Arasında Savunma Teknolojisi İş Birliği
Kaynak: ShiftDelete.Net - Mekke Anlaşması
Türkiye, Suudi Arabistan ve Pakistan, 7 Ağustos'ta Mekke'de üçlü bir savunma anlaşması imzaladı. Anlaşma, taraflardan birine yönelik silahlı saldırının tüm taraflara yapılmış kabul edilmesinin yanında ortak savunma teknolojileri geliştirilmesini ve askeri unsurların birlikte çalışabilmesini hedefliyor.
Bu düzeyde bir entegrasyonun ortak veri bağı standartları, uyumlu komuta-kontrol yazılımları ve dost-düşman tanıma sistemleri gibi teknik altyapılara ihtiyaç duyacağı değerlendiriliyor. AKINCI'nın Suudi Arabistan'da yerelleştirilmesi ve KAAN için daha önce gündeme gelen ortak yatırım seçeneği de iş birliğinin teknolojik zeminini güçlendiriyor.
Modern savunma ittifakları yalnızca imzalanan belgelerde değil, kullanılan yazılım protokollerinde kuruluyor. İki ordunun aynı veriyi güvenli biçimde paylaşabilmesi ve aynı operasyonel resmi görebilmesi, siyasi açıklamalardan daha kalıcı bir bağ oluşturabilir.
Ancak birlikte çalışabilirlik aynı zamanda teknolojik bağımlılık yaratır. Bu nedenle standartların kim tarafından belirlendiği, verinin nerede tutulduğu ve kritik yazılımların mülkiyeti en az ortak üretim kadar önemli olacak.
## Haftanın Açık Kaynak Radarı
### Qwen3.8-27B: Yerel Yapay Zeka İçin Yeni Bir Aday
Kaynak: LOG - Qwen3.8-Max ve Qwen3.8-27B | Alibaba Cloud
Alibaba, Qwen3.8-Max ile birlikte daha küçük Qwen3.8-27B modelinin ağırlıklarını da açık olarak yayımlayacağını duyurdu. Modellerin Hugging Face ve ModelScope üzerinden paylaşılması bekleniyor.
Burada önemli bir ayrıntı var: 9 Ağustos itibarıyla Qwen3.8-27B henüz indirilebilir durumda değil; açık ağırlıkların takip eden hafta yayımlanacağı açıklandı. Lisans koşulları kesinleşmeden modeli teknik anlamda tamamen "açık kaynak" olarak tanımlamak yerine "açık ağırlıklı" demek daha doğru.
27 milyar parametre sınıfındaki güçlü bir modelin şirket içinde veya yerel donanımda çalıştırılabilmesi; veri gizliliği, maliyet kontrolü ve sağlayıcı bağımsızlığı açısından ciddi değer taşıyor.
Kapalı API'lerde fiyat, kullanım politikası veya erişim koşulları tek taraflı değişebilir. Açık ağırlıklar ise ekiplerin modeli kendi altyapılarında değerlendirmesine ve özelleştirmesine imkan verir. Yine de modelin gerçek değeri; yayımlandıktan sonra lisansı, donanım ihtiyacı ve bağımsız test sonuçları görüldüğünde anlaşılacak.
## Haftanın Genel Okuması
Bu haftanın haberleri bize üç büyük değişimi gösteriyor.
Birincisi, yapay zeka kapasitesi hızla ucuzluyor. Rekabet artık yalnızca model kalitesi üzerinden değil, kullanım maliyeti ve erişilebilirlik üzerinden de ilerliyor.
İkincisi, yazılım ve tasarım ekiplerinde üretimden doğrulamaya doğru bir güç kayması yaşanıyor. Kod veya arayüz üretmek kolaylaşırken güvenilirlik, test, standart ve yönetişim daha değerli hale geliyor.
Üçüncüsü, teknoloji giderek daha fazla jeopolitik bir varlığa dönüşüyor. Yapay zeka modellerinden oyun şirketlerine, veri bağlarından tasarım araçlarına kadar teknolojik altyapı; ekonomik ve siyasi gücün merkezinde yer alıyor.
Sizce bu haftanın en önemli gelişmesi hangisiydi?
@@ -0,0 +1,179 @@
---
title: "Software with Poyraz #0309082026"
category: "Newsletter"
date: "2026-08-09"
readTime: "7 min read"
author: "Poyraz Avsever"
slug: "software-with-poyraz-0309082026"
excerpt: "This week, we have a packed agenda, from AI cost competition and DeepMind's leadership shift to the 100x developer debate, Figma's file architecture updates, and changing power dynamics in tech."
coverImage: "/blog/images/poyraz-ile-yazilima-dair-0309082026-cover.png"
lang: "en"
---
# Software with Poyraz #0309082026
Hello from the week of August 3-9, 2026 :)
One common theme stands out in this week's technology agenda: as scale grows, costs, organizations, and power balances are being reshaped.
Chinese AI labs pushed pricing boundaries lower while Google DeepMind went through a historic leadership change. The software world debated the idea of the "100x developer" while security teams dealt with critical network vulnerabilities.
Figma changed its file architecture, tech layoffs passed last year's total before the year ended, and one of the biggest brands in the gaming industry changed hands through a 55 billion dollar transaction.
Let's look at the week's most important developments and what they tell us.
## Artificial Intelligence Developments
### Chinese AI Labs Push on Price and Performance
Source: Pakistan Today - Qwen3.8-Max and DeepSeek V4-Flash | Alibaba Cloud
Alibaba introduced Qwen3.8-Max, a 2.4 trillion parameter model. The model uses a Mixture of Experts architecture and activates only 95 billion of its total parameters for each request.
It is also reported to have a one million token context window and to have worked autonomously for 16 days on an internal software project.
Around the same period, DeepSeek announced V4-Flash pricing at 0.14 dollars per million input tokens and 0.28 dollars per million output tokens. According to reporting based on Artificial Analysis data, that price can be as low as one percent of some American competitors' costs.
The biggest shift in the AI race is no longer only about asking, "Who built the strongest model?" The question of who can offer the same reasoning capacity more cheaply is becoming just as decisive.
Lower model usage costs will open space for processing large document collections, running long-lived software agents, and building product ideas that previously did not make economic sense. But cheap tokens alone are not enough. Reliability, evaluation systems, data security, and model governance will become the new competitive fronts.
### A Historic Leadership Change at Google DeepMind
Source: The Guardian - Google DeepMind leadership change
Demis Hassabis, DeepMind's founder and CEO of 16 years, stepped away from daily operational responsibilities and moved into the roles of DeepMind Chair and Alphabet Chief Scientist. Operational leadership was handed over to CTO Koray Kavukcuoglu.
During the same period, longtime Google engineers Jeff Dean and Sanjay Ghemawat left the company to start a new venture called Discovery Loop, focused on machine learning, science, and engineering. Alphabet shares closed the day of the announcement down 4 percent.
Reading this only as a case of "brain drain" would be incomplete. Alphabet may be separating scientific vision from the operational needs of large-scale products such as Gemini by placing them into different leadership layers.
Still, the departure of names like Jeff Dean and Sanjay Ghemawat is an important signal. In the AI era, the biggest competitors of large companies will not only be other technology giants; they will also be the small, fast, research-focused teams that can emerge from within.
## Software Developments
### The Myth of the "100x Developer" in the AI Era
Source: Stack Overflow - Explorers, exploiters, and the myth of the 100x engineer
Stack Overflow's analysis describes developers who adopt AI tools early and achieve unusual productivity gains as "explorers." Most teams, however, prefer to use workflows that have already been tested and made reliable instead of discovering new methods themselves.
The core idea is that people seen as "100x developers" are not fundamentally different by birth. Curiosity, willingness to adapt, and freedom to experiment become more visible with AI. The job of leaders is not to find a few exceptional employees, but to turn their discoveries into methods the whole team can use.
The era in which code production speed alone was treated as a success metric is ending. A developer writing one hundred times more code does not mean the team is producing one hundred times more value.
The real issue is whether the code being produced is tested, secured, and shipped sustainably. The strongest engineering organizations of the future will not be the ones dependent on a few "super developers"; they will be the ones that can turn explorers' lessons into standards, evaluation systems, and automated quality gates.
### Cisco Releases a Critical IOS XE Security Update
Source: Cisco Security Advisory - IOS XE Security Hardening Release
Cisco released a critical security hardening update that fixes seven vulnerabilities found through internal testing in IOS XE software. The flaws include CVE-2026-20272, a vulnerability with a CVSS score of 9.8 that could allow unauthorized command execution.
Cisco says it has not found evidence that the vulnerabilities are being actively exploited. However, there is no workaround for the flaws; affected systems need to be moved to fixed software releases.
The habit of saying "let's leave the update for the next maintenance window" is becoming increasingly dangerous for critical infrastructure. Especially in devices at the center of the network, such as routers and switches, a single delay can affect the attack surface of an entire organization.
Patch management should no longer be an operation manually tracked only by IT teams. Inventory discovery, version control, risk prioritization, and staged rollout processes need to be automated as much as possible.
### Your MVP May Not Need a Kubernetes Cluster
Source: Stack Overflow - Your MVP doesn't need a Kubernetes cluster
In a Stack Overflow Podcast episode featuring Render CEO Anurag Goel, the discussion focused on why early-stage startups should not begin by managing Kubernetes and complex cloud infrastructure.
The message is quite clear: if a startup has not yet found product-market fit, spending limited engineering capacity on infrastructure management can slow down the actual product. Managed services offer a faster and more economical starting point for most MVPs.
One of the most expensive habits in software is trying to solve scale problems that do not exist yet. Designing a microservice architecture for millions of users when the product does not yet have one hundred users is often not technical preparation; it is delayed product development.
At the beginning, a simple application, a managed database, and a reliable deployment pipeline may be enough. Kubernetes is not a badge of success. It is a powerful but costly answer to needs that appear at a certain scale.
## Design Developments
### Figma Reorganizes File Architecture and AI Spending
Source: Figma - File management updates | Figma - Manage AI credits
As of August 3, Figma started renaming "Projects" to "Folders." On paid plans, folders can now be nested up to ten levels deep. Folder permissions are also being simplified so they can either inherit from the parent folder or be restricted to specific people.
The platform also gives administrators the ability to manage employees' access to paid AI credits. Admins can set full access, custom monthly limits, or disable access per user; they can also review credit requests and track usage amounts.
When these two updates are read together, the current direction of design tools becomes clearer. Enterprise design files are no longer simple visual documents; they increasingly resemble large software repositories with permissions, hierarchy, and dependencies.
Managing AI credits per user also shows that an "AI FinOps" mindset is entering design workflows. Teams will soon debate not only which design is better, but also which AI operation is worth its cost.
### AI Drift in Design Systems: Taking AI Out of the Loop at the Right Time
Source: TJ Pitre - Use AI to Need Less AI
An analysis by TJ Pitre, also highlighted in Smashing Magazine's weekly selection, focuses on the problem of "drift" created when AI interprets design systems. In this context, drift means the gap that can emerge between design and code.
The article argues that instead of leaving every check to AI again, design rules should be turned into machine-readable contracts. Color tokens, component properties, and allowed variants should be enforced by deterministic systems rather than reinterpreted by a model every time.
AI is strong at generating creative options, but it is still fragile in areas where the same rule must be followed exactly every time. In products that depend on pixel-level precision, even a small deviation can spread inconsistency across the whole component system.
The future role of designers will not only be writing good prompts. Deciding which decisions can be left to AI and which must be protected by fixed system rules will become a much more valuable skill.
## Technology News
### 2026 Tech Layoffs Have Already Passed Last Year's Total
Source: Fast Company - Tech layoffs August 2026 update
In the first week of August, Zillow announced layoffs affecting more than 500 people, TikTok 250, Etsy around 220, and Google 52.
According to Layoffs.fyi data, technology sector job losses in 2026 reached 125,759 as of August 6. That means the 122,606 total recorded across all of 2025 has already been passed before the end of the year.
Reading this only as "AI is taking people's jobs" is too simple. I think the bigger change is that company capital is being redirected. As organizations allocate billions of dollars to data centers, GPUs, and AI infrastructure, they are questioning operating expenses more aggressively.
This does not mean AI has nothing to do with job losses. The effect does not always appear as one employee being directly replaced by one model; investment budgets shifting from human labor to infrastructure is also part of the same transformation.
### A Saudi-Led Consortium Buys EA for 55 Billion Dollars
Source: SEPE - Saudi-led group completes $55bn purchase of EA
A consortium led by Saudi Arabia's Public Investment Fund completed the 55 billion dollar acquisition of Electronic Arts. The company behind brands such as The Sims, Battlefield, and EA Sports FC was taken private as a result of the transaction.
The acquisition is a leveraged deal that places a significant amount of debt on EA's balance sheet.
The gaming industry is no longer only a market that produces entertainment. It is a strategic power area that provides access to global culture, young audiences, and digital distribution channels.
Saudi Arabia's investments in gaming under Vision 2030 can be read not only as part of the transition away from an oil-based economy, but also as a long-term cultural influence strategy. The critical question from here is how the new ownership structure will affect EA's creative decisions and global brands.
### Defense Technology Cooperation Between Turkey, Saudi Arabia, and Pakistan
Source: ShiftDelete.Net - Mecca Agreement
Turkey, Saudi Arabia, and Pakistan signed a trilateral defense agreement in Mecca on August 7. In addition to treating an armed attack against one party as an attack against all parties, the agreement aims to develop shared defense technologies and enable military interoperability.
This level of integration is expected to require technical infrastructure such as common data link standards, compatible command-and-control software, and identification friend or foe systems. The localization of AKINCI in Saudi Arabia and the previously discussed joint investment option for KAAN also strengthen the technological foundation of the cooperation.
Modern defense alliances are built not only through signed documents, but also through software protocols. If two armies can securely share the same data and see the same operational picture, that can create a more lasting bond than political statements.
But interoperability also creates technological dependency. That is why who defines the standards, where the data is stored, and who owns the critical software will be at least as important as joint production.
## Open Source Radar of the Week
### Qwen3.8-27B: A New Candidate for Local AI
Source: LOG - Qwen3.8-Max and Qwen3.8-27B | Alibaba Cloud
Alibaba announced that, alongside Qwen3.8-Max, it will also publish the weights of the smaller Qwen3.8-27B model openly. The models are expected to be shared through Hugging Face and ModelScope.
There is an important detail here: as of August 9, Qwen3.8-27B is not yet available for download; the open weights are expected to be released the following week. Before the license terms are finalized, it is more accurate to describe the model as "open-weight" rather than fully "open source" in the technical sense.
The ability to run a strong model in the 27 billion parameter class inside a company or on local hardware creates serious value for data privacy, cost control, and provider independence.
With closed APIs, pricing, usage policy, or access conditions can change unilaterally. Open weights allow teams to evaluate and customize the model on their own infrastructure. Still, the model's real value will only become clear after release, once its license, hardware requirements, and independent benchmark results are visible.
## This Week's Bigger Picture
This week's news points to three major shifts.
First, AI capacity is getting cheaper quickly. Competition is no longer moving only through model quality, but also through usage cost and accessibility.
Second, software and design teams are seeing a power shift from production to verification. As producing code or interfaces becomes easier, reliability, testing, standards, and governance become more valuable.
Third, technology is increasingly becoming a geopolitical asset. From AI models to gaming companies, from data links to design tools, technological infrastructure sits at the center of economic and political power.
Which development do you think was the most important one this week?
+129
View File
@@ -0,0 +1,129 @@
import "server-only";
import fs from "node:fs/promises";
import path from "node:path";
import matter from "gray-matter";
export type AnimationSourceLocale = "tr" | "en";
export type AnimationSource = {
slug: string;
title: string;
excerpt: string;
coverImage: string;
platform: string;
tools: string[];
date: string;
author: string;
markdown: string;
lang: AnimationSourceLocale;
};
const ANIMATION_SOURCES_DIR = path.join(
process.cwd(),
"content",
"animation-sources",
);
function toSafeString(value: unknown, fallback = "") {
if (typeof value !== "string") return fallback;
const trimmed = value.trim();
return trimmed || fallback;
}
function toStringArray(value: unknown) {
if (Array.isArray(value)) {
return value
.filter((item): item is string => typeof item === "string")
.map((item) => item.trim())
.filter(Boolean);
}
if (typeof value === "string") {
return value
.split(",")
.map((item) => item.trim())
.filter(Boolean);
}
return [];
}
function normalizeFileSlug(fileName: string) {
return fileName.replace(/\.(tr|en)\.md$/i, "").replace(/\.md$/i, "");
}
function normalizeLocale(value: unknown): AnimationSourceLocale {
return value === "en" ? "en" : "tr";
}
function mapMarkdownToAnimationSource(
fileName: string,
raw: string,
): AnimationSource {
const parsed = matter(raw);
const fallbackSlug = normalizeFileSlug(fileName);
return {
slug: toSafeString(parsed.data.slug, fallbackSlug),
title: toSafeString(parsed.data.title, fallbackSlug),
excerpt: toSafeString(parsed.data.excerpt),
coverImage: toSafeString(
parsed.data.coverImage,
"/media/cursor-portrait/poyraz-bottom-right-poster.webp",
),
platform: toSafeString(parsed.data.platform, "Web"),
tools: toStringArray(parsed.data.tools),
date: toSafeString(parsed.data.date),
author: toSafeString(parsed.data.author, "Poyraz Avsever"),
markdown: parsed.content.trim(),
lang: normalizeLocale(parsed.data.lang),
};
}
function toTimestamp(value: string) {
const timestamp = Date.parse(value);
return Number.isNaN(timestamp) ? 0 : timestamp;
}
export async function listAnimationSources(
locale?: string,
): Promise<AnimationSource[]> {
let files: string[];
try {
files = await fs.readdir(ANIMATION_SOURCES_DIR);
} catch {
return [];
}
const sources = await Promise.all(
files
.filter((fileName) => fileName.endsWith(".md"))
.map(async (fileName) => {
const raw = await fs.readFile(
path.join(ANIMATION_SOURCES_DIR, fileName),
"utf8",
);
return mapMarkdownToAnimationSource(fileName, raw);
}),
);
return sources
.filter((source) => !locale || source.lang === locale)
.sort((a, b) => {
const dateDifference = toTimestamp(b.date) - toTimestamp(a.date);
return dateDifference || a.title.localeCompare(b.title);
});
}
export async function getAnimationSourceBySlug(
slug: string,
locale: string,
): Promise<AnimationSource | null> {
const safeSlug = slug.trim().toLowerCase();
if (!safeSlug) return null;
const sources = await listAnimationSources(locale);
return sources.find((source) => source.slug.toLowerCase() === safeSlug) ?? null;
}
+21
View File
@@ -21,4 +21,25 @@ export const SPONSORS: Sponsor[] = [
logo: "/sponsors/testsprite.png", logo: "/sponsors/testsprite.png",
websiteUrl: "https://testsprite.com", websiteUrl: "https://testsprite.com",
}, },
{
id: "minimax",
name: "MiniMax",
job: "AI Video & Model Platformu",
logo: "/sponsors/minimax.png",
websiteUrl: "https://www.minimax.io",
},
{
id: "higgsfield",
name: "Higgsfield",
job: "AI Creative Suite",
logo: "/sponsors/higgsfield.png",
websiteUrl: "https://higgsfield.ai",
},
{
id: "hosting-dunyam",
name: "Hosting Dünyam",
job: "Hosting Sağlayıcısı",
logo: "/sponsors/hosting-dunyam.png",
websiteUrl: "https://hostingdunyam.com",
},
]; ];
+56 -2
View File
@@ -10,7 +10,7 @@ import {
import { REFERENCES } from "@/data/references"; import { REFERENCES } from "@/data/references";
import { VOLUNTEER_COMMUNITY_ITEMS } from "@/data/volunteer-community"; import { VOLUNTEER_COMMUNITY_ITEMS } from "@/data/volunteer-community";
import { YOUTUBE_VIDEO_LINKS } from "@/data/youtube-videos"; import { YOUTUBE_VIDEO_LINKS } from "@/data/youtube-videos";
import { NAV_LINKS, SOCIAL_LINKS } from "@/lib/links"; import { NAV_DROPDOWN_GROUPS, NAV_LINKS, SOCIAL_LINKS } from "@/lib/links";
import { getLocalizedValue } from "@/lib/locale"; import { getLocalizedValue } from "@/lib/locale";
export type CommandPaletteItem = { export type CommandPaletteItem = {
@@ -28,10 +28,19 @@ export type CommandPaletteGroup = {
items: CommandPaletteItem[]; items: CommandPaletteItem[];
}; };
export type AnimationSourceSearchItem = {
slug: string;
title: string;
excerpt: string;
platform: string;
tools: string[];
};
export function getCommandPaletteGroups( export function getCommandPaletteGroups(
locale: string, locale: string,
tLinks: (key: string) => string, tLinks: (key: string) => string,
tNav: { (key: string): string; has: (key: string) => boolean } tNav: { (key: string): string; has: (key: string) => boolean },
animationSources: AnimationSourceSearchItem[] = [],
): CommandPaletteGroup[] { ): CommandPaletteGroup[] {
const navigationItems: CommandPaletteItem[] = NAV_LINKS.map((item) => { const navigationItems: CommandPaletteItem[] = NAV_LINKS.map((item) => {
const label = tNav.has(item.id) ? tNav(item.id) : item.label; const label = tNav.has(item.id) ? tNav(item.id) : item.label;
@@ -62,6 +71,39 @@ export function getCommandPaletteGroups(
], ],
})); }));
const dropdownGroups: CommandPaletteGroup[] = NAV_DROPDOWN_GROUPS.map(
(group) => ({
id: `navigation-${group.id}`,
heading: tNav.has(group.id) ? tNav(group.id) : group.label,
items: group.items.map((item) => ({
id: item.id,
label: tNav.has(item.id) ? tNav(item.id) : item.label,
href: item.href,
icon: item.icon,
external: item.external,
keywords: [group.label, item.label, ...item.keywords],
})),
}),
);
const animationSourceItems: CommandPaletteItem[] = animationSources.map(
(source) => ({
id: `animation-source-${source.slug}`,
label: source.title,
href: `/animation-sources/${source.slug}`,
icon: "mdi:motion-play-outline",
keywords: [
source.excerpt,
source.platform,
...source.tools,
"animation",
"prompt",
locale === "tr" ? "animasyon" : "motion",
locale === "tr" ? "kaynak" : "source",
],
}),
);
const blogItems: CommandPaletteItem[] = [ const blogItems: CommandPaletteItem[] = [
{ {
id: "blog-index", id: "blog-index",
@@ -281,6 +323,18 @@ export function getCommandPaletteGroups(
heading: locale === "tr" ? "İçerikler" : "Contents", heading: locale === "tr" ? "İçerikler" : "Contents",
items: contentItems, items: contentItems,
}, },
...dropdownGroups,
...(animationSourceItems.length > 0
? [
{
id: "animation-sources-data",
heading: tNav.has("animationResources")
? tNav("animationResources")
: "Animation Sources",
items: animationSourceItems,
},
]
: []),
{ {
id: "social", id: "social",
heading: "Social", heading: "Social",
+16
View File
@@ -0,0 +1,16 @@
export const TOTAL_DURATION = 4;
export const ACTIVE_START = 0.25;
export const ACTIVE_END = 3.75;
export const DEFAULT_TIME = 2;
export const SMOOTHING = 0.12;
export function clamp(value: number, minimum: number, maximum: number) {
return Math.min(Math.max(value, minimum), maximum);
}
export function mapPointerYToTime(pointerY: number, viewportHeight: number) {
if (viewportHeight <= 0) return DEFAULT_TIME;
const progress = clamp(1 - pointerY / viewportHeight, 0, 1);
return ACTIVE_START + progress * (ACTIVE_END - ACTIVE_START);
}
+19
View File
@@ -70,6 +70,25 @@ export const SOCIAL_LINKS = [
}, },
] as const; ] as const;
export const NAV_DROPDOWN_GROUPS = [
{
id: "others",
label: "Diğerleri",
icon: "mdi:dots-horizontal",
insertAfter: "gallery",
items: [
{
id: "animationResources",
label: "Animasyon Kaynakları",
href: "/animation-sources",
icon: "mdi:motion-play-outline",
external: false,
keywords: ["animasyon", "animation", "kaynak", "resource", "motion"],
},
],
},
] as const;
export const TOP_ICON_LINKS = [ export const TOP_ICON_LINKS = [
{ {
id: "ui-kit", id: "ui-kit",
+45
View File
@@ -0,0 +1,45 @@
export type MarkdownHeading = {
id: string;
text: string;
level: 2 | 3;
};
export function cleanMarkdownHeading(text: string) {
return text
.replace(/\[([^\]]+)\]\([^\)]+\)/g, "$1")
.replace(/[*_~`]/g, "")
.trim();
}
export function slugifyMarkdownHeading(text: string) {
return cleanMarkdownHeading(text)
.toLocaleLowerCase("tr-TR")
.normalize("NFKD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/ı/g, "i")
.replace(/[^a-z0-9\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "");
}
export function parseMarkdownHeadings(markdown: string): MarkdownHeading[] {
const headings: MarkdownHeading[] = [];
for (const line of markdown.split("\n")) {
const match = /^(#{2,3})\s+(.+)$/.exec(line.trim());
if (!match) continue;
const text = cleanMarkdownHeading(match[2]);
const id = slugifyMarkdownHeading(text);
if (!id || !text) continue;
headings.push({
id,
text,
level: match[1].length as 2 | 3,
});
}
return headings;
}
+13
View File
@@ -9,6 +9,9 @@
"search": "Search", "search": "Search",
"social": "Social", "social": "Social",
"socialLinks": "Social Links", "socialLinks": "Social Links",
"others": "Others",
"animationResources": "Animation Resources",
"backToMenu": "Back to menu",
"menu": "Menu", "menu": "Menu",
"mobileMenu": "Mobile Menu", "mobileMenu": "Mobile Menu",
"resume": "Resume" "resume": "Resume"
@@ -82,6 +85,16 @@
"diagramLoading": "Diagram rendering...", "diagramLoading": "Diagram rendering...",
"diagramError": "Could not render Mermaid diagram." "diagramError": "Could not render Mermaid diagram."
}, },
"AnimationSources": {
"title": "Animation Sources",
"description": "Production notes, prompts, and reusable code from animations I create for web and mobile products.",
"empty": "No animation sources have been added yet.",
"back": "Back to animation sources",
"toc": "Table of Contents",
"closeToc": "Close table of contents",
"copy": "Copy",
"copied": "Copied"
},
"Links": { "Links": {
"desc": "Here you can find all my social accounts, portfolio pages, and quick access links in one place. Open and share directly.", "desc": "Here you can find all my social accounts, portfolio pages, and quick access links in one place. Open and share directly.",
"allLinks": "All Links", "allLinks": "All Links",
+13
View File
@@ -9,6 +9,9 @@
"search": "Ara", "search": "Ara",
"social": "Sosyal", "social": "Sosyal",
"socialLinks": "Sosyal Bağlantılar", "socialLinks": "Sosyal Bağlantılar",
"others": "Diğerleri",
"animationResources": "Animasyon Kaynakları",
"backToMenu": "Menüye dön",
"menu": "Menü", "menu": "Menü",
"mobileMenu": "Mobil Menü", "mobileMenu": "Mobil Menü",
"resume": "Özgeçmiş" "resume": "Özgeçmiş"
@@ -82,6 +85,16 @@
"diagramLoading": "Diyagram hazırlanıyor...", "diagramLoading": "Diyagram hazırlanıyor...",
"diagramError": "Mermaid diyagramı oluşturulamadı." "diagramError": "Mermaid diyagramı oluşturulamadı."
}, },
"AnimationSources": {
"title": "Animasyon Kaynakları",
"description": "Web ve mobil ürünler için hazırladığım animasyonların üretim süreçleri, promptları ve uygulanabilir kodları.",
"empty": "Henüz animasyon kaynağı eklenmedi.",
"back": "Animasyon kaynaklarına dön",
"toc": "İçindekiler",
"closeToc": "İçindekileri kapat",
"copy": "Kopyala",
"copied": "Kopyalandı"
},
"Links": { "Links": {
"desc": "Burada sosyal hesaplarım, portfolyo sayfalarım ve hızlı erişim linklerimin tamamı tek yerde duruyor. Direkt açıp paylaşabilirsin.", "desc": "Burada sosyal hesaplarım, portfolyo sayfalarım ve hızlı erişim linklerimin tamamı tek yerde duruyor. Direkt açıp paylaşabilirsin.",
"allLinks": "Tüm Linkler", "allLinks": "Tüm Linkler",
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB