style(shorts): monochrome flat design — no gradients, no purple

User directive: no gradient, no purple (consistent with GMW black-white
monochrome theme). Full redesign of parts.tsx:
- Solid near-black scene backgrounds (5 subtle variants: circle, plus,
  barcode, rings, diagonal motifs as faint white SVG line art)
- GlassCard → flat white card (black text) — high contrast inversion
- CaptionKaraoke: black on white card, active sentence full-opacity
- KineticTitle: white on black; accent word = inverted white block w/ black ink
- StatCounter: big number framed by 2 offset rectangle outlines (editorial)
- BrandTag: white outline pill (flat), ProgressBar: solid white fill
- Removed ALL color props/palettes; removed gradient backgrounds/orbs
Perf: lighter than v2 (7.1MB vs 10.3MB); ~2min render per 32s short
This commit is contained in:
MythEclipse
2026-09-22 21:17:14 +07:00
parent 1db00c4182
commit 4a61766ee4
2 changed files with 155 additions and 115 deletions
+2 -2
View File
@@ -81,7 +81,7 @@ const Scene: React.FC<{
<AbsoluteFill> <AbsoluteFill>
<SvgBackground variant={index} /> <SvgBackground variant={index} />
<SlideMotion> <SlideMotion>
{label ? <BrandTag label={label} variant={index} /> : null} {label ? <BrandTag label={label} /> : null}
{scene === "stat" && stat ? ( {scene === "stat" && stat ? (
<StatCounter value={stat.value} label={stat.label} /> <StatCounter value={stat.value} label={stat.label} />
@@ -106,7 +106,7 @@ const Scene: React.FC<{
</> </>
)} )}
<ProgressBar total={duration} variant={index} /> <ProgressBar total={duration} />
{/* time code (subtle, right side) */} {/* time code (subtle, right side) */}
<div <div
style={{ style={{
+153 -113
View File
@@ -10,69 +10,102 @@ import "@fontsource/inter/700.css";
import "@fontsource/montserrat/800.css"; import "@fontsource/montserrat/800.css";
/* ───────────────────────────────────────────────────────────────────────── /* ─────────────────────────────────────────────────────────────────────────
Shorts design system. Shorts design system — MONOCHROME FLAT.
Fonts: Inter 700 (captions/body), Montserrat 800 (display). No gradients, no purple. Black & white, flat shapes, bold typography.
Fonts: Montserrat 800 (display), Inter 700 (captions/body).
Safe zone: 900×1400 centered (universal TikTok/Reels/Shorts). Safe zone: 900×1400 centered (universal TikTok/Reels/Shorts).
Perf rule: NO blur filters / SVG noise (software Chrome ~10x slower). Perf rule: NO blur / SVG noise filters (software Chrome ~10x slower).
───────────────────────────────────────────────────────────────────────── */ ───────────────────────────────────────────────────────────────────────── */
export const FONT_DISPLAY = "Montserrat, Inter, system-ui, sans-serif"; export const FONT_DISPLAY = "Montserrat, Inter, system-ui, sans-serif";
export const FONT_BODY = "Inter, system-ui, sans-serif"; export const FONT_BODY = "Inter, system-ui, sans-serif";
/** Per-segment background palette (variety between scenes). */ /** Solid near-black scene backgrounds (subtle variety, still monochrome). */
const PALETTES = [ const SCENES = [
{ base: "linear-gradient(155deg,#0b1020 0%,#141a33 45%,#1b1030 100%)", a1: "99,102,241", a2: "236,72,153" }, { bg: "#0d0d0f", motif: "circles" }, // concentric ring
{ base: "linear-gradient(155deg,#0a1628 0%,#0e2a3a 45%,#0a1f2e 100%)", a1: "56,189,248", a2: "16,185,129" }, { bg: "#121216", motif: "plus" }, // plus marks
{ base: "linear-gradient(155deg,#160b20 0%,#2a1040 45%,#1b0a2e 100%)", a1: "168,85,247", a2: "244,114,182" }, { bg: "#101014", motif: "barcode" }, // vertical bars
{ base: "linear-gradient(155deg,#1a0f0b 0%,#2e1a12 45%,#21100a 100%)", a1: "251,146,60", a2: "239,68,68" }, { bg: "#15151a", motif: "rings" }, // target rings
{ bg: "#0f0f12", motif: "diag" }, // diagonal ticks
]; ];
/** Animated background: gradient + drifting pre-softened orbs + dot grid. */ /** Flat mono background: solid color + faint white SVG motif per scene. */
export const SvgBackground: React.FC<{ variant?: number }> = ({ variant = 0 }) => { export const SvgBackground: React.FC<{ variant?: number }> = ({ variant = 0 }) => {
const frame = useCurrentFrame();
const { width, height } = useVideoConfig(); const { width, height } = useVideoConfig();
const frame = useCurrentFrame();
const t = frame / 60; const t = frame / 60;
const p = PALETTES[variant % PALETTES.length]; const s = SCENES[variant % SCENES.length];
const stroke = "rgba(255,255,255,0.14)";
const strokeSoft = "rgba(255,255,255,0.07)";
return ( return (
<AbsoluteFill style={{ background: p.base }}> <AbsoluteFill style={{ background: s.bg }}>
<div {/* faint motion — shape drifts very slowly (transform only, cheap) */}
style={{ <svg
position: "absolute", width={width}
width: width * 0.85, height={height}
height: width * 0.85, viewBox={`0 0 ${width} ${height}`}
left: width * 0.08 + Math.sin(t * 0.9) * 70, style={{ position: "absolute", inset: 0 }}
top: height * 0.06 + Math.sin(t * 0.7) * 50, >
borderRadius: "50%", {s.motif === "circles" && (
background: `radial-gradient(circle, rgba(${p.a1},0.34) 0%, rgba(${p.a1},0.16) 42%, transparent 70%)`, <>
}} <circle cx={width * 0.82} cy={height * 0.2} r={240} fill="none" stroke={stroke} strokeWidth={1.5} />
/> <circle cx={width * 0.82} cy={height * 0.2} r={300} fill="none" stroke={strokeSoft} strokeWidth={1} />
<div <circle cx={width * 0.14} cy={height * 0.78} r={90} fill="none" stroke={strokeSoft} strokeWidth={1} />
style={{ </>
position: "absolute", )}
width: width * 0.65, {s.motif === "plus" && (
height: width * 0.65, <>
right: -width * 0.12 + Math.cos(t * 1.1) * 60, {[0.12, 0.22, 0.5, 0.72, 0.88].map((x, i) => (
bottom: height * 0.16 + Math.cos(t * 0.8) * 40, <g key={i} transform={`translate(${width * x}, ${height * (0.2 + i * 0.16)})`} stroke={stroke} strokeWidth={2}>
borderRadius: "50%", <line x1={-13} y1={0} x2={13} y2={0} />
background: `radial-gradient(circle, rgba(${p.a2},0.30) 0%, rgba(${p.a2},0.13) 42%, transparent 70%)`, <line x1={0} y1={-13} x2={0} y2={13} />
}} </g>
/> ))}
{/* faint dot grid */} </>
)}
{s.motif === "barcode" && (
<>
{[30, 70, 110, 150, 190, 230, 270, 310].map((x, i) => (
<rect key={i} x={x} y={height * 0.12} width={i % 3 === 0 ? 6 : 3} height={height * 0.3} fill={stroke} />
))}
{[width - 330, width - 290, width - 250, width - 210, width - 170, width - 130, width - 90].map((x, i) => (
<rect key={i} x={x} y={height * 0.62} width={i % 2 === 0 ? 5 : 2} height={height * 0.26} fill={stroke} />
))}
</>
)}
{s.motif === "rings" && (
<>
<circle cx={width * 0.5} cy={height * 0.34} r={150} fill="none" stroke={stroke} strokeWidth={2} />
<circle cx={width * 0.5} cy={height * 0.34} r={120} fill="none" stroke={strokeSoft} strokeWidth={1.5} />
<circle cx={width * 0.5} cy={height * 0.34} r={90} fill="none" stroke={stroke} strokeWidth={1} />
</>
)}
{s.motif === "diag" && (
<>
{[0, 1, 2, 3, 4, 5].map((i) => (
<line key={i} x1={width * 0.1 + i * 46} y1={height * 0.05} x2={width * 0.25 + i * 46} y2={height * 0.45} stroke={strokeSoft} strokeWidth={2} />
))}
{[0, 1, 2, 3].map((i) => (
<line key={i} x1={width * 0.68 + i * 40} y1={height * 0.55} x2={width * 0.8 + i * 40} y2={height * 0.95} stroke={stroke} strokeWidth={2} />
))}
</>
)}
</svg>
{/* slow drift to keep the frame alive */}
<div <div
style={{ style={{
position: "absolute", position: "absolute",
inset: 0, inset: 0,
backgroundImage: "radial-gradient(rgba(255,255,255,0.07) 1px, transparent 1px)", transform: `translateY(${Math.sin(t * 0.8) * 10}px)`,
backgroundSize: "64px 64px", pointerEvents: "none",
opacity: 0.5,
}} }}
/> />
</AbsoluteFill> </AbsoluteFill>
); );
}; };
/** Glass card (translucent, rounded, border) for caption blocks. */ /** Flat card (solid white — inversion against the black background). */
export const GlassCard: React.FC<{ export const GlassCard: React.FC<{
children: React.ReactNode; children: React.ReactNode;
style?: React.CSSProperties; style?: React.CSSProperties;
@@ -87,12 +120,10 @@ export const GlassCard: React.FC<{
left: "5%", left: "5%",
right: "5%", right: "5%",
bottom: "13%", bottom: "13%",
padding: "30px 32px", padding: "30px 34px",
borderRadius: 28, borderRadius: 20,
background: "rgba(8,10,18,0.68)", background: "#f2f2f4",
border: "1px solid rgba(255,255,255,0.14)", transform: `translateY(${(1 - pop) * 26}px)`,
boxShadow: "0 14px 44px rgba(0,0,0,0.4)",
transform: `translateY(${(1 - pop) * 26}px) scale(${0.97 + pop * 0.03})`,
...style, ...style,
}} }}
> >
@@ -101,11 +132,11 @@ export const GlassCard: React.FC<{
); );
}; };
/** Karaoke caption: active sentence full-bright, others dimmed (synced to TTS). */ /** Karaoke caption: active sentence full-black, others grey (on white card). */
export const CaptionKaraoke: React.FC<{ export const CaptionKaraoke: React.FC<{
sentences: { w: string; start: number; end: number }[]; sentences: { w: string; start: number; end: number }[];
size?: number; size?: number;
}> = ({ sentences, size = 44 }) => { }> = ({ sentences, size = 46 }) => {
const frame = useCurrentFrame(); const frame = useCurrentFrame();
const { fps } = useVideoConfig(); const { fps } = useVideoConfig();
const now = frame / fps; const now = frame / fps;
@@ -114,13 +145,6 @@ export const CaptionKaraoke: React.FC<{
for (let i = 0; i < sentences.length; i++) { for (let i = 0; i < sentences.length; i++) {
if (now >= sentences[i].start) { activeIdx = i; break; } if (now >= sentences[i].start) { activeIdx = i; break; }
} }
const active = sentences[activeIdx];
const flash = active
? interpolate(now, [active.start, active.start + 0.12, active.start + 0.35], [1, 1.06, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
})
: 1;
return ( return (
<div <div
@@ -128,16 +152,13 @@ export const CaptionKaraoke: React.FC<{
fontFamily: FONT_BODY, fontFamily: FONT_BODY,
fontWeight: 700, fontWeight: 700,
fontSize: size, fontSize: size,
lineHeight: 1.34, lineHeight: 1.3,
color: "#ffffff", color: "#000000",
letterSpacing: "-0.01em", letterSpacing: "-0.01em",
textShadow: "0 2px 14px rgba(0,0,0,0.55)",
transform: `scale(${flash})`,
transformOrigin: "left center",
}} }}
> >
{sentences.map((s, i) => ( {sentences.map((s, i) => (
<span key={i} style={{ opacity: i === activeIdx ? 1 : 0.4 }}> <span key={i} style={{ opacity: i === activeIdx ? 1 : 0.35 }}>
{s.w} {s.w}
{i < sentences.length - 1 ? " " : ""} {i < sentences.length - 1 ? " " : ""}
</span> </span>
@@ -146,14 +167,14 @@ export const CaptionKaraoke: React.FC<{
); );
}; };
/** Big display title with word-by-word kinetic entrance. */ /** Big display title, word-by-word kinetic entrance, accent = inverted block. */
export const KineticTitle: React.FC<{ export const KineticTitle: React.FC<{
text: string; text: string;
size?: number; size?: number;
accentIndex?: number; accentIndex?: number;
accentColor?: string; accentColor?: string;
position?: "top" | "center"; position?: "top" | "center";
}> = ({ text, size = 88, accentIndex = -1, accentColor = "#a5b4fc", position = "top" }) => { }> = ({ text, size = 88, accentIndex = -1, position = "top" }) => {
const frame = useCurrentFrame(); const frame = useCurrentFrame();
const { fps } = useVideoConfig(); const { fps } = useVideoConfig();
const pop = spring({ frame, fps, config: { damping: 13, stiffness: 100 } }); const pop = spring({ frame, fps, config: { damping: 13, stiffness: 100 } });
@@ -175,20 +196,23 @@ export const KineticTitle: React.FC<{
key={i} key={i}
style={{ style={{
display: "inline-block", display: "inline-block",
marginRight: "0.2em", marginRight: "0.18em",
marginBottom: "0.08em",
fontFamily: FONT_DISPLAY, fontFamily: FONT_DISPLAY,
fontWeight: 800, fontWeight: 800,
fontSize: size, fontSize: size,
lineHeight: 1.1, lineHeight: 1.1,
letterSpacing: "-0.03em", letterSpacing: "-0.03em",
color: i === accentIndex ? accentColor : "#ffffff", color: i === accentIndex ? "#000000" : "#ffffff",
textShadow: "0 4px 24px rgba(0,0,0,0.5)", background: i === accentIndex ? "#ffffff" : "transparent",
padding: i === accentIndex ? "0 0.1em 0.02em" : "0",
borderRadius: 10,
opacity: interpolate(frame, [i * dur, i * dur + dur * 0.5], [0, 1], { opacity: interpolate(frame, [i * dur, i * dur + dur * 0.5], [0, 1], {
extrapolateRight: "clamp", extrapolateRight: "clamp",
}), }),
transform: `scale(${interpolate(frame, [i * dur, i * dur + dur * 0.6], [0.78, 1], { transform: `translateY(${interpolate(frame, [i * dur, i * dur + dur * 0.6], [26, 0], {
extrapolateRight: "clamp", extrapolateRight: "clamp",
})})`, })}px)`,
}} }}
> >
{w} {w}
@@ -198,12 +222,11 @@ export const KineticTitle: React.FC<{
); );
}; };
/** Huge stat/number card (pattern interrupt for facts). */ /** Huge stat with a thin rectangle frame behind (flat, editorial). */
export const StatCounter: React.FC<{ export const StatCounter: React.FC<{
value: string; value: string;
label: string; label: string;
color?: string; }> = ({ value, label }) => {
}> = ({ value, label, color = "#a5b4fc" }) => {
const frame = useCurrentFrame(); const frame = useCurrentFrame();
const { fps } = useVideoConfig(); const { fps } = useVideoConfig();
const pop = spring({ frame, fps, config: { damping: 14, stiffness: 90 } }); const pop = spring({ frame, fps, config: { damping: 14, stiffness: 90 } });
@@ -211,34 +234,55 @@ export const StatCounter: React.FC<{
<div <div
style={{ style={{
position: "absolute", position: "absolute",
left: "6%", left: "10%",
right: "6%", right: "10%",
top: "34%", top: "26%",
textAlign: "center", textAlign: "center",
transform: `scale(${0.85 + pop * 0.15})`, transform: `scale(${0.85 + pop * 0.15})`,
}} }}
> >
<div <div
style={{ style={{
position: "absolute",
inset: "-28px -40px",
border: "2px solid rgba(255,255,255,0.85)",
borderRadius: 16,
transform: `rotate(${(1 - pop) * 6}deg)`,
}}
/>
<div
style={{
position: "absolute",
inset: "-16px -24px",
border: "1px solid rgba(255,255,255,0.25)",
borderRadius: 12,
transform: `translate(${(1 - pop) * 10}px, ${(1 - pop) * 10}px)`,
}}
/>
<div
style={{
position: "relative",
fontFamily: FONT_DISPLAY, fontFamily: FONT_DISPLAY,
fontWeight: 800, fontWeight: 800,
fontSize: 132, fontSize: 134,
color, color: "#ffffff",
lineHeight: 1, lineHeight: 1,
letterSpacing: "-0.04em", letterSpacing: "-0.04em",
textShadow: "0 6px 30px rgba(0,0,0,0.55)",
}} }}
> >
{value} {value}
</div> </div>
<div <div
style={{ style={{
position: "relative",
fontFamily: FONT_BODY, fontFamily: FONT_BODY,
fontWeight: 700, fontWeight: 700,
fontSize: 36, fontSize: 34,
color: "rgba(255,255,255,0.8)", color: "rgba(255,255,255,0.85)",
marginTop: 20, marginTop: 22,
lineHeight: 1.3, lineHeight: 1.3,
textTransform: "uppercase",
letterSpacing: "0.02em",
}} }}
> >
{label} {label}
@@ -247,36 +291,32 @@ export const StatCounter: React.FC<{
); );
}; };
/** Top-left scene label pill. */ /** Top-left scene label (flat outline pill). */
export const BrandTag: React.FC<{ label: string; variant?: number }> = ({ label, variant = 0 }) => { export const BrandTag: React.FC<{ label: string }> = ({ label }) => (
const p = PALETTES[variant % PALETTES.length]; <div
return ( style={{
<div position: "absolute",
style={{ left: "6%",
position: "absolute", top: "6%",
left: "6%", padding: "10px 22px",
top: "6%", borderRadius: 999,
padding: "10px 22px", border: "2px solid #ffffff",
borderRadius: 999, color: "#ffffff",
background: `rgba(${p.a1},0.16)`, fontSize: 28,
border: `1px solid rgba(${p.a1},0.45)`, fontWeight: 700,
color: `rgb(${p.a1})`, fontFamily: FONT_BODY,
fontSize: 30, letterSpacing: "0.14em",
fontWeight: 700, textTransform: "uppercase",
fontFamily: FONT_BODY, background: "transparent",
letterSpacing: "0.1em", }}
textTransform: "uppercase", >
}} {label}
> </div>
{label} );
</div>
);
};
/** Bottom progress bar (per-segment fill). */ /** Bottom progress bar — solid white fill on faint track. */
export const ProgressBar: React.FC<{ total: number; variant?: number }> = ({ total, variant = 0 }) => { export const ProgressBar: React.FC<{ total: number }> = ({ total }) => {
const frame = useCurrentFrame(); const frame = useCurrentFrame();
const p = PALETTES[variant % PALETTES.length];
const pct = interpolate(frame, [0, total], [0, 100], { extrapolateRight: "clamp" }); const pct = interpolate(frame, [0, total], [0, 100], { extrapolateRight: "clamp" });
return ( return (
<div <div
@@ -285,9 +325,9 @@ export const ProgressBar: React.FC<{ total: number; variant?: number }> = ({ tot
left: "6%", left: "6%",
right: "6%", right: "6%",
bottom: "6%", bottom: "6%",
height: 8, height: 7,
borderRadius: 99, borderRadius: 99,
background: "rgba(255,255,255,0.16)", background: "rgba(255,255,255,0.18)",
overflow: "hidden", overflow: "hidden",
}} }}
> >
@@ -296,7 +336,7 @@ export const ProgressBar: React.FC<{ total: number; variant?: number }> = ({ tot
width: `${pct}%`, width: `${pct}%`,
height: "100%", height: "100%",
borderRadius: 99, borderRadius: 99,
background: `linear-gradient(90deg, rgb(${p.a1}), rgb(${p.a2}))`, background: "#ffffff",
}} }}
/> />
</div> </div>
@@ -308,8 +348,8 @@ export const SlideMotion: React.FC<{ children: React.ReactNode }> = ({ children
const frame = useCurrentFrame(); const frame = useCurrentFrame();
const { fps } = useVideoConfig(); const { fps } = useVideoConfig();
const t = frame / fps; const t = frame / fps;
const y = Math.sin(t * 1.4) * 14; const y = Math.sin(t * 1.4) * 12;
const scale = 1 + Math.sin(t * 0.8) * 0.014; const scale = 1 + Math.sin(t * 0.8) * 0.012;
return ( return (
<AbsoluteFill style={{ transform: `translateY(${y}px) scale(${scale})` }}> <AbsoluteFill style={{ transform: `translateY(${y}px) scale(${scale})` }}>
{children} {children}