完成文字阴影效果
This commit is contained in:
@@ -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."
|
||||
},
|
||||
|
||||
@@ -89,6 +89,11 @@
|
||||
"faqQuestion1": "为什么预览框内的文字内容会显示问号?",
|
||||
"faqAnswer1": "因为该内容的语言没有被选中字体支持,建议更换字体或者上传自定义字体"
|
||||
},
|
||||
"Effects": {
|
||||
"title": "特效",
|
||||
"shadowOption": "阴影",
|
||||
"shadowOptionHelp": "阴影只在纯色背景下有效"
|
||||
},
|
||||
"Footer": {
|
||||
"copyright": "© {year} 3D文字设计工具 版权所有"
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -83,7 +83,7 @@ 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"}>
|
||||
@@ -95,14 +95,14 @@ export default function BackgroundSelector({
|
||||
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>
|
||||
@@ -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
|
||||
|
||||
93
src/components/common/Effects.tsx
Normal file
93
src/components/common/Effects.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 { 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 () => {
|
||||
@@ -228,7 +233,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user