mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
refactor: move canvas size to be a project-level setting instead of editor-level
This commit is contained in:
@@ -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"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user