当前位置:   article > 正文

一周时间,开发了一款封面图生成工具

一周时间,开发了一款封面图生成工具

65d7e457b690c9d803d4d34d50428547.png

介绍

这是一款封面图的制作工具,根据简单的配置即可生成一张好看的封面图,目前已有七款主题可以选择。做这个工具的初衷来自平时写文章,都为封面图发愁,去图片 网站上搜索很难找到满意的,而且当你要的图如果要搭配上文章的标题,使用 Photoshop 等软件操作成本太大。为此突然来了灵感,何不自己开发一个在线的工具直接生成。

开发前期构思

一款工具型的软件,界面一定要简洁,操作方面。所以在布局上没有必要占满整个页面,宽屏上限定宽度然后相对居中。

内容上软件整体上会分成三块:

  • 预览区域

  • 内容配置区域

  • 样式配置区域

这样一来布局上可以采取列布局或者行布局。我能想到的有:

db73abb306467a44eff5e7b9c143f573.png

由于根据个人喜好最终定下来第二种样式的布局。

代码实现

根据布局,我定义了三个函数组件来实现对应的“预览区”、“内容配置区”和“样式配置区”和一个主页面渲染函数。

  1. // 页面主函数
  2. export function Main(props) {
  3.   // ...
  4. }
  5. // 内容配置函数
  6. export function ContentForm(props) {
  7.   // ...
  8. }
  9. // 样式配置函数
  10. export function ConfigForm(props) {
  11.   // ...
  12. }
  13. // 封面图预览函数
  14. export function CoverImage(props) {
  15.   // ...
  16. }

这里 UI 组件是引用 Material UI[1],也是本站引用的唯一外部 UI 框架。

页面主函数

主函数中定义了全局共享的配置变量 config 和改变状态的函数 handleConfigChange。它们两会当成参数传入到其它组件中使用。

  1. export function Main({ normal }) {
  2.   const coverRef = useRef();
  3.   
  4.   const [config, setConfig] = useState({
  5.     font: 'serif',
  6.     bgColor: '#949ee5',
  7.     gradientBgColor: '',
  8.     icon: 'react',
  9.     ratio: 0.5,
  10.     width: 800,
  11.     title: '欢迎来到太空编程站点',
  12.     author: '编程范儿',
  13.     theme: 'basic',
  14.     bgImg: 'https://spacexcode.oss-cn-hangzhou.aliyuncs.com/1704985651753-e2a2eb6d-71c6-4293-8d8c-49203c7410bb.jpeg'
  15.   });
  16.  
  17.   const handleConfigChange = (val, key) => {
  18.     setConfig((prev) => ({ ...prev, [key]: val }));
  19.   };
  20.   const downloadImage = (scale, format) => {
  21.     // todo
  22.   };
  23.   const handleCopyImg = (cb) => {
  24.     //todo
  25.   };
  26.   return (
  27.     <Box sx={{ padding: '40px 0' }}>
  28.       <Grid container spacing={3}>
  29.         <Grid item xs={12} md={ normal ? 8 : 12 }>
  30.           <Box className={styles.card} sx={{ padding: '20px 10px', overflowX: 'auto' }}>
  31.             {/* 生成图显示 */}
  32.             <div ref={coverRef} className={styles.preview} style={{ width: config.width + 'px'}}>
  33.               <CoverImage config={config} />
  34.             </div>
  35.           </Box>
  36.           <Box className={styles.card} sx={{ padding: '10px 20px 40px', marginTop: '24px' }}>
  37.             <ContentForm config={config} handleConfigChange={handleConfigChange} />
  38.           </Box>
  39.         </Grid>
  40.           {/* 配置 */}
  41.         <Grid item xs={12} md={normal ? 4 : 6}>
  42.           <Box className={styles.card} sx={{ padding: '10px 20px 40px' }}>
  43.             <ConfigForm config={config} handleConfigChange={handleConfigChange} downloadImage={downloadImage} handleCopyImg={handleCopyImg} />
  44.           </Box>
  45.         </Grid>
  46.       </Grid>
  47.     </Box>
  48.   )
  49. }

因为页面主函数主要是集成其它三个组件,没有什么逻辑,我们来一一讲讲“内容配置函数”、“样式配置函数”和“封面图预览函数”这三个函数的实现。

内容配置函数

封面图中的内容配置就三项:标题作者图标

标题和作者是两个简单的文本输入框,图标数据是我本地写了一个列表,图标本身是一段 SVG 代码。使用的 React 函数组件返回。

  1. export function ContentForm ({ config, handleConfigChange }) {
  2.   return (
  3.     <>
  4.       <Box className={styles.setItem}>
  5.         <Typography variant="h5">标题</Typography>
  6.         <TextField
  7.           value={config.title}
  8.           onChange={e => handleConfigChange(e.target.value, 'title')}
  9.           placeholder='标题'
  10.           size='small'
  11.           multiline
  12.           rows={3}
  13.           fullWidth
  14.         />
  15.       </Box>
  16.       <Box className={styles.setItem}>
  17.         <Typography variant="h5">作者</Typography>
  18.         <TextField
  19.           value={config.author}
  20.           onChange={e => handleConfigChange(e.target.value, 'author')}
  21.           placeholder='作者'
  22.           size='small'
  23.           fullWidth
  24.         />
  25.       </Box>
  26.       <Box className={styles.setItem}>
  27.         <Typography variant="h5">图标</Typography>
  28.         <Box sx={{ display: 'flex', gap: '10px' }}>
  29.           <Select
  30.             value={config.icon}
  31.             onChange={val => handleConfigChange(val.target.value, 'icon')}
  32.             label=''
  33.             size='small'
  34.             fullWidth
  35.           >
  36.             {devicons.map(el => (
  37.               <MenuItem value={el.value} key={el.value}>
  38.                 <div className={styles.selectIconRow}><span>{el.label}</span>{selectDevicon(el.value)}</div></MenuItem>
  39.             ))}
  40.           </Select>
  41.           <Button component="label" size='small' variant="contained" sx={{ width: '120px' }} startIcon={<AddPhotoAlternateIcon />}>
  42.             上传<VisuallyHiddenInput type="file" onChange={(e) => handleConfigChange(URL.createObjectURL(e.target.files[0]), 'customIcon')} />
  43.           </Button>
  44.         </Box>
  45.       </Box>
  46.     </>
  47.   )
  48. }

说明下:这里我只贴主要代码。

样式配置函数

样式配置主要是对封面图的 Layout、上面文字的字体、背景色和图片的长宽进行设置。同时这个区域还包含两个操作按钮:图片的复制和导出。

主题

这里我定义了七款主题,分别对它们进行命名“basic”、“background”、“stylish”、“outline”、“modern”、“preview”和“mobile”。后面会根据 命名对主题进行调用。

在配置里,我们对不同的主题设计了 Layout 模型,放在选项中进行选择,另外还分别对它们建立了真实渲染的文件。分别放在 themes 和 themeSkeleton 两个目录下。

我们这里就以 basic 主题进行讲解,其它类似。

  1. import { Skeleton } from '@mui/material';
  2. export default const BasicTheme = () => {
  3.   return (
  4.     <div className={styles.basicTheme}>
  5.       <Skeleton animation={false} variant="rectangular" sx={{ padding: '8px' }} width={116} height={68}>
  6.         <div className={styles.content}>
  7.           <Skeleton animation={false} variant="text" width={'100%'} height={10} />
  8.           <Skeleton animation={false} variant="text" width={'70%'} height={10} />
  9.           <div className={styles.bt}>
  10.             <Skeleton animation={false} variant="rounded" width={10} height={10} />
  11.             <Skeleton animation={false} variant="text" width={'20%'} height={8} />
  12.           </div>
  13.         </div>
  14.       </Skeleton>
  15.     </div>
  16.   );
  17. }

每个 UI 框架都有 Skeleton 骨架屏组件,我们可以直接使用它来生成我们的主题模型。很轻松就实现了布局。

而主题的渲染组件则要通过读取配置来做实现样式的定制。

  1. export default const BasicTheme = ({ config }) => {
  2.   const { title, bgColor, gradientBgColor, author, icon, font, customIcon, width, ratio } = config;
  3.   const height = width * ratio + 'px';
  4.   return (
  5.     <div className={styles.basicTheme}>
  6.       <div className={styles.main} style={{ backgroundColor: bgColor, backgroundImage: gradientBgColor, height: height }}>
  7.         <div className={clsx(styles.content, styles['font-' + font])}>
  8.           <div style={{ padding: '0 3rem' }}>
  9.             <h1>{title}</h1>
  10.           </div>
  11.           <div className={styles.bt}>
  12.             {
  13.               customIcon ?
  14.                 <div className={styles.customIcon}>
  15.                   <img src={customIcon} alt="img" />
  16.                 </div>
  17.                 :
  18.                 <div className={styles.devicon}>
  19.                   {selectDevicon(icon)}
  20.                 </div>
  21.             }
  22.             <h2 className={styles.author}>{author}</h2>
  23.           </div>
  24.         </div>
  25.       </div>
  26.     </div>
  27.   );
  28. }

配置中的主题名和实际的主题组件函数做了映射。

  1. const selectTheme = (theme) => {
  2.   switch (theme) {
  3.     case 'basic':
  4.       return <BasicTheme config={config} />;
  5.     case 'modern':
  6.       return <ModernTheme config={config} />;
  7.     case 'outline':
  8.       return <OutlineTheme config={config} />;
  9.     case 'background':
  10.       return <BackgroundTheme config={config} />;
  11.     case 'preview':
  12.       return <PreviewTheme config={config} />;
  13.     case 'stylish':
  14.       return <StylishTheme config={config} />;
  15.     case 'mobile':
  16.       return <MobileTheme config={config} />;
  17.     default:
  18.       return <BasicTheme config={config} />;
  19.   }
  20. };
字体

字体选项中有每个字体的命名,它们被存在配置变量中,在主题渲染函数中会被用在类名中。然后对相应的类名设置对应的 font-family

背景色

背景色有两种类型:纯色和渐变色,分别通过 CSS 的 background-color 和 background-image 属性进行设置。

渐变色我们预定义了八种:

  1. const bgColorOptions = [
  2.   'linear-gradient(310deg,rgb(214,233,255),rgb(214,229,255),rgb(209,214,255),rgb(221,209,255),rgb(243,209,255),rgb(255,204,245),rgb(255,204,223),rgb(255,200,199),rgb(255,216,199),rgb(255,221,199))',
  3.   'linear-gradient(160deg,rgb(204,251,252),rgb(197,234,254),rgb(189,211,255))',
  4.   'linear-gradient(150deg,rgb(255,242,158),rgb(255,239,153),rgb(255,231,140),rgb(255,217,121),rgb(255,197,98),rgb(255,171,75),rgb(255,143,52),rgb(255,115,33),rgb(255,95,20),rgb(255,87,15))',
  5.   'linear-gradient(345deg,rgb(211,89,255),rgb(228,99,255),rgb(255,123,247),rgb(255,154,218),rgb(255,185,208),rgb(255,209,214),rgb(255,219,219))',
  6.   'linear-gradient(150deg,rgb(0,224,245),rgb(31,158,255),rgb(51,85,255))',
  7.   'linear-gradient(330deg,rgb(255,25,125),rgb(45,13,255),rgb(0,255,179))',
  8.   'linear-gradient(150deg,rgb(0,176,158),rgb(19,77,93),rgb(16,23,31))',
  9.   'linear-gradient(150deg,rgb(95,108,138),rgb(48,59,94),rgb(14,18,38))'
  10. ]

纯色的选择放了一个取色器,另外后面还放了一个随机生成颜色的按钮,这里也是人为定了一些颜色,然后从中随机选取。

长宽设置

长度通过 Slider 滑块组件进行设置,为了保证生成的图片大小在合理的范围内,这里设置了最大和最小边界值,区间范围在 [600, 820] 之间。

宽度的实现是通过设置长宽比来实现的。

1:23:54:75:8 这几个比例都能保证图片有较好的效果。

复制和下载

图片生成好之后,我预想了会有两个动作,一个是下载保存到本地,另一个是为了快捷使用,如果是在聊天工具,类似微信、QQ或者钉钉的聊天框中可直接 粘帖复制好的图片。另外富文本编辑器也支持。

这里我们首先要用到核心组件 html2canvas 来帮我们实现从页面 html 元素转为 canvas 对象,进而实现图片的保存和复制。

  1. const handleCopyImg = (cb) => {
  2.   if (!coverRef.current) return;
  3.   html2canvas(coverRef.current, {
  4.     useCORS: true,
  5.     scale: 1,
  6.     backgroundColor: 'transparent'
  7.   }).then((canvas) => {
  8.     canvas.toBlob(async blob => {
  9.       console.log(blob);
  10.       const data = [
  11.         new ClipboardItem({
  12.           [blob.type]: blob,
  13.         }),
  14.       ];
  15.       await navigator.clipboard.write(data)
  16.         .then(
  17.           () => {
  18.             console.log("复制成功!");
  19.             cb && cb();
  20.           },
  21.           () => {
  22.             console.error("失败.");
  23.           }
  24.         );
  25.     });
  26.   })
  27. };

图片保存的时候会弹出类型和大小选择的选项,支持 png 和 jpg 格式的导出,另外为了在 retina 屏幕上适配,也提供了 2X 图的导出。

  1. const downloadImage = (scale, format) => {
  2.   if (!coverRef.current) return;
  3.   html2canvas(coverRef.current, {
  4.     useCORS: true,
  5.     scale: scale,
  6.     backgroundColor: 'transparent'
  7.   }).then((canvas) => {
  8.     let newImg = new Image()
  9.     const date = new Date()
  10.     newImg.src = canvas.toDataURL('image/' + format) // 'image/png'
  11.     const a = document.createElement("a");
  12.     a.style.display = "none";
  13.     a.href = newImg.src;
  14.     a.download = `spacexcode-cover-${date.getMinutes()}${date.getSeconds()}.${format}`;
  15.     a.rel = "noopener noreferrer";
  16.     document.body.append(a);
  17.     a.click();
  18.     setTimeout(() => {
  19.         a.remove();
  20.     }, 1000);
  21.   })
  22. };

为了做一款好用的工具,还是尽量多想想,包含一些特殊的使用场景。

工具地址:https://spacexcode.com/coverview

参考资料

[1]

Material UI: https://mui.com/

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/81332
推荐阅读
相关标签
  

闽ICP备14008679号