diff --git a/app/home/page.tsx b/app/home/page.tsx
index 685ebd5..8afd5e3 100644
--- a/app/home/page.tsx
+++ b/app/home/page.tsx
@@ -3,7 +3,7 @@ import { ErrorBoundary } from "@/components/ErrorBoundary";
/**
* Editor Page - Public Access
- *
+ *
* This page is now publicly accessible without authentication.
*/
export default async function EditorPage() {
diff --git a/app/layout.tsx b/app/layout.tsx
index d243541..3d9fa2a 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -18,19 +18,31 @@ export const metadata: Metadata = {
default: "Stage",
template: "%s | Stage",
},
- description: "Create stunning showcase images for your projects with customizable templates and layouts. A fully in-browser canvas editor for adding images, text, and backgrounds—no external services required.",
- keywords: ["image editor", "canvas editor", "design tool", "image showcase", "template builder", "in-browser editor", "client-side export"],
+ description:
+ "Create stunning showcase images for your projects with customizable templates and layouts. A fully in-browser canvas editor for adding images, text, and backgrounds—no external services required.",
+ keywords: [
+ "image editor",
+ "canvas editor",
+ "design tool",
+ "image showcase",
+ "template builder",
+ "in-browser editor",
+ "client-side export",
+ ],
authors: [{ name: "Stage" }],
creator: "Stage",
publisher: "Stage",
- metadataBase: new URL(process.env.BETTER_AUTH_URL || "https://stage-psi-one.vercel.app"),
+ metadataBase: new URL(
+ process.env.BETTER_AUTH_URL || "https://stage-psi-one.vercel.app"
+ ),
openGraph: {
type: "website",
locale: "en_US",
url: "/",
siteName: "Stage",
title: "Stage - Image Showcase Builder",
- description: "Create stunning showcase images for your projects with customizable templates and layouts",
+ description:
+ "Create stunning showcase images for your projects with customizable templates and layouts",
images: [
{
url: "https://stage-psi-one.vercel.app/og.jpeg",
@@ -43,7 +55,8 @@ export const metadata: Metadata = {
twitter: {
card: "summary_large_image",
title: "Stage - Image Showcase Builder",
- description: "Create stunning showcase images for your projects with customizable templates and layouts",
+ description:
+ "Create stunning showcase images for your projects with customizable templates and layouts",
images: ["https://stage-psi-one.vercel.app/og.jpeg"],
creator: "@stage",
},
@@ -80,7 +93,11 @@ export default function RootLayout({
return (
-
+
window.removeEventListener('resize', updateViewportSize);
+ window.addEventListener("resize", updateViewportSize);
+ return () => window.removeEventListener("resize", updateViewportSize);
}, []);
useEffect(() => {
@@ -148,13 +148,13 @@ function CanvasRenderer({ image }: { image: HTMLImageElement }) {
]);
useEffect(() => {
- if (!noise.enabled || noise.type === 'none') {
+ if (!noise.enabled || noise.type === "none") {
setNoiseImage(null);
return;
}
const img = new window.Image();
- img.crossOrigin = 'anonymous';
+ img.crossOrigin = "anonymous";
img.onload = () => setNoiseImage(img);
img.onerror = () => setNoiseImage(null);
img.src = `/${noise.type}.jpg`;
@@ -185,7 +185,7 @@ function CanvasRenderer({ image }: { image: HTMLImageElement }) {
groupCenterY,
} = dimensions;
- const showFrame = frame.enabled && frame.type !== 'none';
+ const showFrame = frame.enabled && frame.type !== "none";
const has3DTransform =
perspective3D.rotateX !== 0 ||
@@ -204,20 +204,20 @@ function CanvasRenderer({ image }: { image: HTMLImageElement }) {
width: `${containerWidth}px`,
maxWidth: `${containerWidth}px`,
aspectRatio: responsiveDimensions.aspectRatio,
- maxHeight: 'calc(100vh - 200px)',
- backgroundColor: 'transparent',
- padding: '0px',
+ maxHeight: "calc(100vh - 200px)",
+ backgroundColor: "transparent",
+ padding: "0px",
}}
>
{
const clickedOnTransformer =
- e.target.getParent()?.className === 'Transformer';
+ e.target.getParent()?.className === "Transformer";
if (clickedOnTransformer) {
return;
}
@@ -269,8 +269,7 @@ function CanvasRenderer({ image }: { image: HTMLImageElement }) {
(key) => e.target.attrs.id === key
);
- const clickedOnText =
- e.target.attrs.text !== undefined;
+ const clickedOnText = e.target.attrs.text !== undefined;
if (!clickedOnOverlay && !clickedOnText) {
setSelectedOverlayId(null);
@@ -384,7 +383,7 @@ export default function ClientCanvas() {
}
const img = new window.Image();
- img.crossOrigin = 'anonymous';
+ img.crossOrigin = "anonymous";
img.onload = () => setImage(img);
img.onerror = () => setScreenshot({ src: null });
img.src = screenshot.src;
diff --git a/components/canvas/layers/TextOverlayLayer.tsx b/components/canvas/layers/TextOverlayLayer.tsx
index fce77e0..bff02e0 100644
--- a/components/canvas/layers/TextOverlayLayer.tsx
+++ b/components/canvas/layers/TextOverlayLayer.tsx
@@ -1,10 +1,10 @@
-'use client';
+"use client";
-import { Layer, Text, Transformer } from 'react-konva';
-import Konva from 'konva';
-import { useRef, useEffect } from 'react';
-import { getFontCSS } from '@/lib/constants/fonts';
-import type { TextOverlay } from '@/lib/store';
+import { Layer, Text, Transformer } from "react-konva";
+import Konva from "konva";
+import { useRef, useEffect } from "react";
+import { getFontCSS } from "@/lib/constants/fonts";
+import type { TextOverlay } from "@/lib/store";
interface TextOverlayLayerProps {
textOverlays: TextOverlay[];
@@ -43,6 +43,16 @@ export function TextOverlayLayer({
}
}, [selectedTextId, textOverlays]);
+ useEffect(() => {
+ Object.values(textRefs.current).forEach((node) => {
+ if (!node) return;
+ const w = node.width();
+ const h = node.height();
+ node.offsetX(w / 2);
+ node.offsetY(h / 2);
+ });
+ }, [textOverlays]);
+
return (
{textOverlays.map((overlay) => {
@@ -64,18 +74,15 @@ export function TextOverlayLayer({
x={textX}
y={textY}
text={overlay.text}
+ rotation={overlay.rotation}
fontSize={overlay.fontSize}
fontFamily={getFontCSS(overlay.fontFamily)}
fill={overlay.color}
opacity={overlay.opacity}
- offsetX={0}
- offsetY={0}
align="center"
verticalAlign="middle"
shadowColor={
- overlay.textShadow.enabled
- ? overlay.textShadow.color
- : undefined
+ overlay.textShadow.enabled ? overlay.textShadow.color : undefined
}
shadowBlur={
overlay.textShadow.enabled ? overlay.textShadow.blur : 0
@@ -87,9 +94,9 @@ export function TextOverlayLayer({
overlay.textShadow.enabled ? overlay.textShadow.offsetY : 0
}
fontStyle={
- String(overlay.fontWeight).includes('italic')
- ? 'italic'
- : 'normal'
+ String(overlay.fontWeight).includes("italic")
+ ? "italic"
+ : "normal"
}
fontVariant={String(overlay.fontWeight)}
draggable={true}
@@ -130,18 +137,19 @@ export function TextOverlayLayer({
updateTextOverlay(overlay.id, {
position: { x: newX, y: newY },
fontSize: newFontSize,
+ rotation: node.rotation(),
});
}}
onMouseEnter={(e) => {
const container = e.target.getStage()?.container();
if (container) {
- container.style.cursor = 'move';
+ container.style.cursor = "move";
}
}}
onMouseLeave={(e) => {
const container = e.target.getStage()?.container();
if (container) {
- container.style.cursor = 'default';
+ container.style.cursor = "default";
}
}}
/>
@@ -151,16 +159,13 @@ export function TextOverlayLayer({
ref={textTransformerRef}
keepRatio={false}
enabledAnchors={[
- 'top-left',
- 'top-right',
- 'bottom-left',
- 'bottom-right',
+ "top-left",
+ "top-right",
+ "bottom-left",
+ "bottom-right",
]}
boundBoxFunc={(oldBox, newBox) => {
- if (
- Math.abs(newBox.width) < 20 ||
- Math.abs(newBox.height) < 20
- ) {
+ if (Math.abs(newBox.width) < 20 || Math.abs(newBox.height) < 20) {
return oldBox;
}
return newBox;
@@ -169,4 +174,3 @@ export function TextOverlayLayer({
);
}
-
diff --git a/components/overlays/overlay-controls.tsx b/components/overlays/overlay-controls.tsx
index db00446..91818c5 100644
--- a/components/overlays/overlay-controls.tsx
+++ b/components/overlays/overlay-controls.tsx
@@ -1,12 +1,12 @@
-'use client'
+"use client";
-import { useState } from 'react'
-import { Button } from '@/components/ui/button'
-import { Slider } from '@/components/ui/slider'
-import { useImageStore } from '@/lib/store'
-import { Trash2, Eye, EyeOff } from 'lucide-react'
-import { getCldImageUrl } from '@/lib/cloudinary'
-import { OVERLAY_PUBLIC_IDS } from '@/lib/cloudinary-overlays'
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Slider } from "@/components/ui/slider";
+import { useImageStore } from "@/lib/store";
+import { Trash2, Eye, EyeOff } from "lucide-react";
+import { getCldImageUrl } from "@/lib/cloudinary";
+import { OVERLAY_PUBLIC_IDS } from "@/lib/cloudinary-overlays";
export function OverlayControls() {
const {
@@ -14,66 +14,70 @@ export function OverlayControls() {
updateImageOverlay,
removeImageOverlay,
clearImageOverlays,
- } = useImageStore()
+ } = useImageStore();
- const [selectedOverlayId, setSelectedOverlayId] = useState(null)
+ const [selectedOverlayId, setSelectedOverlayId] = useState(
+ null
+ );
const selectedOverlay = imageOverlays.find(
(overlay) => overlay.id === selectedOverlayId
- )
+ );
const handleUpdateSize = (value: number[]) => {
if (selectedOverlay) {
- updateImageOverlay(selectedOverlay.id, { size: value[0] })
+ updateImageOverlay(selectedOverlay.id, { size: value[0] });
}
- }
+ };
const handleUpdateRotation = (value: number[]) => {
if (selectedOverlay) {
- updateImageOverlay(selectedOverlay.id, { rotation: value[0] })
+ updateImageOverlay(selectedOverlay.id, { rotation: value[0] });
}
- }
+ };
const handleUpdateOpacity = (value: number[]) => {
if (selectedOverlay) {
- updateImageOverlay(selectedOverlay.id, { opacity: value[0] })
+ updateImageOverlay(selectedOverlay.id, { opacity: value[0] });
}
- }
+ };
const handleToggleFlipX = () => {
if (selectedOverlay) {
- updateImageOverlay(selectedOverlay.id, { flipX: !selectedOverlay.flipX })
+ updateImageOverlay(selectedOverlay.id, { flipX: !selectedOverlay.flipX });
}
- }
+ };
const handleToggleFlipY = () => {
if (selectedOverlay) {
- updateImageOverlay(selectedOverlay.id, { flipY: !selectedOverlay.flipY })
+ updateImageOverlay(selectedOverlay.id, { flipY: !selectedOverlay.flipY });
}
- }
+ };
const handleToggleVisibility = (id: string) => {
- const overlay = imageOverlays.find((o) => o.id === id)
+ const overlay = imageOverlays.find((o) => o.id === id);
if (overlay) {
- updateImageOverlay(id, { isVisible: !overlay.isVisible })
+ updateImageOverlay(id, { isVisible: !overlay.isVisible });
}
- }
+ };
- const handleUpdatePosition = (axis: 'x' | 'y', value: number[]) => {
+ const handleUpdatePosition = (axis: "x" | "y", value: number[]) => {
if (selectedOverlay) {
updateImageOverlay(selectedOverlay.id, {
position: {
...selectedOverlay.position,
[axis]: value[0],
},
- })
+ });
}
- }
+ };
return (
-
Image Overlays
+
+ Image Overlays
+