javascript
html用转义字符画菱形,JavaScript生成字符画(ASCII Art)
今天玩一些新的東西,大家都沒有看過這樣的視頻:
或者 這樣的圖片:
網上有很多生成這種圖片/視頻的工具,但是每個程序員都有一顆造輪子的心,我們當然要玩出自己的花樣啦。老規矩,還是先講原理,建議先用自己的方式實現一遍。原理很簡單首先準備一組排好序的不同 “著色密度?” 的ascii字符(事實上你可以用任何字符),比如 #KDGLftji+;,:. ,接著將源圖轉為灰度圖,然后遍歷圖中的像素,根據r/g/b通道的值來匹配字符串中相應 “著色密度?” 的字符,值越小則顏色越深,字符的“密度”也應越大。如果需要保留顏色,只需將灰度圖和原圖的像素位置一一對應即可。在開始實現功能之前,我們需要先了解一下顏色矩陣(ColorMatrix)。在計算機中,每個像素的顏色可以用一個向量(有的文章也叫矢量或分量)矩陣表示:[R, G, B, A]。顏色變換矩陣通常是用一個5x5的矩陣來表示,和空間中一個n維向量的平移變換需要用一個n+1維的矩陣來表示一樣,顏色矩陣也需要引入一個齊次坐標來進行“平移操作”。以下是一些常見的顏色變換矩陣:
亮度矩陣
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才對,之所以比例不同,按照網上的解釋,
這個比例主要是根據人眼中三種不同的感光細胞的感光強度比例分配的
還有一組比較常用的比例是0.2125,0.7154,0.0721,至于怎么來的還希望哪位大佬指點迷津。
下面是頁面的html結構
ascii art* {
margin: 0;
padding: 0;
}
canvas, img, #container {
display: block;
margin: auto;
}
#container {
line-height: 12px;
font-size: 12px;
font-family: 'SimHei', monospace;
letter-spacing: 6px;
}
(function () {
// 這里是js代碼
})()
解釋一下幾個關鍵點,首先我們輸出的文字必須是等寬字體,我這里使用的是黑體:font-family: 'SimHei', monospace; 別忘了加上fallback:monospace。等寬字體是指每個字寬高都固定的字體,這里的固定寬高是指同一種文字,比如中文的黑體寬度是英文的兩倍,其他字體我沒有試過,大家可以自己去實驗。這也是我設置了?letter-spacing: 6px; 的原因:當黑體設置了font-size=line-height時,中文是寬高相等,英文寬是高的一半。
接下來是js代碼:
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'
img.complete ? onImgLoaded() : (img.onload = onImgLoaded) // 確保onImgLoaded被執行
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標簽換行
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('
')}
container.innerHTML = strArray.join('')
}
先解釋一下這行:img.complete ? onImgLoaded() : (img.onload = onImgLoaded)
通常來說img.onload = 必須要放在 img.src = 之前,來保證onload回調一定會執行,否則的話如果圖片在執行這段代碼之前已經被瀏覽器緩存了,則有可能不會觸發onload回調。但是有時候由于業務的需要,有些操作必須要在圖片載入完成后執行,可是不一定立即執行,碰到這種情況,就可以用到Image對象的complete屬性,該屬性會返回當前圖片是否加載完成的bollean值。于是,通過上面這行代碼,就可以確保onImgLoaded函數在圖片載入完成后一定會被觸發。(本案例該寫法不必須,但是建議養成這個習慣)
上面實際上已經完成了核心的功能,接下來對我們的代碼做一些優化——
如果我們需要提供改變字體大小的功能怎么辦?可以先直接把字體大小相關的字面值抽出為一個變量,如fontSize?:
...
...
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()
}
但是PC瀏覽器不允許字體小于12px怎么辦呢?我們可以用css的scale來縮放容器就行了,修改代碼如下:
...
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'
...
...
}
...
好了,現在我們生成的是灰色的圖,但是如何生成彩色的圖呢,估計大家第一反應就是給每個字外面包一層標簽(比如span、font),但是筆者試了之后發現一旦圖片尺寸稍微大一些,性能下降非??鋸?#xff0c;一度把我的瀏覽器給弄崩潰了(╥╯^╰╥),小伙伴們可以自行嘗試。于是我打算用canvas來做渲染而不是使用開銷極大的dom,上面的代碼大部分可以重用,我修改了一下后的html結構:
ascii art* {
margin: 0;
padding: 0;
}
canvas, img {
display: block;
margin: auto;
}
(function () {
// canvas 實現
})()
這是js代碼:
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'
img.complete ? onImgLoaded() : (img.onload = onImgLoaded)
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結構如下:
...
...
您的瀏覽器不支持 HTML5 video 標簽。
...
...
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
video.onloadeddata = function () {
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)
}
除了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)
}
多余的這些代碼其實可以稱為是一段?動畫或游戲渲染的范式。因為的requestAnimationFrame渲染頻率是根據瀏覽器的刷新率來的,而電腦實時的性能會影響屏幕的刷新率,但是通常我們的動畫都是固定的幀率,為了保持最終渲染出來的幀率盡可能的符合設計,所以一般會根據設計的幀率來計算出每一幀的耗時,然后根據每一幀的實際耗時來算出理想狀態下的變化量,以下就是比較常規的設計范式:
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)
}
教程結束~~~~じゃない
那gif怎么搞呢?
emmmm,gif-frames?可以把gif導出多張序列幀,后面的原理基本就和視頻差不太多了,就給大家當課后作業吧 23333
Demo4:See the Pen ascii_art_pure by Kay (@oj8kay) on CodePen.
總結
以上是生活随笔為你收集整理的html用转义字符画菱形,JavaScript生成字符画(ASCII Art)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: php fastcgi exp,ngin
- 下一篇: gradle idea java ssm