为“摇摆不定的画布"制作动画就像在 Discord 的登录页面中一样?
作为参考,我说的是
它有许多非常酷的效果,点和(较暗的阴影)随着鼠标移动,但我对摆动边缘"效果更感兴趣,在较小程度上快速摆动/页面加载时缩小"(加载时在画布中缩放会产生类似的,如果不是更便宜"的效果).
不幸的是,我无法以 MCVE 的方式制作太多东西,因为我不确定从哪里开始.我尝试挖掘 Discord 的资产,但我对 Webpack 不够熟悉,无法确定发生了什么.
我能够在动画波浪/摆动"上挖掘的所有内容都是 CSS 驱动的 SVG 或剪辑路径边框,我想制作一些更有机的东西.
解决方案非常有趣的问题.我已经缩小了 blob,因此它在下面的预览中可见.
这是一个更大尺寸的codepen.
const SCALE = 0.25;常量 TWO_PI = Math.PI * 2;const HALF_PI = Math.PI/2;const canvas = document.createElement("canvas");常量 c = canvas.getContext("2d");canvas.width = window.innerWidth;canvas.height = window.innerHeight;document.body.appendChild(canvas);类斑点{构造函数(){this.wobbleIncrement = 0;//使用它来改变 blob 的大小this.radius = 500;//将此视为详细级别//`bezierSkin` 中的连接数this.segments = 12;this.step = HALF_PI/this.segments;this.anchors = [];this.radii = [];this.thetaOff = [];常量凹凸半径 = 100;常量 halfBumpRadius = 凹凸半径/2;for (让 i = 0; i < this.segments + 2; i++) {this.anchors.push(0, 0);this.radii.push(Math.random() * bumpRadius - halfBumpRadius);this.thetaOff.push(Math.random() * TWO_PI);}this.theta = 0;this.thetaRamp = 0;this.thetaRampDest = 12;this.rampDamp = 25;}更新() {this.thetaRamp += (this.thetaRampDest - this.thetaRamp)/this.rampDamp;this.theta += 0.03;this.anchors = [0, this.radius];for (let i = 0; i <= this.segments + 2; i++) {常量正弦 = Math.sin(this.thetaOff[i] + this.theta + this.thetaRamp);常数 rad = this.radius + this.radii[i] * sine;const theta = this.step * i;常量 x = rad * Math.sin(theta);常量 y = rad * Math.cos(theta);this.anchors.push(x, y);}c.save();c.translate(-10, -10);c.scale(SCALE, SCALE);c.fillStyle = "蓝色";c.beginPath();c.moveTo(0, 0);bezierSkin(this.anchors, false);c.lineTo(0, 0);c.fill();c.restore();}}常量斑点 = 新斑点();函数循环(){c.clearRect(0, 0, canvas.width, canvas.height);blob.update();window.requestAnimationFrame(loop);}环形();//xy 坐标数组,封闭布尔值功能贝塞尔皮肤(贝兹,关闭=真){常量 avg = calcAvgs(bez);const 长度 = bez.length;如果(关闭){c.moveTo(avg[0], avg[1]);for (让 i = 2; i <长度; i += 2) {让 n = i + 1;c.quadraticCurveTo(bez[i], bez[n], avg[i], avg[n]);}c.quadraticCurveTo(bez[0], bez[1], avg[0], avg[1]);} 别的 {c.moveTo(bez[0], bez[1]);c.lineTo(avg[0], avg[1]);for (让 i = 2; i <长度 - 2; i += 2) {让 n = i + 1;c.quadraticCurveTo(bez[i], bez[n], avg[i], avg[n]);}c.lineTo(bez[leng - 2], bez[leng - 1]);}}//通过平均控制点创建锚点函数 calcAvgs(p) {常量平均 = [];const 长度 = p.length;让前一个;for (让 i = 2; i < 长度; i++) {上一页 = i - 2;avg.push((p[prev] + p[i])/2);}//关闭avg.push((p[0] + p[leng - 2])/2, (p[1] + p[leng - 1])/2);返回平均值;}
这里发生了很多事情.为了创建这种效果,您需要很好地了解如何定义二次贝塞尔曲线.一旦你有了它,就有一个多年来我使用过很多次的老把戏.要生成平滑链接的二次贝塞尔曲线,请定义点列表并计算它们的平均值.然后使用这些点作为控制点,新的平均点作为锚点.请参阅 bezierSkin
和 calcAvgs
函数.
有了绘制平滑贝塞尔曲线的能力,剩下的就是将点定位在圆弧中,然后为它们设置动画.为此,我们使用了一些数学:
x = 半径 * sin(theta)y = 半径 * cos(theta)
将极坐标转换为笛卡尔坐标.其中 theta
是圆 [0 - 2pi]
圆周上的角度.
至于动画,这里还有很多内容 - 我会看看这个周末是否有更多时间来更新答案,提供更多细节和信息,但希望这会有所帮助.
For reference, I'm talking about the dark-gray space in the upper left of Discord's Login Page. For anyone who can't access that link, here's a screenshot:
It has a number of effects that are really cool, the dots and (darker shadows) move with the mouse, but I'm more interested in the "wobbly edge" effect, and to a lesser extent the "fast wobble/scale in" on page load (scaling in the canvas on load would give a similar, if not "cheaper" effect).
Unfortunately, I can't produce much in the way of a MCVE, because I'm not really sure where to start. I tried digging through Discord's assets, but I'm not familiar enough to Webpack to be able to determine what's going on.
Everything I've been able to dig up on "animated wave/wobble" is CSS powered SVG or clip-path borders, I'd like to produce something a bit more organic.
解决方案Very interesting problem. I've scaled the blob down so it is visible in the preview below.
Here is a codepen as well at a larger size.
const SCALE = 0.25;
const TWO_PI = Math.PI * 2;
const HALF_PI = Math.PI / 2;
const canvas = document.createElement("canvas");
const c = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
document.body.appendChild(canvas);
class Blob {
constructor() {
this.wobbleIncrement = 0;
// use this to change the size of the blob
this.radius = 500;
// think of this as detail level
// number of conections in the `bezierSkin`
this.segments = 12;
this.step = HALF_PI / this.segments;
this.anchors = [];
this.radii = [];
this.thetaOff = [];
const bumpRadius = 100;
const halfBumpRadius = bumpRadius / 2;
for (let i = 0; i < this.segments + 2; i++) {
this.anchors.push(0, 0);
this.radii.push(Math.random() * bumpRadius - halfBumpRadius);
this.thetaOff.push(Math.random() * TWO_PI);
}
this.theta = 0;
this.thetaRamp = 0;
this.thetaRampDest = 12;
this.rampDamp = 25;
}
update() {
this.thetaRamp += (this.thetaRampDest - this.thetaRamp) / this.rampDamp;
this.theta += 0.03;
this.anchors = [0, this.radius];
for (let i = 0; i <= this.segments + 2; i++) {
const sine = Math.sin(this.thetaOff[i] + this.theta + this.thetaRamp);
const rad = this.radius + this.radii[i] * sine;
const theta = this.step * i;
const x = rad * Math.sin(theta);
const y = rad * Math.cos(theta);
this.anchors.push(x, y);
}
c.save();
c.translate(-10, -10);
c.scale(SCALE, SCALE);
c.fillStyle = "blue";
c.beginPath();
c.moveTo(0, 0);
bezierSkin(this.anchors, false);
c.lineTo(0, 0);
c.fill();
c.restore();
}
}
const blob = new Blob();
function loop() {
c.clearRect(0, 0, canvas.width, canvas.height);
blob.update();
window.requestAnimationFrame(loop);
}
loop();
// array of xy coords, closed boolean
function bezierSkin(bez, closed = true) {
const avg = calcAvgs(bez);
const leng = bez.length;
if (closed) {
c.moveTo(avg[0], avg[1]);
for (let i = 2; i < leng; i += 2) {
let n = i + 1;
c.quadraticCurveTo(bez[i], bez[n], avg[i], avg[n]);
}
c.quadraticCurveTo(bez[0], bez[1], avg[0], avg[1]);
} else {
c.moveTo(bez[0], bez[1]);
c.lineTo(avg[0], avg[1]);
for (let i = 2; i < leng - 2; i += 2) {
let n = i + 1;
c.quadraticCurveTo(bez[i], bez[n], avg[i], avg[n]);
}
c.lineTo(bez[leng - 2], bez[leng - 1]);
}
}
// create anchor points by averaging the control points
function calcAvgs(p) {
const avg = [];
const leng = p.length;
let prev;
for (let i = 2; i < leng; i++) {
prev = i - 2;
avg.push((p[prev] + p[i]) / 2);
}
// close
avg.push((p[0] + p[leng - 2]) / 2, (p[1] + p[leng - 1]) / 2);
return avg;
}
There are lots of things going on here. In order to create this effect you need a good working knowledge of how quadratic bezier curves are defined. Once you have that, there is an old trick that I've used many many times over the years. To generate smooth linked quadratic bezier curves, define a list of points and calculate their averages. Then use the points as control points and the new averaged points as anchor points. See the bezierSkin
and calcAvgs
functions.
With the ability to draw smooth bezier curves, the rest is about positioning the points in an arc and then animating them. For this we use a little math:
x = radius * sin(theta)
y = radius * cos(theta)
That converts polar to cartesian coordinates. Where theta
is the angle on the circumference of a circle [0 - 2pi]
.
As for the animation, there is a good deal more going on here - I'll see if I have some more time this weekend to update the answer with more details and info, but hopefully this will be helpful.
相关文章