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

View File

@ -57,14 +57,18 @@ function ProjectSettingsTabs() {
}
function ProjectInfoView() {
const { activeProject, updateProjectFps } = useProjectStore();
const { canvasPresets, setCanvasSize } = useEditorStore();
const { activeProject, updateProjectFps, updateCanvasSize } =
useProjectStore();
const { canvasPresets } = useEditorStore();
const { getDisplayName } = useAspectRatio();
const handleAspectRatioChange = (value: string) => {
const preset = canvasPresets.find((p) => p.name === value);
if (preset) {
setCanvasSize({ width: preset.width, height: preset.height });
updateCanvasSize(
{ width: preset.width, height: preset.height },
"preset"
);
}
};

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 };
}

View File

@ -1,7 +1,7 @@
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 const PLATFORM_LAYOUTS: Record<PlatformLayout, string> = {
@ -17,9 +17,7 @@ interface EditorState {
isInitializing: boolean;
isPanelsReady: boolean;
// Canvas/Project settings
canvasSize: CanvasSize;
canvasMode: CanvasMode;
// Editor UI settings
canvasPresets: CanvasPreset[];
layoutGuide: LayoutGuideSettings;
@ -27,9 +25,6 @@ interface EditorState {
setInitializing: (loading: boolean) => void;
setPanelsReady: (ready: boolean) => void;
initializeApp: () => Promise<void>;
setCanvasSize: (size: CanvasSize) => void;
setCanvasSizeToOriginal: (aspectRatio: number) => void;
setCanvasSizeFromAspectRatio: (aspectRatio: number) => void;
setLayoutGuide: (settings: Partial<LayoutGuideSettings>) => void;
toggleLayoutGuide: (platform: PlatformLayout) => void;
}
@ -41,96 +36,56 @@ const DEFAULT_CANVAS_PRESETS: CanvasPreset[] = [
{ name: "4:3", width: 1440, height: 1080 },
];
// Helper function to find the best matching canvas preset for an aspect ratio
const 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 };
};
export const useEditorStore = create<EditorState>((set, get) => ({
// Initial states
isInitializing: true,
isPanelsReady: false,
canvasSize: { width: 1920, height: 1080 }, // Default 16:9 HD
canvasMode: "preset" as CanvasMode,
canvasPresets: DEFAULT_CANVAS_PRESETS,
layoutGuide: {
platform: null,
},
// Actions
setInitializing: (loading) => {
set({ isInitializing: loading });
},
setPanelsReady: (ready) => {
set({ isPanelsReady: ready });
},
initializeApp: async () => {
console.log("Initializing video editor...");
set({ isInitializing: true, isPanelsReady: false });
set({ isPanelsReady: true, isInitializing: false });
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) => {
set((state) => ({
export const useEditorStore = create<EditorState>()(
persist(
(set) => ({
// Initial states
isInitializing: true,
isPanelsReady: false,
canvasPresets: DEFAULT_CANVAS_PRESETS,
layoutGuide: {
...state.layoutGuide,
...settings,
platform: null,
},
}));
},
toggleLayoutGuide: (platform) => {
set((state) => ({
layoutGuide: {
platform: state.layoutGuide.platform === platform ? null : platform,
// Actions
setInitializing: (loading) => {
set({ isInitializing: loading });
},
}));
},
}));
setPanelsReady: (ready) => {
set({ isPanelsReady: ready });
},
initializeApp: async () => {
console.log("Initializing video editor...");
set({ isInitializing: true, isPanelsReady: false });
set({ isPanelsReady: true, isInitializing: false });
console.log("Video editor ready");
},
setLayoutGuide: (settings) => {
set((state) => ({
layoutGuide: {
...state.layoutGuide,
...settings,
},
}));
},
toggleLayoutGuide: (platform) => {
set((state) => ({
layoutGuide: {
platform: state.layoutGuide.platform === platform ? null : platform,
},
}));
},
}),
{
name: "editor-settings",
partialize: (state) => ({
layoutGuide: state.layoutGuide,
}),
}
)
);

View File

@ -5,6 +5,22 @@ import { toast } from "sonner";
import { useMediaStore } from "./media-store";
import { useTimelineStore } from "./timeline-store";
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 {
activeProject: TProject | null;
@ -28,6 +44,7 @@ interface ProjectStore {
options?: { backgroundColor?: string; blurIntensity?: number }
) => Promise<void>;
updateProjectFps: (fps: number) => Promise<void>;
updateCanvasSize: (size: CanvasSize, mode: CanvasMode) => Promise<void>;
// Bookmark methods
toggleBookmark: (time: number) => Promise<void>;
@ -144,18 +161,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
},
createNewProject: async (name: string) => {
const newProject: TProject = {
id: generateUUID(),
name,
thumbnail: "",
createdAt: new Date(),
updatedAt: new Date(),
backgroundColor: "#000000",
backgroundType: "color",
blurIntensity: 8,
bookmarks: [],
fps: 30,
};
const newProject: TProject = { ...DEFAULT_PROJECT, name };
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) => {
const { savedProjects } = get();

View File

@ -10,17 +10,16 @@ import {
ensureMainTrack,
validateElementTrackCompatibility,
} from "@/types/timeline";
import { useEditorStore } from "./editor-store";
import {
useMediaStore,
getMediaAspectRatio,
type MediaItem,
} from "./media-store";
import { findBestCanvasPreset } from "@/lib/editor-utils";
import { storageService } from "@/lib/storage/storage-service";
import { useProjectStore } from "./project-store";
import { generateUUID } from "@/lib/utils";
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
import { toast } from "sonner";
import { checkElementOverlaps, resolveElementOverlaps } from "@/lib/timeline";
// Helper function to manage element naming with suffixes
@ -533,9 +532,10 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
mediaItem &&
(mediaItem.type === "image" || mediaItem.type === "video")
) {
const editorStore = useEditorStore.getState();
editorStore.setCanvasSizeFromAspectRatio(
getMediaAspectRatio(mediaItem)
const projectStore = useProjectStore.getState();
projectStore.updateCanvasSize(
findBestCanvasPreset(getMediaAspectRatio(mediaItem)),
"original"
);
}

View File

@ -1,5 +1,7 @@
export type BackgroundType = "blur" | "mirror" | "color";
export type CanvasMode = "preset" | "original" | "custom";
export interface CanvasSize {
width: number;
height: number;

View File

@ -1,3 +1,5 @@
import { CanvasSize } from "./editor";
export interface TProject {
id: string;
name: string;
@ -10,4 +12,12 @@ export interface TProject {
blurIntensity?: number; // in pixels (4, 8, 18)
fps?: 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";
}