mirror of
https://github.com/digitalsamba/claude-code-video-toolkit.git
synced 2026-09-18 19:41:13 +08:00
BRAND: animated synthwave banner for README (showcase/banner project)
New showcase/banner Remotion project renders a 5s 1080x500 banner — amber phosphor CRT aesthetic with a NARRATE ▸ SCORE ▸ GENERATE ▸ COMPOSE ▸ RENDER pipeline that ignites left-to-right, on a scrolling perspective grid with scanlines and a power-on sweep. Three alternates (outrun / dusk / midnight) stay registered in src/Root.tsx for future re-rendering. Palette-optimized GIF (2.8MB, 960x444, 64 colors via ffmpeg) + MP4 + poster PNG land in assets/banner/ and are referenced from the README header. .gitignore updated to allow committing the banner assets despite the project-wide *.mp4 / *.gif exclusion. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
# banner-showcase
|
||||
|
||||
The animated banner shown at the top of the main `README.md`. Source for the file at `assets/banner/toolkit-banner.{gif,mp4,poster.png}`.
|
||||
|
||||
## Theme
|
||||
|
||||
The live banner uses the `amber` theme — mono phosphor CRT aesthetic on a dark grid. Three alternates (`outrun`, `dusk`, `midnight`) are kept registered in `src/Root.tsx` so we can revisit or re-render if the branding changes.
|
||||
|
||||
## Re-rendering
|
||||
|
||||
```bash
|
||||
cd showcase/banner
|
||||
npm install
|
||||
npm run studio # Preview / scrub in Remotion Studio
|
||||
npm run render # MP4 → out/toolkit-banner.mp4
|
||||
npm run render:gif # GIF → out/toolkit-banner.gif
|
||||
npm run render:poster # Static PNG poster frame
|
||||
```
|
||||
|
||||
After rendering, copy the outputs over the top of `assets/banner/`:
|
||||
|
||||
```bash
|
||||
cp out/toolkit-banner.{mp4,gif} out/toolkit-banner-poster.png ../../assets/banner/
|
||||
```
|
||||
|
||||
### Shrinking the GIF
|
||||
|
||||
Remotion's native GIF is honest-quality but large. The published `assets/banner/toolkit-banner.gif` is a palette-optimized pass via ffmpeg:
|
||||
|
||||
```bash
|
||||
ffmpeg -y -i out/toolkit-banner.mp4 \
|
||||
-vf "fps=20,scale=960:444:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=64[p];[s1][p]paletteuse=dither=bayer:bayer_scale=5" \
|
||||
out/toolkit-banner.gif
|
||||
```
|
||||
|
||||
64 colors is plenty for the amber theme. For the full-saturation variants (`outrun`, `dusk`) bump `max_colors=128` or accept a larger file.
|
||||
|
||||
## Structure
|
||||
|
||||
- `src/themes.ts` — all four color palettes in one file; swap `themes.amber` in `Root.tsx` to change the primary
|
||||
- `src/SynthwaveBackground.tsx` — base color, optional banded sun, perspective grid
|
||||
- `src/Wordmark.tsx` — "CLAUDE CODE VIDEO TOOLKIT" drop-in with glow + optional chromatic aberration
|
||||
- `src/Pipeline.tsx` — `NARRATE ▸ SCORE ▸ GENERATE ▸ COMPOSE ▸ RENDER` sequential ignite
|
||||
- `src/CRTOverlay.tsx` — scanlines, power-on sweep, vignette
|
||||
Generated
+2926
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "banner-showcase",
|
||||
"version": "1.0.0",
|
||||
"description": "Animated synthwave banner for the claude-code-video-toolkit",
|
||||
"scripts": {
|
||||
"studio": "remotion studio",
|
||||
"render": "remotion render ToolkitBanner out/toolkit-banner.mp4",
|
||||
"render:gif": "remotion render ToolkitBanner out/toolkit-banner.gif --codec=gif",
|
||||
"render:poster": "remotion still ToolkitBanner out/toolkit-banner-poster.png --frame=120"
|
||||
},
|
||||
"dependencies": {
|
||||
"@remotion/cli": "^4.0.0",
|
||||
"@remotion/google-fonts": "^4.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"remotion": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { Config } from '@remotion/cli/config';
|
||||
|
||||
Config.setEntryPoint('./src/index.ts');
|
||||
@@ -0,0 +1,70 @@
|
||||
import { AbsoluteFill, interpolate, useCurrentFrame } from 'remotion';
|
||||
import type { Theme } from './themes';
|
||||
|
||||
export const CRTOverlay: React.FC<{ theme: Theme }> = ({ theme }) => {
|
||||
const frame = useCurrentFrame();
|
||||
|
||||
const sweepY = interpolate(frame, [0, 9], [-20, 120], {
|
||||
extrapolateRight: 'clamp',
|
||||
});
|
||||
const sweepOpacity = interpolate(frame, [0, 4, 9], [0, 1, 0], {
|
||||
extrapolateRight: 'clamp',
|
||||
});
|
||||
|
||||
const powerOn = interpolate(frame, [0, 9], [0.1, 1], {
|
||||
extrapolateRight: 'clamp',
|
||||
extrapolateLeft: 'clamp',
|
||||
});
|
||||
|
||||
const flicker = frame % 90 === 0 || frame % 137 === 0 ? 0.85 : 1;
|
||||
|
||||
const scanAlpha = Math.min(0.08 * theme.crt.scanlineOpacity, 0.12);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AbsoluteFill style={{ background: '#000', opacity: 1 - powerOn, pointerEvents: 'none' }} />
|
||||
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
backgroundImage: `repeating-linear-gradient(to bottom, rgba(255,255,255,${scanAlpha}) 0px, rgba(255,255,255,${scanAlpha}) 1px, transparent 1px, transparent 3px)`,
|
||||
mixBlendMode: 'overlay',
|
||||
opacity: flicker,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: `${sweepY}%`,
|
||||
height: 4,
|
||||
background: `linear-gradient(to bottom, transparent, ${theme.crt.sweepColor}, transparent)`,
|
||||
filter: 'blur(2px)',
|
||||
opacity: sweepOpacity,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
background: `radial-gradient(ellipse at center, transparent 55%, rgba(0,0,0,${theme.crt.vignetteStrength}) 100%)`,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 60,
|
||||
background: 'linear-gradient(to bottom, rgba(255,255,255,0.05), transparent)',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,117 @@
|
||||
import { Fragment } from 'react';
|
||||
import { AbsoluteFill, interpolate, useCurrentFrame } from 'remotion';
|
||||
import { loadFont } from '@remotion/google-fonts/VT323';
|
||||
import type { Theme } from './themes';
|
||||
|
||||
const { fontFamily } = loadFont();
|
||||
|
||||
const STAGES = ['NARRATE', 'SCORE', 'GENERATE', 'COMPOSE', 'RENDER'];
|
||||
const STAGE_START = 55;
|
||||
const STAGE_STRIDE = 12;
|
||||
|
||||
export const Pipeline: React.FC<{ theme: Theme }> = ({ theme }) => {
|
||||
const frame = useCurrentFrame();
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
paddingBottom: 74,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 18,
|
||||
fontFamily,
|
||||
fontSize: 42,
|
||||
letterSpacing: 3,
|
||||
}}
|
||||
>
|
||||
{STAGES.map((stage, i) => {
|
||||
const ignite = STAGE_START + i * STAGE_STRIDE;
|
||||
const arrowIgnite = ignite + 6;
|
||||
return (
|
||||
<Fragment key={stage}>
|
||||
<Stage label={stage} igniteFrame={ignite} frame={frame} theme={theme} />
|
||||
{i < STAGES.length - 1 && (
|
||||
<Arrow igniteFrame={arrowIgnite} frame={frame} theme={theme} />
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
const Stage: React.FC<{
|
||||
label: string;
|
||||
igniteFrame: number;
|
||||
frame: number;
|
||||
theme: Theme;
|
||||
}> = ({ label, igniteFrame, frame, theme }) => {
|
||||
const brightness = interpolate(
|
||||
frame,
|
||||
[igniteFrame - 2, igniteFrame, igniteFrame + 4, igniteFrame + 14],
|
||||
[0, 1.35, 1, 0.88],
|
||||
{ extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
|
||||
);
|
||||
const hueMix = interpolate(frame, [igniteFrame, igniteFrame + 14], [0, 1], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp',
|
||||
});
|
||||
|
||||
const color = lerpHex(theme.pipeline.ignite, theme.pipeline.settle, hueMix);
|
||||
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
color,
|
||||
opacity: brightness,
|
||||
textShadow: `0 0 8px ${color}, 0 0 20px ${color}bb, 0 0 36px ${color}55`,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const Arrow: React.FC<{
|
||||
igniteFrame: number;
|
||||
frame: number;
|
||||
theme: Theme;
|
||||
}> = ({ igniteFrame, frame, theme }) => {
|
||||
const opacity = interpolate(
|
||||
frame,
|
||||
[igniteFrame - 2, igniteFrame, igniteFrame + 4],
|
||||
[0, 1.2, 1],
|
||||
{ extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
|
||||
);
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
color: theme.pipeline.arrow,
|
||||
opacity,
|
||||
textShadow: `0 0 10px ${theme.pipeline.arrowShadow}, 0 0 22px ${theme.pipeline.arrowShadow}`,
|
||||
}}
|
||||
>
|
||||
▸
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
function lerpHex(a: string, b: string, t: number): string {
|
||||
const parse = (h: string) => [
|
||||
parseInt(h.slice(1, 3), 16),
|
||||
parseInt(h.slice(3, 5), 16),
|
||||
parseInt(h.slice(5, 7), 16),
|
||||
];
|
||||
const [ar, ag, ab] = parse(a);
|
||||
const [br, bg, bb] = parse(b);
|
||||
const mix = (x: number, y: number) => Math.round(x + (y - x) * t);
|
||||
const hex = (n: number) => n.toString(16).padStart(2, '0');
|
||||
return `#${hex(mix(ar, br))}${hex(mix(ag, bg))}${hex(mix(ab, bb))}`;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Composition } from 'remotion';
|
||||
import { ToolkitBanner } from './ToolkitBanner';
|
||||
import { themes } from './themes';
|
||||
|
||||
const shared = {
|
||||
component: ToolkitBanner,
|
||||
durationInFrames: 150,
|
||||
fps: 30,
|
||||
width: 1080,
|
||||
height: 500,
|
||||
};
|
||||
|
||||
export const RemotionRoot: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
{/* Primary — the banner rendered for the toolkit README */}
|
||||
<Composition
|
||||
id="ToolkitBanner"
|
||||
{...shared}
|
||||
defaultProps={{ theme: themes.amber }}
|
||||
/>
|
||||
|
||||
{/* Alternates — kept registered so we can revisit or re-render if branding changes */}
|
||||
<Composition id="Banner-Outrun" {...shared} defaultProps={{ theme: themes.outrun }} />
|
||||
<Composition id="Banner-Dusk" {...shared} defaultProps={{ theme: themes.dusk }} />
|
||||
<Composition id="Banner-Midnight" {...shared} defaultProps={{ theme: themes.midnight }} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
import { AbsoluteFill, interpolate, useCurrentFrame } from 'remotion';
|
||||
import type { Theme } from './themes';
|
||||
|
||||
export const SynthwaveBackground: React.FC<{ theme: Theme }> = ({ theme }) => {
|
||||
const frame = useCurrentFrame();
|
||||
|
||||
const fadeIn = interpolate(frame, [8, 25], [0, 1], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp',
|
||||
});
|
||||
|
||||
const gridScroll = (frame * 1.2) % 60;
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ background: theme.background, overflow: 'hidden' }}>
|
||||
{theme.sun.enabled && (
|
||||
<>
|
||||
{/* Sun horizon glow */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
top: '42%',
|
||||
width: 620,
|
||||
height: 620,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
borderRadius: '50%',
|
||||
background: `radial-gradient(circle at 50% 50%, ${theme.sun.inner} 0%, ${theme.sun.mid} 28%, ${theme.sun.outer} 60%, ${theme.background} 82%)`,
|
||||
filter: 'blur(2px)',
|
||||
opacity: fadeIn * 0.85,
|
||||
}}
|
||||
/>
|
||||
{/* Sun bands — synthwave stripes cutting the sun */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
top: '42%',
|
||||
width: 620,
|
||||
height: 620,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
borderRadius: '50%',
|
||||
overflow: 'hidden',
|
||||
opacity: fadeIn * 0.9,
|
||||
WebkitMaskImage:
|
||||
'radial-gradient(circle at 50% 50%, black 0%, black 48%, transparent 52%)',
|
||||
maskImage:
|
||||
'radial-gradient(circle at 50% 50%, black 0%, black 48%, transparent 52%)',
|
||||
}}
|
||||
>
|
||||
{[0, 1, 2, 3, 4].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: `${62 + i * 7}%`,
|
||||
height: `${3 + i * 1.2}%`,
|
||||
background: theme.sun.bandColor,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Perspective grid */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: '-10%',
|
||||
right: '-10%',
|
||||
height: '52%',
|
||||
perspective: '340px',
|
||||
perspectiveOrigin: '50% 0%',
|
||||
opacity: fadeIn,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
transform: 'rotateX(58deg)',
|
||||
transformOrigin: '50% 0%',
|
||||
backgroundImage: `
|
||||
linear-gradient(to right, ${theme.grid.vertical} ${theme.grid.thickness}px, transparent ${theme.grid.thickness}px),
|
||||
linear-gradient(to bottom, ${theme.grid.horizontal} ${theme.grid.thickness}px, transparent ${theme.grid.thickness}px)
|
||||
`,
|
||||
backgroundSize: '60px 60px',
|
||||
backgroundPosition: `0 ${gridScroll}px, 0 ${gridScroll}px`,
|
||||
}}
|
||||
/>
|
||||
{/* Horizon haze */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '18%',
|
||||
background: `linear-gradient(to bottom, ${theme.background} 0%, ${theme.grid.vertical} 50%, transparent 100%)`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { AbsoluteFill } from 'remotion';
|
||||
import { SynthwaveBackground } from './SynthwaveBackground';
|
||||
import { Wordmark } from './Wordmark';
|
||||
import { Pipeline } from './Pipeline';
|
||||
import { CRTOverlay } from './CRTOverlay';
|
||||
import type { Theme } from './themes';
|
||||
|
||||
export const ToolkitBanner: React.FC<{ theme: Theme }> = ({ theme }) => {
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
<SynthwaveBackground theme={theme} />
|
||||
<Wordmark theme={theme} />
|
||||
<Pipeline theme={theme} />
|
||||
<CRTOverlay theme={theme} />
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
import { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion';
|
||||
import { loadFont } from '@remotion/google-fonts/Audiowide';
|
||||
import type { Theme } from './themes';
|
||||
|
||||
const { fontFamily } = loadFont();
|
||||
|
||||
export const Wordmark: React.FC<{ theme: Theme }> = ({ theme }) => {
|
||||
const frame = useCurrentFrame();
|
||||
|
||||
const drop = interpolate(frame, [20, 48], [-30, 0], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp',
|
||||
easing: Easing.bezier(0.16, 1, 0.3, 1),
|
||||
});
|
||||
const fade = interpolate(frame, [20, 48], [0, 1], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp',
|
||||
});
|
||||
|
||||
const flickerA = frame > 48 && frame < 54 ? (frame % 2 === 0 ? 0.4 : 1) : 1;
|
||||
const flickerB = frame > 78 && frame < 82 ? (frame % 2 === 0 ? 0.7 : 1) : 1;
|
||||
const flicker = flickerA * flickerB;
|
||||
|
||||
const baseStyle: React.CSSProperties = {
|
||||
fontFamily,
|
||||
fontSize: 52,
|
||||
fontWeight: 400,
|
||||
letterSpacing: 4,
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
paddingTop: 96,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
transform: `translateY(${drop}px)`,
|
||||
opacity: fade * flicker,
|
||||
}}
|
||||
>
|
||||
{theme.wordmark.aberration.enabled && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
...baseStyle,
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
transform: `translate(-${theme.wordmark.aberration.offset}px, 0)`,
|
||||
color: theme.wordmark.aberration.left,
|
||||
opacity: 0.65,
|
||||
}}
|
||||
aria-hidden
|
||||
>
|
||||
CLAUDE CODE VIDEO TOOLKIT
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
...baseStyle,
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
transform: `translate(${theme.wordmark.aberration.offset}px, 0)`,
|
||||
color: theme.wordmark.aberration.right,
|
||||
opacity: 0.65,
|
||||
}}
|
||||
aria-hidden
|
||||
>
|
||||
CLAUDE CODE VIDEO TOOLKIT
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
...baseStyle,
|
||||
color: theme.wordmark.color,
|
||||
textShadow: theme.wordmark.glow.join(', '),
|
||||
}}
|
||||
>
|
||||
CLAUDE CODE VIDEO TOOLKIT
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
import { registerRoot } from 'remotion';
|
||||
import { RemotionRoot } from './Root';
|
||||
|
||||
registerRoot(RemotionRoot);
|
||||
@@ -0,0 +1,174 @@
|
||||
export type Theme = {
|
||||
name: string;
|
||||
background: string;
|
||||
sun: {
|
||||
enabled: boolean;
|
||||
inner: string;
|
||||
mid: string;
|
||||
outer: string;
|
||||
bandColor: string;
|
||||
};
|
||||
grid: {
|
||||
vertical: string;
|
||||
horizontal: string;
|
||||
thickness: number;
|
||||
};
|
||||
wordmark: {
|
||||
color: string;
|
||||
glow: string[];
|
||||
aberration: {
|
||||
enabled: boolean;
|
||||
offset: number;
|
||||
left: string;
|
||||
right: string;
|
||||
};
|
||||
};
|
||||
pipeline: {
|
||||
ignite: string;
|
||||
settle: string;
|
||||
arrow: string;
|
||||
arrowShadow: string;
|
||||
};
|
||||
crt: {
|
||||
scanlineOpacity: number;
|
||||
vignetteStrength: number;
|
||||
sweepColor: string;
|
||||
};
|
||||
};
|
||||
|
||||
// V1 — Outrun: classic synthwave, dialed back from the opening prototype
|
||||
export const outrun: Theme = {
|
||||
name: 'outrun',
|
||||
background: '#120823',
|
||||
sun: {
|
||||
enabled: true,
|
||||
inner: '#ffd2e8',
|
||||
mid: '#d53b8a',
|
||||
outer: '#5b1982',
|
||||
bandColor: '#120823',
|
||||
},
|
||||
grid: {
|
||||
vertical: 'rgba(213, 59, 138, 0.55)',
|
||||
horizontal: 'rgba(74, 160, 204, 0.35)',
|
||||
thickness: 1.5,
|
||||
},
|
||||
wordmark: {
|
||||
color: '#fff8ff',
|
||||
glow: ['0 0 3px #fff', '0 0 10px #ff9fc8', '0 0 24px #d53b8a'],
|
||||
aberration: {
|
||||
enabled: true,
|
||||
offset: 2,
|
||||
left: '#7adef5',
|
||||
right: '#ff9fc8',
|
||||
},
|
||||
},
|
||||
pipeline: {
|
||||
ignite: '#7adef5',
|
||||
settle: '#ff9fc8',
|
||||
arrow: '#fff8ff',
|
||||
arrowShadow: '#d53b8a',
|
||||
},
|
||||
crt: {
|
||||
scanlineOpacity: 0.9,
|
||||
vignetteStrength: 0.55,
|
||||
sweepColor: 'rgba(122, 222, 245, 0.75)',
|
||||
},
|
||||
};
|
||||
|
||||
// V2 — Dusk: muted sunset, one main accent, no chromatic aberration
|
||||
export const dusk: Theme = {
|
||||
name: 'dusk',
|
||||
background: '#1a1530',
|
||||
sun: {
|
||||
enabled: true,
|
||||
inner: '#ffcfbd',
|
||||
mid: '#c47892',
|
||||
outer: '#2a2a4a',
|
||||
bandColor: '#1a1530',
|
||||
},
|
||||
grid: {
|
||||
vertical: 'rgba(196, 120, 146, 0.28)',
|
||||
horizontal: 'rgba(147, 162, 220, 0.22)',
|
||||
thickness: 1,
|
||||
},
|
||||
wordmark: {
|
||||
color: '#f8e8ff',
|
||||
glow: ['0 0 2px #fff', '0 0 14px #d4b3ff'],
|
||||
aberration: { enabled: false, offset: 0, left: '', right: '' },
|
||||
},
|
||||
pipeline: {
|
||||
ignite: '#f8e8ff',
|
||||
settle: '#d4b3ff',
|
||||
arrow: '#a192d0',
|
||||
arrowShadow: 'rgba(196, 120, 146, 0.6)',
|
||||
},
|
||||
crt: {
|
||||
scanlineOpacity: 0.7,
|
||||
vignetteStrength: 0.5,
|
||||
sweepColor: 'rgba(212, 179, 255, 0.6)',
|
||||
},
|
||||
};
|
||||
|
||||
// V3 — Amber: mono phosphor CRT terminal, no sun, no synthwave
|
||||
export const amber: Theme = {
|
||||
name: 'amber',
|
||||
background: '#0a0603',
|
||||
sun: {
|
||||
enabled: false,
|
||||
inner: '', mid: '', outer: '', bandColor: '',
|
||||
},
|
||||
grid: {
|
||||
vertical: 'rgba(255, 165, 82, 0.22)',
|
||||
horizontal: 'rgba(255, 165, 82, 0.18)',
|
||||
thickness: 1,
|
||||
},
|
||||
wordmark: {
|
||||
color: '#ffa552',
|
||||
glow: ['0 0 2px #ffd8a8', '0 0 10px #ffa552', '0 0 26px #ff7a1a'],
|
||||
aberration: { enabled: false, offset: 0, left: '', right: '' },
|
||||
},
|
||||
pipeline: {
|
||||
ignite: '#ffd8a8',
|
||||
settle: '#ffa552',
|
||||
arrow: '#ffa552',
|
||||
arrowShadow: 'rgba(255, 122, 26, 0.7)',
|
||||
},
|
||||
crt: {
|
||||
scanlineOpacity: 1.4,
|
||||
vignetteStrength: 0.7,
|
||||
sweepColor: 'rgba(255, 216, 168, 0.8)',
|
||||
},
|
||||
};
|
||||
|
||||
// V4 — Midnight: minimal, no sun, cool cyan only
|
||||
export const midnight: Theme = {
|
||||
name: 'midnight',
|
||||
background: '#0a0e1f',
|
||||
sun: {
|
||||
enabled: false,
|
||||
inner: '', mid: '', outer: '', bandColor: '',
|
||||
},
|
||||
grid: {
|
||||
vertical: 'rgba(110, 231, 255, 0.3)',
|
||||
horizontal: 'rgba(110, 231, 255, 0.22)',
|
||||
thickness: 1,
|
||||
},
|
||||
wordmark: {
|
||||
color: '#fff',
|
||||
glow: ['0 0 2px #fff', '0 0 12px #6ee7ff', '0 0 26px #3a9dbb'],
|
||||
aberration: { enabled: false, offset: 0, left: '', right: '' },
|
||||
},
|
||||
pipeline: {
|
||||
ignite: '#e0fbff',
|
||||
settle: '#6ee7ff',
|
||||
arrow: '#6ee7ff',
|
||||
arrowShadow: 'rgba(110, 231, 255, 0.7)',
|
||||
},
|
||||
crt: {
|
||||
scanlineOpacity: 0.6,
|
||||
vignetteStrength: 0.45,
|
||||
sweepColor: 'rgba(110, 231, 255, 0.7)',
|
||||
},
|
||||
};
|
||||
|
||||
export const themes = { outrun, dusk, amber, midnight };
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2018",
|
||||
"module": "commonjs",
|
||||
"jsx": "react",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"lib": ["dom", "dom.iterable", "esnext"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user