http://bald.cat

spacecats animation

I have been itching to build a simple (shiny) LED board contraption for some time. If you'd indulge on a bit of idealism for a minute; the experience would be analagous to one sending something off to the printer - thoughts/code becoming physical.

The ascii-art based javascript/css animation from the front page of this site (... cats, space), is a bit of a step in that direction, this is it's workbook.

The animation converts an image into it's ascii-art representation - the simplified text based visual pattern - utilizing the Canvas api, and cycles the colours with CSS Keyframes.

What I was interested in is how one maps individual pixels from source to target constructs. Roughly, the image is resized so that each character output is based on the calculations performed on it's appropriate pixel; the working image's size is the image divided by the font size. The returned text is then chopped up into segments; a delay is added to each CSS animation.

Despite the current optimizations - which might need to be verified more thoroughly yet - transparencies are expensive; the animation therefore disables itself when the window loses focus. Each browser usually spawns a new helper for animations, one should definitely keep an eye on CPU load.

css

@keyframes scanline
{
    0% { color: rgba(100,100,100,.1); }
    20% { color: rgba(255,189,75,.9); }
    40% { color: rgba(65,68,81,.4); }
    100% { color: rgba(100,100,100,0); }
}

#spacecats-animation figure div.animate span
{
    animation: scanline 8s ease-in-out 1s infinite;
}

containers

#spacecats-animation figure
{
    position: relative;
    margin: -233px auto 0;
    height: 299px;
    z-index: -1;
}

#spacecats-animation figcaption
{
    height: 1px;
    overflow: hidden;
    margin-top: -1px;
}
figure#spacecats div {
    display: block;
    overflow: hidden;

    position: absolute;
    right: 0;
    bottom: 0;

    width: 473px;
    height: 299px;

    font-family: inconsolata, monospace;
    font-size: 8Px;
    line-height: 1;
    white-space: pre;
}
figure#spacecats div span {
    display: inline;
    margin: 0;
    padding: 0;

    color: rgba(100,100,100,0);

    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

javascript

(function (container) {
    if(!(!!container)) return;

    function init() {
	var imageSource = (container.currentStyle || window.getComputedStyle(container, null)).backgroundImage;
	if(!(!!imageSource)) return;

	var image = new Image();
	if(('"' == imageSource.charAt(4))) {
	    image.src = imageSource.substring(5, (imageSource.length - 2));
	} else {
	    image.src = imageSource.substring(4, (imageSource.length - 1));
	}

	if(image.complete) {
	    saveBatteryLife(animate(render(container, toAscii(image))));
	} else {
	    image.addEventListener('load', function() {
		saveBatteryLife(animate(render(container, toAscii(image))));
	    });
	}
    }

    <<animate>>
    <<render>>
    <<toAscii>>
    <<saveBatteryLife>>

    window.addEventListener('load', init);
})(document.getElementById('spacecats-animation'));
function animate(canvas) {
    if (!(!!canvas)) return;
    var state = !!(canvas.className == "animate");

    return function(animate) {
	if(animate === state) return;
	!animate ? canvas.className = "" : canvas.className = "animate";
	state = animate;
    }
}
function toAscii(sourceImage) {
    if(!sourceImage || !(!!window.CanvasRenderingContext2D)) return;

    var chars = [" "," "," ","b","a","l","d","c","a","t"],
	charsCount = (chars.length - 1),
	fontSize = 8,
	width = (2 * Math.floor(sourceImage.width / fontSize)),
	height = Math.floor(sourceImage.height / fontSize),
	asciiImage = '';

    var canvas = document.createElement('canvas');
    canvas.setAttribute('width', width);
    canvas.setAttribute('height', height);

    var ctx = canvas.getContext('2d', { alpha: false });
    if(!(!!ctx)) return;

    ctx.drawImage(sourceImage, 0, 0, width, height);

    var pixels = ctx.getImageData(0, 0, width, height);
    var pixelsCount = pixels.data.length;

    var line = '';
    for(var pixel=0; pixel <= pixelsCount; pixel += 4) {
	var scale = Math.max(pixels.data[pixel], pixels.data[(pixel + 1)], pixels.data[(pixel + 2)]) / 255;
	var character = chars[Math.floor(scale * charsCount)];

	if(line.length >= width) {
	    asciiImage += line + '\n';
	    line = '';
	}

	line += character;
    }

    return asciiImage;
}
function render(container, asciiImage) {
    if(!asciiImage) return;

    var animation = document.createDocumentFragment(),
	figure = document.createElement('figure'),
	caption = document.createElement('figcaption'),
	canvas = document.createElement('div');

    figure.setAttribute("id", "spacecats");
    animation.appendChild(figure);

    caption.innerText = 'spacecats splash image animation';
    figure.appendChild(caption);

    canvas.className = "animate";
    figure.appendChild(canvas);

    (function(segmentSize, imageLength) {
	for(var start=0; start < imageLength; start += segmentSize) {
	    var segment = document.createElement('span'),
		end = (start + segmentSize),
		delay = (3 - ((end / segmentSize) * .2)).toFixed(2);

	    segment.textContent = asciiImage.substring(start, end);
	    segment.setAttribute('style', 'animation-delay: ' + delay + 's');
	    canvas.appendChild(segment);
	}
    })(400, asciiImage.length);

    container.appendChild(animation);

    return canvas;
}
function saveBatteryLife(animate) {
    if (typeof animate === 'undefined') return;

    var prefix = (function selectPrefix(prefixes) {
	for(var count = prefixes.length; count--;) {
	    var prefix = prefixes[count];
	    if (typeof document[prefix + (!!prefix ? 'Hidden' : 'hidden')] !== 'undefined')
		return prefix;
	}
    })(['ms', 'o', 'webkit', 'moz', '']);

    function visibilityState() {
	var state = document[(!!prefix ? prefix + 'V' : 'v') + 'isibilityState'] || document[(!!prefix ? prefix + 'Hidden' : 'hidden')];

	if(state == 'visible') {
	    animate(true);
	} else if(state == 'hidden') {
	    animate(false);
	} else {
	    animate(!state);
	}
    }

    document.addEventListener((prefix + 'visibilitychange'), visibilityState, false);

    function onEvent(state, animate) {
	return function() {
	    animate(state);
	}
    }

    document.addEventListener('blur', (onEvent(false, animate)), false);
    document.addEventListener('focus', (onEvent(true, animate)), false);
    window.addEventListener('blur', (onEvent(false, animate)), false);
    window.addEventListener('focus', (onEvent(true, animate)), false);
}

2018 - Élő László hello at bald dot cat