the Andrew Bailey

Background Text and Fake jQuery

I love having a fast website. A lot of it comes down to not having a lot of frameworks and libraries running. On my page, I only have one stylesheet, and one script. That doesn't mean that I can't get creative. My favorite is the glowing links when you hover over them. The summaries that look like rockstar autographed posters on the homepage are pretty sweet. (Those posters might be my favorite, if it wasn't for the difficulty in getting it to work just right.)

For my day job, I maintain websites that heavily use jQuery. It's nice, but it's not necessary. I certainly don't have a hipster allergic reaction to it (OMG that so old why do you still use that, I hate it, etc.). I've been able to do quite a bit with native APIs. A few weeks ago, I came across bling.js. It's not really a framework; it's a few lines that emulate the jQuery interface by calling equivalent vanilla APIs. The tagline? "Because you want the $ of jQuery without the jQuery." What I have isn't the original, and it borrows some ideas suggested further down in the comments. Check it out:

if(undefined===window.$){
    window.$=document.querySelectorAll.bind(document);
    [EventTarget.prototype,window,XMLHttpRequest.prototype].forEach(
        function setOn(p){
            Object.defineProperty(p,"on",{get(){
                return function onElement(t,f){
                    this.addEventListener(t,f);
                    return this;
    };}});});
    [NodeList.prototype,HTMLCollection.prototype].forEach(
        function setOnArray(p){
            Object.setPrototypeOf(p,Array.prototype);
            Object.defineProperty(p,"on",{get(){
                return function onArray(t,f){
                    this.forEach(function onEach(e){
                        e.addEventListener(t,f);});
                    return this;
    };}});
});}

It adds the $ function, which is document.querySelectorAll() underneath. That means you can't pass elements or documents into it (no $(document).ready(...)), only selector strings. It adds the on() function to EventTarget (which means all elements and nodes), window, XMLHttpRequest, NodeList, and HTMLCollection, and this passes to the addEventListener of the constituent objects. It also makes NodeList and HTMLCollection act like Array, so you can do fun things like forEach() and reduce() on them.

Even with my single stylesheet and script (and an unneeded fake jQuery), I can pull off some neat effects. If you'd look past all the text and pretty pictures, there was a solid dark green-blue background. From the techniques I learned from the posters, I set out to put some texture on the background that wasn't obviously repeating, and wasn't distracting.

Screenshot of theAndrewBailey.com, with exaggerated colors.

I decided to add some low contrast background text to the page. While it violates all kinds of accessibility guidelines about text contrast, you're not actually supposed to read it. It's there for aesthetic purposes only. It's not important. In fact, it's the more visible text on the page, but fancier. Sure, you might be able to catch it here and there, but that's by design.

Here's the relevant CSS:

.background-text[data-background-text]{
    position: relative;
}
.background-text[data-background-text]::before{
    font-family: "Reey", sans-serif;
    color: #000;
    font-size: 350%;
    line-height: 1em;
    text-align: justify;
    z-index: -1;
    position: absolute;
    top: -0.5em;
    left: 0;
    height: calc(100% + 0.5em);
    width: 100%;
    overflow: hidden;
    content: attr(data-background-text);
    text-rendering: optimizespeed;
}

That makes an element fill its background with unobtrusive (but large) dark text. You might notice that the text itself is in a data attribute. That gets filled by this Javascript:

function getTextFromElements(root,selector="main article p,main article>p>img"){
    return root.querySelectorAll(selector).reduce(function getTextInReverseOrder(accum,elem){
        switch(elem.tagName){
            case "IFRAME":
                return null!==elem.contentDocument.documentElement?
                    elem.contentDocument.documentElement.dataset.backgroundText+" "+accum
                    : accum;
            case "IMG":
                return elem.alt+" "+elem.title+" "+accum;
            default:
                return elem.textContent+" "+accum;
        }
        return accum;
    }," ");
};
function redoBackgroundText(e){
    if("background-text"===e.data){
        document.documentElement.dataset.backgroundText=(getTextFromElements(document)+
            getTextFromElements(document,".comment>p,article>iframe,aside"))
            .replace(/\s+/gum," ").trim();
    }
}
function inIframe(){try{return window.self!==window.top;}catch(e){return true;}}

if(inIframe()){
    document.documentElement.dataset.backgroundText=getTextFromElements(document,".comment>p");
    window.parent.postMessage("background-text","*");
}else{
    document.documentElement.classList.add("background-text");
    redoBackgroundText({data:"background-text"});
    window.on("message",redoBackgroundText);
}

getTextFromElements() does what it says: it queries an element with a selector, and gets text from those elements. redoBackgroundText() calls that getTextFromElements function a lot, and sets the data attribute. It also expects to be called in response to a postMessage event. inIframe() detects if you're running in an <iframe>, because it's important that different things happen in vs. not in one, which is what the if does. When in one, the if gets text from some elements, and sets the background text on the document, and sends a postMessage event. If this is not in an <iframe>, set the background-text class on the document, call redoBackgroundText() with a fake event, and register redoBackgroundText as a postMessage event listener.

In this way, the background text is based on the existing text on the page, but in vaguely reverse order. And when an <iframe> is in use (my comment sections), the comments are included, too!

So now I have this awesome background. But wait, there's more! To show it off, I need to have things be transparent. In my previous design, I had a gradient over the preview text below the posters. It went from transparent to the background color (to have it fade out). Since the negative space in the text wasn't transparent, there was an obvious edge where the background didn't show through. I needed to mask the gradient to the text:

.indexPage>article>p{
    background-image: linear-gradient(#50C0C0, rgb(2, 10, 29));
    -webkit-text-fill-color: transparent;
    -webkit-background-clip: text;
    background-clip: text;
}

The background-image sets the fade out gradient. The -webkit-text-fill-color: transparent tells all the text (including links) to take on the background (not have any color of its own). The magic in background-clip: text clears out the background image in places where it isn't text.

I hope this will allow you to rip off my (probably terrible) design. Do you like it? If not, I showed you some neat tricks anyway. Have fun!

Posted under Programming. 0 complaints.