C语言期中考试浪漫爱心代码,前端也来凑凑热闹
前言
大家好啊,我是小包,前段时间想缓缓节奏,没想到一放松就是这么久,想死大家了,回归回归,开启开卷旅途!虽说天天偷懒,闲暇偷学了一些 Threejs
,最近的文章应该都会围绕着 Threejs
来展开。
本篇算是 Threejs 专栏的开篇之作,但小包这次不想上来就讲大道理,教如何使用 Threejs
,而是准备先以一个浪漫的案例让大家初步体验一下 2D 整活与 3D 整活的差别。
最近程序员有点火,火的有点突然,《点燃我,温暖你》一剧中程序员李峋给女朋友送了一个超级炫酷的跳动爱心,这个被称为"李峋浪漫爱心"的片段瞬间出现在了好多地方,包括我的家庭群 😂
喃,它就这个样子,讲实话,的确有点炫酷。虽然小包很菜,但浪漫这方面程序员不能认输,就这么愉快的决定了,本文的就围绕李峋浪漫爱心和 3D 爱心两个案例来体验一下不同的开发思路和模式。
李峋浪漫爱心代码实现
先来看效果(颜色使用 cyan 天青色):
爱心收缩并没有视频中的那么有力,小包没能找到好的数学函数来实现爱心的律动感,我有罪。
实现分析
咱们首先来分析一下实现要点:
- 爱心是核心组成,可以初步看做多层爱心: 外层爱心、爱心轮廓、内层爱心等
- 使用粒子组成爱心
- 炫酷的动画,这部分是难点
爱心实现
在前端中,你问如何绘制一个图像,那 canvas
不逞多让,其提供的 lineTo
和 stroke
方法可以绘制出你想要的任何图形。
那么难点来了,lineTo 连线点要如何确定,也就是爱心各个位置的点如何得出。遇事不决找数学。
高等数学中,极坐标系中有几个超级经典的曲线,例如浪漫的心形线、阿基米德螺线等。小包沿着这个思路就开始查度娘,竟然找到了这么多爱心的绘制方式,数学家的浪漫(傲娇的大拇指 👍)。
六种爱心各有各的不同,爱心一很可爱,爱心六更动感一些,顺从内心,选择爱心六。(当然我不会说重要原因之一是参数方程的模式容易绘制)
Step1: 根据方程编写出爱心点的生成函数
// scale 为放大倍数
// width height 为画布大小,将生成的爱心移到画布中央
function generatorHeart(t, scale = 11.6) {
let x = 16 * Math.sin(t) ** 3;
let y = -(
13 * Math.cos(t) -
5 * Math.cos(2 * t) -
2 * Math.cos(3 * t) -
Math.cos(4 * t)
);
x = x * scale + width / 2;
y = y * scale + height / 2;
return new Point(x, y);
}
Step2: 生成一圈爱心点
t
的取值范围为 [0,2Π]
,循环生成 360
个点。
const hearts = [];
for (let i = 0; i < 360; i++) {
hearts.push(generatorHeart(2 * Math.PI * (i / 360)));
}
Step3: 绘制爱心
遍历每个点,使用 canvas
连成线。
function drawHeart2(context, points) {
context.beginPath();
points.forEach((point) => {
context.strokeStyle = "#00ffff";
context.lineTo(point.x, point.y);
context.stroke();
});
context.closePath();
}
等等,我们要的是粒子爱心,应该怎样由连线爱心转化为粒子爱心那?
Step4: 粒子爱心
canvas
提供了 arc
方法可以绘制圆或者弧, fill
可以实现填充效果,因此可以借助 arc
方法生成多个很小的圆,使用 fill
填充,形成粒子效果。
function drawHeart(context, points) {
points.forEach((point) => {
context.beginPath();
context.fillStyle = "#00ffff";
context.arc(point.x, point.y, point.size, 0, Math.PI * 2);
context.fill();
context.closePath();
});
}
整体结构实现
这部分开始咱们首先来梳理一下整体代码,丰满一下整体的逻辑,再继续制造浪漫。
Step1: 定义 Point 类
Point
为每个粒子的基类,共需要 x,y,size
三个属性。
class Point {
constructor(x, y, size) {
this.x = x;
this.y = y;
this.size = size;
}
}
Step2: 定义 Heart 类,将与爱心相关的代码都集成到类中
这里小包先抛出几个问题,咱们一起来思考 Heart
类应该有什么?
- 如何实现爱心动起来的效果?
这里借助了帧动画的思想,先提前计算出各帧中粒子的对应位置,通过 window.requestAnimationFrame
的回调函数每次重新刷新画布,绘制对应帧的图像。
- 爱心边缘的粒子可以通过公式生成,那爱心内部和爱心外部的粒子如何生成?
说实话最开始小包没有想到解决办法,既可以保证粒子的随机分布,还能保证粒子的尽可能均匀,这看起来是个令人挠头的数学问题。在这个危难之间,码农高天大佬给出了他的解决方案,原版代码是用 python
实现的,小包就借鉴了大佬实现方案中的核心公式,将其转换为前端版本。
- 内层粒子使用
-log(x)
函数生成,-log(x)
在(0,1]
函数曲线陡峭,能更好的保证内层粒子的随机性。 - 帧动画粒子位置通过
sin(x)
函数计算得来,sin(x)
是周期函数,整体相对平稳,爱心的动效比较平稳,不突兀。(如果想实现更炫酷的动效,需要修改计算函数,例如贝塞尔曲线等)
有了上面的分析,Heart
类所具备的东西就有初步头绪了。
- 各层爱心粒子及其初始化生成函数
- 帧动画各帧中粒子位置计算函数
- 爱心外层的粒子根据各帧生成,爱心边缘和内部的粒子根据公式计算每帧的位置
// 代码为简化版
class Heart {
constructor(particles, generateFrame) {
this.particles = particles;
this.generateFrame = generateFrame; // 帧数量
this.boardHeart = []; // 爱心的轮廓
this.middleHeart = []; // 爱心轮廓内部
this.centerHeart = []; // 爱心中间
this.allHearts = []; // 所有的爱心粒子
this.initHeart(); // 初始化爱心内部粒子
for (let i = 0; i < generateFrame; i++) {
this.calcFrame(i); // 计算 20 帧图像
}
}
initHeart() {
// 爱心边缘粒子
for (let i = 0; i < this.particles; i++) {
const deg = (2 * Math.PI * rand(0, 360)) / 360;
this.boardHeart.push(generatorHeart(deg));
}
this.boardHeart.forEach((point) => {
for (let j = 0; j < 3; j++) {
this.middleHeart.push(
// 根据公式生成内部粒子
scatterHeart(point, 0.05 * rand(j, j + 1, false))
);
}
});
// 根据类似规则继续生成几层内部粒子
}
calc_position(point, ratio) {
// 计算对应粒子在每个帧对应的位置
const force =
1 / ((point.x - width / 2) ** 2 + (point.y - height / 2) ** 2) ** 0.52;
const dx = ratio * force * (point.x - width / 2) + rand(-1, 1, false);
const dy = ratio * force * (point.y - height / 2) + rand(-1, 1, false);
return new Point(point.x - dx, point.y - dy, 0);
}
calcFrame(frame) {
// 计算每帧对应的粒子的位置
const ratio = 20 * curve((frame / 18) * Math.PI);
const haloRadius = Math.floor(4 + 6 * (1 + curve((frame / 18) * Math.PI)));
const haloNums = Math.floor(
this.particles * 2 +
this.particles * 3 * Math.abs(curve((frame / 18) * Math.PI) ** 2)
);
const nowFramePoints = [];
const haloSet = new Set();
for (let i = 0; i < haloNums; i++) {
const delta = rand(0, 360);
let point = generatorHeart(2 * Math.PI * (delta / 360));
let shrinkPoint = shrinkHeart(point, haloRadius);
shrinkPoint.x += rand(-20, 20);
shrinkPoint.y += rand(-20, 20);
shrinkPoint.size = rand(0.4, 1.4, false);
nowFramePoints.push(shrinkPoint);
}
this.boardHeart.forEach((point) => {
const calcPoint = this.calc_position(point, ratio);
calcPoint.size = rand(0.6, 1.6, false);
nowFramePoints.push(calcPoint);
});
this.middleHeart.forEach((point) => {
const calcPoint = this.calc_position(point, ratio);
calcPoint.size = rand(0.4, 1.4, false);
nowFramePoints.push(calcPoint);
});
this.centerHeart.forEach((point) => {
const calcPoint = this.calc_position(point, ratio);
calcPoint.size = rand(0.4, 1.4, false);
nowFramePoints.push(calcPoint);
});
this.allHearts[frame] = nowFramePoints;
}
}
爱心跳动——收官
各帧的粒子位置计算完毕后,利用 requestAnimationFrame
和 canvas
实现爱心跳动效果。
// 根据位置和大小绘制全部粒子
function drawHeart(context, points) {
points.forEach((point) => {
context.beginPath();
context.fillStyle = "#00ffff";
context.arc(point.x, point.y, point.size, 0, Math.PI * 2);
context.fill();
context.closePath();
});
}
// 帧动画切换
function render(k) {
heartCtx.clearRect(0, 0, width, height);
drawHeart(heartCtx, heartCls.allHearts[k]);
}
let k = 0;
(function animateloop() {
k++;
k = k % 80;
if (k % 4 === 0) {
render(k / 4);
}
requestAnimationFrame(animateloop);
})();
3D 爱心
李峋浪漫爱心的代码实现起来还是有几分麻烦的,下面咱们来初步体验一下简单的 3D 开发。本文小包不会细讲各种概念,咱们就一起顺着走一遍,体验一下 Threejs
的强大和魅力。
基础配置
本部分代码基于 Vite + Vue3 开发
Step1: 设置 scene,通俗来讲就是给个展示的地,划个地盘
const scene = new THREE.Scene();
Step2: 添加 camera,地盘搭好了,得有个看客,camera 就是定义这个看客欣赏的方式
const camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.01,
10000
);
camera.position.set(0, 0, 300);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
scene.add(camera);
Step3: 设置 renderer,决定将场地中的何种东西以何种样式展示给看客看
const renderer = new THREE.WebGLRenderer({
antialias: true, // 抗锯齿
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.setPixelRatio(window.devicePixelRatio);
Step4: 添加 light,打个光,万事具备了,得来个炫酷的光感,设置一些来自四面八方的光照
const light1 = new THREE.DirectionalLight(0x333333, 1);
light1.position.set(0, 0, 20);
scene.add(light1);
const light2 = new THREE.DirectionalLight(0x333333, 1);
light2.position.set(0, 0, -10);
scene.add(light2);
const light3 = new THREE.DirectionalLight(0x333333, 1);
light3.position.set(20, 0, 0);
scene.add(light3);
const light4 = new THREE.DirectionalLight(0x333333, 1);
light4.position.set(-20, 0, 0);
scene.add(light4);
const light5 = new THREE.DirectionalLight(0x333333, 1);
light5.position.set(0, 10, 0);
scene.add(light5);
const light6 = new THREE.DirectionalLight(0x333333, 0.3);
light6.position.set(5, 20, 0);
scene.add(light6);
const light7 = new THREE.DirectionalLight(0xeeeeee, 0.3);
light7.position.set(0, 20, 5);
scene.add(light7);
const light8 = new THREE.DirectionalLight(0x333333, 0.3);
light8.position.set(0, 20, -5);
scene.add(light8);
const light9 = new THREE.DirectionalLight(0x333333, 0.3);
light9.position.set(-5, 10, 0);
scene.add(light9);
const light10 = new THREE.AmbientLight(0x333333, 1);
light10.position.set(0, 100, 100);
scene.add(light10);
Step5: 一切硬件条件都具备了,引入模型爱心,模型哪里来的,先保密一下
onMounted(() => {
canvasDom.value.appendChild(renderer.domElement);
renderer.setClearColor("#000");
scene.background = new THREE.Color("#C0C0C0");
scene.environment = new THREE.Color("#C0C0C0");
render();
// 引入 loader
const loader = new GLTFLoader();
loader.setMeshoptDecoder(MeshoptDecoder);
loader.load("/model/heart.glb", (gltf) => {
const heart = gltf.scene;
scene.add(heart);
});
});
忽略小包丑陋可怜的配色,配色真的是小包的世界难题。
Step6: 给模型添加几个简单动效
这里使用 gsap
动画库给爱心添加一个跳跃动画和旋转动画,让爱心"活"起来。
gsap.to(heart.rotation, {
y: Math.PI * 2,
duration: 6,
repeat: -1,
});
gsap.to(heart.position, {
y: 0.8,
duration: 1,
yoyo: true,
repeat: -1,
});
一个简单的 3D 爱心效果就轻松的实现了,如果你想了解更多 Threejs
的知识和案例,务必关注小包的专栏: Three 从入门到实战
源码仓库
总结
李峋浪漫爱心和 3D
爱心都已经成功实现了,下面一起来复盘一下。这两个案例并不算是平衡的比较,小包这里只是略作对比,作为 Threejs
的一个学习导引。
李峋浪漫爱心 | 3D 爱心 | |
---|---|---|
实现难度 | 较大,还涉及部分公式 | 较易 |
炫酷程度 | 炫酷 | 一般(这个案例比较简单,3D世界还是特别炫酷的) |
繁琐之处 | 公式、canvas绘制 | 如何找到合适的模型,如何打光等 |
适应性 | 根据自己的想法可以天马行空的创造 | 一定程度上受模型的约束 |
本文小包分别带大家实现了李峋浪漫爱心和 3D
爱心,李峋浪漫爱心实现起来非常炫酷,但实现难度偏高;3D 爱心的难度就相对低很多,只要你能找到炫酷的 3D 模型,就可以实现一个不错的效果,相对的 3D 效果受模型的限制性也较大,但谁又能拒绝这满屏幕的立体炫酷感那?
接下来跟着小包一起拥抱 Threejs
,一起去 3D
的世界遨游吧。
后语
我是 战场小包 ,一个快速成长中的小前端,希望可以和大家一起进步。
一路加油,冲向未来!!!
疫情早日结束 人间恢复太平
转载自:https://juejin.cn/post/7165655522170175501