Spaces:
Running
Running
File size: 6,743 Bytes
f75bd44 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | import { Group, Meta, Transcript, TweetExtra } from "common-utils";
import { AbsoluteFill } from "remotion";
import { RenderUtils } from "../RenderUtils";
function formatTweetDate(date: Date) {
const hours = date.getHours();
const minutes = date.getMinutes();
const ampm = hours >= 12 ? "PM" : "AM";
const hour12 = hours % 12 === 0 ? 12 : hours % 12;
const minStr = minutes.toString().padStart(2, "0");
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const month = months[date.getMonth()];
const day = date.getDate();
const year = date.getFullYear();
return `${hour12}:${minStr} ${ampm} · ${month} ${day}, ${year}`;
}
export default function TweetPoster({
transcript,
meta,
}: {
transcript: Transcript;
meta: Meta;
}) {
const textParts = transcript.audioCaption?.words;
if (!textParts) {
return (
<AbsoluteFill color="#ffffff">
<h1>
<div>Empty tweet text!</div>
</h1>
</AbsoluteFill>
);
}
const { width, height } = meta.resolution || { width: 1080, height: 1920 };
// Scale solely from WIDTH (as requested)
const W = Math.max(320, width); // guard for tiny canvases
// Outer padding around the card (kept modest so card fills width but not flush)
const outerPad = Math.max(Math.round(W * 0.04), 16); // 4% of width, min 16px
// Card internal padding
const boxPad = Math.max(Math.round(W * 0.03), 14); // 3% of width, min 14px
// Avatar + typography strictly from width
const avatarSize = Math.max(Math.round(W * 0.08), 56); // 8% of width
const fontNamePx = Math.round(W * 0.035); // display name
const fontUserPx = Math.round(W * 0.028); // @username
const fontMainPx = Math.round(W * 0.042); // tweet text
const fontDatePx = Math.round(W * 0.026); // footer date
// Reasonable clamps to avoid extremes on very large/small canvases
const clamp = (v: number, min: number, max: number) => Math.min(max, Math.max(min, v));
const fontName = `${clamp(fontNamePx, 16, 40)}px`;
const fontUsername = `${clamp(fontUserPx, 14, 32)}px`;
const fontMain = `${clamp(fontMainPx, 18, 48)}px`;
const fontDate = `${clamp(fontDatePx, 12, 28)}px`;
let bgColor = (transcript.extras as TweetExtra)?.textBgColor || "#ffffff";
const fullName = textParts[0]?.word;
const fullNameStyle = textParts[0]?.textStyle;
const username = textParts[1]?.word;
const usernameStyle = textParts[1]?.textStyle;
const tweetLine1 = textParts[2]?.word;
const tweetLine1Style = textParts[2]?.textStyle;
const tweetLine2 = textParts[3]?.word;
const tweetLine2Style = textParts[3]?.textStyle;
const avatar = RenderUtils.tryStaticFile(transcript.mediaAbsPaths?.[0]?.path);
return (
<AbsoluteFill
style={{
background: bgColor,
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
height: "100%",
boxSizing: "border-box",
padding: 0,
}}
>
{/* Outer pad keeps some breathing room; box fills remaining width */}
<div
style={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
boxSizing: "border-box",
padding: outerPad,
}}
>
{/* Tweet box now fully fits parent width with the outerPad margin */}
<div
style={{
width: "100%",
// height is auto so small/large content won't crop;
// allow it to grow and just keep nice rounding/shadow
background: "#fff",
border: "1px solid #e1e8ed",
borderRadius: Math.max(Math.round(W * 0.02), 16),
boxShadow: `0 ${Math.round(W * 0.04)}px ${Math.round(
W * 0.08
)}px rgba(0,0,0,0.10)`,
padding: `${boxPad}px ${Math.round(boxPad * 1.2)}px`,
fontFamily: "Segoe UI, Arial, sans-serif",
boxSizing: "border-box",
display: "flex",
flexDirection: "column",
justifyContent: "flex-start",
// Prevent cropping: let content define height; wrap long words
overflow: "visible",
wordBreak: "break-word",
}}
>
{/* Header */}
<div
style={{
display: "flex",
alignItems: "center",
marginBottom: boxPad,
minWidth: 0,
}}
>
<img
src={avatar}
alt="avatar"
style={{
width: avatarSize,
height: avatarSize,
minWidth: 40,
minHeight: 40,
borderRadius: "50%",
marginRight: boxPad,
border: "2px solid #e1e8ed",
objectFit: "cover",
flexShrink: 0,
}}
/>
<div style={{ minWidth: 0 }}>
<div
style={{
fontWeight: 700,
fontSize: fontName,
lineHeight: 1.15,
whiteSpace: "pre-wrap",
...fullNameStyle,
}}
>
{fullName}
</div>
<div
style={{
color: "#657786",
fontSize: fontUsername,
lineHeight: 1.15,
whiteSpace: "pre-wrap",
...usernameStyle,
}}
>
{username}
</div>
</div>
</div>
{/* Tweet text */}
<div
style={{
fontSize: fontMain,
lineHeight: 1.45,
marginBottom: boxPad,
whiteSpace: "pre-wrap",
}}
>
<span style={tweetLine1Style}>{tweetLine1}</span>
{tweetLine2 && (
<>
{"\n"}
<span style={tweetLine2Style}>{tweetLine2}</span>
</>
)}
</div>
{/* Footer */}
<div
style={{
color: "#657786",
fontSize: fontDate,
marginTop: Math.round(boxPad * 0.5),
display: "flex",
gap: Math.round(W * 0.01),
alignItems: "baseline",
flexWrap: "wrap",
}}
>
<span>{formatTweetDate(new Date())}</span>
<span>·</span>
<span style={{ color: "#1da1f2" }}>Follow</span>
</div>
</div>
</div>
</AbsoluteFill>
);
}
|