编写一个互动(并且超级令人满意)的光标:7个简单的步骤 + 2KB的代码
近期我制作了这个光标动画,人们似乎很喜欢它 :)
这是一个很好看的作品,但同时也非常简单,只需2KB的JS代码。
而且,这种方法非常通用,可以作为其他美丽作品的模板使用。因此,它值得有一个逐步指南!
步骤#1:设置
我们正在<canvas>元素上绘图,并且需要<canvas>全屏显示。
canvas { position: fixed; top: 0; left: 0; }
<canvas></canvas>
setupCanvas(); window.addEventListener("resize", setupCanvas); function setupCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; }
当然,我们需要跟踪光标位置。
const pointer = { x: .5 * window.innerWidth, y: .5 * window.innerHeight, } window.addEventListener("click", e => { updateMousePosition(e.clientX, e.clientY); }); window.addEventListener("mousemove", e => { updateMousePosition(e.clientX, e.clientY); }); window.addEventListener("touchmove", e => { updateMousePosition(e.targetTouches[0].clientX, e.targetTouches[0].clientY); }); function updateMousePosition(eX, eY) { pointer.x = eX; pointer.y = eY; }
步骤#2:动画循环
要看到最简单的鼠标跟随动画,我们只需要使用该方法循环重画画布window.requestAnimationFrame(),并在每一步绘制以指针坐标为中心的圆。
const p = {x: 0, y: 0}; // coordinate to draw update(0); function update(t) { ctx.clearRect(0, 0, canvas.width, canvas.height); // copy cursor position p.x = poiner.x; p.y = poiner.y; // draw a dot ctx.beginPath(); ctx.arc(p.x, p.y, 5, 0, 2 * Math.PI); ctx.fill(); window.requestAnimationFrame(update); }
通过上面的代码,我们有一个跟随鼠标的黑色圆圈。
步骤#3:添加延迟
现在,圆圈会尽可能快地跟随光标。让我们添加一个延迟,以便点以某种弹性方式赶上目标位置。
const params = { // ... spring: .4 }; // p.x = poiner.x; // p.y = poiner.y; p.x += (pointer.x - p.x) * params.spring; p.y += (pointer.y - p.y) * params.spring; ctx.beginPath(); ctx.arc(p.x, p.y, 5, 0, 2 * Math.PI); ctx.fill();
spring 参数用于确定点追上光标位置的速度。 像 0.1 这样的小值会使其跟随非常慢,而 spring = 1 意味着没有延迟。
步骤#3:创建鼠标轨迹
让我们创建一个点数据的轨迹数组,每个点都保存我们用来计算延迟的x/y坐标和dx/增量。dy
const params = { // ... pointsNumber: 30 }; // const p = {x: 0, y: 0}; const trail = new Array(params.pointsNumber); for (let i = 0; i < params.pointsNumber; i++) { trail[i] = { x: poiner.x, y: poiner.y, dx: 0, dy: 0, } }
我们现在绘制整个轨迹,而不是单个点,其中每个点都试图赶上前一个点。第一个点追上了光标坐标 ( pointer),并且第一个点的延迟更长 - 只是因为它对我来说看起来更好:)
function update(t) { ctx.clearRect(0, 0, canvas.width, canvas.height); trail.forEach((p, pIdx) => { const prev = pIdx === 0 ? pointer : trail[pIdx - 1]; const spring = pIdx === 0 ? .4 * params.spring : params.spring; p.dx = (prev.x - p.x) * spring; p.dy = (prev.y - p.y) * spring; p.x += p.dx; p.y += p.dy; ctx.beginPath(); ctx.arc(p.x, p.y, 5, 0, 2 * Math.PI); ctx.fill(); }); window.requestAnimationFrame(update); }
步骤#4:将点转向线
绘制折线而不是点很容易。
trail.forEach((p, pIdx) => { const prev = pIdx === 0 ? pointer : trail[pIdx - 1]; p.dx = (prev.x - p.x) * params.spring; p.dy = (prev.y - p.y) * params.spring; p.x += p.dx; p.y += p.dy; // ctx.beginPath(); // ctx.arc(p.x, p.y, 5, 0, 2 * Math.PI); // ctx.fill(); if (pIdx === 0) { // start the line on the first point ctx.beginPath(); ctx.moveTo(p.x, p.y); } else { // continue with new line segment to the following one ctx.lineTo(p.x, p.y); } }); // draw the thing ctx.stroke();
步骤#5:累积速度
使光标动画看起来非常漂亮的是累积增量。让我们不仅使用dx/dy来表示到邻居位置的距离,而且还累加这个距离。
为了防止增量值变得超级大超级快,我们还在每个步骤上将dx/dy与新参数相乘。friction
const params = { // ... friction: .5 }; ... // ... // p.dx = (prev.x - p.x) * spring; // p.dy = (prev.y - p.y) * spring; p.dx += (prev.x - p.x) * spring; p.dy += (prev.y - p.y) * spring; p.dx *= params.friction; p.dy *= params.friction; // as before p.x += p.dx; p.y += p.dy; // ...
步骤#6:平滑线条
动议完成!让我们让笔画看起来更好,并将每条线段替换为贝塞尔曲线。
trail.forEach((p, pIdx) => { // calc p.x and p.y if (pIdx === 0) { ctx.beginPath(); ctx.moveTo(p.x, p.y); // } else { // ctx.lineTo(p.x, p.y); } }); for (let i = 1; i < trail.length - 1; i++) { const xc = .5 * (trail[i].x + trail[i + 1].x); const yc = .5 * (trail[i].y + trail[i + 1].y); ctx.quadraticCurveTo(trail[i].x, trail[i].y, xc, yc); } ctx.stroke();
光滑的!
步骤#7:调整线宽
对于此演示,最后一步是将默认值lineWidth1px 替换为每个段变得更小的动态值。
const params = { baseWidth: .9, }; ... for (let i = 1; i < trail.length - 1; i++) { // ... ctx.quadraticCurveTo(trail[i].x, trail[i].y, xc, yc); ctx.lineWidth = params.baseWidth * (params.pointsNumber - i); }
文章来源:https://www.toymoban.com/diary/js/395.html
全源码示例
HTML
<canvas></canvas> <div class="links"> <a href="https://www.toymoban.com" target="_blank">tutorial<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg> </a> </div>
CSS
body, html { padding: 0; margin: 0; overscroll-behavior: none; } /* for tutorial link only */ .links { position: fixed; bottom: 10px; right: 10px; font-size: 18px; font-family: sans-serif; background-color: white; padding: 10px; } a { text-decoration: none; color: black; margin-left: 1em; } a:hover { text-decoration: underline; } a img.icon { display: inline-block; height: 1em; margin: 0 0 -0.1em 0.3em; }
JS
const canvas = document.querySelector("canvas"); const ctx = canvas.getContext('2d'); // for intro motion let mouseMoved = false; const pointer = { x: .5 * window.innerWidth, y: .5 * window.innerHeight, } const params = { pointsNumber: 40, widthFactor: .3, mouseThreshold: .6, spring: .4, friction: .5 }; const trail = new Array(params.pointsNumber); for (let i = 0; i < params.pointsNumber; i++) { trail[i] = { x: pointer.x, y: pointer.y, dx: 0, dy: 0, } } window.addEventListener("click", e => { updateMousePosition(e.pageX, e.pageY); }); window.addEventListener("mousemove", e => { mouseMoved = true; updateMousePosition(e.pageX, e.pageY); }); window.addEventListener("touchmove", e => { mouseMoved = true; updateMousePosition(e.targetTouches[0].pageX, e.targetTouches[0].pageY); }); function updateMousePosition(eX, eY) { pointer.x = eX; pointer.y = eY; } setupCanvas(); update(0); window.addEventListener("resize", setupCanvas); function update(t) { // for intro motion if (!mouseMoved) { pointer.x = (.5 + .3 * Math.cos(.002 * t) * (Math.sin(.005 * t))) * window.innerWidth; pointer.y = (.5 + .2 * (Math.cos(.005 * t)) + .1 * Math.cos(.01 * t)) * window.innerHeight; } ctx.clearRect(0, 0, canvas.width, canvas.height); trail.forEach((p, pIdx) => { const prev = pIdx === 0 ? pointer : trail[pIdx - 1]; const spring = pIdx === 0 ? .4 * params.spring : params.spring; p.dx += (prev.x - p.x) * spring; p.dy += (prev.y - p.y) * spring; p.dx *= params.friction; p.dy *= params.friction; p.x += p.dx; p.y += p.dy; }); ctx.beginPath(); ctx.moveTo(trail[0].x, trail[0].y); for (let i = 1; i < trail.length - 1; i++) { const xc = .5 * (trail[i].x + trail[i + 1].x); const yc = .5 * (trail[i].y + trail[i + 1].y); ctx.quadraticCurveTo(trail[i].x, trail[i].y, xc, yc); ctx.lineWidth = params.widthFactor * (params.pointsNumber - i); ctx.stroke(); } ctx.lineTo(trail[trail.length - 1].x, trail[trail.length - 1].y); ctx.stroke(); window.requestAnimationFrame(update); } function setupCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight;
文章来源地址https://www.toymoban.com/diary/js/395.html
到此这篇关于使用Javascript编写一个鼠标交互式跟随特效和光标交互效果的文章就介绍到这了,更多相关内容可以在右上角搜索或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!