先上效果图:

再上代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</
title> <style> html, body, canvas { -webkit-user-select: none; -moz-user-select:
none; -ms-user-select: none; user-select: none; } html, body { width: 100%;
height: 100%; overflow: hidden; margin: 0; display: flex; align-items: center;
justify-content: center; background: #191919; } .asset-img { display: none; } </
style> </head> <body> <canvas></canvas> <img class="asset-img" id="light-img"
src="
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAMAAADzN3VRAAAABGdBTUEA
ALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAACnVBMVEUAAAD40ED40D740Dv40En40Dr40FH40Df40D/40D340
Dn40Dz40Dj40AD40Ff40Db40DT40EH40EL40ED40ED40Dv40D/4zz/4zz74zz/4zz/40D74zz/4zz/4zz/40UH40kD40UL40kH4zz/4zz340D34zz740kH41U
T410f410j410j410f40D/40D/4zz/40ED40EH400T410f42Er4103410/411D41UT40kH40D/4z0D4zz/41UX410j42E7411L42Ff42Vv42Vz42En41kb40UH
4zz/40Dz40ED41UT42Ej4107411b42l743WT432r4323432r410/42En4zz340Dn400H410f42mD432v44nf45IP45Yn45IT44nj432r411b400L40D/4zz/4
1UT42Er411P432v44oD46Zv47q/48Lj46Zr432n40UH410f42Ff44nf48r/499T4997499T48r744nf43WP40D741EH42Vv4+O34+PP4+O347q745IL432j40
UD432345Yj48LX49934+PP4+Pj4993477T45Ib432z411D400D40D/40UH432n45IP47a7432j42Vv410/400H40Dr4zz340UD410b410346Zj48r343WP42F
f410f40D/41kT42Er42l744nv46Zf42l3411L410r41kX40ED410f42E7411b421/45IL45Ib432n421740UH40D340Dz4zz741EP410j410743WT432f432v
43mj43GT42Ej41kT4zz740ED41EX411P42Ff42Fv411f410j41kX400P41034107411D42E340D740UH410j41kb41ET400H40D/40ED4z0D4zz74zz340D//
//+WdN8lAAAA3nRSTlMAAAAAAAAAAAAAAAAAAAAAAAAAAgMCBAcJDAkDCA0SFRsbGxMDBQscJSwwMS0SDAUFFyExQExUVyIYDQULJzlOZHeDhDonFwoDASE5V
XGNpbO4tFM5BwEcMJa3zdjb2Mu2cBwDESM+Y7bU4ebo4bUUK3bL6vDy7+rKogIZgPb49ebXsBu02+jx9/jw6Nu0VRsBGbDX5q9/UhkCCRUqS+HqoHMqESI9i9
Thi2E9IhEvTG+V1tq0kxsDAQchN1Ggr7SvoDghChUjYXJ/cjgnIUlSVEkFGi8qIhsFFAcICAaJi/ApAAAAAWJLR0Te6W7imwAAAAd0SU1FB+YMHBEUINvSacM
AAAIKSURBVCjPY2AgBBgZmZiFRYQZgQBVnEVUTFxCUlJSSlxMlAVJjplVWkZWTl5BUUleWVZGhZkZYZKqmpy6hqaWtpaOhrqunj7MREZGA1lDI2MTUzNzM1MT
YwtLK2uIFCOjiI2hrZ29g6OTs5Ojg72Lq5u7Bxs7SMZTXNfL28fXzz8gMMjfzzc4xEI3NIwVrEUyPMLeNzIqOiY2Lj4hMtE+Ikk2mYkRKCOekpqW7peRmZWdk
52bmeeXnqYhJ84BkpHKLzAt9C/KKi4pLSvPragsNNXMl/JkZGCqUqvWMqsJiMkuqa2rL2tobKox06qWTOZg4EiWbNY2d25pbWvv6Ozo6u7pde7T6pf04GTgmG
AzEahn0uSGsvqO+rIpjVOnTdeaoTaTmYGLfdbsOXMLKyvmzS/rKps/r2jBwrmL5KWAfmViEl+8ZGn6srzlK6Z0T1mxPG/lqtVr1oLdxp0sqbRu/YaNeUWbNm8
q2rJ1w/p12yS38wDDh3PHTrldu/dsWLZ33/4DB5dt2HPosO7OMFBwM/KKHjl6bPf64yemOZ88dXz96TPyR6T5+EFByi6ganP07LrV585fmH5x6bqzs21UmZhg
0XDJXe7ykjnrtK5cvXZdzv0GMycs6jiZpXdKLs532+Z2M0VSXAQ5KTAKeorcuu1uc+TO3e07hNDSCCMjNyczKz8nN8FkxgAAgH2hXtQzzA4AAAAldEVYdGRhd
GU6Y3JlYXRlADIwMjItMTItMjhUMTc6MjA6MzIrMDA6MDA1MNVoAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIyLTEyLTI4VDE3OjIwOjMyKzAwOjAwRG1t1AAAAA
BJRU5ErkJggg==" alt="base64"> </body> <script> class Mouse { constructor(canvas)
{ this.pos = new Vector(-1000, -1000) this.radius = 40 canvas.onmousemove = e =>
this.pos.setXY(e.clientX, e.clientY) canvas.ontouchmove = e => this.pos.setXY(e.
touches[0].clientX, e.touches[0].clientY) canvas.ontouchcancel = () => this.pos.
setXY(-1000, -1000) canvas.ontouchend = () => this.pos.setXY(-1000, -1000) } }
class Dot { constructor(x, y) { this.pos = new Vector(x, y) this.oldPos = new
Vector(x, y) this.friction = 0.97 this.gravity = new Vector(0, 0.6) this.mass =
1 this.pinned = false this.lightImg = document.querySelector('#light-img') this.
lightSize= 15 } update(mouse) { if (this.pinned) return let vel = Vector.sub(
this.pos, this.oldPos) this.oldPos.setXY(this.pos.x, this.pos.y) vel.mult(this.
friction) vel.add(this.gravity) let { x: dx, y: dy } = Vector.sub(mouse.pos,
this.pos) const dist = Math.sqrt(dx * dx + dy * dy) const direction = new Vector
(dx / dist, dy / dist) const force = Math.max((mouse.radius - dist) / mouse.
radius, 0) if (force > 0.6) this.pos.setXY(mouse.pos.x, mouse.pos.y) else { this
.pos.add(vel) this.pos.add(direction.mult(force)) } } drawLight(ctx) { ctx.
drawImage( this.lightImg, this.pos.x - this.lightSize / 2, this.pos.y - this.
lightSize/ 2, this.lightSize, this.lightSize ) } draw(ctx) { ctx.fillStyle =
'#aaa' ctx.fillRect(this.pos.x - this.mass, this.pos.y - this.mass, this.mass *
2, this.mass * 2) } } class Stick { constructor(p1, p2) { this.startPoint = p1
this.endPoint = p2 this.length = this.startPoint.pos.dist(this.endPoint.pos)
this.tension = 0.3 } update() { const dx = this.endPoint.pos.x - this.startPoint
.pos.x const dy = this.endPoint.pos.y - this.startPoint.pos.y const dist = Math.
sqrt(dx * dx + dy * dy) const diff = (dist - this.length) / dist const offsetX =
diff* dx * this.tension const offsetY = diff * dy * this.tension const m = this
.startPoint.mass + this.endPoint.mass const m1 = this.endPoint.mass / m const m2
= this.startPoint.mass / m if (!this.startPoint.pinned) { this.startPoint.pos.x
+= offsetX * m1 this.startPoint.pos.y += offsetY * m1 } if (!this.endPoint.
pinned) { this.endPoint.pos.x -= offsetX * m2 this.endPoint.pos.y -= offsetY *
m2} } draw(ctx) { ctx.beginPath() ctx.strokeStyle = '#999' ctx.moveTo(this.
startPoint.pos.x, this.startPoint.pos.y) ctx.lineTo(this.endPoint.pos.x, this.
endPoint.pos.y) ctx.stroke() ctx.closePath() } } class Rope { constructor(config
) { this.x = config.x this.y = config.y this.segments = config.segments || 10
this.gap = config.gap || 15 this.color = config.color || 'gray' this.dots = []
this.sticks = [] this.iterations = 10 this.create() } pin(index) { this.dots[
index].pinned = true } create() { for (let i = 0; i < this.segments; i++) { this
.dots.push(new Dot(this.x, this.y + i * this.gap)) } for (let i = 0; i < this.
segments- 1; i++) { this.sticks.push(new Stick(this.dots[i], this.dots[i + 1]))
} } update(mouse) { this.dots.forEach(dot => { dot.update(mouse) }) for (let i =
0; i < this.iterations; i++) { this.sticks.forEach(stick => { stick.update() })
} } draw(ctx) { this.dots.forEach(dot => { dot.draw(ctx) }) this.sticks.forEach(
stick => { stick.draw(ctx) }) this.dots[this.dots.length - 1].drawLight(ctx) } }
class App { static width = innerWidth static height = innerHeight static dpr =
devicePixelRatio> 1 ? 2 : 1 static interval = 1000 / 60 constructor() { this.
canvas= document.querySelector('canvas') this.ctx = this.canvas.getContext('2d')
this.mouse = new Mouse(this.canvas) this.resize() window.addEventListener(
'resize', this.resize.bind(this)) this.createRopes() } createRopes() { this.
ropes= [] const TOTAL = App.width * 0.06 for (let i = 0; i < TOTAL + 1; i++) {
const x = randomNumBetween(App.width * 0.3, App.width * 0.7) const y = 0 const
gap= randomNumBetween(App.height * 0.05, App.height * 0.08) const segments = 10
const rope = new Rope({ x, y, gap, segments }) rope.pin(0) this.ropes.push(rope)
} } resize() { App.width = innerWidth App.height = innerHeight this.canvas.style
.width = '100%' this.canvas.style.height = '100%' this.canvas.width = App.width
* App.dpr this.canvas.height = App.height * App.dpr this.ctx.scale(App.dpr, App.
dpr) this.createRopes() } render() { let now, delta let then = Date.now() const
frame = () => { requestAnimationFrame(frame) now = Date.now() delta = now - then
if (delta < App.interval) return then = now - (delta % App.interval) this.ctx.
clearRect(0, 0, App.width, App.height) // draw here this.ropes.forEach(rope => {
rope.update(this.mouse) rope.draw(this.ctx) }) } requestAnimationFrame(frame) }
} function randomNumBetween(min, max) { return Math.random() * (max - min) + min
} window.addEventListener('load', () => { const app = new App() app.render() })
class Vector { constructor(x, y) { this.x = x || 0 this.y = y || 0 } static add(
v1, v2) { return new Vector(v1.x + v2.x, v1.y + v2.y) } static sub(v1, v2) {
return new Vector(v1.x - v2.x, v1.y - v2.y) } add(x, y) { if (arguments.length
=== 1) { this.x += x.x this.y += x.y } else if (arguments.length === 2) { this.x
+= x this.y += y } return this } sub(x, y) { if (arguments.length === 1) { this.
x-= x.x this.y -= x.y } else if (arguments.length === 2) { this.x -= x this.y -=
y} return this } mult(v) { if (typeof v === 'number') { this.x *= v this.y *= v
} else { this.x *= v.x this.y *= v.y } return this } setXY(x, y) { this.x = x
this.y = y return this } dist(v) { const dx = this.x - v.x const dy = this.y - v
.y return Math.sqrt(dx * dx + dy * dy) } } </script> </html>
代码直接粘贴到html页面就能使用,顺滑的不可言说

技术
下载桌面版
GitHub
Gitee
SourceForge
百度网盘(提取码:draw)
云服务器优惠
华为云优惠券
腾讯云优惠券
阿里云优惠券
Vultr优惠券
站点信息
问题反馈
邮箱:[email protected]
吐槽一下
QQ群:766591547
关注微信