8 Commits

Author SHA1 Message Date
Vercel
9155419710 Fix React Server Components CVE vulnerabilities
Updated dependencies to fix Next.js and React CVE vulnerabilities.

The fix-react2shell-next tool automatically updated the following packages to their secure versions:
- next
- react-server-dom-webpack
- react-server-dom-parcel  
- react-server-dom-turbopack

All package.json files have been scanned and vulnerable versions have been patched to the correct fixed versions based on the official React advisory.

Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
2025-12-23 10:51:35 +00:00
ymk
6ff08a0340 解决编译错误 2025-08-19 18:57:10 +08:00
ymk
505167ea92 添加友链 2025-08-19 18:38:32 +08:00
ymk
1c7e2fbb10 处理小bug 2025-08-19 18:31:10 +08:00
ymk
956f1f6c56 完成文字阴影效果 2025-08-19 16:56:28 +08:00
ymk
3d72b3b253 Merge branch 'develop' 2025-08-18 15:29:16 +08:00
ymk
551c33f220 Merge branch 'develop' 2025-08-18 14:22:30 +08:00
ymk
a0c9ae3bfc 增加seo相关配置 2025-08-13 10:18:36 +08:00
20 changed files with 406 additions and 113 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文字设计工具 版权所有"
},

View File

@@ -18,7 +18,7 @@
"lucide-react": "^0.536.0",
"lz-string": "^1.5.0",
"motion": "^12.23.12",
"next": "15.2.4",
"next": "15.2.8",
"next-intl": "^4.3.4",
"next-themes": "^0.4.6",
"react": "19.0.0",
@@ -46,4 +46,4 @@
"@types/react": "19.0.12",
"@types/react-dom": "19.0.4"
}
}
}

100
pnpm-lock.yaml generated
View File

@@ -13,10 +13,10 @@ importers:
version: 3.2.1(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@vercel/analytics':
specifier: ^1.5.0
version: 1.5.0(next@15.2.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
version: 1.5.0(next@15.2.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
'@vercel/speed-insights':
specifier: ^1.2.0
version: 1.2.0(next@15.2.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
version: 1.2.0(next@15.2.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@@ -36,11 +36,11 @@ importers:
specifier: ^12.23.12
version: 12.23.12(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
next:
specifier: 15.2.4
version: 15.2.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
specifier: 15.2.8
version: 15.2.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
next-intl:
specifier: ^4.3.4
version: 4.3.4(next@15.2.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(typescript@5.9.2)
version: 4.3.4(next@15.2.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(typescript@5.9.2)
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -338,56 +338,56 @@ packages:
'@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
'@next/env@15.2.4':
resolution: {integrity: sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g==}
'@next/env@15.2.8':
resolution: {integrity: sha512-TaEsAki14R7BlgywA05t2PFYfwZiNlGUHyIQHVyloXX3y+Dm0HUITe5YwTkjtuOQuDhuuLotNEad4VtnmE11Uw==}
'@next/eslint-plugin-next@15.2.3':
resolution: {integrity: sha512-eNSOIMJtjs+dp4Ms1tB1PPPJUQHP3uZK+OQ7iFY9qXpGO6ojT6imCL+KcUOqE/GXGidWbBZJzYdgAdPHqeCEPA==}
'@next/swc-darwin-arm64@15.2.4':
resolution: {integrity: sha512-1AnMfs655ipJEDC/FHkSr0r3lXBgpqKo4K1kiwfUf3iE68rDFXZ1TtHdMvf7D0hMItgDZ7Vuq3JgNMbt/+3bYw==}
'@next/swc-darwin-arm64@15.2.5':
resolution: {integrity: sha512-4OimvVlFTbgzPdA0kh8A1ih6FN9pQkL4nPXGqemEYgk+e7eQhsst/p35siNNqA49eQA6bvKZ1ASsDtu9gtXuog==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-x64@15.2.4':
resolution: {integrity: sha512-3qK2zb5EwCwxnO2HeO+TRqCubeI/NgCe+kL5dTJlPldV/uwCnUgC7VbEzgmxbfrkbjehL4H9BPztWOEtsoMwew==}
'@next/swc-darwin-x64@15.2.5':
resolution: {integrity: sha512-ohzRaE9YbGt1ctE0um+UGYIDkkOxHV44kEcHzLqQigoRLaiMtZzGrA11AJh2Lu0lv51XeiY1ZkUvkThjkVNBMA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-linux-arm64-gnu@15.2.4':
resolution: {integrity: sha512-HFN6GKUcrTWvem8AZN7tT95zPb0GUGv9v0d0iyuTb303vbXkkbHDp/DxufB04jNVD+IN9yHy7y/6Mqq0h0YVaQ==}
'@next/swc-linux-arm64-gnu@15.2.5':
resolution: {integrity: sha512-FMSdxSUt5bVXqqOoZCc/Seg4LQep9w/fXTazr/EkpXW2Eu4IFI9FD7zBDlID8TJIybmvKk7mhd9s+2XWxz4flA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-musl@15.2.4':
resolution: {integrity: sha512-Oioa0SORWLwi35/kVB8aCk5Uq+5/ZIumMK1kJV+jSdazFm2NzPDztsefzdmzzpx5oGCJ6FkUC7vkaUseNTStNA==}
'@next/swc-linux-arm64-musl@15.2.5':
resolution: {integrity: sha512-4ZNKmuEiW5hRKkGp2HWwZ+JrvK4DQLgf8YDaqtZyn7NYdl0cHfatvlnLFSWUayx9yFAUagIgRGRk8pFxS8Qniw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-x64-gnu@15.2.4':
resolution: {integrity: sha512-yb5WTRaHdkgOqFOZiu6rHV1fAEK0flVpaIN2HB6kxHVSy/dIajWbThS7qON3W9/SNOH2JWkVCyulgGYekMePuw==}
'@next/swc-linux-x64-gnu@15.2.5':
resolution: {integrity: sha512-bE6lHQ9GXIf3gCDE53u2pTl99RPZW5V1GLHSRMJ5l/oB/MT+cohu9uwnCK7QUph2xIOu2a6+27kL0REa/kqwZw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-musl@15.2.4':
resolution: {integrity: sha512-Dcdv/ix6srhkM25fgXiyOieFUkz+fOYkHlydWCtB0xMST6X9XYI3yPDKBZt1xuhOytONsIFJFB08xXYsxUwJLw==}
'@next/swc-linux-x64-musl@15.2.5':
resolution: {integrity: sha512-y7EeQuSkQbTAkCEQnJXm1asRUuGSWAchGJ3c+Qtxh8LVjXleZast8Mn/rL7tZOm7o35QeIpIcid6ufG7EVTTcA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-win32-arm64-msvc@15.2.4':
resolution: {integrity: sha512-dW0i7eukvDxtIhCYkMrZNQfNicPDExt2jPb9AZPpL7cfyUo7QSNl1DjsHjmmKp6qNAqUESyT8YFl/Aw91cNJJg==}
'@next/swc-win32-arm64-msvc@15.2.5':
resolution: {integrity: sha512-gQMz0yA8/dskZM2Xyiq2FRShxSrsJNha40Ob/M2n2+JGRrZ0JwTVjLdvtN6vCxuq4ByhOd4a9qEf60hApNR2gQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-x64-msvc@15.2.4':
resolution: {integrity: sha512-SbnWkJmkS7Xl3kre8SdMF6F/XDh1DTFEhp0jRTj/uB8iPKoU2bb2NDfcu+iifv1+mxQEd1g2vvSxcZbXSKyWiQ==}
'@next/swc-win32-x64-msvc@15.2.5':
resolution: {integrity: sha512-tBDNVUcI7U03+3oMvJ11zrtVin5p0NctiuKmTGyaTIEAVj9Q77xukLXGXRnWxKRIIdFG4OTA2rUVGZDYOwgmAA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -2356,8 +2356,8 @@ packages:
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
next@15.2.4:
resolution: {integrity: sha512-VwL+LAaPSxEkd3lU2xWbgEOtrM8oedmyhBqaVNmgKB+GvZlCy9rgaEc+y2on0wv+l0oSFqLtYD6dcC1eAedUaQ==}
next@15.2.8:
resolution: {integrity: sha512-pe2trLKZTdaCuvNER0S9Wp+SP2APf7SfFmyUP9/w1SFA2UqmW0u+IsxCKkiky3n6um7mryaQIlgiDnKrf1ZwIw==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
hasBin: true
peerDependencies:
@@ -3082,34 +3082,34 @@ snapshots:
'@tybys/wasm-util': 0.10.0
optional: true
'@next/env@15.2.4': {}
'@next/env@15.2.8': {}
'@next/eslint-plugin-next@15.2.3':
dependencies:
fast-glob: 3.3.1
'@next/swc-darwin-arm64@15.2.4':
'@next/swc-darwin-arm64@15.2.5':
optional: true
'@next/swc-darwin-x64@15.2.4':
'@next/swc-darwin-x64@15.2.5':
optional: true
'@next/swc-linux-arm64-gnu@15.2.4':
'@next/swc-linux-arm64-gnu@15.2.5':
optional: true
'@next/swc-linux-arm64-musl@15.2.4':
'@next/swc-linux-arm64-musl@15.2.5':
optional: true
'@next/swc-linux-x64-gnu@15.2.4':
'@next/swc-linux-x64-gnu@15.2.5':
optional: true
'@next/swc-linux-x64-musl@15.2.4':
'@next/swc-linux-x64-musl@15.2.5':
optional: true
'@next/swc-win32-arm64-msvc@15.2.4':
'@next/swc-win32-arm64-msvc@15.2.5':
optional: true
'@next/swc-win32-x64-msvc@15.2.4':
'@next/swc-win32-x64-msvc@15.2.5':
optional: true
'@nodelib/fs.scandir@2.1.5':
@@ -4164,14 +4164,14 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true
'@vercel/analytics@1.5.0(next@15.2.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)':
'@vercel/analytics@1.5.0(next@15.2.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)':
optionalDependencies:
next: 15.2.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
next: 15.2.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: 19.0.0
'@vercel/speed-insights@1.2.0(next@15.2.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)':
'@vercel/speed-insights@1.2.0(next@15.2.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)':
optionalDependencies:
next: 15.2.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
next: 15.2.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: 19.0.0
'@webgpu/types@0.1.64': {}
@@ -5192,11 +5192,11 @@ snapshots:
negotiator@1.0.0: {}
next-intl@4.3.4(next@15.2.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(typescript@5.9.2):
next-intl@4.3.4(next@15.2.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(typescript@5.9.2):
dependencies:
'@formatjs/intl-localematcher': 0.5.10
negotiator: 1.0.0
next: 15.2.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
next: 15.2.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: 19.0.0
use-intl: 4.3.4(react@19.0.0)
optionalDependencies:
@@ -5207,9 +5207,9 @@ snapshots:
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
next@15.2.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
next@15.2.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
'@next/env': 15.2.4
'@next/env': 15.2.8
'@swc/counter': 0.1.3
'@swc/helpers': 0.5.15
busboy: 1.6.0
@@ -5219,14 +5219,14 @@ snapshots:
react-dom: 19.0.0(react@19.0.0)
styled-jsx: 5.1.6(react@19.0.0)
optionalDependencies:
'@next/swc-darwin-arm64': 15.2.4
'@next/swc-darwin-x64': 15.2.4
'@next/swc-linux-arm64-gnu': 15.2.4
'@next/swc-linux-arm64-musl': 15.2.4
'@next/swc-linux-x64-gnu': 15.2.4
'@next/swc-linux-x64-musl': 15.2.4
'@next/swc-win32-arm64-msvc': 15.2.4
'@next/swc-win32-x64-msvc': 15.2.4
'@next/swc-darwin-arm64': 15.2.5
'@next/swc-darwin-x64': 15.2.5
'@next/swc-linux-arm64-gnu': 15.2.5
'@next/swc-linux-arm64-musl': 15.2.5
'@next/swc-linux-x64-gnu': 15.2.5
'@next/swc-linux-x64-musl': 15.2.5
'@next/swc-win32-arm64-msvc': 15.2.5
'@next/swc-win32-x64-msvc': 15.2.5
sharp: 0.33.5
transitivePeerDependencies:
- '@babel/core'

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,6 +1,7 @@
import Footer from "@/components/Footer";
import Header from "@/components/Header";
import { Box, Flex } from "@radix-ui/themes";
import { Metadata } from "next";
export default function Page() {
@@ -15,4 +16,51 @@ export default function Page() {
</Flex>
)
}
const host = process.env.NEXT_PUBLIC_HOST;
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const name = "features-form";
const title = "new features wanted";
const description = title;
return {
title,
description,
openGraph: {
title,
description,
url: `${host}/${locale}/${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}/${name}`,
languages: {
en: `${host}/en/${name}`,
zh: `${host}/zh/${name}`,
},
},
};
}

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");
@@ -68,6 +68,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,12 +7,13 @@ 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");
@@ -22,9 +23,11 @@ export default function Page({ textProp, backgroundProp }: { textProp: TextProp
} 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,9 +4,10 @@ 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",
@@ -27,9 +28,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 +73,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 +134,6 @@ export default function PreviewToolbar({
}
});
}
const handleDownload = async () => {
@@ -193,11 +198,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 +230,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

@@ -167,16 +167,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 +218,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 +233,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 = true;
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;
}