cutship.ai1 / 7
Carousel · 4:5
Marker Scrawl Carousel
by CutshipVerified
An event announcement in the language of a cool restaurant feed: direct-flash food and drink photos, your name scrawled across them in electric-blue marker, tiny letter-spaced caps, huge grotesk words with the time scribbled over them, and a cream menu with hand-drawn wine glass, fork and olive doodles.
#streetflash#boldtype#carousel#instagram#event#launch#restaurant#food#drinks#supperclub#marker#handwritten#flash#menu#4:5
0 taken · 1 views
Included with Pro
Sign up free to Make this
Sign up free to unlock the spec, agent prompt and Make this.
components/remotion/marker-scrawl-carousel/component.tsx
import React, { useEffect, useState } from "react";
import { AbsoluteFill, Img, continueRender, delayRender, random, staticFile, useCurrentFrame } from "remotion";
import { loadFont as loadKristi } from "@remotion/google-fonts/Kristi";
import { loadFont as loadNothing } from "@remotion/google-fonts/NothingYouCouldDo";
import { loadFont as loadReenie } from "@remotion/google-fonts/ReenieBeanie";
import { loadFont as loadRockSalt } from "@remotion/google-fonts/RockSalt";
import { loadFont as loadPermanent } from "@remotion/google-fonts/PermanentMarker";
import { loadFont as loadHomemade } from "@remotion/google-fonts/HomemadeApple";
import { loadFont as loadArchivoBlack } from "@remotion/google-fonts/ArchivoBlack";
import { loadFont as loadAnton } from "@remotion/google-fonts/Anton";
import { loadFont as loadBebas } from "@remotion/google-fonts/BebasNeue";
import { loadFont as loadBigShoulders } from "@remotion/google-fonts/BigShoulders";
import { loadFont as loadArchivo } from "@remotion/google-fonts/Archivo";
import { getAvailableFonts } from "@remotion/google-fonts";
import type { MarkerScrawlCarouselProps } from "./schema";
const opts = { weights: ["400"] as "400"[], subsets: ["latin"] as "latin"[] };
const SCRIPT: Record<string, { family: string; em: number }> = {
Kristi: { family: loadKristi("normal", opts).fontFamily, em: 0.36 },
"Nothing You Could Do": { family: loadNothing("normal", opts).fontFamily, em: 0.5 },
"Reenie Beanie": { family: loadReenie("normal", opts).fontFamily, em: 0.36 },
"Rock Salt": { family: loadRockSalt("normal", opts).fontFamily, em: 0.72 },
"Permanent Marker": { family: loadPermanent("normal", opts).fontFamily, em: 0.5 },
"Homemade Apple": { family: loadHomemade("normal", opts).fontFamily, em: 0.58 },
};
const DISPLAY: Record<string, { family: string; em: number; weight: number; track: string }> = {
"Archivo Black": { family: loadArchivoBlack("normal", opts).fontFamily, em: 0.8, weight: 400, track: "-0.035em" },
Anton: { family: loadAnton("normal", opts).fontFamily, em: 0.47, weight: 400, track: "-0.01em" },
"Bebas Neue": { family: loadBebas("normal", opts).fontFamily, em: 0.4, weight: 400, track: "0em" },
"Big Shoulders": { family: loadBigShoulders("normal", { weights: ["900"] as "900"[], subsets: ["latin"] as "latin"[] }).fontFamily, em: 0.5, weight: 900, track: "-0.01em" },
};
const { fontFamily: capsFont } = loadArchivo("normal", { weights: ["600"], subsets: ["latin"] });
/** Loads a brand-kit Google Font by family name; returns it once ready. */
const useBrandFont = (family?: string) => {
const [ready, setReady] = useState<string | null>(null);
useEffect(() => {
if (!family) return setReady(null);
const entry = getAvailableFonts().find((f) => f.fontFamily.toLowerCase() === family.toLowerCase());
if (!entry) return;
const handle = delayRender(`Loading font ${family}`);
entry
.load()
.then((mod) => mod.loadFont("normal", { subsets: ["latin"] }).waitUntilDone())
.then(() => setReady(entry.fontFamily))
.catch(() => setReady(null))
.finally(() => continueRender(handle));
}, [family]);
return ready;
};
const resolve = (src: string) => (src.startsWith("http") || src.startsWith("/") ? src : staticFile(src));
const f1 = (n: number) => n.toFixed(1);
/* ---------------------------------------------------------------- marker strokes */
type Pt = [number, number];
/** Catmull-Rom through the points so a few hand-placed points read as one fluid stroke. */
const smooth = (pts: Pt[], per = 9): Pt[] => {
if (pts.length < 3) return pts;
const out: Pt[] = [];
for (let i = 0; i < pts.length - 1; i++) {
const p0 = pts[Math.max(0, i - 1)];
const p1 = pts[i];
const p2 = pts[i + 1];
const p3 = pts[Math.min(pts.length - 1, i + 2)];
for (let k = 0; k < per; k++) {
const t = k / per;
const t2 = t * t;
const t3 = t2 * t;
const c = (a: number, b: number, cc: number, d: number) =>
0.5 * (2 * b + (-a + cc) * t + (2 * a - 5 * b + 4 * cc - d) * t2 + (-a + 3 * b - 3 * cc + d) * t3);
out.push([c(p0[0], p1[0], p2[0], p3[0]), c(p0[1], p1[1], p2[1], p3[1])]);
}
}
out.push(pts[pts.length - 1]);
return out;
};
/**
* One marker stroke as a filled outline: heavier where the pen lands, thinner as it
* lifts off, with a slow wobble in the width. Returns an SVG path string.
*/
const brush = (pts: Pt[], width: number, seed: string): string => {
const s = smooth(pts);
const n = s.length;
const phase = random(`${seed}-ph`) * 6;
const left: Pt[] = [];
const right: Pt[] = [];
const half: number[] = [];
for (let i = 0; i < n; i++) {
const a = s[Math.max(0, i - 1)];
const b = s[Math.min(n - 1, i + 1)];
const dx = b[0] - a[0];
const dy = b[1] - a[1];
const len = Math.hypot(dx, dy) || 1;
const nx = -dy / len;
const ny = dx / len;
const t = i / Math.max(1, n - 1);
const press = (0.62 + 0.38 * Math.sin(Math.PI * Math.min(1, t * 1.25 + 0.05))) * (1 - 0.32 * t);
const wob = 1 + Math.sin(t * 9 + phase) * 0.1 + (random(`${seed}-${i}`) - 0.5) * 0.06;
const h = (width / 2) * press * wob;
half.push(h);
left.push([s[i][0] + nx * h, s[i][1] + ny * h]);
right.push([s[i][0] - nx * h, s[i][1] - ny * h]);
}
const body = `M${left.map(([x, y]) => `${f1(x)} ${f1(y)}`).join(" L")} L${right
.reverse()
.map(([x, y]) => `${f1(x)} ${f1(y)}`)
.join(" L")} Z`;
const cap = ([x, y]: Pt, r: number) => `M${f1(x - r)} ${f1(y)} a${f1(r)} ${f1(r)} 0 1 0 ${f1(2 * r)} 0 a${f1(r)} ${f1(r)} 0 1 0 ${f1(-2 * r)} 0`;
return `${body} ${cap(s[0], half[0])} ${cap(s[n - 1], half[n - 1])}`;
};
/** Hand-wobbled ellipse that overshoots its start, like a quick circle around something. */
const loopPts = (cx: number, cy: number, rx: number, ry: number, seed: string, turns = 1.12, start = -2.2): Pt[] => {
const steps = 26;
return Array.from({ length: steps + 1 }, (_, k) => {
const t = start + (k / steps) * Math.PI * 2 * turns;
const w = 1 + (random(`${seed}-${k}`) - 0.5) * 0.05 + (k / steps) * 0.07;
return [cx + Math.cos(t) * rx * w, cy + Math.sin(t) * ry * w] as Pt;
});
};
type DoodleKind = "wineglass" | "fork" | "star" | "olive" | "martini" | "sparkle" | "arrow" | "swash";
/** Strokes for each doodle in a 100 x 100 box. */
const DOODLES: Record<DoodleKind, Pt[][]> = {
wineglass: [
[[27, 12], [25, 30], [32, 45], [50, 53], [67, 46], [74, 29], [72, 10]],
[[26, 12], [49, 15], [73, 10]],
[[28, 28], [50, 32], [73, 27]],
[[50, 53], [50.5, 72], [49.5, 88]],
[[33, 91], [50, 87], [68, 90]],
],
fork: [
[[51, 97], [50.5, 72], [50, 50]],
[[33, 8], [32, 30], [36, 44], [50, 51], [64, 44], [68, 30], [67, 9]],
[[45, 6], [44.5, 26], [45, 46]],
[[56, 6], [56.5, 26], [55.5, 47]],
],
star: [
[[50, 8], [61, 38], [93, 39], [67, 58], [77, 90], [50, 70], [23, 90], [33, 58], [7, 38], [40, 37], [51, 10]],
],
olive: [
loopPts(46, 56, 25, 31, "olive", 1.08, -1.4),
loopPts(47, 50, 8, 7, "pit", 1.1, 0.4),
[[88, 8], [64, 34], [36, 66], [18, 88]],
],
martini: [
[[10, 10], [30, 26], [50, 50]],
[[50, 50], [70, 27], [91, 9]],
[[10, 10], [50, 13], [91, 9]],
[[50, 50], [50.5, 72], [49.5, 88]],
[[32, 91], [50, 88], [69, 91]],
loopPts(62, 24, 7, 6, "mo", 1.1),
[[44, 2], [58, 22], [74, 38]],
],
sparkle: [
[[50, 8], [50.5, 50], [50, 92]],
[[10, 51], [50, 49.5], [90, 50]],
[[24, 24], [50, 50], [76, 77]],
],
arrow: [
[[8, 14], [20, 52], [48, 80], [86, 88]],
[[66, 70], [87, 88], [64, 100]],
],
swash: [[[2, 60], [28, 46], [60, 44], [86, 36], [98, 28]]],
};
const Doodle: React.FC<{
kind: DoodleKind;
x: number;
y: number;
size: number;
rot?: number;
color: string;
weight?: number;
flip?: boolean;
}> = ({ kind, x, y, size, rot = 0, color, weight = 6, flip }) => (
<svg
width={size}
height={size}
viewBox="0 0 100 100"
style={{ position: "absolute", left: x, top: y, overflow: "visible", transform: `rotate(${rot}deg)${flip ? " scaleX(-1)" : ""}`, filter: "url(#ms-ink)" }}
>
{DOODLES[kind].map((s, k) => (
<path key={k} d={brush(s, weight, `${kind}-${k}-${x}`)} fill={color} />
))}
</svg>
);
/** A quick loop drawn around a box (page coordinates). */
const Loop: React.FC<{ x: number; y: number; w: number; h: number; color: string; seed: string; weight?: number; rot?: number }> = ({
x,
y,
w,
h,
color,
seed,
weight = 9,
rot = 0,
}) => (
<svg
width={w}
height={h}
style={{ position: "absolute", left: x, top: y, overflow: "visible", transform: `rotate(${rot}deg)`, filter: "url(#ms-ink)" }}
>
<path d={brush(loopPts(w / 2, h / 2, w / 2, h / 2, seed), weight, seed)} fill={color} />
</svg>
);
/* ---------------------------------------------------------------- surfaces */
const InkFilter: React.FC = () => (
<svg width="0" height="0" style={{ position: "absolute" }}>
<filter id="ms-ink" x="-10%" y="-10%" width="120%" height="120%">
<feTurbulence type="fractalNoise" baseFrequency="0.022" numOctaves="2" seed="3" result="wob" />
<feDisplacementMap in="SourceGraphic" in2="wob" scale="5" xChannelSelector="R" yChannelSelector="G" result="w" />
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="1" seed="8" result="grit" />
<feDisplacementMap in="w" in2="grit" scale="2.2" xChannelSelector="R" yChannelSelector="G" />
</filter>
</svg>
);
const Grain: React.FC<{ strength: number }> = ({ strength }) => (
<svg width="1080" height="1350" style={{ position: "absolute", inset: 0, mixBlendMode: "overlay", opacity: strength, pointerEvents: "none" }}>
<filter id="ms-grain">
<feTurbulence type="fractalNoise" baseFrequency="0.85" numOctaves="2" seed="21" />
<feColorMatrix type="saturate" values="0" />
</filter>
<rect width="1080" height="1350" filter="url(#ms-grain)" />
</svg>
);
/** Full-bleed photo with a direct-flash grade (hard contrast, falloff at the edges). */
const FlashPhoto: React.FC<{ src: string; position?: string; dark?: number }> = ({ src, position = "50% 50%", dark = 0 }) => (
<>
<Img
src={resolve(src)}
style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", objectPosition: position, filter: "contrast(1.1) saturate(1.08) brightness(1.02)" }}
/>
<div
style={{
position: "absolute",
inset: 0,
background: `radial-gradient(ellipse 78% 70% at 50% 46%, rgba(0,0,0,0) 52%, rgba(0,0,0,${0.42 + dark}) 100%)`,
}}
/>
<div
style={{
position: "absolute",
inset: 0,
background: `linear-gradient(rgba(0,0,0,${0.3 + dark}) 0%, rgba(0,0,0,${dark}) 20%, rgba(0,0,0,${dark}) 76%, rgba(0,0,0,${0.32 + dark}) 100%)`,
}}
/>
</>
);
/* ---------------------------------------------------------------- component */
export const MarkerScrawlCarousel: React.FC<MarkerScrawlCarouselProps> = ({
wordmark,
topLine,
coverLine,
coverScrawl,
dayWords,
dayScrawl,
photoNote,
photoCaps,
offerWords,
offerScrawl,
circleNote,
circleCaps,
menuTitle,
menuItems,
menuNote,
closingScrawl,
closingDetails,
closingCta,
coverImage,
images,
inkColor,
paperColor,
lightColor,
darkColor,
scriptFont,
displayFont,
markerWeight,
brandKit,
}) => {
const slide = useCurrentFrame();
const brandScript = useBrandFont(brandKit?.fonts?.[0]);
const brandDisplay = useBrandFont(brandKit?.fonts?.[1]);
const ink = brandKit?.colors?.[0] ?? inkColor;
const script = brandScript ? { family: brandScript, em: 0.45 } : SCRIPT[scriptFont] ?? SCRIPT.Kristi;
const display = brandDisplay ? { family: brandDisplay, em: 0.66, weight: 900, track: "-0.02em" } : DISPLAY[displayFont] ?? DISPLAY["Archivo Black"];
const pics = [coverImage, ...images].map((p) => (p ?? "").trim());
const photoFor = (n: number) => pics[n] || "";
/** Fit script text to a width (rough per-font character widths). */
const fitScript = (text: string, max: number, width: number) => Math.min(max, width / Math.max(1, text.length * script.em));
const Scrawl: React.FC<{ text: string; size: number; x?: number; y?: number; rot?: number; style?: React.CSSProperties; color?: string }> = ({
text,
size,
x,
y,
rot = -6,
style,
color = ink,
}) => (
<div
style={{
position: x === undefined ? "relative" : "absolute",
left: x,
top: y,
fontFamily: script.family,
fontSize: size,
lineHeight: 1,
color,
WebkitTextStroke: `${f1((size / 100) * markerWeight)}px ${color}`,
paintOrder: "stroke fill",
transform: `rotate(${rot}deg)`,
whiteSpace: "nowrap",
filter: "url(#ms-ink)",
...style,
}}
>
{text}
</div>
);
const Caps: React.FC<{ text: string; color: string; size?: number; style?: React.CSSProperties; shadow?: boolean }> = ({ text, color, size = 21, style, shadow }) => (
<div
style={{
fontFamily: capsFont,
fontWeight: 600,
fontSize: size,
letterSpacing: "0.34em",
textTransform: "uppercase",
color,
textShadow: shadow ? "0 1px 3px rgba(0,0,0,0.35)" : undefined,
whiteSpace: "nowrap",
...style,
}}
>
{text}
</div>
);
const TopLine: React.FC<{ onPhoto: boolean }> = ({ onPhoto }) => (
<div style={{ position: "absolute", top: 58, left: 0, right: 0, display: "flex", justifyContent: "center" }}>
<Caps text={topLine} color={onPhoto ? lightColor : darkColor} shadow={onPhoto} style={{ opacity: onPhoto ? 0.94 : 0.85, paddingLeft: "0.34em" }} />
</div>
);
/** Small wordmark signature, like the logo scrawled on every tile. */
const Signature: React.FC<{ x?: number; y: number; size?: number; rot?: number; align?: "center" | "right" | "left" }> = ({
y,
size = 104,
rot = -5,
align = "center",
x = 0,
}) => (
<div
style={{
position: "absolute",
top: y,
left: align === "left" ? x : 0,
right: align === "right" ? x : 0,
width: align === "center" ? "100%" : undefined,
display: "flex",
justifyContent: align === "center" ? "center" : align === "right" ? "flex-end" : "flex-start",
}}
>
<Scrawl text={wordmark} size={fitScript(wordmark, size, 560)} rot={rot} />
</div>
);
/** Huge grotesk words, one word per line, sized to fill the width. */
const BigWords: React.FC<{ text: string; color: string; children?: React.ReactNode; style?: React.CSSProperties; maxSize?: number }> = ({
text,
color,
children,
style,
maxSize = 360,
}) => {
const words = text.trim().toUpperCase().split(/\s+/).filter(Boolean);
const letter = (c: string) => (/[IJ1:.,'!|]/.test(c) ? 0.42 : /[MW]/.test(c) ? 1.12 : /[FLTEPSZ]/.test(c) ? 0.86 : 1);
const widest = Math.max(1, ...words.map((w) => [...w].reduce((a, c) => a + letter(c), 0)));
const size = Math.min(maxSize, 968 / (widest * display.em));
return (
<div style={{ position: "absolute", left: 54, right: 54, ...style }}>
<div style={{ position: "relative", display: "inline-block" }}>
{words.map((w, k) => (
<div
key={k}
style={{
fontFamily: display.family,
fontWeight: display.weight,
fontSize: size,
lineHeight: 0.86,
letterSpacing: display.track,
color,
whiteSpace: "nowrap",
}}
>
{w}
</div>
))}
{children}
</div>
</div>
);
};
const surface = (n: number, position?: string, dark?: number) => {
const src = photoFor(n);
return src ? <FlashPhoto src={src} position={position} dark={dark} /> : null;
};
let content: React.ReactNode;
let onPhoto = false;
if (slide <= 0) {
/* ---------- cover: the wordmark, huge, scrawled over a flash photo */
onPhoto = !!photoFor(0);
const size = fitScript(wordmark, 420, 1000);
content = (
<>
{surface(0, "50% 50%", 0.04)}
{!onPhoto ? (
<>
<Doodle kind="wineglass" x={780} y={170} size={220} rot={12} color={ink} />
<Doodle kind="fork" x={110} y={930} size={210} rot={-16} color={ink} />
<Doodle kind="olive" x={800} y={950} size={160} rot={8} color={ink} />
<Doodle kind="star" x={470} y={1010} size={90} rot={-8} color={ink} weight={7} />
</>
) : null}
<div style={{ position: "absolute", left: 0, right: 0, top: 640 - size * 0.62, display: "flex", justifyContent: "center" }}>
<div style={{ position: "relative" }}>
<Scrawl text={coverScrawl} size={fitScript(coverScrawl, 92, 480)} x={-10} y={-size * 0.5} rot={-9} />
<Scrawl text={wordmark} size={size} rot={-7} />
<Doodle kind="swash" x={size * 0.55} y={size * 0.5} size={Math.min(560, size * 1.5)} rot={-8} color={ink} weight={4} />
</div>
</div>
<div style={{ position: "absolute", left: 60, right: 60, bottom: 74, display: "flex", justifyContent: "center" }}>
<Caps text={coverLine} color={onPhoto ? lightColor : darkColor} shadow={onPhoto} size={20} />
</div>
</>
);
} else if (slide === 1) {
/* ---------- big words + scrawled time */
onPhoto = !!photoFor(1);
const txt = onPhoto ? lightColor : darkColor;
const sz = fitScript(dayScrawl, 250, 640);
content = (
<>
{surface(1, "50% 40%", 0.06)}
{!onPhoto ? <Doodle kind="wineglass" x={120} y={850} size={260} rot={-9} color={ink} weight={4.6} /> : null}
<BigWords text={dayWords} color={txt} style={{ top: 300 }}>
<Scrawl text={dayScrawl} size={sz} x={undefined} rot={-9} style={{ position: "absolute", right: -30, bottom: -sz * 0.62 }} />
</BigWords>
<Signature y={1170} size={96} />
</>
);
} else if (slide === 2) {
/* ---------- photo with a scrawled note and arrow */
onPhoto = !!photoFor(2);
content = (
<>
{surface(2, "50% 55%")}
{!onPhoto ? <Doodle kind="olive" x={430} y={520} size={460} rot={6} color={ink} weight={3.4} /> : null}
<Scrawl text={photoNote} size={fitScript(photoNote, 138, 760)} x={74} y={190} rot={-7} />
<Doodle kind="arrow" x={240} y={330} size={280} rot={-4} color={ink} weight={6} />
<div style={{ position: "absolute", left: 66, bottom: 82 }}>
<Caps text={photoCaps} color={onPhoto ? lightColor : darkColor} shadow={onPhoto} size={19} />
</div>
<Signature y={1150} size={92} align="right" x={60} rot={-6} />
</>
);
} else if (slide === 3) {
/* ---------- big words at the bottom, scrawl over the top of them */
onPhoto = !!photoFor(3);
const txt = onPhoto ? lightColor : darkColor;
const sz = fitScript(offerScrawl, 270, 600);
content = (
<>
{surface(3, "50% 45%", 0.06)}
{!onPhoto ? <Doodle kind="martini" x={590} y={190} size={330} rot={10} color={ink} weight={3.6} /> : null}
<BigWords text={offerWords} color={txt} style={{ bottom: 150 }}>
<Scrawl text={offerScrawl} size={sz} rot={-10} style={{ position: "absolute", right: 20, top: -sz * 0.62 }} />
</BigWords>
{onPhoto ? <Doodle kind="sparkle" x={70} y={170} size={86} rot={8} color={ink} weight={6} /> : null}
</>
);
} else if (slide === 4) {
/* ---------- photo with a hand-drawn circle around the moment */
onPhoto = !!photoFor(4);
const sz = fitScript(circleNote, 130, 820);
content = (
<>
{surface(4, "50% 50%")}
{!onPhoto ? <Doodle kind="wineglass" x={390} y={430} size={300} rot={-6} color={ink} weight={3.6} /> : null}
<Loop x={230} y={360} w={620} h={470} color={ink} seed="ring" weight={15} rot={-8} />
<div style={{ position: "absolute", left: 0, right: 0, top: 880, display: "flex", justifyContent: "center" }}>
<Scrawl text={circleNote} size={sz} rot={-5} />
</div>
<div style={{ position: "absolute", left: 60, right: 60, bottom: 78, display: "flex", justifyContent: "center" }}>
<Caps text={circleCaps} color={onPhoto ? lightColor : darkColor} shadow={onPhoto} size={19} />
</div>
</>
);
} else if (slide === 5) {
/* ---------- cream menu with doodles */
const n = menuItems.length;
const nameSize = n <= 4 ? 70 : n === 5 ? 62 : 52;
const gap = n <= 4 ? 34 : n === 5 ? 26 : 20;
content = (
<>
<Doodle kind="wineglass" x={560} y={120} size={230} rot={-10} color={ink} />
<Doodle kind="fork" x={800} y={100} size={230} rot={18} color={ink} />
<Doodle kind="olive" x={840} y={380} size={150} rot={-6} color={ink} weight={7} />
<Doodle kind="star" x={640} y={420} size={90} rot={12} color={ink} weight={8} />
<Scrawl text={menuTitle} size={fitScript(menuTitle, 340, 560)} x={60} y={160} rot={-7} />
<div style={{ position: "absolute", left: 76, right: 76, top: 560, display: "flex", flexDirection: "column", gap }}>
{menuItems.map((m, k) => (
<div key={k} style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<div
style={{
fontFamily: display.family,
fontWeight: display.weight,
fontSize: nameSize,
lineHeight: 0.95,
letterSpacing: display.track,
textTransform: "uppercase",
color: darkColor,
}}
>
{m.name}
</div>
<Caps text={m.detail} color={darkColor} size={18} style={{ opacity: 0.72 }} />
</div>
))}
</div>
<Doodle kind="arrow" x={690} y={1110} size={120} rot={200} color={ink} weight={6} />
<Scrawl text={menuNote} size={fitScript(menuNote, 104, 420)} x={600} y={1200} rot={-6} />
</>
);
} else {
/* ---------- closing: see you there */
onPhoto = !!photoFor(5);
const lines = closingScrawl.trim().split(/\s+/);
const mid = Math.ceil(lines.length / 2);
const l1 = lines.slice(0, mid).join(" ");
const l2 = lines.slice(mid).join(" ");
const sz = fitScript(l1.length > l2.length ? l1 : l2, 270, 900);
const txt = onPhoto ? lightColor : darkColor;
content = (
<>
{surface(5, "50% 50%", 0.28)}
{!onPhoto ? (
<>
<Doodle kind="star" x={110} y={170} size={110} rot={-12} color={ink} />
<Doodle kind="sparkle" x={860} y={240} size={80} rot={10} color={ink} weight={6} />
</>
) : null}
<div style={{ position: "absolute", left: 0, right: 0, top: 260, display: "flex", flexDirection: "column", alignItems: "center" }}>
<Scrawl text={l1} size={sz} rot={-7} style={{ marginLeft: -120 }} />
{l2 ? <Scrawl text={l2} size={sz} rot={-7} style={{ marginLeft: 160, marginTop: -sz * 0.18 }} /> : null}
</div>
<div style={{ position: "absolute", left: 0, right: 0, top: 880, display: "flex", flexDirection: "column", alignItems: "center", gap: 18 }}>
{closingDetails.map((d, k) => (
<Caps key={k} text={d} color={txt} shadow={onPhoto} size={24} style={{ paddingLeft: "0.34em" }} />
))}
</div>
<div style={{ position: "absolute", left: 0, right: 0, top: 1080, display: "flex", justifyContent: "center" }}>
<div style={{ position: "relative", padding: "30px 54px" }}>
<Caps text={closingCta} color={txt} shadow={onPhoto} size={21} style={{ paddingLeft: "0.34em" }} />
<Loop x={-6} y={-4} w={closingCta.length * 15 + 120} h={92} color={ink} seed="cta" weight={6} rot={-3} />
</div>
</div>
<Signature y={1215} size={84} />
</>
);
}
return (
<AbsoluteFill style={{ backgroundColor: paperColor, overflow: "hidden" }}>
<InkFilter />
{content}
<TopLine onPhoto={onPhoto} />
<Grain strength={onPhoto ? 0.22 : 0.3} />
</AbsoluteFill>
);
};
Remotion code you can paste anywhere, or install it: npx cutship add marker-scrawl-carousel
MIT License: use it in any video, personal or commercial. License
More from Cutship
Creator page →