添加渐变色
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
After Width: | Height: | Size: 4.2 KiB |
@@ -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 |
@@ -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
|
After Width: | Height: | Size: 480 KiB |
@@ -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 |
@@ -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 |
@@ -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 |
|
Before Width: | Height: | Size: 25 KiB |
@@ -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>
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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();
|
||||
|
||||