├── readme.md ├── test.html ├── textualizer.js └── textualizer.min.js /readme.md: -------------------------------------------------------------------------------- 1 | Textualizer is a jQuery plug-in that allows you to transition through blurbs of text. When transitioning to a new blurb, any character that is common to the next blurb is kept on the screen, and moved to its new position. 2 | 3 | See it in action [here](http://krisk.github.io/textualizer/#demo) 4 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Textualizer example 5 | 6 | 7 | 8 | 19 | 20 | 21 |
22 |
23 |

Textualizer is a jQuery plug-in that allows you to transition through blurbs of text.

24 |

When transitioning to a new blurb, any character that is common to the next blurb is kept on the screen, and moved to its new position.

25 |

Textualize: verb - to put into text, set down as concrete and unchanging. Use Textualizer to transition through blurbs of text.

26 |

Blurb: noun - a short summary or some words of praise accompanying a creative work. A promotional description.

27 |
28 |
29 | 30 | 31 | 32 | 39 | -------------------------------------------------------------------------------- /textualizer.js: -------------------------------------------------------------------------------- 1 | /** 2 | Textualizer v2.5.0 3 | @author Kirollos Risk 4 | 5 | Dual licensed under the MIT or GPL Version 2 licenses. 6 | 7 | Copyright (c) 2011 Kirollos Risk 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. 26 | */ 27 | (function ($, window) { 28 | 29 | "use strict"; 30 | 31 | var Textualizer, 32 | 33 | COMMON_CHARACTER_ARRANGE_DELAY = 1000, 34 | REMAINING_CHARACTERS_DELAY = 500, 35 | EFFECT_DURATION = 2000, 36 | REMAINING_CHARACTERS_APPEARANCE_MAX_DELAY = 2000, 37 | REMOVE_CHARACTERS_MAX_DELAY = 2000, 38 | 39 | EVENT_CHANGED = 'textualizer.changed'; 40 | 41 | // Gets the computed style of an element 42 | function getStyle(element) { 43 | 44 | var computedStyle, key, camelCasedStyle, i, len, styleList = {}; 45 | 46 | if (window.getComputedStyle) { 47 | computedStyle = window.getComputedStyle(element, null); 48 | 49 | if (computedStyle.length) { 50 | for (i = 0, len = computedStyle.length; i < len; i++) { 51 | camelCasedStyle = computedStyle[i].replace(/\-([a-z])/, function (a, b) { 52 | return b.toUpperCase(); 53 | }); 54 | 55 | styleList[camelCasedStyle] = computedStyle.getPropertyValue(computedStyle[i]); 56 | } 57 | } else { 58 | for (key in computedStyle) { 59 | if (typeof computedStyle[key] !== 'function' && key !== 'length') { 60 | styleList[key] = computedStyle[key]; 61 | } 62 | } 63 | } 64 | } else { 65 | computedStyle = element.currentStyle || element.style; 66 | 67 | for (key in computedStyle) { 68 | if (Object.prototype.hasOwnProperty.call(computedStyle, key)) { 69 | styleList[key] = computedStyle[key]; 70 | } 71 | } 72 | } 73 | 74 | return styleList; 75 | } 76 | 77 | function Character() { 78 | this.character = null; // A character 79 | this.domNode = null; // The span element that wraps around the character 80 | this.pos = null; // The domNode position 81 | this.used = false; 82 | this.inserted = false; 83 | this.visited = false; 84 | } 85 | 86 | function Snippet() { 87 | this.str = ''; // The text string 88 | this.characterList = []; // Array of ch objects 89 | } 90 | 91 | Snippet.prototype = { 92 | // Loops through , and find the first character that matches , and hasn't been already used. 93 | use: function (val) { 94 | var ch = null; 95 | 96 | $.each(this.characterList, function () { 97 | if (this.character === val && !this.used) { 98 | this.used = true; 99 | ch = this; 100 | return false; // break; 101 | } 102 | }); 103 | 104 | return ch; 105 | }, 106 | // Resets ever character in 107 | reset: function () { 108 | $.each(this.characterList, function () { 109 | this.inserted = false; 110 | this.used = false; 111 | }); 112 | } 113 | }; 114 | 115 | Textualizer = function ($element, data, options) { 116 | var self = this, 117 | list = [], 118 | snippets, 119 | 120 | index, previous, showCharEffect = null, 121 | 122 | playing = false, 123 | paused = false, 124 | 125 | elementHeight, position, 126 | 127 | $clone, $container, $phantomContainer; 128 | 129 | // If an effect is chosen, then look for it in the list of effects 130 | if (options.effect !== 'random') { 131 | $.each($.fn.textualizer.effects, function () { 132 | if (this[0] === options.effect) { 133 | showCharEffect = this[1]; 134 | return false; // break; 135 | } 136 | }); 137 | } 138 | 139 | // Clone the target element, and remove the id attribute (if it has one) 140 | // Why remove the id? Cuz when we clone an element, the id is also copied. That's a very bad thing, 141 | $clone = $element.clone().removeAttr('id').appendTo(window.document.body); 142 | 143 | // Copy all the styles. This is especially necessary if the clone was being styled by id in a stylesheet) 144 | $clone.css(getStyle($element[0])); 145 | 146 | // Note that the clone needs to be visible so we can do the proper calculation 147 | // of the position of every character. Ergo, move the clone outside of the window's 148 | // visible area. 149 | $clone.css({ 150 | position: 'absolute', 151 | top: '-1000px' 152 | }); 153 | 154 | $phantomContainer = $('
').css({ 155 | 'position': 'relative', 156 | 'visibility': 'hidden' 157 | }).appendTo($clone); 158 | 159 | // Make sure any animating character disappear when outside the boundaries of 160 | // the element 161 | $element.css('overflow', 'hidden'); 162 | 163 | // Contains transitioning text 164 | $container = $('
').css('position', 'relative').appendTo($element); 165 | 166 | elementHeight = $element.height(); 167 | 168 | position = { 169 | bottom: elementHeight 170 | }; 171 | 172 | function positionSnippet(snippet, phantomSnippets) { 173 | // If options.centered is true, then we need to center the text. 174 | // This cannot be done solely with CSS, because of the absolutely positioned characters 175 | // within a relative container. Ergo, to achieve a vertically-aligned look, do 176 | // the following simple math: 177 | var yOffset = options.centered ? (elementHeight - $phantomContainer.height()) / 2 : 0; 178 | 179 | // Figure out the positioning, and clone the character's domNode 180 | $.each(phantomSnippets, function (index, c) { 181 | c.pos = c.domNode.position(); 182 | c.domNode = c.domNode.clone(); 183 | 184 | c.pos.top += yOffset; 185 | 186 | c.domNode.css({ 187 | 'left': c.pos.left, 188 | 'top': c.pos.top, 189 | 'position': 'absolute' 190 | }); 191 | 192 | snippet.characterList.push(c); 193 | }); 194 | 195 | $phantomContainer.html(''); 196 | } 197 | 198 | /* PRIVATE FUNCTIONS */ 199 | 200 | // Add all chars first to the phantom container. Let the browser deal with the formatting. 201 | function addCharsToSnippet(i) { 202 | var phantomSnippets = [], 203 | snippet = new Snippet(), 204 | j, ch, c, len; 205 | 206 | snippet.str = list[i]; 207 | snippets.push(snippet); 208 | 209 | for (j = 0, len = snippet.str.length; j < len; j++) { 210 | ch = snippet.str.charAt(j); 211 | 212 | if (ch === '') { 213 | $phantomContainer.append(' '); 214 | } else { 215 | c = new Character(); 216 | c.character = ch; 217 | c.domNode = $('').text(ch); 218 | 219 | $phantomContainer.append(c.domNode); 220 | phantomSnippets.push(c); 221 | } 222 | } 223 | 224 | positionSnippet(snippet, phantomSnippets); 225 | 226 | return snippet; 227 | } 228 | 229 | function getHideEffect() { 230 | var dfd, eff; 231 | 232 | eff = [ 233 | 234 | function (target) { 235 | dfd = $.Deferred(); 236 | target.animate({ 237 | top: position.bottom, 238 | opacity: 'hide' 239 | }, dfd.resolve); 240 | return dfd.promise(); 241 | }, function (target) { 242 | dfd = $.Deferred(); 243 | target.fadeOut(1000, dfd.resolve); 244 | return dfd.promise(); 245 | }]; 246 | 247 | return eff[Math.floor(Math.random() * eff.length)]; 248 | } 249 | 250 | function removeCharacters(previousSnippet, currentSnippet) { 251 | var keepList = [], 252 | removeList = [], 253 | finalDfd = $.Deferred(), 254 | hideEffect = getHideEffect(), 255 | currChar; 256 | 257 | // For every character in the previous text, check if it exists in the current text. 258 | // YES ==> keep the character in the DOM 259 | // NO ==> remove the character from the DOM 260 | $.each(previousSnippet.characterList, function (index, prevChar) { 261 | currChar = currentSnippet.use(prevChar.character); 262 | 263 | if (currChar) { 264 | currChar.domNode = prevChar.domNode; // use the previous DOM domNode 265 | currChar.inserted = true; 266 | 267 | keepList.push(currChar); 268 | } else { 269 | (function hideCharacter(deferred) { 270 | removeList.push(deferred); 271 | hideEffect(prevChar.domNode.delay(Math.random() * REMOVE_CHARACTERS_MAX_DELAY)).done(function () { 272 | prevChar.domNode.remove(); 273 | deferred.resolve(); 274 | }); 275 | })($.Deferred()); 276 | } 277 | }); 278 | 279 | $.when.apply(null, removeList).done(function () { 280 | return finalDfd.resolve(keepList); 281 | }); 282 | 283 | return finalDfd.promise(); 284 | } 285 | 286 | function showCharacters(snippet) { 287 | var effects = $.fn.textualizer.effects, 288 | 289 | effect = options.effect === 'random' ? effects[Math.floor(Math.random() * (effects.length - 2)) + 1][1] : showCharEffect, 290 | 291 | finalDfd = $.Deferred(), 292 | animationDfdList = []; 293 | 294 | // Iterate through all ch objects 295 | $.each(snippet.characterList, function (index, ch) { 296 | // If the character has not been already inserted, animate it, with a delay 297 | if (!ch.inserted) { 298 | 299 | ch.domNode.css({ 300 | 'left': ch.pos.left, 301 | 'top': ch.pos.top 302 | }); 303 | 304 | (function animateCharacter(deferred) { 305 | window.setTimeout(function () { 306 | effect({ 307 | item: ch, 308 | container: $container, 309 | dfd: deferred 310 | }); 311 | }, Math.random() * REMAINING_CHARACTERS_APPEARANCE_MAX_DELAY); 312 | animationDfdList.push(deferred); 313 | })($.Deferred()); 314 | 315 | } 316 | }); 317 | 318 | // When all characters have finished moving to their position, resolve the final promise 319 | $.when.apply(null, animationDfdList).done(function () { 320 | finalDfd.resolve(); 321 | }); 322 | 323 | return finalDfd.promise(); 324 | } 325 | 326 | function moveAndShowRemainingCharacters(characters, currentSnippet) { 327 | var finalDfd = $.Deferred(), 328 | rearrangeDfdList = []; 329 | 330 | // Move charactes that are common to their new position 331 | window.setTimeout(function () { 332 | $.each(characters, function (index, item) { 333 | 334 | (function rearrangeCharacters(deferred) { 335 | item.domNode.animate({ 336 | 'left': item.pos.left, 337 | 'top': item.pos.top 338 | }, options.rearrangeDuration, deferred.resolve); 339 | rearrangeDfdList.push(deferred.promise()); 340 | })($.Deferred()); 341 | 342 | }); 343 | // When all the characters have moved to their new position, show the remaining characters 344 | $.when.apply(null, rearrangeDfdList).done(function () { 345 | window.setTimeout(function () { 346 | showCharacters(currentSnippet).done(function () { 347 | finalDfd.resolve(); 348 | }); 349 | }, REMAINING_CHARACTERS_DELAY); 350 | }); 351 | }, COMMON_CHARACTER_ARRANGE_DELAY); 352 | 353 | return finalDfd.promise(); 354 | } 355 | 356 | function rotater() { 357 | // If we've reached the last snippet 358 | if (index === list.length - 1) { 359 | 360 | // Reset the position of every character in every snippet 361 | $.each(snippets, function (j, snippet) { 362 | snippet.reset(); 363 | }); 364 | index = -1; 365 | 366 | // If loop=false, pause (i.e., pause at this last blurb) 367 | if (!options.loop) { 368 | self.pause(); 369 | } 370 | } 371 | 372 | index++; 373 | next(index); // rotate the next snippet 374 | } 375 | 376 | function rotate(i) { 377 | var dfd = $.Deferred(), 378 | current = snippets[i]; 379 | 380 | // If this is the first time the blurb is encountered, each character in the blurb is wrapped in 381 | // a span and appended to an invisible container, thus we're able to calculate the character's position 382 | if (!current) { 383 | current = addCharsToSnippet(i); 384 | } 385 | 386 | if (previous) { 387 | removeCharacters(previous, current).done(function (characters) { 388 | moveAndShowRemainingCharacters(characters, current).done(function () { 389 | dfd.resolve(); 390 | }); 391 | }); 392 | 393 | } else { 394 | showCharacters(current).done(function () { 395 | dfd.resolve(); 396 | }); 397 | } 398 | 399 | previous = current; 400 | 401 | return dfd.promise(); 402 | } 403 | 404 | function next(i) { 405 | if (paused) { 406 | return; 407 | } 408 | 409 | // returns a promise, which completes when a blurb has finished animating. When that 410 | // promise is fulfilled, transition to the next blurb. 411 | rotate(i).done(function () { 412 | $element.trigger(EVENT_CHANGED, { 413 | index: i 414 | }); 415 | window.setTimeout(rotater, options.duration); 416 | }); 417 | } 418 | 419 | /* PRIVILEDGED FUNCTIONS */ 420 | 421 | this.data = function (dataSource) { 422 | this.stop(); 423 | list = dataSource; 424 | snippets = []; 425 | }; 426 | 427 | this.stop = function () { 428 | this.pause(); 429 | playing = false; 430 | previous = null; 431 | index = 0; 432 | $container.empty(); 433 | $phantomContainer.empty(); 434 | }; 435 | 436 | this.pause = function () { 437 | paused = true; 438 | playing = false; 439 | }; 440 | 441 | this.start = function () { 442 | if (list.length === 0 || playing) { 443 | return; 444 | } 445 | 446 | index = index || 0; 447 | playing = true; 448 | paused = false; 449 | 450 | next(index); 451 | }; 452 | 453 | this.destroy = function () { 454 | $container.parent().removeData('textualizer').end().remove(); 455 | 456 | $phantomContainer.remove(); 457 | }; 458 | 459 | if (data && data instanceof Array) { 460 | this.data(data); 461 | } 462 | }; 463 | 464 | $.fn.textualizer = function ( /*args*/ ) { 465 | var args = arguments, 466 | snippets, options, instance, txtlzr; 467 | 468 | // Creates a textualizer instance (if it doesn't already exist) 469 | txtlzr = (function ($element) { 470 | 471 | instance = $element.data('textualizer'); 472 | 473 | if (!instance) { 474 | snippets = []; 475 | 476 | if (args.length === 1 && args[0] instanceof Array) { 477 | snippets = args[0]; 478 | } else if (args.length === 1 && typeof args[0] === 'object') { 479 | options = args[0]; 480 | } else if (args.length === 2) { 481 | snippets = args[0]; 482 | options = args[1]; 483 | } 484 | 485 | if (snippets.length === 0) { 486 | $element.find('p').each(function () { 487 | snippets.push($(this).text()); 488 | }); 489 | } 490 | 491 | // Clear the contents in the container, since this is where the blurbs will go 492 | $element.html(""); 493 | 494 | // Create a textualizer instance, and store in the HTML node's metadata 495 | instance = new Textualizer($element, snippets, $.extend({}, $.fn.textualizer.defaults, options)); 496 | $element.data('textualizer', instance); 497 | } 498 | 499 | return instance; 500 | 501 | })(this); 502 | 503 | if (typeof args[0] === 'string' && txtlzr[args[0]]) { 504 | txtlzr[args[0]].apply(txtlzr, Array.prototype.slice.call(args, 1)); 505 | } 506 | 507 | return this; 508 | }; 509 | 510 | $.fn.textualizer.defaults = { 511 | effect: 'random', 512 | duration: 2000, 513 | rearrangeDuration: 1000, 514 | centered: false, 515 | loop: true 516 | }; 517 | 518 | // Effects for characters transition+animation. Customize as you please 519 | $.fn.textualizer.effects = [ 520 | ['none', function (obj) { 521 | obj.container.append(obj.item.domNode.show()); 522 | }], 523 | ['fadeIn', function (obj) { 524 | obj.container.append(obj.item.domNode.fadeIn(EFFECT_DURATION, obj.dfd.resolve)); 525 | return obj.dfd.promise(); 526 | }], 527 | ['slideLeft', function (obj) { 528 | obj.item.domNode.appendTo(obj.container).css({ 529 | 'left': -1000 530 | }).show().animate({ 531 | 'left': obj.item.pos.left 532 | }, EFFECT_DURATION, obj.dfd.resolve); 533 | 534 | return obj.dfd.promise(); 535 | }], 536 | ['slideTop', function (obj) { 537 | obj.item.domNode.appendTo(obj.container).css({ 538 | 'top': -1000 539 | }).show().animate({ 540 | 'top': obj.item.pos.top 541 | }, EFFECT_DURATION, obj.dfd.resolve); 542 | 543 | return obj.dfd.promise(); 544 | }] 545 | ]; 546 | 547 | })(jQuery, window); -------------------------------------------------------------------------------- /textualizer.min.js: -------------------------------------------------------------------------------- 1 | (function(b,j){function x(a){var b,d,f,n={};if(j.getComputedStyle)if(a=j.getComputedStyle(a,null),a.length){d=0;for(f=a.length;d").text(i),l.append(r.domNode),h.push(r));var s=d.centered?(z-l.height())/2:0;b.each(h,function(a,b){b.pos=b.domNode.position();b.domNode=b.domNode.clone();b.pos.top+=s;b.domNode.css({left:b.pos.left,top:b.pos.top,position:"absolute"});g.characterList.push(b)});l.html("");o=g}if(v){var h=v,x=o,B=[],C=[],D=b.Deferred(),E,p;k=[function(a){p=b.Deferred();a.animate({top:F.bottom,opacity:"hide"},p.resolve);return p.promise()},function(a){p=b.Deferred();a.fadeOut(1E3, 5 | p.resolve);return p.promise()}];E=k[Math.floor(Math.random()*k.length)];var w;b.each(h.characterList,function(a,d){if(w=x.use(d.character))w.domNode=d.domNode,w.inserted=!0,B.push(w);else{var c=b.Deferred();C.push(c);E(d.domNode.delay(Math.random()*L)).done(function(){d.domNode.remove();c.resolve()})}});b.when.apply(null,C).done(function(){return D.resolve(B)});D.promise().done(function(a){var c=o,g=b.Deferred(),h=[];j.setTimeout(function(){b.each(a,function(a,c){var e=b.Deferred();c.domNode.animate({left:c.pos.left, 6 | top:c.pos.top},d.rearrangeDuration,e.resolve);h.push(e.promise())});b.when.apply(null,h).done(function(){j.setTimeout(function(){f(c).done(function(){g.resolve()})},J)})},I);g.promise().done(function(){e.resolve()})})}else f(o).done(function(){e.resolve()});v=o;e.promise().done(function(){a.trigger(M,{index:c});j.setTimeout(n,d.duration)})}}var G=this,t=[],u,e,v,s=null,i=!1,y=!1,z,F,m,h,l;"random"!==d.effect&&b.each(b.fn.textualizer.effects,function(){if(this[0]===d.effect)return s=this[1],!1});m= 7 | a.clone().removeAttr("id").appendTo(j.document.body);m.css(x(a[0]));m.css({position:"absolute",top:"-1000px"});l=b("
").css({position:"relative",visibility:"hidden"}).appendTo(m);a.css("overflow","hidden");h=b("
").css("position","relative").appendTo(a);z=a.height();F={bottom:z};this.data=function(a){this.stop();t=a;u=[]};this.stop=function(){this.pause();i=!1;v=null;e=0;h.empty();l.empty()};this.pause=function(){y=!0;i=!1};this.start=function(){0===t.length||i||(e=e||0,i=!0,y=!1,q(e))}; 8 | this.destroy=function(){h.parent().removeData("textualizer").end().remove();l.remove()};c&&c instanceof Array&&this.data(c)};b.fn.textualizer=function(){var a=arguments,c,d,f;f=this.data("textualizer");f||(c=[],1===a.length&&a[0]instanceof Array?c=a[0]:1===a.length&&"object"===typeof a[0]?d=a[0]:2===a.length&&(c=a[0],d=a[1]),0===c.length&&this.find("p").each(function(){c.push(b(this).text())}),this.html(""),f=new q(this,c,b.extend({},b.fn.textualizer.defaults,d)),this.data("textualizer",f));d=f;"string"=== 9 | typeof a[0]&&d[a[0]]&&d[a[0]].apply(d,Array.prototype.slice.call(a,1));return this};b.fn.textualizer.defaults={effect:"random",duration:2E3,rearrangeDuration:1E3,centered:!1,loop:!0};b.fn.textualizer.effects=[["none",function(a){a.container.append(a.item.domNode.show())}],["fadeIn",function(a){a.container.append(a.item.domNode.fadeIn(2E3,a.dfd.resolve));return a.dfd.promise()}],["slideLeft",function(a){a.item.domNode.appendTo(a.container).css({left:-1E3}).show().animate({left:a.item.pos.left},2E3, 10 | a.dfd.resolve);return a.dfd.promise()}],["slideTop",function(a){a.item.domNode.appendTo(a.container).css({top:-1E3}).show().animate({top:a.item.pos.top},2E3,a.dfd.resolve);return a.dfd.promise()}]]})(jQuery,window); --------------------------------------------------------------------------------