7 Commits

Author SHA1 Message Date
ymk
c7de49e575 调整默认参数 2025-08-19 19:22:48 +08:00
ymk
04901b7244 添加Add shadow博客 2025-08-19 19:15:41 +08:00
ymk
d701332485 解决编译错误 2025-08-19 19:15:41 +08:00
ymk
97c34b02e0 添加友链 2025-08-19 19:15:41 +08:00
ymk
7ac5eaf833 处理小bug 2025-08-19 19:15:41 +08:00
ymk
12b6e7d48f 完成文字阴影效果 2025-08-19 19:15:41 +08:00
ymk
1e081625d0 增加seo相关配置 2025-08-19 19:15:41 +08:00
26 changed files with 598 additions and 92 deletions

View File

@@ -89,6 +89,11 @@
"faqQuestion1": "Why do text characters sometimes appear as question marks in the preview?",
"faqAnswer1": "Because the selected font doesn't support the language of the text. Try changing the font or uploading a custom font that supports the language."
},
"Effects": {
"title": "Effects",
"shadowOption": "Shadow",
"shadowOptionHelp": "Shadow works only with Pure Color background"
},
"Footer": {
"copyright": "© {year} Screen Designer. All rights reserved."
},

View File

@@ -89,6 +89,11 @@
"faqQuestion1": "为什么预览框内的文字内容会显示问号?",
"faqAnswer1": "因为该内容的语言没有被选中字体支持,建议更换字体或者上传自定义字体"
},
"Effects": {
"title": "特效",
"shadowOption": "阴影",
"shadowOptionHelp": "阴影只在纯色背景下有效"
},
"Footer": {
"copyright": "© {year} 3D文字设计工具 版权所有"
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -0,0 +1,9 @@
我要写一篇博文介绍如何设置文字阴影。你是一个专业的SEO优化师从seo的角度来帮我组织这个blog。
1 打开 https://fast3dtext.com/editor
2 输入想要的Text
3 调整背景、字体、颜色
4 打开Effect - Shadow的开关选择自己喜欢的阴影颜色
5 操控三维视角
6 下载
至此,即完成了文字阴影

View File

@@ -0,0 +1,18 @@
import Cover2 from "./512_288.png";
import { BlogItem } from "../list";
export const Blog: BlogItem = {
id: "Add-Text-Shadow",
date: "2025-08-19",
cover: Cover2,
en: {
title: "How to Add Stunning Text Shadow Effects to 3D Text Online",
summary:
"Learn how to create eye-catching text shadow effects for your 3D text designs using Fast3DText. This step-by-step guide shows you how to enable shadows, choose shadow colors, and create professional-looking 3D text with depth and dimension. Perfect for designers and content creators.",
},
zh: {
title: "如何为3D文字添加惊艳的阴影效果在线工具",
summary:
"本文教你使用 Fast3DText 为3D文字添加专业的阴影效果。通过开启阴影开关、选择阴影颜色和调整三维视角你可以轻松创建具有深度感和立体感的3D文字设计。适合设计师和内容创作者使用。",
},
};

View File

@@ -0,0 +1,89 @@
import { Box, Heading, Text, Link, Flex } from '@radix-ui/themes';
import Image from 'next/image';
import img from "./1024_576.png";
export default function Page() {
return (
<Flex gap={"4"} direction={"column"} justify={"start"} className='text-left'>
<Heading as="h1" size="7" mb="4" className='text-center'>How to Add Stunning Text Shadow Effects to 3D Text Online</Heading>
<Flex justify={"center"}>
<Image src={img} alt="3D Text with Shadow Effect Example" width={1024} height={576} />
</Flex>
<Text as="p" mb="4">
Want to make your <strong>3D text designs</strong> stand out with professional shadow effects? This comprehensive guide shows you how to easily add <strong>text shadow effects</strong> to your 3D text using <Link href="https://fast3dtext.com/editor" target="_blank" rel="noopener noreferrer">Fast3DText.com</Link> - the best free online 3D text generator with shadow capabilities.
</Text>
<Heading as="h2" size="5" mt="6" mb="3">🎯 Why Add Text Shadow Effects?</Heading>
<Text as="p" mb="4">
Text shadows add depth, dimension, and professionalism to your 3D designs. They create visual hierarchy, improve readability, and make your text pop against any background. Perfect for social media graphics, presentations, logos, and marketing materials.
</Text>
<Heading as="h2" size="5" mt="6" mb="3">🚀 Step-by-Step Guide to Adding Text Shadows</Heading>
<Heading as="h3" size="4" mt="4" mb="2">1. Open the 3D Text Editor</Heading>
<Text as="p" mb="4">
Start by visiting 👉 <Link href="https://fast3dtext.com/editor" target="_blank" rel="noopener noreferrer">https://fast3dtext.com/editor</Link>
</Text>
<Heading as="h3" size="4" mt="4" mb="2">2. Enter Your Text Content</Heading>
<Text as="p" mb="4">
Type the text you want to transform into 3D with shadow effects. You can enter single words, phrases, or multiple words separated by spaces for individual control.
</Text>
<Heading as="h3" size="4" mt="4" mb="2">3. Customize Basic Settings</Heading>
<Text as="p" mb="2">
Set up your foundation:
</Text>
<ul style={{ listStyleType: 'disc', paddingLeft: '20px' }}>
<li><strong>Background</strong>: Choose solid colors, gradients, or transparent background</li>
<li><strong>Font</strong>: Select from various 3D-compatible fonts</li>
<li><strong>Text Color</strong>: Pick your main text color</li>
</ul>
<Heading as="h3" size="4" mt="4" mb="2">4. Enable Shadow Effects</Heading>
<Text as="p" mb="4">
This is the key step! Navigate to the <strong>Effect panel</strong> and toggle on the <strong>Shadow switch</strong>. This activates the shadow functionality for your 3D text.
</Text>
<Heading as="h3" size="4" mt="4" mb="2">5. Choose Your Shadow Color</Heading>
<Text as="p" mb="4">
Select the perfect shadow color that complements your text. You can choose:
</Text>
<ul style={{ listStyleType: 'disc', paddingLeft: '20px' }}>
<li><strong>Matching colors</strong> for subtle effects</li>
<li><strong>Contrasting colors</strong> for dramatic impact</li>
<li><strong>Dark shadows</strong> for traditional depth effects</li>
<li><strong>Colored shadows</strong> for creative designs</li>
</ul>
<Heading as="h3" size="4" mt="4" mb="2">6. Adjust 3D Perspective</Heading>
<Text as="p" mb="4">
Drag and rotate the 3D scene to find the perfect angle that showcases your shadow effect. The shadow will dynamically adjust based on your 3D viewpoint.
</Text>
<Heading as="h3" size="4" mt="4" mb="2">7. Download Your Creation</Heading>
<Text as="p" mb="4">
Click the <strong>Download button</strong> to save your 3D text with shadow effects as a high-quality PNG image. Ready to use in your projects!
</Text>
<Heading as="h2" size="5" mt="6" mb="3">💡 Pro Tips for Best Results</Heading>
<ul style={{ listStyleType: 'disc', paddingLeft: '20px' }}>
<li>Use darker shadow colors for more pronounced effects</li>
<li>Experiment with different shadow intensities</li>
<li>Consider your background color when choosing shadow colors</li>
<li>Try multiple angles to find the most flattering shadow presentation</li>
</ul>
<Heading as="h2" size="5" mt="6" mb="3">🎨 Creative Applications</Heading>
<Text as="p" mb="4">
Text shadow effects are perfect for: logo design, social media graphics, YouTube thumbnails, presentation slides, website headers, marketing materials, and personal projects.
</Text>
<Text as="p" mt="6" style={{ fontWeight: 'bold' }}>
Start creating stunning 3D text with professional shadow effects today at <Link href="https://fast3dtext.com/editor" target="_blank" rel="noopener noreferrer">Fast3DText.com</Link>!
</Text>
</Flex>
);
}

View File

@@ -0,0 +1,82 @@
import Footer from "@/components/Footer";
import Header from "@/components/Header";
import { Box, Flex, } from "@radix-ui/themes";
import { useLocale } from "next-intl";
import En from "./en";
import Zh from "./zh";
import { Locales } from "@/i18n/config";
import { Metadata } from "next";
import { Blog } from "./data";
export default function Page() {
const locale = useLocale() as "en" | "zh";
return (
<Flex direction={"column"} gap={"4"}>
<Header />
<Flex justify={"center"} >
<Box className="md:w-2/3 w-full">
{locale == "en" && (<En></En>)}
{locale == "zh" && (<Zh></Zh>)}
</Box>
</Flex>
<Footer />
</Flex>
)
}
const host = process.env.NEXT_PUBLIC_HOST;
const locales = Locales;
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const name = Blog.id;
const title = Blog[locale as "en" | "zh"].title;
const description = Blog[locale as "en" | "zh"].summary;
return {
title,
description,
keywords: [],
openGraph: {
title,
description,
url: `${host}/${locale}/blogs/${name}`,
images: [
{
url: `${process.env.NEXT_PUBLIC_HOST}/og-image.png`,
width: 1200,
height: 630,
alt: title,
},
],
locale: locale,
type: "website",
},
twitter: {
card: "summary_large_image",
title,
description,
images: [`${process.env.NEXT_PUBLIC_HOST}/og-image.png`],
},
alternates: {
canonical: `${host}/blogs/${name}`,
languages: {
en: `${host}/en/blogs/${name}`,
zh: `${host}/zh/blogs/${name}`,
},
},
};
}

View File

@@ -0,0 +1,89 @@
import { Box, Heading, Text, Link, Flex } from '@radix-ui/themes';
import Image from 'next/image';
import img from "./1024_576.png";
export default function Page() {
return (
<Flex gap={"4"} direction={"column"} justify={"start"} className='text-left'>
<Heading as="h1" size="7" mb="4" className='text-center'>3D文字添加惊艳的阴影效果线</Heading>
<Flex justify={"center"}>
<Image src={img} alt="3D文字阴影效果示例" width={1024} height={576} />
</Flex>
<Text as="p" mb="4">
<strong>3D文字设计</strong>使<Link href="https://fast3dtext.com/editor" target="_blank" rel="noopener noreferrer">Fast3DText.com</Link>3D文字添加<strong></strong> - 线3D文字生成器
</Text>
<Heading as="h2" size="5" mt="6" mb="3">🎯 </Heading>
<Text as="p" mb="4">
3D设计增添深度稿Logo设计和营销材料
</Text>
<Heading as="h2" size="5" mt="6" mb="3">🚀 </Heading>
<Heading as="h3" size="4" mt="4" mb="2">1. 3D文字编辑器</Heading>
<Text as="p" mb="4">
访 👉 <Link href="https://fast3dtext.com/editor" target="_blank">https://fast3dtext.com/editor</Link>
</Text>
<Heading as="h3" size="4" mt="4" mb="2">2. </Heading>
<Text as="p" mb="4">
3D文字
</Text>
<Heading as="h3" size="4" mt="4" mb="2">3. </Heading>
<Text as="p" mb="2">
</Text>
<ul style={{ listStyleType: 'disc', paddingLeft: '20px' }}>
<li><strong></strong>: </li>
<li><strong></strong>: 3D兼容字体中选择</li>
<li><strong></strong>: </li>
</ul>
<Heading as="h3" size="4" mt="4" mb="2">4. </Heading>
<Text as="p" mb="4">
<strong></strong><strong></strong>3D文字的阴影功能
</Text>
<Heading as="h3" size="4" mt="4" mb="2">5. </Heading>
<Text as="p" mb="4">
</Text>
<ul style={{ listStyleType: 'disc', paddingLeft: '20px' }}>
<li><strong></strong></li>
<li><strong></strong></li>
<li><strong></strong></li>
<li><strong></strong></li>
</ul>
<Heading as="h3" size="4" mt="4" mb="2">6. 3D视角</Heading>
<Text as="p" mb="4">
3D场景3D视角动态调整
</Text>
<Heading as="h3" size="4" mt="4" mb="2">7. </Heading>
<Text as="p" mb="4">
<strong></strong>3D文字保存为高质量的PNG图像
</Text>
<Heading as="h2" size="5" mt="6" mb="3">💡 </Heading>
<ul style={{ listStyleType: 'disc', paddingLeft: '20px' }}>
<li>使</li>
<li></li>
<li></li>
<li></li>
</ul>
<Heading as="h2" size="5" mt="6" mb="3">🎨 </Heading>
<Text as="p" mb="4">
Logo设计YouTube缩略图稿
</Text>
<Text as="p" mt="6" style={{ fontWeight: 'bold' }}>
<Link href="https://fast3dtext.com/editor" target="_blank" rel="noopener noreferrer">Fast3DText.com</Link> 3D文字
</Text>
</Flex>
);
}

View File

@@ -15,8 +15,10 @@ export interface BlogItem {
import { StaticImageData } from "next/image";
import { Blog as Create3DTextBlog } from "./Create-3D-Text-with-the-Barbie-Font/data";
import { Blog as Create3DLetterBlog } from "./Create-3D-Letters/data";
import { Blog as AddTextShadowBlog } from "./Add-Text-Shadow/data";
export const blogs = [
Create3DLetterBlog,
Create3DTextBlog,
AddTextShadowBlog,
] satisfies BlogItem[];

View File

@@ -36,7 +36,7 @@ export default function Page() {
<h2 className="text-3xl font-bold text-center mb-12">
{t("toolTitle")}
</h2>
<Editor textProp={text} backgroundProp={undefined}></Editor>
<Editor textProp={text} backgroundProp={undefined} effectProp={undefined}></Editor>
</section>
{/* Features Section */}

View File

@@ -1,16 +1,46 @@
import { OnlyPage } from "@/components/editor/OnlyPage";
import { decodeText } from "@/lib/utils";
import { decode } from "@/lib/utils";
import { Metadata } from "next";
import { getTranslations } from "next-intl/server";
export default async function Page({ params }: { params: Promise<{ data: string }> }) {
const { data } = await params
let backgroundProp, textProp, effectProp
if (data) {
try {
const { bg, text, effect } = decode(data);
backgroundProp = bg;
textProp = text;
effectProp = effect;
} catch (error) {
console.error("parse data from url error", error)
}
}
return (<OnlyPage textProp={textProp} backgroundProp={backgroundProp} effectProp={effectProp}></OnlyPage>)
}
const host = process.env.NEXT_PUBLIC_HOST;
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string, data: string }>;
}): Promise<Metadata> {
const { locale, data } = await params;
const t = await getTranslations({ locale, namespace: "TextEditor" });
const name = `editor/${data}`;
let backgroundProp, textProp
if (data) {
try {
const { bg, text } = JSON.parse(decodeText(data));
const { bg, text } = decode(data);
backgroundProp = bg;
textProp = text;
} catch (error) {
@@ -18,6 +48,38 @@ export default async function Page({ params }: { params: Promise<{ data: string
}
}
return (<OnlyPage textProp={textProp} backgroundProp={backgroundProp}></OnlyPage>)
const description = t("seoDescription") + `bacground: ${JSON.stringify(backgroundProp)}; text: ${JSON.stringify(textProp)})}`;
}
return {
title: t("seoTitle"),
description,
openGraph: {
title: t("seoTitle"),
description,
url: `${host}/${locale}/${name}`,
images: [
{
url: `${process.env.NEXT_PUBLIC_HOST}/og-image.png`,
width: 1200,
height: 630,
alt: t("seoTitle"),
},
],
locale: locale,
type: "website",
},
twitter: {
card: "summary_large_image",
title: t("seoTitle"),
description,
images: [`${process.env.NEXT_PUBLIC_HOST}/og-image.png`],
},
alternates: {
canonical: `${host}/${name}`,
languages: {
en: `${host}/en/${name}`,
zh: `${host}/zh/${name}`,
},
},
};
}

View File

@@ -7,7 +7,7 @@ import { Metadata } from "next";
import { getTranslations } from "next-intl/server";
const host = process.env.NEXT_PUBLIC_HOST;
export default function Page() {
return (<OnlyPage textProp={undefined} backgroundProp={undefined}></OnlyPage>)
return (<OnlyPage textProp={undefined} backgroundProp={undefined} effectProp={undefined}></OnlyPage>)
}
const locales = Locales;

View File

@@ -1,18 +0,0 @@
import Footer from "@/components/Footer";
import Header from "@/components/Header";
import { Box, Flex } from "@radix-ui/themes";
export default function Page() {
return (
<Flex direction={"column"} gap={"4"}>
<Header />
<Box p="4" className="text-center">
<iframe src="https://docs.google.com/forms/d/e/1FAIpQLSeFbI-Bu-RsuYg1SP3_-L7wo5OOIfp5XR7H4E7jYgullaCm7g/viewform?embedded=true" className="w-full h-full" >Loading</iframe>
</Box>
<Footer />
</Flex>
)
}

View File

@@ -4,7 +4,7 @@ import { routing } from "@/i18n/routing";
import { getTranslations, setRequestLocale } from "next-intl/server";
import { jsonLdScriptProps } from "react-schemaorg";
import { WebSite } from "schema-dts";
import { Geist, Geist_Mono } from "next/font/google";
// import { Geist, Geist_Mono } from "next/font/google";
import { Analytics } from "@vercel/analytics/react";
import { SpeedInsights } from "@vercel/speed-insights/next";
import "../globals.css";
@@ -12,15 +12,15 @@ import { ThemeProvider } from "next-themes";
import { Theme } from "@radix-ui/themes";
const host = process.env.NEXT_PUBLIC_HOST;
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
// const geistSans = Geist({
// variable: "--font-geist-sans",
// subsets: ["latin"],
// });
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
// const geistMono = Geist_Mono({
// variable: "--font-geist-mono",
// subsets: ["latin"],
// });
export default async function RootLayout({
children,
@@ -66,7 +66,7 @@ export default async function RootLayout({
/>
</head>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
// className={`${geistSans.variable} ${geistMono.variable} antialiased`}
suppressHydrationWarning
>

View File

@@ -41,7 +41,7 @@ export default function HomePage() {
<Heading as="h2" size={"8"}>
{t("toolTitle")}
</Heading>
<Editor textProp={undefined} backgroundProp={undefined}></Editor>
<Editor textProp={undefined} backgroundProp={undefined} effectProp={undefined}></Editor>
</Flex>
</Section>

View File

@@ -1,6 +1,6 @@
import { Box, Flex, Heading, Link } from "@radix-ui/themes";
import { useLocale, useTranslations } from "next-intl";
import Image from "next/image";
export default function Footer() {
const f = useTranslations("Footer");
const t = useTranslations("Header");
@@ -32,14 +32,6 @@ export default function Footer() {
>
{t("blogName")}
</Link>
<Link
href="/features-form"
className="text-sm text-muted-foreground hover:text-primary"
>
Features Wanted
</Link>
</Flex>
<Flex gap={"2"} direction={"column"} >
@@ -68,6 +60,14 @@ export default function Footer() {
>
UIUXDECK
</Link>
<Link
href="https://twelve.tools"
className="text-sm text-muted-foreground hover:text-primary"
target="_blank"
>
<Image src="https://twelve.tools/badge0-white.svg" alt="Featured on Twelve Tools" width={100} height={28} />
</Link>
</Flex>
</Flex>

View File

@@ -7,24 +7,27 @@ import PreviewToolbar from "./common/PreviewToolbar";
import { useState } from "react";
import { useTranslations } from "next-intl";
import TextSetting, { TextProp } from "./common/TextSetting";
import Effects, { EffectProp } from "./common/Effects";
/**
* 全特性工具栏
* @returns
*/
export default function Page({ textProp, backgroundProp }: { textProp: TextProp | undefined, backgroundProp: BackgroundProp | undefined }) {
export default function Page({ textProp, backgroundProp, effectProp }: { textProp: TextProp | undefined, backgroundProp: BackgroundProp | undefined, effectProp: EffectProp | undefined }) {
const t = useTranslations("TextEditor");
backgroundProp = backgroundProp || {
color: "#c4b1b1",
color: "#a49494",
image: null,
} satisfies BackgroundProp;
textProp = textProp || TextProp.default(t("defaultText"));
effectProp = effectProp || { enableShadow: true, shadowColor: "#000000" } satisfies EffectProp;
const [background, setBackground] = useState<BackgroundProp>(backgroundProp!);
const [text, setText] = useState<TextProp>(textProp!);
const [effect, setEffect] = useState<EffectProp>(effectProp);
return (
<Flex gap={"2"}>
@@ -34,10 +37,12 @@ export default function Page({ textProp, backgroundProp }: { textProp: TextProp
setBackground={setBackground}
/>
<TextSetting text={text} setText={setText} />
<Effects effect={effect} setEffect={setEffect} background={background} />
</Flex>
<Flex className="w-2/3" direction={"column"} justify={"between"}>
<PreviewToolbar background={background} text={text} />
<PreviewToolbar background={background} text={text} effect={effect} />
</Flex>
</Flex>
);

View File

@@ -37,10 +37,12 @@ export default function Header() {
{t("blogName")}
</Link>
</Flex>
<Flex align="center" gap="4" className="w-1/4">
<Link href="https://github.com/wms-why/fast3dtextonline" target="_blank">
<svg width="24" height="24" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.49933 0.25C3.49635 0.25 0.25 3.49593 0.25 7.50024C0.25 10.703 2.32715 13.4206 5.2081 14.3797C5.57084 14.446 5.70302 14.2222 5.70302 14.0299C5.70302 13.8576 5.69679 13.4019 5.69323 12.797C3.67661 13.235 3.25112 11.825 3.25112 11.825C2.92132 10.9874 2.44599 10.7644 2.44599 10.7644C1.78773 10.3149 2.49584 10.3238 2.49584 10.3238C3.22353 10.375 3.60629 11.0711 3.60629 11.0711C4.25298 12.1788 5.30335 11.8588 5.71638 11.6732C5.78225 11.205 5.96962 10.8854 6.17658 10.7043C4.56675 10.5209 2.87415 9.89918 2.87415 7.12104C2.87415 6.32925 3.15677 5.68257 3.62053 5.17563C3.54576 4.99226 3.29697 4.25521 3.69174 3.25691C3.69174 3.25691 4.30015 3.06196 5.68522 3.99973C6.26337 3.83906 6.8838 3.75895 7.50022 3.75583C8.1162 3.75895 8.73619 3.83906 9.31523 3.99973C10.6994 3.06196 11.3069 3.25691 11.3069 3.25691C11.7026 4.25521 11.4538 4.99226 11.3795 5.17563C11.8441 5.68257 12.1245 6.32925 12.1245 7.12104C12.1245 9.9063 10.4292 10.5192 8.81452 10.6985C9.07444 10.9224 9.30633 11.3648 9.30633 12.0413C9.30633 13.0102 9.29742 13.7922 9.29742 14.0299C9.29742 14.2239 9.42828 14.4496 9.79591 14.3788C12.6746 13.4179 14.75 10.7025 14.75 7.50024C14.75 3.49593 11.5036 0.25 7.49933 0.25Z" fill="currentColor" fillRule="evenodd" clipRule="evenodd"></path></svg>
</Link>
<LanguageSwitcher />
<ModeToggle />
</Flex>

View File

@@ -83,33 +83,33 @@ export default function BackgroundSelector({
};
return (
<Box className="p-4 border rounded-lg min-w-64">
<Box className="p-4 border rounded-lg min-w-64 border-t-2 border-t-purple-500 shadow">
<Heading as="h2" size="4" className="font-medium text-lg">{t("title")}</Heading>
<Flex gap={"2"} p="2" direction={"column"}>
<Flex gap="2" align={"center"}>
<Checkbox checked={backgroundType.includes("color")} onClick={(e) => handleBackgroundTypeChange("color")} className="cursor-pointer" />
<Heading as="h3" size={"3"}>{t("colorOption")}</Heading>
<Heading as="h3" size={"3"} className="w-24">{t("colorOption")}</Heading>
<Flex gap={"4"} >
<input
type="color"
id="color-picker"
value={color || "black"}
onChange={handleColorChange}
className="w-1/3 h-10 rounded-md cursor-pointer"
className="w-1/3 h-8 rounded-md cursor-pointer"
/>
{color && (<input
type="text"
value={color}
onChange={handleColorChange}
className="w-1/2 h-10 rounded-md cursor-pointer pl-4"
className="w-1/2 h-8 rounded-md cursor-pointer pl-4"
/>)}
</Flex>
</Flex>
<Flex gap="2" align={"center"}>
<Checkbox checked={backgroundType.includes("image")} onClick={(e) => handleBackgroundTypeChange("image")} className="cursor-pointer" />
<Heading as="h3" size={"3"}>{t("imageOption")}</Heading>
<Heading as="h3" size={"3"} className="w-24">{t("imageOption")}</Heading>
<Flex gap={"4"} >
<input
type="file"
@@ -117,7 +117,7 @@ export default function BackgroundSelector({
accept="image/*"
onChange={handleImageUpload}
className="block w-full text-sm text-muted-foreground
file:mr-4 file:py-2 file:px-4
file:mr-4 file:py-1 file:px-2
file:rounded-md file:border-0
file:text-sm file:font-semibold
file:bg-primary file:text-primary-foreground

View File

@@ -0,0 +1,93 @@
'use client'
import { Box, Checkbox, Flex, Heading, IconButton, Tooltip } from "@radix-ui/themes";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import { BackgroundProp } from "./BackgroundSelector";
import { CircleQuestionMarkIcon } from "lucide-react";
export interface EffectProp {
enableShadow: boolean;
shadowColor: string;
}
export default function EffectsPage({
effect,
setEffect,
background
}: {
effect: EffectProp;
setEffect: (e: EffectProp) => void;
background: BackgroundProp
}) {
const t = useTranslations("Effects");
// const [shadowValid, setShadowValid] = useState(true);
// useEffect(() => {
// setShadowValid(!background.image);
// }, [background])
return (
<Box className="p-4 border rounded-lg min-w-64 border-t-2 border-t-purple-500 shadow">
<Heading as="h2" size="4" className="font-medium text-lg">{t("title")}</Heading>
<Flex gap={"2"} p="2" direction={"column"}>
<Flex gap="2" align={"center"}>
<Checkbox checked={effect.enableShadow} onClick={(e) => setEffect({ ...effect, enableShadow: !effect.enableShadow })} />
{t("shadowOption")}
<input
type="color"
id="color-picker"
value={effect.shadowColor}
onChange={e => setEffect({ ...effect, shadowColor: e.target.value })}
className="w-1/3 h-8 rounded-md cursor-pointer"
/>
<input
type="text"
value={effect.shadowColor}
onChange={e => setEffect({ ...effect, shadowColor: e.target.value })}
className="w-1/3 h-8 rounded-md cursor-pointer pl-4"
/>
</Flex>
{/* <Flex gap="2" align={"center"}>
<Checkbox checked={backgroundType.includes("color")} onClick={(e) => handleBackgroundTypeChange("color")} className="cursor-pointer" />
<Heading as="h3" size={"3"}>{t("colorOption")}</Heading>
<Flex gap={"4"} >
<input
type="color"
id="color-picker"
value={color || "black"}
onChange={handleColorChange}
className="w-1/3 h-10 rounded-md cursor-pointer"
/>
{color && (<input
type="text"
value={color}
onChange={handleColorChange}
className="w-1/2 h-10 rounded-md cursor-pointer pl-4"
/>)}
</Flex>
</Flex>
<Flex gap="2" align={"center"}>
<Checkbox checked={backgroundType.includes("image")} onClick={(e) => handleBackgroundTypeChange("image")} className="cursor-pointer" />
<Heading as="h3" size={"3"}>{t("imageOption")}</Heading>
<Flex gap={"4"} >
<input
type="file"
id="file-upload"
accept="image/*"
onChange={handleImageUpload}
className="block w-full text-sm text-muted-foreground
file:mr-4 file:py-2 file:px-4
file:rounded-md file:border-0
file:text-sm file:font-semibold
file:bg-primary file:text-primary-foreground
hover:file:bg-primary/90"
/>
</Flex>
</Flex> */}
</Flex>
</Box>
);
}

View File

@@ -4,15 +4,17 @@ import { useLocale, useTranslations } from "next-intl";
import { Eye, Download, Share } from "lucide-react";
import { BackgroundProp } from "./BackgroundSelector";
import { Text, Flex, Button, Select, AlertDialog, Code, AspectRatio } from "@radix-ui/themes";
import { getPicture, resize, init as threeInit, updateBackground, updateTextProps } from "./ThreeTools";
import { getPicture, resize, init as threeInit, updateBackground, updateEffectProp, updateTextProp } from "./ThreeTools";
import { TextProp } from "./TextSetting";
import { encodeText, getShareLink } from "@/lib/utils";
import { getShareLink } from "@/lib/utils";
import { EffectProp } from "./Effects";
const Sizes = [
"1920x1080",
"1024x576",
"1024x768",
"800x600",
"512x288"
]
interface Size {
width: number;
@@ -27,9 +29,11 @@ const AspectRatios = Sizes.map(o => {
export default function PreviewToolbar({
background,
text,
effect
}: {
background: BackgroundProp;
text: TextProp;
effect: EffectProp
}) {
let host = process.env.NEXT_PUBLIC_HOST?.substring("https://".length);
const t = useTranslations("PreviewBar");
@@ -70,12 +74,17 @@ export default function PreviewToolbar({
useEffect(() => {
updateTextProps(text);
updateTextProp(text);
console.log("text change", text);
}, [text]);
useEffect(() => {
updateEffectProp(effect);
console.log("effect change", effect);
}, [effect]);
const generateImage = async (w: number, h: number): Promise<string> => {
return new Promise((resolve, reject) => {
@@ -126,9 +135,6 @@ export default function PreviewToolbar({
}
});
}
const handleDownload = async () => {
@@ -193,11 +199,8 @@ export default function PreviewToolbar({
}
const bg = { ...background, image: null };
let txt = JSON.stringify({ bg, text });
txt = encodeText(txt);
const link = getShareLink(txt, locale);
const link = getShareLink({ bg, text }, locale);
setShareLink(link);
@@ -228,7 +231,7 @@ export default function PreviewToolbar({
}, [handleFullScreen]);
return (
<Flex direction={"column"} justify={"center"} align={"center"} p="2" className="rounded-lg border w-full" gap={"2"}>
<Flex direction={"column"} justify={"center"} align={"center"} p="2" className="shadow rounded-lg border w-full border-t-2 border-t-purple-500" gap={"2"}>
<Flex gap={"4"} >
{t("tipsTitle")}:
<Text>{t("mouseLeft")}</Text>

View File

@@ -43,7 +43,7 @@ export class TextProp {
}
return {
text,
color: "#8e86fe",
color: ["#ce6464", "#63635a"],
colorGradientDir: "l2r",
font,
fontUrl: getOnlineFontPath(font, FontWeight.Regular),
@@ -79,9 +79,6 @@ export default function TextSetting({
setText: (text: TextProp) => void;
}) {
const locale = useLocale();
const t = useTranslations("TextEditor");
const [uploadFonts, setUploadFonts] = useState<UploadFont[]>([]);
const isPureColor = !Array.isArray(text.color);
@@ -167,16 +164,14 @@ export default function TextSetting({
setFontWeightEnabled(map);
};
return (
<Flex className="p-4 border rounded-lg " gap={"3"} direction={"column"}>
<Flex className="p-4 border rounded-lg border-t-2 border-t-purple-500 shadow" gap={"3"} direction={"column"}>
<Heading as="h2" size="4" className="font-medium text-lg" >{t("title")}</Heading>
<textarea
value={text.text}
onChange={e => setText({ ...text, text: e.target.value })}
className="w-full p-3 border rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
rows={4}
rows={2}
/>
<div className="space-y-1">
<Heading as="h3" size={"3"} >{t("textColor")}</Heading>
@@ -220,13 +215,13 @@ export default function TextSetting({
type="color"
value={textGradientColor[0]}
onChange={e => setTextGradientColor([e.target.value, text.color[1]])}
className="w-1/2 h-10 rounded-md cursor-pointer"
className="w-1/2 h-8 rounded-md cursor-pointer"
/>
<input
type="text"
value={textGradientColor[0]}
onChange={e => setTextGradientColor([e.target.value, text.color[1]])}
className="w-1/2 h-10 rounded-md cursor-pointer pl-4"
className="w-1/2 h-8 rounded-md cursor-pointer pl-4"
/>
</Flex>
<Flex gap={"4"}>
@@ -235,13 +230,13 @@ export default function TextSetting({
value={textGradientColor[1]}
onChange={e => setTextGradientColor([text.color[0], e.target.value])}
className="w-1/2 h-10 rounded-md cursor-pointer"
className="w-1/2 h-8 rounded-md cursor-pointer"
/>
<input
type="text"
value={textGradientColor[1]}
onChange={e => setTextGradientColor([text.color[0], e.target.value])}
className="w-1/2 h-10 rounded-md cursor-pointer pl-4"
className="w-1/2 h-8 rounded-md cursor-pointer pl-4"
/>
</Flex>
</Box>

View File

@@ -2,10 +2,12 @@ import * as THREE from "three";
import { BackgroundProp } from "./BackgroundSelector";
import { FontLoader } from "three/addons/loaders/FontLoader.js";
import { TextGeometry } from "three/addons/geometries/TextGeometry.js";
import { ShadowMapViewer } from "three/addons/utils/ShadowMapViewer.js";
THREE.Cache.enabled = true;
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { ColorGradientDir, TextProp } from "./TextSetting";
import { EffectProp } from "./Effects";
let camera: THREE.PerspectiveCamera,
scene: THREE.Scene,
@@ -28,9 +30,10 @@ export function init(
renderer.setPixelRatio(1);
renderer.setSize(width, height, false);
renderer.setAnimationLoop(animate);
renderer.shadowMap.enabled = true;
camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000);
camera.position.set(0, 80, 0);
camera.position.set(0, 0, 50);
// const domWidth = container.clientWidth;
// const domHeight = container.clientHeight;
@@ -49,9 +52,10 @@ export function init(
// controls
controls = new OrbitControls(camera, renderer.domElement);
controls.screenSpacePanning = false;
// controls.screenSpacePanning = false;
controls.enabled = true;
// controls.enablePan = false;
//controls.addEventListener( 'change', render ); // call this only in static scenes (i.e., if there is no animation loop)
@@ -63,8 +67,6 @@ export function init(
controls.minDistance = 0.1;
controls.maxDistance = 50000;
controls.maxPolarAngle = Math.PI / 2;
// lights
const dirLight1 = new THREE.DirectionalLight(0xffffff, 3);
@@ -72,9 +74,25 @@ export function init(
scene.add(dirLight1);
const dirLight2 = new THREE.DirectionalLight(0x002288, 3);
dirLight2.position.set(-1, -1, -1);
dirLight2.position.set(-10, 10, 50);
dirLight2.castShadow = true;
dirLight2.shadow.camera.left = -100;
dirLight2.shadow.camera.top = 100;
dirLight2.shadow.camera.bottom = -100;
dirLight2.shadow.camera.right = 100;
dirLight2.shadow.camera.near = 0;
dirLight2.shadow.camera.far = 200;
dirLight2.shadow.bias = -0.000222;
dirLight2.shadow.mapSize.width = 2048;
dirLight2.shadow.mapSize.height = 2048;
scene.add(dirLight2);
// const helper = new THREE.DirectionalLightHelper(dirLight2, 50);
// scene.add(helper);
// const helper2 = new THREE.CameraHelper(dirLight2.shadow.camera);
// scene.add(helper2);
const ambientLight = new THREE.AmbientLight(0x555555);
scene.add(ambientLight);
}
@@ -113,7 +131,7 @@ export function resize(
let textMesh: THREE.Mesh;
let lastTextProps: TextProp | null = null;
export async function updateTextProps(textProps: TextProp) {
export async function updateTextProp(textProps: TextProp) {
// const mirror = true;
// const plane = new THREE.Mesh(
// new THREE.PlaneGeometry(10000, 10000),
@@ -143,8 +161,10 @@ export async function updateTextProps(textProps: TextProp) {
let size = new THREE.Vector3();
geo.boundingBox?.getSize(size);
let textMesh1 = new THREE.Mesh(geo, mat);
textMesh1.rotateX(-Math.PI / 2);
textMesh1.scale.multiplyScalar(100 / size.x);
// textMesh1.rotateX(-Math.PI / 2);
textMesh1.scale.multiplyScalar(50).divideScalar(size.x);
textMesh1.castShadow = true;
textMesh1.receiveShadow = true;
scene.add(textMesh1);
@@ -159,6 +179,9 @@ export async function updateTextProps(textProps: TextProp) {
let geo = await getTextGeometry(textProps);
textMesh.geometry.dispose();
textMesh.geometry = geo;
let size = new THREE.Vector3();
geo.boundingBox?.getSize(size);
textMesh.scale.set(1, 1, 1).multiplyScalar(50).divideScalar(size.x);
if (Array.isArray(textProps.color)) {
setGradient(
@@ -271,6 +294,7 @@ async function getTextGeometry(textProps: TextProp) {
textGeo.computeBoundingBox();
textGeo.center();
textGeo.translate(0, size / 2, depth / 2);
textGeo.computeVertexNormals();
return textGeo;
@@ -334,3 +358,28 @@ export function getPicture(width: number, height: number) {
renderer.setSize(lastWidth, lastHeight, false);
return img;
}
let shadowPlane: THREE.Mesh | null = null;
export function updateEffectProp(effect: EffectProp) {
// && background.color && !background.image
if (effect.enableShadow) {
if (!shadowPlane) {
shadowPlane = new THREE.Mesh(
new THREE.PlaneGeometry(100, 100, 10, 10),
new THREE.ShadowMaterial({
color: new THREE.Color(effect.shadowColor),
opacity: 0.3,
})
);
shadowPlane.receiveShadow = true;
}
scene.add(shadowPlane);
(shadowPlane.material as THREE.ShadowMaterial).color.set(
effect.shadowColor
);
shadowPlane.visible = true;
} else {
shadowPlane && (shadowPlane.visible = false);
}
}

View File

@@ -6,10 +6,12 @@ import Header from "@/components/Header";
import { Box, Container, Flex, Heading, Text, Card } from "@radix-ui/themes";
import { HelpCircle } from "lucide-react";
import { useTranslations } from "next-intl";
import { EffectProp } from "../common/Effects";
export function OnlyPage({ textProp, backgroundProp }: {
export function OnlyPage({ textProp, backgroundProp, effectProp }: {
textProp: TextProp | undefined;
backgroundProp: BackgroundProp | undefined
backgroundProp: BackgroundProp | undefined;
effectProp: EffectProp | undefined;
}) {
const t = useTranslations('TextEditor');
@@ -20,6 +22,7 @@ export function OnlyPage({ textProp, backgroundProp }: {
<FullEditor
textProp={textProp}
backgroundProp={backgroundProp}
effectProp={effectProp}
/>
</Container>
<Container p="4">

View File

@@ -2,18 +2,21 @@ import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
import CryptoJS from "crypto-js";
import LZString from "lz-string";
import { BackgroundProp } from "@/components/common/BackgroundSelector";
import { EffectProp } from "@/components/common/Effects";
import { TextProp } from "@/components/common/TextSetting";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
const SECRET_KEY = "fast3dtext-ymk";
export function encodeText(text: string) {
function encodeText(text: string) {
const compressed = LZString.compressToEncodedURIComponent(text);
const encrypted = CryptoJS.AES.encrypt(compressed, SECRET_KEY).toString();
return encodeURIComponent(encrypted);
}
export function decodeText(encodedText: string) {
function decodeText(encodedText: string) {
const decoded = decodeURIComponent(encodedText);
const decrypted = CryptoJS.AES.decrypt(decoded, SECRET_KEY).toString(
CryptoJS.enc.Utf8
@@ -22,6 +25,16 @@ export function decodeText(encodedText: string) {
return decompressed;
}
export function getShareLink(data: string, locale: string) {
return `${window.location.origin}/${locale}/editor/${data}`;
export interface ShareObj {
bg: BackgroundProp;
text: TextProp;
effect?: EffectProp;
}
export function getShareLink(data: ShareObj, locale: string) {
const dataStr = JSON.stringify(data);
return `${window.location.origin}/${locale}/editor/${encodeText(dataStr)}`;
}
export function decode(data: string) {
const decoded = decodeText(data);
return JSON.parse(decoded) as ShareObj;
}