预览
可以先在下面这里在线体验效果↓(请用PC浏览)
moayuisuda.github.io/moa-line/.
噪声
噪声概述
噪声在我的理解中是“连续的随机”,比如你输入noise(0.01), noise(0.02), noise(0.03)......参数很接近,那么生成的结果值虽然随机,但也同样会很接近。如果想深入了解噪声的原理,推荐这一篇 blog.csdn.net/candycat199…
想象一下连绵起伏的海面,相邻位置的高度是不是就符合这种“连续且随机”呢?
直观印象
这次用到的是三维噪声simplex3,你可以将它想象成一个装着-1到1区间内连续随机数的大立方体,我们输入一个立方体内的坐标(x, y, z)
,就可以取出一个随机值,我用three.js生成了一个立方体来直观展示噪声的特性↓
const span = 100; // 正方体长度
// 要想得到连续的随机值,传入的参数必须也相差很小,一般要在0.1这种数量级以下。
const scale = 100;
/* scale = 100代表一个three.js的单位长度映射为0.01(1/100)个噪声立方体单位长度。也就是说
我们在three.js中生成的正方体是100*100*100,但是仅仅映射了噪声立方体内1*1*1的情况。*/
for (let z = 0; z < span; z++) {
for (let y = 0; y < span; y++) {
for (let x = 0; x < span; x++) {
let point = new THREE.Vector3(x, y, z); // 每一个point都是组成立方体的一个点
// 比如当x,y,z为(4,5,5)时映射的噪声立方体坐标为(0.04, 0.05, 0.05)
// noise.simplex3()的范围-1到1,seed范围就是0到1
let seed = 0.5 * (noise.simplex3(x / scale, y / scale, z / scale) + 1);
// *color会直观反映seed,这里的三个参数范围就是0-1(1就相当于255),颜色越黑,值越小
let color = new THREE.Color(seed, seed, seed);
Geometry.vertices.push(point);
Geometry.colors.push(color);
}
}
}
改变scale为1000↓
let seed = 0.5 * (noise.simplex3(x / scale, y / scale, z / scale) + 1);
let color = new THREE.Color(seed, seed, seed); // 颜色越黑,值越小
可以看到颜色变化已经不明显了,因为scale
变大了10倍,上面这两步中相邻x, y, z之间x / scale, y / scale, z / scale
相差也变小了10倍,导致noise.simplex3(x / scale, y / scale, z / scale)
相差也很小,color
也就自然相差很小了。
开始正题
先来画圆吧
<!DOCTYPE html>
<html>
<head>
<style>
body {
margin: 0;
}
.main {
background: #000;
height: 100vh;
}
</style>
</head>
<body>
<div class="main"></div>
</body>
<script>
const wave = function({
dom, // 挂载在哪个dom上
span = 50, // 单个元素的大小
zIndex = -999 // canvas的z-index
}) {
// 将父dom变为层叠上下文
dom.style.position = "relative";
dom.style.zIndex = 0;
dom.style.overflow = "hidden";
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
// 因为是个“背景”,所以需要将z-index设置得很小,如果父元素不是层叠上下文,canvas将会直接被父元素遮盖住。
canvas.style.position = "absolute";
canvas.style.zIndex = zIndex;
canvas.height = parseInt(getComputedStyle(dom)["height"]);
canvas.width = parseInt(getComputedStyle(dom)["width"]);
dom.appendChild(canvas);
let r = span / 2; // 单个元素的半径
// 单个定位点
function Point({ cx, cy }) {
this.cx = cx;
this.cy = cy;
}
// 定位点的draw方法,用于生成形状,现在我们简单的只用arc来画圆
Point.prototype.draw = function() {
context.beginPath();
context.strokeStyle = "#ffffff";
context.arc(this.cx, this.cy, r, 0, 2 * Math.PI);
context.stroke();
};
// 初始化所有定位点,放在points数组
let points = [];
function initPoints() {
for (let y = 0; y < canvas.height; y += span) {
for (let x = 0; x < canvas.width; x += span) {
// 这里可能造成绘制的条形超过canvas边界,x < canvas.height即使在canvas的倒数第一行也成立,也会继续向下
points.push(
new Point({
cx: x + r,
cy: y + r
})
);
}
}
}
// 每一次requestAnimationFrame都调用的方法,重绘画布
function draw() {
context.clearRect(0, 0, canvas.width, canvas.height); // 清空画布
// 调用每一个point的draw方法
for (let i of points) {
i.draw();
}
}
let id; // 动画id
function animate() {
draw();
id = requestAnimationFrame(animate);
}
initPoints();
animate();
// 返回canvas与stop方法用于停止动画
return {
canvas,
stop() {
cancelAnimationFrame(id);
}
};
};
// 运行
wave({
dom: document.querySelector(".main")
});
</script>
</html>
运行后你应该会看到下面的样子↓
引入噪声
首先我们需要引入噪声库,请自行复制然后script引入 raw.githubusercontent.com/josephg/noi…
对于每个point,我们需要用它的cx与cy属性来映射到噪声立方体中的坐标,但是一个是平面一个是三维怎么映射?用三维切一个面不就是二维了,我们把noise.simplex3(x, y, z)
中的z
直接设置为0
,noise.simplex3(x, y, 0)
就变成了一个二维噪声。
先在函数的参数中多加一个scale,也就是前面说的与噪声立方体的映射比
const wave = function({
dom, // 挂载在哪个dom上
span = 50, // 单个元素的大小
scale = 1000, // ----------------- 前面说的与噪声立方体的映射比
zIndex = -999 // canvas的z-index
})
改造上面的Point.prototype.draw
Point.prototype.draw = function() {
context.beginPath();
let seed = 0.5 * (noise.simplex3(this.cx / scale, this.cy / scale, 0) + 1); // seed范围0-1
context.strokeStyle = `rgba(255, 255, 255, ${seed})`; // 以seed作为alpha值
context.arc(this.cx, this.cy, r, 0, 2 * Math.PI);
context.stroke();
}
重新运行你应该会看到下面这样的东西↓
圆圈的透明度变得“随机且连续”了。
让它动起来
在上面我们虽然写了animate()
函数,但是因为图形的属性并不会有改变,所以看起来是静止的。
还记得我们之前将噪声函数的第三个参数置零了吗↓
noise.simplex3(this.cx / scale, this.cy / scale, 0)
而现在,我们将它重新启用,用什么来作为它的值呢?时间。
将上面的let seed = 0.5 * (noise.simplex3(this.cx / scale, this.cy / scale, 0) + 1);
变为let seed = 0.5 * (noise.simplex3(this.cx / scale, this.cy / scale, time) + 1);
我们需要定义一个全局的变量time
,与一个控制时间快慢的参数speed
,并在animate中time += speed;
const wave = function({
dom, // 挂载在哪个dom上
span = 50, // 单个元素的大小
scale = 1000, // 前面说与噪声立方体的映射比
speed = 0.01, // -----------------speed参数
zIndex = -999 // canvas的z-index
})
let id; // 动画id
let time = 0; // -------------初始化time为0
function animate() {
draw();
time += speed;
id = requestAnimationFrame(animate);
}
然后你就会发现你的图动起来了↓
到这里所有的基本代码就已经完成了,接下来就是对Point.prototype.draw
的各种改造了,我们来实现一个线条动画
Point.prototype.draw = function() {
context.beginPath();
let s = noise.simplex3(this.cx / scale, this.cy / scale, time);
let sa = Math.abs(s); // 噪声的绝对值,用来生成alpha值与lineWidth
context.strokeStyle = `rgba(255, 255, 255, ${sa})`;
context.lineWidth = Math.abs(s) * 8;
let a = Math.PI * 2 * s; // 角度
let ap = Math.PI + a; // 角度 + 180度
context.moveTo(this.cx + Math.cos(a) * r, this.cy);
context.lineTo(this.cx + Math.cos(ap) * r, this.cy + Math.sin(ap) * r);
context.stroke();
};
context.moveTo(this.cx + Math.cos(a) * r, this.cy);
这一句可能会有人比较疑惑,为什么不用context.moveTo(this.cx + Math.cos(a) * r, this.cy + Math.sin(a));
,各位可以将前者换成后者后试验一下,效果会没趣不少,因为那就相当于只是从圆的一头连接到它的另一头了。
通过调整speed
、scale
、span
等参数就能获得很多有趣的效果↓
接下来发挥你的想象力去改造Point.prototype.draw
吧
此示例的完整代码,记得更改噪声库的src↓
<!DOCTYPE html>
<html>
<head>
<style>
body {
margin: 0;
}
.main {
background: #000;
height: 100vh;
}
</style>
</head>
<body>
<div class="main"></div>
</body>
<script src="./perlin.js"></script>
<script>
const wave = function({
dom,
span = 50,
scale = 1000,
speed = 0.01,
zIndex = -999
}) {
dom.style.position = "relative";
dom.style.zIndex = 0;
dom.style.overflow = "hidden";
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
canvas.style.position = "absolute";
canvas.style.zIndex = zIndex;
canvas.height = parseInt(getComputedStyle(dom)["height"]);
canvas.width = parseInt(getComputedStyle(dom)["width"]);
dom.appendChild(canvas);
let r = span / 2;
function Point({ cx, cy }) {
this.cx = cx;
this.cy = cy;
}
Point.prototype.draw = function() {
context.beginPath();
let s = noise.simplex3(this.cx / scale, this.cy / scale, time);
let sa = Math.abs(s);
context.strokeStyle = `rgba(255, 255, 255, ${sa})`;
context.lineWidth = Math.abs(s) * 8;
let a = Math.PI * 2 * s;
let ap = Math.PI + a;
context.moveTo(this.cx, this.cx);
context.lineTo(this.cx + Math.cos(ap) * r, this.cy + Math.sin(ap) * r);
context.stroke();
};
let points = [];
function initPoints() {
for (let y = 0; y < canvas.height; y += span) {
for (let x = 0; x < canvas.width; x += span) {
points.push(
new Point({
cx: x + r,
cy: y + r
})
);
}
}
}
function draw() {
context.clearRect(0, 0, canvas.width, canvas.height);
for (let i of points) {
i.draw();
}
}
let id;
let time = 0;
function animate() {
draw();
time += speed;
id = requestAnimationFrame(animate);
}
initPoints();
animate();
return {
canvas,
stop() {
cancelAnimationFrame(id);
}
};
};
wave({
dom: document.querySelector(".main"),
scale: 5000,
speed: 0.002,
span: 100
});
</script>
</html>
颜色渐变
接下来我们要实现点击一下dom颜色就变换一下,并且能自定义颜色转换的颜色数组,颜色转换的时间。首先来看一下新的参数:
const wave = function({
dom,
span = 50,
scale = 1000,
speed = 0.01,
zIndex = -999,
duration = 2000, // -------------- 颜色转换时间
colors = [
[212, 192, 255],
[192, 255, 244],
[255, 192, 203]
] // ------------- 颜色数组
})
渐变
首先我们要实现渐变,并且还需要能自定义渐变的时间,写一个通用的线性渐变函数↓
const copy = target => {
let re = [];
for (let i in target) {
re[i] = target[i];
}
return re;
};
const move = (
origin,
target,
duration,
after, // 每次数值更改后的回调
fn = pro => {
return Math.sqrt(pro, 2);
} // 缓动函数
) => {
if (fn(1) != 1) throw '[moaline-move] The fn must satisfy "fn (1) == 1"'; // 当参数为1时,对应的值也一定要为1
let st, sp;
st = performance.now(); // 保存开始时间
sp = copy(origin); // 保存源属性
let d = {}; // 源与目标之间每一项的距离
for (let i in origin) {
d[i] = target[i] - origin[i];
}
let frame = t => {
let pro = (t - st) / duration; // 当前进程
if (pro >= 1) {
return;
}
for (let i in origin) {
origin[i] = sp[i] + fn(pro) * d[i]; // fn(pro)得出当前时间对应的缓动函数的距离百分比,再乘以总距离
}
if(after) after(copy(origin), pro);
requestAnimationFrame(frame);
};
frame(st);
};
绑定事件
接着我们在dom上绑定一个事件,让它点击一下就能进行渐变。
let ci = 0;
const color = [...colors[ci]];
function clickE() {
let target = [...colors[++ci % colors.length]];
move(color, target, duration);
}
dom.addEventListener("click", clickE);
数值绑定
既然color
能渐变了,就把它绑定到Point.prototype.draw
中颜色相关的地方吧。
Point.prototype.draw = function() {
context.beginPath();
let s = noise.simplex3(this.cx / scale, this.cy / scale, time);
let sa = Math.abs(s);
context.strokeStyle = `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${sa})`;
context.lineWidth = Math.abs(s) * 8;
let a = Math.PI * 2 * s;
let ap = Math.PI + a;
context.moveTo(this.cx + Math.sin(a) * r, this.cy + Math.cos(a) * r);
context.lineTo(this.cx + Math.cos(ap) * r, this.cy + Math.sin(ap) * r);
context.stroke();
};
接着把body的背景色去掉
.main {
/* background: #000; */
height: 100vh;
}
完整代码如下(记得自行改变noise库的src)
<!DOCTYPE html>
<html>
<head>
<style>
body {
margin: 0;
}
.main {
/* background: #000; */
height: 100vh;
}
</style>
</head>
<body>
<div class="wrapper"></div>
<div class="main"></div>
</body>
<script src="./perlin.js"></script>
<script>
const wave = function({
dom,
span = 50,
scale = 1000,
speed = 0.01,
zIndex = -999,
duration = 2000,
colors = [
[212, 192, 255],
[192, 255, 244],
[255, 192, 203]
]
}) {
dom.style.position = "relative";
dom.style.zIndex = 0;
dom.style.overflow = "hidden";
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
canvas.style.position = "absolute";
canvas.style.zIndex = zIndex;
canvas.height = parseInt(getComputedStyle(dom)["height"]);
canvas.width = parseInt(getComputedStyle(dom)["width"]);
dom.appendChild(canvas);
let r = span / 2;
function Point({ cx, cy }) {
this.cx = cx;
this.cy = cy;
}
Point.prototype.draw = function() {
context.beginPath();
let s = noise.simplex3(this.cx / scale, this.cy / scale, time);
let sa = Math.abs(s);
context.strokeStyle = `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${sa})`;
context.lineWidth = Math.abs(s) * 8;
let a = Math.PI * 2 * s;
let ap = Math.PI + a;
context.moveTo(this.cx + Math.sin(a) * r, this.cy + Math.cos(a) * r);
context.lineTo(this.cx + Math.cos(ap) * r, this.cy + Math.sin(ap) * r);
context.stroke();
};
let points = [];
function initPoints() {
for (let y = 0; y < canvas.height; y += span) {
for (let x = 0; x < canvas.width; x += span) {
points.push(
new Point({
cx: x + r,
cy: y + r
})
);
}
}
}
function draw() {
context.clearRect(0, 0, canvas.width, canvas.height);
for (let i of points) {
i.draw();
}
}
let id;
let time = 0;
function animate() {
draw();
time += speed;
id = requestAnimationFrame(animate);
}
let ci = 0;
const color = [...colors[ci]];
function clickE() {
let target = [...colors[++ci % colors.length]];
move(color, target, duration);
}
dom.addEventListener("click", clickE);
const copy = target => {
let re = [];
for (let i in target) {
re[i] = target[i];
}
return re;
};
const move = (
origin,
target,
duration,
after, // 每次数值更改后的回调
fn = pro => {
return Math.sqrt(pro, 2);
} // 缓动函数
) => {
if (fn(1) != 1) throw '[moaline-move] The fn must satisfy "fn (1) == 1"'; // 当参数为1时,对应的值也一定要为1
let st, sp;
st = performance.now(); // 保存开始时间
sp = copy(origin); // 保存源属性
let d = {}; // 源与目标之间每一项的距离
for (let i in origin) {
d[i] = target[i] - origin[i];
}
let frame = t => {
let pro = (t - st) / duration; // 当前进程
if (pro >= 1) {
return;
}
for (let i in origin) {
origin[i] = sp[i] + fn(pro) * d[i]; // fn(pro)得出当前时间对应的缓动函数的距离百分比,再乘以总距离
}
if(after) after(copy(origin), pro);
requestAnimationFrame(frame);
};
frame(st);
};
initPoints();
animate();
return {
canvas,
stop() {
cancelAnimationFrame(id);
dom.removeEventListener('click', clickE);
}
};
};
wave({
dom: document.querySelector(".main"),
scale: 1000,
speed: 0.0006,
span: 200,
duration: 1000
});
</script>
</html>
最后
发挥想象,将噪声的生成值灵活运用,或者添加一些交互,一定会有更加厉害的效果。但是记得不需要时一定要调用返回的stop()
方法停止动画。
还可多个canvas叠加。