特效描述:利用HTML5 Canvas实现一碗面条特效。利用HTML5 Canvas实现一碗面条特效
代码结构
1. HTML代码
// Initiate Canvas
let can = document.getElementById('can'),
ctx = can.getContext('2d'),
cRad = 250;
can.width = cRad * 2;
can.height = cRad * 2;
ctx.translate(cRad, cRad);
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
// Mouse listeners
let mouse = {
x: -cRad,
y: -cRad
};
can.onmousemove = (e) => {
mouse.x = e.clientX - cRad;
mouse.y = e.clientY - cRad;
};
// World variables
let noodles = [];
const bowlRad = cRad - 50,
mouseRad = 20;
// Object: Node - single Noodle joint
function Node(initX, initY, rad) {
this.p = {x: initX, y: initY};
this.v = {x: 0, y: 0};
this.f = {x: 0, y: 0};
this.r = rad;
this.friction = 0.15;
}
Node.prototype.applyForce = function(dx, dy) {
this.f.x += dx;
this.f.y += dy;
}
Node.prototype.step = function() {
// Stay in Bowl
let centerDis = Math.sqrt((this.p.x * this.p.x) + (this.p.y * this.p.y));
if (centerDis > bowlRad) {
let ang = Math.atan2(this.p.y, this.p.x);
this.applyForce(
(bowlRad - centerDis) * Math.cos(ang),
(bowlRad - centerDis) * Math.sin(ang)
);
}
// Mouse interaction
let mouseDis = Math.sqrt(Math.pow(mouse.x - this.p.x, 2) + Math.pow(mouse.y - this.p.y, 2));
if (mouseDis < this.r + mouseRad) {
let ang = Math.atan2(this.p.y - mouse.y, this.p.x - mouse.x);
this.applyForce(
Math.sqrt(this.r + mouseRad / mouseDis) * Math.cos(ang),
Math.sqrt(this.r + mouseRad / mouseDis) * Math.sin(ang)
);
}
// Apply Movement
this.applyForce(this.v.x * -this.friction, this.v.y * -this.friction);
this.v.x += this.f.x;
this.v.y += this.f.y;
this.p.x += this.v.x;
this.p.y += this.v.y;
this.f = {x: 0, y: 0};
}
// Object: Noodle - a string of connected nodes
function Noodle(initX, initY, length, thickness) {
this.nodes = [];
this.elastic = 0.4;
this.thickness = thickness;
// Random colour
let lightness = (50 + Math.round(Math.random() * 40));
this.color = 'hsl(48, 93%, ' + lightness + '%)';
this.colorOutline = 'hsl(48, 93%, ' + (lightness - 20) + '%)';
// Initiate nodes, slightly out of line
let nodeNum = length / thickness,
offsetY = 0;
for(let i = 0; i < nodeNum; i++) {
this.nodes.push(new Node(
initX + (i * thickness),
initY + offsetY,
thickness / 2
));
offsetY += (Math.random() * thickness * 2) - thickness;
}
noodles.push(this);
}
// Draw noodle as curve connecting nodes
Noodle.prototype.draw = function() {
ctx.beginPath();
ctx.moveTo(this.nodes[0].p.x, this.nodes[0].p.y);
let n = 1;
for (; n < this.nodes.length - 2; n++) {
let xc = (this.nodes[n].p.x + this.nodes[n + 1].p.x) / 2,
yc = (this.nodes[n].p.y + this.nodes[n + 1].p.y) / 2;
ctx.quadraticCurveTo(this.nodes[n].p.x, this.nodes[n].p.y, xc, yc);
}
ctx.quadraticCurveTo(
this.nodes[n].p.x,
this.nodes[n].p.y,
this.nodes[n + 1].p.x,
this.nodes[n + 1].p.y
);
ctx.strokeStyle = this.colorOutline;
ctx.lineWidth = this.thickness + 2;
ctx.stroke();
ctx.strokeStyle = this.color;
ctx.lineWidth = this.thickness;
ctx.stroke();
}
// Apply restoration force to all nodes to keep them in line
Noodle.prototype.step = function() {
for(let i = 0; i < this.nodes.length; i++) {
let n = this.nodes[i];
if (i > 0) {
// Find closest distance between previous node
let nPrev = this.nodes[i - 1],
ang = Math.atan2(nPrev.p.y - n.p.y, nPrev.p.x - n.p.x),
nearN = {
x: n.p.x + (Math.cos(ang) * n.r),
y: n.p.y + (Math.sin(ang) * n.r)
},
nearNp = {
x: nPrev.p.x + (Math.cos(ang + Math.PI) * n.r),
y: nPrev.p.y + (Math.sin(ang + Math.PI) * n.r)
};
n.applyForce(
(nearNp.x - nearN.x) * this.elastic,
(nearNp.y - nearN.y) * this.elastic
);
nPrev.applyForce(
(nearN.x - nearNp.x) * this.elastic,
(nearN.y - nearNp.y) * this.elastic
);
}
n.step();
}
}
// World loop
function step() {
ctx.clearRect(-cRad, -cRad, cRad * 2, cRad * 2);
noodles.forEach(n => n.step());
// Draw Bowl
ctx.beginPath();
ctx.arc(0, 0, bowlRad + 25, 0, Math.PI * 2);
ctx.fillStyle = '#e2d3ad';
ctx.fill();
ctx.strokeStyle = '#333';
ctx.lineWidth = 32;
ctx.stroke();
ctx.strokeStyle = '#ddd';
ctx.lineWidth = 30;
ctx.stroke();
// Draw Noodles
noodles.forEach(n => n.draw());
window.requestAnimationFrame(step);
}
// Initiate Noodles
for(let i = 0; i < 150; i++) {
let initX = Math.round(-bowlRad + (Math.random() * bowlRad)),
initY = Math.round(-bowlRad + (Math.random() * bowlRad * 2)),
length = 100 + (Math.random() * 250),
thick = Math.round(10 + (Math.random() * 15));
new Noodle(initX, initY, length, thick);
}
// Initiate World
step();