mirror of
https://ghproxy.com/https://github.com/StreakingMan/solvable-sheep-game
synced 2025-05-23 21:56:07 +08:00
Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
09e43ae02a | ||
![]() |
c5bd3f01d9 | ||
![]() |
be0faff9fd | ||
![]() |
cebf64847f | ||
![]() |
ffbb38495f | ||
![]() |
e553e5360f | ||
![]() |
d234bd06a8 | ||
![]() |
680811c50c | ||
![]() |
ffabf6805f | ||
![]() |
58bff1ce98 |
0
.husky/commit-msg
Executable file → Normal file
0
.husky/commit-msg
Executable file → Normal file
0
.husky/pre-commit
Executable file → Normal file
0
.husky/pre-commit
Executable file → Normal file
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -2,6 +2,20 @@
|
|||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
## [1.1.0](https://github.com/StreakingMan/solvable-sheep-game/compare/v1.0.1...v1.1.0) (2023-05-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 本地图片文件压缩 ([ffbb384](https://github.com/StreakingMan/solvable-sheep-game/commit/ffbb38495f2c8c124f6039496f613e6c416d9db4))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 图片文件大小校验逻辑 ([be0faff](https://github.com/StreakingMan/solvable-sheep-game/commit/be0faff9fdf4aea6a2659325520d68b2f348c656))
|
||||
* 自定义主题编辑回显 ([ffabf68](https://github.com/StreakingMan/solvable-sheep-game/commit/ffabf6805f81dd03bd070dd09a32e51a51d1138f))
|
||||
* 音频文件切换为minio外链 ([680811c](https://github.com/StreakingMan/solvable-sheep-game/commit/680811c50cbef3fec80b73407daddcf48847f3bd))
|
||||
|
||||
### [1.0.1](https://github.com/StreakingMan/solvable-sheep-game/compare/v1.0.0...v1.0.1) (2022-10-19)
|
||||
|
||||
|
||||
|
|
|
@ -76,7 +76,9 @@ vite+react 实现,欢迎 star、issue、pr、fork(尽量标注原仓库地
|
|||
|
||||
## 资助
|
||||
|
||||
由于各种白嫖的静态资源托管、后台服务的免费额度都已用完,目前自费升级了相关套餐。
|
||||
如果您喜欢这个项目,觉得本项目对你有帮助的话,可以扫描下方付款码请我喝杯咖啡 ☕️/分摊后台服务费用~ 😘
|
||||
~~由于各种白嫖的静态资源托管、后台服务的免费额度都已用完,目前自费升级了相关套餐。~~
|
||||
如果您喜欢这个项目,觉得本项目对你有帮助的话,可以扫描下方付款码请我喝杯咖啡 ☕️/~~分摊后台服务费用~~~ 😘
|
||||
|
||||
2023.5.5 更新:Bmob 服务到期,后台服务已下线,相关功能暂时无法使用,如有需要请自行搭建后台服务
|
||||
|
||||

|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
游戏的核心逻辑已经封装到了 `src/components/Game.tsx` ,方便大家魔改, 主题配置的类型声明见 `src/themes/interface.ts`
|
||||
|
||||
你可以先通过超酷的[stackblitz](https://stackblitz.com/edit/solvable-sheep-game?file=diy%2Fdiy.theme.json&terminal=dev:diy)
|
||||
在线体验一番(等待依赖安装完成后,编辑配置将实时更新)再回来看这里的指南。
|
||||
|
||||
## 准备工作
|
||||
|
||||
### 环境准备
|
||||
|
@ -68,8 +71,8 @@ yarn dev:diy
|
|||
yarn build:diy
|
||||
```
|
||||
|
||||
会在 `diy/diy-dist` 下生成静态资源,直接将这些文件复制服务器上做代理即可。如果嫌麻烦,推荐使用 [vercel](https://vercel.com/)
|
||||
一键部署(每月免费 100G 流量), 将更改后的项目推到自己的 github(gitlab,bitbucket 同样支持)仓库,
|
||||
会在 `diy/diy-dist` 下生成静态资源,直接将这些文件复制服务器上做代理即可。如果嫌麻烦,推荐使用 [vercel](https://vercel.com/)
|
||||
一键部署(每月免费 100G 流量), 将更改后的项目推到自己的 github(gitlab,bitbucket 同样支持)仓库,
|
||||
使用 github 账号登录 vercel 后导入该项目,构建模版选择 vite,
|
||||
构建命令更改为 `yarn build:diy` 输出地址改为 `diy/diy-dist` 即可 。见下图:
|
||||
|
||||
|
@ -79,7 +82,7 @@ yarn build:diy
|
|||
|
||||
## 其他
|
||||
|
||||
如果您想体验项目的完整功能,则需要注册一个 [Bmob](https://www.bmobapp.com/) 账号,
|
||||
如果您想体验项目的完整功能,则需要注册一个 [Bmob](https://www.bmobapp.com/) 账号,
|
||||
注册后新建应用(有一年的白嫖版,免费请求数虽然很客观,但并发数有限制,请根据自己的实际流量
|
||||
选择升级套餐,或者其他存储方案)
|
||||
|
||||
|
@ -94,9 +97,6 @@ ps: 如果您的项目托管在公共仓库中,请注意保护密钥,本地
|
|||
|
||||
`config` 表用来存储自定义配置的 json 字符串,需要新增 `content` 列
|
||||
|
||||
`file` 表则是为了节省 vercel 流量,将一些默认文件转为 base64 字符串存到了数据库中,需要添加三列
|
||||

|
||||
|
||||
`rank` 表,储存排名信息
|
||||

|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 58 KiB |
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "solvable-sheep-game",
|
||||
"private": false,
|
||||
"version": "1.0.1",
|
||||
"version": "1.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
|
|
@ -15,6 +15,8 @@ import {
|
|||
randomString,
|
||||
wrapThemeDefaultSounds,
|
||||
LAST_UPLOAD_TIME_STORAGE_KEY,
|
||||
canvasToFile,
|
||||
createCanvasByImgSrc,
|
||||
} from '../utils';
|
||||
import { copy } from 'clipboard';
|
||||
import { CloseIcon } from './CloseIcon';
|
||||
|
@ -144,10 +146,14 @@ const ConfigDialog: FC<{
|
|||
new Array(10).fill('')
|
||||
);
|
||||
// 文件体积校验开关
|
||||
const initEnableFileSizeValidate = localStorage.getItem(
|
||||
CUSTOM_THEME_FILE_VALIDATE_STORAGE_KEY
|
||||
);
|
||||
const [enableFileSizeValidate, setEnableFileSizeValidate] =
|
||||
useState<boolean>(
|
||||
localStorage.getItem(CUSTOM_THEME_FILE_VALIDATE_STORAGE_KEY) !==
|
||||
'false'
|
||||
initEnableFileSizeValidate === null
|
||||
? true
|
||||
: initEnableFileSizeValidate === 'true'
|
||||
);
|
||||
useEffect(() => {
|
||||
localStorage.setItem(
|
||||
|
@ -163,7 +169,7 @@ const ConfigDialog: FC<{
|
|||
type: 'bgm' | 'background' | 'sound' | 'icon';
|
||||
file?: File;
|
||||
idx?: number;
|
||||
}) => void = ({ type, file, idx }) => {
|
||||
}) => void = async ({ type, file, idx }) => {
|
||||
if (!file) return;
|
||||
switch (type) {
|
||||
case 'bgm':
|
||||
|
@ -181,16 +187,20 @@ const ConfigDialog: FC<{
|
|||
break;
|
||||
case 'background':
|
||||
setBackgroundError('');
|
||||
if (enableFileSizeValidate && file.size > 80 * 1024) {
|
||||
return setBackgroundError('请选择80k以内全损画质的图片');
|
||||
try {
|
||||
const _file = enableFileSizeValidate
|
||||
? await canvasToFile({
|
||||
canvas: await createCanvasByImgSrc({
|
||||
imgSrc: await getFileBase64String(file),
|
||||
}),
|
||||
maxFileSize: 20 * 1024,
|
||||
})
|
||||
: file;
|
||||
const fileBase64 = await getFileBase64String(_file);
|
||||
updateCustomTheme('background', fileBase64);
|
||||
} catch (e: any) {
|
||||
setBackgroundError(e);
|
||||
}
|
||||
getFileBase64String(file)
|
||||
.then((res) => {
|
||||
updateCustomTheme('background', res);
|
||||
})
|
||||
.catch((e) => {
|
||||
setBackgroundError(e);
|
||||
});
|
||||
break;
|
||||
case 'sound':
|
||||
setSoundError('');
|
||||
|
@ -208,23 +218,27 @@ const ConfigDialog: FC<{
|
|||
case 'icon':
|
||||
if (idx == null) return;
|
||||
setIconErrors(makeIconErrors(idx, ''));
|
||||
if (enableFileSizeValidate && file.size > 5 * 1024) {
|
||||
return setIconErrors(
|
||||
makeIconErrors(idx, '请选择5k以内的图片文件')
|
||||
try {
|
||||
const _file = enableFileSizeValidate
|
||||
? await canvasToFile({
|
||||
canvas: await createCanvasByImgSrc({
|
||||
imgSrc: await getFileBase64String(file),
|
||||
}),
|
||||
maxFileSize: 4 * 1024,
|
||||
})
|
||||
: file;
|
||||
const fileBase64 = await getFileBase64String(_file);
|
||||
updateCustomTheme(
|
||||
'icons',
|
||||
customTheme.icons.map((icon, _idx) =>
|
||||
_idx === idx
|
||||
? { ...icon, content: fileBase64 }
|
||||
: icon
|
||||
)
|
||||
);
|
||||
} catch (e: any) {
|
||||
setIconErrors(makeIconErrors(idx, e));
|
||||
}
|
||||
getFileBase64String(file)
|
||||
.then((res) => {
|
||||
updateCustomTheme(
|
||||
'icons',
|
||||
customTheme.icons.map((icon, _idx) =>
|
||||
_idx === idx ? { ...icon, content: res } : icon
|
||||
)
|
||||
);
|
||||
})
|
||||
.catch((e) => {
|
||||
setIconErrors(makeIconErrors(idx, e));
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -251,7 +265,9 @@ const ConfigDialog: FC<{
|
|||
if (configString) {
|
||||
const parseRes = JSON.parse(configString);
|
||||
if (typeof parseRes === 'object') {
|
||||
setCustomTheme(parseRes);
|
||||
setTimeout(() => {
|
||||
setCustomTheme(parseRes);
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
@ -357,11 +373,7 @@ const ConfigDialog: FC<{
|
|||
})
|
||||
.catch(({ error, code }) => {
|
||||
setTimeout(() => {
|
||||
setConfigError(
|
||||
code === 10007
|
||||
? '上传数据长度已超过bmob的限制'
|
||||
: error
|
||||
);
|
||||
setConfigError(error);
|
||||
}, 3000);
|
||||
})
|
||||
.finally(() => {
|
||||
|
@ -391,6 +403,17 @@ const ConfigDialog: FC<{
|
|||
<CloseIcon fill={'#fff'} />
|
||||
</div>
|
||||
<h2>自定义主题</h2>
|
||||
<p style={{ color: 'red' }}>
|
||||
后台服务到期,分享链接功能已停用,有需要的同学参考
|
||||
<a
|
||||
href="https://github.com/StreakingMan/solvable-sheep-game/blob/master/diy/README.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
👉这里👈
|
||||
</a>
|
||||
自行部署,感谢支持!🙏
|
||||
</p>
|
||||
|
||||
<InputContainer label={'标题'} required>
|
||||
<input
|
||||
|
@ -408,7 +431,7 @@ const ConfigDialog: FC<{
|
|||
</InputContainer>
|
||||
<InputContainer label={'BGM'}>
|
||||
<div className={style.tip}>
|
||||
接口上传体积有限制,上传文件请全力压缩到80k以下
|
||||
接口上传体积有限制,上传文件请全力压缩到80k以下,推荐使用外链
|
||||
</div>
|
||||
<input
|
||||
type={'file'}
|
||||
|
@ -430,7 +453,7 @@ const ConfigDialog: FC<{
|
|||
</InputContainer>
|
||||
<InputContainer label={'背景图'}>
|
||||
<div className={style.tip}>
|
||||
接口上传体积有限制,上传文件请全力压缩到80k以下
|
||||
接口上传体积有限制,上传的图片将会被严重压缩,推荐使用外链
|
||||
</div>
|
||||
<input
|
||||
type={'file'}
|
||||
|
@ -536,7 +559,7 @@ const ConfigDialog: FC<{
|
|||
onChange={(e) => onNewSoundChange('name', e.target.value)}
|
||||
/>
|
||||
<div className={style.tip}>
|
||||
接口上传体积有限制,上传文件请全力压缩到10k以下
|
||||
接口上传体积有限制,上传文件请全力压缩到10k以下,推荐使用外链
|
||||
</div>
|
||||
<input
|
||||
type={'file'}
|
||||
|
@ -560,7 +583,7 @@ const ConfigDialog: FC<{
|
|||
</InputContainer>
|
||||
<InputContainer label={'图标素材'} required>
|
||||
<div className={style.tip}>
|
||||
接口上传体积有限制,上传文件请全力压缩到5k以下,推荐尺寸56*56
|
||||
接口上传体积有限制,上传的图片将会被严重压缩,推荐使用外链
|
||||
</div>
|
||||
</InputContainer>
|
||||
{customTheme.icons.map((icon, idx) => (
|
||||
|
@ -685,7 +708,7 @@ const ConfigDialog: FC<{
|
|||
</div>
|
||||
)}
|
||||
<div className={style.tip}>
|
||||
接口上传内容总体积有限制,上传文件失败请进一步压缩文件,或者使用外链(自行搜索【免费图床】【免费mp3外链】【对象存储服务】等关键词)。
|
||||
接口上传内容总体积有限制,上传文件失败请尝试进一步压缩文件,推荐使用外链(自行搜索【免费图床】【免费mp3外链】【对象存储服务】等关键词)。
|
||||
本地整活,勾选右侧关闭文件大小校验👉
|
||||
<input
|
||||
type={'checkbox'}
|
||||
|
@ -710,16 +733,17 @@ const ConfigDialog: FC<{
|
|||
>
|
||||
保存并预览
|
||||
</button>
|
||||
<button
|
||||
className={classNames(
|
||||
'primary flex-grow',
|
||||
style.uploadBtn,
|
||||
uploading && style.uploading
|
||||
)}
|
||||
onClick={onGenQrLinkClick}
|
||||
>
|
||||
生成二维码&链接
|
||||
</button>
|
||||
{/*<button*/}
|
||||
{/* className={classNames(*/}
|
||||
{/* 'primary flex-grow',*/}
|
||||
{/* style.uploadBtn,*/}
|
||||
{/* uploading && style.uploading*/}
|
||||
{/* )}*/}
|
||||
{/* onClick={onGenQrLinkClick}*/}
|
||||
{/* disabled*/}
|
||||
{/*>*/}
|
||||
{/* 生成二维码&链接*/}
|
||||
{/*</button>*/}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { FC, MouseEventHandler, useState } from 'react';
|
|||
import style from './WxQrCode.module.scss';
|
||||
import classNames from 'classnames';
|
||||
const WxQrCode: FC<{ title?: string; onClick?: MouseEventHandler }> = ({
|
||||
title = '如果您喜欢这个项目的话,可以点击扫描下方收款码分担后台相关费用(或请我喝杯咖啡),感谢~😘',
|
||||
title = '如果您喜欢这个项目的话,点击扫描下方收款码请我喝杯咖啡,感谢~😘',
|
||||
onClick,
|
||||
}) => {
|
||||
const [fullScreen, setFullScreen] = useState<Record<number, boolean>>({
|
||||
|
|
83
src/main.tsx
83
src/main.tsx
|
@ -5,11 +5,7 @@ import './styles/global.scss';
|
|||
import './styles/utils.scss';
|
||||
import Bmob from 'hydrogen-js-sdk';
|
||||
import {
|
||||
DEFAULT_BGM_STORAGE_KEY,
|
||||
domRelatedOptForTheme,
|
||||
LAST_LEVEL_STORAGE_KEY,
|
||||
LAST_SCORE_STORAGE_KEY,
|
||||
LAST_TIME_STORAGE_KEY,
|
||||
parsePathCustomThemeId,
|
||||
PLAYING_THEME_ID_STORAGE_KEY,
|
||||
resetScoreStorage,
|
||||
|
@ -77,58 +73,37 @@ Bmob.initialize(
|
|||
import.meta.env.VITE_BMOB_SECCODE
|
||||
);
|
||||
|
||||
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, increment } = res as any;
|
||||
localStorage.setItem(customThemeIdFromPath, content);
|
||||
try {
|
||||
const customTheme = JSON.parse(content);
|
||||
successTrans(customTheme);
|
||||
} catch (e) {
|
||||
errorTip('主题配置解析失败');
|
||||
}
|
||||
// 统计访问次数
|
||||
increment('visitNum');
|
||||
// @ts-ignore
|
||||
res.save();
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
errorTip(error);
|
||||
});
|
||||
// 请求主题
|
||||
if (customThemeIdFromPath) {
|
||||
const storageTheme = localStorage.getItem(customThemeIdFromPath);
|
||||
if (storageTheme) {
|
||||
try {
|
||||
const customTheme = JSON.parse(storageTheme);
|
||||
successTrans(customTheme);
|
||||
} catch (e) {
|
||||
errorTip('主题配置解析失败');
|
||||
}
|
||||
} else {
|
||||
successTrans(getDefaultTheme());
|
||||
Bmob.Query('config')
|
||||
.get(customThemeIdFromPath)
|
||||
.then((res) => {
|
||||
const { content, increment } = res as any;
|
||||
localStorage.setItem(customThemeIdFromPath, content);
|
||||
try {
|
||||
const customTheme = JSON.parse(content);
|
||||
successTrans(customTheme);
|
||||
} catch (e) {
|
||||
errorTip('主题配置解析失败');
|
||||
}
|
||||
// 统计访问次数
|
||||
increment('visitNum');
|
||||
// @ts-ignore
|
||||
res.save();
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
errorTip(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 音效资源请求
|
||||
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(({ error }) => {
|
||||
errorTip(error);
|
||||
});
|
||||
} else {
|
||||
loadTheme();
|
||||
successTrans(getDefaultTheme());
|
||||
}
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
import { Theme } from '../interface';
|
||||
import {
|
||||
DEFAULT_BGM_STORAGE_KEY,
|
||||
DEFAULT_CLICK_SOUND_STORAGE_KEY,
|
||||
DEFAULT_TRIPLE_SOUND_STORAGE_KEY,
|
||||
} from '../../utils';
|
||||
|
||||
const icons = <const>[
|
||||
`🎨`,
|
||||
|
@ -25,7 +20,7 @@ export const getDefaultTheme: () => Theme<DefaultSoundNames> = () => {
|
|||
title: '有解的羊了个羊',
|
||||
desc: '真的可以通关~',
|
||||
dark: true,
|
||||
maxLevel: 5,
|
||||
maxLevel: 20,
|
||||
backgroundColor: '#8dac85',
|
||||
icons: icons.map((icon) => ({
|
||||
name: icon,
|
||||
|
@ -36,16 +31,13 @@ export const getDefaultTheme: () => Theme<DefaultSoundNames> = () => {
|
|||
sounds: [
|
||||
{
|
||||
name: 'button-click',
|
||||
src:
|
||||
localStorage.getItem(DEFAULT_CLICK_SOUND_STORAGE_KEY) || '',
|
||||
src: 'https://minio.streakingman.com/solvable-sheep-game/sound-button-click.mp3',
|
||||
},
|
||||
{
|
||||
name: 'triple',
|
||||
src:
|
||||
localStorage.getItem(DEFAULT_TRIPLE_SOUND_STORAGE_KEY) ||
|
||||
'',
|
||||
src: 'https://minio.streakingman.com/solvable-sheep-game/sound-triple.mp3',
|
||||
},
|
||||
],
|
||||
bgm: localStorage.getItem(DEFAULT_BGM_STORAGE_KEY) || '',
|
||||
bgm: 'https://minio.streakingman.com/solvable-sheep-game/sound-disco.mp3',
|
||||
};
|
||||
};
|
||||
|
|
104
src/utils.ts
104
src/utils.ts
|
@ -9,9 +9,6 @@ export const LAST_CUSTOM_THEME_ID_STORAGE_KEY = 'lastCustomThemeId';
|
|||
export const LAST_UPLOAD_TIME_STORAGE_KEY = 'lastUploadTime';
|
||||
export const CUSTOM_THEME_STORAGE_KEY = 'customTheme';
|
||||
export const CUSTOM_THEME_FILE_VALIDATE_STORAGE_KEY = 'customThemeFileValidate';
|
||||
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 USER_NAME_STORAGE_KEY = 'username';
|
||||
export const USER_ID_STORAGE_KEY = 'userId';
|
||||
export const PLAYING_THEME_ID_STORAGE_KEY = 'playingThemeId';
|
||||
|
@ -174,3 +171,104 @@ export const timestampToUsedTimeString: (time: number) => string = (time) => {
|
|||
return '时间转换出错';
|
||||
}
|
||||
};
|
||||
|
||||
export const dataURLToFile: (dataURL: string, filename?: string) => File = (
|
||||
// #endregion dataURLToFile
|
||||
dataURL,
|
||||
filename
|
||||
) => {
|
||||
const isDataURL = dataURL.startsWith('data:');
|
||||
if (!isDataURL) throw new Error('Data URL 错误');
|
||||
const _fileName = filename || new Date().getTime().toString();
|
||||
const mimeType = dataURL.match(/^data:([^;]+);/)?.[1] || '';
|
||||
// base64转二进制
|
||||
const binaryString = atob(dataURL.split(',')[1]);
|
||||
let binaryStringLength = binaryString.length;
|
||||
const bytes = new Uint8Array(binaryStringLength);
|
||||
while (binaryStringLength--) {
|
||||
bytes[binaryStringLength] = binaryString.charCodeAt(binaryStringLength);
|
||||
}
|
||||
return new File([bytes], _fileName, { type: mimeType });
|
||||
};
|
||||
|
||||
interface drawImgSrcInCanvasParams {
|
||||
imgSrc: string;
|
||||
canvas: HTMLCanvasElement;
|
||||
scale?: number;
|
||||
}
|
||||
export const drawImgSrcInCanvas: (
|
||||
params: drawImgSrcInCanvasParams
|
||||
) => Promise<void> = async ({ imgSrc, canvas, scale = 1 }) => {
|
||||
if (scale < 0) throw new Error('scale不能小于0');
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx || !(ctx instanceof CanvasRenderingContext2D)) {
|
||||
throw new Error('Failed to get 2D context');
|
||||
}
|
||||
const img = document.createElement('img');
|
||||
img.setAttribute('src', imgSrc);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
img.onload = () => {
|
||||
const width = img.width * scale;
|
||||
const height = img.height * scale;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
resolve();
|
||||
};
|
||||
img.onerror = () => {
|
||||
reject(new Error('图片加载失败'));
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
interface createCanvasByImgSrcParams {
|
||||
imgSrc: HTMLImageElement['src'];
|
||||
scale?: number;
|
||||
}
|
||||
export const createCanvasByImgSrc: (
|
||||
params: createCanvasByImgSrcParams
|
||||
) => Promise<HTMLCanvasElement> = async ({ imgSrc, scale = 1 }) => {
|
||||
if (scale < 0) throw new Error('scale不能小于0');
|
||||
const canvas = document.createElement('canvas');
|
||||
await drawImgSrcInCanvas({
|
||||
imgSrc,
|
||||
canvas,
|
||||
scale,
|
||||
});
|
||||
return canvas;
|
||||
};
|
||||
|
||||
interface CanvasToFileParams {
|
||||
canvas: HTMLCanvasElement;
|
||||
fileName?: string;
|
||||
maxFileSize?: number;
|
||||
}
|
||||
export const canvasToFile: (
|
||||
params: CanvasToFileParams
|
||||
) => Promise<File> = async ({ canvas, fileName, maxFileSize }) => {
|
||||
// #endregion canvasToFile
|
||||
const MIME_TYPE = 'image/png';
|
||||
const dataURL = canvas.toDataURL(MIME_TYPE);
|
||||
const _fileName = fileName || new Date().getTime().toString();
|
||||
const genFile = dataURLToFile(dataURL, _fileName);
|
||||
// 判断是否需要压缩
|
||||
if (maxFileSize && genFile.size > maxFileSize) {
|
||||
let scale = Math.sqrt(maxFileSize / genFile.size);
|
||||
if (scale > 0.9) scale = 0.9;
|
||||
// TODO 暂时通过canvas绘制缩放图像进行递归压缩,后续考虑其他方式
|
||||
// TODO 不断生成canvas, 调研内存是否会泄漏
|
||||
const _canvas = await createCanvasByImgSrc({
|
||||
imgSrc: dataURL,
|
||||
scale,
|
||||
});
|
||||
return canvasToFile({
|
||||
canvas: _canvas,
|
||||
fileName: _fileName,
|
||||
maxFileSize,
|
||||
});
|
||||
} else {
|
||||
return genFile;
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue
Block a user