Search code examples
javascriptcssanimationfadeinscramble

How do I fade in text in a div, while simultaneously descrambling the text in JavaScript


How do I perform an animated transformation on text while performing a fade in effect during the animated transformation?

        // ——————————————————————————————————————————————————
        // TextScramble
        // ——————————————————————————————————————————————————

        class TextScramble {
            constructor(elm, numWords) {
                this.el = el
                this.numWords = numWords;
                this.chars = '!<>-_\\/[]{}—=+*^?#1234567890________'
                this.update = this.update.bind(this)
            }
            setText(newText) {
                const oldText = this.el.innerText
                const length = Math.max(oldText.length, newText.length)
                const promise = new Promise((resolve) => this.resolve = resolve)
                this.queue = []
                for (let i = 0; i < length; i++) {
                    const from = oldText[i] || ''
                    const to = newText[i] || ''
                    const start = Math.floor(Math.random() * 40)
                    const end = start + Math.floor(Math.random() * 40)
                    this.queue.push({ from, to, start, end })
                }
                cancelAnimationFrame(this.frameRequest)
                this.frame = 0
                this.update()
                return promise
            }
            update() {
                let output = ''
                let complete = 0
                for (let i = 0, n = this.queue.length; i < n; i++) {
                    let { from, to, start, end, char } = this.queue[i]
                    if (this.frame >= end) {
                        complete++
                        output += to
                    } else if (this.frame >= start) {
                        if (!char || Math.random() < 0.28) {
                            char = this.randomChar()
                            this.queue[i].char = char
                        }
                        output += `<span class="dud">${char}</span>`
                    } else {
                        output += from
                    }
                }
                this.el.innerHTML = output
                if (complete === this.queue.length) {
                    this.resolve()
                } 
                else {
                    this.frameRequest = requestAnimationFrame(this.update)
                    this.frame++
                }
            }
            randomChar() {
                return this.chars[Math.floor(Math.random() * this.chars.length)]
            }
        }

        // ——————————————————————————————————————————————————
        // Example
        // ——————————————————————————————————————————————————

        const phrases = {
            'Coding' : 'none',
            'With' : 'none',
            'Muhammad': 'none',
            'Coding With Muhammad' : 'fade'
        }
        let phraseValues = Object.keys(phrases);
        const el = document.querySelector('.text')
        const fx = new TextScramble(el, phraseValues.length)

        let counter = 0
        let animation = phraseValues[0];

        let animate = () => {
            return function(callback) {
            document.querySelector(".text").animate([
                // keyframes
                { opacity: '0' },
                { opacity: '1' }
                    ], {
                    // timing options
                    duration: 3500,
                    iterations: 1
                });
                callback();
            }
        }   

        const next = () => {
        fx.setText(phraseValues[counter]).then(() => {
                if (counter <= phraseValues.length)
                    setTimeout(next, 800)
                else {
                    animation = phrases[phraseValues[counter]]
                    setTimeout(animate(next), 800)
                }
            })
            counter = (counter + 1) % phraseValues.length
        }
        next()
html, body {
            font-family: 'Roboto Mono', monospace;
            background: #212121;
            height: 100%;
        }
        .container {
            height: 100%;
            width: 100%;
            justify-content: center;
            align-items: center;
            display: flex;
        }
        .text {
            font-weight: 100;
            font-size: 28px;
            color: #FAFAFA;
        }
        .dud {
            color: #757575;
        }
        .fadeIn {
            animation: fade 10s;
        }
 <div class="container">
        <div class="text"></div>
    </div>


Solution

  • I was very close:

    I moved the callback invocation statement above the closure return statement below it. I left the animation keyframes as is.

    Finally I modified the setTimeout in the phrase to be animated, to be as follows:

    I placed a next and animate as callbacks in the setTimeout together to be called back in cascade sequence.

    I used a anonymous function definition using arrow notation.

    On animate (the function I wrote) I passed in the fx.update function definition as the callback, and invoked the closure function returned animate as the Higher Order Function, like so.

    See Results!

    // ——————————————————————————————————————————————————
                // TextScramble
                // ——————————————————————————————————————————————————
        
                class TextScramble {
                    constructor(elm, numWords) {
                        this.el = el
                        this.numWords = numWords;
                        this.chars = '!<>-_\\/[]{}—=+*^?#1234567890________'
                        this.update = this.update.bind(this)
                    }
                    setText(newText) {
                        const oldText = this.el.innerText
                        const length = Math.max(oldText.length, newText.length)
                        const promise = new Promise((resolve) => this.resolve = resolve)
                        this.queue = []
                        for (let i = 0; i < length; i++) {
                            const from = oldText[i] || ''
                            const to = newText[i] || ''
                            const start = Math.floor(Math.random() * 40)
                            const end = start + Math.floor(Math.random() * 40)
                            this.queue.push({ from, to, start, end })
                        }
                        cancelAnimationFrame(this.frameRequest)
                        this.frame = 0
                        this.update()
                        return promise
                    }
                    update = () => {
                        let output = ''
                        let complete = 0
                        for (let i = 0, n = this.queue.length; i < n; i++) {
                            let { from, to, start, end, char } = this.queue[i]
                            if (this.frame >= end) {
                                complete++
                                output += to
                            } else if (this.frame >= start) {
                                if (!char || Math.random() < 0.28) {
                                    char = this.randomChar()
                                    this.queue[i].char = char
                                }
                                output += `<span class="span">${char}</span>`
                            } else {
                                output += from
                            }
                        }
                        this.el.innerHTML = output
                        if (complete === this.queue.length) {
                            this.resolve()
                        } 
                        else {
                            this.frameRequest = requestAnimationFrame(this.update)
                            this.frame++
                        }
                    }
                    randomChar() {
                        return this.chars[Math.floor(Math.random() * this.chars.length)]
                    }
                }
        
                // ——————————————————————————————————————————————————
                // Example
                // ——————————————————————————————————————————————————
        
                const phrases = {
                    'Coding' : 'none',
                    'With' : 'none',
                    'Muhammad': 'none',
                    'Coding With Muhammad' : 'fade'
                }
                let phraseValues = Object.keys(phrases);
                const el = document.querySelector('.text')
                const fx = new TextScramble(el, phraseValues.length)
        
                let counter = 0
                let animation = phraseValues[0];
        
                let animate = (callback) => {
                    callback();
                    return function() {
                        document.querySelector(".text").animate([
                           // keyframes
                           { opacity: '0' },
                           { opacity: '1' }
                        ], {
                            // timing options
                            duration: 3500
                        });
                    }
                }   
        
                const next = () => {
                fx.setText(phraseValues[counter]).then(() => {
                        if (counter < phraseValues.length-1)
                            setTimeout(next, 800)
                        else {
                            setTimeout(() => {next, animate(next, fx.update)()}, 800)
                        }
                    })
                    counter = (counter + 1) % phraseValues.length
                }
                next()
    html, body {
                font-family: 'Roboto Mono', monospace;
                background: #212121;
                height: 100%;
            }
            .container {
                height: 100%;
                width: 100%;
                justify-content: center;
                align-items: center;
                display: flex;
            }
            .text {
                font-weight: 100;
                font-size: 28px;
                color: #FAFAFA;
            }
            .dud {
                color: #757575;
            }
    <div class="container">
            <div class="text"></div>
     </div>