diff --git a/dictionary/en.json b/dictionary/en.json index 9d7504f..0f925f4 100644 --- a/dictionary/en.json +++ b/dictionary/en.json @@ -1,4 +1,8 @@ { + "Metadata": { + "title": "3D Text Generator", + "description": "Tool for create 3d text, with customizable fonts, colors, background and effects" + }, "Header": { "appName": "Fast3DText", "editorName": "Editor", @@ -69,35 +73,35 @@ "seoTitle": "3D Text Generator - More Options", "seoDescription": "Fast Create professional 3D text with more options. control the color, font, and effect", "title": "Text Editor", - "defaultText": "please input your text", + "defaultText": "Welcome", "textColor": "Text Color", + "color": "Color", + "textGradientColor": "Gradient Color", + "l2r": "From Left to Right", + "t2b": "From Top to Bottom", "fontFamily": "Font Family", "how2UploadFont": "How to Upload Font?", "fontWeight": "Font Weight", "uploadFontButton": "Upload Font | Files with the same name will replace the fonts uploaded previously", "uploadedFonts": "Uploaded Fonts" }, - "Metadata": { - "title": "Screen Designer", - "description": "Tool for designing custom screen backgrounds" - }, "Footer": { "copyright": "© {year} Screen Designer. All rights reserved." }, "DoNotWriteOnThisPage": { - "seoTitle": "Do Not Write On This Page - 3D Text Generator", - "seoDescription": "Free online tool to generate 'Do Not Write On This Page' 3D Text. Customize text, colors and download high-resolution images for notebooks, whiteboards and screens.", - "toolTitle": "'Do Not Write On This Page' Generator", - "heroTitle": "'Do Not Write On This Page' 3D Text Generator", - "heroSubtitle": "Generate and customize perfect backgrounds for notebooks, whiteboards and digital screens", - "feature1Title": "Text Customization", - "feature1Desc": "Fully customize text content, fonts and colors to create your perfect design", - "feature2Title": "Multiple Background Options", - "feature2Desc": "Choose from color presets or upload your own images as backgrounds", - "feature3Title": "High-Resolution Export", - "feature3Desc": "Download print-ready high-quality images in multiple formats", - "ctaTitle": "Create Your Custom Background Now", - "ctaSubtitle": "Start generating professional 'Do Not Write On This Page' designs", + "seoTitle": "3D Text Generator - Professional 'Do Not Write On This Page' Creator", + "seoDescription": "Free online 3D text generator for creating stunning 'Do Not Write On This Page' designs. Customize 3D depth, lighting effects, colors and download high-resolution images for notebooks, whiteboards and screens.", + "toolTitle": "3D 'Do Not Write On This Page' Generator", + "heroTitle": "Professional 3D 'Do Not Write On This Page' Creator", + "heroSubtitle": "Generate and customize perfect 3D text backgrounds for notebooks, whiteboards and digital screens", + "feature1Title": "3D Text Customization", + "feature1Desc": "Fully customize 3D text content, fonts, colors and depth effects to create your perfect design", + "feature2Title": "Advanced 3D Effects", + "feature2Desc": "Adjust 3D perspective, lighting and choose from multiple background options", + "feature3Title": "High-Quality 3D Export", + "feature3Desc": "Download print-ready high-quality 3D text images in PNG, SVG and more formats", + "ctaTitle": "Create Your 3D Background Now", + "ctaSubtitle": "Start generating professional 3D 'Do Not Write On This Page' designs", "ctaButton": "Generate 3D Text" } } \ No newline at end of file diff --git a/dictionary/zh.json b/dictionary/zh.json index 75e8187..5ec66cd 100644 --- a/dictionary/zh.json +++ b/dictionary/zh.json @@ -1,4 +1,8 @@ { + "Metadata": { + "title": "3D文字生成器", + "description": "专业的3D文字生成工具,可自定义字体、颜色、背景和特效,轻松创建惊艳的3D文字效果" + }, "Header": { "appName": "Fast3DText", "editorName": "编辑器", @@ -69,35 +73,35 @@ "seoTitle": "3D 文字生成器 - 更多的选项", "seoDescription": "快速创建具有更多选项的专业 3D 文本。控制颜色、字体和效果", "title": "文字编辑", - "defaultText": "输入您的文字", + "defaultText": "欢迎", "textColor": "文字颜色", + "color": "纯色", + "textGradientColor": "渐变色", + "l2r": "从左到右", + "t2b": "从上到下", "fontFamily": "选择字体", "how2UploadFont": "如何上传字体?", "fontWeight": "字体粗细", "uploadFontButton": "上传字体 | 相同名字的文件会替换前面上传的字体", "uploadedFonts": "已上传字体" }, - "Metadata": { - "title": "屏幕背景设计工具", - "description": "用于设计自定义屏幕背景的工具" - }, "Footer": { - "copyright": "© {year} 屏幕背景设计工具 版权所有" + "copyright": "© {year} 3D文字设计工具 版权所有" }, "DoNotWriteOnThisPage": { - "seoTitle": "请勿在此页书写生成器 - 专业背景制作工具", - "seoDescription": "免费在线生成专业的'请勿在此页书写'背景。自定义文字、颜色,下载高分辨率图片,适用于笔记本、白板和屏幕。", - "toolTitle": "请勿在此页书写生成器", - "heroTitle": "专业'请勿在此页书写'背景制作工具", - "heroSubtitle": "为笔记本、白板和数字屏幕生成并定制完美背景", - "feature1Title": "文字定制", - "feature1Desc": "完全自定义文字内容、字体和颜色,打造完美设计", - "feature2Title": "多种背景选项", - "feature2Desc": "可选择预设颜色或上传自定义图片作为背景", - "feature3Title": "高清导出", - "feature3Desc": "下载印刷级高质量图片,支持多种格式", - "ctaTitle": "立即创建您的自定义背景", - "ctaSubtitle": "开始生成专业的'请勿在此页书写'设计", - "ctaButton": "生成背景" + "seoTitle": "3D文字生成器 - 专业'请勿在此页书写'背景制作", + "seoDescription": "免费在线生成3D效果的'请勿在此页书写'背景。自定义3D文字深度、颜色和特效,下载高分辨率图片,适用于笔记本、白板和屏幕。", + "toolTitle": "3D'请勿在此页书写'生成器", + "heroTitle": "专业3D'请勿在此页书写'背景制作工具", + "heroSubtitle": "使用3D文字技术为笔记本、白板和数字屏幕生成惊艳背景", + "feature1Title": "3D文字定制", + "feature1Desc": "完全自定义3D文字内容、字体、颜色和深度效果,打造专业设计", + "feature2Title": "3D特效选项", + "feature2Desc": "调整3D深度、光照和透视效果,可选择颜色或图片背景", + "feature3Title": "高清3D导出", + "feature3Desc": "下载印刷级高质量3D文字图片,支持PNG/SVG等多种格式", + "ctaTitle": "立即创建3D'请勿在此页书写'背景", + "ctaSubtitle": "开始生成专业的3D文字设计", + "ctaButton": "生成3D文字" } } \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..add3a18 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/file.svg b/public/file.svg deleted file mode 100644 index 004145c..0000000 --- a/public/file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/globe.svg b/public/globe.svg deleted file mode 100644 index 567f17b..0000000 --- a/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000..5f68219 Binary files /dev/null and b/public/logo.png differ diff --git a/public/next.svg b/public/next.svg deleted file mode 100644 index 5174b28..0000000 --- a/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg deleted file mode 100644 index d2f8422..0000000 --- a/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/window.svg b/public/window.svg deleted file mode 100644 index b2b2a44..0000000 --- a/public/window.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/app/favicon.ico b/src/app/favicon.ico deleted file mode 100644 index 718d6fe..0000000 Binary files a/src/app/favicon.ico and /dev/null differ diff --git a/src/components/common/BackgroundSelector.tsx b/src/components/common/BackgroundSelector.tsx index 8a8cf6f..df6db4c 100644 --- a/src/components/common/BackgroundSelector.tsx +++ b/src/components/common/BackgroundSelector.tsx @@ -41,7 +41,7 @@ export default function BackgroundSelector({ return ( - {t("title")} + {t("title")} @@ -68,55 +68,28 @@ export default function BackgroundSelector({ {background.type === "color" && ( - - - {t("selectColor")} - - - // - // - // {t("selectColor")} - // - // - // + )} {background.type === "image" && ( - - - {t("uploadImage")} - - - + /> )} diff --git a/src/components/common/PreviewToolbar.tsx b/src/components/common/PreviewToolbar.tsx index c014e23..5194525 100644 --- a/src/components/common/PreviewToolbar.tsx +++ b/src/components/common/PreviewToolbar.tsx @@ -49,18 +49,12 @@ export default function PreviewToolbar({ resize(split[0], split[1], container.current!.clientWidth, container.current!.clientHeight); } useEffect(() => { - function init() { - if (!container.current) { - setTimeout(init, 100); - } else { - const box = container.current; - const split = Sizes[aspectRadio].split("x").map(Number); - threeInit(box, split[0], split[1]); - } + if (container.current) { + const box = container.current!; + const split = Sizes[aspectRadio].split("x").map(Number); + threeInit(box, split[0], split[1]); + console.log("init three"); } - - init(); - }, []); useEffect(updateSize, [aspectRadio]); @@ -68,6 +62,7 @@ export default function PreviewToolbar({ useEffect(() => { updateBackground(background); + console.log("background change", background); }, [background]); @@ -77,7 +72,6 @@ export default function PreviewToolbar({ console.log("text change", text); - }, [text]); const handleDownload = () => { diff --git a/src/components/common/TextSetting.tsx b/src/components/common/TextSetting.tsx index cab9d59..c3d0bb4 100644 --- a/src/components/common/TextSetting.tsx +++ b/src/components/common/TextSetting.tsx @@ -1,21 +1,22 @@ 'use client' -import { Flex, Heading, Select, Tooltip, IconButton, Link } from "@radix-ui/themes"; -import { PlusIcon, MessageCircleQuestionIcon, CircleQuestionMark, CircleQuestionMarkIcon } from "lucide-react"; +import { Flex, Heading, Select, Tooltip, IconButton, Link, Box, Tabs, RadioGroup } from "@radix-ui/themes"; +import { PlusIcon, CircleQuestionMarkIcon } from "lucide-react"; import { useLocale, useTranslations } from "next-intl"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; export const FontWeights = ["Regular", "Bold"]; export const FontNames = ["Gentilis", "Helvetiker", "Optimer", "Noto_Sans_SC_zh", "Alibaba_PuHuiTi_3.0_zh"]; -type FontFrom = "online" | "upload"; +export type ColorGradientDir = "l2r" | "t2b"; +export type FontFrom = "online" | "upload"; export class TextProp { text: string - color: string + color: string | string[] + colorGradientDir: ColorGradientDir fontFrom: FontFrom font: string fontUrl: string weight: string - constructor( text: string, color: string, @@ -25,6 +26,7 @@ export class TextProp { this.text = text; this.color = color; + this.colorGradientDir = "l2r"; this.fontFrom = fontFrom; this.font = font; this.fontUrl = getOnlineFontPath(font, weight); @@ -41,6 +43,7 @@ export class TextProp { return { text, color: "#8e86fe", + colorGradientDir: "l2r", font, fontUrl: getOnlineFontPath(font, FontWeights[0]), weight: FontWeights[0], @@ -67,6 +70,7 @@ export interface UploadFont { url: string; } +type TextMode = "color" | "gradient"; export default function TextSetting({ text, @@ -77,23 +81,73 @@ export default function TextSetting({ }) { const locale = useLocale(); - let inited = false; const t = useTranslations("TextEditor"); const [uploadFonts, setUploadFonts] = useState([]); + const isPureColor = !Array.isArray(text.color); + const [textColorMode, setTextColorMode] = useState(isPureColor ? "color" : "gradient"); + const [textColor, setTextColor] = useState(isPureColor ? text.color as string : "#000000"); + const [textGradientColor, setTextGradientColor] = useState(!isPureColor ? text.color as string[] : ["#ce6464", "#63635a"]); + const [colorGradientDir, setColorGradientDir] = useState(text.colorGradientDir as ColorGradientDir); + + let inited = useRef(false); useEffect(() => { + + if (!inited.current) { + inited.current = true; + return; + } + if (uploadFonts.length > 0) { handleSelectFont(uploadFonts[uploadFonts.length - 1].name) } else { + handleSelectFont(FontNames[0]) + } + }, [uploadFonts]); - if (inited) { - handleSelectFont(FontNames[0]) - } + let initTextColorMode = useRef(false); + useEffect(() => { + + if (!initTextColorMode.current) { + initTextColorMode.current = true; + return; } - inited = true; - }, [uploadFonts]); + if (textColorMode === "gradient") { + setText({ ...text, color: textGradientColor }) + } else { + setText({ ...text, color: textColor }) + } + }, [textColorMode]); + + let initTextColor = useRef(false); + useEffect(() => { + if (!initTextColor.current) { + initTextColor.current = true; + return; + } + + setText({ ...text, color: textColor }) + }, [textColor]); + + let initTextGradientColor = useRef(false); + useEffect(() => { + if (!initTextGradientColor.current) { + initTextGradientColor.current = true; + return; + } + setText({ ...text, color: textGradientColor }) + }, [textGradientColor]); + + let initTextGradientColorDir = useRef(false); + useEffect(() => { + if (!initTextGradientColorDir.current) { + initTextGradientColorDir.current = true; + return; + } + setText({ ...text, colorGradientDir: colorGradientDir }) + }, [colorGradientDir]); const handleSelectFont = (font: string) => { if (FontNames.indexOf(font) !== -1) { @@ -106,7 +160,7 @@ export default function TextSetting({ return ( - {t("title")} + {t("title")} setText({ ...text, text: e.target.value })} @@ -114,22 +168,85 @@ export default function TextSetting({ rows={4} /> - - {t("textColor")} - - setText({ ...text, color: e.target.value })} - className="w-full h-10 rounded-md cursor-pointer" - /> + {t("textColor")} + setTextColorMode(e as "color" | "gradient")}> + + {t("color")} + {t("textGradientColor")} + + + + + setTextColor(e.target.value)} + className="w-1/3 h-10 rounded-md cursor-pointer" + /> + setTextColor(e.target.value)} + className="w-1/3 h-10 rounded-md cursor-pointer pl-4" + /> + + + + + + + + + setColorGradientDir(value as ColorGradientDir) + }> + {t("l2r")} + {t("t2b")} + + + + + setTextGradientColor([e.target.value, text.color[1]])} + className="w-1/2 h-10 rounded-md cursor-pointer" + /> + setTextGradientColor([e.target.value, text.color[1]])} + className="w-1/2 h-10 rounded-md cursor-pointer pl-4" + /> + + + setTextGradientColor([text.color[0], e.target.value])} + + className="w-1/2 h-10 rounded-md cursor-pointer" + /> + setTextGradientColor([text.color[0], e.target.value])} + className="w-1/2 h-10 rounded-md cursor-pointer pl-4" + /> + + + + + + + + + - - + {t("fontFamily")} - + @@ -190,9 +307,9 @@ export default function TextSetting({ - + {t("fontWeight")} - + setText({ ...text, weight: e })}> diff --git a/src/components/common/ThreeTools.ts b/src/components/common/ThreeTools.ts index 37cc4c0..17a9a48 100644 --- a/src/components/common/ThreeTools.ts +++ b/src/components/common/ThreeTools.ts @@ -5,28 +5,18 @@ import { TextGeometry } from "three/addons/geometries/TextGeometry.js"; THREE.Cache.enabled = true; import { OrbitControls } from "three/addons/controls/OrbitControls.js"; -import { TextProp } from "./TextSetting"; +import { ColorGradientDir, TextProp } from "./TextSetting"; let camera: THREE.PerspectiveCamera, scene: THREE.Scene, renderer: THREE.WebGLRenderer, controls: OrbitControls, container: HTMLCanvasElement; - -console.log("three tool loaded"); - -let inited = false; - export function init( _container: HTMLCanvasElement, width: number, height: number ) { - // if (inited) { - // renderer.dispose(); - // } - - inited = true; container = _container; scene = new THREE.Scene(); @@ -135,11 +125,18 @@ export async function updateTextProps(textProps: TextProp) { // scene.add(plane); if (lastTextProps == null) { - let mat = new THREE.MeshLambertMaterial({ - color: textProps.color, + const geo = await getTextGeometry(textProps); + const mat = new THREE.MeshLambertMaterial({ side: THREE.DoubleSide, }); - let geo = await getTextGeometry(textProps); + + if (Array.isArray(textProps.color)) { + // 渐变颜色处理 + setGradient(textProps.color, textProps.colorGradientDir, geo, mat); + } else { + // 单色处理 + setColor(textProps.color, mat); + } let size = new THREE.Vector3(); geo.boundingBox?.getSize(size); let textMesh1 = new THREE.Mesh(geo, mat); @@ -155,20 +152,86 @@ export async function updateTextProps(textProps: TextProp) { return; } - const colorSame = textProps.color == lastTextProps.color; - - if (colorSame) { + if (needUpdateGeo(textProps)) { let geo = await getTextGeometry(textProps); textMesh.geometry.dispose(); textMesh.geometry = geo; } else { - (textMesh.material as THREE.MeshLambertMaterial).color.set(textProps.color); + if (Array.isArray(textProps.color)) { + setGradient( + textProps.color, + textProps.colorGradientDir, + textMesh.geometry, + textMesh.material as THREE.MeshLambertMaterial + ); + } else { + // 单色处理 + setColor(textProps.color, textMesh.material as THREE.MeshLambertMaterial); + } } scene.add(textMesh); lastTextProps = textProps; } +function needUpdateGeo(textProps: TextProp) { + return ( + lastTextProps?.text != textProps.text || + lastTextProps?.font != textProps.font || + lastTextProps?.weight != textProps.weight + ); +} + +function setGradient( + colors: string[], + dir: ColorGradientDir, + geo: THREE.BufferGeometry, + mat: THREE.MeshLambertMaterial +) { + // 渐变颜色处理 + mat.vertexColors = true; + mat.needsUpdate = true; + mat.color.set(1, 1, 1); + const startColor = new THREE.Color(colors[0]); + const endColor = new THREE.Color(colors[1]); + + const colorss = []; + const position = geo.attributes.position; + + if (dir == "l2r") { + const maxX = geo.boundingBox!.max.x; + const minX = geo.boundingBox!.min.x; + for (let i = 0; i < position.count; i++) { + const x = position.getX(i); + const t = (x - minX) / (maxX - minX); // 归一化 + const color = new THREE.Color().lerpColors(startColor, endColor, t); + colorss.push(color.r, color.g, color.b); + } + } else if (dir == "t2b") { + const maxY = geo.boundingBox!.max.y; + const minY = geo.boundingBox!.min.y; + for (let i = 0; i < position.count; i++) { + const y = position.getY(i); + const t = (y - minY) / (maxY - minY); // 归一化 + const color = new THREE.Color().lerpColors(startColor, endColor, 1.0 - t); + colorss.push(color.r, color.g, color.b); + } + } + + geo.setAttribute( + "color", + new THREE.Float32BufferAttribute(new Float32Array(colorss), 3) + ); + geo.attributes.color.needsUpdate = true; +} + +function setColor(color: string, mat: THREE.MeshLambertMaterial) { + // 渐变颜色处理 + mat.vertexColors = false; + mat.color.set(color); + mat.needsUpdate = true; +} + async function getTextGeometry(textProps: TextProp) { let text = textProps.text; let bevelEnabled = true; @@ -193,7 +256,7 @@ async function getTextGeometry(textProps: TextProp) { textGeo.computeBoundingBox(); textGeo.center(); - + textGeo.computeVertexNormals(); return textGeo; // if (mirror) { @@ -224,6 +287,24 @@ export function updateBackground(bg: BackgroundProp) { } } +// function createGradientTexture(colors: string[]) { +// const canvas = document.createElement("canvas"); +// canvas.width = 256; +// canvas.height = 1; +// const ctx = canvas.getContext("2d")!; + +// const gradient = ctx.createGradientGradient(0, 0, 256, 0); +// const step = 1 / (colors.length - 1); +// colors.forEach((color, i) => { +// gradient.addColorStop(i * step, color); +// }); + +// ctx.fillStyle = gradient; +// ctx.fillRect(0, 0, 256, 1); + +// return canvas.toDataURL(); +// } + export function getPicture(width: number, height: number) { if (width == 0 || height == 0) { render();