Одуванчик
Одуванчик на канвасе. Реагирует на движение курсора.
HTML
<div class="container">
<canvas class="js-canvas"></canvas>
</div>
SCSShtml, body { margin: 0; padding: 0; }
body {
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
canvas {
box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.05);
}
JS
Дополнительная библиотека//unpkg.com/simplex-noise@2.4.0/simplex-noise.js
скриптconst simplex = new SimplexNoise(Math.random());
const angleBetween = (vec1, vec2) => Math.atan2(vec2.y - vec1.y, vec2.x - vec1.x);
const distanceBetween = (vec1, vec2) => Math.hypot(vec1.x - vec2.x, vec1.y - vec2.y);
const TAU = Math.PI * 2;
class Stage {
constructor(canvas, width, height) {
this.canvas = canvas;
this.context = this.canvas.getContext('2d');
this.setSize(width, height);
}
clear() {
this.context.clearRect(0, 0, this.width, this.height);
}
setSize(width, height) {
this.width = width;
this.height = height;
this.center = {
x: this.width * 0.5,
y: this.height * 0.5,
};
this.canvas.width = this.width;
this.canvas.height = this.height;
}
getRandomPosition() {
return { x: this.width * Math.random(), y: this.height * Math.random() };
}
}
class Element {
constructor(position, radius) {
this.positionCurrent = { x: position.x,y: position.y };
this.positionDefault = { x: position.x, y: position.y };
this.radius = radius;
this.maxReach = 50;
}
update(focusPoint, distanceMax, reachForce) {
const distance = distanceBetween(this.positionDefault, focusPoint);
this.reachTo(distance, focusPoint, reachForce);
this.scale(distance, distanceMax);
}
scale(distance, distanceMax) {
this.radius = Math.max(0.5, (1 - (distance / distanceMax)) * 10);
}
reachTo(distance, focusPoint, reachForce) {
const { positionDefault: pd } = this;
const angle = angleBetween(pd, focusPoint);
const distanceNorm = Math.min(distance / reachForce, 1);
const length = 50 * distanceNorm;
this.positionCurrent.x = pd.x + (Math.cos(angle) * length);
this.positionCurrent.y = pd.y + (Math.sin(angle) * length);
}
drawStok(ctx, center) {
ctx.beginPath();
ctx.strokeStyle = '#ccc';
ctx.lineWidth = 0.5;
ctx.moveTo(this.positionCurrent.x, this.positionCurrent.y);
ctx.lineTo(center.x, center.y)
ctx.stroke();
ctx.closePath();
}
drawElement(ctx, center) {
ctx.save();
ctx.translate(this.positionCurrent.x, this.positionCurrent.y);
ctx.beginPath();
ctx.arc(0, 0, this.radius, 0, TAU, false);
ctx.fill();
ctx.closePath();
ctx.restore();
}
}
class Scene {
constructor(stage) {
this.phase = 0;
this.stage = stage;
this.padding = 100;
this.numOrbits = 10;
this.radius = (stage.width - (this.padding * 2)) * 0.5;
this.elements = [];
this.rafId = null;
this.automated = true;
this.setFocusPoint(stage.center);
}
reset() {
this.stage.clear();
this.generate();
}
generate() {
this.elements = [];
const { numOrbits, stage: { center } } = this;
const radiusStep = this.radius / numOrbits;
for (let i = 0; i < numOrbits; i++) {
const orbitRadius = radiusStep * (i + 1);
const numCircles = Math.ceil(orbitRadius / 5);
const angleStep = TAU / numCircles;
for (let q = 0; q < numCircles; q++) {
const x = center.x + (Math.cos(angleStep * q) * orbitRadius);
const y = center.y + (Math.sin(angleStep * q) * orbitRadius);
const radius = 5;
this.elements.push(new Element({ x, y }, radius ));
}
}
}
setFocusPoint(point) {
this.focusPoint = { x: point.x, y: point.y };
}
run() {
this.stage.clear();
const { context, center } = this.stage;
if (this.automated) {
const radius = (this.radius * 2) + ((this.radius * 0.5) * simplex.noise3D(this.phase, this.phase, this.phase));
const angle = TAU * simplex.noise3D(this.phase, this.phase, this.phase);
this.focusPoint = {
x: center.x + (Math.cos(angle) * radius),
y: center.y + (Math.sin(angle) * radius),
};
}
this.elements.forEach((e) => {
e.update(this.focusPoint, this.radius * 2, this.stage.width * 0.01);
e.drawStok(context, center);
});
this.elements.forEach((e) => {
e.drawElement(context, center);
});
this.phase += 0.001;
this.rafId = requestAnimationFrame(() => this.run());
}
}
const stage = new Stage(document.querySelector('.js-canvas'), 700, 700);
const scene = new Scene(stage);
const onPointerMove = (e) => {
const target = (e.touches && e.touches.length) ? e.touches[0] : e;
scene.setFocusPoint({
x: target.clientX - e.target.offsetLeft,
y: target.clientY - e.target.offsetTop,
});
};
const onPointerOver = () => {
scene.automated = false;
};
const onPointerLeave = () => {
scene.automated = true;
};
stage.canvas.addEventListener('mousemove', onPointerMove);
stage.canvas.addEventListener('touchmove', onPointerMove);
stage.canvas.addEventListener('mouseenter', onPointerOver);
stage.canvas.addEventListener('touchstart', onPointerOver);
stage.canvas.addEventListener('mouseleave', onPointerLeave);
stage.canvas.addEventListener('touchend', onPointerLeave);
scene.generate();
scene.run();