添加渐变色

This commit is contained in:
ymk
2025-08-06 20:02:43 +08:00
parent 8d37708d7a
commit b2a11c0fb6
14 changed files with 311 additions and 143 deletions

View File

@@ -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"
}
}

View File

@@ -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文字"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1 +0,0 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 391 B

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

Before

Width:  |  Height:  |  Size: 629 B

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

Before

Width:  |  Height:  |  Size: 385 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -41,7 +41,7 @@ export default function BackgroundSelector({
return (
<Box className="space-y-4 p-4 border rounded-lg min-w-64">
<Heading size={"3"} className="font-medium text-lg">{t("title")}</Heading>
<Heading as="h2" size="4" className="font-medium text-lg">{t("title")}</Heading>
<Flex gap={"2"} p="2">
<Flex gap={"1"} align={"center"}>
<Radio name="background-type" value="1" checked={background.type === "color"} onChange={() =>
@@ -68,55 +68,28 @@ export default function BackgroundSelector({
<Box className="w-full">
{background.type === "color" && (
<Text as="label" size="2">
<input
type="color"
id="color-picker"
value={background.color}
onChange={handleColorChange}
className="w-full h-10 rounded-md cursor-pointer"
/>
{t("selectColor")}
</Text>
// <div className="flex flex-col gap-2">
// <label
// htmlFor="color-picker"
// className="text-sm text-muted-foreground"
// >
// {t("selectColor")}
// </label>
// <input
// type="color"
// id="color-picker"
// value={background.color}
// onChange={handleColorChange}
// className="w-full h-10 rounded-md cursor-pointer"
// />
// </div>
<input
type="color"
id="color-picker"
value={background.color}
onChange={handleColorChange}
className="w-full h-10 rounded-md cursor-pointer"
/>
)}
{background.type === "image" && (
<div className="flex flex-col gap-2">
<label
htmlFor="file-upload"
className="text-sm text-muted-foreground"
>
{t("uploadImage")}
</label>
<input
type="file"
id="file-upload"
accept="image/*"
onChange={handleImageUpload}
className="block w-full text-sm text-muted-foreground
<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"
/>
</div>
/>
)}
</Box>
</Box>

View File

@@ -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 = () => {

View File

@@ -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<UploadFont[]>([]);
const isPureColor = !Array.isArray(text.color);
const [textColorMode, setTextColorMode] = useState<TextMode>(isPureColor ? "color" : "gradient");
const [textColor, setTextColor] = useState<string>(isPureColor ? text.color as string : "#000000");
const [textGradientColor, setTextGradientColor] = useState<string[]>(!isPureColor ? text.color as string[] : ["#ce6464", "#63635a"]);
const [colorGradientDir, setColorGradientDir] = useState<ColorGradientDir>(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 (
<Flex className="p-4 border rounded-lg " gap={"3"} direction={"column"}>
<Heading size={"3"} className="font-medium text-lg" >{t("title")}</Heading>
<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 })}
@@ -114,22 +168,85 @@ export default function TextSetting({
rows={4}
/>
<div className="space-y-1">
<label className="block text-sm text-muted-foreground">
{t("textColor")}
</label>
<input
type="color"
value={text.color}
onChange={e => setText({ ...text, color: e.target.value })}
className="w-full h-10 rounded-md cursor-pointer"
/>
<Heading as="h3" size={"3"} >{t("textColor")}</Heading>
<Tabs.Root value={textColorMode} onValueChange={(e) => setTextColorMode(e as "color" | "gradient")}>
<Tabs.List>
<Tabs.Trigger value="color">{t("color")}</Tabs.Trigger>
<Tabs.Trigger value="gradient">{t("textGradientColor")}</Tabs.Trigger>
</Tabs.List>
<Box >
<Tabs.Content value="color">
<Flex gap={"6"} p="2">
<input
type="color"
value={textColor}
onChange={e => setTextColor(e.target.value)}
className="w-1/3 h-10 rounded-md cursor-pointer"
/>
<input
type="text"
value={textColor}
onChange={e => setTextColor(e.target.value)}
className="w-1/3 h-10 rounded-md cursor-pointer pl-4"
/>
</Flex>
</Tabs.Content>
<Tabs.Content value="gradient">
<Flex gap={"2"} p="2" direction="column">
<Box>
<RadioGroup.Root value={text.colorGradientDir} orientation={"vertical"} name="colorGradientDir" onValueChange={(value) =>
setColorGradientDir(value as ColorGradientDir)
}>
<RadioGroup.Item value="l2r">{t("l2r")}</RadioGroup.Item>
<RadioGroup.Item value="t2b">{t("t2b")}</RadioGroup.Item>
</RadioGroup.Root>
</Box>
<Box>
<Flex gap={"4"}>
<input
type="color"
value={textGradientColor[0]}
onChange={e => setTextGradientColor([e.target.value, text.color[1]])}
className="w-1/2 h-10 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"
/>
</Flex>
<Flex gap={"4"}>
<input
type="color"
value={textGradientColor[1]}
onChange={e => setTextGradientColor([text.color[0], e.target.value])}
className="w-1/2 h-10 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"
/>
</Flex>
</Box>
</Flex>
</Tabs.Content>
</Box>
</Tabs.Root>
</div>
<div className="space-y-1">
<Flex gap={"2"}>
<label className="block text-sm text-muted-foreground">
<Heading as="h3" size={"3"} >
{t("fontFamily")}
</label>
</Heading>
<Tooltip content={t("how2UploadFont")} >
<Link href={`/${locale}/blogs/Create-3D-Text-with-the-Barbie-Font`}>
<IconButton radius="full" variant="ghost" >
@@ -190,9 +307,9 @@ export default function TextSetting({
</Tooltip>
</div>
<div className="space-y-2">
<label className="block text-sm text-muted-foreground">
<Heading as="h3" size={"3"} >
{t("fontWeight")}
</label>
</Heading>
<Select.Root defaultValue={`${text.weight}`} onValueChange={(e) => setText({ ...text, weight: e })}>
<Select.Trigger />

View File

@@ -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();