man!在console中随心所欲的打印图片和字符画

一. 前言

在上一章中,我们简单的实现了一下console的自定义和一些默认的方法。有兴趣的可以看上一篇文章在项目中,让你的console.log输出更炫酷。今天把上篇文章中的坑填一下。继续把这个hooks完善一下。

二. 打印图片

原则上来说,console.log只能用于输出文本信息,不能只能用于输出图片。但是我们可以通过几种方法实现一下。

2.1 直接引入URL

既然console可以接受css的样式,那我们就可以通过css在background-image中塞入一个图片的 URL, 完成图片的显示。

回到Hooks文件中,新增一个image方法:

js 复制代码
const image = (url: string) => {
  const styles = `
    background-image: url('${url}');
    background-size: 20px;
    background-repeat: no-repeat;
    padding-left: 25px;
    font-weight: bold;
  `;
  console.log("%c 占位", styles);
}

image: (url: string) => image(url)

Vue中引入一张图片,然后使用一下:

js 复制代码
image('./assets/test.jpeg')

会发现图片并没有在console出来。这个原因是因为我们在占位符号中使用了 本地引用的资源

下面是我找到的一些说法,具体应该就是因为浏览器的安全限制。所以这里我们使用第二种方法,把资源转为 Base64字符

2.2 Base64字符

其实原理也简单,就是把image图片转为base64字符就可以了。步骤就是通过读取image,然后把image画到canvas上,最后调用toDataURL方法就可以了。

js 复制代码
// 图片url转为dataBase
const imageUrlToDataBase = (url: string): Promise<string> => {
  const img = new Image();  
  // 异步执行的,用promise包裹一下
  return new Promise((resolve, reject) => {
    img.onload = () => {
      const canvas: HTMLCanvasElement = document.createElement('canvas')
      const ctx: CanvasRenderingContext2D | null = canvas.getContext('2d');
      if (ctx) { 
        canvas.width = img.width; 
        canvas.height = img.height; 
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        ctx.drawImage(img, 0, 0);
        const dataUrl = canvas.toDataURL('image/png'); 
        resolve(dataUrl)
      }
    }; 

    img.onerror = () => {
      reject('图片读取失败')
    }
    img.src = url;
  })
}

然后我们回到项目中,在Vue里面使用一下:

js 复制代码
import imgSrc from './assets/11.jpeg';
image(imgSrc)

打印出来了,很好。

之后我们完善一下image方法

js 复制代码
const image = (url: string) => {
  imageUrlToDataBase(url).then((res: string) => {
    console.log(`%c 占位`, `
        background-image: url(${res});
        background-repeat: no-repeat;
    `)
  }).catch((err: string) => {
    console.log(err)
  })
}

我们去页面上看一下具体的效果:

额。。图片是出来了,但是为什么出现了这么一点点

原因是因为,在默认情况下,背景图可能只显示在文本 行高 范围内。也就是说图片的大小只会和你前面占位符的大小相同。

我做了一下实验,如果在style里面改变一下font-size的大小,可以看一下效果:

js 复制代码
const image = (url: string) => {
  imageUrlToDataBase(url).then((res: string) => {
    console.log(`%c 占位`, `
        background-image: url(${res});
        background-repeat: no-repeat;
        font-size: 30px;
    `)
  }).catch((err: string) => {
    console.log(err)
  })
}

确实,图片变大了。但是尺寸还是有问题,而且可以看到'占位'两个字还存在。所以这里要对style的样式进行调整一下

  • 改变占位字 : 把占位的字变成透明色
  • 修改图片size : 修改图片的size,让图片可以完整展开。
  • 预留图片加载位置 要预先定好图片的加载位置。这里可以使用padding其实就是用padding把元素撑开

让我们重新修改代码。imageUrlToDataBase这个方法要多reslove出来两个参数,图片的 宽度高度

js 复制代码
    resolve({
        dataUrl,
        width: img.width,
        height: img.height
    })

然后修改一下image代码:

js 复制代码
const image = (url: string) => {
  imageUrlToDataBase(url).then((res: {dataUrl: string, width: number, height: number}) => {
    console.log(`%c 占位`, `
        background-image: url(${res.dataUrl});
        background-repeat: no-repeat;
        background-size: ${res.width}px ${res.height}px;
        color: transparent;
        padding: ${res.height}px ${res.width}px;
    `)
  }).catch((err: string) => {
    console.log(err)
  })
}

最后我们看一下效果。

图片出来了!但是还有个问题。

我用的图片的大小是54723072的,超过了我控制台可以显示出来的大小。这样就会导致一个问题,图片没有办法完全展示出来

所以针对这个问题,我们有必要加一个参数,可以对图片的大小、尺寸进行放大缩小处理

2.3 尺寸控制

我的想法是增加一个参数size。size里面可以设置scalewidthheight三个参数。

  • scale 图片长、宽改变的比例。
  • width 图片长度改变的值。
  • height 图片宽度改变的值。

修改一下image里面的代码:

js 复制代码
const image = (url: string, size: {scale?: number, width?: number, height?: number}) => {
  const { scale = 1, width = 0, height = 0 } = size 
  imageUrlToDataBase(url).then((res: {dataUrl: string, w: number, h: number}) => {
    const rw = (width ? width : res.w) * scale;   // 处理之后的宽
    const rh = (height ? height : res.h) * scale;   // 处理之后的高
    const pt = Math.floor(rh / 2);
    const pl = Math.floor(rw / 2);
    console.log(`%c 占位`, `
        padding: ${pt}px ${pl}px;
        background-image: url(${res.dataUrl});
        background-repeat: no-repeat;
        background-size: ${rw}px ${rh}px;
        color: transparent;
    `)
  }).catch((err: string) => {
    console.log(err)
  })
}

默认scale为1、width为0, height为0,。在解构里面处理一下,不传也设置为默认值。 这里padding不能直接设置为width或者height宽度。设置一半即可(因为padding是四个方面)

然后我们在页面上使用一下:

js 复制代码
image(imgSrc, { scale: 0.1 })

这时候图片看起来就舒服很多了。

还有个小小的优化点,如果我设置了宽高的话,可能会出现图片被压缩变形的情况。

js 复制代码
image(imgSrc, { scale: 1, width: 400, height: 400 })

这里可以看出来,图片是被拉长了。这是因为我们设置了background-image属性导致的,但是我觉得影响不是很大,这里就不做单独的处理了。

最后要注意一点的是,如果图片是一个外部的URL链接,一定要保证这个图片被允许访问,否则的话就会报错403

三. 打印字符画

3.1 什么是字符画

字符画(ASCII Art) 是一种用计算机字符(如字母、数字、符号)拼凑而成的图像或图案,最早诞生于计算机文本模式时代(如早期终端、电传打字机)。它通过排列字符的密度、形状和位置来模拟图形效果,无需依赖图像文件,仅用纯文本即可呈现视觉内容。

这个其实蛮好玩的,有些网站也会打印出来。比如B站。

前端如何生成字符画然后在console显示呢?

3.2 String.raw

因为 String.raw 会忽略转义字符,保持原始字符串格式,所以非常适合输出字符画。

可以举个简单的例子:

js 复制代码
const str = String.raw`\n\n\n/n/n/n`;
console.log(str);

如果我们直接打印\n\n\n/n/n/n,会看到前面的换行符号被执行了。

但是这里需要用户自己先定义一下字符画,通过String.raw。想通过变量或者模版字符串的方式实现,会有很多问题。

3.3 动态生成字符画

除了用字符串转为字符画的方式之外,我们还可以通过算法,把图片转为字符画。新建一个方法AscArt

js 复制代码
const AscArt = (url: string) => {
  const img = new Image();  
  img.crossOrigin = 'anonymous';
  return new Promise((resolve, reject) => {
    img.onload = async () => {
      const asc = await generateAsciiArt(img)
      console.log(asc)
    }; 
    img.onerror = () => {
      reject('图片读取失败')
    }
    img.src = url;
  })
}

通过图片,然后转为字符画。核心的算法是generateAsciiArt

js 复制代码
const generateAsciiArt = (img: HTMLImageElement) => {
  return new Promise((resolve, reject) => {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    // 设置画布尺寸(缩小图片以提高字符画清晰度)
    const scale = 0.1; // 缩放比例
    canvas.width = img.width * scale;
    canvas.height = img.height * scale;
    if (ctx) {
      ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
      const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
      const asciiChars = "@%#*+=-:. "; // 从黑到白的字符
      let asciiArt = "";
      for (let y = 0; y < canvas.height; y++) {
        for (let x = 0; x < canvas.width; x++) {
          const pos = (y * canvas.width + x) * 4;
          // 计算灰度值(R+G+B 取平均)
          const gray = (pixels[pos] + pixels[pos + 1] + pixels[pos + 2]) / 3;
          // 映射到字符
          const charIndex = Math.floor((gray / 255) * (asciiChars.length - 1));
          asciiArt += asciiChars[charIndex];
        }
        asciiArt += "\n";
        resolve(asciiArt)
      }
    }
  })
}

其实这个算法就是类似灰度算法,把元素字符内的rgb转换为acs,然后打印出来。

最终结果:

3.4 库

如果实在不想手写,就可以用前端的库来生成。这里推荐几个库供大家使用figlet.jsascii-art

四. 结尾

终于写完了。写的比较粗,大家有兴趣的多多提意见吧!!最后放一下完整代码:

js 复制代码
enum Color {
 SUCCESS = '#67C23A',
 WARNING = '#E6A23C',
 DANGER = '#F56C6C',
 NORMAL = '#909399',
 PRIMARY = '#409EFF'
}

const success = (title: string, content: string) => {
  basePrint(title, content, Color.SUCCESS)
}

const warning = (title: string, content: string) => {
  basePrint(title, content, Color.WARNING)
}

const error = (title: string, content: string) => {
  basePrint(title, content, Color.DANGER)
}

const normal = (title: string, content: string) => {
  basePrint(title, content, Color.NORMAL)
}

const primary = (title: string, content: string) => {
  basePrint(title, content, Color.PRIMARY)
}

const basePrint = (title: string, content: string, color: string) => {
  const titleStyle = title ? `background:${color}; border:1px solid ${color}; padding: 1px; color: #fff; font-size: 13px` : ''
  const contentStyle = content ? `border:1px solid ${color}; padding: 1px; color: ${color}; font-size: 13px` : ''
  console.log(`%c ${title} %c ${content}`, `${titleStyle}`, `${contentStyle}`)
}

const matchStrToArray = (str: string) => {
  const strArr = str ? str.split(',') : []
  const result: {content: string, style: string}[] = []
  strArr.forEach((cur: string) => {
    const _arr = cur.split('&');
    const content = _arr.length > 0 ? _arr[0].split('=').length > 0 ? _arr[0].split('=')[1] : '' : ''
    const style = _arr.length > 1 ? _arr[1]!.match(/\{([^{}]*)\}/g)!.map((match: string) => match.slice(1, -1))[0] : ''
    result.push({
      content, style
    })
  })
  return result
}

const arrayToPrintContent = (array: {content: string, style: string}[]): {content: string[], color: string} => {
  let contentString: string[] = []
  let colorString = ''
  array.forEach(item => {
    colorString = `${colorString} ` + `%c ${item.content}`
    contentString.push(item.style)
  })
  return {
    content: contentString,
    color: colorString
  } 
}

const custom = (express: {content: string, style: string}[] | string) => {
  let strArr: {content: string, style: string}[]
  if (typeof express === 'string') {
    const res = matchStrToArray(express)
    strArr = res
  }
  else {
    strArr = express
  }
  const {content, color} = arrayToPrintContent(strArr)
  console.log(color, ...content)   // 这里用扩展运算符把参数展开
}

const complicated = (express: any[] | {}) => {
  if (Array.isArray(express)) {
    console.table(express)
  }
  else {
    const customArray = Object.entries(express).map(([key, value]) => ({
      id: key,
      value: value,
    }));
    console.table(customArray)
  }
}


const image = (url: string, size?: {scale?: number, width?: number, height?: number}) => {
  const { scale = 1, width = 0, height = 0 } = size ? size : { scale: 1, width: 0, height: 0}
  imageUrlToDataBase(url).then((res: {dataUrl: string, w: number, h: number}) => {
    const rw = (width ? width : res.w) * scale;
    const rh = (height ? height : res.h) * scale;
    const pt = Math.floor(rh / 2);
    const pl = Math.floor(rw / 2);
    console.log(rw)
    console.log(rh)
    console.log(`%c 占位`, `
        padding: ${pt}px ${pl}px;
        background-image: url(${res.dataUrl});
        background-repeat: no-repeat;
        background-size: ${rw}px ${rh}px;
        color: transparent;
    `)
  }).catch((err: string) => {
    console.log(err)
  })
}

const imageUrlToDataBase = (url: string): Promise<{dataUrl: string, w: number, h: number}> => {
  const img = new Image();  
  img.crossOrigin = 'anonymous';
  return new Promise((resolve, reject) => {
    img.onload = () => {
      const canvas: HTMLCanvasElement = document.createElement('canvas')
      const ctx: CanvasRenderingContext2D | null = canvas.getContext('2d');
      if (ctx) { 
        canvas.width = img.width; 
        canvas.height = img.height; 
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        ctx.drawImage(img, 0, 0);
        const dataUrl = canvas.toDataURL('image/png'); 
        resolve({
          dataUrl,
          w: img.width,
          h: img.height
      })
      }
    }; 
    img.onerror = () => {
      reject('图片读取失败')
    }
    img.src = url;
  })
}

const isEnv = () => {
  return import.meta.env.MODE === 'production' 
}

const generateAsciiArt = (img: HTMLImageElement) => {
  return new Promise((resolve, reject) => {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    // 设置画布尺寸(缩小图片以提高字符画清晰度)
    const scale = 0.1; // 缩放比例
    canvas.width = img.width * scale;
    canvas.height = img.height * scale;
    if (ctx) {
      ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
      const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
      const asciiChars = "@%#*+=-:. "; // 从黑到白的字符
      let asciiArt = "";
      for (let y = 0; y < canvas.height; y++) {
        for (let x = 0; x < canvas.width; x++) {
          const pos = (y * canvas.width + x) * 4;
          // 计算灰度值(R+G+B 取平均)
          const gray = (pixels[pos] + pixels[pos + 1] + pixels[pos + 2]) / 3;
          // 映射到字符
          const charIndex = Math.floor((gray / 255) * (asciiChars.length - 1));
          asciiArt += asciiChars[charIndex];
        }
        asciiArt += "\n";
        resolve(asciiArt)
      }
    }
  })
}

const AscArt = (url: string) => {
  const img = new Image();  
  img.crossOrigin = 'anonymous';
  return new Promise((resolve, reject) => {
    img.onload = async () => {
      const asc = await generateAsciiArt(img)
      console.log(asc)
    }; 
    img.onerror = () => {
      reject('图片读取失败')
    }
    img.src = url;
  })
}

export const consoleExtension = () => {
  return {
    success: (title: string, content: string) => success(title, content),
    warning: (title: string, content: string) => warning(title, content),
    error: (title: string, content: string) => error(title, content),
    normal: (title: string, content: string) => normal(title, content),
    primary: (title: string, content: string) => primary(title, content),
    custom: (array: {content: string, style: string}[] | string) => custom(array),
    complicated: (express: any[] | {}) => complicated(express),
    image: (url: string, size?: {scale?: number | undefined, width?: number | undefined, height?: number | undefined}) => image(url, size),
    AscArt: (url: string) => AscArt(url),
  }
}
相关推荐
gAlAxy...17 分钟前
深入理解 Cookie 与 Session —— Web 状态保持详解与实战
前端
专注VB编程开发20年24 分钟前
c#,vb.net全局多线程锁,可以在任意模块或类中使用,但尽量用多个锁提高效率
java·前端·数据库·c#·.net
JarvanMo28 分钟前
Google Connect 8月14日纪实
前端
猩猩程序员1 小时前
Go 1.24 全面拥抱 Swiss Table:让内置 map 提速 60% 的秘密
前端
1024小神1 小时前
vue3 + vite项目,如果在build的时候对代码加密混淆
前端·javascript
轻语呢喃1 小时前
useRef :掌握 DOM 访问与持久化状态的利器
前端·javascript·react.js
wwy_frontend2 小时前
useState 的 9个常见坑与最佳实践
前端·react.js
w_y_fan2 小时前
flutter_riverpod: ^2.6.1 应用笔记 (一)
前端·flutter
Jerry2 小时前
Compose 界面工具包
前端
Focusbe2 小时前
从0到1开发一个AI助手
前端·人工智能·面试