編寫一個(gè)互動(dòng)(并且超級(jí)令人滿意)的光標(biāo):7個(gè)簡單的步驟 + 2KB的代碼
近期我制作了這個(gè)光標(biāo)動(dòng)畫,人們似乎很喜歡它 :)
這是一個(gè)很好看的作品,但同時(shí)也非常簡單,只需2KB的JS代碼。
而且,這種方法非常通用,可以作為其他美麗作品的模板使用。因此,它值得有一個(gè)逐步指南!
步驟#1:設(shè)置
我們正在<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; }
當(dāng)然,我們需要跟蹤光標(biāo)位置。
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:動(dòng)畫循環(huán)
要看到最簡單的鼠標(biāo)跟隨動(dòng)畫,我們只需要使用該方法循環(huán)重畫畫布window.requestAnimationFrame(),并在每一步繪制以指針坐標(biāo)為中心的圓。
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); }
通過上面的代碼,我們有一個(gè)跟隨鼠標(biāo)的黑色圓圈。
步驟#3:添加延遲
現(xiàn)在,圓圈會(huì)盡可能快地跟隨光標(biāo)。讓我們添加一個(gè)延遲,以便點(diǎn)以某種彈性方式趕上目標(biāo)位置。
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 參數(shù)用于確定點(diǎn)追上光標(biāo)位置的速度。 像 0.1 這樣的小值會(huì)使其跟隨非常慢,而 spring = 1 意味著沒有延遲。
步驟#3:創(chuàng)建鼠標(biāo)軌跡
讓我們創(chuàng)建一個(gè)點(diǎn)數(shù)據(jù)的軌跡數(shù)組,每個(gè)點(diǎn)都保存我們用來計(jì)算延遲的x/y坐標(biāo)和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, } }
我們現(xiàn)在繪制整個(gè)軌跡,而不是單個(gè)點(diǎn),其中每個(gè)點(diǎn)都試圖趕上前一個(gè)點(diǎn)。第一個(gè)點(diǎn)追上了光標(biāo)坐標(biāo) ( pointer),并且第一個(gè)點(diǎn)的延遲更長 - 只是因?yàn)樗鼘ξ襾碚f看起來更好:)
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:將點(diǎn)轉(zhuǎn)向線
繪制折線而不是點(diǎn)很容易。
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:累積速度
使光標(biāo)動(dòng)畫看起來非常漂亮的是累積增量。讓我們不僅使用dx/dy來表示到鄰居位置的距離,而且還累加這個(gè)距離。
為了防止增量值變得超級(jí)大超級(jí)快,我們還在每個(gè)步驟上將dx/dy與新參數(shù)相乘。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:平滑線條
動(dòng)議完成!讓我們讓筆畫看起來更好,并將每條線段替換為貝塞爾曲線。
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:調(diào)整線寬
對于此演示,最后一步是將默認(rèn)值lineWidth1px 替換為每個(gè)段變得更小的動(dòng)態(tài)值。
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); }
文章來源:http://www.zghlxwxcb.cn/article/395.html
全源碼示例
HTML
<canvas></canvas> <div class="links"> <a href="http://www.zghlxwxcb.cn" 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;
文章來源地址http://www.zghlxwxcb.cn/article/395.html
到此這篇關(guān)于使用Javascript編寫一個(gè)鼠標(biāo)交互式跟隨特效和光標(biāo)交互效果的文章就介紹到這了,更多相關(guān)內(nèi)容可以在右上角搜索或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!