diff --git a/.eslintrc.cjs b/.eslintrc.cjs index fdefba2..bd0d5d7 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -26,5 +26,6 @@ module.exports = { 'prettier/prettier': 'error', '@typescript-eslint/no-explicit-any': 0, '@typescript-eslint/ban-ts-comment': 0, + '@typescript-eslint/no-non-null-assertion': 0, }, }; diff --git a/index.html b/index.html index 3a6d14a..0699d18 100644 --- a/index.html +++ b/index.html @@ -19,18 +19,185 @@ s.parentNode.insertBefore(hm, s); })(); + -
+
+ +
+
+
+
+
+
+
+ 加载中...
+ + 稍后再试或 + 返回首页 + +
+
+
- diff --git a/public/wxqrcode.png b/public/wxqrcode.png new file mode 100644 index 0000000..c696f9a Binary files /dev/null and b/public/wxqrcode.png differ diff --git a/src/App.scss b/src/App.scss index c78dff5..8049876 100644 --- a/src/App.scss +++ b/src/App.scss @@ -3,99 +3,11 @@ width: 100%; max-width: 500px; margin: 0 auto; -} + position: relative; -.app { - width: 100%; - margin: 0 auto; -} - -.scene { - &-container { - width: 100%; - padding-bottom: 100%; - position: relative; - margin: 10% 0; + >:not(.background) { + z-index: 1; } - - &-inner { - position: absolute; - left: 0; - right: 0; - bottom: 0; - top: 0; - overflow: visible; - font-size: 28px; - } -} - -.symbol { - width: 12.5%; - padding-bottom: 12.5%; - position: absolute; - transition: 150ms; - left: 0; - top: 0; - border-radius: 8px; - - &-inner { - position: absolute; - left: 0; - right: 0; - bottom: 0; - top: 0; - display: flex; - justify-content: center; - align-items: center; - border-radius: 8px; - border: 2px solid #444; - transition: 0.3s; - overflow: hidden; - user-select: none; - - img { - width: 100%; - height: 100%; - object-fit: cover; - -webkit-user-drag: none; - } - } -} - -.queue-container { - border-radius: 8px; - width: 100%; - padding-bottom: 15%; - border: 2px solid gray; - margin-bottom: 16px; -} - -.modal { - position: fixed; - width: 100vw; - height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - backdrop-filter: blur(10px); - background-color: rgb(0 0 0 / 10%); - top: 0; - left: 0; -} - -.bgm-button { - position: fixed; - left: 0; - top: 0; - padding: 4px; - width: 36px; - height: 36px; -} - -.zhenghuo-button { - width: 100%; - margin-top: 8px; } .background { @@ -105,5 +17,5 @@ width: 100vw; height: 100vh; object-fit: cover; - z-index: -1; + z-index: 0; } diff --git a/src/App.tsx b/src/App.tsx index 65dced5..b24afee 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,649 +1,99 @@ -import React, { - FC, - MouseEventHandler, - useEffect, - useRef, - useState, -} from 'react'; - +import React, { FC, useEffect, useState } from 'react'; import './App.scss'; -import { BilibiliLink, PersonalInfo } from './components/PersonalInfo'; import { - parsePathCustomThemeId, - parsePathThemeName, - randomString, - waitTimeout, + domRelatedOptForTheme, + LAST_LEVEL_STORAGE_KEY, + LAST_SCORE_STORAGE_KEY, + wrapThemeDefaultSounds, } from './utils'; -import { defaultTheme } from './themes/default'; -import { Icon, Theme } from './themes/interface'; -import { fishermanTheme } from './themes/fisherman'; -import { jinlunTheme } from './themes/jinlun'; -import { ikunTheme } from './themes/ikun'; -import { pddTheme } from './themes/pdd'; +import { Theme } from './themes/interface'; +import Game from './components/Game'; import { BeiAn } from './components/BeiAn'; +import { Title } from './components/Title'; +import { PersonalInfo } from './components/PersonalInfo'; import { Info } from './components/Info'; -import { owTheme } from './themes/ow'; +import { ThemeChanger } from './components/ThemeChanger'; import { ConfigDialog } from './components/ConfigDialog'; -import Bmob from 'hydrogen-js-sdk'; -// 内置主题 -const builtInThemes: Theme[] = [ - defaultTheme, - fishermanTheme, - jinlunTheme, - ikunTheme, - pddTheme, - owTheme, -]; +// 读取缓存关卡得分 +const initLevel = Number(localStorage.getItem(LAST_LEVEL_STORAGE_KEY) || '1'); +const initScore = Number(localStorage.getItem(LAST_SCORE_STORAGE_KEY) || '0'); -// 最大关卡 -const maxLevel = 50; +const App: FC<{ theme: Theme }> = ({ theme: initTheme }) => { + console.log('initTheme', initTheme); + // console.log(JSON.stringify(theme)); -interface MySymbol { - id: string; - status: number; // 0->1->2 - isCover: boolean; - x: number; - y: number; - icon: Icon; -} + const [theme, setTheme] = useState>(initTheme); + const [diyDialogShow, setDiyDialogShow] = useState(false); -type Scene = MySymbol[]; - -// 8*8网格 4*4->8*8 -const makeScene: (level: number, icons: Icon[]) => Scene = (level, icons) => { - const curLevel = Math.min(maxLevel, level); - const iconPool = icons.slice(0, 2 * curLevel); - const offsetPool = [0, 25, -25, 50, -50].slice(0, 1 + curLevel); - - const scene: Scene = []; - - const range = [ - [2, 6], - [1, 6], - [1, 7], - [0, 7], - [0, 8], - ][Math.min(4, curLevel - 1)]; - - const randomSet = (icon: Icon) => { - const offset = - offsetPool[Math.floor(offsetPool.length * Math.random())]; - const row = - range[0] + Math.floor((range[1] - range[0]) * Math.random()); - const column = - range[0] + Math.floor((range[1] - range[0]) * Math.random()); - scene.push({ - isCover: false, - status: 0, - icon, - id: randomString(6), - x: column * 100 + offset, - y: row * 100 + offset, - }); + const changeTheme = (theme: Theme) => { + wrapThemeDefaultSounds(theme); + domRelatedOptForTheme(theme); + setTheme({ ...theme }); }; - // 大于5级别增加icon池 - let compareLevel = curLevel; - while (compareLevel > 0) { - iconPool.push( - ...iconPool.slice(0, Math.min(10, 2 * (compareLevel - 5))) - ); - compareLevel -= 5; - } - - for (const icon of iconPool) { - for (let i = 0; i < 6; i++) { - randomSet(icon); - } - } - - return scene; -}; - -// o(n) 时间复杂度的洗牌算法 -const fastShuffle: (arr: T[]) => T[] = (arr) => { - const res = arr.slice(); - for (let i = 0; i < res.length; i++) { - const idx = (Math.random() * res.length) >> 0; - [res[i], res[idx]] = [res[idx], res[i]]; - } - return res; -}; - -// 洗牌 -const washScene: (level: number, scene: Scene) => Scene = (level, scene) => { - const updateScene = fastShuffle(scene); - const offsetPool = [0, 25, -25, 50, -50].slice(0, 1 + level); - const range = [ - [2, 6], - [1, 6], - [1, 7], - [0, 7], - [0, 8], - ][Math.min(4, level - 1)]; - - const randomSet = (symbol: MySymbol) => { - const offset = - offsetPool[Math.floor(offsetPool.length * Math.random())]; - const row = - range[0] + Math.floor((range[1] - range[0]) * Math.random()); - const column = - range[0] + Math.floor((range[1] - range[0]) * Math.random()); - symbol.x = column * 100 + offset; - symbol.y = row * 100 + offset; - symbol.isCover = false; + const previewTheme = (_theme: Theme) => { + const theme = JSON.parse(JSON.stringify(_theme)); + wrapThemeDefaultSounds(theme); + domRelatedOptForTheme(theme); + setTheme(theme); }; - for (const symbol of updateScene) { - if (symbol.status !== 0) continue; - randomSet(symbol); - } - - return updateScene; -}; - -interface SymbolProps extends MySymbol { - onClick: MouseEventHandler; -} - -const Symbol: FC = ({ x, y, icon, isCover, status, onClick }) => { - return ( -
-
- {typeof icon.content === 'string' ? ( - icon.content.startsWith('http') ? ( - /*图片外链*/ - - ) : ( - /*字符表情*/ - {icon.content} - ) - ) : ( - /*ReactNode*/ - icon.content - )} -
-
- ); -}; - -// 从url初始化主题 -const themeFromPath: string = parsePathThemeName(location.href); -const customThemeIdFromPath = parsePathCustomThemeId(location.href); -const CUSTOM_THEME_FAIL_TIP = '查询配置失败'; - -const App: FC = () => { - const [curTheme, setCurTheme] = useState>( - customThemeIdFromPath - ? { title: '', icons: [], sounds: [], name: '' } - : defaultTheme - ); - const [themes, setThemes] = useState[]>(builtInThemes); - const [pureMode, setPureMode] = useState(!!customThemeIdFromPath); - - const [scene, setScene] = useState(makeScene(1, curTheme.icons)); - const [level, setLevel] = useState(1); - const [queue, setQueue] = useState([]); - const [sortedQueue, setSortedQueue] = useState< - Record - >({}); - const [finished, setFinished] = useState(false); - const [tipText, setTipText] = useState(''); - const [animating, setAnimating] = useState(false); - const [configDialogShow, setConfigDialogShow] = useState(false); - - // 音效 - const soundRefMap = useRef>({}); - - // 第一次点击时播放bgm - const bgmRef = useRef(null); - const [bgmOn, setBgmOn] = useState(false); - const [once, setOnce] = useState(false); + // 生产环境才统计 useEffect(() => { - if (!bgmRef.current) return; - if (bgmOn) { - bgmRef.current.volume = 0.5; - bgmRef.current.play().then(); - } else { - bgmRef.current.pause(); - } - }, [bgmOn]); - - // 初始化主题 - useEffect(() => { - if (customThemeIdFromPath) { - // 自定义主题 - const storageTheme = localStorage.getItem(customThemeIdFromPath); - if (storageTheme) { - // 节省请求 - try { - const customTheme = JSON.parse(storageTheme); - if (!customTheme.pure) { - setPureMode(false); - setThemes([...themes, customTheme]); - } - setCurTheme(customTheme); - } catch (e) { - console.log(e); - } - } else { - Bmob.Query('config') - .get(customThemeIdFromPath) - .then((res) => { - // @ts-ignore - const { content } = res; - localStorage.setItem(customThemeIdFromPath, content); - try { - const customTheme = JSON.parse(content); - if (!customTheme.pure) { - setPureMode(false); - setThemes([...themes, customTheme]); - } - setCurTheme(customTheme); - } catch (e) { - console.log(e); - } - }) - .catch((e) => { - setCurTheme({ - ...curTheme, - title: CUSTOM_THEME_FAIL_TIP, - }); - console.log(e); - }); - } - } else if (themeFromPath) { - // 内置主题 - setCurTheme( - themes.find((theme) => theme.name === themeFromPath) ?? - defaultTheme - ); + console.log(import.meta.env.MODE); + if (import.meta.env.PROD) { + const busuanziScript = document.createElement('script'); + busuanziScript.src = + '//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js'; + document.getElementById('root')?.appendChild(busuanziScript); } }, []); - // 主题切换 - useEffect(() => { - // 初始化时不加载bgm - if (once) { - setBgmOn(false); - setTimeout(() => { - setBgmOn(true); - }, 300); - } - restart(); - // 更改路径query - if (customThemeIdFromPath) return; - history.pushState( - {}, - curTheme.title, - `/?theme=${encodeURIComponent(curTheme.name)}` - ); - }, [curTheme]); - - // 队列区排序 - useEffect(() => { - const cache: Record = {}; - // 加上索引,避免以id字典序来排 - const idx = 0; - for (const symbol of queue) { - if (cache[idx + symbol.icon.name]) { - cache[idx + symbol.icon.name].push(symbol); - } else { - cache[idx + symbol.icon.name] = [symbol]; - } - } - const temp = []; - for (const symbols of Object.values(cache)) { - temp.push(...symbols); - } - const updateSortedQueue: typeof sortedQueue = {}; - let x = 50; - for (const symbol of temp) { - updateSortedQueue[symbol.id] = x; - x += 100; - } - setSortedQueue(updateSortedQueue); - }, [queue]); - - // 初始化覆盖状态 - useEffect(() => { - checkCover(scene); - }, []); - - // 向后检查覆盖 - const checkCover = (scene: Scene) => { - const updateScene = scene.slice(); - for (let i = 0; i < updateScene.length; i++) { - // 当前item对角坐标 - const cur = updateScene[i]; - cur.isCover = false; - if (cur.status !== 0) continue; - const { x: x1, y: y1 } = cur; - const x2 = x1 + 100, - y2 = y1 + 100; - - for (let j = i + 1; j < updateScene.length; j++) { - const compare = updateScene[j]; - if (compare.status !== 0) continue; - - // 两区域有交集视为选中 - // 两区域不重叠情况取反即为交集 - const { x, y } = compare; - - if (!(y + 100 <= y1 || y >= y2 || x + 100 <= x1 || x >= x2)) { - cur.isCover = true; - break; - } - } - } - setScene(updateScene); - }; - - // 弹出 - const pop = () => { - if (!queue.length) return; - const updateQueue = queue.slice(); - const symbol = updateQueue.shift(); - if (!symbol) return; - const find = scene.find((s) => s.id === symbol.id); - if (find) { - setQueue(updateQueue); - find.status = 0; - find.x = 100 * Math.floor(8 * Math.random()); - find.y = 700; - checkCover(scene); - // 音效 - if (soundRefMap.current?.['sound-shift']) { - soundRefMap.current['sound-shift'].currentTime = 0; - soundRefMap.current['sound-shift'].play(); - } - } - }; - - // 撤销 - const undo = () => { - if (!queue.length) return; - const updateQueue = queue.slice(); - const symbol = updateQueue.pop(); - if (!symbol) return; - const find = scene.find((s) => s.id === symbol.id); - if (find) { - setQueue(updateQueue); - find.status = 0; - checkCover(scene); - // 音效 - if (soundRefMap.current?.['sound-undo']) { - soundRefMap.current['sound-undo'].currentTime = 0; - soundRefMap.current['sound-undo'].play(); - } - } - }; - - // 洗牌 - const wash = () => { - checkCover(washScene(level, scene)); - // 音效 - if (soundRefMap.current?.['sound-wash']) { - soundRefMap.current['sound-wash'].currentTime = 0; - soundRefMap.current['sound-wash'].play(); - } - }; - - // 加大难度 - const levelUp = () => { - if (level >= maxLevel) { - return; - } - setFinished(false); - setLevel(level + 1); - setQueue([]); - checkCover(makeScene(level + 1, curTheme.icons)); - }; - - // 重开 - const restart = () => { - setFinished(false); - setLevel(1); - setQueue([]); - checkCover(makeScene(1, curTheme.icons)); - }; - - // 点击item - const clickSymbol = async (idx: number) => { - if (finished || animating) return; - - if (!once) { - setBgmOn(true); - setOnce(true); - } - - const updateScene = scene.slice(); - const symbol = updateScene[idx]; - if (symbol.isCover || symbol.status !== 0) return; - symbol.status = 1; - - // 点击音效 - // 不知道为啥敲可选链会提示错误。。。 - if ( - soundRefMap.current && - soundRefMap.current[symbol.icon.clickSound] - ) { - soundRefMap.current[symbol.icon.clickSound].currentTime = 0; - soundRefMap.current[symbol.icon.clickSound].play().then(); - } - - let updateQueue = queue.slice(); - updateQueue.push(symbol); - - setQueue(updateQueue); - checkCover(updateScene); - - setAnimating(true); - await waitTimeout(150); - - const filterSame = updateQueue.filter((sb) => sb.icon === symbol.icon); - - // 三连了 - if (filterSame.length === 3) { - updateQueue = updateQueue.filter((sb) => sb.icon !== symbol.icon); - for (const sb of filterSame) { - const find = updateScene.find((i) => i.id === sb.id); - if (find) { - find.status = 2; - // 三连音效 - if ( - soundRefMap.current && - soundRefMap.current[symbol.icon.tripleSound] - ) { - soundRefMap.current[ - symbol.icon.tripleSound - ].currentTime = 0; - soundRefMap.current[symbol.icon.tripleSound] - .play() - .then(); - } - } - } - } - - // 输了 - if (updateQueue.length === 7) { - setTipText('失败了'); - setFinished(true); - } - - if (!updateScene.find((s) => s.status !== 2)) { - // 胜利 - if (level === maxLevel) { - setTipText('完成挑战'); - setFinished(true); - return; - } - // 升级 - setLevel(level + 1); - setQueue([]); - checkCover(makeScene(level + 1, curTheme.icons)); - } else { - setQueue(updateQueue); - checkCover(updateScene); - } - - setAnimating(false); - }; - - // 自定义整活 - const customZhenghuo = (theme: Theme) => { - setCurTheme(theme); - }; - return ( <> - {curTheme.background && ( + {theme.background && ( background )} -

- {curTheme.title}{' '} - {curTheme.title === CUSTOM_THEME_FAIL_TIP && ( - 返回首页 - )} -

- - {curTheme.desc} - - {!pureMode && } -

- {!pureMode && ( - <> - 主题: - {/*TODO themes维护方式调整*/} - - - )} - Level: {level} -

- -
-
-
- {scene.map((item, idx) => ( - clickSymbol(idx)} - /> - ))} -
-
-
-
-
- - - - -
- - {!pureMode && ( - - )} - - - - - - {pureMode && } - - {/*提示弹窗*/} - {finished && ( -
-

{tipText}

- -
- )} - - {/*自定义主题弹窗*/} - setConfigDialogShow(false)} - previewMethod={customZhenghuo} + + <PersonalInfo /> + <Game + key={theme.title} + theme={theme} + initLevel={initLevel} + initScore={initScore} /> - - {/*bgm*/} - <button className="bgm-button" onClick={() => setBgmOn(!bgmOn)}> - {bgmOn ? '🔊' : '🔈'} - <audio - ref={bgmRef} - loop - src={ - curTheme?.bgm || - 'https://wj1.kumeiwp.com:912/wj/bl/2022/03/14/48298e7f30fdd8c21f02a3f5ef080134.mp3' - } - /> - </button> - - {/*音效*/} - {curTheme.sounds.map((sound) => ( - <audio - key={sound.name} - ref={(ref) => { - if (ref) soundRefMap.current[sound.name] = ref; - }} - src={sound.src} - /> - ))} + <div className={'flex-spacer'} /> + <p style={{ textAlign: 'center', fontSize: 10, opacity: 0.5 }}> + <span id="busuanzi_container_site_pv"> + {' '} + 累计访问: + <span id="busuanzi_value_site_pv" />次 + </span> + <br /> + <BeiAn /> + </p> + <Info /> + {!theme.pure && ( + <> + <ThemeChanger + changeTheme={changeTheme} + onDiyClick={() => setDiyDialogShow(true)} + /> + <ConfigDialog + show={diyDialogShow} + closeMethod={() => setDiyDialogShow(false)} + previewMethod={previewTheme} + /> + </> + )} </> ); }; diff --git a/src/components/BeiAn.tsx b/src/components/BeiAn.tsx index 43f9309..a59f660 100644 --- a/src/components/BeiAn.tsx +++ b/src/components/BeiAn.tsx @@ -2,14 +2,12 @@ import React, { FC } from 'react'; export const BeiAn: FC = () => { return ( - <p style={{ textAlign: 'center' }}> - <a - href="https://beian.miit.gov.cn/" - target="_blank" - rel="noopener noreferrer nofollow" - > - 浙ICP备17007857号-2 - </a> - </p> + <a + href="https://beian.miit.gov.cn/" + target="_blank" + rel="noopener noreferrer nofollow" + > + 浙ICP备17007857号-2 + </a> ); }; diff --git a/src/components/ConfigDialog.tsx b/src/components/ConfigDialog.tsx index 806f78d..cca9525 100644 --- a/src/components/ConfigDialog.tsx +++ b/src/components/ConfigDialog.tsx @@ -2,7 +2,6 @@ import React, { FC, useEffect, useRef, useState } from 'react'; import style from './ConfigDialog.module.scss'; import classNames from 'classnames'; import { Icon, Sound, Theme } from '../themes/interface'; -import { defaultSounds } from '../themes/default'; import { QRCodeCanvas } from 'qrcode.react'; import Bmob from 'hydrogen-js-sdk'; import { captureElement } from '../utils'; @@ -226,25 +225,7 @@ export const ConfigDialog: FC<{ if (!title) return Promise.reject('请填写标题'); if (icons.length !== 10) return Promise.reject('图片素材需要提供10张'); - let hasDefaultMaterial = false; - const customIcons = icons.map((icon) => { - if (!icon.clickSound) { - hasDefaultMaterial = true; - icon.clickSound = 'button-click'; - } - if (!icon.tripleSound) { - hasDefaultMaterial = true; - icon.tripleSound = 'triple'; - } - return { ...icon }; - }); - const customSounds = sounds.map((sounds) => ({ ...sounds })); - if (hasDefaultMaterial) { - customSounds.push(...defaultSounds); - } - const customTheme: Theme<any> = { - name: `自定义-${title}`, // 恭喜你发现纯净模式彩蛋🎉,点击文字十次可以开启纯净模式 pure: pureCount !== 0 && pureCount % 10 === 0, title, @@ -252,11 +233,13 @@ export const ConfigDialog: FC<{ bgm, background, backgroundBlur, - icons: customIcons, - sounds: customSounds, + icons, + sounds, }; - return Promise.resolve(customTheme); + console.log(customTheme); + + return Promise.resolve(JSON.parse(JSON.stringify(customTheme))); }; // 预览 @@ -335,11 +318,24 @@ export const ConfigDialog: FC<{ > <p onClick={() => setPureCount(pureCount + 1)}> 目前自定义仅支持配置https链接,可网上自行搜索素材复制链接,或者将自己处理好的素材上传第三方存储服务/图床上再复制外链 - (想白嫖的话自行搜索【免费图床】【免费对象存储】【免费mp3外链】等) + (想白嫖的话自行搜索【免费图床】【免费对象存储】【免费mp3外链】等)。 {pureCount != 0 && pureCount % 10 === 0 && '🎉🎉🎉恭喜发现彩蛋!主题分享后将开启纯净模式~'} </p> + <div className="flex-container flex-no-wrap"> + <img + style={{ width: 120, objectFit: 'contain' }} + src="/wxqrcode.png" + alt="" + /> + <p style={{ margin: 0 }}> + <strong> + 开发不易,如果您喜欢这个项目的话可酌情扫左侧二维码 + 请我喝杯咖啡(后台相关费用用爱发电中,感谢支持) + </strong> + </p> + </div> {/*基本配置*/} <h4 className="flex-container flex-center"> diff --git a/src/components/Game.scss b/src/components/Game.scss new file mode 100644 index 0000000..a7666b1 --- /dev/null +++ b/src/components/Game.scss @@ -0,0 +1,112 @@ +.game { + width: 100%; + margin: 0 auto; + padding-top: 10%; + padding-bottom: 2.5%; +} + +.scene { + &-container { + width: 100%; + padding-bottom: 112.5%; + position: relative; + } + + &-inner { + position: absolute; + left: 0; + right: 0; + bottom: 0; + top: 0; + overflow: visible; + font-size: 28px; + } +} + +.symbol { + width: 12.5%; + padding-bottom: 12.5%; + position: absolute; + transition: 150ms; + left: 0; + top: 0; + border-radius: 8px; + + &-inner { + position: absolute; + left: 0; + right: 0; + bottom: 0; + top: 0; + display: flex; + justify-content: center; + align-items: center; + border-radius: 8px; + border: 2px solid #444; + transition: 0.3s; + overflow: hidden; + user-select: none; + + img { + width: 100%; + height: 100%; + object-fit: cover; + -webkit-user-drag: none; + } + } +} + +.queue-container { + padding-bottom: 18.75%; + margin-bottom: 16px; + position: relative; + + &::after { + content: ''; + position: absolute; + left: 3.125%; + right: 3.125%; + top: 0; + bottom: 0; + border-radius: 12px; + background-color: rgb(0 0 0 / 16%); + border: 8px solid rgb(0 0 0 / 8%); + } +} + +.modal { + position: fixed; + width: 100vw; + height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + backdrop-filter: blur(10px); + background-color: rgb(0 0 0 / 10%); + top: 0; + left: 0; + z-index: 10 !important; +} + +.bgm-button { + position: fixed; + left: 8px; + top: 8px; + padding: 4px; + width: 36px; + height: 36px; + cursor: pointer; +} + +.zhenghuo-button { + width: 100%; + margin-top: 8px; +} + +.level { + font-size: 1.8em; + font-weight: 900; + line-height: 2; + text-shadow: 4px 6px 2px rgb(0 0 0 / 20%); +} diff --git a/src/components/Game.tsx b/src/components/Game.tsx new file mode 100644 index 0000000..9fb8a20 --- /dev/null +++ b/src/components/Game.tsx @@ -0,0 +1,504 @@ +import React, { + FC, + MouseEventHandler, + useEffect, + useRef, + useState, +} from 'react'; + +import './Game.scss'; +import { + LAST_LEVEL_STORAGE_KEY, + LAST_SCORE_STORAGE_KEY, + randomString, + waitTimeout, +} from '../utils'; +import { Icon, Theme } from '../themes/interface'; + +// 最大关卡 +const maxLevel = 50; + +interface MySymbol { + id: string; + status: number; // 0->1->2 + isCover: boolean; + x: number; + y: number; + icon: Icon; +} + +type Scene = MySymbol[]; + +// 8*8网格 4*4->8*8 +const makeScene: (level: number, icons: Icon[]) => Scene = (level, icons) => { + const curLevel = Math.min(maxLevel, level); + const iconPool = icons.slice(0, 2 * curLevel); + const offsetPool = [0, 25, -25, 50, -50].slice(0, 1 + curLevel); + + const scene: Scene = []; + + const range = [ + [2, 6], + [1, 6], + [1, 7], + [0, 7], + [0, 8], + ][Math.min(4, curLevel - 1)]; + + const randomSet = (icon: Icon) => { + const offset = + offsetPool[Math.floor(offsetPool.length * Math.random())]; + const row = + range[0] + Math.floor((range[1] - range[0]) * Math.random()); + const column = + range[0] + Math.floor((range[1] - range[0]) * Math.random()); + scene.push({ + isCover: false, + status: 0, + icon, + id: randomString(6), + x: column * 100 + offset, + y: row * 100 + offset, + }); + }; + + // 大于5级别增加icon池 + let compareLevel = curLevel; + while (compareLevel > 0) { + iconPool.push( + ...iconPool.slice(0, Math.min(10, 2 * (compareLevel - 5))) + ); + compareLevel -= 5; + } + + for (const icon of iconPool) { + for (let i = 0; i < 6; i++) { + randomSet(icon); + } + } + + return scene; +}; + +// o(n) 时间复杂度的洗牌算法 +const fastShuffle: <T = any>(arr: T[]) => T[] = (arr) => { + const res = arr.slice(); + for (let i = 0; i < res.length; i++) { + const idx = (Math.random() * res.length) >> 0; + [res[i], res[idx]] = [res[idx], res[i]]; + } + return res; +}; + +// 洗牌 +const washScene: (level: number, scene: Scene) => Scene = (level, scene) => { + const updateScene = fastShuffle(scene); + const offsetPool = [0, 25, -25, 50, -50].slice(0, 1 + level); + const range = [ + [2, 6], + [1, 6], + [1, 7], + [0, 7], + [0, 8], + ][Math.min(4, level - 1)]; + + const randomSet = (symbol: MySymbol) => { + const offset = + offsetPool[Math.floor(offsetPool.length * Math.random())]; + const row = + range[0] + Math.floor((range[1] - range[0]) * Math.random()); + const column = + range[0] + Math.floor((range[1] - range[0]) * Math.random()); + symbol.x = column * 100 + offset; + symbol.y = row * 100 + offset; + symbol.isCover = false; + }; + + for (const symbol of updateScene) { + if (symbol.status !== 0) continue; + randomSet(symbol); + } + + return updateScene; +}; + +interface SymbolProps extends MySymbol { + onClick: MouseEventHandler; +} + +const Symbol: FC<SymbolProps> = ({ x, y, icon, isCover, status, onClick }) => { + return ( + <div + className="symbol" + style={{ + transform: `translateX(${x}%) translateY(${y}%)`, + backgroundColor: isCover ? '#999' : 'white', + opacity: status < 2 ? 1 : 0, + }} + onClick={onClick} + > + <div + className="symbol-inner" + style={{ opacity: isCover ? 0.4 : 1 }} + > + {typeof icon.content === 'string' ? ( + icon.content.startsWith('http') ? ( + /*图片外链*/ + <img src={icon.content} alt="" /> + ) : ( + /*字符表情*/ + <i>{icon.content}</i> + ) + ) : ( + /*ReactNode*/ + icon.content + )} + </div> + </div> + ); +}; + +const Game: FC<{ + theme: Theme<any>; + initLevel: number; + initScore: number; +}> = ({ theme, initLevel, initScore }) => { + console.log('Game FC'); + const [scene, setScene] = useState<Scene>( + makeScene(initLevel, theme.icons) + ); + const [level, setLevel] = useState<number>(initLevel); + const [score, setScore] = useState<number>(initScore); + const [queue, setQueue] = useState<MySymbol[]>([]); + const [sortedQueue, setSortedQueue] = useState< + Record<MySymbol['id'], number> + >({}); + const [finished, setFinished] = useState<boolean>(false); + const [tipText, setTipText] = useState<string>(''); + const [animating, setAnimating] = useState<boolean>(false); + + // 音效 + const soundRefMap = useRef<Record<string, HTMLAudioElement>>({}); + + // 第一次点击时播放bgm + const bgmRef = useRef<HTMLAudioElement>(null); + const [bgmOn, setBgmOn] = useState<boolean>(false); + const [once, setOnce] = useState<boolean>(false); + useEffect(() => { + if (!bgmRef.current) return; + if (bgmOn) { + bgmRef.current.volume = 0.5; + bgmRef.current.play().then(); + } else { + bgmRef.current.pause(); + } + }, [bgmOn]); + + // 关卡缓存 + useEffect(() => { + localStorage.setItem(LAST_LEVEL_STORAGE_KEY, level.toString()); + localStorage.setItem(LAST_SCORE_STORAGE_KEY, score.toString()); + }, [level]); + + // 队列区排序 + useEffect(() => { + const cache: Record<string, MySymbol[]> = {}; + // 加上索引,避免以id字典序来排 + const idx = 0; + for (const symbol of queue) { + if (cache[idx + symbol.icon.name]) { + cache[idx + symbol.icon.name].push(symbol); + } else { + cache[idx + symbol.icon.name] = [symbol]; + } + } + const temp = []; + for (const symbols of Object.values(cache)) { + temp.push(...symbols); + } + const updateSortedQueue: typeof sortedQueue = {}; + let x = 50; + for (const symbol of temp) { + updateSortedQueue[symbol.id] = x; + x += 100; + } + setSortedQueue(updateSortedQueue); + }, [queue]); + + // 初始化覆盖状态 + useEffect(() => { + checkCover(scene); + }, []); + + // 向后检查覆盖 + const checkCover = (scene: Scene) => { + const updateScene = scene.slice(); + for (let i = 0; i < updateScene.length; i++) { + // 当前item对角坐标 + const cur = updateScene[i]; + cur.isCover = false; + if (cur.status !== 0) continue; + const { x: x1, y: y1 } = cur; + const x2 = x1 + 100, + y2 = y1 + 100; + + for (let j = i + 1; j < updateScene.length; j++) { + const compare = updateScene[j]; + if (compare.status !== 0) continue; + + // 两区域有交集视为选中 + // 两区域不重叠情况取反即为交集 + const { x, y } = compare; + + if (!(y + 100 <= y1 || y >= y2 || x + 100 <= x1 || x >= x2)) { + cur.isCover = true; + break; + } + } + } + setScene(updateScene); + }; + + // 弹出 + const popTime = useRef(0); + const pop = () => { + if (!queue.length) return; + const updateQueue = queue.slice(); + const symbol = updateQueue.shift(); + setScore(score - 1); + if (!symbol) return; + const find = scene.find((s) => s.id === symbol.id); + if (find) { + setQueue(updateQueue); + find.status = 0; + find.x = 100 * (popTime.current % 7); + popTime.current++; + find.y = 800; + checkCover(scene); + // 音效 + if (soundRefMap.current?.['sound-shift']) { + soundRefMap.current['sound-shift'].currentTime = 0; + soundRefMap.current['sound-shift'].play(); + } + } + }; + + // 撤销 + const undo = () => { + if (!queue.length) return; + setScore(score - 1); + const updateQueue = queue.slice(); + const symbol = updateQueue.pop(); + if (!symbol) return; + const find = scene.find((s) => s.id === symbol.id); + if (find) { + setQueue(updateQueue); + find.status = 0; + checkCover(scene); + // 音效 + if (soundRefMap.current?.['sound-undo']) { + soundRefMap.current['sound-undo'].currentTime = 0; + soundRefMap.current['sound-undo'].play(); + } + } + }; + + // 洗牌 + const wash = () => { + setScore(score - 1); + checkCover(washScene(level, scene)); + // 音效 + if (soundRefMap.current?.['sound-wash']) { + soundRefMap.current['sound-wash'].currentTime = 0; + soundRefMap.current['sound-wash'].play(); + } + }; + + // 加大难度 + const levelUp = () => { + if (level >= maxLevel) { + return; + } + setFinished(false); + setLevel(level + 1); + setQueue([]); + checkCover(makeScene(level + 1, theme.icons)); + }; + + // 重开 + const restart = () => { + setFinished(false); + setScore(0); + setLevel(1); + setQueue([]); + checkCover(makeScene(1, theme.icons)); + }; + + // 点击item + const clickSymbol = async (idx: number) => { + if (finished || animating) return; + + if (!once) { + setBgmOn(true); + setOnce(true); + } + + const updateScene = scene.slice(); + const symbol = updateScene[idx]; + if (symbol.isCover || symbol.status !== 0) return; + symbol.status = 1; + + // 点击音效 + // 不知道为啥敲可选链会提示错误。。。 + if ( + soundRefMap.current && + soundRefMap.current[symbol.icon.clickSound] + ) { + soundRefMap.current[symbol.icon.clickSound].currentTime = 0; + soundRefMap.current[symbol.icon.clickSound].play().then(); + } + + let updateQueue = queue.slice(); + updateQueue.push(symbol); + + setQueue(updateQueue); + checkCover(updateScene); + + setAnimating(true); + await waitTimeout(150); + + const filterSame = updateQueue.filter((sb) => sb.icon === symbol.icon); + + // 三连了 + if (filterSame.length === 3) { + // 三连一次+3分 + setScore(score + 3); + updateQueue = updateQueue.filter((sb) => sb.icon !== symbol.icon); + for (const sb of filterSame) { + const find = updateScene.find((i) => i.id === sb.id); + if (find) { + find.status = 2; + // 三连音效 + if ( + soundRefMap.current && + soundRefMap.current[symbol.icon.tripleSound] + ) { + soundRefMap.current[ + symbol.icon.tripleSound + ].currentTime = 0; + soundRefMap.current[symbol.icon.tripleSound] + .play() + .then(); + } + } + } + } + + // 输了 + if (updateQueue.length === 7) { + setTipText('失败了'); + setFinished(true); + } + + if (!updateScene.find((s) => s.status !== 2)) { + // 胜利 + if (level === maxLevel) { + setTipText('完成挑战'); + setFinished(true); + return; + } + // 升级 + // 通关奖励关卡对应数值分数 + setScore(score + level); + setLevel(level + 1); + setQueue([]); + checkCover(makeScene(level + 1, theme.icons)); + } else { + setQueue(updateQueue); + checkCover(updateScene); + } + + setAnimating(false); + }; + + return ( + <> + <div className="game"> + <div className="scene-container"> + <div className="scene-inner"> + {scene.map((item, idx) => ( + <Symbol + key={item.id} + {...item} + x={ + item.status === 0 + ? item.x + : item.status === 1 + ? sortedQueue[item.id] + : -1000 + } + y={item.status === 0 ? item.y : 945} + onClick={() => clickSymbol(idx)} + /> + ))} + </div> + </div> + </div> + <div className="queue-container" /> + <div className="flex-container flex-between"> + <button className="flex-grow" onClick={pop}> + 弹出 + </button> + <button className="flex-grow" onClick={undo}> + 撤销 + </button> + <button className="flex-grow" onClick={wash}> + 洗牌 + </button> + <button + className="flex-grow" + onClick={() => { + // 跳关扣关卡对应数值的分 + setScore(score - level); + levelUp(); + }} + > + 下一关 + </button> + </div> + <div className="level"> + 关卡{level}|剩余{scene.filter((i) => i.status === 0).length} + |得分{score} + </div> + + {/*提示弹窗*/} + {finished && ( + <div className="modal"> + <h1>{tipText}</h1> + <button onClick={restart}>再来一次</button> + </div> + )} + + {/*bgm*/} + {theme.bgm && ( + <button className="bgm-button" onClick={() => setBgmOn(!bgmOn)}> + {bgmOn ? '🔊' : '🔈'} + <audio ref={bgmRef} loop src={theme.bgm} /> + </button> + )} + + {/*音效*/} + {theme.sounds.map((sound) => ( + <audio + key={sound.name} + ref={(ref) => { + if (ref) soundRefMap.current[sound.name] = ref; + }} + src={sound.src} + /> + ))} + </> + ); +}; + +export default Game; diff --git a/src/components/Info.module.scss b/src/components/Info.module.scss new file mode 100644 index 0000000..00da815 --- /dev/null +++ b/src/components/Info.module.scss @@ -0,0 +1,77 @@ +/* 可封装 */ +.info { + position: fixed; + left: 8px; + bottom: 8px; + transition: 0.3s; + padding: 16px; + width: 36px; + height: 36px; + border-radius: 18px; + background: rgb(0 0 0/ 50%); + color: white; + overflow: hidden; + backdrop-filter: blur(8px); + box-sizing: border-box; + cursor: pointer; + font-size: 14px; + + p { + margin: 0; + opacity: 0; + transition: 0.6s; + } + + a { + color: #747bff; + } + + .icon { + position: absolute; + left: 14px; + top: 2px; + font-size: 24px; + font-weight: 900; + transition: 0.2s; + } + + .close { + position: absolute; + border-radius: 8px; + background-color: rgb(0 0 0/20%); + width: 36px; + height: 36px; + right: 0; + top: 0; + line-height: 36px; + text-align: center; + transform: scale(0); + color: white; + cursor: pointer; + user-select: none; + } + + &.open { + height: 100px; + border-radius: 8px; + + @media screen and (max-width: 500px) { + width: calc(100% - 70px); + } + @media screen and (min-width: 501px) { + width: 500px; + } + + p { + opacity: 1; + } + + .close { + transform: scale(1); + } + + .icon { + transform: scale(0); + } + } +} diff --git a/src/components/Info.tsx b/src/components/Info.tsx index 94566ff..973f61c 100644 --- a/src/components/Info.tsx +++ b/src/components/Info.tsx @@ -1,13 +1,14 @@ -import React, { CSSProperties, FC } from 'react'; - -export const Info: FC<{ style?: CSSProperties }> = ({ style }) => { +import React, { CSSProperties, FC, useState } from 'react'; +import style from './Info.module.scss'; +import classNames from 'classnames'; +export const Info: FC = () => { + const [open, setOpen] = useState(false); return ( - <div style={style}> - <p> - <span id="busuanzi_container_site_pv"> - 累计访问:<span id="busuanzi_value_site_pv"></span>次 - </span> - </p> + <div + onClick={() => !open && setOpen(true)} + className={classNames(style.info, open && style.open)} + > + <div className={style.icon}>i</div> <p> bgm素材: <a @@ -17,22 +18,6 @@ export const Info: FC<{ style?: CSSProperties }> = ({ style }) => { > 洛天依,言和原创《普通DISCO》 </a> - 、 - <a - href="https://music.163.com/#/song?id=135022" - target="_blank" - rel="noreferrer" - > - 贫民百万歌星 - </a> - 、 - <a - href="https://y.qq.com/n/ryqq/songDetail/0020Nusb3QJGn9" - target="_blank" - rel="noreferrer" - > - 只因你太美 - </a> </p> <p> 玩法来源➡️羊了个羊➡️ @@ -44,6 +29,10 @@ export const Info: FC<{ style?: CSSProperties }> = ({ style }) => { 3 Tiles </a> </p> + <p>仅供交流,禁止商用</p> + <div className={style.close} onClick={() => setOpen(false)}> + X + </div> </div> ); }; diff --git a/src/components/PersonalInfo.module.scss b/src/components/PersonalInfo.module.scss new file mode 100644 index 0000000..ccd0252 --- /dev/null +++ b/src/components/PersonalInfo.module.scss @@ -0,0 +1,137 @@ +@keyframes gradient { + 0% { + background-position: 0 50%; + } + + 50% { + background-position: 100% 50%; + } + + 100% { + background-position: 0 50%; + } +} + +.info { + position: fixed; + right: 8px; + top: 8px; + padding: 4px; + width: 36px; + height: 36px; + border-radius: 18px; + animation: gradient 4s ease infinite; + background-image: linear-gradient( + -45deg, + #ee775288, + #e73c7e88, + #23a6d588, + #23d5ab88 + ); + background-size: 400% 400%; + background-position: 0 0; + box-sizing: border-box; + transition: 0.4s; + backdrop-filter: blur(8px); + z-index: 9; + overflow: hidden; + color: white; + + * { + transition: 0.6s; + } + + a { + text-decoration: underline; + color: white; + font-weight: 900; + } + + .close { + position: absolute; + border-radius: 8px; + background-color: rgb(0 0 0/20%); + width: 36px; + height: 36px; + right: 0; + top: 0; + line-height: 36px; + text-align: center; + transform: scale(0); + color: white; + cursor: pointer; + user-select: none; + } + + .github { + &Icon { + position: absolute; + right: 6px; + top: 6px; + cursor: pointer; + } + + &Link { + position: absolute; + right: -196px; + top: 26px; + } + } + + .bilibili { + &Icon { + position: absolute; + right: -100px; + top: 10px; + } + + &Link { + position: absolute; + right: -196px; + top: 16px; + } + } + + &.open { + height: 100px; + border-radius: 8px; + + @media screen and (max-width: 500px) { + width: calc(100% - 16px); + } + @media screen and (min-width: 501px) { + width: 500px; + } + + .close { + transform: scale(1); + } + + .github { + &Icon { + right: calc(100% - 70px); + top: 18px; + width: 36px; + height: 36px; + } + + &Link { + right: calc(100% - 200px); + top: 26px; + } + } + + .bilibili { + &Icon { + height: 36px; + right: 26px; + top: 50px; + } + + &Link { + right: 110px; + top: 58px; + } + } + } +} diff --git a/src/components/PersonalInfo.tsx b/src/components/PersonalInfo.tsx index 4ae55ff..c2b6f4a 100644 --- a/src/components/PersonalInfo.tsx +++ b/src/components/PersonalInfo.tsx @@ -1,56 +1,81 @@ -import React, { FC } from 'react'; +import React, { FC, useState } from 'react'; +import style from './PersonalInfo.module.scss'; +import classNames from 'classnames'; + +const GithubIcon: FC = () => { + return ( + <svg + width="24" + height="24" + aria-hidden="true" + viewBox="0 0 16 16" + fill="currentColor" + className={style.githubIcon} + > + <path + fillRule="evenodd" + d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" + /> + </svg> + ); +}; + +const BiliBiliIcon: FC = () => { + return ( + <svg + height="16" + aria-hidden="true" + viewBox="0 0 2240 1024" + className={style.bilibiliIcon} + > + <path + d="M2079.810048 913.566175c-10.01309 0-18.554608 0.799768-26.936172-0.159954-16.987063-1.951433-33.974126-1.567544-50.99318-2.079395-10.972811-0.287916-10.652904-0.287916-11.580634-10.90883-2.71921-32.406582-5.694345-64.781173-8.605499-97.155764-2.527266-28.439735-4.926568-56.91146-7.70976-85.319204-2.527266-26.040432-5.566382-52.016883-8.317583-78.025324-2.623238-24.440897-5.054531-48.913784-7.77374-73.322691a12681.114551 12681.114551 0 0 0-10.684895-92.133223c-3.295042-27.128116-6.558094-54.320213-10.205034-81.416339a20559.272961 20559.272961 0 0 0-17.530905-125.979387c-6.398141-44.723002-14.075909-89.22207-22.105576-133.657156-1.439582-7.965685-1.247637-8.253601 6.36615-9.533229 31.670796-5.406429 63.501545-10.01309 95.716183-9.309295 3.486987 0.095972 7.005964 0.159954 10.460959 0.607823 5.662354 0.703795 8.605499 3.454996 8.925406 10.045081 1.119675 22.969325 2.71921 45.938649 4.414717 68.875983 2.71921 37.589076 5.662354 75.178151 8.477537 112.735236 1.791479 24.184971 3.327033 48.305961 5.150503 72.426951 2.911154 38.772732 5.982261 77.513473 8.925406 116.286205 1.791479 23.705111 3.359024 47.474203 5.182494 71.179313 2.783191 34.805885 5.822308 69.579778 8.637489 104.353672 1.791479 22.137566 3.391014 44.307123 5.278466 66.44469 2.783191 32.79047 5.790317 65.580941 8.63749 98.371411 2.143377 25.592562 4.09481 51.249106 6.270178 77.673426zM853.670395 114.918282c4.638652 0 11.644616-0.511851 18.554607 0.127963 8.797443 0.799768 10.49295 3.071107 11.036793 11.900541 2.527266 40.372267 4.894578 80.776524 7.581796 121.180782 2.943145 43.571337 6.174206 87.078693 9.405267 130.586048 2.975135 39.956388 5.950271 79.912775 9.149341 119.869163 3.486987 43.891244 7.357862 87.718507 10.876839 131.609751 2.655228 33.622229 4.926568 67.244457 7.677768 100.898677 2.623238 31.222926 5.694345 62.38187 8.509527 93.572805 2.399303 26.8402 4.830596 53.71239 7.165918 80.58458 0.735786 8.509527 0.127963 9.053369-9.053369 8.829434-24.025018-0.575833-47.922073-3.391014-71.947091-2.71921-5.502401 0.159954-7.101936-2.367312-8.029666-7.581796-1.983424-11.356699-1.663517-22.905343-2.879163-34.390006-3.295042-30.359177-5.182494-60.846317-7.965685-91.269474-2.495275-27.639967-5.502401-55.215953-8.349574-82.82393-2.527266-25.240664-5.02254-50.481329-7.709759-75.753984-2.687219-24.792795-5.534392-49.61758-8.349573-74.442365-2.591247-22.841362-5.118512-45.682723-7.869713-68.524085-4.062819-33.462275-8.093648-66.92455-12.508365-100.322844-4.062819-30.647093-8.66948-61.198214-12.988225-91.813317-5.886289-41.587914-12.508365-83.079855-19.834236-124.411842a1393.96288 1393.96288 0 0 0-5.310457-28.023856c-0.959721-4.702633-0.095972-7.421843 5.278466-8.157629 14.139891-1.887451 28.24779-4.830596 42.451663-6.206196 14.203872-1.311619 28.407744-3.966847 45.106891-2.71921z m1006.075609 403.33878c27.064134 0 27.703949 0.191944 32.054684 24.536869 5.342447 30.03927 9.08536 60.334465 12.636328 90.62966 3.742912 32.278619 7.517815 64.557238 10.972811 96.867848 2.783191 26.008441 5.118512 52.080864 7.74175 78.089305 2.7512 27.256079 5.662354 54.416185 8.509527 81.640274 1.567544 15.387528 3.039117 30.775056 4.798605 46.130593 0.511851 4.446708-0.831758 6.81402-5.214485 7.325871-9.245313 1.055693-18.426645 2.27134-27.639967 3.263052-16.891091 1.82347-33.814173 3.614949-50.737254 5.182493-8.733462 0.799768-9.309294 0.319907-10.940821-8.125638-14.843686-76.617733-29.719363-153.171485-44.435086-229.821208-9.789155-50.961189-19.322384-101.95437-28.919595-152.915559a805.525894 805.525894 0 0 1-3.582959-21.081873c-0.639814-4.030829 0.44787-6.622075 5.022541-7.70976 30.48714-7.133927 61.294186-12.636328 89.733921-14.011927z m-1137.077537 0c28.951586 0 28.823623 0.095972 33.302322 26.360339 6.909992 40.660183 11.804569 81.544301 16.187295 122.556382 4.286754 39.796434 8.957397 79.560878 13.148179 119.357311 2.847173 27.224088 5.086522 54.512157 7.74175 81.704255 1.887451 19.354375 4.126801 38.644769 6.174206 57.967153 0.255926 2.367312 0.383888 4.734624 0.543842 7.133927 0.415879 9.469248 0 10.237025-9.117351 11.164755-18.074747 1.887451-36.181485 3.454996-54.256232 5.246476-6.558094 0.639814-13.084197 1.599535-19.57831 2.239349-8.63749 0.799768-8.925406 0.767777-10.620913-7.965685-6.078234-30.679084-11.964523-61.422149-17.914793-92.101233-14.267853-73.898523-28.69566-147.733065-42.867542-221.631589-5.662354-29.559409-10.524941-59.246781-16.091323-88.838181-1.023702-5.406429-0.255926-7.933694 5.342447-9.245313 30.199223-7.037955 60.590391-12.540355 88.006423-13.947946z m382.128944 309.861946v124.027954c0 1.183656-0.127963 2.399303 0.03199 3.582959 0.607823 6.014252-1.599535 8.66948-7.805731 8.413555-8.157629-0.351898-16.251277-0.127963-24.408906 0.063981-17.019054 0.319907-34.070098-0.351898-51.057162 1.599535-9.405267 1.087684-9.213322 0.511851-10.141052-9.405266-2.783191-31.222926-5.822308-62.413861-8.669481-93.636787-2.623238-28.823623-4.99055-57.711228-7.677768-86.534851-2.71921-29.655381-5.758326-59.214791-8.509527-88.838181-1.887451-19.770254-3.550968-39.508518-5.214485-59.278772-2.175368-25.720525-4.190782-51.409059-6.462122-77.129585-0.959721-10.844848-0.159954-12.380402 10.588923-13.500076a531.877423 531.877423 0 0 1 83.527724-2.591247c6.941982 0.383888 13.851974 1.727498 20.570022 3.359024 8.477536 2.015414 9.405267 3.263052 9.853137 12.124476 0.92773 17.850812 1.855461 35.701624 2.335321 53.584427 0.543842 19.866226 0.095972 39.764443 0.831758 59.63067 1.855461 54.800074 1.567544 109.664129 2.207359 164.528184z m1134.806197 5.630364v117.437869c0 1.983424-0.063981 3.966847 0.03199 5.982262 0.415879 5.150503-1.983424 6.973973-6.878001 6.941982-12.028504-0.095972-24.025018 0-36.021531 0.159954-13.564058 0.127963-27.096125 0.063981-40.628192 1.535553-8.925406 1.023702-8.989387 0.351898-9.789155-8.509527-3.678931-40.660183-7.549806-81.320366-11.260728-122.04453-3.391014-37.525094-6.526103-75.082179-9.981099-112.639265-3.550968-38.740741-7.421843-77.38551-10.90883-116.09426-1.727498-19.386366-3.16708-38.772732-4.606661-58.159097-0.575833-8.445546 0.351898-9.949109 9.885127-10.716886 16.571184-1.311619 33.078387-3.550968 49.777534-3.263051 16.635165 0.319907 33.302322-0.607823 49.841515 2.559256 14.011928 2.687219 14.715723 3.486987 15.547481 18.458635 2.399303 44.051198 1.663517 88.230358 3.231061 132.281556 1.599535 46.89837 0.479861 93.79674 1.759489 146.069549zM1831.498213 305.135c9.789155 0.575833 17.498914 0.095972 25.176683 1.791479 4.894578 1.119675 7.357862 3.327033 7.837723 8.573509 2.303331 25.240664 4.798605 50.51332 7.32587 75.785975 2.015414 20.50604 4.158791 41.012081 6.238188 61.518121l0.191944 1.183656c1.663517 12.924244 1.279628 13.276142-11.292718 13.979937-11.196746 0.607823-22.361501 1.599535-33.558247 2.27134-7.357862 0.44787-9.693183 1.695507-10.90883-9.021378-4.190782-37.813011-9.053369-75.530049-13.692021-113.311069a1185.0316 1185.0316 0 0 0-4.286754-31.798759c-0.92773-5.982261 1.407591-9.277304 7.005964-9.757164 7.357862-0.671805 14.715723-0.863749 19.962198-1.215647z m-1133.398606 0.159954c7.549806 0.415879 15.323547-0.159954 22.937334 1.599535 4.350736 0.991712 6.558094 2.815182 6.973973 7.773741 0.92773 11.83656 2.7512 23.641129 3.870875 35.477689 3.550968 36.309448 6.909992 72.650886 10.237025 108.992324 0.703795 7.901704 0.543842 8.061657-6.84601 8.605499-13.116188 0.959721-26.264367 1.919442-39.412546 2.463284-7.645778 0.351898-8.605499-0.575833-9.56522-8.381564-3.327033-26.744227-6.462122-53.520446-9.661192-80.296664-2.591247-22.073585-4.766615-44.14717-7.901704-66.156773-0.863749-6.078234 1.119675-7.74175 5.982262-8.733462 7.709759-1.567544 15.451509-1.055693 23.385203-1.343609z m399.147998 100.002936c0 23.001315 0.063981 45.97064-0.031991 69.003946 0 10.332997-0.127963 10.396978-10.396978 10.269016a324.289753 324.289753 0 0 1-36.981252-1.919443c-7.933694-0.991712-8.093648-0.735786-8.317583-9.149341-0.799768-28.119828-1.631526-56.239655-2.207359-84.359483-0.415879-19.034468-0.639814-38.004955-1.791479-57.039422-0.607823-9.821146-0.063981-9.917118 9.373276-10.045081 13.915956-0.159954 27.799921 0.479861 41.619904 2.591247 8.317583 1.279628 8.701471 1.279628 8.733462 10.49295 0.063981 23.385204 0.063981 46.770407 0.063981 70.187602h-0.063981z m1135.38203 0.607824c0 23.033306 0.063981 46.034621-0.031991 69.035936 0 9.661192-0.159954 9.725174-9.853137 9.661192a505.32514 505.32514 0 0 1-38.132917-1.791479c-6.302168-0.479861-8.157629-3.135089-7.74175-8.861425 0.063981-0.799768 0-1.599535 0-2.399302-0.959721-44.403095-1.919442-88.7742-2.815182-133.177296-0.031991-2.367312-0.159954-4.734624-0.063982-7.133926 0.127963-8.957397 0.159954-9.181332 9.149341-9.117351 12.380402 0.063981 24.664832 0.703795 37.013243 1.919442 15.067621 1.503563 12.412393 3.359024 12.476375 15.259566 0.063981 22.169557 0.031991 44.403095 0 66.604643z m-1565.593 54.000306c0.287916 12.636328 0.287916 12.604337-11.804569 15.547481-8.221611 2.015414-16.443221 4.222773-24.728813 6.046243-7.069945 1.599535-8.317583 0.703795-9.53323-6.238187-8.445546-47.090314-16.8591-94.212619-25.240664-141.334924-1.695507-9.757164-1.247637-10.364988 8.349573-12.060495 11.804569-2.079396 23.577148-4.126801 35.381717-5.950271 7.517815-1.183656 8.477536-0.767777 9.9811 7.517815 2.975135 16.731138 5.790317 33.526256 7.997675 50.385357 3.423005 26.680246 6.238187 53.456464 9.309295 80.168701 0.255926 1.951433 0.191944 3.966847 0.287916 5.91828z m1064.138735-136.696273c15.451509-2.527266 31.030982-5.086522 46.610454-7.549806 5.598373-0.863749 7.29388 2.655228 8.029666 7.645778 2.655228 18.426645 5.982261 36.725327 8.157629 55.183962 3.19907 26.744227 7.581797 53.360492 8.413555 80.328655 0.063981 2.7512 0.031991 5.566382 0.095972 8.317583 0.159954 4.286754-1.983424 6.494113-5.950271 7.421843-10.556932 2.367312-21.113864 4.734624-31.638805 7.261889-5.054531 1.215647-6.750038-0.92773-7.581796-5.854298-3.16708-18.746552-6.81402-37.397131-10.045081-56.079702-5.47041-30.775056-10.780867-61.582103-16.091323-92.38915-0.127963-1.119675 0-2.303331 0-4.286754z m-710.64147 108.032603c-0.44787 16.37924 0.543842 30.647093-1.695507 44.914947-0.671805 4.510689-1.983424 7.421843-6.846011 7.837722-10.428969 0.863749-20.825947 1.695507-31.190935 2.7512-5.02254 0.543842-6.430131-1.631526-7.261889-6.558094-2.335321-14.55577-1.919442-29.303484-3.327033-43.923234-2.655228-27.607976-3.774903-55.407897-5.566383-83.111846-0.44787-6.750038-1.119675-13.436095-1.663516-20.186134-0.287916-3.774903 1.215647-5.886289 5.246475-6.046242 13.500077-0.543842 26.936172-3.007126 40.50023-2.527266 7.933694 0.287916 8.605499 0.799768 9.181331 8.797443 0.351898 5.534392 0.255926 11.132765 0.383889 16.699147l2.239349 81.352357z m1134.902169-15.867388c0 19.066459 0.223935 38.132918-0.031991 57.199376-0.159954 9.917118-1.279628 10.780867-10.652904 11.644616-9.277304 0.863749-18.490626 1.567544-27.735939 2.559256-5.214485 0.543842-7.645778-0.991712-7.965685-6.973973-1.34361-25.336637-3.16708-50.673273-4.926568-75.977919-1.3756-20.985901-2.943145-41.939811-4.414717-62.893722-0.159954-2.399303-0.031991-4.798605-0.191944-7.165917-0.223935-4.190782 1.055693-6.654066 5.758326-6.81402 13.116188-0.44787 26.136404-2.975135 39.348564-2.495274 8.061657 0.287916 8.18962 0.415879 8.797444 8.797443 1.951433 27.32006 2.143377 54.704102 2.015414 82.120134zM628.295894 756.171918c16.571184 18.234701 17.402942 39.828425 11.932532 62.413861-5.502401 22.585436-18.042756 41.204025-33.23834 57.903171-25.49659 27.895893-56.303637 48.497905-89.062116 65.99682-56.399609 30.135242-116.190232 50.161422-178.572103 61.997982-44.882956 8.477536-90.053828 15.00364-135.704561 17.498914-13.915956 0.767777-27.799921 1.407591-41.715876 1.311619-10.077071 0-20.186133 0.287916-30.231214-0.063981-8.541518-0.319907-9.789155-1.791479-10.49295-10.716886-2.591247-32.022693-4.798605-64.077378-7.645778-96.100071-3.327033-37.109215-7.229899-74.18644-10.812858-111.295654-2.623238-26.8402-4.894578-53.744381-7.773741-80.520599-3.327033-31.542833-7.069945-63.021684-10.716885-94.564517-3.327033-29.111539-6.526103-58.28706-10.045081-87.430591-3.934856-32.278619-7.997676-64.493257-12.31642-96.707894a8228.968456 8228.968456 0 0 0-13.212161-92.996973 5984.500754 5984.500754 0 0 0-24.312934-152.627642 3243.825263 3243.825263 0 0 0-23.67312-123.740038c-1.151665-5.502401 0.511851-7.709759 5.342448-9.725174C52.335283 47.609843 98.465876 28.063524 144.724432 8.77313c8.605499-3.582959 17.434933-6.590085 26.584274-8.285592 6.334159-1.183656 7.965685 0.127963 7.773741 6.494113-0.479861 16.283268 0.191944 32.630517-1.407591 48.849803a161.393095 161.393095 0 0 0-0.639814 13.084197c-0.735786 58.383032-1.439582 116.798056 0.095972 175.213079 1.34361 51.185124 4.030829 102.338258 7.005964 153.491392 2.335321 40.372267 5.694345 80.744534 9.149341 121.052819 3.391014 39.508518 7.517815 78.953054 11.38869 118.461572 0.735786 7.517815 1.407591 8.221611 9.949108 7.069945a381.329176 381.329176 0 0 1 50.833227-4.190782c52.880632-0.127963 104.897514 7.133927 156.338564 19.322384 45.010919 10.684895 88.806191 24.920757 130.777993 44.818975 20.793957 9.853136 40.692174 21.241827 58.830902 35.701624 6.174206 4.862587 11.676606 10.46096 16.891091 16.315259z m1126.840512-9.597211c20.47405 17.946784 27.927883 39.924397 22.105576 67.116494-4.830596 22.425483-15.771416 41.268006-30.359177 58.127107-23.417194 27.096125-51.856929 47.698138-82.631985 64.909136-60.334465 33.782182-124.603787 55.727804-192.168151 68.396122a1151.089465 1151.089465 0 0 1-111.455609 15.547481c-21.177845 1.82347-42.451662 4.09481-66.220754 2.623238h-27.76793c-5.406429 0-8.477536-1.695507-8.925406-8.125638-2.047405-28.087837-4.414717-56.143683-6.941983-84.19953-2.687219-29.623391-5.662354-59.246781-8.477536-88.870172-2.559256-27.224088-4.926568-54.512157-7.709759-81.736245-2.559256-25.656544-5.502401-51.249106-8.285592-76.873659-2.591247-24.057008-5.086522-48.114017-7.933695-72.139035-3.423005-29.111539-7.037955-58.223079-10.652904-87.334618-3.391014-27.160107-6.750038-54.288222-10.364987-81.416338a6133.577429 6133.577429 0 0 0-12.156467-87.142675c-5.694345-37.653057-11.804569-75.178151-17.818822-112.767227a3259.14881 3259.14881 0 0 0-29.111539-158.993792c-0.44787-2.335321-0.671805-4.734624-1.3756-7.005964-1.663517-5.118512-0.063981-7.837722 4.958559-9.821146C1191.012355 47.641834 1238.61452 24.448575 1288.2321 6.149893c6.494113-2.431293 13.052207-5.150503 20.058171-5.854299 6.302168-0.639814 7.901704 0.383888 7.29388 7.101936-3.327033 36.43741-1.407591 73.066765-3.135089 109.536166-1.407591 29.751354-1.247637 59.598679 0.255926 89.382023 0.351898 7.549806 0.639814 15.131602 0.575832 22.649418-0.383888 35.765606 1.503563 71.499221 3.327033 107.200845 2.335321 47.186286 5.758326 94.276601 9.245313 141.398906 2.527266 34.006117 5.822308 67.948253 9.021379 101.922379 1.695507 18.586598 3.518977 37.141206 5.822308 55.631832 1.247637 10.205034 1.759489 10.301006 11.772578 8.957396 17.658868-2.399303 35.349726-4.350736 53.200539-4.09481 62.637796 0.799768 124.027954 10.684895 184.266447 27.863902 40.788146 11.580634 80.488608 26.040432 117.981712 46.290547a253.55831 253.55831 0 0 1 47.218277 32.438573zM308.676783 922.811488c23.161269-11.068783 135.608589-98.947243 144.533995-113.279078-54.576139-23.513166-109.344222-45.362816-168.239105-63.24562l23.70511 176.524698z m1277.196815-107.520752c2.879163-3.103098 2.559256-5.502401-1.343609-7.229899-7.773741-3.550968-15.4835-7.325871-23.353213-10.556932-42.003793-17.179007-84.19953-33.814173-127.482951-47.37823-3.774903-1.151665-7.645778-3.774903-12.476374-1.535554l23.321222 173.45359c3.454996 0.767777 4.798605-0.831758 6.33416-1.919442 39.316574-28.855614 78.889073-57.35933 116.638102-88.390312 6.36615-5.182494 12.668318-10.396978 18.362663-16.443221z" + fill="currentColor" + /> + </svg> + ); +}; export const GithubLink: FC = () => { return ( <a + className={style.githubLink} href="https://github.com/StreakingMan/solvable-sheep-game" target="_blank" rel="noreferrer" > - 点个✨不迷路 @StreakingMan - <svg - width="16" - height="16" - aria-hidden="true" - viewBox="0 0 16 16" - fill="currentColor" - > - <path - fillRule="evenodd" - d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" - /> - </svg> + @StreakingMan </a> ); }; -export const BilibiliLink: FC = () => { +export const BiliBiliLink: FC = () => { return ( <a + className={style.bilibiliLink} href="https://space.bilibili.com/18424564" target="_blank" rel="noreferrer" > @streaking_man - <svg - height="16" - aria-hidden="true" - viewBox="0 0 2240 1024" - className="navbar_logo" - > - <path - d="M2079.810048 913.566175c-10.01309 0-18.554608 0.799768-26.936172-0.159954-16.987063-1.951433-33.974126-1.567544-50.99318-2.079395-10.972811-0.287916-10.652904-0.287916-11.580634-10.90883-2.71921-32.406582-5.694345-64.781173-8.605499-97.155764-2.527266-28.439735-4.926568-56.91146-7.70976-85.319204-2.527266-26.040432-5.566382-52.016883-8.317583-78.025324-2.623238-24.440897-5.054531-48.913784-7.77374-73.322691a12681.114551 12681.114551 0 0 0-10.684895-92.133223c-3.295042-27.128116-6.558094-54.320213-10.205034-81.416339a20559.272961 20559.272961 0 0 0-17.530905-125.979387c-6.398141-44.723002-14.075909-89.22207-22.105576-133.657156-1.439582-7.965685-1.247637-8.253601 6.36615-9.533229 31.670796-5.406429 63.501545-10.01309 95.716183-9.309295 3.486987 0.095972 7.005964 0.159954 10.460959 0.607823 5.662354 0.703795 8.605499 3.454996 8.925406 10.045081 1.119675 22.969325 2.71921 45.938649 4.414717 68.875983 2.71921 37.589076 5.662354 75.178151 8.477537 112.735236 1.791479 24.184971 3.327033 48.305961 5.150503 72.426951 2.911154 38.772732 5.982261 77.513473 8.925406 116.286205 1.791479 23.705111 3.359024 47.474203 5.182494 71.179313 2.783191 34.805885 5.822308 69.579778 8.637489 104.353672 1.791479 22.137566 3.391014 44.307123 5.278466 66.44469 2.783191 32.79047 5.790317 65.580941 8.63749 98.371411 2.143377 25.592562 4.09481 51.249106 6.270178 77.673426zM853.670395 114.918282c4.638652 0 11.644616-0.511851 18.554607 0.127963 8.797443 0.799768 10.49295 3.071107 11.036793 11.900541 2.527266 40.372267 4.894578 80.776524 7.581796 121.180782 2.943145 43.571337 6.174206 87.078693 9.405267 130.586048 2.975135 39.956388 5.950271 79.912775 9.149341 119.869163 3.486987 43.891244 7.357862 87.718507 10.876839 131.609751 2.655228 33.622229 4.926568 67.244457 7.677768 100.898677 2.623238 31.222926 5.694345 62.38187 8.509527 93.572805 2.399303 26.8402 4.830596 53.71239 7.165918 80.58458 0.735786 8.509527 0.127963 9.053369-9.053369 8.829434-24.025018-0.575833-47.922073-3.391014-71.947091-2.71921-5.502401 0.159954-7.101936-2.367312-8.029666-7.581796-1.983424-11.356699-1.663517-22.905343-2.879163-34.390006-3.295042-30.359177-5.182494-60.846317-7.965685-91.269474-2.495275-27.639967-5.502401-55.215953-8.349574-82.82393-2.527266-25.240664-5.02254-50.481329-7.709759-75.753984-2.687219-24.792795-5.534392-49.61758-8.349573-74.442365-2.591247-22.841362-5.118512-45.682723-7.869713-68.524085-4.062819-33.462275-8.093648-66.92455-12.508365-100.322844-4.062819-30.647093-8.66948-61.198214-12.988225-91.813317-5.886289-41.587914-12.508365-83.079855-19.834236-124.411842a1393.96288 1393.96288 0 0 0-5.310457-28.023856c-0.959721-4.702633-0.095972-7.421843 5.278466-8.157629 14.139891-1.887451 28.24779-4.830596 42.451663-6.206196 14.203872-1.311619 28.407744-3.966847 45.106891-2.71921z m1006.075609 403.33878c27.064134 0 27.703949 0.191944 32.054684 24.536869 5.342447 30.03927 9.08536 60.334465 12.636328 90.62966 3.742912 32.278619 7.517815 64.557238 10.972811 96.867848 2.783191 26.008441 5.118512 52.080864 7.74175 78.089305 2.7512 27.256079 5.662354 54.416185 8.509527 81.640274 1.567544 15.387528 3.039117 30.775056 4.798605 46.130593 0.511851 4.446708-0.831758 6.81402-5.214485 7.325871-9.245313 1.055693-18.426645 2.27134-27.639967 3.263052-16.891091 1.82347-33.814173 3.614949-50.737254 5.182493-8.733462 0.799768-9.309294 0.319907-10.940821-8.125638-14.843686-76.617733-29.719363-153.171485-44.435086-229.821208-9.789155-50.961189-19.322384-101.95437-28.919595-152.915559a805.525894 805.525894 0 0 1-3.582959-21.081873c-0.639814-4.030829 0.44787-6.622075 5.022541-7.70976 30.48714-7.133927 61.294186-12.636328 89.733921-14.011927z m-1137.077537 0c28.951586 0 28.823623 0.095972 33.302322 26.360339 6.909992 40.660183 11.804569 81.544301 16.187295 122.556382 4.286754 39.796434 8.957397 79.560878 13.148179 119.357311 2.847173 27.224088 5.086522 54.512157 7.74175 81.704255 1.887451 19.354375 4.126801 38.644769 6.174206 57.967153 0.255926 2.367312 0.383888 4.734624 0.543842 7.133927 0.415879 9.469248 0 10.237025-9.117351 11.164755-18.074747 1.887451-36.181485 3.454996-54.256232 5.246476-6.558094 0.639814-13.084197 1.599535-19.57831 2.239349-8.63749 0.799768-8.925406 0.767777-10.620913-7.965685-6.078234-30.679084-11.964523-61.422149-17.914793-92.101233-14.267853-73.898523-28.69566-147.733065-42.867542-221.631589-5.662354-29.559409-10.524941-59.246781-16.091323-88.838181-1.023702-5.406429-0.255926-7.933694 5.342447-9.245313 30.199223-7.037955 60.590391-12.540355 88.006423-13.947946z m382.128944 309.861946v124.027954c0 1.183656-0.127963 2.399303 0.03199 3.582959 0.607823 6.014252-1.599535 8.66948-7.805731 8.413555-8.157629-0.351898-16.251277-0.127963-24.408906 0.063981-17.019054 0.319907-34.070098-0.351898-51.057162 1.599535-9.405267 1.087684-9.213322 0.511851-10.141052-9.405266-2.783191-31.222926-5.822308-62.413861-8.669481-93.636787-2.623238-28.823623-4.99055-57.711228-7.677768-86.534851-2.71921-29.655381-5.758326-59.214791-8.509527-88.838181-1.887451-19.770254-3.550968-39.508518-5.214485-59.278772-2.175368-25.720525-4.190782-51.409059-6.462122-77.129585-0.959721-10.844848-0.159954-12.380402 10.588923-13.500076a531.877423 531.877423 0 0 1 83.527724-2.591247c6.941982 0.383888 13.851974 1.727498 20.570022 3.359024 8.477536 2.015414 9.405267 3.263052 9.853137 12.124476 0.92773 17.850812 1.855461 35.701624 2.335321 53.584427 0.543842 19.866226 0.095972 39.764443 0.831758 59.63067 1.855461 54.800074 1.567544 109.664129 2.207359 164.528184z m1134.806197 5.630364v117.437869c0 1.983424-0.063981 3.966847 0.03199 5.982262 0.415879 5.150503-1.983424 6.973973-6.878001 6.941982-12.028504-0.095972-24.025018 0-36.021531 0.159954-13.564058 0.127963-27.096125 0.063981-40.628192 1.535553-8.925406 1.023702-8.989387 0.351898-9.789155-8.509527-3.678931-40.660183-7.549806-81.320366-11.260728-122.04453-3.391014-37.525094-6.526103-75.082179-9.981099-112.639265-3.550968-38.740741-7.421843-77.38551-10.90883-116.09426-1.727498-19.386366-3.16708-38.772732-4.606661-58.159097-0.575833-8.445546 0.351898-9.949109 9.885127-10.716886 16.571184-1.311619 33.078387-3.550968 49.777534-3.263051 16.635165 0.319907 33.302322-0.607823 49.841515 2.559256 14.011928 2.687219 14.715723 3.486987 15.547481 18.458635 2.399303 44.051198 1.663517 88.230358 3.231061 132.281556 1.599535 46.89837 0.479861 93.79674 1.759489 146.069549zM1831.498213 305.135c9.789155 0.575833 17.498914 0.095972 25.176683 1.791479 4.894578 1.119675 7.357862 3.327033 7.837723 8.573509 2.303331 25.240664 4.798605 50.51332 7.32587 75.785975 2.015414 20.50604 4.158791 41.012081 6.238188 61.518121l0.191944 1.183656c1.663517 12.924244 1.279628 13.276142-11.292718 13.979937-11.196746 0.607823-22.361501 1.599535-33.558247 2.27134-7.357862 0.44787-9.693183 1.695507-10.90883-9.021378-4.190782-37.813011-9.053369-75.530049-13.692021-113.311069a1185.0316 1185.0316 0 0 0-4.286754-31.798759c-0.92773-5.982261 1.407591-9.277304 7.005964-9.757164 7.357862-0.671805 14.715723-0.863749 19.962198-1.215647z m-1133.398606 0.159954c7.549806 0.415879 15.323547-0.159954 22.937334 1.599535 4.350736 0.991712 6.558094 2.815182 6.973973 7.773741 0.92773 11.83656 2.7512 23.641129 3.870875 35.477689 3.550968 36.309448 6.909992 72.650886 10.237025 108.992324 0.703795 7.901704 0.543842 8.061657-6.84601 8.605499-13.116188 0.959721-26.264367 1.919442-39.412546 2.463284-7.645778 0.351898-8.605499-0.575833-9.56522-8.381564-3.327033-26.744227-6.462122-53.520446-9.661192-80.296664-2.591247-22.073585-4.766615-44.14717-7.901704-66.156773-0.863749-6.078234 1.119675-7.74175 5.982262-8.733462 7.709759-1.567544 15.451509-1.055693 23.385203-1.343609z m399.147998 100.002936c0 23.001315 0.063981 45.97064-0.031991 69.003946 0 10.332997-0.127963 10.396978-10.396978 10.269016a324.289753 324.289753 0 0 1-36.981252-1.919443c-7.933694-0.991712-8.093648-0.735786-8.317583-9.149341-0.799768-28.119828-1.631526-56.239655-2.207359-84.359483-0.415879-19.034468-0.639814-38.004955-1.791479-57.039422-0.607823-9.821146-0.063981-9.917118 9.373276-10.045081 13.915956-0.159954 27.799921 0.479861 41.619904 2.591247 8.317583 1.279628 8.701471 1.279628 8.733462 10.49295 0.063981 23.385204 0.063981 46.770407 0.063981 70.187602h-0.063981z m1135.38203 0.607824c0 23.033306 0.063981 46.034621-0.031991 69.035936 0 9.661192-0.159954 9.725174-9.853137 9.661192a505.32514 505.32514 0 0 1-38.132917-1.791479c-6.302168-0.479861-8.157629-3.135089-7.74175-8.861425 0.063981-0.799768 0-1.599535 0-2.399302-0.959721-44.403095-1.919442-88.7742-2.815182-133.177296-0.031991-2.367312-0.159954-4.734624-0.063982-7.133926 0.127963-8.957397 0.159954-9.181332 9.149341-9.117351 12.380402 0.063981 24.664832 0.703795 37.013243 1.919442 15.067621 1.503563 12.412393 3.359024 12.476375 15.259566 0.063981 22.169557 0.031991 44.403095 0 66.604643z m-1565.593 54.000306c0.287916 12.636328 0.287916 12.604337-11.804569 15.547481-8.221611 2.015414-16.443221 4.222773-24.728813 6.046243-7.069945 1.599535-8.317583 0.703795-9.53323-6.238187-8.445546-47.090314-16.8591-94.212619-25.240664-141.334924-1.695507-9.757164-1.247637-10.364988 8.349573-12.060495 11.804569-2.079396 23.577148-4.126801 35.381717-5.950271 7.517815-1.183656 8.477536-0.767777 9.9811 7.517815 2.975135 16.731138 5.790317 33.526256 7.997675 50.385357 3.423005 26.680246 6.238187 53.456464 9.309295 80.168701 0.255926 1.951433 0.191944 3.966847 0.287916 5.91828z m1064.138735-136.696273c15.451509-2.527266 31.030982-5.086522 46.610454-7.549806 5.598373-0.863749 7.29388 2.655228 8.029666 7.645778 2.655228 18.426645 5.982261 36.725327 8.157629 55.183962 3.19907 26.744227 7.581797 53.360492 8.413555 80.328655 0.063981 2.7512 0.031991 5.566382 0.095972 8.317583 0.159954 4.286754-1.983424 6.494113-5.950271 7.421843-10.556932 2.367312-21.113864 4.734624-31.638805 7.261889-5.054531 1.215647-6.750038-0.92773-7.581796-5.854298-3.16708-18.746552-6.81402-37.397131-10.045081-56.079702-5.47041-30.775056-10.780867-61.582103-16.091323-92.38915-0.127963-1.119675 0-2.303331 0-4.286754z m-710.64147 108.032603c-0.44787 16.37924 0.543842 30.647093-1.695507 44.914947-0.671805 4.510689-1.983424 7.421843-6.846011 7.837722-10.428969 0.863749-20.825947 1.695507-31.190935 2.7512-5.02254 0.543842-6.430131-1.631526-7.261889-6.558094-2.335321-14.55577-1.919442-29.303484-3.327033-43.923234-2.655228-27.607976-3.774903-55.407897-5.566383-83.111846-0.44787-6.750038-1.119675-13.436095-1.663516-20.186134-0.287916-3.774903 1.215647-5.886289 5.246475-6.046242 13.500077-0.543842 26.936172-3.007126 40.50023-2.527266 7.933694 0.287916 8.605499 0.799768 9.181331 8.797443 0.351898 5.534392 0.255926 11.132765 0.383889 16.699147l2.239349 81.352357z m1134.902169-15.867388c0 19.066459 0.223935 38.132918-0.031991 57.199376-0.159954 9.917118-1.279628 10.780867-10.652904 11.644616-9.277304 0.863749-18.490626 1.567544-27.735939 2.559256-5.214485 0.543842-7.645778-0.991712-7.965685-6.973973-1.34361-25.336637-3.16708-50.673273-4.926568-75.977919-1.3756-20.985901-2.943145-41.939811-4.414717-62.893722-0.159954-2.399303-0.031991-4.798605-0.191944-7.165917-0.223935-4.190782 1.055693-6.654066 5.758326-6.81402 13.116188-0.44787 26.136404-2.975135 39.348564-2.495274 8.061657 0.287916 8.18962 0.415879 8.797444 8.797443 1.951433 27.32006 2.143377 54.704102 2.015414 82.120134zM628.295894 756.171918c16.571184 18.234701 17.402942 39.828425 11.932532 62.413861-5.502401 22.585436-18.042756 41.204025-33.23834 57.903171-25.49659 27.895893-56.303637 48.497905-89.062116 65.99682-56.399609 30.135242-116.190232 50.161422-178.572103 61.997982-44.882956 8.477536-90.053828 15.00364-135.704561 17.498914-13.915956 0.767777-27.799921 1.407591-41.715876 1.311619-10.077071 0-20.186133 0.287916-30.231214-0.063981-8.541518-0.319907-9.789155-1.791479-10.49295-10.716886-2.591247-32.022693-4.798605-64.077378-7.645778-96.100071-3.327033-37.109215-7.229899-74.18644-10.812858-111.295654-2.623238-26.8402-4.894578-53.744381-7.773741-80.520599-3.327033-31.542833-7.069945-63.021684-10.716885-94.564517-3.327033-29.111539-6.526103-58.28706-10.045081-87.430591-3.934856-32.278619-7.997676-64.493257-12.31642-96.707894a8228.968456 8228.968456 0 0 0-13.212161-92.996973 5984.500754 5984.500754 0 0 0-24.312934-152.627642 3243.825263 3243.825263 0 0 0-23.67312-123.740038c-1.151665-5.502401 0.511851-7.709759 5.342448-9.725174C52.335283 47.609843 98.465876 28.063524 144.724432 8.77313c8.605499-3.582959 17.434933-6.590085 26.584274-8.285592 6.334159-1.183656 7.965685 0.127963 7.773741 6.494113-0.479861 16.283268 0.191944 32.630517-1.407591 48.849803a161.393095 161.393095 0 0 0-0.639814 13.084197c-0.735786 58.383032-1.439582 116.798056 0.095972 175.213079 1.34361 51.185124 4.030829 102.338258 7.005964 153.491392 2.335321 40.372267 5.694345 80.744534 9.149341 121.052819 3.391014 39.508518 7.517815 78.953054 11.38869 118.461572 0.735786 7.517815 1.407591 8.221611 9.949108 7.069945a381.329176 381.329176 0 0 1 50.833227-4.190782c52.880632-0.127963 104.897514 7.133927 156.338564 19.322384 45.010919 10.684895 88.806191 24.920757 130.777993 44.818975 20.793957 9.853136 40.692174 21.241827 58.830902 35.701624 6.174206 4.862587 11.676606 10.46096 16.891091 16.315259z m1126.840512-9.597211c20.47405 17.946784 27.927883 39.924397 22.105576 67.116494-4.830596 22.425483-15.771416 41.268006-30.359177 58.127107-23.417194 27.096125-51.856929 47.698138-82.631985 64.909136-60.334465 33.782182-124.603787 55.727804-192.168151 68.396122a1151.089465 1151.089465 0 0 1-111.455609 15.547481c-21.177845 1.82347-42.451662 4.09481-66.220754 2.623238h-27.76793c-5.406429 0-8.477536-1.695507-8.925406-8.125638-2.047405-28.087837-4.414717-56.143683-6.941983-84.19953-2.687219-29.623391-5.662354-59.246781-8.477536-88.870172-2.559256-27.224088-4.926568-54.512157-7.709759-81.736245-2.559256-25.656544-5.502401-51.249106-8.285592-76.873659-2.591247-24.057008-5.086522-48.114017-7.933695-72.139035-3.423005-29.111539-7.037955-58.223079-10.652904-87.334618-3.391014-27.160107-6.750038-54.288222-10.364987-81.416338a6133.577429 6133.577429 0 0 0-12.156467-87.142675c-5.694345-37.653057-11.804569-75.178151-17.818822-112.767227a3259.14881 3259.14881 0 0 0-29.111539-158.993792c-0.44787-2.335321-0.671805-4.734624-1.3756-7.005964-1.663517-5.118512-0.063981-7.837722 4.958559-9.821146C1191.012355 47.641834 1238.61452 24.448575 1288.2321 6.149893c6.494113-2.431293 13.052207-5.150503 20.058171-5.854299 6.302168-0.639814 7.901704 0.383888 7.29388 7.101936-3.327033 36.43741-1.407591 73.066765-3.135089 109.536166-1.407591 29.751354-1.247637 59.598679 0.255926 89.382023 0.351898 7.549806 0.639814 15.131602 0.575832 22.649418-0.383888 35.765606 1.503563 71.499221 3.327033 107.200845 2.335321 47.186286 5.758326 94.276601 9.245313 141.398906 2.527266 34.006117 5.822308 67.948253 9.021379 101.922379 1.695507 18.586598 3.518977 37.141206 5.822308 55.631832 1.247637 10.205034 1.759489 10.301006 11.772578 8.957396 17.658868-2.399303 35.349726-4.350736 53.200539-4.09481 62.637796 0.799768 124.027954 10.684895 184.266447 27.863902 40.788146 11.580634 80.488608 26.040432 117.981712 46.290547a253.55831 253.55831 0 0 1 47.218277 32.438573zM308.676783 922.811488c23.161269-11.068783 135.608589-98.947243 144.533995-113.279078-54.576139-23.513166-109.344222-45.362816-168.239105-63.24562l23.70511 176.524698z m1277.196815-107.520752c2.879163-3.103098 2.559256-5.502401-1.343609-7.229899-7.773741-3.550968-15.4835-7.325871-23.353213-10.556932-42.003793-17.179007-84.19953-33.814173-127.482951-47.37823-3.774903-1.151665-7.645778-3.774903-12.476374-1.535554l23.321222 173.45359c3.454996 0.767777 4.798605-0.831758 6.33416-1.919442 39.316574-28.855614 78.889073-57.35933 116.638102-88.390312 6.36615-5.182494 12.668318-10.396978 18.362663-16.443221z" - fill="currentColor" - /> - </svg> </a> ); }; export const PersonalInfo: FC = () => { + const [open, setOpen] = useState(false); return ( - <p> - <GithubLink />、<BilibiliLink /> - </p> + <div + onClick={() => !open && setOpen(true)} + className={classNames(style.info, open && style.open)} + > + <GithubIcon /> + <GithubLink /> + <BiliBiliIcon /> + <BiliBiliLink /> + <div className={style.close} onClick={() => setOpen(false)}> + X + </div> + </div> ); }; diff --git a/src/components/ThemeChanger.module.scss b/src/components/ThemeChanger.module.scss new file mode 100644 index 0000000..c186400 --- /dev/null +++ b/src/components/ThemeChanger.module.scss @@ -0,0 +1,48 @@ +.container { + position: fixed; + right: 8px; + bottom: 8px; + transition: 0.3s; + color: white; + backdrop-filter: blur(8px); + box-sizing: border-box; + cursor: pointer; + user-select: none; + line-height: 52px; + width: 52px; + height: 52px; + overflow: visible; + + .square { + width: 52px; + height: 52px; + border-radius: 8px; + background: rgb(0 0 0/ 40%); + position: absolute; + right: 0; + top: 0; + transition: 0.3s; + overflow: hidden; + + img { + width: 100%; + height: 100%; + object-fit: contain; + } + } + + &.open { + .diy { + width: 80px; + font-weight: 900; + font-size: 18px; + background-image: linear-gradient( + -45deg, + #ee7752, + #e73c7e, + #23a6d5, + #23d5ab + ); + } + } +} diff --git a/src/components/ThemeChanger.tsx b/src/components/ThemeChanger.tsx new file mode 100644 index 0000000..29441f6 --- /dev/null +++ b/src/components/ThemeChanger.tsx @@ -0,0 +1,76 @@ +import React, { FC, useState } from 'react'; +import style from './ThemeChanger.module.scss'; +import classNames from 'classnames'; +import { fishermanTheme } from '../themes/fisherman'; +import { jinlunTheme } from '../themes/jinlun'; +import { ikunTheme } from '../themes/ikun'; +import { pddTheme } from '../themes/pdd'; +import { getDefaultTheme } from '../themes/default'; +import { Theme } from '../themes/interface'; + +const BuiltinThemes = [ + getDefaultTheme(), + fishermanTheme, + jinlunTheme, + ikunTheme, + pddTheme, +]; + +export const ThemeChanger: FC<{ + changeTheme: (theme: Theme<any>) => void; + onDiyClick: () => void; +}> = ({ changeTheme, onDiyClick }) => { + const [open, setOpen] = useState(false); + return ( + <div className={classNames(style.container, open && style.open)}> + {BuiltinThemes.map((theme, idx) => ( + <div + className={classNames(style.square)} + key={theme.title} + style={{ + opacity: open ? 1 : 0.3, + transform: open + ? `translateY(-${110 * (idx + 1)}%)` + : '', + }} + onClick={() => { + setOpen(false); + changeTheme(theme); + }} + > + {typeof theme.icons[0].content === 'string' ? ( + theme.icons[0].content.startsWith('http') ? ( + /*图片外链*/ + <img src={theme.icons[0].content} alt="" /> + ) : ( + /*字符表情*/ + <i>{theme.icons[0].content}</i> + ) + ) : ( + /*ReactNode*/ + theme.icons[0].content + )} + </div> + ))} + <div + className={classNames(style.square, style.diy)} + onClick={() => { + setOpen(false); + onDiyClick(); + }} + style={{ + opacity: open ? 1 : 0.3, + transform: open ? `translateY(-${110 * 6}%)` : '', + }} + > + DIY! + </div> + <div + onClick={() => setOpen(!open)} + className={classNames(style.square)} + > + {open ? '收起' : '更多'} + </div> + </div> + ); +}; diff --git a/src/components/Title.module.scss b/src/components/Title.module.scss new file mode 100644 index 0000000..4519879 --- /dev/null +++ b/src/components/Title.module.scss @@ -0,0 +1,63 @@ +@keyframes jump { + 0% { + transform: translateY(0); + } + + 10% { + transform: translateY(-50%); + } + + 20% { + transform: translateY(0); + } + + 100% { + transform: translateY(0); + } +} + +@keyframes scale { + 0% { + transform: scale(1); + } + + 100% { + transform: scale(1.05); + } +} + + +.title { + margin: 0; + line-height: 1.15; + font-size: 2rem; + padding-top: 1rem; + animation: scale 0.27s infinite alternate; + z-index: 1; + + * { + transition: 0.3s; + } + + .item { + display: inline-block; + animation-name: jump; + animation-iteration-count: infinite; + animation-duration: 3s; + animation-play-state: running; + text-shadow: 4px 6px 2px rgb(0 0 0 / 20%); + } +} + +.title, +.description { + text-align: center; +} + +.description { + margin: 0; + line-height: 1.5; + font-size: 1rem; + font-weight: 400; + padding-top: 0.5rem; +} diff --git a/src/components/Title.tsx b/src/components/Title.tsx new file mode 100644 index 0000000..b8190dc --- /dev/null +++ b/src/components/Title.tsx @@ -0,0 +1,24 @@ +import React, { FC } from 'react'; +import style from './Title.module.scss'; + +export const Title: FC<{ title: string; desc?: string }> = ({ + title, + desc, +}) => { + return ( + <> + <h1 className={style.title}> + {[...title].map((str, i) => ( + <span + className={style.item} + style={{ animationDelay: i / 10 + 's' }} + key={`${i}`} + > + {str} + </span> + ))} + </h1> + {desc && <h2 className={style.description}>{desc}</h2>} + </> + ); +}; diff --git a/src/main.tsx b/src/main.tsx index 31f647a..d3f35d3 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,6 +4,51 @@ import App from './App'; import './styles/global.scss'; import './styles/utils.scss'; import Bmob from 'hydrogen-js-sdk'; +import { + DEFAULT_BGM_STORAGE_KEY, + domRelatedOptForTheme, + parsePathCustomThemeId, + wrapThemeDefaultSounds, +} from './utils'; +import { getDefaultTheme } from './themes/default'; +import { Theme } from './themes/interface'; + +// react渲染 +const render = (theme: Theme<any>) => { + ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + <React.StrictMode> + <App theme={theme} /> + </React.StrictMode> + ); +}; + +// 错误提示 +const errorTip = (tip: string) => { + setTimeout(() => { + document.getElementById('loading')?.classList.add('error'); + document.getElementById('loadingText')!.innerText = tip; + document.getElementById('backHomeTip')!.style.visibility = 'visible'; + }, 600); +}; + +// 加载成功后数据转换(runtime)以及转场 +const successTrans = (theme: Theme<any>) => { + wrapThemeDefaultSounds(theme); + + setTimeout(() => { + domRelatedOptForTheme(theme); + const root = document.getElementById('root'); + root!.style.opacity = '0'; + document.getElementById('loading')?.classList.add('success'); + setTimeout(() => { + render(theme); + root!.style.opacity = '1'; + }, 600); + }, 500); +}; + +// 从url初始化主题 +const customThemeIdFromPath = parsePathCustomThemeId(location.href); // Bmob初始化 // @ts-ignore @@ -12,8 +57,54 @@ Bmob.initialize( import.meta.env.VITE_BMOB_SECCODE ); -ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( - <React.StrictMode> - <App /> - </React.StrictMode> -); +const loadTheme = () => { + // 请求主题 + if (customThemeIdFromPath) { + const storageTheme = localStorage.getItem(customThemeIdFromPath); + if (storageTheme) { + try { + const customTheme = JSON.parse(storageTheme); + successTrans(customTheme); + } catch (e) { + errorTip('主题配置解析失败'); + } + } else { + Bmob.Query('config') + .get(customThemeIdFromPath) + .then((res) => { + const { content } = res as any; + localStorage.setItem(customThemeIdFromPath, content); + try { + const customTheme = JSON.parse(content); + successTrans(customTheme); + } catch (e) { + errorTip('主题配置解析失败'); + } + }) + .catch(({ error }) => { + errorTip(error); + }); + } + } else { + successTrans(getDefaultTheme()); + } +}; + +// 音效资源请求 +if (!localStorage.getItem(DEFAULT_BGM_STORAGE_KEY)) { + const query = Bmob.Query('file'); + query.equalTo('type', '==', 'default'); + query + .find() + .then((results) => { + for (const file of results as any) { + localStorage.setItem(file.name, file.base64); + } + loadTheme(); + }) + .catch((e) => { + errorTip(e); + }); +} else { + loadTheme(); +} diff --git a/src/styles/global.scss b/src/styles/global.scss index c769682..6eadd68 100644 --- a/src/styles/global.scss +++ b/src/styles/global.scss @@ -1,11 +1,6 @@ -:root { - font-family: Inter, Avenir, Helvetica, Arial, sans-serif; - font-size: 16px; - line-height: 24px; - font-weight: 400; - color-scheme: light dark; - color: rgb(255 255 255 / 87%); - background-color: #242424; +body { + padding: 0 32px; + -webkit-tap-highlight-color: transparent; font-synthesis: none; text-rendering: optimizelegibility; -webkit-font-smoothing: antialiased; @@ -13,24 +8,26 @@ text-size-adjust: 100%; } -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; +#root { + text-align: center; + width: 100%; + max-width: 500px; + margin: 0 auto; + min-height: 100vh; + display: flex; + flex-direction: column; } -a:hover { - color: #535bf2; -} +button { + color: white; + border: none; + padding: 8px 16px; + border-radius: 8px; + background-color: #3338; -body { - margin: 0; - padding: 0 32px; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; + &.primary { + background-color: #747bff; + } } select { @@ -45,40 +42,4 @@ input { padding: 8px 12px; } -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; - word-break: keep-all; - outline: none; - &.primary { - background-color: #646cff; - color: white; - } - - &:hover:not(.primary) { - border-color: #646cff; - } -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #fff; - } - - a:hover { - color: #747bff; - } - - button { - background-color: #f9f9f9; - } -} diff --git a/src/styles/utils.scss b/src/styles/utils.scss index dc777f3..f204e4b 100644 --- a/src/styles/utils.scss +++ b/src/styles/utils.scss @@ -4,6 +4,10 @@ flex-wrap: wrap; } +.flex-no-wrap { + flex-wrap: nowrap; +} + .flex-center { justify-content: center; align-items: center; diff --git a/src/themes/default/index.ts b/src/themes/default/index.ts index 5a0e86a..e1054f4 100644 --- a/src/themes/default/index.ts +++ b/src/themes/default/index.ts @@ -1,4 +1,9 @@ import { Theme } from '../interface'; +import { + DEFAULT_BGM_STORAGE_KEY, + DEFAULT_CLICK_SOUND_STORAGE_KEY, + DEFAULT_TRIPLE_SOUND_STORAGE_KEY, +} from '../../utils'; const icons = <const>[ `🎨`, @@ -15,27 +20,31 @@ const icons = <const>[ export type DefaultSoundNames = 'button-click' | 'triple'; -import soundButtonClickUrl from './sounds/sound-button-click.mp3'; -import soundTripleUrl from './sounds/sound-triple.mp3'; -export const defaultSounds: Theme<DefaultSoundNames>['sounds'] = [ - { - name: 'button-click', - src: soundButtonClickUrl, - }, - { - name: 'triple', - src: soundTripleUrl, - }, -]; - -export const defaultTheme: Theme<DefaultSoundNames> = { - title: '有解的羊了个羊(DEMO)', - name: '默认', - icons: icons.map((icon) => ({ - name: icon, - content: icon, - clickSound: 'button-click', - tripleSound: 'triple', - })), - sounds: defaultSounds, +export const getDefaultTheme: () => Theme<DefaultSoundNames> = () => { + return { + title: '有解的羊了个羊', + desc: '真的可以通关~', + dark: true, + backgroundColor: '#8dac85', + icons: icons.map((icon) => ({ + name: icon, + content: icon, + clickSound: 'button-click', + tripleSound: 'triple', + })), + sounds: [ + { + name: 'button-click', + src: + localStorage.getItem(DEFAULT_CLICK_SOUND_STORAGE_KEY) || '', + }, + { + name: 'triple', + src: + localStorage.getItem(DEFAULT_TRIPLE_SOUND_STORAGE_KEY) || + '', + }, + ], + bgm: localStorage.getItem(DEFAULT_BGM_STORAGE_KEY) || '', + }; }; diff --git a/src/themes/default/sounds/sound-button-click.mp3 b/src/themes/default/sounds/sound-button-click.mp3 deleted file mode 100644 index 39b8ee6..0000000 Binary files a/src/themes/default/sounds/sound-button-click.mp3 and /dev/null differ diff --git a/src/themes/default/sounds/sound-triple.mp3 b/src/themes/default/sounds/sound-triple.mp3 deleted file mode 100644 index f6f06be..0000000 Binary files a/src/themes/default/sounds/sound-triple.mp3 and /dev/null differ diff --git a/src/themes/fisherman/index.tsx b/src/themes/fisherman/index.tsx index 2185bd7..953f6b7 100644 --- a/src/themes/fisherman/index.tsx +++ b/src/themes/fisherman/index.tsx @@ -1,7 +1,7 @@ // 钓鱼佬主题 import React from 'react'; import { Theme } from '../interface'; -import { DefaultSoundNames, defaultSounds } from '../default'; +import { DefaultSoundNames } from '../default'; const imagesUrls = import.meta.glob('./images/*.png', { import: 'default', @@ -17,12 +17,11 @@ const fishes = Object.entries(imagesUrls).map(([key, value]) => ({ export const fishermanTheme: Theme<DefaultSoundNames> = { title: '🐟鱼了个鱼🐟', - name: '钓鱼佬', icons: fishes.map(({ name, content }) => ({ name, content, clickSound: 'button-click', tripleSound: 'triple', })), - sounds: defaultSounds, + sounds: [], }; diff --git a/src/themes/ikun/index.tsx b/src/themes/ikun/index.tsx index 5747922..e9401ee 100644 --- a/src/themes/ikun/index.tsx +++ b/src/themes/ikun/index.tsx @@ -53,7 +53,6 @@ const icons = Object.entries(imagesUrls).map(([key, value]) => ({ export const ikunTheme: Theme<SoundNames> = { title: '🐔鸡了个鸡🐔', - name: 'iKun', bgm, icons: icons.map(({ name, content }) => ({ name, diff --git a/src/themes/interface.ts b/src/themes/interface.ts index 08b4951..8f3078d 100644 --- a/src/themes/interface.ts +++ b/src/themes/interface.ts @@ -14,14 +14,14 @@ export interface Sound<T = string> { type Operation = 'shift' | 'undo' | 'wash'; -// TODO title name 冗余 export interface Theme<SoundNames> { title: string; - desc?: ReactNode; - name: string; + desc?: string; bgm?: string; background?: string; + backgroundColor?: string; backgroundBlur?: boolean; + dark?: boolean; pure?: boolean; icons: Icon<SoundNames>[]; sounds: Sound<SoundNames>[]; diff --git a/src/themes/jinlun/index.tsx b/src/themes/jinlun/index.tsx index 78ab71c..cbeb7b1 100644 --- a/src/themes/jinlun/index.tsx +++ b/src/themes/jinlun/index.tsx @@ -25,7 +25,6 @@ const icons = Object.entries(imagesUrls).map(([key, value]) => ({ export const jinlunTheme: Theme<string> = { title: '🐎马了个马🐎', - name: '金轮', icons: icons.map(({ name, content }) => ({ name, content, diff --git a/src/themes/ow/images/ow.png b/src/themes/ow/images/ow.png deleted file mode 100644 index 83ed96c..0000000 Binary files a/src/themes/ow/images/ow.png and /dev/null differ diff --git a/src/themes/ow/images/上勾拳.png b/src/themes/ow/images/上勾拳.png deleted file mode 100644 index 4c34d14..0000000 Binary files a/src/themes/ow/images/上勾拳.png and /dev/null differ diff --git a/src/themes/ow/images/你倒是跑啊.png b/src/themes/ow/images/你倒是跑啊.png deleted file mode 100644 index aaba22a..0000000 Binary files a/src/themes/ow/images/你倒是跑啊.png and /dev/null differ diff --git a/src/themes/ow/images/吃个娜娜.png b/src/themes/ow/images/吃个娜娜.png deleted file mode 100644 index 37f2b00..0000000 Binary files a/src/themes/ow/images/吃个娜娜.png and /dev/null differ diff --git a/src/themes/ow/images/末日铁拳来了.png b/src/themes/ow/images/末日铁拳来了.png deleted file mode 100644 index 0d3f502..0000000 Binary files a/src/themes/ow/images/末日铁拳来了.png and /dev/null differ diff --git a/src/themes/ow/images/杀人哥嘎嘎.png b/src/themes/ow/images/杀人哥嘎嘎.png deleted file mode 100644 index dc76be5..0000000 Binary files a/src/themes/ow/images/杀人哥嘎嘎.png and /dev/null differ diff --git a/src/themes/ow/images/李甲抡.png b/src/themes/ow/images/李甲抡.png deleted file mode 100644 index 69f8667..0000000 Binary files a/src/themes/ow/images/李甲抡.png and /dev/null differ diff --git a/src/themes/ow/images/毁天灭地.png b/src/themes/ow/images/毁天灭地.png deleted file mode 100644 index 0265e27..0000000 Binary files a/src/themes/ow/images/毁天灭地.png and /dev/null differ diff --git a/src/themes/ow/images/离开嗷ruai.png b/src/themes/ow/images/离开嗷ruai.png deleted file mode 100644 index 172cb7a..0000000 Binary files a/src/themes/ow/images/离开嗷ruai.png and /dev/null differ diff --git a/src/themes/ow/images/脑瘫.png b/src/themes/ow/images/脑瘫.png deleted file mode 100644 index fd2ba67..0000000 Binary files a/src/themes/ow/images/脑瘫.png and /dev/null differ diff --git a/src/themes/ow/index.tsx b/src/themes/ow/index.tsx deleted file mode 100644 index 4ba3348..0000000 --- a/src/themes/ow/index.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { Theme } from '../interface'; -import React from 'react'; -import { defaultSounds } from '../default'; - -const soundUrls = import.meta.glob('./sounds/*.mp3', { - import: 'default', - eager: true, -}); - -const sounds = Object.entries(soundUrls).map(([key, value]) => ({ - name: key.slice(9, -4), - src: value, -})) as Theme<string>['sounds']; - -const imagesUrls = import.meta.glob('./images/*.png', { - import: 'default', - eager: true, -}); - -const icons = Object.entries(imagesUrls).map(([key, value]) => ({ - name: key.slice(9, -4), - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - content: <img src={value} alt="" />, -})); - -export const owTheme: Theme<string> = { - title: '守望先锋', - desc: ( - <p> - 感谢 - <a - href="https://space.bilibili.com/228122468" - target="_blank" - rel="noreferrer" - > - 白板特工华南虎 - </a> - 提供素材 - </p> - ), - name: 'OW', - icons: icons.map(({ name, content }) => ({ - name, - content, - clickSound: 'button-click', - tripleSound: name === 'ow' ? 'triple' : name, - })), - sounds: [...defaultSounds, ...sounds], -}; diff --git a/src/themes/ow/sounds/上勾拳.mp3 b/src/themes/ow/sounds/上勾拳.mp3 deleted file mode 100644 index 14ea09f..0000000 Binary files a/src/themes/ow/sounds/上勾拳.mp3 and /dev/null differ diff --git a/src/themes/ow/sounds/你倒是跑啊.mp3 b/src/themes/ow/sounds/你倒是跑啊.mp3 deleted file mode 100644 index 3bfdf0c..0000000 Binary files a/src/themes/ow/sounds/你倒是跑啊.mp3 and /dev/null differ diff --git a/src/themes/ow/sounds/吃个娜娜.mp3 b/src/themes/ow/sounds/吃个娜娜.mp3 deleted file mode 100644 index a9d6a92..0000000 Binary files a/src/themes/ow/sounds/吃个娜娜.mp3 and /dev/null differ diff --git a/src/themes/ow/sounds/末日铁拳来了.mp3 b/src/themes/ow/sounds/末日铁拳来了.mp3 deleted file mode 100644 index 1bc1c57..0000000 Binary files a/src/themes/ow/sounds/末日铁拳来了.mp3 and /dev/null differ diff --git a/src/themes/ow/sounds/杀人哥嘎嘎.mp3 b/src/themes/ow/sounds/杀人哥嘎嘎.mp3 deleted file mode 100644 index 235b7de..0000000 Binary files a/src/themes/ow/sounds/杀人哥嘎嘎.mp3 and /dev/null differ diff --git a/src/themes/ow/sounds/李甲抡.mp3 b/src/themes/ow/sounds/李甲抡.mp3 deleted file mode 100644 index 9bb5bfc..0000000 Binary files a/src/themes/ow/sounds/李甲抡.mp3 and /dev/null differ diff --git a/src/themes/ow/sounds/毁天灭地.mp3 b/src/themes/ow/sounds/毁天灭地.mp3 deleted file mode 100644 index ae3ae87..0000000 Binary files a/src/themes/ow/sounds/毁天灭地.mp3 and /dev/null differ diff --git a/src/themes/ow/sounds/离开嗷ruai.mp3 b/src/themes/ow/sounds/离开嗷ruai.mp3 deleted file mode 100644 index 201d43f..0000000 Binary files a/src/themes/ow/sounds/离开嗷ruai.mp3 and /dev/null differ diff --git a/src/themes/ow/sounds/脑瘫.mp3 b/src/themes/ow/sounds/脑瘫.mp3 deleted file mode 100644 index 299ec00..0000000 Binary files a/src/themes/ow/sounds/脑瘫.mp3 and /dev/null differ diff --git a/src/themes/pdd/index.tsx b/src/themes/pdd/index.tsx index 231a5b0..e639d06 100644 --- a/src/themes/pdd/index.tsx +++ b/src/themes/pdd/index.tsx @@ -1,7 +1,6 @@ // 骚猪主题 import React from 'react'; import { Theme } from '../interface'; -import { defaultSounds } from '../default'; import bgm from './sounds/bgm.mp3'; const soundUrls = import.meta.glob('./sounds/*.mp3', { @@ -28,20 +27,7 @@ const images = Object.entries(imagesUrls).map(([key, value]) => ({ export const pddTheme: Theme<string> = { title: '🐷猪了个猪🐷', - desc: ( - <p> - 感谢 - <a - href="https://space.bilibili.com/81966051" - target="_blank" - rel="noreferrer" - > - 猪酱的日常 - </a> - 提供素材 - </p> - ), - name: '骚猪', + desc: '感谢 @猪酱的日常 提供素材', bgm: bgm, icons: images.map(({ name, content }) => ({ name, @@ -49,5 +35,5 @@ export const pddTheme: Theme<string> = { clickSound: 'button-click', tripleSound: name, })), - sounds: [defaultSounds[0], ...sounds], + sounds, }; diff --git a/src/utils.ts b/src/utils.ts index 441d5da..439b4fa 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,11 @@ import { Theme } from './themes/interface'; +import { getDefaultTheme } from './themes/default'; + +export const LAST_LEVEL_STORAGE_KEY = 'lastLevel'; +export const LAST_SCORE_STORAGE_KEY = 'lastScore'; +export const DEFAULT_BGM_STORAGE_KEY = 'defaultBgm'; +export const DEFAULT_TRIPLE_SOUND_STORAGE_KEY = 'defaultTripleSound'; +export const DEFAULT_CLICK_SOUND_STORAGE_KEY = 'defaultClickSound'; export const randomString: (len: number) => string = (len) => { const pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; @@ -18,14 +25,7 @@ export const waitTimeout: (timeout: number) => Promise<void> = (timeout) => { }); }; -// 从url获取内置主题name -export const parsePathThemeName: (url: string) => string = (url) => { - const urlObj = new URL(url); - const params = urlObj.searchParams; - return decodeURIComponent(params.get('theme') || '默认'); -}; - -// 从url解析自定义主题JSON +// 从url获取自定义主题Id export const parsePathCustomThemeId: (url: string) => string = (url) => { const urlObj = new URL(url); const params = urlObj.searchParams; @@ -63,3 +63,47 @@ export const captureElement = (id: string, filename: string) => { 1 ); }; + +export const wrapThemeDefaultSounds: (theme: Theme<any>) => void = (theme) => { + const defaultTheme = getDefaultTheme(); + // 默认音频资源补充 + if (!theme.bgm) { + theme.bgm = defaultTheme.bgm; + } + let hasUseDefaultTriple, hasUseDefaultClick; + for (const icon of theme.icons) { + if (!icon.clickSound) icon.clickSound = 'button-click'; + if (!icon.tripleSound) icon.tripleSound = 'triple'; + if (icon.clickSound === 'button-click') hasUseDefaultClick = true; + if (icon.tripleSound === 'triple') hasUseDefaultTriple = true; + } + if ( + hasUseDefaultClick && + !theme.sounds.find((s) => s.name === 'button-click') + ) { + const defaultClick = defaultTheme.sounds.find( + (s) => s.name === 'button-click' + ); + defaultClick && theme.sounds.push(defaultClick); + } + if (hasUseDefaultTriple && !theme.sounds.find((s) => s.name === 'triple')) { + const defaultTripleSound = defaultTheme.sounds.find( + (s) => s.name === 'triple' + ); + defaultTripleSound && theme.sounds.push(defaultTripleSound); + } + + // 兼容旧数据 + for (const sound of theme.sounds) { + if (['triple', 'button-click'].includes(sound.name)) + // @ts-ignore + sound.src = defaultTheme.sounds.find( + (s) => s.name === sound.name + ).src; + } +}; + +export const domRelatedOptForTheme = (theme: Theme<any>) => { + document.body.style.backgroundColor = theme.backgroundColor || 'white'; + document.body.style.color = theme.dark ? 'white' : 'rgb(0 0 0 / 60%)'; +};