Gemini一次成型龙跟随鼠标html5+canvas特效

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas Dragon Cursor</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}body{overflow:hidden;background:#1a1a1a;cursor:none}canvas{display:block;width:100vw;height:100vh}
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
const canvas=document.getElementById('c');
const ctx=canvas.getContext('2d');
let width,height;
const mouse={x:window.innerWidth/2,y:window.innerHeight/2};
const target={x:window.innerWidth/2,y:window.innerHeight/2};
let tick=0;
//配置参数
const config={
segmentCount:40,
headSize:25, //基础大小,视觉上会因为绘画扩大
followSpeed:0.12,
colorHead:'#FFD700',
colorBody:'#DC143C',
colorScale:'#FF8C00'
};
//龙的身体节段
class Segment{
constructor(x,y,r){
this.x=x;
this.y=y;
this.r=r;
this.angle=0;
}
}
const segments=[];
function init(){
width=canvas.width=window.innerWidth;
height=canvas.height=window.innerHeight;
segments.length=0;
let r=config.headSize;
for(let i=0;i<config.segmentCount;i++){
segments.push(new Segment(width/2,height/2,r));
r*=0.94; //身体逐渐变细
}
}
window.addEventListener('resize',init);
window.addEventListener('mousemove',e=>{
target.x=e.clientX;
target.y=e.clientY;
});
window.addEventListener('touchmove',e=>{
target.x=e.touches[0].clientX;
target.y=e.touches[0].clientY;
},{passive:true});
//核心循环
function loop(){
requestAnimationFrame(loop);
ctx.fillStyle='rgba(20,20,25,0.4)'; //拖尾效果背景
ctx.fillRect(0,0,width,height);
tick+=0.05;
//头部平滑移动
const dx=target.x-mouse.x;
const dy=target.y-mouse.y;
mouse.x+=dx*config.followSpeed;
mouse.y+=dy*config.followSpeed;
//更新头部节段
segments[0].x=mouse.x;
segments[0].y=mouse.y;
//计算头部角度
const headAngle=Math.atan2(dy,dx);
if(Math.abs(dx)>1||Math.abs(dy)>1){
//移动时头部朝向运动方向
let diff=headAngle-segments[0].angle;
while(diff<-Math.PI)diff+=Math.PI*2;
while(diff>Math.PI)diff-=Math.PI*2;
segments[0].angle+=diff*0.1;
}
//更新身体节段(IK类似逻辑)
for(let i=1;i<segments.length;i++){
const seg=segments[i];
const prev=segments[i-1];
//理想距离
const targetDist=prev.r+seg.r*0.5;
//计算角度和距离
let dx2=prev.x-seg.x;
let dy2=prev.y-seg.y;
const dist=Math.sqrt(dx2*dx2+dy2*dy2);
const angle=Math.atan2(dy2,dx2);
seg.angle=angle;
//如果停止,添加呼吸/盘旋的正弦波偏移
const isIdle=Math.abs(dx)<2&&Math.abs(dy)<2;
if(isIdle){
const wave=Math.sin(tick+(i*0.2))*2;
seg.x+=(Math.cos(angle)*0.5);
seg.y+=(Math.sin(angle)*0.5);
//缓慢向目标靠拢,但保持波浪
if(dist>targetDist){
seg.x+=Math.cos(angle)*(dist-targetDist)*0.1;
seg.y+=Math.sin(angle)*(dist-targetDist)*0.1;
}
//侧向漂浮
seg.x+=Math.cos(angle+Math.PI/2)*Math.sin(tick*0.5+i*0.1)*0.5;
seg.y+=Math.sin(angle+Math.PI/2)*Math.sin(tick*0.5+i*0.1)*0.5;
}else{
//紧跟
if(dist>targetDist){
const tx=prev.x-Math.cos(angle)*targetDist;
const ty=prev.y-Math.sin(angle)*targetDist;
seg.x+=(tx-seg.x)*0.3;
seg.y+=(ty-seg.y)*0.3;
}
}
}
//绘制
drawDragon();
}
function drawDragon(){
//倒序绘制,头部最后画覆盖在最上层
for(let i=segments.length-1;i>=0;i--){
const s=segments[i];
ctx.save();
ctx.translate(s.x,s.y);
ctx.rotate(s.angle);
if(i===0){
drawHead(s);
}else{
drawBody(s,i);
}
ctx.restore();
}
}
function drawBody(s,index){
//脊背
ctx.fillStyle=config.colorBody;
ctx.beginPath();
ctx.arc(0,0,s.r,0,Math.PI*2);
ctx.fill();
//鳞片高光
ctx.fillStyle=config.colorScale;
ctx.beginPath();
ctx.ellipse(0,-s.r*0.4,s.r*0.6,s.r*0.3,0,0,Math.PI*2);
ctx.fill();
//背鳍
if(index%3===0){
ctx.fillStyle=config.colorHead;
ctx.beginPath();
ctx.moveTo(-s.r*0.5,-s.r*0.8);
ctx.lineTo(s.r*0.5,-s.r*0.8);
ctx.lineTo(0,-s.r*1.8);
ctx.fill();
}
}
function drawHead(s){
const size=s.r; 
//龙须 - 动态
ctx.strokeStyle=config.colorHead;
ctx.lineWidth=2;
ctx.lineCap='round';
const whiskerWave=Math.sin(tick)*10;
ctx.beginPath();
ctx.moveTo(size*0.5,-size*0.2);
ctx.bezierCurveTo(size*1.5,-size*1.5,size*1.5,size*1.5+whiskerWave,size*3,size+whiskerWave);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(size*0.5,size*0.2);
ctx.bezierCurveTo(size*1.5,size*1.5,size*1.5,-size*1.5-whiskerWave,size*3,-size-whiskerWave);
ctx.stroke();
//头骨
ctx.shadowBlur=20;
ctx.shadowColor=config.colorHead;
ctx.fillStyle=config.colorBody;
//脸型
ctx.beginPath();
ctx.ellipse(-size*0.2,0,size*1.2,size,0,0,Math.PI*2);
ctx.fill();
//鼻子/吻部
ctx.fillStyle=config.colorScale;
ctx.beginPath();
ctx.ellipse(size*0.8,0,size*0.6,size*0.5,0,0,Math.PI*2);
ctx.fill();
ctx.shadowBlur=0;
//眼睛
const eyeOffset=size*0.4;
const eyeSize=size*0.25;
ctx.fillStyle='#00FF00'; //绿眼
ctx.beginPath();
ctx.arc(0,-eyeOffset,eyeSize,0,Math.PI*2);
ctx.arc(0,eyeOffset,eyeSize,0,Math.PI*2);
ctx.fill();
//瞳孔
ctx.fillStyle='#000';
ctx.beginPath();
ctx.arc(0,-eyeOffset,eyeSize/3,0,Math.PI*2);
ctx.arc(0,eyeOffset,eyeSize/3,0,Math.PI*2);
ctx.fill();
//龙角
ctx.fillStyle='#8B4513';
ctx.beginPath();
ctx.moveTo(-size*0.5,-eyeOffset);
ctx.quadraticCurveTo(-size*1.5,-size*1.5,-size*0.8,-size*2.5);
ctx.lineTo(-size*0.2,-size*1.5);
ctx.fill();
ctx.beginPath();
ctx.moveTo(-size*0.5,eyeOffset);
ctx.quadraticCurveTo(-size*1.5,size*1.5,-size*0.8,size*2.5);
ctx.lineTo(-size*0.2,size*1.5);
ctx.fill();
}
init();
loop();
</script>
</body>
</html>
相关推荐
知了清语18 小时前
是的,微信小程序的 show-menu-by-longpress 真的会让你无语
前端
Hao_Harrision18 小时前
50天50个小项目 (React19 + Tailwindcss V4) ✨| RangeSlider(范围滑块组件)
前端·typescript·react·tailwindcss·vite7
CC码码18 小时前
不修改DOM的高亮黑科技,你可能还不知道
前端·javascript·面试
虚诚18 小时前
vue2中树形表格怎么实现
前端·javascript·vue.js·ecmascript·vue2·树形结构
wuhen_n19 小时前
Promise与async/await
前端
LYFlied19 小时前
前端路由核心原理深入剖析
前端
用户190176844786519 小时前
vue3规范化示例
前端
用户190176844786519 小时前
Git分支管理与代码合并实践:保持特性分支与主分支同步
前端
没有鸡汤吃不下饭19 小时前
前端打包出一个项目(文件夹),怎么本地快速启一个服务运行
前端·javascript
liusheng19 小时前
Capacitor + React 的 iOS 侧滑返回手势
前端·ios