feat: layout guide for tiktok
This commit is contained in:
parent
0386624ff5
commit
9ec5a02252
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
|
|
@ -0,0 +1,27 @@
|
|||
"use client";
|
||||
|
||||
import { useEditorStore } from "@/stores/editor-store";
|
||||
import Image from "next/image";
|
||||
|
||||
function TikTokGuide() {
|
||||
return (
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<Image
|
||||
src="/platform-guides/tiktok-blueprint.png"
|
||||
alt="TikTok layout guide"
|
||||
className="absolute inset-0 w-full h-full object-contain"
|
||||
draggable={false}
|
||||
fill
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function LayoutGuideOverlay() {
|
||||
const { layoutGuide } = useEditorStore();
|
||||
|
||||
if (layoutGuide.platform === null) return null;
|
||||
if (layoutGuide.platform === "tiktok") return <TikTokGuide />;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -8,7 +8,14 @@ import { useEditorStore } from "@/stores/editor-store";
|
|||
import { VideoPlayer } from "@/components/ui/video-player";
|
||||
import { AudioPlayer } from "@/components/ui/audio-player";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Play, Pause, Expand, SkipBack, SkipForward } from "lucide-react";
|
||||
import {
|
||||
Play,
|
||||
Pause,
|
||||
Expand,
|
||||
SkipBack,
|
||||
SkipForward,
|
||||
LayoutGrid,
|
||||
} from "lucide-react";
|
||||
import { useState, useRef, useEffect, useCallback } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { formatTimeCode } from "@/lib/time";
|
||||
|
|
@ -16,6 +23,16 @@ import { EditableTimecode } from "@/components/ui/editable-timecode";
|
|||
import { FONT_CLASS_MAP } from "@/lib/font-config";
|
||||
import { useProjectStore } from "@/stores/project-store";
|
||||
import { TextElementDragState } from "@/types/editor";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { LayoutGuideOverlay } from "./layout-guide-overlay";
|
||||
import { Label } from "../ui/label";
|
||||
import { SocialsIcon } from "../icons";
|
||||
import { PLATFORM_LAYOUTS, type PlatformLayout } from "@/stores/editor-store";
|
||||
|
||||
interface ActiveElement {
|
||||
element: TimelineElement;
|
||||
|
|
@ -496,6 +513,7 @@ export function PreviewPanel() {
|
|||
renderElement(elementData, index)
|
||||
)
|
||||
)}
|
||||
<LayoutGuideOverlay />
|
||||
{activeProject?.backgroundType === "blur" &&
|
||||
blurBackgroundElements.length === 0 &&
|
||||
activeElements.length > 0 && (
|
||||
|
|
@ -758,6 +776,7 @@ function FullscreenPreview({
|
|||
renderElement(elementData, index)
|
||||
)
|
||||
)}
|
||||
<LayoutGuideOverlay />
|
||||
{activeProject?.backgroundType === "blur" &&
|
||||
blurBackgroundElements.length === 0 &&
|
||||
activeElements.length > 0 && (
|
||||
|
|
@ -799,6 +818,7 @@ function PreviewToolbar({
|
|||
getTotalDuration: () => number;
|
||||
}) {
|
||||
const { isPlaying } = usePlaybackStore();
|
||||
const { layoutGuide, toggleLayoutGuide } = useEditorStore();
|
||||
|
||||
if (isExpanded) {
|
||||
return (
|
||||
|
|
@ -818,9 +838,53 @@ function PreviewToolbar({
|
|||
return (
|
||||
<div
|
||||
data-toolbar
|
||||
className="flex justify-between gap-2 px-1.5 pr-4 py-1.5 border border-border/50 w-auto absolute bottom-4 right-4 bg-black/20 rounded-full backdrop-blur-l text-white"
|
||||
className="flex justify-end gap-2 h-auto pb-5 pr-5 w-full"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="text"
|
||||
size="icon"
|
||||
className="h-auto p-0"
|
||||
title="Toggle layout guide"
|
||||
>
|
||||
<SocialsIcon className="!size-6" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80">
|
||||
<div className="grid gap-4">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium leading-none">Layout guide</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Show platform-specific layout guides to help align your
|
||||
content with interface elements like profile pictures,
|
||||
usernames, and interaction buttons.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="none"
|
||||
checked={layoutGuide.platform === null}
|
||||
onCheckedChange={() => toggleLayoutGuide(layoutGuide.platform || "tiktok")}
|
||||
/>
|
||||
<Label htmlFor="none">None</Label>
|
||||
</div>
|
||||
{Object.entries(PLATFORM_LAYOUTS).map(([platform, label]) => (
|
||||
<div key={platform} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={platform}
|
||||
checked={layoutGuide.platform === platform}
|
||||
onCheckedChange={() => toggleLayoutGuide(platform as PlatformLayout)}
|
||||
/>
|
||||
<Label htmlFor={platform}>{label}</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Button
|
||||
variant="text"
|
||||
size="icon"
|
||||
|
|
|
|||
|
|
@ -163,3 +163,33 @@ export function DataBuddyIcon({
|
|||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function SocialsIcon({
|
||||
className = "",
|
||||
size = 32,
|
||||
}: {
|
||||
className?: string;
|
||||
size?: number;
|
||||
}) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 345 243"
|
||||
fill="none"
|
||||
className={className}
|
||||
>
|
||||
<g opacity="0.5">
|
||||
<path d="M203.75 4H39.25C19.782 4 4 19.782 4 39.25V203.75C4 223.218 19.782 239 39.25 239H203.75C223.218 239 239 223.218 239 203.75V39.25C239 19.782 223.218 4 203.75 4Z" fill="#FFFC00"/>
|
||||
<path d="M97.1738 194.02C88.9121 188.053 82.4863 184.84 66.8809 187.594C64.5859 188.053 60.4551 189.43 59.9961 185.299C59.0781 182.086 59.0781 177.037 56.7832 176.578C42.5547 174.742 37.5059 171.988 35.2109 169.234C34.293 168.316 33.834 166.021 35.6699 165.562C59.9961 160.973 71.4707 137.564 73.7656 132.975C76.5195 126.09 71.9297 121.959 63.209 119.205C59.0781 117.828 52.1934 115.992 52.1934 111.402C52.1934 109.107 54.4883 107.73 56.7832 106.812C58.6191 106.354 60.4551 105.895 62.291 106.812C67.7988 109.107 72.8477 110.025 75.6016 107.73C75.6016 95.3379 72.3887 79.7324 77.4375 66.8809C83.4043 52.6523 98.0918 39.8008 121.5 39.8008C144.908 39.8008 159.596 52.6523 165.562 66.8809C170.611 79.7324 167.398 95.3379 167.398 107.73C170.152 110.025 175.201 109.107 180.709 106.812C182.545 105.895 184.381 106.354 186.217 106.812C188.512 107.73 190.807 109.107 190.807 111.402C190.807 115.992 183.922 117.828 179.791 119.205C171.07 121.959 166.48 126.09 169.234 132.975C171.529 137.564 183.004 160.973 207.33 165.562C209.166 166.021 208.707 168.316 207.789 169.234C205.494 171.988 200.445 174.742 186.217 176.578C183.922 177.037 183.922 182.086 183.004 185.299C182.545 189.43 178.414 188.053 176.119 187.594C160.514 184.84 154.088 188.053 145.826 194.02C139.065 199.872 130.442 203.126 121.5 203.199C111.861 203.658 104.059 199.527 97.1738 194.02Z" fill="white"/>
|
||||
<path d="M203.75 4H39.25C19.782 4 4 19.782 4 39.25V203.75C4 223.218 19.782 239 39.25 239H203.75C223.218 239 239 223.218 239 203.75V39.25C239 19.782 223.218 4 203.75 4Z" stroke="black" strokeWidth="7"/>
|
||||
<path d="M97.1738 194.02C88.9121 188.053 82.4863 184.84 66.8809 187.594C64.5859 188.053 60.4551 189.43 59.9961 185.299C59.0781 182.086 59.0781 177.037 56.7832 176.578C42.5547 174.742 37.5059 171.988 35.2109 169.234C34.293 168.316 33.834 166.021 35.6699 165.562C59.9961 160.973 71.4707 137.564 73.7656 132.975C76.5195 126.09 71.9297 121.959 63.209 119.205C59.0781 117.828 52.1934 115.992 52.1934 111.402C52.1934 109.107 54.4883 107.73 56.7832 106.812C58.6191 106.354 60.4551 105.895 62.291 106.812C67.7988 109.107 72.8477 110.025 75.6016 107.73C75.6016 95.3379 72.3887 79.7324 77.4375 66.8809C83.4043 52.6523 98.0918 39.8008 121.5 39.8008C144.908 39.8008 159.596 52.6523 165.562 66.8809C170.611 79.7324 167.398 95.3379 167.398 107.73C170.152 110.025 175.201 109.107 180.709 106.812C182.545 105.895 184.381 106.354 186.217 106.812C188.512 107.73 190.807 109.107 190.807 111.402C190.807 115.992 183.922 117.828 179.791 119.205C171.07 121.959 166.48 126.09 169.234 132.975C171.529 137.564 183.004 160.973 207.33 165.562C209.166 166.021 208.707 168.316 207.789 169.234C205.494 171.988 200.445 174.742 186.217 176.578C183.922 177.037 183.922 182.086 183.004 185.299C182.545 189.43 178.414 188.053 176.119 187.594C160.514 184.84 154.088 188.053 145.826 194.02C139.065 199.872 130.442 203.126 121.5 203.199C111.861 203.658 104.059 199.527 97.1738 194.02Z" stroke="black" strokeWidth="7"/>
|
||||
</g>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M133.5 4H321.5C334.48 4 345 14.5205 345 27.5V215.5C345 228.48 334.48 239 321.5 239H133.5C120.52 239 110 228.48 110 215.5V27.5C110 14.5205 120.52 4 133.5 4Z" fill="#010101"/>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M261.497 99.4086C271.784 106.758 284.388 111.083 297.999 111.083V84.9035C295.423 84.9045 292.854 84.6357 290.333 84.1016V104.709C276.723 104.709 264.121 100.384 253.831 93.0345V146.459C253.831 173.185 232.155 194.85 205.417 194.85C195.44 194.85 186.167 191.835 178.464 186.665C187.256 195.65 199.516 201.224 213.08 201.224C239.821 201.224 261.498 179.56 261.498 152.833L261.497 99.4086ZM270.953 72.9965C265.696 67.2559 262.244 59.8365 261.497 51.634V48.267H254.233C256.061 58.6916 262.298 67.5981 270.953 72.9965ZM195.376 166.157C192.438 162.308 190.851 157.598 190.858 152.756C190.858 140.533 200.773 130.622 213.005 130.622C215.285 130.621 217.551 130.97 219.723 131.659V104.894C217.185 104.547 214.622 104.399 212.061 104.454V125.286C209.887 124.597 207.62 124.247 205.34 124.249C193.107 124.249 183.193 134.159 183.193 146.384C183.193 155.027 188.149 162.512 195.376 166.157Z" fill="#EE1D52"/>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M253.831 93.0345C264.121 100.384 276.723 104.709 290.333 104.709V84.1016C282.736 82.4848 276.011 78.5162 270.953 72.9965C262.298 67.5981 256.061 58.6916 254.233 48.267H235.152V152.832C235.108 165.022 225.21 174.892 213.004 174.892C205.811 174.892 199.422 171.465 195.376 166.157C188.149 162.512 183.193 155.027 183.193 146.384C183.193 134.159 193.107 124.249 205.34 124.249C207.683 124.249 209.942 124.614 212.061 125.286V104.454C185.792 104.996 164.665 126.449 164.665 152.833C164.665 166.003 169.926 177.942 178.464 186.665C186.167 191.835 195.44 194.85 205.417 194.85C232.155 194.85 253.831 173.185 253.831 146.459V93.0345Z" fill="white"/>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M290.333 84.1016V78.5293C283.482 78.5396 276.766 76.6229 270.953 72.9965C276.099 78.6269 282.874 82.5087 290.333 84.1016ZM254.233 48.267C254.058 47.2708 253.924 46.2679 253.831 45.2608V41.8938H227.485V146.459C227.443 158.648 217.545 168.518 205.339 168.518C201.754 168.518 198.372 167.669 195.376 166.157C199.422 171.465 205.811 174.892 213.004 174.892C225.21 174.892 235.108 165.022 235.152 152.832V48.267H254.233ZM212.061 104.454L212.061 98.5212C209.859 98.2202 207.64 98.0698 205.418 98.071C178.676 98.071 157 119.736 157 146.459C157 163.214 165.518 177.979 178.464 186.665C169.926 177.942 164.666 166.002 164.666 152.832C164.666 126.449 185.791 104.996 212.061 104.454Z" fill="#69C9D0"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,15 @@ import { create } from "zustand";
|
|||
import { CanvasSize, CanvasPreset } from "@/types/editor";
|
||||
|
||||
type CanvasMode = "preset" | "original" | "custom";
|
||||
export type PlatformLayout = "tiktok";
|
||||
|
||||
export const PLATFORM_LAYOUTS: Record<PlatformLayout, string> = {
|
||||
tiktok: "TikTok",
|
||||
};
|
||||
|
||||
interface LayoutGuideSettings {
|
||||
platform: PlatformLayout | null;
|
||||
}
|
||||
|
||||
interface EditorState {
|
||||
// Loading states
|
||||
|
|
@ -12,6 +21,7 @@ interface EditorState {
|
|||
canvasSize: CanvasSize;
|
||||
canvasMode: CanvasMode;
|
||||
canvasPresets: CanvasPreset[];
|
||||
layoutGuide: LayoutGuideSettings;
|
||||
|
||||
// Actions
|
||||
setInitializing: (loading: boolean) => void;
|
||||
|
|
@ -20,6 +30,8 @@ interface EditorState {
|
|||
setCanvasSize: (size: CanvasSize) => void;
|
||||
setCanvasSizeToOriginal: (aspectRatio: number) => void;
|
||||
setCanvasSizeFromAspectRatio: (aspectRatio: number) => void;
|
||||
setLayoutGuide: (settings: Partial<LayoutGuideSettings>) => void;
|
||||
toggleLayoutGuide: (platform: PlatformLayout) => void;
|
||||
}
|
||||
|
||||
const DEFAULT_CANVAS_PRESETS: CanvasPreset[] = [
|
||||
|
|
@ -70,6 +82,9 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
|||
canvasSize: { width: 1920, height: 1080 }, // Default 16:9 HD
|
||||
canvasMode: "preset" as CanvasMode,
|
||||
canvasPresets: DEFAULT_CANVAS_PRESETS,
|
||||
layoutGuide: {
|
||||
platform: null,
|
||||
},
|
||||
|
||||
// Actions
|
||||
setInitializing: (loading) => {
|
||||
|
|
@ -101,4 +116,21 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
|||
const newCanvasSize = findBestCanvasPreset(aspectRatio);
|
||||
set({ canvasSize: newCanvasSize, canvasMode: "custom" });
|
||||
},
|
||||
|
||||
setLayoutGuide: (settings) => {
|
||||
set((state) => ({
|
||||
layoutGuide: {
|
||||
...state.layoutGuide,
|
||||
...settings,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
toggleLayoutGuide: (platform) => {
|
||||
set((state) => ({
|
||||
layoutGuide: {
|
||||
platform: state.layoutGuide.platform === platform ? null : platform,
|
||||
},
|
||||
}));
|
||||
},
|
||||
}));
|
||||
|
|
|
|||
Loading…
Reference in New Issue