效果截图
实现代码
线上体验:three.js+cannon.js Web 3D
javascript
<!DOCTYPE html>
<head>
<title>three.js+cannon.js Web 3D</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
<meta name="renderer" content="webkit">
<meta name="force-rendering" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=10,chrome=1">
<meta data-rh="true" name="keywords" content="three.js,JavaScript,cannon.js">
<meta data-rh="true" name="description" content="three.js+cannon.js Web 3D">
<meta data-rh="true" property="og:title" content="THREE.JS and CANNON.JS">
<meta data-rh="true" property="og:url" content="">
<meta data-rh="true" property="og:description" content="three.js+cannon.js Web 3D">
<meta data-rh="true" property="og:image" content="">
<meta data-rh="true" property="og:type" content="article">
<meta data-rh="true" property="og:site_name" content="">
<link rel="icon" href="">
<style>
.fullscreen {
margin: 0px;
padding: 0px;
width: 100vw;
height: 100vh;
overflow: hidden;
}
html, body {
overflow: hidden;
font-family: '宋体', sans-serif;
color: #2f2f2f;
}
#threeImage{
display: none;
}
</style>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.162.0/+esm",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.162.0/examples/jsm/",
"lil-gui": "https://threejsfundamentals.org/3rdparty/dat.gui.module.js",
"@tweenjs/tween.js": "https://cdn.jsdelivr.net/npm/@tweenjs/tween.js@23.1.1/dist/tween.esm.js",
"cannonjs": "https://cdn.bootcdn.net/ajax/libs/cannon.js/0.6.2/cannon.min.js"
}
}
</script>
</head>
<body class="fullscreen">
<div id="threeImage" >
<img id="testImage" src="" crossorigin="anonymous"/>
</div>
<canvas></canvas>
<script type="module">
import * as THREE from 'three';
import * as TWEEN from '@tweenjs/tween.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { GUI } from 'lil-gui';
import 'cannonjs';
let container;
let scene, camera, renderer, controls;
let NEAR = 0.1, FAR = 1000;
let light, ambient, stats, info, mouseX, mouseY;
let SHADOW_MAP_WIDTH = 512;
let SHADOW_MAP_HEIGHT = 512;
let MARGIN = 0;
let SCREEN_WIDTH = window.innerWidth;
let SCREEN_HEIGHT = window.innerHeight - 2 * MARGIN;
//物理世界
let world;
// 创建默认材质
let defaultMaterial = null
//创建材质
let bMaterial = null
let cube, tempCubes = new THREE.Object3D();
let color = "#12345678";
function setup() {
setupScene();
setupCamera();
setupRenderer();
setupLights();
setPlane();
setControls();
initCannon();
setupEventListeners();
animate();
addGUI();
}
function setupScene() {
scene = new THREE.Scene();
scene.fog = new THREE.Fog( 0x222222, 1000, FAR );
scene.background = new THREE.Color(0xf5e6d3);
}
function setupCamera() {
let res = SCREEN_WIDTH / SCREEN_HEIGHT;
camera = new THREE.PerspectiveCamera(45, res, NEAR, FAR);
//camera.up.set(0,0,1);
//camera.position.set(0,30,20);
//camera.position.z = 19;
//camera.position.y = -45;
camera.position.set(0, 30, 30)
//camera.lookAt(0, 0, 0);
//camera.lookAt(new THREE.Vector3(0, 0, 0));
}
let rotWorldMatrix;
function rotateAroundWorldAxis(object, axis, radians) {
//object:需要旋转的物体,axis:旋转中心轴向量,radians:
rotWorldMatrix = new THREE.Matrix4();
rotWorldMatrix.makeRotationAxis(axis.normalize(), radians);
rotWorldMatrix.multiply(object.matrix);
// pre-multiply
object.matrix = rotWorldMatrix;
object.rotation.setFromRotationMatrix(object.matrix);
}
function setupRenderer() {
renderer = new THREE.WebGLRenderer({
clearColor: 0x000000,
clearAlpha: 1,
antialias: true,// 抗锯齿
logarithmicDepthBuffer: true // 使用Three进行加载模型时,总会遇到模型相接处或某些区域出现频闪问题或内容被相邻近元素覆盖掉的情况,对数缓存开启可解决,使用对数缓存
});
renderer.setSize(window.innerWidth, window.innerHeight);
//renderer.setSize( SCREEN_WIDTH, SCREEN_HEIGHT );
renderer.setClearColor( scene.fog.color, 1 );
renderer.autoClear = false;
renderer.shadowMapEnabled = true;
renderer.shadowMapSoft = true;
//开启阴影效果 设置阴影类型
// BasicShadowMap 能够给出没有经过过滤的阴影映射 ------ 速度最快,但质量最差。
// PCFShadowMap 为默认值,使用Percentage-Closer Filtering (PCF)算法来过滤阴影映射。
// PCFSoftShadowMap 和PCFShadowMap一样使用 Percentage-Closer Filtering (PCF)算法过滤阴影映射,但在使用低分辨率阴影图时具有更好的软阴影。
// VSMShadowMap 使用Variance Shadow Map (VSM)算法来过滤阴影映射。当使用VSMShadowMap时,所有阴影接收者也将会投射阴影
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.BasicShadowMap
renderer.shadowMap.autoUpdate = false
renderer.shadowMap.needsUpdate = true
// 是否使用传统照明模式,默认为是,关闭传统照明模式即可模仿物理光照,光亮随距离可递减
renderer.useLegacyLights = false;
// 设置色调映射
renderer.toneMapping = THREE.ACESFilmicToneMapping;
// 曝光强度
renderer.toneMappingExposure = 0.8
renderer.outputEncoding = THREE.sRGBEncoding;
container = document.createElement( 'div' );
document.body.appendChild( container );
//document.body.appendChild(renderer.domElement);
//renderer.domElement.style.position = "relative";
//renderer.domElement.style.top = MARGIN + 'px';
container.appendChild( renderer.domElement );
}
function setPlane(){
let planeGeometry = new THREE.PlaneGeometry(60, 20);
let planeMaterial = new THREE.MeshBasicMaterial({color: 0xffffff});
let plane = new THREE.Mesh(planeGeometry, planeMaterial);
// 几何体绕着x轴旋转-90度
plane.rotateX(-Math.PI/2);
// 设置平面网格为接受阴影的投影面
plane.receiveShadow = true;
scene.add(plane);
}
function setupLights() {
let ambientLight = new THREE.AmbientLight(0xffffff);
ambientLight.castShadow = true;
scene.add(ambientLight);
// LIGHTS
ambient = new THREE.AmbientLight( 0xffffff );
ambient.castShadow = true;
scene.add( ambient );
// 添加聚光灯1
addSpotlight(50,50,50);
// 添加聚光灯2
addSpotlight(-50,50,50);
// 添加聚光灯3
addSpotlight(50,50,-50);
// 添加聚光灯4
addSpotlight(-50,50,-50);
addLight();
}
function addLight(){
light = new THREE.SpotLight( 0xffffff );
light.position.set( 30, 30, 30 );
light.target.position.set( 0, 0, 0 );
light.castShadow = true;
light.shadowCameraNear = 10;
light.shadowCameraFar = 100;//camera.far;
light.shadowCameraFov = 30;
light.shadowMapBias = 0.0039;
light.shadowMapDarkness = 0.5;
light.shadowMapWidth = SHADOW_MAP_WIDTH;
light.shadowMapHeight = SHADOW_MAP_HEIGHT;
light.shadowCameraVisible = true;
scene.add( light );
}
function setupEventListeners() {
window.addEventListener("resize", onWindowResize);
}
function onDocumentMouseMove( event ) {
mouseX = ( event.clientX - windowHalfX );
mouseY = ( event.clientY - windowHalfY );
}
function onWindowResize( event ) {
SCREEN_WIDTH = window.innerWidth;
SCREEN_HEIGHT = window.innerHeight;
renderer.setSize( SCREEN_WIDTH, SCREEN_HEIGHT );
//camera.aspect = window.innerWidth / window.innerHeight;
//renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = SCREEN_WIDTH / SCREEN_HEIGHT;
camera.updateProjectionMatrix();
//controls.screen.width = SCREEN_WIDTH;
//controls.screen.height = SCREEN_HEIGHT;
camera.radius = ( SCREEN_WIDTH + SCREEN_HEIGHT ) / 4;
}
//let clock = new THREE.Clock()
function render () {
renderer.clear();
renderer.render(scene, camera);
controls.update();
//let delta = clock.getDelta()
//world.step(delta)
// 更新物理世界 step ( dt , [timeSinceLastCalled] , [maxSubSteps=10] )
// dt:固定时间戳(要使用的固定时间步长)
// [timeSinceLastCalled]:自上次调用函数以来经过的时间
// [maxSubSteps=10]:每个函数调用可执行的最大固定步骤数
// * 设置更新物理世界world的步长timestep
// * 这里选用60Hz的速度,即1.0 / 60.0
world.step(1.0 / 60.0)
//world.fixedStep()
}
function animate() {
render();
requestAnimationFrame(animate);
}
function addTexture(url){
// 加载纹理
const textureLoader = new THREE.TextureLoader()
const texture = textureLoader.load(url)
//textureLoader.load(url, (texture) => {
// texture.mapping = THREE.EquirectangularReflectionMapping
// scene.background = texture
// scene.environment = texture
// // 背景模糊强度
// scene.backgroundBlurriness = 0.01
//})
return texture
}
function setControls(){
controls = new OrbitControls( camera, renderer.domElement );
controls.enableDamping = true;
controls.rotateSpeed = 1.0;
controls.zoomSpeed = 1.2;
controls.panSpeed = 0.2;
controls.noZoom = false;
controls.noPan = false;
controls.staticMoving = false;
controls.dynamicDampingFactor = 0.3;
const radius = 10;
controls.minDistance = 0.0;
controls.maxDistance = radius * 10;
controls.enablePan = true
controls.dampingFactor = 0.25
controls.screenSpacePanning = false
controls.enableZoom = true
controls.zoomScale = 10
controls.minZoom = 1
controls.maxZoom = 100
//controls.minPolarAngle = 1 * -Math.PI / 180
//controls.maxPolarAngle = 90 * Math.PI / 180
//controls.minAzimuthAngle = 90 * -Math.PI / 180
//controls.maxAzimuthAngle = 90 * Math.PI / 180
//controls.keys = [ 65, 83, 68 ]; // [ rotateKey, zoomKey, panKey ]
//controls.screen.width = SCREEN_WIDTH;
//controls.screen.height = SCREEN_HEIGHT;
/*
// Trackball controls
controls = new THREE.TrackballControls( camera, renderer.domElement );
controls.rotateSpeed = 1.0;
controls.zoomSpeed = 1.2;
controls.panSpeed = 0.2;
controls.noZoom = false;
controls.noPan = false;
controls.staticMoving = false;
controls.dynamicDampingFactor = 0.3;
var radius = 100;
controls.minDistance = 0.0;
controls.maxDistance = radius * 1000;
//controls.keys = [ 65, 83, 68 ]; // [ rotateKey, zoomKey, panKey ]
controls.screen.width = SCREEN_WIDTH;
controls.screen.height = SCREEN_HEIGHT;
*/
}
function addSpotlight (x,y,z){
const spotLight2 = new THREE.SpotLight(0xffffff, 1)
spotLight2.position.set(x, y, z)
spotLight2.target.position.set( 0, 0, 0 )
spotLight2.castShadow = true
spotLight2.shadow.camera.near = 0.1
spotLight2.shadow.camera.far = 30
spotLight2.shadow.camera.fov = 30
spotLight2.shadow.mapSize.width = 256
spotLight2.shadow.mapSize.height = 256
// 设置灯光 bias ,解决自阴影问题
spotLight2.shadow.bias = -0.0008
spotLight2.power = 1
scene.add(spotLight2)
// 使用辅助器对灯光和阴影进行调整
//const cameraHelper = new THREE.SpotLightHelper(spotLight2)
//scene.add(cameraHelper)
}
function initCannon() {
// 初始化物理世界
world = new CANNON.World()
// 设置物理世界重力加速度 单位:m/s² 重力加速度x、y、z分量值,假设y轴竖直向上,这样重力就速度就是y方向负方向。
world.gravity.set(0, -9.82, 0)
// npm install cannon-es-debugger
// 加入 cannon-es-debugger 可以展示模型的物理世界的轮廓
// scene: 场景
// 物理世界
// 第三个参数为可选参数,其中的的onInit方法返回场景中的每个刚体和对应的物理世界的轮廓的three mesh
// const cannonDebugger = CannonDebugger(scene, world)
// const cannonDebugger = CannonDebugger(scene, world, {
// onInit(body: CANNON.Body, mesh: THREE.Mesh) {
// //
// mesh.visible = true
// console.log(body);
// },
// })
// 还要在每帧更新调用中更新 Update the CannonDebugger meshes
// cannonDebugger.update()
// 创建默认材质
defaultMaterial = new CANNON.Material('default')
//创建足球材质
bMaterial = new CANNON.Material('bMaterial')
// 定义两种材质之间的摩擦因数和弹力系数 设置地面材质和小球材质之间的碰撞反弹恢复系数
const defaultContactMaterial = new CANNON.ContactMaterial(defaultMaterial, bMaterial, {
friction: 5,
restitution: 0.5, //反弹恢复系数
})
// 把关联的材质添加到物理世界中
world.addContactMaterial(defaultContactMaterial)
// NaiveBroadphase Cannon 默认的算法。检测物体碰撞时,一个基础的方式是检测每个物体是否与其他每个物体发生了碰撞
// GridBroadphase 网格检测。轴对齐的均匀网格 Broadphase。将空间划分为网格,网格内进行检测。
// SAPBroadphase(Sweep-and-Prune) 扫描-剪枝算法,性能最好。
// 默认为 NaiveBroadphase,建议替换为 SAPBroadphase
// 碰撞算法
world.broadphase = new CANNON.SAPBroadphase(world)
//world.broadphase = new CANNON.NaiveBroadphase();
// 创建一个物理世界的平面
const planeShape = new CANNON.Plane()
// 创建一个刚体
const planeBody = new CANNON.Body({
mass: 0, // 设置质量为0,不受碰撞的影响
shape: planeShape,
position: new CANNON.Vec3(0, 1, 0)
})
// 改变平面默认的方向,法线默认沿着z轴,旋转到平面向上朝着y方向
// 设置刚体旋转(设置旋转X轴)旋转规律类似threejs 平面
planeBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2)
// 将刚体添加到物理世界当中
world.addBody(planeBody)
const textureLoader = new THREE.TextureLoader();
let texture = textureLoader.load('https://ts2.cn.mm.bing.net/th?id=OJ.nhM9wE4TnIlRRg');
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(1,1);
texture.needsPMREMUpdate = true
texture.minFilter = THREE.LinearFilter
texture.magFilter = THREE.LinearFilter
scene.environment = texture;
let cubeGeometry = new THREE.BoxGeometry(1,1,1);
// http://www.webgl3d.cn/pages/21003f/ PBR材质金属度、粗糙度以及环境贴图
const cMaterial = new THREE.MeshPhysicalMaterial({
map: texture,
side: THREE.DoubleSide,
normalMap: texture,
normalScale: new THREE.Vector2(1.1,1.1),
specularMap: texture,
specular: 0xffffff,
shininess: 10,
envMap: texture,
color: color,
metalness: 0.5,//像金属的程度, 非金属材料,如木材或石材,使用0.0,金属使用1.0。
roughness: 0.5,//模型表面的光滑或者说粗糙程度,越光滑镜面反射能力越强,越粗糙,表面镜面反射能力越弱,更多地表现为漫反射。
clearcoat: 1,
clearcoatRoughness: 0.01,
envMapIntensity: 2.5,
opacity: 0.5,
transparent: true
});
cube = new THREE.Mesh(cubeGeometry,cMaterial);
cube.position.x=0;
cube.position.y=2;
cube.position.z=0;
cube.castShadow = true;
scene.add(cube);
}
const calcMeshCenter = (group)=>{
/**
* 包围盒全自动计算:模型整体居中
*/
let box3 = new THREE.Box3()
// 计算层级模型group的包围盒
// 模型group是加载一个三维模型返回的对象,包含多个网格模型
box3.expandByObject(group)
// 计算一个层级模型对应包围盒的几何体中心在世界坐标中的位置
let center = new THREE.Vector3()
box3.getCenter(center)
// console.log('查看几何体中心坐标', center);
// 重新设置模型的位置,使之居中。
group.position.x = group.position.x - center.x
group.position.y = group.position.y - center.y
group.position.z = group.position.z - center.z
}
const gui = new GUI({
width: 280,
title: 'Setting',
autoPlace: true
})
var params = new function() {
this.color = 0x123456;
this.position = -35;
this.visible = true;
};
function addGUI(){
gui.addColor(params, "color").onChange(e => {
cube.material.color.set(e);
});
gui.add(params, "position", -172, 28).onChange(e => {
tempCubes.position.x = e
});
}
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d',{
//频繁读取画布信息
willReadFrequently: true
});
function initCanvasSize(){
//canvas绘制的图形是位图,即栅格图像或点阵图像,当将它渲染到高清屏时,会被放大,每个像素点会用devicePixelRatio的平方个物理像素点来渲染,因此图片会变得模糊。
//将画布尺寸设置为画板尺寸的window.devicePixelRatio倍
canvas.width = window.innerWidth * window.devicePixelRatio;
canvas.height = "150"//window.innerHeight * window.devicePixelRatio;
}
initCanvasSize();
//获取[min,max]范围内的随机数
function getRandom(min, max){
return Math.floor(Math.random()*(max+1-min)+min);
}
//通过构造函数生成圆形颗粒
class Particle{
constructor(){
//生成圆圈的半径
const r = Math.min(canvas.width,canvas.height)/2;
//画布的中心点
const cx = canvas.width/2;
const cy = canvas.height/2;
//大圆上的随机角度并换算成弧度
const rad = getRandom(0,360)*(Math.PI/180);
//粒子的坐标
this.x = cx + r*Math.cos(rad);
this.y = cy + r*Math.sin(rad);
//粒子的尺寸
this.size = getRandom(1*window.devicePixelRatio,5*window.devicePixelRatio);
}
randomHexColor() { //随机生成十六进制颜色
const hex = Math.floor(Math.random() * 16777216).toString(16); //生成ffffff以内16进制数
while (hex.length < 6) { //while循环判断hex位数,少于6位前面加0凑够6位
hex = '0' + hex;
}
return '#' + hex; //返回'#'开头16进制颜色
}
draw(){
ctx.beginPath();
ctx.fillStyle = color;
//ctx.fillStyle = this.randomHexColor();
ctx.arc(this.x,this.y,this.size,0,2*Math.PI);
ctx.fill();
}
moveTo(targetX,targetY){
//设定一个缓动时间 500 ms毫秒
//起始位置
const startX = this.x;
const startY = this.y;
const duration = 500;
//横向速度 目标位置 减去 当前位置 再除以运动的时间
const xSpeed = (targetX - startX)/duration;
//纵向速度 目标位置 减去 当前位置 再除以运动的时间
const ySpeed = (targetY - startY)/duration;
//起始时间
const startTime = Date.now();
//增加动画效果
const _move = ()=>{
//运动时间
const t = Date.now() - startTime;
//x方向运动距离 起始位置 加 速度乘以时间
const x = startX + xSpeed * t;
//y方向运动距离 起始位置 加 速度乘以时间
const y = startY + ySpeed * t;
//x,y缓动
this.x = x;
this.y = y;
//超过设定时间取消运动
if(t>=duration){
// 赋值为目标位置
this.x = targetX
this.y = targetY
}
//重新注册raf
requestAnimationFrame(_move)
}
//执行移动
_move();
}
}
//点数据组
const partciles = [];
//需要绘制的文本
let text = null;
//填充点数据
for(let i = 0;i<1;i++){
//画单个点
const p = new Particle();
//启动绘制
//p.draw();
//添加进数组
partciles.push(p);
}
//清空画布
function clear(){
//清空画布
ctx.clearRect(0,0,canvas.width,canvas.height);
tempCtx.clearRect(0,0,tempCanvas.width,tempCanvas.height);
// 将缓存 canvas 复制到旧的 canvas
//ctx.drawImage(tempCanvas,0,0,canvas.width,canvas.height);
}
// 新建一个 canvas 作为缓存 canvas
const tempCanvas = document.createElement('canvas');
// 设置缓冲画布的宽高
tempCanvas.width = canvas.width;
tempCanvas.height = canvas.height;
const tempCtx = tempCanvas.getContext('2d');
//绘制
function draw(){
//在每次绘制文本之前清空画布
clear();
// 开始绘制
//let img = document.createElement('img');
//let data = canvas.toDataURL();
//img.src = data;
// 缓存 canvas 绘制完成
//tempCtx.drawImage(img,0,0);
update();
//遍历数组中的所有点
partciles.forEach((p)=>{
//绘制点
p.draw();
//p.moveTo(0,0);
})
//下一次绘制时注册RAF,重新绘制
requestAnimationFrame(draw);
}
//生成文字
function getText(){
//得到当前时间 并做截取
return new Date().toTimeString().substring(0,8);
}
//绘制文字
function update(){
//获得文本
const newText = getText();
if(newText == text){
//相同就不绘制
return;
}
text = newText;
//绘制 文本
const { width, height } = tempCanvas;
if(drImage){
// let temp = localStorage.getItem("threeImage");
// CSSImageValue 或 HTMLCanvasElement 或 HTMLImageElement 或 HTMLVideoElement 或 ImageBitmap 或 OffscreenCanvas 或 SVGImageElement 或 VideoFrame
//tempCtx.drawImage(document.getElementById("testImage"),0,0,tempCanvas.width,tempCanvas.height);
tempCtx.fillStyle = "#000";
tempCtx.textBaseline = "middle";
tempCtx.font = `${100*window.devicePixelRatio}px '华文细黑', sans-serif`;
//计算出绘制出的文本的宽度
const textWidth = tempCtx.measureText(text).width;
tempCtx.fillText(text,(width - textWidth)/3,height/2);
//imageBox.appendChild(tempCanvas);
}
//拿到画布上黑色像素点的坐标
const points = getPoints();
//console.log(points);
//如果数组长度小于文本坐标的长度 删除多余部分即可
if(points.length < partciles.length){
partciles.splice(points.length);
}
scene.remove(tempCubes); // 更新前删除旧数码显示
tempCubes = null;
tempCubes = new THREE.Object3D();
for(let i=0;i<points.length;i++){
let p = partciles[i];
//粒子不足创建即可
if(!p){
p = new Particle();
partciles.push(p);
}
const [x,y] = points[i];
p.moveTo(x,y);
let tempCube = cube.clone();
tempCube.position.set((x/10),(y/10),(y/10));
tempCubes.add(tempCube);
}
if(tempCubes){
//calcMeshCenter(tempCubes);
rotateAroundWorldAxis(tempCubes,new THREE.Vector3(1,0,0), Math.PI);
tempCubes.position.set(-36,10,0);
tempCubes.rotateX(-Math.PI/2);
tempCubes.scale.set(0.5, 0.5, 0.5);
//flipMesh(tempCubes);
let position = new THREE.Vector3();
let quaternion = new THREE.Quaternion();
let scale = new THREE.Vector3();
tempCubes.matrixWorld.decompose( position, quaternion, scale );
tempCubes.updateMatrixWorld( true );
scene.add(tempCubes);
}
}
function flipMesh(object3D) {
object3D.applyMatrix(new THREE.Matrix4().makeScale(-1, 1, 1));
reverseWindingOrder(object3D);
}
function reverseWindingOrder(object3D) {
// TODO: Something is missing, the objects are flipped alright but the light reflection on them is somehow broken
if (object3D.type === "Mesh") {
var geometry = object3D.geometry;
for (var i = 0, l = geometry.faces.length; i < l; i++) {
var face = geometry.faces[i];
var temp = face.a;
face.a = face.c;
face.c = temp;
}
var faceVertexUvs = geometry.faceVertexUvs[0];
for (i = 0, l = faceVertexUvs.length; i < l; i++) {
var vector2 = faceVertexUvs[i][0];
faceVertexUvs[i][0] = faceVertexUvs[i][2];
faceVertexUvs[i][2] = vector2;
}
geometry.computeFaceNormals();
geometry.computeVertexNormals();
}
if (object3D.children) {
for (var j = 0, jl = object3D.children.length; j < jl; j++) {
reverseWindingOrder(object3D.children[j]);
}
}
}
function getPoints(){
// 得到画布上制定范围内的像素点信息
const { width, height, data} = tempCtx.getImageData(0,0,tempCanvas.width,tempCanvas.height);
//console.log(data);
const points = [];
//像素点取稀释点
const gap = 4;
for(let i = 0; i< width;i+=gap){
for(let j = 0; j< height;j+=gap){
//通过行号、列号 计算像素点的下标
const index = (i+j*width)*4;
const r = data[index];
const g = data[index+1];
const b = data[index+2];
const a = data[index+3];
//判断是否透明
if(r===0&&g===0&&b===0&&a===255){
//console.log(r,g,b,a);
points.push([i,j])
}
}
}
return points;
}
let imageBox = document.getElementById('threeImage')
const loadImage = (url) => {
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = () => {
resolve(img);
}
img.onerror = (err) => {
reject(err)
}
img.src = url
img.alt = "threeImage";
})
}
function startDownload(url) {
document.getElementById("testImage").src = url;
document.getElementById("testImage").width = window.innerWidth
document.getElementById("testImage").height = window.innerHeight
loadImage(url).then((resImage)=>{
imageReceived(resImage)
}).catch((err)=>{
throw new Error(err); // reject promise
});
//let imageURL = url;
//let imageDescription = "three image";
//downloadedImg = new Image();
//downloadedImg.crossOrigin = "Anonymous";
//downloadedImg.addEventListener("load", imageReceived, false);
//downloadedImg.alt = imageDescription;
//downloadedImg.src = imageURL;
}
let drImage = null
function imageReceived(downloadedImg) {
const c = document.createElement("canvas");
let context = c.getContext("2d");
c.width = window.innerWidth || downloadedImg.width;
c.height = window.innerHeight || downloadedImg.height;
c.innerText = downloadedImg.alt;
//downloadedImg.setAttribute('crossOrigin', 'anonymous');
//context.drawImage(downloadedImg, 0, 0, c.width, c.height);
//imageBox.appendChild(c);
//确保图片被加载完成 使用drawImage绘制到画布上面
drImage = downloadedImg; //画布已被跨域数据污染的操作
try {
localStorage.setItem("threeImage", c.toDataURL("image/png"));
draw();
// 复杂问题拆解成简单化问题
// 画单个点,画很多个点,画很多个运动的点
// 提升代码掌控能力,开发思维
} catch (err) {
console.error(`Error: ${err}`);
}
}
//需要服务器端支持 CORS policy: No 'Access-Control-Allow-Origin'
startDownload('https://ts2.cn.mm.bing.net/th?id=OJ.nhM9wE4TnIlRRg');
setup();
</script>
</body>
</html>