mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
feat: masks, properties refactor, shaders, storage migrations, and more
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
import { PlusSignIcon, RulerIcon } from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { GuideDefinition } from "@/lib/guides/types";
|
||||
|
||||
function CustomGuideOptions() {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" className="flex-1">
|
||||
<HugeiconsIcon icon={PlusSignIcon} />
|
||||
Add guide line
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const customGuide = {
|
||||
id: "custom",
|
||||
label: "Custom",
|
||||
renderPreview: () => <HugeiconsIcon size={16} icon={RulerIcon} />,
|
||||
renderTriggerIcon: () => <HugeiconsIcon icon={RulerIcon} />,
|
||||
renderOverlay: () => null,
|
||||
renderOptions: () => <CustomGuideOptions />,
|
||||
} as const satisfies GuideDefinition;
|
||||
@@ -0,0 +1,125 @@
|
||||
import {
|
||||
GridTableIcon,
|
||||
LayoutThreeColumnIcon,
|
||||
LayoutThreeRowIcon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { NumberField } from "@/components/ui/number-field";
|
||||
import {
|
||||
GRID_MIN,
|
||||
GRID_MAX,
|
||||
DEFAULT_GRID_CONFIG,
|
||||
} from "@/constants/guide-constants";
|
||||
import { usePreviewStore } from "@/stores/preview-store";
|
||||
import { clampRound } from "@/utils/math";
|
||||
import { cn } from "@/utils/ui";
|
||||
import type { GuideDefinition } from "@/lib/guides/types";
|
||||
|
||||
function GridLines({
|
||||
rows,
|
||||
cols,
|
||||
color,
|
||||
}: {
|
||||
rows: number;
|
||||
cols: number;
|
||||
color: string;
|
||||
}) {
|
||||
const verticals = Array.from(
|
||||
{ length: cols - 1 },
|
||||
(_, i) => ((i + 1) / cols) * 100,
|
||||
);
|
||||
const horizontals = Array.from(
|
||||
{ length: rows - 1 },
|
||||
(_, i) => ((i + 1) / rows) * 100,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{verticals.map((pct) => (
|
||||
<div
|
||||
key={`v-${pct}`}
|
||||
className={cn("absolute top-0 bottom-0 w-px", color)}
|
||||
style={{ left: `${pct}%` }}
|
||||
/>
|
||||
))}
|
||||
{horizontals.map((pct) => (
|
||||
<div
|
||||
key={`h-${pct}`}
|
||||
className={cn("absolute left-0 right-0 h-px", color)}
|
||||
style={{ top: `${pct}%` }}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function GridGuidePreview() {
|
||||
return (
|
||||
<div className="relative aspect-video w-full">
|
||||
<GridLines rows={3} cols={4} color="bg-foreground/15" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GridGuideOverlay() {
|
||||
const { rows, cols } = usePreviewStore((s) => s.gridConfig);
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0">
|
||||
<GridLines rows={rows} cols={cols} color="bg-white/35" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GridGuideOptions() {
|
||||
const rows = usePreviewStore((s) => s.gridConfig.rows);
|
||||
const cols = usePreviewStore((s) => s.gridConfig.cols);
|
||||
const setGridConfig = usePreviewStore((s) => s.setGridConfig);
|
||||
|
||||
const clampGridValue = (value: number) =>
|
||||
clampRound({ value, min: GRID_MIN, max: GRID_MAX });
|
||||
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<NumberField
|
||||
icon={<HugeiconsIcon icon={LayoutThreeRowIcon} />}
|
||||
value={rows}
|
||||
min={GRID_MIN}
|
||||
max={GRID_MAX}
|
||||
isDefault={rows === DEFAULT_GRID_CONFIG.rows}
|
||||
onReset={() => setGridConfig({ rows: DEFAULT_GRID_CONFIG.rows })}
|
||||
onScrub={(value) => setGridConfig({ rows: clampGridValue(value) })}
|
||||
onChange={(event) => {
|
||||
const parsed = Number.parseInt(event.target.value, 10);
|
||||
if (!Number.isNaN(parsed))
|
||||
setGridConfig({ rows: clampGridValue(parsed) });
|
||||
}}
|
||||
className="flex-1"
|
||||
/>
|
||||
<NumberField
|
||||
icon={<HugeiconsIcon icon={LayoutThreeColumnIcon} />}
|
||||
value={cols}
|
||||
min={GRID_MIN}
|
||||
max={GRID_MAX}
|
||||
isDefault={cols === DEFAULT_GRID_CONFIG.cols}
|
||||
onReset={() => setGridConfig({ cols: DEFAULT_GRID_CONFIG.cols })}
|
||||
onScrub={(value) => setGridConfig({ cols: clampGridValue(value) })}
|
||||
onChange={(event) => {
|
||||
const parsed = Number.parseInt(event.target.value, 10);
|
||||
if (!Number.isNaN(parsed))
|
||||
setGridConfig({ cols: clampGridValue(parsed) });
|
||||
}}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const gridGuide = {
|
||||
id: "grid",
|
||||
label: "Grid",
|
||||
renderPreview: () => <GridGuidePreview />,
|
||||
renderTriggerIcon: () => <HugeiconsIcon icon={GridTableIcon} />,
|
||||
renderOverlay: () => <GridGuideOverlay />,
|
||||
renderOptions: () => <GridGuideOptions />,
|
||||
} as const satisfies GuideDefinition;
|
||||
@@ -0,0 +1,65 @@
|
||||
import Image from "next/image";
|
||||
import type { GuideDefinition } from "@/lib/guides/types";
|
||||
import { TikTokLayout } from "./tiktok-layout";
|
||||
|
||||
function PlatformLogo({
|
||||
domain,
|
||||
className = "size-4",
|
||||
}: {
|
||||
domain: string;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<Image
|
||||
src={`https://cdn.brandfetch.io/${domain}/w/64/h/64`}
|
||||
alt=""
|
||||
width={18}
|
||||
height={18}
|
||||
className={className}
|
||||
draggable={false}
|
||||
unoptimized
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function PlatformGuidePreview({ domain }: { domain: string }) {
|
||||
return <PlatformLogo domain={domain} />;
|
||||
}
|
||||
|
||||
function platformGuide({
|
||||
id,
|
||||
label,
|
||||
domain,
|
||||
}: {
|
||||
id: string;
|
||||
label: string;
|
||||
domain: string;
|
||||
}): GuideDefinition {
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
renderPreview: () => <PlatformGuidePreview domain={domain} />,
|
||||
renderTriggerIcon: () => <PlatformLogo domain={domain} />,
|
||||
renderOverlay: () => null,
|
||||
};
|
||||
}
|
||||
|
||||
export const tiktokGuide: GuideDefinition = {
|
||||
...platformGuide({ id: "tiktok", label: "TikTok", domain: "tiktok.com" }),
|
||||
renderOverlay: () => <TikTokLayout />,
|
||||
};
|
||||
export const igReelsGuide = platformGuide({
|
||||
id: "ig-reels",
|
||||
label: "Reels",
|
||||
domain: "instagram.com",
|
||||
});
|
||||
export const ytShortsGuide = platformGuide({
|
||||
id: "yt-shorts",
|
||||
label: "Shorts",
|
||||
domain: "youtube.com",
|
||||
});
|
||||
export const spotlightGuide = platformGuide({
|
||||
id: "spotlight",
|
||||
label: "Spotlight",
|
||||
domain: "snapchat.com",
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,13 @@
|
||||
import { GUIDE_REGISTRY } from "./registry";
|
||||
import type { GuideDefinition } from "@/lib/guides/types";
|
||||
|
||||
export { GUIDE_REGISTRY, isGuideId } from "./registry";
|
||||
export type { GuideDefinition, GuideId, GuideRenderProps } from "./registry";
|
||||
|
||||
export function getGuideById(guideId: string | null): GuideDefinition | null {
|
||||
if (!guideId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return GUIDE_REGISTRY.find((guide) => guide.id === guideId) ?? null;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { GuideDefinition } from "@/lib/guides/types";
|
||||
import { gridGuide } from "./definitions/grid";
|
||||
// import { customGuide } from "./definitions/custom";
|
||||
import {
|
||||
tiktokGuide,
|
||||
igReelsGuide,
|
||||
ytShortsGuide,
|
||||
spotlightGuide,
|
||||
} from "./definitions/platforms";
|
||||
|
||||
export type { GuideDefinition, GuideRenderProps } from "@/lib/guides/types";
|
||||
|
||||
export const GUIDE_REGISTRY = [
|
||||
gridGuide,
|
||||
tiktokGuide,
|
||||
igReelsGuide,
|
||||
ytShortsGuide,
|
||||
spotlightGuide,
|
||||
|
||||
// todo: wire up custom guide fully, then uncomment this:
|
||||
// customGuide,
|
||||
] as const satisfies readonly GuideDefinition[];
|
||||
|
||||
export type GuideId = (typeof GUIDE_REGISTRY)[number]["id"];
|
||||
|
||||
export function isGuideId(value: string): value is GuideId {
|
||||
return GUIDE_REGISTRY.some((guide) => guide.id === value);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export interface GridConfig {
|
||||
rows: number;
|
||||
cols: number;
|
||||
}
|
||||
|
||||
export interface GuideRenderProps {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface GuideDefinition {
|
||||
id: string;
|
||||
label: string;
|
||||
renderPreview: () => ReactNode;
|
||||
renderTriggerIcon: () => ReactNode;
|
||||
renderOverlay: (props: GuideRenderProps) => ReactNode;
|
||||
renderOptions?: () => ReactNode;
|
||||
}
|
||||
Reference in New Issue
Block a user