refactor: move canvas size to be a project-level setting instead of editor-level

This commit is contained in:
Maze Winther
2025-08-04 20:45:48 +02:00
parent f0dd30ebfc
commit b0329f1372
7 changed files with 163 additions and 117 deletions
@@ -57,14 +57,18 @@ function ProjectSettingsTabs() {
} }
function ProjectInfoView() { function ProjectInfoView() {
const { activeProject, updateProjectFps } = useProjectStore(); const { activeProject, updateProjectFps, updateCanvasSize } =
const { canvasPresets, setCanvasSize } = useEditorStore(); useProjectStore();
const { canvasPresets } = useEditorStore();
const { getDisplayName } = useAspectRatio(); const { getDisplayName } = useAspectRatio();
const handleAspectRatioChange = (value: string) => { const handleAspectRatioChange = (value: string) => {
const preset = canvasPresets.find((p) => p.name === value); const preset = canvasPresets.find((p) => p.name === value);
if (preset) { if (preset) {
setCanvasSize({ width: preset.width, height: preset.height }); updateCanvasSize(
{ width: preset.width, height: preset.height },
"preset"
);
} }
}; };
+46
View File
@@ -0,0 +1,46 @@
import { CanvasSize } from "@/types/editor";
const DEFAULT_CANVAS_PRESETS = [
{ name: "16:9", width: 1920, height: 1080 },
{ name: "9:16", width: 1080, height: 1920 },
{ name: "1:1", width: 1080, height: 1080 },
{ name: "4:3", width: 1440, height: 1080 },
];
/**
* Helper function to find the best matching canvas preset for an aspect ratio
* @param aspectRatio The target aspect ratio to match
* @returns The best matching canvas size
*/
export function findBestCanvasPreset(aspectRatio: number): CanvasSize {
// Calculate aspect ratio for each preset and find the closest match
let bestMatch = DEFAULT_CANVAS_PRESETS[0]; // Default to 16:9 HD
let smallestDifference = Math.abs(
aspectRatio - bestMatch.width / bestMatch.height
);
for (const preset of DEFAULT_CANVAS_PRESETS) {
const presetAspectRatio = preset.width / preset.height;
const difference = Math.abs(aspectRatio - presetAspectRatio);
if (difference < smallestDifference) {
smallestDifference = difference;
bestMatch = preset;
}
}
// If the difference is still significant (> 0.1), create a custom size
// based on the media aspect ratio with a reasonable resolution
const bestAspectRatio = bestMatch.width / bestMatch.height;
if (Math.abs(aspectRatio - bestAspectRatio) > 0.1) {
// Create custom dimensions based on the aspect ratio
if (aspectRatio > 1) {
// Landscape - use 1920 width
return { width: 1920, height: Math.round(1920 / aspectRatio) };
}
// Portrait or square - use 1080 height
return { width: Math.round(1080 * aspectRatio), height: 1080 };
}
return { width: bestMatch.width, height: bestMatch.height };
}
+15 -60
View File
@@ -1,7 +1,7 @@
import { create } from "zustand"; import { create } from "zustand";
import { CanvasSize, CanvasPreset } from "@/types/editor"; import { persist } from "zustand/middleware";
import { CanvasPreset } from "@/types/editor";
type CanvasMode = "preset" | "original" | "custom";
export type PlatformLayout = "tiktok"; export type PlatformLayout = "tiktok";
export const PLATFORM_LAYOUTS: Record<PlatformLayout, string> = { export const PLATFORM_LAYOUTS: Record<PlatformLayout, string> = {
@@ -17,9 +17,7 @@ interface EditorState {
isInitializing: boolean; isInitializing: boolean;
isPanelsReady: boolean; isPanelsReady: boolean;
// Canvas/Project settings // Editor UI settings
canvasSize: CanvasSize;
canvasMode: CanvasMode;
canvasPresets: CanvasPreset[]; canvasPresets: CanvasPreset[];
layoutGuide: LayoutGuideSettings; layoutGuide: LayoutGuideSettings;
@@ -27,9 +25,6 @@ interface EditorState {
setInitializing: (loading: boolean) => void; setInitializing: (loading: boolean) => void;
setPanelsReady: (ready: boolean) => void; setPanelsReady: (ready: boolean) => void;
initializeApp: () => Promise<void>; initializeApp: () => Promise<void>;
setCanvasSize: (size: CanvasSize) => void;
setCanvasSizeToOriginal: (aspectRatio: number) => void;
setCanvasSizeFromAspectRatio: (aspectRatio: number) => void;
setLayoutGuide: (settings: Partial<LayoutGuideSettings>) => void; setLayoutGuide: (settings: Partial<LayoutGuideSettings>) => void;
toggleLayoutGuide: (platform: PlatformLayout) => void; toggleLayoutGuide: (platform: PlatformLayout) => void;
} }
@@ -41,46 +36,12 @@ const DEFAULT_CANVAS_PRESETS: CanvasPreset[] = [
{ name: "4:3", width: 1440, height: 1080 }, { name: "4:3", width: 1440, height: 1080 },
]; ];
// Helper function to find the best matching canvas preset for an aspect ratio export const useEditorStore = create<EditorState>()(
const findBestCanvasPreset = (aspectRatio: number): CanvasSize => { persist(
// Calculate aspect ratio for each preset and find the closest match (set) => ({
let bestMatch = DEFAULT_CANVAS_PRESETS[0]; // Default to 16:9 HD
let smallestDifference = Math.abs(
aspectRatio - bestMatch.width / bestMatch.height
);
for (const preset of DEFAULT_CANVAS_PRESETS) {
const presetAspectRatio = preset.width / preset.height;
const difference = Math.abs(aspectRatio - presetAspectRatio);
if (difference < smallestDifference) {
smallestDifference = difference;
bestMatch = preset;
}
}
// If the difference is still significant (> 0.1), create a custom size
// based on the media aspect ratio with a reasonable resolution
const bestAspectRatio = bestMatch.width / bestMatch.height;
if (Math.abs(aspectRatio - bestAspectRatio) > 0.1) {
// Create custom dimensions based on the aspect ratio
if (aspectRatio > 1) {
// Landscape - use 1920 width
return { width: 1920, height: Math.round(1920 / aspectRatio) };
}
// Portrait or square - use 1080 height
return { width: Math.round(1080 * aspectRatio), height: 1080 };
}
return { width: bestMatch.width, height: bestMatch.height };
};
export const useEditorStore = create<EditorState>((set, get) => ({
// Initial states // Initial states
isInitializing: true, isInitializing: true,
isPanelsReady: false, isPanelsReady: false,
canvasSize: { width: 1920, height: 1080 }, // Default 16:9 HD
canvasMode: "preset" as CanvasMode,
canvasPresets: DEFAULT_CANVAS_PRESETS, canvasPresets: DEFAULT_CANVAS_PRESETS,
layoutGuide: { layoutGuide: {
platform: null, platform: null,
@@ -103,20 +64,6 @@ export const useEditorStore = create<EditorState>((set, get) => ({
console.log("Video editor ready"); console.log("Video editor ready");
}, },
setCanvasSize: (size) => {
set({ canvasSize: size, canvasMode: "preset" });
},
setCanvasSizeToOriginal: (aspectRatio) => {
const newCanvasSize = findBestCanvasPreset(aspectRatio);
set({ canvasSize: newCanvasSize, canvasMode: "original" });
},
setCanvasSizeFromAspectRatio: (aspectRatio) => {
const newCanvasSize = findBestCanvasPreset(aspectRatio);
set({ canvasSize: newCanvasSize, canvasMode: "custom" });
},
setLayoutGuide: (settings) => { setLayoutGuide: (settings) => {
set((state) => ({ set((state) => ({
layoutGuide: { layoutGuide: {
@@ -133,4 +80,12 @@ export const useEditorStore = create<EditorState>((set, get) => ({
}, },
})); }));
}, },
})); }),
{
name: "editor-settings",
partialize: (state) => ({
layoutGuide: state.layoutGuide,
}),
}
)
);
+41 -12
View File
@@ -5,6 +5,22 @@ import { toast } from "sonner";
import { useMediaStore } from "./media-store"; import { useMediaStore } from "./media-store";
import { useTimelineStore } from "./timeline-store"; import { useTimelineStore } from "./timeline-store";
import { generateUUID } from "@/lib/utils"; import { generateUUID } from "@/lib/utils";
import { CanvasSize, CanvasMode } from "@/types/editor";
const DEFAULT_PROJECT: TProject = {
id: generateUUID(),
name: "Untitled",
thumbnail: "",
createdAt: new Date(),
updatedAt: new Date(),
backgroundColor: "#000000",
backgroundType: "color",
blurIntensity: 8,
bookmarks: [],
fps: 30,
canvasSize: { width: 1920, height: 1080 },
canvasMode: "preset",
};
interface ProjectStore { interface ProjectStore {
activeProject: TProject | null; activeProject: TProject | null;
@@ -28,6 +44,7 @@ interface ProjectStore {
options?: { backgroundColor?: string; blurIntensity?: number } options?: { backgroundColor?: string; blurIntensity?: number }
) => Promise<void>; ) => Promise<void>;
updateProjectFps: (fps: number) => Promise<void>; updateProjectFps: (fps: number) => Promise<void>;
updateCanvasSize: (size: CanvasSize, mode: CanvasMode) => Promise<void>;
// Bookmark methods // Bookmark methods
toggleBookmark: (time: number) => Promise<void>; toggleBookmark: (time: number) => Promise<void>;
@@ -144,18 +161,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
}, },
createNewProject: async (name: string) => { createNewProject: async (name: string) => {
const newProject: TProject = { const newProject: TProject = { ...DEFAULT_PROJECT, name };
id: generateUUID(),
name,
thumbnail: "",
createdAt: new Date(),
updatedAt: new Date(),
backgroundColor: "#000000",
backgroundType: "color",
blurIntensity: 8,
bookmarks: [],
fps: 30,
};
set({ activeProject: newProject }); set({ activeProject: newProject });
@@ -428,6 +434,29 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
} }
}, },
updateCanvasSize: async (size: CanvasSize, mode: CanvasMode) => {
const { activeProject } = get();
if (!activeProject) return;
const updatedProject = {
...activeProject,
canvasSize: size,
canvasMode: mode,
updatedAt: new Date(),
};
try {
await storageService.saveProject(updatedProject);
set({ activeProject: updatedProject });
await get().loadAllProjects(); // Refresh the list
} catch (error) {
console.error("Failed to update canvas size:", error);
toast.error("Failed to update canvas size", {
description: "Please try again",
});
}
},
getFilteredAndSortedProjects: (searchQuery: string, sortOption: string) => { getFilteredAndSortedProjects: (searchQuery: string, sortOption: string) => {
const { savedProjects } = get(); const { savedProjects } = get();
+5 -5
View File
@@ -10,17 +10,16 @@ import {
ensureMainTrack, ensureMainTrack,
validateElementTrackCompatibility, validateElementTrackCompatibility,
} from "@/types/timeline"; } from "@/types/timeline";
import { useEditorStore } from "./editor-store";
import { import {
useMediaStore, useMediaStore,
getMediaAspectRatio, getMediaAspectRatio,
type MediaItem, type MediaItem,
} from "./media-store"; } from "./media-store";
import { findBestCanvasPreset } from "@/lib/editor-utils";
import { storageService } from "@/lib/storage/storage-service"; import { storageService } from "@/lib/storage/storage-service";
import { useProjectStore } from "./project-store"; import { useProjectStore } from "./project-store";
import { generateUUID } from "@/lib/utils"; import { generateUUID } from "@/lib/utils";
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
import { toast } from "sonner";
import { checkElementOverlaps, resolveElementOverlaps } from "@/lib/timeline"; import { checkElementOverlaps, resolveElementOverlaps } from "@/lib/timeline";
// Helper function to manage element naming with suffixes // Helper function to manage element naming with suffixes
@@ -533,9 +532,10 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
mediaItem && mediaItem &&
(mediaItem.type === "image" || mediaItem.type === "video") (mediaItem.type === "image" || mediaItem.type === "video")
) { ) {
const editorStore = useEditorStore.getState(); const projectStore = useProjectStore.getState();
editorStore.setCanvasSizeFromAspectRatio( projectStore.updateCanvasSize(
getMediaAspectRatio(mediaItem) findBestCanvasPreset(getMediaAspectRatio(mediaItem)),
"original"
); );
} }
+2
View File
@@ -1,5 +1,7 @@
export type BackgroundType = "blur" | "mirror" | "color"; export type BackgroundType = "blur" | "mirror" | "color";
export type CanvasMode = "preset" | "original" | "custom";
export interface CanvasSize { export interface CanvasSize {
width: number; width: number;
height: number; height: number;
+10
View File
@@ -1,3 +1,5 @@
import { CanvasSize } from "./editor";
export interface TProject { export interface TProject {
id: string; id: string;
name: string; name: string;
@@ -10,4 +12,12 @@ export interface TProject {
blurIntensity?: number; // in pixels (4, 8, 18) blurIntensity?: number; // in pixels (4, 8, 18)
fps?: number; fps?: number;
bookmarks?: number[]; bookmarks?: number[];
canvasSize: CanvasSize;
/**
* Tracks how the canvas size was set:
* - "preset": User selected a standard preset (e.g. 16:9, 9:16)
* - "original": Using the first media item's dimensions
* - "custom": User set a custom aspect ratio or dimensions
*/
canvasMode: "preset" | "original" | "custom";
} }