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