4 264 Text / Codepen

Текст из частиц

Текст формируемый из частиц, на которые мы можем воздействовать за счет наведения курсора.


CSS

html, body {
    height: 100%;
    background: black;
}

JS

https://cdnjs.cloudflare.com/ajax/libs/stats.js/r16/Stats.min.js
https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.6/dat.gui.min.js
"use strict"

const {
    abs,
    acos,
    asin,
    atan,
    atan2,
    ceil,
    cos,
    floor,
    max,
    min,
    PI,
    pow,
    random,
    round,
    sin,
    sqrt,
    tan
} = Math
const HALF_PI = 0.5 * PI
const QUART_PI = 0.25 * PI
const TAU = 2 * PI
const TO_RAD = PI / 180
const G = 6.67 * pow(10, -11)
const EPSILON = 2.220446049250313e-16
const rand = n => n * random()
const randIn = (_min, _max) => rand(_max - _min) + _min
const randRange = n => n - rand(2 * n)
const fadeIn = (t, m) => t / m
const fadeOut = (t, m) => (m - t) / m
const fadeInOut = (t, m) => {
    let hm = 0.5 * m
    return abs((t + hm) % m - hm) / hm
}
const dist = (x1, y1, x2, y2) => sqrt(pow(x2 - x1, 2) + pow(y2 - y1, 2))
const angle = (x1, y1, x2, y2) => atan2(y2 - y1, x2 - x1)
const lerp = (a, b, t) => (1 - t) * a + t * b
const clamp = (n, _min, _max) => min(max(n, _min), _max)
const norm = (n, _min, _max) => (n - _min) / (_max - _min)
const fract = n => n - (n | 0)
const vh = p => p * window.innerHeight * 0.01
const vw = p => p * window.innerWidth * 0.01
const vmin = p => min(vh(p), vw(p))
const vmax = p => max(vh(p), vw(p))
const intToRGBA = n => {
    let r, g, b, a

    n >>>= 0

    r = (n & 0xff000000) >>> 24
    g = (n & 0xff0000) >>> 16
    b = (n & 0xff00) >>> 8
    a = (n & 0xff) / 255

    return `rgba(${[r, g, b, a].join()})`
}
const drawTypes = {
    FILL: 'fill',
    STROKE: 'stroke'
}
const textAlignTypes = {
    CENTER: 'center',
    END: 'end',
    LEFT: 'left',
    RIGHT: 'right',
    START: 'start'
}
const textBaselineTypes = {
    ALPHABETIC: 'alphabetic',
    BOTTOM: 'bottom',
    HANGING: 'hanging',
    MIDDLE: 'middle',
    TOP: 'top'
}

Array.prototype.lerp = function(t = [], a = 0) {
    this.forEach((n, i) => (this[i] = lerp(n, t[i], a)))
}

Float32Array.prototype.get = function(i = 0, n = 0) {
    const t = i + n

    let r = []

    for (; i < t; i++) {
        r.push(this[i])
    }

    return r
}

class PropsArray {
    constructor(count = 0, props = [], type = 'float') {
        this.count = count
        this.props = props
        this.spread = props.length // TODO: Need to implement indexing based on spread
        this.values = type === 'float'
            ? new Float32Array(count * props.length)
            : new Uint32Array(count * props.length)
    }
    get length() {
        return this.values.length
    }
    set(a = [], i = 0) {
        this.values.set(a, i)
    }
    setMap(o = {}, i = 0) {
        this.set(Object.values(o), i)
    }
    get(i = 0) {
        return this.values.get(i, this.spread)
    }
    getMap(i = 0) {
        return this.get(i).reduce(
            (r, v, i) => ({
                ...r,
                ...{ [this.props[i]]: v }
            }),
            {}
        )
    }
    forEach(cb) {
        let i = 0
        
        for (; i < this.length; i += this.spread) {
            cb(this.get(i), i, this)
        }
    }
    map(cb) {
        let i = 0
        
        for (; i < this.length; i += this.spread) {
            this.set(cb(this.get(i), i, this), i)
        }
    }
    async* read() {
        let i = 0
        
        for (; i < this.length; i += this.spread) {
            yield { index: i, value: this.get(i) }
        }
    }
}

function createOffscreenCanvas (width, height) {
    let _canvas

    if (typeof OffscreenCanvas !== undefined) {
        _canvas = new OffscreenCanvas(parseFloat(width), parseFloat(height))
    } else {
        _canvas = createCanvas(width, height)
    }

    return _canvas
}

function createCanvas (width, height) {
    const canvas = document.createElement('canvas')

    canvas.width = width
    canvas.height = height

    return canvas
}

function createContext2D (width = innerWidth, height = innerHeight, contextAttributes) {
    return createCanvas(width, height).getContext('2d', contextAttributes)
}

function createOffscreenContext2D (width = innerWidth, height = innerHeight, contextAttributes) {
    return createOffscreenCanvas(width, height).getContext('2d', contextAttributes)
}

function createRenderingContext (width, height) {
    const contextAttributes = {
        desynchronized: true,
        willReadFrequently: true
    }

    const ctx = createContext2D(width, height, contextAttributes)
    const buffer = createOffscreenContext2D(width, height, contextAttributes)

    ctx.canvas.style.position = 'absolute'

    document.body.appendChild(ctx.canvas)

    return {
        buffer,
        ctx
    }
}
const options = {
    mouse: {
        lerpAmt: 0.5,
        repelThreshold: 100
    },
    particles: {
        density: 3,
        get pixelDensity () {
            return (4 - this.density) * 4
        },
        pLerpAmt: 0.25,
        vLerpAmt: 0.1
    },
    text: {
        drawType: drawTypes.STROKE,
        fontColor: [60, 200, 255, 255],
        fontSize: 120,
        get fontStyle () {
            return `${this.fontSize}px Oswald`
        },
        message: 'PCVECTOR'
    }
}
const particleProps = [
    'x',
    'y',
    'vx',
    'vy',
    'bx',
    'by'
]
const { buffer, ctx } = createRenderingContext()

// Utils: https://codepen.io/seanfree/pen/LvrJWz

let hover = false
let userx = 0
let usery = 0
let repelx = 0
let repely = 0
let centerx = 0
let centery = 0
let particles
let width
let height
let imageBuffer
let gui
let stats

window.addEventListener('resize', setup)
window.addEventListener('mousemove', mousemove)
window.addEventListener('mouseout', mousemove)
window.addEventListener('load', start)

function start () {
    createStats()
    createGUI()
    setup()
    run()    
}

function setup () {
    resize()
    clearBuffer()
    setTextStyles()
    mapParticles()
}

function run () {
    requestAnimationFrame(run)
    
    stats.begin()

    update()
    render()

    stats.end()
}

function update () {
    if (hover) {
        repelx = lerp(repelx, userx, options.mouse.lerpAmt)
        repely = lerp(repely, usery, options.mouse.lerpAmt)
    } else {
        repelx = lerp(repelx, centerx, options.mouse.lerpAmt)
        repely = lerp(repely, centery, options.mouse.lerpAmt)
    }
}

function render () {
    clearBuffer()
    clearScreen()
    drawParticles()
    renderFrame()
}

function mapParticles () {
    drawMessage()

    const pixelData = new Uint32Array(buffer.getImageData(0, 0, width, height).data)
    const _particles = []

    let i, x, y, bx, by, vx, vy

    for (i = 0; i < pixelData.length; i += 4) {
        if (pixelData[i + 3] && !(i % options.particles.pixelDensity)) {
            x = rand(width) | 0
            y = rand(height) | 0
            bx = (i / 4) % width
            by = ((i / 4) / width) | 0
            vx = 0
            vy = 0

            _particles.push(x, y, vx, vy, bx, by)
        }
    }

    particles = new PropsArray(_particles.length / particleProps.length, particleProps)
    particles.set(_particles, 0)
}

function drawParticles () {
    let i, index, x, _x, y, _y, vx, vy, bx, by

    imageBuffer.data.fill(0)

    particles.forEach(([x, y, vx, vy, bx, by], index) => {
        _x = x | 0
        _y = y | 0

        if (!outOfBounds(_x, _y, width, height)) {
            i = 4 * (_x + _y * width)

            fillPixel(imageBuffer, i, options.text.fontColor)
        }

        particles.set(updatePixelCoords(x, y, vx, vy, bx, by), index)
    })

    buffer.putImageData(imageBuffer, 0, 0)
}

function fillPixel (imageData, i, [r, g, b, a]) {
    imageData.data.set([r, g, b, a], i)
}

function updatePixelCoords (x, y, vx, vy, bx, by) {
    let rd, dx, dy, phi, f

    rd = dist(x, y, repelx, repely)

    phi = angle(repelx, repely, x, y)
    f = (pow(options.mouse.repelThreshold, 2) / rd) * (rd / options.mouse.repelThreshold)

    dx = bx - x
    dy = by - y

    vx = lerp(vx, dx + (cos(phi) * f), options.particles.vLerpAmt)
    vy = lerp(vy, dy + (sin(phi) * f), options.particles.vLerpAmt)

    x = lerp(x, x + vx, options.particles.pLerpAmt)
    y = lerp(y, y + vy, options.particles.pLerpAmt)

    return [x, y, vx, vy]
}

function outOfBounds (x, y, width, height) {
    return x < 1 || x > width - 1 || y < 1 || y > height - 1
}

function renderFrame () {
    ctx.save()

    ctx.filter = 'blur(8px) brightness(200%)'
    ctx.drawImage(buffer.canvas, 0, 0)

    ctx.filter = 'blur(0)'
    ctx.globalCompositeOperation = 'lighter'
    ctx.drawImage(buffer.canvas, 0, 0)

    ctx.restore()
}

function clearScreen () {
    clear(ctx)
}

function clearBuffer () {
    clear(buffer)
}

function clear (_ctx) {
    _ctx.clearRect(0, 0, _ctx.canvas.width, _ctx.canvas.height)
}

function drawMessage () {
    drawText(
        options.text.message,
        centerx,
        centery,
        options.text.drawType
    )
}

function setTextStyles () {
    setFont(options.text.fontStyle)
    setTextBaseline(textBaselineTypes.MIDDLE)
    setTextAlign(textAlignTypes.CENTER)
}

function drawText (str = '', x = 0, y = 0, type = drawTypes.FILL) {
    buffer[`${type}Text`](str, x, y)
}

function setFont (font) {
    buffer.font = font
}

function setTextAlign (align = textAlignTypes.LEFT) {
    buffer.textAlign = align
}

function setTextBaseline (baseline = textBaselineTypes.ALPHABETIC) {
    buffer.textBaseline = baseline
}

function resize () {
    buffer.canvas.width = width = innerWidth
  buffer.canvas.height = height = innerHeight

  buffer.drawImage(ctx.canvas, 0, 0)

    ctx.canvas.width = innerWidth
  ctx.canvas.height = innerHeight
  
  ctx.drawImage(buffer.canvas, 0, 0)

    centerx = 0.5 * innerWidth
    centery = 0.5 * innerHeight

    imageBuffer = buffer.createImageData(width, height)
}

function mousemove ({ type, clientX, clientY }) {
    hover = type === 'mousemove'
    userx = clientX
    usery = clientY
}

function createStats () {
    stats = new Stats()
    document.body.appendChild(stats.domElement)
    stats.domElement.style.position = 'absolute'
}

function createGUI () {
    gui = new dat.GUI()

    addTextOptions()
    addMouseOptions()
    addParticleOptions()
}

function addTextOptions () {
    const textFolder = gui.addFolder('text')

    textFolder.add(options.text, 'drawType', Object.values(drawTypes))
        .onFinishChange(setup)
    textFolder.addColor(options.text, 'fontColor')
    textFolder.add(options.text, 'fontSize', 20, 200)
        .onFinishChange(setup)
    textFolder.add(options.text, 'message')
        .onFinishChange(setup)

    textFolder.open()
}

function addMouseOptions () {
    const mouseFolder = gui.addFolder('mouse')

    mouseFolder.add(options.mouse, 'lerpAmt', 0.05, 1)
    mouseFolder.add(options.mouse, 'repelThreshold', 20, 200)

    mouseFolder.open()
}

function addParticleOptions () {
    const particlesFolder = gui.addFolder('particles')

    particlesFolder.add(options.particles, 'density', 1, 4, 1)
        .onFinishChange(setup)
    particlesFolder.add(options.particles, 'pLerpAmt', 0.05, 1)
        .onFinishChange(setup)
    particlesFolder.add(options.particles, 'vLerpAmt', 0.05, 1)
        .onFinishChange(setup)

    particlesFolder.open()
}
Демо

Комментарии

  • Facebook
  • Вконтакте

Похожие статьи