当前仓库属于暂停状态,部分功能使用受限,详情请查阅 仓库状态说明
33 Star 370 Fork 65

霍啸林/vue-particle-effect
暂停

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
克隆/下载
particle-effect.vue 14.82 KB
一键复制 编辑 原始数据 按行查看 历史
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
<script>
const VALID_DIRECTION = new Set(['left', 'right', 'top', 'bottom'])
const VALID_PARTICLE_TYPE = new Set(['circle', 'rectangle', 'triangle'])
const VALID_PARTICLE_STYLE = new Set(['fill', 'stroke'])
const DEFAULT_SIZE_FUNC = () => Math.floor(Math.random() * 3 + 1)
const DEFAULT_SPEED_FUNC = () => rand(4)
const bezier = (() => {
const kSplineTableSize = 11
const kSampleStepSize = 1.0 / (kSplineTableSize - 1.0)
function A(aA1, aA2) {
return 1.0 - 3.0 * aA2 + 3.0 * aA1
}
function B(aA1, aA2) {
return 3.0 * aA2 - 6.0 * aA1
}
function C(aA1) {
return 3.0 * aA1
}
function calcBezier(aT, aA1, aA2) {
return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT
}
function getSlope(aT, aA1, aA2) {
return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1)
}
function binarySubdivide(aX, aA, aB, mX1, mX2) {
let currentX
let currentT
let i = 0
do {
currentT = aA + (aB - aA) / 2.0
currentX = calcBezier(currentT, mX1, mX2) - aX
if (currentX > 0.0) {
aB = currentT
} else {
aA = currentT
}
} while (Math.abs(currentX) > 0.0000001 && ++i < 10)
return currentT
}
function newtonRaphsonIterate(aX, aGuessT, mX1, mX2) {
for (let i = 0; i < 4; ++i) {
const currentSlope = getSlope(aGuessT, mX1, mX2)
if (currentSlope === 0.0) return aGuessT
const currentX = calcBezier(aGuessT, mX1, mX2) - aX
aGuessT -= currentX / currentSlope
}
return aGuessT
}
function bezier(mX1, mY1, mX2, mY2) {
if (!(mX1 >= 0 && mX1 <= 1 && mX2 >= 0 && mX2 <= 1)) {
return x => x
}
let sampleValues = new Float32Array(kSplineTableSize)
if (mX1 !== mY1 || mX2 !== mY2) {
for (let i = 0; i < kSplineTableSize; ++i) {
sampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2)
}
}
function getTForX(aX) {
let intervalStart = 0.0
let currentSample = 1
const lastSample = kSplineTableSize - 1
for (
;
currentSample !== lastSample && sampleValues[currentSample] <= aX;
++currentSample
) {
intervalStart += kSampleStepSize
}
--currentSample
const dist =
(aX - sampleValues[currentSample]) /
(sampleValues[currentSample + 1] - sampleValues[currentSample])
const guessForT = intervalStart + dist * kSampleStepSize
const initialSlope = getSlope(guessForT, mX1, mX2)
if (initialSlope >= 0.001) {
return newtonRaphsonIterate(aX, guessForT, mX1, mX2)
} else if (initialSlope === 0.0) {
return guessForT
} else {
return binarySubdivide(
aX,
intervalStart,
intervalStart + kSampleStepSize,
mX1,
mX2
)
}
}
return x => {
if (mX1 === mY1 && mX2 === mY2) return x
if (x === 0) return 0
if (x === 1) return 1
return calcBezier(getTForX(x), mY1, mY2)
}
}
return bezier
})()
const easings = (() => {
const names = [
'Quad',
'Cubic',
'Quart',
'Quint',
'Sine',
'Expo',
'Circ',
'Back',
]
const equations = {
In: [
[0.55, 0.085, 0.68, 0.53] /* InQuad */,
[0.55, 0.055, 0.675, 0.19] /* InCubic */,
[0.895, 0.03, 0.685, 0.22] /* InQuart */,
[0.755, 0.05, 0.855, 0.06] /* InQuint */,
[0.47, 0.0, 0.745, 0.715] /* InSine */,
[0.95, 0.05, 0.795, 0.035] /* InExpo */,
[0.6, 0.04, 0.98, 0.335] /* InCirc */,
[0.6, -0.28, 0.735, 0.045] /* InBack */,
],
Out: [
[0.25, 0.46, 0.45, 0.94] /* OutQuad */,
[0.215, 0.61, 0.355, 1.0] /* OutCubic */,
[0.165, 0.84, 0.44, 1.0] /* OutQuart */,
[0.23, 1.0, 0.32, 1.0] /* OutQuint */,
[0.39, 0.575, 0.565, 1.0] /* OutSine */,
[0.19, 1.0, 0.22, 1.0] /* OutExpo */,
[0.075, 0.82, 0.165, 1.0] /* OutCirc */,
[0.175, 0.885, 0.32, 1.275] /* OutBack */,
],
InOut: [
[0.455, 0.03, 0.515, 0.955] /* InOutQuad */,
[0.645, 0.045, 0.355, 1.0] /* InOutCubic */,
[0.77, 0.0, 0.175, 1.0] /* InOutQuart */,
[0.86, 0.0, 0.07, 1.0] /* InOutQuint */,
[0.445, 0.05, 0.55, 0.95] /* InOutSine */,
[1.0, 0.0, 0.0, 1.0] /* InOutExpo */,
[0.785, 0.135, 0.15, 0.86] /* InOutCirc */,
[0.68, -0.55, 0.265, 1.55] /* InOutBack */,
],
}
const functions = {
linear: bezier(0.25, 0.25, 0.75, 0.75),
}
for (let type in equations) {
equations[type].forEach((f, i) => {
functions['ease' + type + names[i]] = bezier.apply(this, f)
})
}
return functions
})()
function rand(value) {
return Math.random() * value - value / 2
}
function isFunc(value) {
return typeof value === 'function'
}
const me = new WeakMap()
export default {
props: {
hidden: {
type: Boolean,
default: false,
},
direction: {
type: String,
default: 'left',
validator(value) {
return VALID_DIRECTION.has(value)
},
},
particleType: {
type: String,
default: 'circle',
validator(value) {
return VALID_PARTICLE_TYPE.has(value)
},
},
particleStyle: {
type: String,
default: 'fill',
validator(value) {
return VALID_PARTICLE_STYLE.has(value)
},
},
particleColor: {
type: String,
default: '#000',
},
duration: {
type: Number,
default: 1000,
},
easing: {
type: [String, Array],
default: 'easeInOutCubic',
validator(value) {
if (Array.isArray(value)) {
return value.length === 4 && value.every(Number.isFinite)
} else {
return value != null
}
},
},
canvasPadding: {
type: Number,
default: 150,
},
size: {
type: [Number, Function],
default: () => DEFAULT_SIZE_FUNC,
},
speed: {
type: [Number, Function],
default: () => DEFAULT_SPEED_FUNC,
},
particlesAmountCoefficient: {
type: Number,
default: 3,
},
oscillationCoefficient: {
type: Number,
default: 20,
},
},
data() {
return {
status: this.hidden ? 'hidden' : 'normal',
progress: this.hidden ? 0 : 1,
rect: {
width: 0,
height: 0,
},
}
},
computed: {
canvasStyles() {
const style = {}
if (this.status === 'hidden' || this.status === 'normal') {
style.visibility = 'hidden'
}
return style
},
wrapperStyles() {
const style = {}
const prop = this.transformStyleProp
const px = this.transformStylePx
if (this.status === 'hiding' || this.status === 'showing') {
style.transform = `${prop}(${px}px)`
} else if (this.status === 'hidden') {
style.visibility = 'hidden'
}
return style
},
contentStyles() {
const style = {}
const prop = this.transformStyleProp
const px = -this.transformStylePx
if (this.status === 'hiding' || this.status === 'showing') {
style.transform = `${prop}(${px}px)`
}
return style
},
isHorizontal() {
return this.direction === 'left' || this.direction === 'right'
},
transformStyleProp() {
return this.isHorizontal ? 'translateX' : 'translateY'
},
transformStylePx() {
const size = this.isHorizontal ? this.rect.width : this.rect.height
const value =
this.direction === 'left' || this.direction === 'top'
? this.progress
: -this.progress
return this.direction === 'left' || this.direction === 'top'
? Math.ceil(size * value)
: Math.floor(size * value)
},
},
watch: {
hidden(newValue) {
if (this.status === 'normal' && newValue) {
this.status = 'hiding'
this.$nextTick(this.startAnimation)
} else if (this.status === 'hidden' && !newValue) {
this.status = 'showing'
this.$nextTick(this.startAnimation)
}
},
},
created() {
me.set(this, {
particles: [],
lastProgress: 0,
inAnimation: false,
beginTimeStamp: 0,
endTimeStamp: 0,
easing: easings.linear,
})
},
updated() {
window.requestAnimationFrame(this.afterDomUpdate)
},
methods: {
afterDomUpdate(now) {
if (now < me.get(this).endTimeStamp) {
this.doAnimation(now)
return
}
switch (this.status) {
case 'hiding':
this.progress = 1
break
case 'showing':
this.progress = 0
break
default:
break
}
},
startAnimation() {
if (!this.$refs) return
if (!this.$refs.canvasRef) return
if (!this.$refs.wrapperRef) return
const data = me.get(this)
if (this.status === 'hiding') {
data.lastProgress = 0
this.progress = 1
} else {
data.lastProgress = 1
this.progress = 0
}
if (Array.isArray(this.easing)) {
data.easing = bezier.apply(this, this.easing)
} else {
data.easing = easings[this.easing] || easings.linear
}
data.beginTimeStamp = window.performance.now()
data.endTimeStamp = data.beginTimeStamp + this.duration
data.particles = []
const wrapperRect = this.$refs.wrapperRef.getBoundingClientRect()
this.$refs.canvasRef.width = wrapperRect.width + this.canvasPadding * 2
this.$refs.canvasRef.height = wrapperRect.height + this.canvasPadding * 2
this.rect.width = wrapperRect.width
this.rect.height = wrapperRect.height
this.$emit('begin')
this.$nextTick(() => this.doAnimation(data.beginTimeStamp))
},
doAnimation(timestamp) {
const { beginTimeStamp, endTimeStamp, easing } = me.get(this)
const now = Math.min(timestamp, endTimeStamp)
let p = (now - beginTimeStamp) / this.duration
if (this.status === 'showing') {
p = 1 - p
}
this.progress = easing(p)
if (this.duration) {
this.addParticles(p)
}
},
loop() {
this.updateParticles()
this.renderParticles()
const data = me.get(this)
if (data.particles.length) {
data.inAnimation = true
window.requestAnimationFrame(this.loop)
} else {
data.inAnimation = false
this.cycleStatus()
this.$emit('complete')
}
},
addParticles(progress) {
const { width, height } = this.rect
const { lastProgress, inAnimation } = me.get(this)
const delta =
this.status === 'hiding'
? progress - lastProgress
: lastProgress - progress
const progressValue =
(this.isHorizontal ? width : height) * progress +
delta * (this.status === 'hiding' ? 1000 : 100)
me.get(this).lastProgress = progress
let x = this.canvasPadding
let y = this.canvasPadding
if (this.isHorizontal) {
x += this.direction === 'left' ? progressValue : width - progressValue
} else {
y += this.direction === 'top' ? progressValue : height - progressValue
}
let i = Math.floor(this.particlesAmountCoefficient * (delta * 100 + 1))
if (i > 0) {
while (i--) {
this.addParticle(
x + (this.isHorizontal ? 0 : width * Math.random()),
y + (this.isHorizontal ? height * Math.random() : 0)
)
}
}
if (!inAnimation) {
window.requestAnimationFrame(this.loop)
me.get(this).inAnimation = true
}
},
addParticle(startX, startY) {
const frames = this.duration * 60 / 1000
const speed = isFunc(this.speed) ? this.speed() : this.speed
const size = isFunc(this.size) ? this.size() : this.size
me.get(this).particles.push({
startX,
startY,
x: this.status === 'hiding' ? 0 : speed * -frames,
y: 0,
angle: rand(360),
counter: this.status === 'hiding' ? 0 : frames,
increase: Math.PI * 2 / 100,
life: 0,
death:
this.status === 'hiding' ? frames - 20 + Math.random() * 40 : frames,
speed,
size,
})
},
updateParticles() {
const { particles } = me.get(this)
for (let i = 0; i < particles.length; i++) {
const p = particles[i]
if (p.life > p.death) {
particles.splice(i, 1)
} else {
p.x += p.speed
p.y = this.oscillationCoefficient * Math.sin(p.counter * p.increase)
p.life++
p.counter += this.status === 'hiding' ? 1 : -1
}
}
},
renderParticles() {
const canvas = this.$refs.canvasRef
const ctx = canvas.getContext('2d')
const { particles } = me.get(this)
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = ctx.strokeStyle = this.particleColor
for (let i = 0; i < particles.length; ++i) {
const p = particles[i]
if (p.life < p.death) {
ctx.translate(p.startX, p.startY)
ctx.rotate(p.angle * Math.PI / 180)
ctx.globalAlpha =
this.status === 'hiding' ? 1 - p.life / p.death : p.life / p.death
ctx.beginPath()
if (this.particleType === 'circle') {
ctx.arc(p.x, p.y, p.size, 0, 2 * Math.PI)
} else if (this.particleType === 'triangle') {
ctx.moveTo(p.x, p.y)
ctx.lineTo(p.x + p.size, p.y + p.size)
ctx.lineTo(p.x + p.size, p.y - p.size)
} else if (this.particleType === 'rectangle') {
ctx.rect(p.x, p.y, p.size, p.size)
}
if (this.particleStyle === 'fill') {
ctx.fill()
} else if (this.particleStyle === 'stroke') {
ctx.closePath()
ctx.stroke()
}
ctx.globalAlpha = 1
ctx.rotate(-p.angle * Math.PI / 180)
ctx.translate(-p.startX, -p.startY)
}
}
},
cycleStatus() {
switch (this.status) {
case 'normal':
this.status = 'hiding'
break
case 'hidden':
this.status = 'showing'
break
case 'hiding':
this.status = 'hidden'
break
case 'showing':
this.status = 'normal'
break
default:
break
}
},
},
}
</script>
<template>
<div :class="$style.particles">
<div
ref="wrapperRef"
:style="wrapperStyles"
:class="$style.wrapper"
>
<div
:style="contentStyles"
:class="$style.content"
>
<slot />
</div>
</div>
<canvas
ref="canvasRef"
:style="canvasStyles"
:class="$style.canvas"
/>
</div>
</template>
<style module>
.particles {
position: relative;
display: inline-block;
}
.wrapper {
position: relative;
display: inline-block;
overflow: hidden;
}
.content:focus,
.content > *:focus {
outline: none;
}
.canvas {
position: absolute;
top: 50%;
left: 50%;
pointer-events: none;
transform: translate3d(-50%, -50%, 0);
}
</style>
Loading...
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
JavaScript
1
https://gitee.com/pxp/vue-particle-effect.git
git@gitee.com:pxp/vue-particle-effect.git
pxp
vue-particle-effect
vue-particle-effect
main

搜索帮助