这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助
![](https://i-blog.csdnimg.cn/blog_migrate/4184dcbffd7638478c769a5574affe73.jpeg)
先上效果
![ThreeJs手搓一个罗盘特效](https://img-blog.csdnimg.cn/img_convert/ca5924e0f7e4424e2e925b9c0b138b9a.webp?x-oss-process=image/format,png)
前言
最近在学Three.js.,对着文档看了一周多,正好赶上码上掘金的活动,就顺便写了一个小demo,手搓一个罗盘特效。
太极
先来看一下太极的实现方式,这里我们使用CircleGeometry,将其分解开来可以看出是由圆形和半圆形组成 。
CircleGeometry
CircleGeometry | 官网案例 |
---|---|
radius | 半径 |
segments | 分段(三角面)的数量 |
thetaStart | 第一个分段的起始角度 |
thetaLength | 圆形扇区的中心角 |
这里不需要用到segments,但是需要颜色,所以定义一个函数传入半径、颜色、起始角度、中心角。
const createCircle = (r, color, thetaStart, thetaLength) => {
const material = new THREE.MeshBasicMaterial({
color: color,
side: THREE.DoubleSide
});
const geometry = new THREE.CircleGeometry(r, 64, thetaStart, thetaLength);
const circle = new THREE.Mesh(geometry, material);
return circle;
};
我们只需要通过传参生产不同大小的圆或半圆,再进行位移就可以实现其效果。
参考代码/73-96行 还有一些需要注意的地方写在注释里了。
罗盘
接下来看罗盘的实现,罗盘由一个个圆环组成,一个圆环又由内圈、外圈、分隔线、文字、八卦构成。
内外圈
内外圈我们使用两个RingGeometry
RingGeometry | 官网案例 |
---|---|
innerRadius | 内部半径 |
outerRadius | 外部半径 |
thetaSegments | 圆环的分段数 |
phiSegments | 圆环的分段数 |
thetaStart | 起始角度 |
thetaLength | 圆心角 |
通过circle控制内外圆圈的尺寸,circleWidth控制圆圈的线宽
const circleWidth = [0.1, 0.1]
const circle = [0, 1];
circle.forEach((i, j) => {
const RingGeo = new THREE.RingGeometry(
innerRing + i,
innerRing + i + circleWidth[j],
64,
1
);
const Ring = new THREE.Mesh(RingGeo, material);
RingGroup.add(Ring);
});
分隔线
分隔线使用的是PlaneGeometry
PlaneGeometry | 官网案例 |
---|---|
width | 宽度 |
height | 高度 |
widthSegments | 宽度分段数 |
heightSegments | 高度分段数 |
关于分隔线,它的长度就是内外圈的差值,所以这里使用外圈的数值,确定与圆心的距离就要使用内圈的数值加上自身长度除2。除此之外,还需要计算分隔线与圆心的夹角。
for (let i = 0; i < lineNum; i++) {
const r = innerRing + circle[1] / 2;
const rad = ((2 * Math.PI) / lineNum) * i;
const x = Math.cos(rad) * r;
const y = Math.sin(rad) * r;
const planeGeo = new THREE.PlaneGeometry(lineWidth, circle[1]);
const line = new THREE.Mesh(planeGeo, material);
line.position.set(x, y, 0);
line.rotation.set(0, 0, rad + Math.PI / 2);
RingGroup.add(line);
}
文字
文字使用的是TextGeometry,定位与分隔线一致,只需要交错开来。
for (let i = 0; i < lineNum; i++) {
const r = innerRing + circle[1] / 2;
const rad = ((2 * Math.PI) / lineNum) * i + Math.PI / lineNum;
const x = Math.cos(rad) * r;
const y = Math.sin(rad) * r;
var txtGeo = new THREE.TextGeometry(text[i % text.length], {
font: font,
size: size,
height: 0.001,
curveSegments: 12,
});
txtGeo.translate(offsetX, offsetY, 0);
var txt = new THREE.Mesh(txtGeo, material);
txt.position.set(x, y, 0);
txt.rotation.set(0, 0, rad + -Math.PI / 2);
RingGroup.add(txtMesh);
不过TextGeometry的使用有一个得注意得前提,我们需要引入字体文件。
const fontLoader = new THREE.FontLoader();
const fontUrl =
"https://xtjj-1253239320.cos.ap-shanghai.myqcloud.com/fonts.json";
let font;
const loadFont = new Promise((resolve, reject) => {
fontLoader.load(
fontUrl,
function (loadedFont) {
font = loadedFont;
resolve();
},
undefined,
function (err) {
reject(err);
}
);
});
八卦
圆环中除了文字之外,还能展示八卦,通过传递baguaData给createBagua生成每一个符号。
const baguaData = [
[1, 1, 1],
[0, 0, 0],
[0, 0, 1],
[0, 1, 0],
[0, 1, 1],
[1, 0, 0],
[1, 0, 1],
[1, 1, 0],
];
for (let i = 0; i < lineNum; i++) {
const r = innerRing + circle[1] / 2;
const rad = ((2 * Math.PI) / lineNum) * i + Math.PI / lineNum;
const x = Math.cos(rad) * r;
const y = Math.sin(rad) * r;
RingGroup.add(
createBagua(baguaData[i % 8], x, y, 0 , rad + Math.PI / 2, text[0]),
);
}
createBagua参考代码/114-146行 ,和分隔线是一样的,使用了PlaneGeometry只是做了一些位置的设置。
视频贴图
在罗盘外,还有一圈视频,这里是用到了VideoTexture,实现也很简单。唯一得注意的是视频的跨域问题,需要配置video.crossOrigin = "anonymous"
const videoSrc = [
"https://xtjj-1253239320.cos.ap-shanghai.myqcloud.com/yAC65vN6.mp4",
"https://xtjj-1253239320.cos.ap-shanghai.myqcloud.com/6Z5VZdZM.mp4",
];
video.src = videoSrc[Math.floor(Math.random() * 2)];
video.crossOrigin = "anonymous";
const texture = new THREE.VideoTexture(video);
...
const material = new THREE.MeshBasicMaterial({
color: 0xffffff,
side: THREE.DoubleSide,
map: texture,
});
动画
动画总共分为三个部分,一块是旋转动画,一块是分解动画和入场动画,我们使用gsap实现。
旋转动画
gsap.to(videoGroup.rotation, {
duration: 30,
y: -Math.PI * 2,
repeat: -1,
ease: "none",
});
分解动画
.to(RingGroup.position, {
duration: 1,
ease: "ease.inOut",
y: Math.random() * 10 - 5,
delay: 5,
})
.to(RingGroup.position, {
duration: 1,
ease: "ease.inOut",
delay: 5,
y: 0,
})
}
入场动画
item.scale.set(1.2, 1.2, 1.2);
gsap.to(item.scale, {
duration: 0.8,
x: 1,
y: 1,
repeat: 0,
ease: "easeInOut",
});
旋转动画与分解动画可以写在生成函数内,也可以写在添加scene时,但是入场动画只能写到scene后,因为在生成时,动画就添加上了,当我们点击开始的时候才会将其加入场景中,而这时动画可能已经执行了。
总代码
html
<!--
灵感来源:一人之下里的八奇技————风后奇门,但是剧中和漫画中施展的罗盘有限,所以就参考了罗盘特效随便排布。
从抖音选取了两段剪辑随机播放。
实现方式:Three.js
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<canvas class="webgl"></canvas>
<div class="box">
<div>大道五十,天衍四九,人遁其一</div>
<img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e80a3fa048e84f02bb5ef5b6b04af87f~tplv-k3u1fbpfcp-no-mark:240:240:240:160.awebp?">
<div class="btn">推衍中...</div>
</div>
</body>
</html>
style
*{
margin: 0;
padding: 0;
}
body {
background-color: #3d3f42;
}
.box{
width: 350px;
height: 250px;
background-color: #000;
position:absolute;
top: calc(50% - 75px);
left: calc(50% - 150px);
border-radius: 10px;
font-size: 16px;
color: #fff;
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
}
.btn {
width: 120px;
height: 35px;
line-height: 35px;
color: #fff;
border: 2px solid #fff;
border-radius: 10px;
font-size: 20px;
transition: 0.5s;
text-align: center;
cursor:default;
opacity: 0.5;
}
img{
width: 200px;
height: 150px;
}
js
import * as THREE from "three@0.125.1";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { gsap } from "gsap@3.5.1";
// Canvas
const canvas = document.querySelector("canvas.webgl");
const box = document.querySelector(".box");
const btn = document.querySelector(".btn");
const video = document.createElement("video");
// Scene
const scene = new THREE.Scene();
//----------------------
const fontLoader = new THREE.FontLoader();
const fontUrl =
"https://xtjj-1253239320.cos.ap-shanghai.myqcloud.com/fonts.json";
let font;
const loadFont = new Promise((resolve, reject) => {
fontLoader.load(
fontUrl,
function (loadedFont) {
font = loadedFont;
resolve();
},
undefined,
function (err) {
reject(err);
}
);
});
const text = {
五行: ["金", "木", "水", "火", "土"],
八卦: ["乾", "坤", "震", "巽", "坎", "艮", "离", "兑"],
数字: ["壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖", "拾"],
天干: ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"],
地支: [
"子",
"丑",
"寅",
"卯",
"辰",
"巳",
"午",
"未",
"申",
"酉",
"戌",
"亥",
],
方位: [
"甲",
"卯",
"乙",
"辰",
"巽",
"巳",
"丙",
"午",
"丁",
"未",
"坤",
"申",
"庚",
"酉",
"辛",
"戍",
"干",
"亥",
"壬",
"子",
"癸",
"丑",
"艮",
"寅",
],
节气: [
"立 春",
"雨 水",
"惊 蛰",
"春 分",
"清 明",
"谷 雨",
"立 夏",
"小 满",
"芒 种",
"夏 至",
"小 暑",
"大 暑",
"立 秋",
"处 暑",
"白 露",
"秋 分",
"寒 露",
"霜 降",
"立 冬",
"小 雪",
"大 雪",
"冬 至",
"小 寒",
"大 寒",
],
天星: [
"天辅",
"天垒",
"天汉",
"天厨",
"天市",
"天掊",
"天苑",
"天衡",
"天官",
"天罡",
"太乙",
"天屏",
"太微",
"天马",
"南极",
"天常",
"天钺",
"天关",
"天潢",
"少微",
"天乙",
"天魁",
"天厩",
"天皇",
],
天干1: [
"甲",
" ",
"乙",
" ",
"丙",
" ",
"丁",
" ",
"戊",
" ",
"己",
" ",
"庚",
" ",
"辛",
" ",
"壬",
" ",
"癸",
" ",
"甲",
" ",
"乙",
" ",
],
地支1: [
"子",
" ",
"丑",
" ",
"寅",
" ",
"卯",
" ",
"辰",
" ",
"巳",
" ",
"午",
" ",
"未",
" ",
"申",
" ",
"酉",
" ",
"戌",
" ",
"亥",
" ",
],
};
const data = [
{
innerRing: 2,
outerRing: 1.5,
lineWidth: 0.1,
circleWidth: [0.1, 0.1],
lineNum: 8,
text: [0xffffff],
offsetX: 0,
offsetY: 0,
size: 0.3,
direction: -1,
duration: 40,
},
{
innerRing: 3.5,
outerRing: 0.7,
lineWidth: 0.15,
circleWidth: [0.1, 0.1],
lineNum: 24,
text: text["方位"],
offsetX: -0.2,
offsetY: -0.08,
size: 0.3,
direction: 1,
duration: 10,
},
{
innerRing: 4.2,
outerRing: 0.7,
lineWidth: 0.15,
circleWidth: [0.1, 0.1],
lineNum: 24,
text: text["八卦"],
offsetX: -0.2,
offsetY: -0.08,
size: 0.3,
direction: -1,
duration: 20,
},
{
innerRing: 4.9,
outerRing: 1.3,
lineWidth: 0.15,
circleWidth: [0.1, 0.1],
lineNum: 24,
text: text["方位"],
offsetX: -0.4,
offsetY: -0.2,
size: 0.6,
direction: 1,
duration: 30,
},
{
innerRing: 6.2,
outerRing: 0.4,
lineWidth: 0.15,
circleWidth: [0, 0],
lineNum: 60,
text: text["地支"],
offsetX: -0.13,
offsetY: 0.01,
size: 0.2,
direction: 1,
duration: 25,
},
{
innerRing: 6.6,
outerRing: 0.4,
lineWidth: 0.15,
circleWidth: [0, 0],
lineNum: 60,
text: text["天干"],
offsetX: -0.13,
offsetY: -0.07,
size: 0.2,
direction: 1,
duration: 25,
},
{
innerRing: 7,
outerRing: 0.5,
lineWidth: 0.15,
circleWidth: [0.1, 0.1],
lineNum: 36,
text: text["天星"],
offsetX: -0.27,
offsetY: -0.03,
size: 0.2,
direction: -1,
duration: 20,
},
{
innerRing: 7.5,
outerRing: 0.5,
lineWidth: 0.15,
circleWidth: [0.1, 0.1],
lineNum: 24,
text: text["节气"],
offsetX: -0.36,
offsetY: -0.03,
size: 0.2,
direction: 1,
duration: 30,
},
{
innerRing: 8,
outerRing: 0.8,
lineWidth: 0.15,
circleWidth: [0.1, 0.1],
lineNum: 48,
text: text["方位"],
offsetX: -0.3,
offsetY: -0.1,
size: 0.4,
direction: 1,
duration: 35,
},
{
innerRing: 8.8,
outerRing: 0.8,
lineWidth: 0.15,
circleWidth: [0.1, 0.1],
lineNum: 32,
text: text["八卦"],
offsetX: -0.3,
offsetY: -0.1,
size: 0.4,
direction: -1,
duration: 60,
},
{
innerRing: 9.6,
outerRing: 0.4,
lineWidth: 0.18,
circleWidth: [0, 0],
lineNum: 120,
text: text["地支1"],
offsetX: -0.13,
offsetY: 0.01,
size: 0.2,
direction: 1,
duration: 30,
},
{
innerRing: 10,
outerRing: 0.4,
lineWidth: 0.18,
circleWidth: [0, 0],
lineNum: 120,
text: text["天干1"],
offsetX: -0.13,
offsetY: -0.07,
size: 0.2,
direction: 1,
duration: 30,
},
{
innerRing: 10.4,
outerRing: 0.5,
lineWidth: 0.1,
circleWidth: [0.1, 0.1],
lineNum: 60,
text: text["数字"],
offsetX: -0.13,
offsetY: -0.02,
size: 0.2,
direction: 1,
duration: 25,
},
{
innerRing: 10.9,
outerRing: 0.5,
lineWidth: 0.15,
circleWidth: [0.1, 0.1],
lineNum: 50,
text: text["五行"],
offsetX: -0.13,
offsetY: -0.02,
size: 0.2,
direction: 1,
duration: 35,
},
{
innerRing: 11.7,
outerRing: 1,
lineWidth: 0.1,
circleWidth: [1, 0],
lineNum: 64,
text: [0x000000],
offsetX: 0,
offsetY: 0,
size: 0.3,
direction: 1,
duration: 30,
},
];
const Rings = [];
const duration = [
0, 0.7, 0.7, 0.7, 0.7, 0, 0.7, 0.7, 0.7, 0.7, 0.7, 0, 0.7, 0.7, 0.7,
];
//Ring
const Ring = ({
innerRing,
outerRing,
lineWidth,
circleWidth,
lineNum,
offsetX,
offsetY,
text,
size,
direction,
duration,
}) => {
const RingGroup = new THREE.Group();
const circle = [0, outerRing];
const material = new THREE.MeshStandardMaterial({
color: 0xffffff,
side: THREE.DoubleSide,
});
// create ring
circle.forEach((i, j) => {
const RingGeo = new THREE.RingGeometry(
innerRing + i,
innerRing + circleWidth[j] + i,
64,
1
);
const Ring = new THREE.Mesh(RingGeo, material);
RingGroup.add(Ring);
});
// create line
for (let i = 0; i < lineNum; i++) {
const r = innerRing + circle[1] / 2;
const rad = ((2 * Math.PI) / lineNum) * i;
const x = Math.cos(rad) * r;
const y = Math.sin(rad) * r;
const planeGeo = new THREE.PlaneGeometry(lineWidth, circle[1]);
const line = new THREE.Mesh(planeGeo, material);
line.position.set(x, y, 0);
line.rotation.set(0, 0, rad + Math.PI / 2);
RingGroup.add(line);
}
// create text
if (text.length > 1) {
for (let i = 0; i < lineNum; i++) {
const r = innerRing + circle[1] / 2;
const rad = ((2 * Math.PI) / lineNum) * i + Math.PI / lineNum;
const x = Math.cos(rad) * r;
const y = Math.sin(rad) * r;
var txtGeo = new THREE.TextGeometry(text[i % text.length], {
font: font,
size: size,
height: 0.001,
curveSegments: 12,
});
txtGeo.translate(offsetX, offsetY, 0);
var txtMater = new THREE.MeshStandardMaterial({ color: 0xffffff });
var txtMesh = new THREE.Mesh(txtGeo, txtMater);
txtMesh.position.set(x, y, 0);
txtMesh.rotation.set(0, 0, rad + -Math.PI / 2);
RingGroup.add(txtMesh);
}
}
// create bagua
if (text.length == 1) {
const baguaData = [
[1, 1, 1],
[0, 0, 0],
[0, 0, 1],
[0, 1, 0],
[0, 1, 1],
[1, 0, 0],
[1, 0, 1],
[1, 1, 0],
];
for (let i = 0; i < lineNum; i++) {
const r = innerRing + circle[1] / 2;
const rad = ((2 * Math.PI) / lineNum) * i + Math.PI / lineNum;
const x = Math.cos(rad) * r;
const y = Math.sin(rad) * r;
RingGroup.add(
createBagua(baguaData[i % 8], x, y, 0.0001, rad + Math.PI / 2, text[0]),
createBagua(baguaData[i % 8], x, y, -0.0001, rad + Math.PI / 2, text[0])
);
}
}
// animation
{
gsap.to(RingGroup.rotation, {
duration: duration,
z: Math.PI * 2 * direction,
repeat: -1,
ease: "none",
});
const amColor = { r: 1, g: 1, b: 1 };
const explode = gsap.timeline({ repeat: -1, delay: 5 });
explode
.to(RingGroup.position, {
duration: 1,
ease: "ease.inOut",
y: Math.random() * 10 - 5,
delay: 5,
})
.to(amColor, {
r: 133 / 255,
g: 193 / 255,
b: 255 / 255,
duration: 2,
onUpdate: () =>
ambientLight.color.setRGB(amColor.r, amColor.g, amColor.b),
})
.to(RingGroup.position, {
duration: 1,
ease: "ease.inOut",
delay: 5,
y: 0,
})
.to(amColor, {
r: 1,
g: 1,
b: 1,
duration: 3,
onUpdate: () =>
ambientLight.color.setRGB(amColor.r, amColor.g, amColor.b),
});
}
// rotate
RingGroup.rotateX(-Math.PI / 2);
return RingGroup;
};
//taiji
const createTaiji = (position, scale) => {
const taiji = new THREE.Group();
const createCircle = (r, color, thetaStart, thetaLength) => {
const material = new THREE.MeshBasicMaterial({
color: color,
side: THREE.DoubleSide,
});
const geometry = new THREE.CircleGeometry(r, 64, thetaStart, thetaLength);
const circle = new THREE.Mesh(geometry, material);
return circle;
};
const ying = createCircle(1.8, 0x000000, 0, Math.PI);
const yang = createCircle(1.8, 0xffffff, Math.PI, Math.PI);
const Lblack = createCircle(0.9, 0x000000, 0, Math.PI * 2);
const Lwhite = createCircle(0.9, 0xffffff, 0, Math.PI * 2);
const Sblack = createCircle(0.25, 0x000000, 0, Math.PI * 2);
const Swhite = createCircle(0.25, 0xffffff, 0, Math.PI * 2);
const Lblack1 = createCircle(0.9, 0x000000, 0, Math.PI * 2);
const Lwhite1 = createCircle(0.9, 0xffffff, 0, Math.PI * 2);
const Sblack1 = createCircle(0.25, 0x000000, 0, Math.PI * 2);
const Swhite1 = createCircle(0.25, 0xffffff, 0, Math.PI * 2);
Lblack.position.set(-0.9, 0, 0.001);
Lwhite.position.set(0.9, 0, 0.001);
Swhite.position.set(-0.9, 0, 0.002);
Sblack.position.set(0.9, 0, 0.002);
Lblack1.position.set(-0.9, 0, -0.001);
Lwhite1.position.set(0.9, 0, -0.001);
Swhite1.position.set(-0.9, 0, -0.002);
Sblack1.position.set(0.9, 0, -0.002);
taiji.add(
ying,
yang,
Lblack,
Lwhite,
Swhite,
Sblack,
Lblack1,
Lwhite1,
Swhite1,
Sblack1
);
gsap.to(taiji.rotation, {
duration: 30,
z: Math.PI * 2,
repeat: -1,
ease: "none",
});
taiji.rotateX(-Math.PI / 2);
taiji.position.set(...position);
taiji.scale.set(...scale);
return taiji;
};
scene.add(createTaiji([0, 0, 0], [1, 1, 1]));
// bagua
const createBagua = (data, x, y, z, deg, color) => {
const idx = [-0.32, 0, 0.32];
const bagua = new THREE.Group();
const material = new THREE.MeshStandardMaterial({
color: color,
side: THREE.DoubleSide,
});
data.forEach((i, j) => {
if (i == 1) {
const yang = new THREE.Mesh(new THREE.PlaneGeometry(1, 0.2), material);
yang.position.set(0, idx[j], 0);
bagua.add(yang);
}
if (i == 0) {
const ying1 = new THREE.Mesh(
new THREE.PlaneGeometry(0.45, 0.2),
material
);
const ying2 = new THREE.Mesh(
new THREE.PlaneGeometry(0.45, 0.2),
material
);
ying1.position.set(-0.275, idx[j], 0);
ying2.position.set(0.275, idx[j], 0);
bagua.add(ying1, ying2);
}
});
bagua.position.set(x, y, z);
bagua.rotation.set(0, 0, deg);
return bagua;
};
const showVideo = () => {
const videoSrc = [
"https://xtjj-1253239320.cos.ap-shanghai.myqcloud.com/yAC65vN6.mp4",
"https://xtjj-1253239320.cos.ap-shanghai.myqcloud.com/6Z5VZdZM.mp4",
];
video.src = videoSrc[Math.floor(Math.random() * 2)];
video.crossOrigin = "anonymous";
const texture = new THREE.VideoTexture(video);
const videoGroup = new THREE.Group();
for (let i = 0; i < 8; i++) {
const r = 25;
const rad = ((2 * Math.PI) / 8) * i;
const x = Math.cos(rad) * r;
const y = Math.sin(rad) * r;
const planeGeo = new THREE.PlaneGeometry(16, 9);
const material = new THREE.MeshBasicMaterial({
color: 0xffffff,
side: THREE.DoubleSide,
map: texture,
});
const plane = new THREE.Mesh(planeGeo, material);
plane.position.set(x, 4.5, y);
if (i % 2 == 0) plane.rotation.set(0, rad + Math.PI / 2, 0);
else plane.rotation.set(0, rad, 0);
videoGroup.add(plane);
}
gsap.to(videoGroup.rotation, {
duration: 30,
y: -Math.PI * 2,
repeat: -1,
ease: "none",
});
scene.add(videoGroup);
};
//loadFont, Rings
loadFont.then(() => {
data.forEach((item) => {
Rings.push(Ring(item));
});
btn.innerText = "入 局";
btn.style.opacity = 1;
btn.style.cursor = "pointer";
});
//start
const start = function () {
const showRing = (item) => {
scene.add(item);
item.scale.set(1.2, 1.2, 1.2);
gsap.to(item.scale, {
duration: 0.8,
x: 1,
y: 1,
repeat: 0,
ease: "easeInOut",
});
};
const tl = gsap.timeline();
Rings.forEach((item, idx) => {
tl.to(".webgl", { duration: duration[idx] }).call(() => {
showRing(item);
});
});
};
btn.addEventListener("click", () => {
box.style.display = "none";
start();
showVideo();
video.play();
video.loop = true;
});
//----------------------
//Light
const ambientLight = new THREE.AmbientLight(0xffffff, 1);
scene.add(ambientLight);
//Sizes
const sizes = {
width: window.innerWidth,
height: window.innerHeight,
};
// Camera
const camera = new THREE.PerspectiveCamera(
75,
sizes.width / sizes.height,
1,
1000
);
camera.position.y = 10;
camera.position.x = 10;
camera.position.z = 10;
camera.lookAt(scene.position);
scene.add(camera);
//Renderer
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true,
alpha: true,
});
renderer.setSize(sizes.width, sizes.height);
//controls
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
controls.maxDistance = 50;
controls.enablePan = false;
const tick = () => {
renderer.render(scene, camera);
controls.update();
window.requestAnimationFrame(tick);
};
tick();
window.addEventListener("resize", () => {
sizes.height = window.innerHeight;
sizes.width = window.innerWidth;
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(window.devicePixelRatio);
});