diff --git a/blog.config.js b/blog.config.js
index 70289e20..7b94df71 100644
--- a/blog.config.js
+++ b/blog.config.js
@@ -49,6 +49,9 @@ const BLOG = {
CONTACT_GITHUB: 'https://github.com/tangly1024',
CONTACT_TELEGRAM: '',
+ // 鼠标点击烟花特效
+ FIREWORKS: process.env.NEXT_PUBLIC_FIREWORKS || true, // 鼠标点击烟花特效
+
// 悬浮挂件
WIDGET_PET: process.env.NEXT_PUBLIC_WIDGET_PET || true, // 是否显示宠物挂件
WIDGET_PET_LINK:
diff --git a/components/Fireworks.js b/components/Fireworks.js
new file mode 100644
index 00000000..3ccdec7e
--- /dev/null
+++ b/components/Fireworks.js
@@ -0,0 +1,209 @@
+/**
+ * https://codepen.io/juliangarnier/pen/gmOwJX
+ * custom by hexo-theme-yun @YunYouJun
+ */
+import React from 'react'
+import anime from 'animejs'
+
+export const Fireworks = () => {
+ React.useEffect(() => {
+ createFireworks({})
+ }, [])
+ return
+}
+
+/**
+ * 创建烟花
+ * @param config
+ */
+function createFireworks(config) {
+ const defaultColors = ['102, 167, 221', '62, 131, 225', '33, 78, 194']
+ const defaultConfig = {
+ colors: defaultColors,
+ numberOfParticules: 20,
+ orbitRadius: {
+ min: 50,
+ max: 100
+ },
+ circleRadius: {
+ min: 10,
+ max: 20
+ },
+ diffuseRadius: {
+ min: 50,
+ max: 100
+ },
+ animeDuration: {
+ min: 900,
+ max: 1500
+ }
+ }
+ config = Object.assign(defaultConfig, config)
+
+ let pointerX = 0
+ let pointerY = 0
+
+ // sky blue
+ const colors = config.colors || defaultColors
+
+ const canvasEl = document.querySelector('.fireworks')
+ const ctx = canvasEl.getContext('2d')
+
+ /**
+ * 设置画布尺寸
+ */
+ function setCanvasSize(canvasEl) {
+ canvasEl.width = window.innerWidth
+ canvasEl.height = window.innerHeight
+ canvasEl.style.width = `${window.innerWidth}px`
+ canvasEl.style.height = `${window.innerHeight}px`
+ }
+
+ /**
+ * update pointer
+ * @param {TouchEvent} e
+ */
+ function updateCoords(e) {
+ pointerX =
+ e.clientX ||
+ (e.touches[0] ? e.touches[0].clientX : e.changedTouches[0].clientX)
+ pointerY =
+ e.clientY ||
+ (e.touches[0] ? e.touches[0].clientY : e.changedTouches[0].clientY)
+ }
+
+ function setParticuleDirection(p) {
+ const angle = (anime.random(0, 360) * Math.PI) / 180
+ const value = anime.random(
+ config.diffuseRadius.min,
+ config.diffuseRadius.max
+ )
+ const radius = [-1, 1][anime.random(0, 1)] * value
+ return {
+ x: p.x + radius * Math.cos(angle),
+ y: p.y + radius * Math.sin(angle)
+ }
+ }
+
+ /**
+ * 在指定位置创建粒子
+ * @param {number} x
+ * @param {number} y
+ * @returns
+ */
+ function createParticule(x, y) {
+ const p = {
+ x,
+ y,
+ color: `rgba(${
+ colors[anime.random(0, colors.length - 1)]
+ },${
+ anime.random(0.2, 0.8)
+ })`,
+ radius: anime.random(config.circleRadius.min, config.circleRadius.max),
+ endPos: null,
+ draw() {}
+ }
+ p.endPos = setParticuleDirection(p)
+ p.draw = function() {
+ ctx.beginPath()
+ ctx.arc(p.x, p.y, p.radius, 0, 2 * Math.PI, true)
+ ctx.fillStyle = p.color
+ ctx.fill()
+ }
+ return p
+ }
+
+ function createCircle(x, y) {
+ const p = {
+ x,
+ y,
+ color: '#000',
+ radius: 0.1,
+ alpha: 0.5,
+ lineWidth: 6,
+ draw() {}
+ }
+ p.draw = function() {
+ ctx.globalAlpha = p.alpha
+ ctx.beginPath()
+ ctx.arc(p.x, p.y, p.radius, 0, 2 * Math.PI, true)
+ ctx.lineWidth = p.lineWidth
+ ctx.strokeStyle = p.color
+ ctx.stroke()
+ ctx.globalAlpha = 1
+ }
+ return p
+ }
+
+ function renderParticule(anim) {
+ for (let i = 0; i < anim.animatables.length; i++) { anim.animatables[i].target.draw() }
+ }
+
+ function animateParticules(x, y) {
+ const circle = createCircle(x, y)
+ const particules = []
+ for (let i = 0; i < config.numberOfParticules; i++) { particules.push(createParticule(x, y)) }
+
+ anime
+ .timeline()
+ .add({
+ targets: particules,
+ x(p) {
+ return p.endPos.x
+ },
+ y(p) {
+ return p.endPos.y
+ },
+ radius: 0.1,
+ duration: anime.random(
+ config.animeDuration.min,
+ config.animeDuration.max
+ ),
+ easing: 'easeOutExpo',
+ update: renderParticule
+ })
+ .add(
+ {
+ targets: circle,
+ radius: anime.random(config.orbitRadius.min, config.orbitRadius.max),
+ lineWidth: 0,
+ alpha: {
+ value: 0,
+ easing: 'linear',
+ duration: anime.random(600, 800)
+ },
+ duration: anime.random(1200, 1800),
+ easing: 'easeOutExpo',
+ update: renderParticule
+ },
+ 0
+ )
+ }
+
+ const render = anime({
+ duration: Infinity,
+ update: () => {
+ ctx.clearRect(0, 0, canvasEl.width, canvasEl.height)
+ }
+ })
+
+ document.addEventListener(
+ 'mousedown',
+ (e) => {
+ render.play()
+ updateCoords(e)
+ animateParticules(pointerX, pointerY)
+ },
+ false
+ )
+
+ setCanvasSize(canvasEl)
+ window.addEventListener(
+ 'resize',
+ () => {
+ setCanvasSize(canvasEl)
+ },
+ false
+ )
+}
diff --git a/package.json b/package.json
index 301e6bc9..99bb3f68 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"@next/bundle-analyzer": "^12.1.1",
"@popperjs/core": "^2.9.3",
"animate.css": "^4.1.1",
+ "animejs": "^3.2.1",
"axios": ">=0.21.1",
"copy-to-clipboard": "^3.3.1",
"feed": "^4.2.2",
diff --git a/pages/_app.js b/pages/_app.js
index ef7b1408..3554446b 100644
--- a/pages/_app.js
+++ b/pages/_app.js
@@ -15,6 +15,7 @@ import dynamic from 'next/dynamic'
import { GlobalContextProvider } from '@/lib/global'
import { DebugPanel } from '@/components/DebugPanel'
import { ThemeSwitch } from '@/components/ThemeSwitch'
+import { Fireworks } from '@/components/Fireworks'
const Ackee = dynamic(() => import('@/components/Ackee'), { ssr: false })
const Gtag = dynamic(() => import('@/components/Gtag'), { ssr: false })
@@ -36,13 +37,14 @@ const MyApp = ({ Component, pageProps }) => {
{JSON.parse(BLOG.ANALYTICS_BUSUANZI_ENABLE) && }
{BLOG.ADSENSE_GOOGLE_ID && }
{BLOG.FACEBOOK_APP_ID && BLOG.FACEBOOK_PAGE_ID && }
+ {JSON.parse(BLOG.FIREWORKS) && }
>
return (
- {externalPlugins}
{/* FontawesomeCDN */}
+ {externalPlugins}
)
diff --git a/styles/globals.css b/styles/globals.css
index 1df15802..fafd097d 100644
--- a/styles/globals.css
+++ b/styles/globals.css
@@ -145,4 +145,12 @@ nav {
.notion-code-copy-button > svg{
pointer-events:none
+}
+
+.fireworks{
+ position: fixed;
+ left: 0;
+ top: 0;
+ z-index: 11;
+ pointer-events: none;
}
\ No newline at end of file