feat: export quality UI and source-relative scaling

- Add 4th 'Original' quality tier to export UI
- Change non-source export to use source-relative scaling (0.6x, 0.75x, 0.9x)
- Update i18n for new quality label
- Accept 'high' in project normalization

Fixes blurry export and makes quality more intuitive
This commit is contained in:
webadderall
2026-03-16 21:49:06 +11:00
parent 3615e09e2c
commit 0ce344b103
7 changed files with 39 additions and 12 deletions
+11 -2
View File
@@ -813,7 +813,7 @@ export function SettingsPanel({
</div>
{exportFormat === 'mp4' && (
<div className="mb-3 bg-white/5 border border-white/5 p-0.5 w-full grid grid-cols-3 h-7 rounded-lg">
<div className="mb-3 bg-white/5 border border-white/5 p-0.5 w-full grid grid-cols-4 h-7 rounded-lg">
<button
onClick={() => onExportQualityChange?.('medium')}
className={cn(
@@ -832,6 +832,15 @@ export function SettingsPanel({
>
{tSettings('export.quality.medium')}
</button>
<button
onClick={() => onExportQualityChange?.('high')}
className={cn(
"rounded-md transition-all text-[10px] font-medium",
exportQuality === 'high' ? "bg-white text-black" : "text-slate-400 hover:text-slate-200"
)}
>
{tSettings('export.quality.high')}
</button>
<button
onClick={() => onExportQualityChange?.('source')}
className={cn(
@@ -839,7 +848,7 @@ export function SettingsPanel({
exportQuality === 'source' ? "bg-white text-black" : "text-slate-400 hover:text-slate-200"
)}
>
{tSettings('export.quality.high')}
{tSettings('export.quality.original')}
</button>
</div>
)}
+14 -5
View File
@@ -1525,12 +1525,21 @@ export default function VideoEditor() {
bitrate = 80_000_000;
}
} else {
// Use quality-based target resolution
const targetHeight = quality === 'medium' ? 720 : 1080;
// Use source-relative quality scaling.
// "source" is handled above; this branch maps the remaining tiers.
const qualityScale =
quality === 'medium' ? 0.6 : quality === 'good' ? 0.75 : 0.9;
const maxWidth = Math.max(2, Math.floor((sourceWidth * qualityScale) / 2) * 2);
const maxHeight = Math.max(2, Math.floor((sourceHeight * qualityScale) / 2) * 2);
const maxAspect = maxWidth / maxHeight;
// Calculate dimensions maintaining aspect ratio
exportHeight = Math.floor(targetHeight / 2) * 2;
exportWidth = Math.floor((exportHeight * aspectRatioValue) / 2) * 2;
if (aspectRatioValue >= maxAspect) {
exportWidth = maxWidth;
exportHeight = Math.max(2, Math.floor((exportWidth / aspectRatioValue) / 2) * 2);
} else {
exportHeight = maxHeight;
exportWidth = Math.max(2, Math.floor((exportHeight * aspectRatioValue) / 2) * 2);
}
// Adjust bitrate for lower resolutions
const totalPixels = exportWidth * exportHeight;
@@ -358,7 +358,13 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
(validAspectRatios.has(editor.aspectRatio as AspectRatio) || isCustomAspectRatio(editor.aspectRatio))
? (editor.aspectRatio as AspectRatio)
: "16:9",
exportQuality: editor.exportQuality === "medium" || editor.exportQuality === "source" ? editor.exportQuality : "good",
exportQuality:
editor.exportQuality === "medium" ||
editor.exportQuality === "good" ||
editor.exportQuality === "high" ||
editor.exportQuality === "source"
? editor.exportQuality
: "good",
exportFormat: editor.exportFormat === "gif" ? "gif" : "mp4",
gifFrameRate:
editor.gifFrameRate === 15 ||
+2 -1
View File
@@ -47,7 +47,8 @@
"quality": {
"low": "Low",
"medium": "Medium",
"high": "High"
"high": "High",
"original": "Original"
},
"loop": "Loop",
"outputDimensions": "Output: {{dimensions}}px",
+2 -1
View File
@@ -47,7 +47,8 @@
"quality": {
"low": "Baja",
"medium": "Media",
"high": "Alta"
"high": "Alta",
"original": "Original"
},
"loop": "Bucle",
"outputDimensions": "Salida: {{dimensions}}px",
+2 -1
View File
@@ -47,7 +47,8 @@
"quality": {
"low": "低",
"medium": "中",
"high": "高"
"high": "高",
"original": "原始"
},
"loop": "循环",
"outputDimensions": "输出:{{dimensions}}px",
+1 -1
View File
@@ -27,7 +27,7 @@ export interface VideoFrameData {
duration: number; // in microseconds
}
export type ExportQuality = 'medium' | 'good' | 'source';
export type ExportQuality = 'medium' | 'good' | 'high' | 'source';
// GIF Export Types
export type ExportFormat = 'mp4' | 'gif';