restyle the rest of the pages
This commit is contained in:
@@ -2,15 +2,49 @@ import { type Dispatch, type SetStateAction } from "react";
|
||||
import type { VideoMetadata } from "../../utils/types.ts";
|
||||
import Selector from "../Selector.tsx";
|
||||
import clsx from "clsx";
|
||||
import { inputClass, monoInputClass, kickerClass } from "../../utils/styles.ts";
|
||||
|
||||
type Props = {
|
||||
metadata: VideoMetadata;
|
||||
setMetadata: Dispatch<SetStateAction<VideoMetadata>>;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const inputClass = "border border-gray-300 bg-white rounded-md w-full p-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-colors";
|
||||
// Mirrors CompressionService.buildBitrate so the number shown is the number used:
|
||||
// 90% of the raw budget, audio capped at 128 kbit/s, video takes the rest.
|
||||
const BUDGET_MULTIPLIER = 0.9;
|
||||
const AUDIO_RATIO = 0.15;
|
||||
const MAX_AUDIO_BITRATE = 128;
|
||||
|
||||
export default function ConfigBox({ setMetadata, className }: Props) {
|
||||
type Estimate = { total: number; audio: number; video: number; bpp: number | null };
|
||||
|
||||
const estimateBitrate = (metadata: VideoMetadata): Estimate | null => {
|
||||
const { fileSize, duration, width, height, fps } = metadata;
|
||||
if (!fileSize || !duration || duration <= 0) return null;
|
||||
|
||||
const total = ((fileSize * 8) / duration) * BUDGET_MULTIPLIER;
|
||||
const audio = Math.min(total * AUDIO_RATIO, MAX_AUDIO_BITRATE);
|
||||
const video = total - audio;
|
||||
|
||||
// Bits per pixel per frame — the usual way to judge whether a bitrate suits a resolution.
|
||||
const bpp = width && height && fps ? (video * 1000) / (width * height * fps) : null;
|
||||
|
||||
return { total, audio, video, bpp };
|
||||
};
|
||||
|
||||
const formatBitrate = (kbps: number): string =>
|
||||
kbps >= 1000 ? `${(kbps / 1000).toFixed(1)} Mbit/s` : `${Math.round(kbps)} kbit/s`;
|
||||
|
||||
const verdictOf = (estimate: Estimate): { word: string; tone: string } => {
|
||||
if (estimate.video <= 0) return { word: "not enough room", tone: "text-live-red" };
|
||||
if (estimate.bpp === null) return { word: "—", tone: "text-muted" };
|
||||
if (estimate.bpp >= 0.15) return { word: "crisp", tone: "text-olive" };
|
||||
if (estimate.bpp >= 0.09) return { word: "clean", tone: "text-olive" };
|
||||
if (estimate.bpp >= 0.05) return { word: "soft", tone: "text-muted" };
|
||||
return { word: "very soft", tone: "text-live-red" };
|
||||
};
|
||||
|
||||
export default function ConfigBox({ metadata, setMetadata, className }: Props) {
|
||||
const updateRes = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const [width, height] = e.target.value.split(",").map(Number);
|
||||
setMetadata(prev => ({ ...prev, width, height }));
|
||||
@@ -21,15 +55,19 @@ export default function ConfigBox({ setMetadata, className }: Props) {
|
||||
};
|
||||
|
||||
const updateFileSize = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setMetadata(prev => ({ ...prev, fileSize: parseInt(e.target.value) * 1000 }));
|
||||
setMetadata(prev => ({ ...prev, fileSize: Math.max(1, parseInt(e.target.value) || 1) * 1000 }));
|
||||
};
|
||||
|
||||
const sizeMb = Math.round(metadata.fileSize / 1000);
|
||||
const estimate = estimateBitrate(metadata);
|
||||
const verdict = estimate && verdictOf(estimate);
|
||||
|
||||
return (
|
||||
<div className={clsx("flex flex-col gap-3 p-6", className)}>
|
||||
<h2 className="text-sm font-semibold text-gray-800 mb-1">Export Settings</h2>
|
||||
<div className={clsx("flex flex-col gap-4", className)}>
|
||||
<p className={kickerClass}>Export settings</p>
|
||||
|
||||
<Selector label="Resolution">
|
||||
<select id="resolution" name="resolution" defaultValue="1280,720"
|
||||
<select id="resolution" name="resolution" value={`${metadata.width},${metadata.height}`}
|
||||
onChange={updateRes} className={inputClass}>
|
||||
<option value="3840,2160">2160p</option>
|
||||
<option value="2560,1440">1440p</option>
|
||||
@@ -40,8 +78,8 @@ export default function ConfigBox({ setMetadata, className }: Props) {
|
||||
</select>
|
||||
</Selector>
|
||||
|
||||
<Selector label="FPS">
|
||||
<select id="fps" name="fps" defaultValue="30"
|
||||
<Selector label="Frames per second">
|
||||
<select id="fps" name="fps" value={String(metadata.fps)}
|
||||
onChange={updateFps} className={inputClass}>
|
||||
<option value="60">60</option>
|
||||
<option value="30">30</option>
|
||||
@@ -49,11 +87,35 @@ export default function ConfigBox({ setMetadata, className }: Props) {
|
||||
</select>
|
||||
</Selector>
|
||||
|
||||
<Selector label="File Size Limit (MB)">
|
||||
<input type="number" min="1" defaultValue="10"
|
||||
onChange={updateFileSize} className={inputClass}
|
||||
<Selector label="Size limit (MB)">
|
||||
<input type="number" min="1" value={sizeMb}
|
||||
onChange={updateFileSize} className={monoInputClass}
|
||||
/>
|
||||
</Selector>
|
||||
|
||||
{/* What that budget actually buys, at this length and resolution. */}
|
||||
<div className="flex flex-col gap-2 bg-fields rounded-field p-3.5">
|
||||
<div className="flex items-baseline justify-between gap-3">
|
||||
<p className={kickerClass}>Estimated bitrate</p>
|
||||
{verdict && <p className={clsx("font-body text-[12px]", verdict.tone)}>{verdict.word}</p>}
|
||||
</div>
|
||||
|
||||
{estimate ? (
|
||||
<>
|
||||
<p className="font-data text-[15px] text-text-primary tabular-nums">
|
||||
{formatBitrate(Math.max(0, estimate.video))}
|
||||
</p>
|
||||
<p className="font-body text-[12px] text-muted">
|
||||
video, plus {formatBitrate(estimate.audio)} audio — {sizeMb} MB
|
||||
across {metadata.duration.toFixed(1)}s
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="font-body text-[12px] text-muted">
|
||||
Pick a trim range to see what the size limit buys you.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user