# JavaScript生成字符画（ASCII Art）

2018/07/13 08:23

R G B A W
R 1 0 0 0 b
G 0 1 0 0 b
B 0 0 1 0 b
A 0 0 0 1 0
W 0 0 0 0 1

R G B A W
R -1 0 0 255 0
G 0 -1 0 255 0
B 0 0 -1 255 0
A 0 0 0 1 0
W 0 0 0 0 1

R G B A W
R 0.3086 0.6094 0.0820 0 0
G 0.3086 0.6094 0.0820 0 0
B 0.3086 0.6094 0.0820 0 0
A 0 0 0 1 0
W 0 0 0 0 1

ps：将像素去色的原理是使R＝G＝B，同时为了保持亮度不变，须使R+G+B尽量等于1 ，理论上来说要平分R、G、B通道值，应该是(R+B+G)/3，即系数应该约为0.3333才对，之所以比例不同，按照网上的解释，

<!DOCTYPE html>
<html lang="zh-cn">
<meta charset="UTF-8">
<title>ascii art</title>
<style>

* {
margin: 0;
}

canvas, img, #container {
display: block;
margin: auto;
}

#container {
line-height: 12px;
font-size: 12px;
font-family: 'SimHei', monospace;
letter-spacing: 6px;
}

</style>
<body>
<img src="./trump.png"/>
<div id="container"></div>
<script>
(function () {
// 这里是js代码
})()
</script>
</body>
</html>


var container = document.getElementById('container')
var offScreenCvs = document.createElement('canvas') // 创建离屏canvas
var offScreenCtx = offScreenCvs.getContext('2d', { alpha: false }) // 关闭透明度
var offScreenCvsWidth, offScreenCvsHeight
var samplerStep = 4 // 采样间隔

var img = new Image()
var onImgLoaded = function () {
offScreenCvsWidth = img.width
offScreenCvsHeight = img.height
offScreenCvs.width = offScreenCvsWidth
offScreenCvs.height = offScreenCvsHeight
offScreenCtx.drawImage(img, 0, 0, offScreenCvsWidth, offScreenCvsHeight)
imageData = offScreenCtx.getImageData(0, 0, offScreenCvsWidth, offScreenCvsHeight)
// 采样点数 = 图片宽度 / 采样间隔；容器边长 = 采样点数 × 字体大小
container.style.width = (offScreenCvsWidth / samplerStep * 12) + 'px'
container.style.height = (offScreenCvsHeight / samplerStep * 12) + 'px'
render()
}
img.src = './trump.png'

var imageData
var x, y, pos
var asciiCharArray = '#KDGLftji+;,:.'.split('') // 准备不同密度的字符数组（降序）
var durationPerChar = Math.ceil(255 / asciiCharArray.length) // 每个字符代表的密度阈值

function render () {
var imageDataContent = imageData.data
var strArray = []
var part1, part2
var letter
var value
for (y = 0; y < offScreenCvsHeight; y += samplerStep) {
strArray.push('<p>') // 使用P标签换行
for (x = 0; x < offScreenCvsWidth; x += samplerStep) {
pos = y * offScreenCvsWidth + x
// 获取RBG加权平均后的灰度值
value = imageDataContent[pos * 4] * 0.3086 + imageDataContent[pos * 4 + 1] * 0.6094 + imageDataContent[pos * 4 + 2] * 0.0820
imageDataContent[pos * 4] = imageDataContent[pos * 4 + 1] = imageDataContent[pos * 4 + 2] = value
// 判断灰度值落在那个密度范围中，拿到对应的字符
part1 = Math.floor(value / durationPerChar)
part2 = value % durationPerChar
letter = part2 ? asciiCharArray[part1] : (part1 ? asciiCharArray[part1 - 1] : 'æ')

strArray.push(letter)
}
strArray.push('</p>')
}
container.innerHTML = strArray.join('')
}

...
...
var fontSize = 18 // 字体大小
...
...
var onImgLoaded = function () {
...
...
container.style.width = (offScreenCvsWidth / samplerStep * fontSize) + 'px'
container.style.height = (offScreenCvsHeight / samplerStep * fontSize) + 'px'
container.style.fontSize = fontSize + 'px'
container.style.lineHeight = fontSize + 'px'
container.style.letterSpacing = (fontSize / 2) + 'px' // SimHei体英文宽是高的一半
render()
}

...
var onImgLoaded = function () {
...
...
imageData = offScreenCtx.getImageData(0, 0, offScreenCvsWidth, offScreenCvsHeight)
if (fontSize < 12) {
// 小于12px则将字体改为12px并通过 transform scale 进行缩放
container.style.transform = 'scale(' + (fontSize / 12) + ')'
container.style.transformOrigin = '50% 0'
fontSize = 12
}
container.style.width = (offScreenCvsWidth * fontSize / samplerStep) + 'px'
...
...
}
...

<!DOCTYPE html>
<html lang="zh-cn">
<meta charset="UTF-8">
<title>ascii art</title>
<style>

* {
margin: 0;
}

canvas, img {
display: block;
margin: auto;
}

</style>
<body>
<img src="./trump.png"/>
<canvas id="ascii-canvas"></canvas>
<script>
(function () {
// canvas 实现
})()
</script>
</body>
</html>


var offScreenCvs = document.createElement('canvas')
var offScreenCtx = offScreenCvs.getContext('2d', { alpha: false })
var asciiCvs = document.getElementById('ascii-canvas')
var asciiCtx = asciiCvs.getContext('2d', { alpha: false })
var offScreenCvsWidth, offScreenCvsHeight, asciiCvsWidth, asciiCvsHeight
var fontSize = 8
var samplerStep = 4

var img = new Image()
var onImgLoaded = function () {
offScreenCvsWidth = img.width
offScreenCvsHeight = img.height
offScreenCvs.width = offScreenCvsWidth
offScreenCvs.height = offScreenCvsHeight
offScreenCtx.drawImage(img, 0, 0, offScreenCvsWidth, offScreenCvsHeight)
imageData = offScreenCtx.getImageData(0, 0, offScreenCvsWidth, offScreenCvsHeight)
asciiCvsWidth = offScreenCvsWidth / samplerStep * fontSize
asciiCvsHeight = (offScreenCvsHeight / samplerStep + 1) * fontSize
asciiCvs.width = asciiCvsWidth
asciiCvs.height = asciiCvsHeight
render()
}
img.src = './trump.png'

var imageData
var x, y, _x, _y, pos
var asciiCharArray = '#KDGLftji+;,:.'.split('')
var durationPerChar = Math.ceil(255 / asciiCharArray.length)

function render () {
var imageDataContent = imageData.data
var part1, part2
var letter
var value
asciiCtx.fillStyle = '#ffffff'
asciiCtx.fillRect(0, 0, asciiCvsWidth, asciiCvsHeight)
asciiCtx.fillStyle = '#000000'
asciiCtx.font = fontSize + 'px SimHei'
for (y = 0, _y = 0; y < offScreenCvsHeight; y += samplerStep, _y++) {
for (x = 0, _x = 0; x < offScreenCvsWidth; x += samplerStep, _x++) {
pos = y * offScreenCvsWidth + x
value = imageDataContent[pos * 4] * 0.3086 + imageDataContent[pos * 4 + 1] * 0.6094 + imageDataContent[pos * 4 + 2] * 0.0820
imageDataContent[pos * 4] = imageDataContent[pos * 4 + 1] = imageDataContent[pos * 4 + 2] = value

part1 = Math.floor(value / durationPerChar)
part2 = value % durationPerChar
letter = part2 ? asciiCharArray[part1] : (part1 ? asciiCharArray[part1 - 1] : 'æ')

asciiCtx.fillText(letter, _x * fontSize, (_y + 1) * fontSize)
}
}
}

...
...
var x, y, _x, _y, pos
var r, g, b
var asciiCharArray = '#KDGLftji+;,:.'.split('')
...
...
function render () {
...
...
for (y = 0, _y = 0; y < offScreenCvsHeight; y += samplerStep, _y++) {
for (x = 0, _x = 0; x < offScreenCvsWidth; x += samplerStep, _x++) {
pos = y * offScreenCvsWidth + x
r = imageDataContent[pos * 4]
g = imageDataContent[pos * 4 + 1]
b = imageDataContent[pos * 4 + 2]
value = r * 0.3086 + g * 0.6094 + b * 0.0820
imageDataContent[pos * 4] = imageDataContent[pos * 4 + 1] = imageDataContent[pos * 4 + 2] = value

part1 = Math.floor(value / durationPerChar)
part2 = value % durationPerChar
letter = part2 ? asciiCharArray[part1] : (part1 ? asciiCharArray[part1 - 1] : 'æ')

asciiCtx.fillStyle = 'rgb(' + r + ',' + g + ',' + b + ')'
asciiCtx.fillText(letter, _x * fontSize, (_y + 1) * fontSize)
}
}
}
...
...

html结构如下：

...
...
<body>
<video id="video">
<source src="./mov_bbb.mp4" type="video/mp4">
<source src="./mov_bbb.ogg" type="video/ogg">
您的浏览器不支持 HTML5 video 标签。
</video>
<canvas id="ascii-canvas"></canvas>
<script>
...
...

js代码如下：

var video = document.getElementById('video')
var offScreenCvs = document.createElement('canvas')
var offScreenCtx = offScreenCvs.getContext('2d', { alpha: false })
var asciiCvs = document.getElementById('ascii-canvas')
var asciiCtx = asciiCvs.getContext('2d', { alpha: false })
var offScreenCvsWidth, offScreenCvsHeight, asciiCvsWidth, asciiCvsHeight
var fontSize = 8
var samplerStep = 4

var maxWidth = 400, maxHeight = 400

offScreenCvsWidth = video.videoWidth
offScreenCvsHeight = video.videoHeight
var ratio = offScreenCvsWidth / offScreenCvsHeight
if (video.videoWidth > maxWidth) {
offScreenCvsWidth = maxWidth
offScreenCvsHeight = Math.floor(offScreenCvsWidth / ratio)
}
if (video.videoHeight > maxHeight) {
offScreenCvsHeight = maxHeight
offScreenCvsWidth = Math.floor(offScreenCvsHeight * ratio)
}
offScreenCvs.width = offScreenCvsWidth
offScreenCvs.height = offScreenCvsHeight
asciiCvsWidth = (offScreenCvsWidth / samplerStep + 1) * fontSize
asciiCvsHeight = (offScreenCvsHeight / samplerStep + 1) * fontSize
asciiCvs.width = asciiCvsWidth
asciiCvs.height = asciiCvsHeight

offScreenCtx.drawImage(video, 0, 0, offScreenCvsWidth, offScreenCvsHeight)
imageData = offScreenCtx.getImageData(0, 0, offScreenCvsWidth, offScreenCvsHeight)
render()

video.onclick = function () {
video.paused ? video.play() : video.pause()
}

video.onplay = function () {
stop = false
rendering = false
requestAnimationFrame(tick)
}

video.onpause = function () {
stop = true
}
}

var imageData
var x, y, _x, _y, pos
var r, g, b
var asciiCharArray = '#KDGLftji+;,:.'.split('')
var durationPerChar = Math.ceil(255 / asciiCharArray.length)

function render () {
var imageDataContent = imageData.data
var part1, part2
var letter
var value
asciiCtx.fillStyle = '#ffffff'
asciiCtx.fillRect(0, 0, asciiCvsWidth, asciiCvsHeight)
asciiCtx.fillStyle = '#000000'
asciiCtx.font = fontSize + 'px SimHei'
for (y = 0, _y = 0; y < offScreenCvsHeight; y += samplerStep, _y++) {
for (x = 0, _x = 0; x < offScreenCvsWidth; x += samplerStep, _x++) {
pos = y * offScreenCvsWidth + x
r = imageDataContent[pos * 4]
g = imageDataContent[pos * 4 + 1]
b = imageDataContent[pos * 4 + 2]
value = r * 0.3086 + g * 0.6094 + b * 0.0820
imageDataContent[pos * 4] = imageDataContent[pos * 4 + 1] = imageDataContent[pos * 4 + 2] = value

part1 = Math.floor(value / durationPerChar)
part2 = value % durationPerChar
letter = part2 ? asciiCharArray[part1] : (part1 ? asciiCharArray[part1 - 1] : 'æ')

asciiCtx.fillStyle = 'rgb(' + r + ',' + g + ',' + b + ')'
asciiCtx.fillText(letter, _x * fontSize, (_y + 1) * fontSize)
}
}
}

var stop = false // 是否停止
var timeNow = Date.now() // 当前时间戳
var timeLast = timeNow // 上一帧时间戳
var delta = 0 // 与上一帧间隔
var interval //
var fps = 60 // 帧率

interval = 1000 / fps // 每帧耗时

var rendering = false
var tick = function () {
if (stop) return false
timeNow = Date.now()
delta = timeNow - timeLast
if (delta > interval) {
timeLast = timeNow

if (!rendering) {
rendering = true
offScreenCtx.drawImage(video, 0, 0, offScreenCvsWidth, offScreenCvsHeight)
imageData = offScreenCtx.getImageData(0, 0, offScreenCvsWidth, offScreenCvsHeight)
render()
rendering = false
}
}
requestAnimationFrame(tick)
}

var tick = function () {
if (!rendering) {
rendering = true
offScreenCtx.drawImage(video, 0, 0, offScreenCvsWidth, offScreenCvsHeight)
imageData = offScreenCtx.getImageData(0, 0, offScreenCvsWidth, offScreenCvsHeight)
render()
rendering = false
}
requestAnimationFrame(tick)
}

var stop = false // 是否停止渲染
var timeNow = Date.now() // 当前时间戳
var timeLast = timeNow // 上一帧时间戳
var delta = 0 // 与上一帧间隔
var fps = 60 // 帧率
var interval = 1000 / fps // 每帧耗时

var rendering = false // 是否渲染某组件
var tick = function () {
if (stop) return false
timeNow = Date.now()
delta = timeNow - timeLast
if (delta > interval) {
timeLast = timeNow

if (!rendering) {
// loop 代码
}

}
requestAnimationFrame(tick)
}

emmmm，gif-frames 可以把gif导出多张序列帧，后面的原理基本就和视频差不太多了，就给大家当课后作业吧 23333

Demo3：trump（dom版）

Demo4：See the Pen ascii_art_pure by Kay (@oj8kay) on CodePen.

1
3 收藏

0 评论
3 收藏
1