完成核心功能
This commit is contained in:
@@ -6,20 +6,25 @@ import BackgroundSelector, {
|
||||
import PreviewToolbar from "./common/PreviewToolbar";
|
||||
import TextSetting, { FontNames, FontWeights, TextProp } from "./common/TextSetting";
|
||||
import { useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
/**
|
||||
* 全特性工具栏
|
||||
* @returns
|
||||
*/
|
||||
export default function Page() {
|
||||
|
||||
|
||||
const t = useTranslations("TextEditor");
|
||||
|
||||
const [background, setBackground] = useState<BackgroundProp>({
|
||||
type: "color",
|
||||
color: "#ffffff",
|
||||
color: "#c4b1b1",
|
||||
image: null,
|
||||
});
|
||||
const [text, setText] = useState<TextProp>({
|
||||
text: "default",
|
||||
color: "black",
|
||||
text: t("defaultText"),
|
||||
color: "#8e86fe",
|
||||
font: FontNames[0],
|
||||
weight: FontWeights[0],
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Eye, Download } from "lucide-react";
|
||||
import { BackgroundProp } from "./BackgroundSelector";
|
||||
import { TextProp } from "./TextSetting";
|
||||
import { Box, Flex } from "@radix-ui/themes";
|
||||
import { init as threeInit, updateBackground, updateTextProps } from "./ThreeTools";
|
||||
import { getPicture, resize, init as threeInit, updateBackground, updateTextProps } from "./ThreeTools";
|
||||
|
||||
const Sizes = [
|
||||
"1920x1080",
|
||||
@@ -13,6 +13,7 @@ const Sizes = [
|
||||
"800x600",
|
||||
]
|
||||
|
||||
|
||||
function gcd(a: number, b: number): number {
|
||||
return b === 0 ? a : gcd(b, a % b);
|
||||
}
|
||||
@@ -29,24 +30,25 @@ export default function PreviewToolbar({
|
||||
background: BackgroundProp;
|
||||
text: TextProp;
|
||||
}) {
|
||||
let host = process.env.NEXT_PUBLIC_HOST?.substring("https://".length);
|
||||
const t = useTranslations("PreviewBar");
|
||||
const [aspectRadio, setAspectRadio] = useState<number>(0);
|
||||
const container = useRef<HTMLCanvasElement>(null);
|
||||
const fullscreenElement = useRef<HTMLImageElement>(null);
|
||||
const [picture, setPicture] = useState<string | null>(null);
|
||||
|
||||
const updateSize = () => {
|
||||
const box = container.current!;
|
||||
const split = Sizes[aspectRadio].split("x").map(Number);
|
||||
box.width = split[0];
|
||||
box.height = split[1];
|
||||
resize(split[0], split[1], container.current!.clientWidth, container.current!.clientHeight);
|
||||
}
|
||||
useEffect(() => {
|
||||
function init() {
|
||||
if (!container.current) {
|
||||
setTimeout(init, 100);
|
||||
} else {
|
||||
updateSize();
|
||||
const box = container.current;
|
||||
threeInit(box);
|
||||
const split = Sizes[aspectRadio].split("x").map(Number);
|
||||
threeInit(box, split[0], split[1]);
|
||||
console.log("three init");
|
||||
}
|
||||
}
|
||||
@@ -69,48 +71,44 @@ export default function PreviewToolbar({
|
||||
|
||||
const handleDownload = () => {
|
||||
|
||||
if (!container.current) return;
|
||||
const canvas = container.current;
|
||||
|
||||
// 创建下载链接
|
||||
// const link = document.createElement("a");
|
||||
// link.download = `background-${downloadSize.width}x${downloadSize.height}.png`;
|
||||
// link.href = canvas.toDataURL("image/png");
|
||||
// link.click();
|
||||
const link = document.createElement("a");
|
||||
link.download = `${host}_${Sizes[aspectRadio]}.png`;
|
||||
link.href = getPicture(0, 0);
|
||||
link.click();
|
||||
|
||||
};
|
||||
|
||||
|
||||
const handleFullScreen = () => {
|
||||
if (!container.current) return;
|
||||
|
||||
const canvas = container.current;
|
||||
const width = window.screen.width;
|
||||
const height = window.screen.height;
|
||||
canvas.style.width = "100vw";
|
||||
canvas.style.height = "100vh";
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
if (canvas.requestFullscreen) {
|
||||
canvas.requestFullscreen();
|
||||
} else if ((canvas as any).webkitRequestFullscreen) {
|
||||
(canvas as any).webkitRequestFullscreen();
|
||||
} else if ((canvas as any).msRequestFullscreen) {
|
||||
(canvas as any).msRequestFullscreen();
|
||||
}
|
||||
const img = getPicture(width, height);
|
||||
setPicture(img);
|
||||
|
||||
// 退出全屏时隐藏canvas
|
||||
const onFullscreenChange = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
setTimeout(() => {
|
||||
|
||||
canvas.style.removeProperty("width");
|
||||
canvas.style.removeProperty("height");
|
||||
updateSize();
|
||||
document.removeEventListener("fullscreenchange", onFullscreenChange);
|
||||
let imgDom = fullscreenElement.current!;
|
||||
|
||||
if (imgDom.requestFullscreen) {
|
||||
imgDom.requestFullscreen();
|
||||
} else if ((imgDom as any).webkitRequestFullscreen) {
|
||||
(imgDom as any).webkitRequestFullscreen();
|
||||
} else if ((imgDom as any).msRequestFullscreen) {
|
||||
(imgDom as any).msRequestFullscreen();
|
||||
}
|
||||
};
|
||||
document.addEventListener("fullscreenchange", onFullscreenChange);
|
||||
|
||||
// 退出全屏时隐藏canvas
|
||||
const onFullscreenChange = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
setPicture(null);
|
||||
document.removeEventListener("fullscreenchange", onFullscreenChange);
|
||||
}
|
||||
};
|
||||
document.addEventListener("fullscreenchange", onFullscreenChange);
|
||||
}, 0);
|
||||
|
||||
};
|
||||
|
||||
@@ -139,6 +137,10 @@ export default function PreviewToolbar({
|
||||
backgroundPosition: "center",
|
||||
}} />
|
||||
|
||||
{picture && (
|
||||
<img ref={fullscreenElement} src={picture} className="w-screen h-screen" />
|
||||
)}
|
||||
|
||||
<Flex gap={"9"} className="justify-around">
|
||||
<button
|
||||
onClick={() => handleFullScreen()}
|
||||
|
||||
@@ -25,12 +25,6 @@ export default function TextEditor({
|
||||
}) {
|
||||
const t = useTranslations("TextEditor");
|
||||
|
||||
useEffect(() => {
|
||||
// 初始化默认文本
|
||||
let textStr = text.text == "default" ? t("defaultText") : text.text;
|
||||
setText({ ...text, text: textStr });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex className="p-4 border rounded-lg " gap={"3"} direction={"column"}>
|
||||
<Heading size={"3"} className="font-medium text-lg" >{t("title")}</Heading>
|
||||
@@ -47,7 +41,7 @@ export default function TextEditor({
|
||||
<input
|
||||
type="color"
|
||||
value={text.color}
|
||||
onChange={e => setText({ ...text, text: e.target.value })}
|
||||
onChange={e => setText({ ...text, color: e.target.value })}
|
||||
className="w-full h-10 rounded-md cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -5,52 +5,66 @@ import { Font, FontLoader } from "three/addons/loaders/FontLoader.js";
|
||||
import { TextGeometry } from "three/addons/geometries/TextGeometry.js";
|
||||
|
||||
THREE.Cache.enabled = true;
|
||||
// import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
||||
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
||||
|
||||
let camera: THREE.PerspectiveCamera,
|
||||
scene: THREE.Scene,
|
||||
renderer: THREE.WebGLRenderer,
|
||||
container: HTMLElement;
|
||||
controls: OrbitControls,
|
||||
container: HTMLCanvasElement;
|
||||
|
||||
export function init(_container: HTMLCanvasElement) {
|
||||
export function init(
|
||||
_container: HTMLCanvasElement,
|
||||
width: number,
|
||||
height: number
|
||||
) {
|
||||
container = _container;
|
||||
scene = new THREE.Scene();
|
||||
// scene.background = new THREE.Color(0xcccccc);
|
||||
// scene.fog = new THREE.FogExp2(0xcccccc, 0.002);
|
||||
|
||||
const width = container.clientWidth;
|
||||
const height = container.clientHeight;
|
||||
|
||||
renderer = new THREE.WebGLRenderer({
|
||||
antialias: true,
|
||||
alpha: true,
|
||||
canvas: container,
|
||||
});
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
// renderer.setSize(width, height);
|
||||
renderer.setPixelRatio(1);
|
||||
renderer.setSize(width, height, false);
|
||||
renderer.setAnimationLoop(animate);
|
||||
|
||||
camera = new THREE.PerspectiveCamera(60, width / height, 1, 1000);
|
||||
camera.position.set(400, 200, 0);
|
||||
camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000);
|
||||
camera.position.set(0, 80, 0);
|
||||
|
||||
// const domWidth = container.clientWidth;
|
||||
// const domHeight = container.clientHeight;
|
||||
|
||||
// camera = new THREE.OrthographicCamera(
|
||||
// domWidth / -2,
|
||||
// domWidth / 2,
|
||||
// domHeight / 2,
|
||||
// domHeight / -2,
|
||||
// 0.1,
|
||||
// 1000
|
||||
// );
|
||||
|
||||
// camera.position.set(0, 10, 0);
|
||||
|
||||
// controls
|
||||
|
||||
// controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.screenSpacePanning = false;
|
||||
|
||||
// controls.enable = false;
|
||||
// controls.listenToKeyEvents(window); // optional
|
||||
controls.enabled = true;
|
||||
|
||||
// //controls.addEventListener( 'change', render ); // call this only in static scenes (i.e., if there is no animation loop)
|
||||
//controls.addEventListener( 'change', render ); // call this only in static scenes (i.e., if there is no animation loop)
|
||||
|
||||
// controls.enableDamping = true; // an animation loop is required when either damping or auto-rotation are enabled
|
||||
// controls.dampingFactor = 0.05;
|
||||
controls.enableDamping = true; // an animation loop is required when either damping or auto-rotation are enabled
|
||||
controls.dampingFactor = 0.05;
|
||||
|
||||
// controls.screenSpacePanning = false;
|
||||
controls.screenSpacePanning = false;
|
||||
|
||||
// controls.minDistance = 100;
|
||||
// controls.maxDistance = 500;
|
||||
controls.minDistance = 0.1;
|
||||
controls.maxDistance = 50000;
|
||||
|
||||
// controls.maxPolarAngle = Math.PI / 2;
|
||||
controls.maxPolarAngle = Math.PI / 2;
|
||||
|
||||
// lights
|
||||
|
||||
@@ -64,22 +78,10 @@ export function init(_container: HTMLCanvasElement) {
|
||||
|
||||
const ambientLight = new THREE.AmbientLight(0x555555);
|
||||
scene.add(ambientLight);
|
||||
|
||||
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
||||
console.log("resize");
|
||||
|
||||
const width = container.clientWidth;
|
||||
const height = container.clientHeight;
|
||||
|
||||
camera.aspect = width / height;
|
||||
camera.updateProjectionMatrix();
|
||||
});
|
||||
|
||||
observer.observe(container);
|
||||
}
|
||||
|
||||
function animate() {
|
||||
// controls.update(); // only required if controls.enableDamping = true, or if controls.autoRotate = true
|
||||
controls.update(); // only required if controls.enableDamping = true, or if controls.autoRotate = true
|
||||
render();
|
||||
}
|
||||
|
||||
@@ -87,6 +89,26 @@ function render() {
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||
export function resize(
|
||||
width: number,
|
||||
height: number,
|
||||
clientWidth: number,
|
||||
clientHeight: number
|
||||
) {
|
||||
console.log("resize to width = " + width + " height = " + height);
|
||||
|
||||
camera.aspect = width / height;
|
||||
// camera = new THREE.OrthographicCamera(
|
||||
// clientWidth / -2,
|
||||
// clientWidth / 2,
|
||||
// clientHeight / 2,
|
||||
// clientHeight / -2,
|
||||
// 0.1,
|
||||
// 1000
|
||||
// );
|
||||
renderer.setSize(width, height, false);
|
||||
}
|
||||
|
||||
let textMesh: THREE.Mesh;
|
||||
let lastTextProps: TextProp | null = null;
|
||||
export async function updateTextProps(textProps: TextProp) {
|
||||
@@ -109,15 +131,18 @@ export async function updateTextProps(textProps: TextProp) {
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
let geo = await getTextGeometry(textProps);
|
||||
|
||||
let size = new THREE.Vector3();
|
||||
geo.boundingBox?.getSize(size);
|
||||
let textMesh1 = new THREE.Mesh(geo, mat);
|
||||
textMesh1.rotation.x = 0;
|
||||
textMesh1.rotation.y = Math.PI * 2;
|
||||
textMesh1.rotateX(-Math.PI / 2);
|
||||
textMesh1.scale.multiplyScalar(100 / size.x);
|
||||
|
||||
scene.add(textMesh1);
|
||||
|
||||
textMesh = textMesh1;
|
||||
|
||||
lastTextProps = textProps;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -130,6 +155,8 @@ export async function updateTextProps(textProps: TextProp) {
|
||||
} else {
|
||||
(textMesh.material as THREE.MeshLambertMaterial).color.set(textProps.color);
|
||||
}
|
||||
|
||||
lastTextProps = textProps;
|
||||
}
|
||||
|
||||
async function getTextGeometry(textProps: TextProp) {
|
||||
@@ -181,17 +208,6 @@ async function loadFont(textProps: TextProp) {
|
||||
return font;
|
||||
}
|
||||
|
||||
function createText(textProps: TextProp, font: Font) {}
|
||||
|
||||
// function refreshText() {
|
||||
// group.remove(textMesh1);
|
||||
// if (mirror) group.remove(textMesh2);
|
||||
|
||||
// if (!text) return;
|
||||
|
||||
// createText();
|
||||
// }
|
||||
|
||||
export function updateBackground(bg: BackgroundProp) {
|
||||
if (bg.type === "color") {
|
||||
scene.background = new THREE.Color(bg.color);
|
||||
@@ -199,3 +215,18 @@ export function updateBackground(bg: BackgroundProp) {
|
||||
scene.background = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getPicture(width: number, height: number) {
|
||||
if (width == 0 || height == 0) {
|
||||
render();
|
||||
return container.toDataURL("image/png");
|
||||
}
|
||||
|
||||
let lastWidth = renderer.domElement.width;
|
||||
let lastHeight = renderer.domElement.height;
|
||||
renderer.setSize(width, height, false);
|
||||
render();
|
||||
const img = container.toDataURL("image/png");
|
||||
renderer.setSize(lastWidth, lastHeight, false);
|
||||
return img;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user