Threejs(下面简称 Three) 作为一个 3D 库,不仅减少了我们学习 OpenGL 和 WebGL 的成本,还大大提升了前端在可视化上给用户带来更多的真实、沉浸式的体验。众所周知,Three 更多的是用 3D 模型 + 投影相机 + 用户交互的方式来构建一个「3D 世界」。
这张专辑,用眼睛去“听” 活动中,在视觉在只能提供「2D 切图」的情况下,需要营造「3D 效果」。为了获得最好视觉体验,仅仅通过贴图很难做到,所以借此机会探索了 Three 的动效方案。
运动往往是相对的,运动的本质可能是「物体动」或「相机动」,本文将从对象动画和相机动画上阐述对 Three 的动效探索。
Three 提供多种相机,其中应用最广的就是投影相机 (PerspectiveCamera) ,通过投影相机可以模拟人眼所看见的效果。
const camera=THREE.PerspectiveCamera(fov, aspect, near, far);
复制代码
参数 含义 默认值 fov fov 是视景体竖直方向上(非水平!)的张角,人类有接近180度的视角大小。该值可根据具体场景所需要的视角设置。 45 aspect 指定渲染结果的横向尺寸和纵向尺寸的比值。该值通常设置为窗口大小的宽高比。 window.innerWidth / window.innerHeight near 表示可以看到多近的物体。这个值通常很小。 0.1 far 表示可以看到多远的物体。这个看情况设置,过大会导致渲染过多;太小可能又会看不到。 1000
ps: 在 Three 中是没有「长度单位」这个概念的,它的数值都是根据比例计算得出,因此这里提到的 0.1 或 1000 都没有具体的含义,而是一种相对长度。
可以看到,通过配置透视相机的相关参数,最终被渲染到屏幕上的,是在 near 到 far 之间,根据 fov 的值和物体远近 d 确定渲染高度,再通过 aspect 值来确定渲染宽度的。
有了相机,我们还要有场景,场景是为了让我们设置我们的空间内「有什么」和「放在哪」的。我们可以在场景中放置物体,光源还有相机。
const scene=new THREE.Scene();
复制代码
是的,创建场景就是这么简单。
为了以群的维度去区分场景中的物体,我们还可以在场景中添加 Group。有了 Group,可以更方便地操作一类物体。 比如创建一个 stoneGroup,并添加到场景中:
const stoneGroup=new THREE.Group();
stoneGroup.name='stoneGroup';
scene.add(stoneGroup);
复制代码
为 Group 命名,允许我们通过 name 来获取到对应的 Group:
const group=scene.getObjectByName(name);
复制代码
Three 提供了多种类型的几何体,可以分为二维网格和三维网格。二维网格顾名思义只有两个维度,可以通过这种几何体创建简单的二维平面;三维网格允许你定义三维物体;在 Three 中定义一个几何体十分简单,只需要选择需要的几何体并传入相应参数创建即可。
查看Three提供的几何体
如果看到 Three 提供的几何体,可以看到有的几何体中它分别提供 Geometery 和 BufferGeometery 版本,关于这两个的区别,可以看这里 回答
大致意思就是使用 Buffer 版本的几何体相较于普通的几何体会将描述物体的数据存放在缓冲区中,减少内存消耗和 CPU 循环。通过它们提供的方法来看,使用 geometry 无疑是对新手友好的。
创建几何体:
// 创建立方体,传入长、宽和高
var cubeGeometry=new THREE.CubeGeometry(40, 40, 40);
// 创建球体,传入半径、宽片段数量和高片段数量
var sphereGeometry=new THREE.SphereGeometry(20, 100, 100);
复制代码
定义材质可以帮助我们决定一个物体在各种环境情况下的具体表现。同样 Three 也提供了多种材质。下面列举几个常用的材质。
名称 描述 MeshBasicMaterial 基础材质,用它定义几何体上的简单颜色或线框 MeshPhongMaterial 受光照影响,用来创建光亮的物体 MeshLambertMaterial 受光照影响,用来创建不光亮的物体 MeshDepthMaterial 根据相机远近来决定如何给网格染色
创建材质:
var basicMaterial=new THREE.MeshBasicMaterial({ color: 0x666666 });
var lambertMaterial=new THREE.MeshLambertMaterial({ color: 0x666666 });
var phongMaterial=new THREE.MeshPhongMaterial({ color: 0x666666 });
var wireMaterial=new THREE.MeshBasicMaterial({ wireframe: true, color: 0x666666 });
复制代码
更多材质和相关信息,可以查看 材质
需要添加到场景中,还需要依赖 Mesh。Mesh 是用来定义材质和几何体之间是如何粘合的,创建网格对象可以应用一个或多个材质和几何体。
创建几何体相同材质不同的网格对象:
var cube=new THREE.Mesh(cubeGeometry, basicMaterial);
var cubePhong=new THREE.Mesh(cubeGeometry, phongMaterial);
scene.add(cube, cubePhong);
复制代码
创建材质相同几何体不同的网格对象:
var cube=new THREE.Mesh(cubeGeometry, basicMaterial);
var sphere=new THREE.Mesh(sphereGeometry, basicMaterial);
scene.add(cube, sphere);
复制代码
创建拥有多个材质几何体的网格对象:
var phongMaterial=new THREE.MeshPhongMaterial({ color: 0x666666 });
var cubeMeshPhong=new THREE.Mesh(cubeGeometry, cubePhongMaterial);
var cubeMeshWire=new THREE.Mesh(cubeGeometry, wireMaterial);
// 网格对象新增材质
cubeMeshPhong.add(cubeMeshWire);
scene.add(cubeMeshPhong);
复制代码
有了场景和相机,我们还需要渲染器把对应的场景用对应的相机可见渲染出来,因此渲染器需要传入场景和相机参数。
// 抗锯齿、canvas 是否支持 alpha 透明度、preserveDrawingBuffer 是否保存 BUFFER 直到手动清除
const renderer=new THREE.WebGLRenderer({
antialias: true, alpha: true, preserveDrawingBuffer: true
});
renderer.setSize(this.width, this.height);
renderer.autoClear=true;
// 清除颜色,第二个参数为 0 表示完全透明,适用于需要透出背景的场景
renderer.setClearColor(0x000000, 0);
renderer.setPixelRatio(window.devicePixelRatio);
复制代码
为了在相机更新后所看见的场景,需要在循环渲染中加上
renderer.render(scene, camera);
复制代码
有了相机场景和渲染器,我们已经可以看到初步的效果了。但3D世界里,静止的物体多无趣啊。于是我们尝试加入动画效果。
Three为动画提供了一系列方法。
参数 含义 AnimationMixer 作为特定对象的动画混合器,可以管理该对象的所有动画 AnimationAction 为播放器指定对应的片段存储一系列行为,用来指定动画快慢,循环类型等 AnimationClip 表示可重用的动画行为片段,用来指定一个动画的动画效果(放大缩小、上下移动等) KeyframeTrack 与时间相关的帧序列,传入时间和值,应用在指定对象的属性上。目前有 BooleanKeyframeTrack VectorKeyframeTrack 等。
那么如何创建一个动画呢?下面这个例子给大家解释如何让网格对象进行简单的上下移动。
创建特定对象的动画混合器:
// 创建纹理
const texture=new THREE.TextureLoader().load(img.src);
// 使用纹理创建贴图
const material=new THREE.SpriteMaterial({ map: texture, color: 0x666666 });
// 使用贴图创建贴图对象
const stone=new THREE.Sprite(material);
// 为贴图对象创建动画混合器
const mixer=new THREE.AnimationMixer(stone);
复制代码
创建动画行为片段:
const getClip=(pos=[0, 0, 0])=> {
const [x, y, z]=pos;
const times=[0, 1]; // 关键帧时间数组,离散的时间点序列
const values=[x, y, z, x, y + 3, z]; // 与时间点对应的值组成的数组
// 创建位置关键帧对象:0时刻对应位置0, 0, 0 10时刻对应位置150, 0, 0
const posTrack=new THREE.VectorKeyframeTrack('stone.position', times, values);
const duration=1;
return new THREE.AnimationClip('stonePosClip', duration, [posTrack]);
};
复制代码
创建动画播放器,确定动画的表现:
const action=mixer.clipAction(getClip([x, y, z]));
action.timeScale=1; // 动画播放一个周期的时间
action.loop=THREE.LoopPingPong; // 动画循环类型
action.play(); // 播放
复制代码
在循环绘制中更新混合器,保证动画的执行:
animate() {
// 更新动画
const delta=this.clock.getDelta();
mixer.update(delta);
requestAnimationFrame(()=> {
animate();
});
}
复制代码
codepen
有了 Animation 我们可以很简单地对物体的一些属性进行操作。但一些贴图相关的动画就很难用 Animation 来实现了,比如:
上图这种,无法通过改变物体的位置、大小等属性实现。于是,还有一种方案 —— 贴图动画。
类似在 CSS3 中对序列图片使用 transform 属性改变位置来达到的动画效果,实际上在 Three 中也可以使用贴图位移的方式实现。
首先,我们要有一个序列图:
作为纹理加载,并且增加到场景中:
const arrowTexture=new THREE.TextureLoader().load(Arrow);
const material=new THREE.SpriteMaterial({ map: arrowTexture, color: 0xffffff });
const arrow=new THREE.Sprite(material);
scene.add(arrow);
复制代码
声明 TextAnimator 对象,实现纹理的位移:
function TextureAnimator(texture, tilesHoriz, tilesVert, numTiles, tileDispDuration) {
// 纹理对象通过引用传入,之后可以直接使用update方法更新纹理位置
this.tilesHorizontal=tilesHoriz;
this.tilesVertical=tilesVert;
// 序列图中的帧数
this.numberOfTiles=numTiles;
texture.wrapS=THREE.RepeatWrapping;
texture.wrapT=THREE.RepeatWrapping;
texture.repeat.set(1 / this.tilesHorizontal, 1 / this.tilesVertical);
// 每一帧停留时长
this.tileDisplayDuration=tileDispDuration;
// 当前帧停留时长
this.currentDisplayTime=0;
// 当前帧
this.currentTile=0;
// 更新函数,通过这个函数对纹理位移进行更新
this.update=(milliSec)=> {
this.currentDisplayTime +=milliSec;
while (this.currentDisplayTime > this.tileDisplayDuration) {
this.currentDisplayTime -=this.tileDisplayDuration;
this.currentTile++;
if (this.currentTile===this.numberOfTiles) { this.currentTile=0; }
const currentColumn=this.currentTile % this.tilesHorizontal;
texture.offset.x=currentColumn / this.tilesHorizontal;
const currentRow=Math.floor(this.currentTile / this.tilesHorizontal);
texture.offset.y=currentRow / this.tilesVertical;
}
};
}
复制代码
// 传入一个一行里有 13 帧的序列图,每张序列图停留 75ms
const arrowAni=new TextureAnimator(arrowTexture, 13, 1, 13, 75);
复制代码
在循环绘制中更新,保证动画的执行:
arrowAni.update(delta);
复制代码
作为引用传入后,对贴图的修改会直接体现在使用该贴图的材质上。
codepen
Three 中还提供了酷炫的粒子动画,使用继承自 Object3D 的 Points 类实现。有了 Points 类我们可以很方便地把一个几何体渲染成一组粒子,并对它们进行控制。
创建粒子我们首先需要创建粒子的材质,可以使用 PointsMaterial 创建粒子材质。
const texture=new THREE.TextureLoader().load('https://p1.music.126.net/jgzbZtWZhDet2jWzED8BTw==/109951164579600342.png');
material=new THREE.PointsMaterial({
color: 0xffffff,
// 映射到材质上的贴图
map: texture,
size: 2,
// 粒子的大小是否和其与摄像机的距离有关,默认值 true
sizeAttenuation: true,
});
// 开启透明度测试,透明度低于0.5的片段会被丢弃,解决贴图边缘感问题
material.alphaTest=0.5;
复制代码
有了粒子材质后,我们可以应用同一个材质批量创建一组粒子,只需要传入一个简单的几何体。
var particles=new THREE.Points( geometry, material );
复制代码
如果你传入的是 BoxGeometry 你可能会得到这样的一组粒子
还可以根据传入的 Shape 得到这样一组粒子
但有趣的粒子绝不是静止的,而是有活动、有过程的。但如果自己动手实现一个粒子的运动又很复杂,因此希望借助一些第三方库实现粒子动画的缓动过程。
tween.js 是一个小型的 JS 库,我们可以使用它为我们的动画声明变化。使用 tween.js 我们不需要关心运动的中间状态,只需要关注粒子的:
// srcPosition, targetPosition;
tweens.push(new TWEEN.Tween(srcPosition).easing(TWEEN.Easing.Exponential.In));
// tweens最终位置、缓动时间
tweens[0].to(targetPosition, 5000);
tweens[0].start();、
复制代码
codepen
其实粒子动画的场景还有很多,我们可以用他们创造雪花飘散、穿梭效果,本质都是粒子的位置变化。
相机在 3D 空间中充当人的眼睛,因此自然的相机动线可以保证交互的自然流畅。
Three 提供了一系列相机控件来控制场景中的相机轨迹,这些控件适用于大部分场景。使用 Controls 开发者可以不再需要去关心用户交互和相机移动的问题。
活动中也涉及到 OrbitControls 的使用,他提供了环绕物体旋转、平移和缩放的方法,但由于对使用二维贴图的情况下,旋转和缩放都容易穿帮,需要被禁止。
// 创建轨迹
const controls=new THREE.OrbitControls(this.camera, this.renderer.domElement);
controls.enabled=!0;
controls.target=new THREE.Vector3();
controls.minDistance=0;
controls.maxDistance=2000;
controls.minPolarAngle=Math.PI / 2;
controls.maxPolarAngle=Math.PI / 2;
// 禁用缩放
controls.enableZoom=!1;
// 禁用旋转
controls.enableRotate !1;
controls.panSpeed=2;
// 修改控件的默认触摸选项,设置为单指双指都为平移操作
controls.touches={
ONE: THREE.TOUCH.PAN,
TWO: THREE.TOUCH.PAN,
};
this.scene.add(this.camera);
复制代码
OrbitControl 还允许我们设置阻尼,设置该值表现为数值越接近 1 越难拖动,开启阻尼后需要我们手动 update 控件。
controls.enableDamping=!0;
controls.dampingFactor=0.2;
复制代码
查看源码可以看到,阻尼的实现就是依赖滑动时的 offset 乘上一个权重,在通过后续的update不断为 panOffset 乘上一个权重实现滑动难,撒手后再滑动一点距离。
// this method is exposed, but perhaps it would be better if we can make it private...
this.update=function () {
// ...
return function update() {
// ...
// 平移
if ( scope.enableDamping===true ) {
// 开启阻尼后会在原本的位移上乘上一个权重
scope.target.addScaledVector( panOffset, scope.dampingFactor );
} else {
scope.target.add( panOffset );
}
// ...
if ( scope.enableDamping===true ) {
sphericalDelta.theta *=( 1 - scope.dampingFactor );
sphericalDelta.phi *=( 1 - scope.dampingFactor );
// 如果没有人为操作,随着时间推移,panOffset会越来越小
panOffset.multiplyScalar( 1 - scope.dampingFactor );
} else {
sphericalDelta.set( 0, 0, 0 );
panOffset.set( 0, 0, 0 );
}
// ...
};
}();
复制代码
官方也提供了 Controls 的 例子 供大家参考。
如果不使用 Controls,仅仅是相机从一个点移动到另一个点,为了更平滑自然的相机轨迹,推荐使用贝塞尔曲线。
贝塞尔曲线是一个由起点、终点和控制点决定的一条时间相关的变化曲线。这里以二阶贝塞尔曲线为例,实现相机的曲线移动。(三维的点有点难说明白,这里用二维坐标来解释)
上图中小黑点的移动轨迹可以看做相机移动的曲线。
从该公式来看,只需要确定 p0、p1 和 p2 三个点,在单位时间下我们可以获得一条确定的曲线。
但是,换成坐标点要怎么做呢?
// 获得贝塞尔曲线
function getBezier(p1, p2) {
// 在指定范围内随机生成一个控制点
const cp={
x: p1.x + Math.random() * 100 + 200,
z: p2.z + Math.random() * 200,
};
let t=0;
// 贝塞尔曲线公式,根据时间确定点的位置
return (deltat)=> {
if (t >=1) return [p2.x, p2.y];
t +=deltat;
if (t > 1) t=1;
const { x: x1, z: z1 }=p1;
const { x: cx, z: cz }=cp;
const { x: x2, z: z2 }=p2;
const x=(1 - t) * (1 - t) * x1 + 2 * t * (1 - t) * cx + t * t * x2;
const z=(1 - t) * (1 - t) * z1
+ 2 * t * (1 - t) * cz + t * t * z2;
return [x, z];
};
}
复制代码
const bezier=getBezier(p1, p2);
复制代码
为了从简,这里只实现了二维坐标的轨迹变化,但三维也是同理。
因为贝塞尔曲线是时间相关曲线,在每一次循环渲染中要传入时间来更新相机位置。
animation() {
const [x, z]=bezier(clock.getDelta());
camera.position.x=x;
camera.position.z=z;
requestAnimationFrame(()=> {
animate();
});
}
复制代码
没赶上 Three 的热潮,只能趁着活动需求给自己补补课了。在三维空间中,动画能够让空间中的物体更加生动,而相机的移动带给用户更强的空间感。
本文介绍了基于 Animation 实现物体的简单运动、 Texture 实现贴图动画以及使用 Points 粒子化的物体动画方案;基于 Controls 和贝塞尔曲线的相机动画方案。
对 Three 有兴趣的朋友,可以通过 官方文档 来学习,里面提供的例子覆盖了大部分场景。
以上是我在活动中涉及到的一些动画方案,难免会出现理解偏差和表达错误,如果有更多的动效方案欢迎一起探讨~
家好,很高兴又见面了,我是"高级前端?进阶?",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力。
今天给大家带来的主题是HTML5 和word的互相转化,话不多说,直接进入正题!
html-docx-js 是一个非常小的库,能够将 HTML 文档转换为 Microsoft Word 2007 及更高版本使用的 DOCX 格式。 html-docx-js 设法使用称为“altchunks”的功能在浏览器中执行转换。 简而言之,它允许以不同的标记语言嵌入内容。 开发者使用 MHT 文档将嵌入内容发送到 Word,因为它允许处理图像。 Word 打开此类文件后,会将外部内容转换为 Word Processing ML(这是 DOCX 文件的标记语言的调用方式)并替换引用。
Microsoft Word for Mac 2008 不支持 Altchunk,LibreOffice 和 Google Docs 也不支持 Altchunk。
关于 html-docx-js 库有几点需要说明:
目前 Mammoth 在 Github 上通过 MIT 协议开源,有超过 1k 的 star、0.3k 的 fork、0.7k 的项目依赖量、NPM 周平均下载量 9k,是一个值得关注的前端开源项目。
var converted=htmlDocx.asBlob(content);
saveAs(converted, "test.docx");
asBlob 可以采用其他选项来控制文档的页面设置:
比如下面的例子:
var converted=htmlDocx.asBlob(content, {
orientation: "landscape",
margins: { top: 720 },
});
saveAs(converted, "test.docx");
需要注意的是,开发者需要传递完整、有效的 HTML(包括 DOCTYPE、html 和 body 标签)。 这可能不太方便,但可以让开发者在样式标签中包含 CSS 规则。
html-docx-js 作为独立”Browserify 模块(UMD)分发。 开发者可以将其作为 html-docx 要求。 如果没有可用的模块加载器,它将把自己注册在 window.htmlDocx。
Mammoth.js 旨在转换 .docx 文档,例如:由 Microsoft Word、Google Docs 和 LibreOffice 创建的文档,并将其转换为 HTML。 Mammoth 的目标是通过使用文档中的语义信息并忽略其他细节来生成简单且干净的 HTML。 例如,Mammoth 将任何具有标题 1 样式的段落转换为 h1 元素,而不是尝试精确复制标题的样式(字体、文本大小、颜色等)。
.docx 使用的结构与 HTML 的结构之间存在很大的不匹配,这意味着对于更复杂的文档来说,转换不太可能完美。 如果开发者仅使用样式来对文档进行语义标记,那么 Mammoth 效果最佳。
Mammoth.js 目前支持以下功能:
Mammoth 在众多平台可用,比如:Python、WordPress、Java/JVM、.NET 等等。目前 Mammoth 在 Github 上通过 BSD-2-Clause 开源,有超过 4.1k 的 star、0.5k 的 fork、4.4k 的项目依赖量、NPM 周平均下载量 76k,是一个值得关注的前端优质开源项目。
以文档转换为例。
Mammoth 允许在转换文档之前对其进行处理。 例如,假设该文档尚未进行语义标记,但开发者知道任何居中对齐的段落都应该是标题,则可以使用 transformDocument 参数来适当地修改文档:
function transformElement(element) {
if (element.children) {
var children=_.map(element.children, transformElement);
element={ ...element, children: children };
}
if (element.type==="paragraph") {
element=transformParagraph(element);
}
return element;
}
function transformParagraph(element) {
if (element.alignment==="center" && !element.styleId) {
return { ...element, styleId: "Heading2" };
} else {
return element;
}
}
var options={
transformDocument: transformElement,
};
TransformDocument 的返回值在 HTML 生成期间使用。同时,上面的代码可以使用 mammoth.transforms.paragraph 函数进行优化,比如:
function transformParagraph(element) {
if (element.alignment==="center" && !element.styleId) {
return { ...element, styleId: "Heading2" };
} else {
return element;
}
}
var options={
transformDocument: mammoth.transforms.paragraph(transformParagraph),
};
或者,如果开发者希望已明确设置为使用等宽字体来表示代码的段落:
const monospaceFonts=["consolas", "courier", "courier new"];
function transformParagraph(paragraph) {
var runs=mammoth.transforms.getDescendantsOfType(paragraph, "run");
var isMatch=runs.length > 0 &&
runs.every(function (run) {
return run.font && monospaceFonts.indexOf(run.font.toLowerCase()) !==-1;
});
if (isMatch) {
return {
...paragraph,
styleId: "code",
styleName: "Code",
};
} else {
return paragraph;
}
}
var options={
transformDocument: mammoth.transforms.paragraph(transformParagraph),
styleMap: ["p[style-name='Code']=> pre:separator('\n')"],
};
关于 Mammoth 库的更多用法,更多 API 示例可以参考文末资料,本文不再过多展开。
本文主要和大家介绍 HTML5 和word互转的两个开源库,分别为:html-docx-js、mammoth.js。相信通过本文的阅读,大家对 html-docx-js、mammoth.js 会有一个初步的了解。
因为篇幅有限,关于 html-docx-js、mammoth.js 的更多用法和特性文章并没有过多展开,如果有兴趣,可以在我的主页继续阅读,同时文末的参考资料提供了大量优秀文档以供学习。最后,欢迎大家点赞、评论、转发、收藏,您的支持是我不断创作的动力。
https://github.com/evidenceprime/html-docx-js
https://github.com/mwilliamson/mammoth.js
https://www.npmjs.com/package/html-docx-js
https://www.npmjs.com/package/mammoth
https://www.tutorialswebsite.com/export-html-to-word-document-with-javascript/
https://www.vecteezy.com/vector-art/136754-free-vector-documents
击右上方红色按钮关注“web秀”,让你真正秀起来
在css3到来之前,都是用js来操作dom元素,计算位置,大小,形成瀑布流布局。但是有了css3之后,一切实现起来就太简单了,没有复杂的逻辑,轻松的几行样式代码就可以搞定。
基于waterfall.js(11.8kb),还得写入基础的样式,初始化等等,对比其他js,已经是很简单了。
var waterfall=new WaterFall({ container: '#waterfall', pins: ".pin", loader: '#loader', gapHeight: 20, gapWidth: 20, pinWidth: 216, threshold: 100 });
但是,有了css3,再简洁的js,都比不过简单的css代码。
关键点,横向 flex 布局嵌套多列纵向 flex 布局,使用了 vw 进行自适应缩放
// pug 模板引擎 div.g-container -for(var i=0; i<4; i++) div.g-queue -for(var j=0; j<8; j++) div.g-item
不熟悉pug模板(jade)的,可以先去了解一下。其实看大致也就懂了,就是循环多个元素,没有复杂的逻辑。
$lineCount: 4; $count: 8; // 随机数(瀑布流某块的高度) @function randomNum($max, $min: 0, $u: 1) { @return ($min + random($max)) * $u; } // 随机颜色值 @function randomColor() { @return rgb(randomNum(255), randomNum(255), randomNum(255)); } .g-container { display: flex; flex-direction: row; justify-content: space-between; overflow: hidden; } .g-queue { display: flex; flex-direction: column; flex-basis: 24%; } .g-item { position: relative; width: 100%; margin: 2.5% 0; } @for $i from 1 to $lineCount+1 { .g-queue:nth-child(#{$i}) { @for $j from 1 to $count+1 { .g-item:nth-child(#{$j}) { height: #{randomNum(300, 50)}px; background: randomColor(); // 瀑布流某块中间的数字 &::after { content: "#{$j}"; position: absolute; color: #fff; font-size: 24px; // 水平垂直居中 top: 50%; left: 50%; transform: translate(-50%, -50%); } } } } }
预览:
CSS 实现瀑布流布局(display: flex)
演示地址: 点击文章结尾“了解更多”
关键点, column-count: 元素内容将被划分的最佳列数 break-inside: 避免在元素内部插入分页符
// pug 模板引擎 div.g-container -for(var j=0; j<32; j++) div.g-item
column-count 看起来比display: flex更科学,模板不需要2层循环,更简洁明了。如果真正用到数据上面,会更好处理。
$count: 32; // 随机数(瀑布流某块的高度) @function randomNum($max, $min: 0, $u: 1) { @return ($min + random($max)) * $u; } // 随机颜色值 @function randomColor() { @return rgb(randomNum(255), randomNum(255), randomNum(255)); } .g-container { column-count: 4; column-gap: .5vw; padding-top: .5vw; } .g-item { position: relative; width: 24vw; margin-bottom: 1vw; break-inside: avoid; } @for $i from 1 to $count+1 { .g-item:nth-child(#{$i}) { height: #{randomNum(300, 50)}px; background: randomColor(); &::after { content: "#{$i}"; position: absolute; color: #fff; font-size: 2vw; top: 50%; left: 50%; transform: translate(-50%, -50%); } } }
预览:
CSS 实现瀑布流布局(column-count)
演示地址: 点击文章结尾“了解更多”
关键点, 使用 grid-template-columns、grid-template-rows 分割行列 使用 grid-row 控制每个 item 的所占格子的大小
// pug 模板引擎 div.g-container -for(var i=0; i<8; i++) div.g-item
样式
$count: 8; // 随机数(瀑布流某块的高度) @function randomNum($max, $min: 0, $u: 1) { @return ($min + random($max)) * $u; } // 随机颜色值 @function randomColor() { @return rgb(randomNum(255), randomNum(255), randomNum(255)); } .g-container { height: 100vh; display: grid; grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(8, 1fr); } @for $i from 1 to $count+1 { .g-item:nth-child(#{$i}) { position: relative; background: randomColor(); margin: 0.5vw; &::after { content: "#{$i}"; position: absolute; color: #fff; font-size: 2vw; top: 50%; left: 50%; transform: translate(-50%, -50%); } } } .g-item { &:nth-child(1) { grid-column: 1; grid-row: 1 / 3; } &:nth-child(2) { grid-column: 2; grid-row: 1 / 4; } &:nth-child(3) { grid-column: 3; grid-row: 1 / 5; } &:nth-child(4) { grid-column: 4; grid-row: 1 / 6; } &:nth-child(5) { grid-column: 1; grid-row: 3 / 9; } &:nth-child(6) { grid-column: 2; grid-row: 4 / 9; } &:nth-child(7) { grid-column: 3; grid-row: 5 / 9; } &:nth-child(8) { grid-column: 4; grid-row: 6 / 9; } }
display: grid样式上面感觉也不好用,需要书写很多grid-column、grid-row。
预览:
CSS 实现瀑布流布局(display: grid)
演示地址: 点击文章结尾“了解更多”
通过,这3种CSS瀑布流布局,你更喜欢哪一种呢?
个人更喜欢column-count,看起来更加清晰,容易理解,代码量也很少。
喜欢小编的点击关注,了解更多知识!
源码地址请点击下方“了解更多”
*请认真填写需求信息,我们会在24小时内与您取得联系。