1-canvas 的响应式布局
canvas 画布的尺寸有两种:
像素尺寸,即canvas画布在高度和宽度上有多少个像素,默认是300*150 css 尺寸,即css 里的width和height 在web前端,dom元素的响应式布局一般是通过css 实现的。 而canvas 则并非如此,canvas 的响应式布局需要考虑其像素尺寸。 基于上一个工程(three.js(四):react + three.js),通过让canvas 画布自适应浏览器的窗口的尺寸,来说一下canvas 的响应式布局
1.将之前的RenderStructure.tsx 复制粘贴一份,改名ResponsiveDesign.tsx,用于写响应式布局
2.将ResponsiveDesign.tsx 页面添加到路由中
import React from "react" ;
import { useRoutes } from "react-router-dom" ;
import "./App.css" ;
import MainLayout from "./view/MainLayout" ;
import Fundamentals from "./view/Fundamentals" ;
import ResponsiveDesign from "./view/ResponsiveDesign" ;
const App: React. FC = ( ) : JSX . Element => {
const routing = useRoutes ( [
{
path: "/" ,
element: < MainLayout / > ,
} ,
{
path: "Fundamentals" ,
element: < Fundamentals / > ,
} ,
{
path: "ResponsiveDesign" ,
element: < ResponsiveDesign / > ,
} ,
] ) ;
return < > { routing} < / > ;
} ;
export default App;
3.在ResponsiveDesign.tsx中先注释renderer 的尺寸设置
4.用css 设置canvas 画布及其父元素的尺寸,使其充满窗口
src/view/ResponsiveDesign
const ResponsiveDesign: React. FC = ( ) : JSX . Element => {
……
return < div ref= { divRef} className= "canvasWrapper" > < / div> ;
} ;
html {
height : 100%;
}
body {
margin : 0;
overflow : hidden;
height : 100%;
}
#root,.canvasWrapper,canvas {
width : 100%;
height : 100%;
}
5.将fullScreen.css 导入ResponsiveDesign.tsx
import "./fullScreen.css" ;
效果如下: 由上图可见,立方体的边界出现了锯齿,这就是位图被css拉伸后失真导致的,默认canvas 画布的尺寸只有300*150。 因此,需要用canvas 画布的像素尺寸自适应窗口。
6.建立一个让canvas 像素尺寸随css 尺寸同步更新的方法。
resizeRendererToDisplaySize ( renderer) ;
function resizeRendererToDisplaySize ( renderer ) {
const { width, height, clientWidth, clientHeight } = renderer. domElement;
const needResize = width !== clientWidth || height !== clientHeight;
if ( needResize) {
renderer. setSize ( clientWidth, clientHeight, false ) ;
}
return needResize;
}
renderer.setSize(w,h,bool)
是重置渲染尺寸的方法,在此方法里会根据w,h参数重置canvas 画布的尺寸。setSize方法源码如下:
this . setSize = function ( width, height, updateStyle ) {
if ( xr. isPresenting ) {
console. warn ( 'THREE.WebGLRenderer: Can\'t change size while VR device is presenting.' ) ;
return ;
}
_width = width;
_height = height;
_canvas. width = Math. floor ( width * _pixelRatio ) ;
_canvas. height = Math. floor ( height * _pixelRatio ) ;
if ( updateStyle !== false ) {
_canvas. style. width = width + 'px' ;
_canvas. style. height = height + 'px' ;
}
this . setViewport ( 0 , 0 , width, height ) ;
} ;
setSize() 方法中的bool 参数很重要,会用于判断是否设置canvas 画布的css 尺寸。
7.当canvas 画布的尺寸变化了,相机视口的宽高比也需要同步调整。拖拽浏览器的边界,缩放浏览器的时候,就可以看到canvas 画布自适应浏览器的尺寸了。
function animate ( ) {
requestAnimationFrame ( animate) ;
if ( resizeRendererToDisplaySize ( renderer) ) {
const { clientWidth, clientHeight } = renderer. domElement;
camera. aspect = clientWidth / clientHeight;
camera. updateProjectionMatrix ( ) ;
}
cubes. forEach ( ( cube ) => {
cube. rotation. x += 0.01 ;
cube. rotation. y += 0.01 ;
} ) ;
renderer. render ( scene, camera) ;
}
camera.aspect 属性是相机视口的宽高比 当相机视口的宽高比变了,相机的透视投影矩阵也会随之改变,因此需要使用camera.updateProjectionMatrix() 方法更新透视投影矩阵。 为什么不把更新相机视口宽高比的方法一起放进resizeRendererToDisplaySize()里,这是为了降低resizeRendererToDisplaySize() 方法和相机的耦合度。具体要不要这么做视项目需求而定。
示例:自适应布局示例
1.新建一个Illustration 页
src/view/Illustration.tsx
import React, { useRef, useEffect, useState } from "react" ;
import { BoxGeometry, DirectionalLight, Mesh, MeshPhongMaterial, PerspectiveCamera, Scene, WebGLRenderer } from "three" ;
import "./Illustration.css" ;
const { innerWidth, innerHeight } = window;
const scene = new Scene ( ) ;
const camera = new PerspectiveCamera ( 75 , innerWidth / innerHeight, 0.1 , 1000 ) ;
const renderer = new WebGLRenderer ( ) ;
const color = 0xffffff ;
const intensity = 1 ;
const light = new DirectionalLight ( color, intensity) ;
light. position. set ( - 1 , 2 , 4 ) ;
scene. add ( light) ;
const geometry = new BoxGeometry ( ) ;
const material = new MeshPhongMaterial ( { color: 0x44aa88 } ) ;
camera. position. z = 5 ;
const cubes = [ - 2 , 0 , 2 ] . map ( ( num) => makeInstance ( num) ) ;
scene. add ( ... cubes) ;
function resizeRendererToDisplaySize ( renderer: WebGLRenderer) {
const { width, height, clientWidth, clientHeight } = renderer. domElement;
const needResize = width !== clientWidth || height !== clientHeight;
if ( needResize) {
renderer. setSize ( clientWidth, clientHeight, false ) ;
}
return needResize;
}
function makeInstance ( x: number ) {
const cube = new Mesh ( geometry, material) ;
cube. position. x = x;
return cube;
}
function animate ( ) {
requestAnimationFrame ( animate) ;
if ( resizeRendererToDisplaySize ( renderer) ) {
const { clientWidth, clientHeight } = renderer. domElement;
camera. aspect = clientWidth / clientHeight;
camera. updateProjectionMatrix ( ) ;
}
cubes. forEach ( ( cube) => {
cube. rotation. x += 0.01 ;
cube. rotation. y += 0.01 ;
} ) ;
renderer. render ( scene, camera) ;
}
const Illustration: React. FC = ( ) : JSX . Element => {
const divRef = useRef < HTMLDivElement> ( null ) ;
let [ btnState, setBtnState] = useState ( [ "small" , "放大" ] ) ;
const toggle = ( ) => {
if ( btnState[ 0 ] === "small" ) {
setBtnState ( [ "big" , "缩小" ] ) ;
} else {
setBtnState ( [ "small" , "放大" ] ) ;
}
} ;
useEffect ( ( ) => {
const { current } = divRef;
if ( current) {
current. innerHTML = "" ;
current. append ( renderer. domElement) ;
}
animate ( ) ;
} , [ ] ) ;
return (
< div className= "cont" >
< p>
立方体,也称正方体,是由6 个正方形面组成的正多面体,故又称正六面体。它有12 条边和8 个顶点。其中正方体是特殊的长方体。立方体是一种特殊的正四棱柱、长方体、三角偏方面体、菱形多面体、平行六面体,就如同正方形是特殊的矩形、菱形、平行四边形一様。立方体具有正八面体对称性,即考克斯特BC3 对称性,施莱夫利符号
,考克斯特- 迪肯符号,与正八面体对偶。
< / p>
< div className= "inllustration" >
< div ref= { divRef} className= { ` canvasWrapper ${ btnState[ 0 ] } ` } > < / div>
< button className= "btn" onClick= { toggle} >
{ btnState[ 1 ] }
< / button>
< / div>
< p>
立方体有11 种不同的展开图,即是说,我们可以有11 种不同的方法切开空心立方体的7 条棱而将其展平为平面图形,见图1 。 [ 2 ] 立方体的11 种不同展开图。
如果我们要将立方体涂色而使相邻的面不带有相同的颜色,则我们至少需要3 种颜色(类似于四色问题)。
立方体是唯一能够独立密铺三维欧几里得空间的柏拉图正多面体,因此立方体堆砌也是四维唯一的正堆砌(三维空间中的堆砌拓扑上等价于四维多胞体)。它又是柏拉图立体中唯一一个有偶数边面——正方形面的,因此,它是柏拉图立体中独一无二的环带多面体(它所有相对的面关于立方体中心中心对称)。
将立方体沿对角线切开,能得到6 个全等的正4 棱柱(但它不是半正的,底面棱长与侧棱长之比为2 : √3)将其正方形面贴到原来的立方体上,能得到菱形十二面体 ( Rhombic
Dodecahedron) (两两共面三角形合成一个菱形)。
< / p>
< p>
立方体的对偶多面体是正八面体。 当正八面体在立方体之内: 正八面体体积: 立方体体积= [ ( 1 / 3 ) ×高×底面积] ×2 : 边= ( 1 / 3 ) ( n/ 2 ) [ ( n) / 2 ] 2 : n= 1 : 6 星形八面体的对角线可组成一个立方体。
截半立方体:从一条棱斩去另一条棱的中点得出 截角立方体
超正方体:立方体在高维度的推广。更加一般的,立方体是一个大家族,即立方形家族(又称超方形、正测形)的3 维成员,它们都具有相似的性质(如二面角都是90 °、有类似的超体积公式,即Vn- cube= a等)。
长方体、偏方面体的特例。
< / p>
< p>
立方体是唯一能够独立密铺三维欧几里得空间的柏拉图正多面体,因此立方体堆砌也是四维唯一的正堆砌(三维空间中的堆砌拓扑上等价于四维多胞体)。它又是柏拉图立体中唯一一个有偶数边面——正方形面的,因此,它是柏拉图立体中独一无二的环带多面体(它所有相对的面关于立方体中心中心对称)。
将立方体沿对角线切开,能得到6 个全等的正4 棱柱(但它不是半正的,底面棱长与侧棱长之比为2 : √3)将其正方形面贴到原来的立方体上,能得到菱形十二面体 ( Rhombic
Dodecahedron) (两两共面三角形合成一个菱形)。
< / p>
< / div>
) ;
} ;
export default Illustration;
2.设置css 样式
src/view/Illustration.css
p {
text-indent : 2em;
line-height : 24px;
font-size : 14px;
}
.cont {
width : 80%;
max-width : 900px;
margin : auto;
}
.inllustration {
position : relative;
float : left;
}
.canvasWrapper {
margin-right : 15px;
transition-property : width, height;
transition-duration : 1s, 1s;
}
.small {
width : 150px;
height : 150px;
}
.big {
width : 100%;
height : 100%;
}
.canvasWrapper canvas {
width : 100%;
height : 100%;
}
.btn {
position : absolute;
top : 0;
left : 0;
cursor : pointer;
}
3.在App.tsx 中,基于Illustration页新建一个路由
import React from "react" ;
import { useRoutes } from "react-router-dom" ;
import Basics from "./view/Basics" ;
import RenderStructure from "./view/RenderStructure" ;
import ResponsiveDesign from "./view/ResponsiveDesign" ;
import Illustration from "./view/Illustration" ;
const App: React. FC = ( ) : JSX . Element => {
const routing = useRoutes ( [
……
{
path: "Illustration" ,
element: < Illustration / > ,
} ,
] ) ;
return < > { routing} < / > ;
} ;
export default App;
4.在首页Basics.tsx中再开一个链接
import React from "react" ;
import { Link } from "react-router-dom" ;
const Basics: React. FC = ( ) : JSX . Element => {
return (
< nav style= { { width: "60%" , margin: "auto" } } >
< h2> three. js 基础示例< / h2>
< ul>
……
< li>
< Link to= "/Illustration" > Illustration 三维插图< / Link>
< / li>
< / ul>
< / nav>
) ;
} ;
export default Basics;
效果如下如: