
触手特效
<!-- TentacleEffect.vue -->
<template>
<canvas ref="canvas"></canvas>
</template>
<script>
import { ref, onMounted, onBeforeUnmount } from 'vue';
export default {
name: 'TentacleEffect',
props: {
// 触手数量
tentacleCount: {
type: Number,
default: 500
},
// 最大长度
maxLength: {
type: Number,
default: 300
},
// 最小长度
minLength: {
type: Number,
default: 50
},
// 每个触手的段数
segmentCount: {
type: Number,
default: 30
},
// 背景色
backgroundColor: {
type: String,
default: 'rgb(30,30,30)'
},
// 触手基础色调
baseHue: {
type: Number,
default: 180 // 青色
}
},
setup(props) {
const canvas = ref(null);
let ctx = null;
let width = 0;
let height = 0;
let animationId = null;
const mouse = { x: false, y: false };
const last_mouse = {};
const target = { x: 0, y: 0 };
const last_target = {};
let t = 0;
const q = 10;
let tent = [];
class Segment {
constructor(parent, l, a, first) {
this.first = first;
if (first) {
this.pos = {
x: parent.x,
y: parent.y
};
} else {
this.pos = {
x: parent.nextPos.x,
y: parent.nextPos.y
};
}
this.l = l;
this.ang = a;
this.nextPos = {
x: this.pos.x + this.l * Math.cos(this.ang),
y: this.pos.y + this.l * Math.sin(this.ang)
};
}
update(t) {
this.ang = Math.atan2(t.y - this.pos.y, t.x - this.pos.x);
this.pos.x = t.x + this.l * Math.cos(this.ang - Math.PI);
this.pos.y = t.y + this.l * Math.sin(this.ang - Math.PI);
this.nextPos.x = this.pos.x + this.l * Math.cos(this.ang);
this.nextPos.y = this.pos.y + this.l * Math.sin(this.ang);
}
fallback(t) {
this.pos.x = t.x;
this.pos.y = t.y;
this.nextPos.x = this.pos.x + this.l * Math.cos(this.ang);
this.nextPos.y = this.pos.y + this.l * Math.sin(this.ang);
}
show() {
ctx.lineTo(this.nextPos.x, this.nextPos.y);
}
}
class Tentacle {
constructor(x, y, l, n, a) {
this.x = x;
this.y = y;
this.l = l;
this.n = n;
this.rand = Math.random();
this.segments = [new Segment(this, this.l / this.n, 0, true)];
for (let i = 1; i < this.n; i++) {
this.segments.push(
new Segment(this.segments[i - 1], this.l / this.n, 0, false)
);
}
}
move(last_target, target) {
this.angle = Math.atan2(target.y - this.y, target.x - this.x);
this.dt = dist(last_target.x, last_target.y, target.x, target.y) + 5;
this.t = {
x: target.x - 0.8 * this.dt * Math.cos(this.angle),
y: target.y - 0.8 * this.dt * Math.sin(this.angle)
};
if (this.t.x) {
this.segments[this.n - 1].update(this.t);
} else {
this.segments[this.n - 1].update(target);
}
for (let i = this.n - 2; i >= 0; i--) {
this.segments[i].update(this.segments[i + 1].pos);
}
if (dist(this.x, this.y, target.x, target.y) <= this.l + dist(last_target.x, last_target.y, target.x, target.y)) {
this.segments[0].fallback({ x: this.x, y: this.y });
for (let i = 1; i < this.n; i++) {
this.segments[i].fallback(this.segments[i - 1].nextPos);
}
}
}
show(target) {
if (dist(this.x, this.y, target.x, target.y) <= this.l) {
ctx.globalCompositeOperation = "color-dodge";
ctx.beginPath();
ctx.lineTo(this.x, this.y);
for (let i = 0; i < this.n; i++) {
this.segments[i].show();
}
ctx.strokeStyle = `hsl(${this.rand * 60 + props.baseHue},100%,${this.rand * 60 + 25}%)`;
ctx.lineWidth = this.rand * 2;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.stroke();
ctx.globalCompositeOperation = "source-over";
}
}
show2(target) {
ctx.beginPath();
if (dist(this.x, this.y, target.x, target.y) <= this.l) {
ctx.arc(this.x, this.y, 2 * this.rand + 1, 0, 2 * Math.PI);
ctx.fillStyle = "white";
} else {
ctx.arc(this.x, this.y, this.rand * 2, 0, 2 * Math.PI);
ctx.fillStyle = "darkcyan";
}
ctx.fill();
}
}
function dist(p1x, p1y, p2x, p2y) {
return Math.sqrt(Math.pow(p2x - p1x, 2) + Math.pow(p2y - p1y, 2));
}
function initTentacles() {
tent = [];
for (let i = 0; i < props.tentacleCount; i++) {
tent.push(
new Tentacle(
Math.random() * width,
Math.random() * height,
Math.random() * (props.maxLength - props.minLength) + props.minLength,
props.segmentCount,
Math.random() * 2 * Math.PI
)
);
}
}
function draw() {
if (mouse.x) {
target.errx = mouse.x - target.x;
target.erry = mouse.y - target.y;
} else {
target.errx = width/2 + (height/2 - q) * Math.sqrt(2) * Math.cos(t) / (Math.pow(Math.sin(t), 2) + 1) - target.x;
target.erry = height/2 + (height/2 - q) * Math.sqrt(2) * Math.cos(t) * Math.sin(t) / (Math.pow(Math.sin(t), 2) + 1) - target.y;
}
target.x += target.errx / 10;
target.y += target.erry / 10;
t += 0.01;
ctx.beginPath();
ctx.arc(target.x, target.y, dist(last_target.x, last_target.y, target.x, target.y) + 5, 0, 2 * Math.PI);
ctx.fillStyle = "hsl(210,100%,80%)";
ctx.fill();
tent.forEach(t => {
t.move(last_target, target);
t.show2(target);
});
tent.forEach(t => {
t.show(target);
});
last_target.x = target.x;
last_target.y = target.y;
}
function animate() {
ctx.clearRect(0, 0, width, height);
draw();
animationId = requestAnimationFrame(animate);
}
onMounted(() => {
const canvasEl = canvas.value;
ctx = canvasEl.getContext('2d');
width = canvasEl.width = window.innerWidth;
height = canvasEl.height = window.innerHeight;
// 设置背景色
document.body.style.backgroundColor = props.backgroundColor;
initTentacles();
animate();
// 事件监听
canvasEl.addEventListener('mousemove', (e) => {
last_mouse.x = mouse.x;
last_mouse.y = mouse.y;
mouse.x = e.pageX - canvasEl.offsetLeft;
mouse.y = e.pageY - canvasEl.offsetTop;
});
canvasEl.addEventListener('mouseleave', () => {
mouse.x = false;
mouse.y = false;
});
window.addEventListener('resize', () => {
width = canvasEl.width = window.innerWidth;
height = canvasEl.height = window.innerHeight;
initTentacles();
});
});
onBeforeUnmount(() => {
if (animationId) {
cancelAnimationFrame(animationId);
}
});
return {
canvas
};
}
};
</script>
<style scoped>
canvas {
display: block;
}
</style>
使用示例:
<template>
<TentacleEffect
:tentacleCount="400"
:maxLength="250"
:minLength="40"
:segmentCount="25"
backgroundColor="rgb(20,20,20)"
:baseHue="200"
/>
</template>
<script>
import TentacleEffect from './components/TentacleEffect.vue'
export default {
components: {
TentacleEffect
}
}
</script>
参数说明:
tentacleCount
: 触手的数量maxLength
: 触手最大长度minLength
: 触手最小长度segmentCount
: 每个触手的段数backgroundColor
: 背景颜色baseHue
: 触手的基础色调值(0-360)
这个组件提供了良好的封装和可配置性,你可以通过调整这些参数来实现不同的视觉效果。触手会对鼠标移动做出反应,当鼠标离开时会自动进行波浪运动。
注意事项:
性能考虑:触手数量会影响性能,建议根据实际情况调整
响应式:组件会自动适应窗口大小变化
清理:组件会在卸载时自动清理动画帧
评论
匿名评论
隐私政策
你无需删除空行,直接评论以获取最佳展示效果