SOURCE

console 命令行工具 X clear

                    
>
console
/** utils */

function parseColor(color, toNumber) {
    if (toNumber === true) {
        if (typeof color === 'number') {
            return (color | 0) // chop off decimal
        }
        if (typeof color === 'string' && color[0] === '#') {
            color = color.slice(1)
        }
        return window.parseInt(color, 16)
    } else {
        if (typeof color === 'number') {
            color = '#' + ('00000' + (color | 0).toString(16)).substr(-6) // pad
        }
        return color
    }
}

function captureMouse(element) {
    let mouse = { x: 0, y: 0, e: null }
    element.addEventListener('mousemove', e => {
        mouse.x = e.offsetX
        mouse.y = e.offsetY
        mouse.e = e
    })
    return mouse
}

function sleep(ms) {
    return new Promise(resolve => { setTimeout(resolve, ms) })
}

function random(min, max) {
    if (arguments.length < 2) {
        max = min
        min = 0
    }
    if (min > max) {
        var hold = max
        max = min
        min = hold
    }
    return Math.floor(Math.random() * (max - min + 1)) + min
}

/**
 * 获取 guid
 */
function guid() {
    function s4() {
        return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1)
    }
    return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4()
}



class Ball3d {
    constructor(props) {
        this.x = 0
        this.y = 0
        this.xpos = 0
        this.ypos = 0
        this.zpos = 0
        this.radius = 40
        this.vx = 0
        this.vy = 0
        this.vz = 0
        this.rotation = 0
        this.scaleX = 1
        this.scaleY = 1
        this.color = '#ff0000'
        this.lineWidth = 1
        this.visible = true
        Object.assign(this, props)
    }
    draw(ctx) {
        ctx.save()
        ctx.translate(this.x, this.y)
        ctx.rotate(this.rotation)
        ctx.scale(this.scaleX, this.scaleY)

        ctx.lineWidth = this.lineWidth
        ctx.fillStyle = this.color
        ctx.beginPath()
        ctx.arc(0, 0, this.radius, 0, (Math.PI * 2), true)
        ctx.closePath()
        ctx.fill()
        if (this.lineWidth > 0) {
            ctx.stroke()
        }
        ctx.restore()
    }
}

const canvas = document.getElementById('canvas')
const width = canvas.width = document.documentElement.clientWidth
const height = canvas.height = document.documentElement.clientHeight
const ctx = canvas.getContext('2d')
const fl = 550
const vpX = width / 2
const vpY = height / 2
const balls = []
let mouse = captureMouse(canvas)
let angleY
let angleX
let isDisperse = false
let ballLength = 0
let loadAnimation = false
let complete = { length: 0 }

// 绕X轴旋转小球
function rotateX(ball, angle) {
    let cos = Math.cos(angle)
    let sin = Math.sin(angle)
    let y1 = ball.ypos * cos - ball.zpos * sin
    let z1 = ball.zpos * cos + ball.ypos * sin
    ball.ypos = y1
    ball.zpos = z1
}

// 绕Y轴旋转小球
function rotateY(ball, angle) {
    let sin = Math.sin(angle)
    let cos = Math.cos(angle)

    let x1 = ball.xpos * cos - ball.zpos * sin
    let z1 = ball.zpos * cos + ball.xpos * sin

    ball.xpos = x1
    ball.zpos = z1
}

// 透视
function setPerspective(ball) {
    if (ball.zpos > -fl) {
        let scale = fl / (fl + ball.zpos)
        ball.scaleX = ball.scaleY = scale
        ball.x = vpX + ball.xpos * scale
        ball.y = vpY + ball.ypos * scale
        ball.visible = true
    } else {
        ball.visible = false
    }
}

function move(ball) {
    rotateX(ball, angleX)
    rotateY(ball, angleY)
    setPerspective(ball)
}

// 排序,让zpos值大的球在前面
function zsort(a, b) {
    return b.zpos - a.zpos
}
function draw(ball) {
    if (ball.visible) {
        ball.draw(ctx)
    }
}

// 获取像素点
function getImageData(target, gird = 12) {
    const canvas = document.createElement('canvas')
    const ctx = canvas.getContext('2d')
    canvas.width = width
    canvas.height = height

    if (typeof target === 'string') {
        // 绘制文字
        ctx.font = '420px bold'
        ctx.fillStyle = '#fff'
        ctx.textBaseline = 'middle'
        ctx.textAlign = 'center'
        ctx.fillText(target, width / 2, height / 2)
    } else {
        // 绘制图片
        ctx.drawImage(target, (width - target.width) / 2, (height - target.height) / 2, target.width, target.height)
    }

    // 获取像素点
    const data = ctx.getImageData(0, 0, width, height).data

    const items = []
    for (let x = 0; x < width; x += gird) {
        for (let y = 0; y < height; y += gird) {
            let pos = (y * width + x) * 4
            if (data[pos + 3] > 128) {
                items.push({ x, y, rgba: [data[pos], data[pos + 1], data[pos + 2], data[pos + 3]] })
            }
        }
    }
    return items
}

function drawFrame() {
    window.requestAnimationFrame(drawFrame)
    ctx.clearRect(0, 0, width, height)
    angleY = (mouse.x - vpX) * 0.00005
    angleX = (mouse.y - vpY) * 0.00005
    balls.sort(zsort)
    if (loadAnimation) {
        animate()
    } else {
        balls.forEach(move)
    }
    balls.forEach(draw)
}

// 离散聚合动画
function animate() {
    const easing = 0.1
    balls.forEach((ball, index) => {
        const dx = ball.tx - ball.xpos
        const dy = ball.ty - ball.ypos
        const dz = ball.tz - ball.zpos
        const dr = ball.tr - ball.radius
        const dist = Math.sqrt(dx * dx + dy * dy + dz * dz + dr * dr)
        ball.xpos += dx * easing
        ball.ypos += dy * easing
        ball.zpos += dz * easing
        ball.radius += dr * easing
        if (dist < 1) {
            if (!complete[ball._id]) {
                complete.length++
                complete[ball._id] = true
            }
            if (complete.length === ballLength) {
                loadAnimation = false
                complete = { length: 0 }
            }
        }
        setPerspective(ball)
    })
}

// 计算每次变换的数量
function calculate(items) {
    const value = balls.length - items.length
    if (value < 0) {
        for (let i = 0; i < Math.abs(value); i++) {
            balls.push(new Ball3d({
                _id: guid(),
                lineWidth: 0,
                radius: random(5, 40),
                xpos: random(width, -width),
                ypos: random(height, -height),
                zpos: random(1000, -1000)
            }))
        }
    } else if (value > 0) {
        balls.splice(0, value)
    }
    balls.forEach((ball, index) => {
        const { x, y, rgba } = items[index]
        const isBlack = rgba[0] === 0 && rgba[1] === 0 && rgba[2] === 0
        const isWhite = rgba[0] === 255 && rgba[1] === 255 && rgba[2] === 255 && rgba[3] === 255
        if (isBlack || isWhite) {
            ball.color = parseColor(Math.random() * 0xffffff)
        } else {
            ball.color = `rgba(${rgba[0]}, ${rgba[1]}, ${rgba[2]}, ${rgba[3]})`
        }
        ball.tx = x - width / 2
        ball.ty = y - height / 2
        ball.tz = 10
        ball.tr = 5
    })
    ballLength = balls.length
}

// 设置离散坐标
function disperse() {
    balls.forEach(ball => {
        ball.tx = random(width, -width)
        ball.ty = random(height, -height)
        ball.tz = random(1000, -1000)
        ball.tr = random(5, 40)
    })
    isDisperse = true
}

function loadImage(src, callback) {
    const image = new Image()
    image.crossOrigin = '*'
    image.src = src
    image.onload = callback
}

function change(imageData) {
    if (loadAnimation) return
    complete = { length: 0 }
    setTimeout(() => {
        calculate(imageData)
        isDisperse = false
        loadAnimation = true
    }, isDisperse ? 0 : 1000)
    if (!isDisperse) disperseAction()
}

function disperseAction() {
    if (loadAnimation) return
    disperse()
    loadAnimation = true
}

async function init() {
    mouse.x = width / 1.8
    mouse.y = height / 1.8

    change(getImageData('\uD83D\uDC0D', 20))
    drawFrame()

    await sleep(5000)
    change(getImageData('\uD83D\uDC33', 20))

    await sleep(5000)
    change(getImageData('\uD83E\uDD8B', 18))
}

document.getElementById('J_confirm').addEventListener('click', () => {
    change(getImageData(document.getElementById('J_textInput').value))
})
document.getElementById('J_disperse').addEventListener('click', disperseAction)

init()
<canvas id="canvas" style="width:100%;height:100%;background: #000;"></canvas>
<div style="position: absolute; bottom: 10px; left: 40%;">
	<input type="text" value="JS" id="J_textInput" />
    <button id="J_confirm">确定</button>
    <button id="J_disperse">离散</button>
</div>
html, body {
    width: 100%;
    height: 100%;
    overflow: hidden;
    margin: 0;
}