Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/home/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
29 changes: 23 additions & 6 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
},
Expand Down Expand Up @@ -80,7 +93,11 @@ export default function RootLayout({
return (
<html lang="en" className="dark">
<head>
<script defer src="https://cloud.umami.is/script.js" data-website-id="11f36f2b-1ef5-4014-bfdb-089aa4770c53"></script>
<script
defer
src="https://cloud.umami.is/script.js"
data-website-id="11f36f2b-1ef5-4014-bfdb-089aa4770c53"
></script>
</head>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
Expand Down
81 changes: 40 additions & 41 deletions components/canvas/ClientCanvas.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
'use client';

import { useEffect, useRef, useState } from 'react';
import { Stage, Layer } from 'react-konva';
import Konva from 'konva';
import { useEditorStore } from '@/lib/store';
import { useImageStore } from '@/lib/store';
import { generatePattern } from '@/lib/patterns';
import { useResponsiveCanvasDimensions } from '@/hooks/useAspectRatioDimensions';
import { getBackgroundCSS } from '@/lib/constants/backgrounds';
import { generateNoiseTexture } from '@/lib/export/export-utils';
import { MockupRenderer } from '@/components/mockups/MockupRenderer';
import { calculateCanvasDimensions } from './utils/canvas-dimensions';
import { BackgroundLayer } from './layers/BackgroundLayer';
import { PatternLayer } from './layers/PatternLayer';
import { NoiseLayer } from './layers/NoiseLayer';
import { MainImageLayer } from './layers/MainImageLayer';
import { TextOverlayLayer } from './layers/TextOverlayLayer';
import { ImageOverlayLayer } from './layers/ImageOverlayLayer';
import { Perspective3DOverlay } from './overlays/Perspective3DOverlay';
import { useBackgroundImage, useOverlayImages } from './hooks/useImageLoading';
"use client";

import { useEffect, useRef, useState } from "react";
import { Stage, Layer } from "react-konva";
import Konva from "konva";
import { useEditorStore } from "@/lib/store";
import { useImageStore } from "@/lib/store";
import { generatePattern } from "@/lib/patterns";
import { useResponsiveCanvasDimensions } from "@/hooks/useAspectRatioDimensions";
import { getBackgroundCSS } from "@/lib/constants/backgrounds";
import { generateNoiseTexture } from "@/lib/export/export-utils";
import { MockupRenderer } from "@/components/mockups/MockupRenderer";
import { calculateCanvasDimensions } from "./utils/canvas-dimensions";
import { BackgroundLayer } from "./layers/BackgroundLayer";
import { PatternLayer } from "./layers/PatternLayer";
import { NoiseLayer } from "./layers/NoiseLayer";
import { MainImageLayer } from "./layers/MainImageLayer";
import { TextOverlayLayer } from "./layers/TextOverlayLayer";
import { ImageOverlayLayer } from "./layers/ImageOverlayLayer";
import { Perspective3DOverlay } from "./overlays/Perspective3DOverlay";
import { useBackgroundImage, useOverlayImages } from "./hooks/useImageLoading";

let globalKonvaStage: Konva.Stage | null = null;

Expand Down Expand Up @@ -118,8 +118,8 @@ function CanvasRenderer({ image }: { image: HTMLImageElement }) {
};

updateViewportSize();
window.addEventListener('resize', updateViewportSize);
return () => window.removeEventListener('resize', updateViewportSize);
window.addEventListener("resize", updateViewportSize);
return () => window.removeEventListener("resize", updateViewportSize);
}, []);

useEffect(() => {
Expand Down Expand Up @@ -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`;
Expand Down Expand Up @@ -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 ||
Expand All @@ -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",
}}
>
<div
style={{
position: 'relative',
position: "relative",
width: `${canvasW}px`,
height: `${canvasH}px`,
minWidth: `${canvasW}px`,
minHeight: `${canvasH}px`,
overflow: 'hidden',
isolation: 'isolate',
overflow: "hidden",
isolation: "isolate",
}}
>
<Perspective3DOverlay
Expand Down Expand Up @@ -249,16 +249,16 @@ function CanvasRenderer({ image }: { image: HTMLImageElement }) {
ref={stageRef}
className="hires-stage"
style={{
display: 'block',
backgroundColor: 'transparent',
overflow: 'hidden',
position: 'relative',
display: "block",
backgroundColor: "transparent",
overflow: "hidden",
position: "relative",
borderRadius: `${backgroundBorderRadius}px`,
contain: 'layout style paint',
contain: "layout style paint",
}}
onMouseDown={(e) => {
const clickedOnTransformer =
e.target.getParent()?.className === 'Transformer';
e.target.getParent()?.className === "Transformer";
if (clickedOnTransformer) {
return;
}
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
54 changes: 29 additions & 25 deletions components/canvas/layers/TextOverlayLayer.tsx
Original file line number Diff line number Diff line change
@@ -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[];
Expand Down Expand Up @@ -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 (
<Layer ref={textLayerRef}>
{textOverlays.map((overlay) => {
Expand All @@ -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
Expand All @@ -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}
Expand Down Expand Up @@ -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";
}
}}
/>
Expand All @@ -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;
Expand All @@ -169,4 +174,3 @@ export function TextOverlayLayer({
</Layer>
);
}

Loading