<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">

  <title><![CDATA[Acko.net]]></title>
  <link href="http://acko.net/atom.xml" rel="self"/>
  <link href="http://acko.net/"/>
  <updated>2013-05-10T11:50:48-07:00</updated>
  <id>http://acko.net/</id>
  <author>
    <name><![CDATA[Steven Wittens]]></name>
    
  </author>

  
  <entry>
    <title type="html"><![CDATA[This is Your Brain on CSS]]></title>
    <link href="http://acko.net/blog/this-is-your-brain-on-css/"/>
    <updated>2012-02-19T00:00:00-08:00</updated>
    <id>http://acko.net/blog/this-is-your-brain-on-css</id>
    <content type="html"><![CDATA[<div style='display: none'><img alt='' src='/files/mri/cover.jpg' /></div><div class='g8 i2 first'><div class='pad'>

<h1>This is Your Brain on CSS</h1>

<p>First things first: the CSS 3D renderer used to power this site is now <a href='https://github.com/unconed/CSS3D.js'>available on GitHub.com</a>. However, it's still limited to only solid lines and planes. It's also limited to WebKit browsers, as Firefox's CSS 3D support just isn't quite there yet.</p>

<p>
  But CSS 3D is not a one trick pony, and as with many things, what you get out of it depends entirely on what you put in. So here's a disembodied head made out of CSS 3D. It consists of nothing more than a bunch of images stacked up against each other, and integrates perfectly with the existing 3D parallax on this site. Click and drag to rotate, or use the slider to look inside.
</p>

<link href='/files/mri/head.css' media='screen' rel='stylesheet' type='text/css' />

<div id='head-3d'>
  <div class='head-viewport'>
    <div class='CSS3DCamera' data-var='transform'>
      <div class='pedestal'>
        
      </div>
      <div class='VolumetricView' data-var='phi slice' />
    </div>
  </div>
  <div class='Slider' data-var='slice' />
</div>

<p>
  Making the basic effect was actually quite easy. I took an MRI from the <a href='http://graphics.stanford.edu/data/voldata/'>Stanford Volume Data Archive</a> and wrote a small script to turn it into a sheet of CSS sprites. There's <a href='/files/mri/MRbrain-color.jpg'>one file for color</a>, <a href='/files/mri/MRbrain-alpha8.png'>one for opacity</a>, totalling about 2.1 MB. Both files are composited into Canvases and placed in slices into the DOM, offset forward or backwards in 3D. Then there's just some minor logic to rotate the slices in 90 degree increments to follow the camera.
</p>

<p>
  But the slices are rendered as is, and the MRI consists of <a href='/files/mri/MRbrain-alpha8.png'>boring grayscale data</a>. Luckily, I can precompute any amount of shaders and effects I want and just bake them into the slices. I geeked out by applying fake specular lighting, for that 'fresh meat' look, and volumetric obscurance to enhance the sense of depth on the inside. I changed the palette to gory colors based on local density, giving the impression of flesh and bone knitting itself together. Creepy, but cool.
</p>

<p>
  I wrapped it in a custom widget, using straight up CSS rather than Three.js this time. I've wanted to play with <a href='http://worrydream.com/Tangle/'>Tangle.js</a>, so I used that to hook up the camera controls and slider. That's pretty much it. In an ideal world, the jarring transition when rotating would be covered up by a nice transition, but the browsers don't like it.
</p>

</div></div><pre class='markdown-html-error' style='border: solid 3px red; background-color: pink'>REXML could not parse this XML/HTML: 
&lt;script&gt;
setTimeout(function () {
  if ($(&apos;div.css3d-support:visible&apos;).length == 0) return;

    var model = {
      initialize: function () {
        // State
        this.theta = 0.0;
        this.phi = 0.5;
        this.slice = 0;
      },

      update: function () {
        this.transform = &apos;rotateX(&apos;+ -this.theta +&apos;rad) rotateY(&apos;+ this.phi +&apos;rad)&apos;;
      },
    };

    Tangle.classes.CSS3DCamera = {
      initialize: function (element, options, tangle, variables) {
        this.element = element;
        this.$element = $(element);

        var that = this;
        $(element).mousedown(function (event) {
          that.drag = true;
          that.dragLast = that.dragOrigin = { x: event.pageX, y: event.pageY };
          event.preventDefault();
        });
        $(document).mouseup(function (event) {
          that.drag = false;
        });
        $(document).mousemove(function (event) {
          if (!that.drag) return;
          var total = { x: event.pageX - that.dragOrigin.x, y: event.pageY - that.dragOrigin.y },
              delta = { x: event.pageX - that.dragLast.x, y: event.pageY - that.dragLast.y };
          that.dragLast = { x: event.pageX, y: event.pageY };
          mousemove(that.dragOrigin, total, delta);
        });

        function mousemove(origin, total, delta) {
          var phi = tangle.getValue(&apos;phi&apos;) + delta.x * .01,
              theta = Math.min(1, Math.max(-.2, tangle.getValue(&apos;theta&apos;) + delta.y * .01));

          tangle.setValue(&apos;phi&apos;, phi);
          tangle.setValue(&apos;theta&apos;, theta);
        }
      },

      update: function (element, value) {
        this.$element.css({
          WebkitTransform: value,
          MozTransform: value,
          transform: value,
        })
      },
    },

    Tangle.classes.Slider = {
      initialize: function (element, options, tangle, variables) {
        var that = this;

        this.tangle = tangle;
        this.element = element;

        this.$element = $(this.element);
        this.$bar = $(&apos;&lt;div class=&quot;bar&quot;&gt;&apos;).appendTo(this.element);
        this.$handle = $(&apos;&lt;div class=&quot;handle&quot;&gt;&apos;).appendTo(this.element);

        var that = this;
        $(this.element).mousedown(function (event) {
          that.origin = that.$element.offset().left;
          that.width = that.$bar.width();
          that.drag = true;
          return false;
        });
        $(document).mousemove(function (event) {
          if (!that.drag) return;
          tangle.setValue(&apos;slice&apos;, Math.max(0, Math.min(1, (event.pageX - that.origin) / that.width)));
        });
        $(document).mouseup(function () {
          that.drag = false;
        });
      },

      update: function (element, value) {
        this.$handle.css(&apos;left&apos;, (100*value) + &apos;%&apos;);
      },
    },

    Tangle.classes.VolumetricView = {
      initialize: function (element, options, tangle, variables) {
        var that = this;

        this.tangle = tangle;
        this.element = element;
        this.$element = $(element);

        this.width = 364;
        this.height = 384;
        this.depth = 256;

        this.resX = 182;
        this.resY = 192;
        this.slices = 108;
        this.stride = 8;
        this.rows = Math.ceil(this.slices / this.stride);

        this.createSlices();

        var load = 0;
        this.image = new Image();
        this.image.onload = function () {
          if (++load == 2) that.drawSlices(); 
        };

        this.mask = new Image();
        this.mask.onload = function () {
          if (++load == 2) that.drawSlices(); 
        };

//        this.image.src = &apos;data/MRbrain.png&apos;;
        this.image.src = &apos;/files/mri/MRbrain-color.jpg&apos;;
        this.mask.src = &apos;/files/mri/MRbrain-alpha8.png&apos;;
      },

      update: function (element, value) {
        var l = Math.abs(Math.cos(value)) &gt; Math.abs(Math.sin(value));

        if (this.l != l || this.slice != slice) {
          var slice = this.tangle.getValue(&apos;slice&apos;), index, 
              n = (l ? Math.cos(value) : Math.sin(value)) &gt; 0,
              sn = n ? slice : 1 - slice;

          index = +(this.$slicesX.length * sn);

          this.$slicesX.css(&apos;display&apos;, l ? &apos;block&apos; : &apos;none&apos;);
          this.$slicesX
            .slice().css(&apos;opacity&apos;, n ? .95 : .001).end()
            .slice(0, index).css(&apos;opacity&apos;, !n ? .95 : .001).end();

          index = +(this.$slicesZ.length * sn);
  //        this.$slicesZ[!l ? &apos;show&apos; : &apos;hide&apos;]();
          this.$slicesZ.css(&apos;display&apos;, !l ? &apos;block&apos; : &apos;none&apos;);
          this.$slicesZ
            .slice(index).css(&apos;opacity&apos;, n ? .95 : .001).end()
            .slice(0, index).css(&apos;opacity&apos;, !n ? .95 : .001).end();

          this.slice = slice;
          this.l = l;
        }
      },

      createSlices: function () {
        this.$element.empty();
        this.ctxX = [];
        this.ctxZ = [];

        // X slices
        for (var i = 0; i &lt; this.slices; ++i) {
          var z = -((i / this.slices) - .5) * this.depth,
              t = &apos;translateZ(&apos; + z + &apos;px) translateX(70px)&apos;;

          var $canvas = $(&apos;&lt;canvas&gt;&apos;)
              .addClass(&apos;x&apos;)
              .attr(&apos;width&apos;, this.resX)
              .attr(&apos;height&apos;, this.resY)
              .css({
                width: this.width,
                height: this.height,
                WebkitTransform: t,
                MozTransform: t,
                transform: t,
                opacity: 1,
              });

          this.$element.append($canvas);
          this.ctxX.push($canvas[0].getContext(&apos;2d&apos;));
        }

        // Z slices
        for (var i = 0; i &lt; this.resX; ++i) {
          var z = -(this.depth - this.width) / 2,
              x = ((i / this.resX) - .5) * this.width,
              t = &apos;translateX(&apos; + x + &apos;px) translateX(70px) rotateY(90deg) translateZ(&apos; + z + &apos;px)&apos;;

          var $canvas = $(&apos;&lt;canvas&gt;&apos;)
              .addClass(&apos;z&apos;)
              .attr(&apos;width&apos;, this.slices)
              .attr(&apos;height&apos;, this.resY)
              .css({
                width: this.depth,
                height: this.height,
                WebkitTransform: t,
                MozTransform: t,
                transform: t,
                opacity: 0,
              });

          this.$element.append($canvas);
          this.ctxZ.push($canvas[0].getContext(&apos;2d&apos;));
        }

        this.$slicesX = this.$element.find(&apos;canvas.x&apos;);
        this.$slicesZ = this.$element.find(&apos;canvas.z&apos;);
      },

      drawSlices: function () {

        var s = this.stride,
            sl = this.slices,
            r = this.rows,
            w = this.resX,
            h = this.resY,
            img = this.image,
            mask = this.mask,
            ctxX = this.ctxX,
            ctxZ = this.ctxZ;

        var alpha, color;

        // X slices
        this.$slicesX.each(function (i) {
          var c = ctxX[i],
              ox = (i % s) * w, oy = Math.floor(i / s) * h;

          // Draw alpha channel and get pixels
          c.drawImage(mask, ox, oy, w, h, 0, 0, w, h);
          alpha = c.getImageData(0, 0, w, h);

          // Draw color channel and get pixels
          c.drawImage(img, ox, oy, w, h, 0, 0, w, h);
          color = c.getImageData(0, 0, w, h);

          // Copy red to alpha.
          var src = alpha.data, dst = color.data;
          for (var y = 0; y &lt; h; ++y) {
            for (var x = 0; x &lt; w; ++x) {
              var o = (x + y * w) * 4;
              dst[o + 3] = src[o];
            }
          }

          // Draw RGBA.
          c.putImageData(color, 0, 0);
        });

        // Z slices
        this.$slicesZ.each(function (i) {
          var c = ctxZ[i];

          // Render transposed slices as vertical strips.
          for (var j = 0; j &lt; sl; ++j) {
            var ox = (j % s) * w, oy = Math.floor(j / s) * h;

            // Draw alpha channel
            c.drawImage(mask, ox + i, oy, 1, h, j, 0, 1, h);
          }

          // Get pixels
          alpha = c.getImageData(0, 0, w, h);

          // Render transposed slices as vertical strips.
          for (var j = 0; j &lt; sl; ++j) {
            var ox = (j % s) * w, oy = Math.floor(j / s) * h;

            // Draw color channel
            c.drawImage(img, ox + i, oy, 1, h, j, 0, 1, h);
          }
          // Get pixels
          color = c.getImageData(0, 0, w, h);

          // Copy red to alpha.
          var src = alpha.data, dst = color.data;
          for (var y = 0; y &lt; h; ++y) {
            for (var x = 0; x &lt; w; ++x) {
              var o = (x + y * w) * 4;
              dst[o + 3] = src[o];
            }
          }

          // Draw RGBA.
          c.putImageData(color, 0, 0);
        });
      },

    };

    var tangle = new Tangle($(&apos;#head-3d&apos;)[0], model);

}, 200);
&lt;/script&gt;</pre><pre class='markdown-html-error' style='border: solid 3px red; background-color: pink'>REXML could not parse this XML/HTML: 
&lt;script&gt;
//
//  Tangle.js
//  Tangle 0.1.0
//
//  Created by Bret Victor on 5/2/10.
//  (c) 2011 Bret Victor.  MIT open-source license.
//
//  ------ model ------
//
//  var tangle = new Tangle(rootElement, model);
//  tangle.setModel(model);
//
//  ------ variables ------
//
//  var value = tangle.getValue(variableName);
//  tangle.setValue(variableName, value);
//  tangle.setValues({ variableName:value, variableName:value });
//
//  ------ UI components ------
//
//  Tangle.classes.myClass = {
//     initialize: function (element, options, tangle, variable) { ... },
//     update: function (element, value) { ... }
//  };
//  Tangle.formats.myFormat = function (value) { return &quot;...&quot;; };
//

var Tangle = this.Tangle = function (rootElement, modelClass) {

    var tangle = this;
    tangle.element = rootElement;
    tangle.setModel = setModel;
    tangle.getValue = getValue;
    tangle.setValue = setValue;
    tangle.setValues = setValues;

    var _model;
    var _nextSetterID = 0;
    var _setterInfosByVariableName = {};   //  { varName: { setterID:7, setter:function (v) { } }, ... }
    var _varargConstructorsByArgCount = [];


    //----------------------------------------------------------
    //
    // construct

    initializeElements();
    setModel(modelClass);
    return tangle;


    //----------------------------------------------------------
    //
    // elements

    function initializeElements() {
        var elements = rootElement.getElementsByTagName(&quot;*&quot;);
        var interestingElements = [];
        
        // build a list of elements with class or data-var attributes
        
        for (var i = 0, length = elements.length; i &lt; length; i++) {
            var element = elements[i];
            if (element.getAttribute(&quot;class&quot;) || element.getAttribute(&quot;data-var&quot;)) {
                interestingElements.push(element);
            }
        }

        // initialize interesting elements in this list.  (Can&apos;t traverse &quot;elements&quot;
        // directly, because elements is &quot;live&quot;, and views that change the node tree
        // will change elements mid-traversal.)
        
        for (var i = 0, length = interestingElements.length; i &lt; length; i++) {
            var element = interestingElements[i];
            
            var varNames = null;
            var varAttribute = element.getAttribute(&quot;data-var&quot;);
            if (varAttribute) { varNames = varAttribute.split(&quot; &quot;); }

            var views = null;
            var classAttribute = element.getAttribute(&quot;class&quot;);
            if (classAttribute) {
                var classNames = classAttribute.split(&quot; &quot;);
                views = getViewsForElement(element, classNames, varNames);
            }
            
            if (!varNames) { continue; }
            
            var didAddSetter = false;
            if (views) {
                for (var j = 0; j &lt; views.length; j++) {
                    if (!views[j].update) { continue; }
                    addViewSettersForElement(element, varNames, views[j]);
                    didAddSetter = true;
                }
            }
            
            if (!didAddSetter) {
                var formatAttribute = element.getAttribute(&quot;data-format&quot;);
                var formatter = getFormatterForFormat(formatAttribute, varNames);
                addFormatSettersForElement(element, varNames, formatter);
            }
        }
    }
            
    function getViewsForElement(element, classNames, varNames) {   // initialize classes
        var views = null;
        
        for (var i = 0, length = classNames.length; i &lt; length; i++) {
            var clas = Tangle.classes[classNames[i]];
            if (!clas) { continue; }
            
            var options = getOptionsForElement(element);
            var args = [ element, options, tangle ];
            if (varNames) { args = args.concat(varNames); }
            
            var view = constructClass(clas, args);
            
            if (!views) { views = []; }
            views.push(view);
        }
        
        return views;
    }
    
    function getOptionsForElement(element) {   // might use dataset someday
        var options = {};

        var attributes = element.attributes;
        var regexp = /^data-[\w\-]+$/;

        for (var i = 0, length = attributes.length; i &lt; length; i++) {
            var attr = attributes[i];
            var attrName = attr.name;
            if (!attrName || !regexp.test(attrName)) { continue; }
            
            options[attrName.substr(5)] = attr.value;
        }
         
        return options;   
    }
    
    function constructClass(clas, args) {
        if (typeof clas !== &quot;function&quot;) {  // class is prototype object
            var View = function () { };
            View.prototype = clas;
            var view = new View();
            if (view.initialize) { view.initialize.apply(view,args); }
            return view;
        }
        else {  // class is constructor function, which we need to &quot;new&quot; with varargs (but no built-in way to do so)
            var ctor = _varargConstructorsByArgCount[args.length];
            if (!ctor) {
                var ctorArgs = [];
                for (var i = 0; i &lt; args.length; i++) { ctorArgs.push(&quot;args[&quot; + i + &quot;]&quot;); }
                var ctorString = &quot;(function (clas,args) { return new clas(&quot; + ctorArgs.join(&quot;,&quot;) + &quot;); })&quot;;
                ctor = eval(ctorString);   // nasty
                _varargConstructorsByArgCount[args.length] = ctor;   // but cached
            }
            return ctor(clas,args);
        }
    }
    

    //----------------------------------------------------------
    //
    // formatters

    function getFormatterForFormat(formatAttribute, varNames) {
        if (!formatAttribute) { formatAttribute = &quot;default&quot;; }

        var formatter = getFormatterForCustomFormat(formatAttribute, varNames);
        if (!formatter) { formatter = getFormatterForSprintfFormat(formatAttribute, varNames); }
        if (!formatter) { log(&quot;Tangle: unknown format: &quot; + formatAttribute); formatter = getFormatterForFormat(null,varNames); }

        return formatter;
    }
        
    function getFormatterForCustomFormat(formatAttribute, varNames) {
        var components = formatAttribute.split(&quot; &quot;);
        var formatName = components[0];
        if (!formatName) { return null; }
        
        var format = Tangle.formats[formatName];
        if (!format) { return null; }
        
        var formatter;
        var params = components.slice(1);
        
        if (varNames.length &lt;= 1 &amp;&amp; params.length === 0) {  // one variable, no params
            formatter = format;
        }
        else if (varNames.length &lt;= 1) {  // one variable with params
            formatter = function (value) {
                var args = [ value ].concat(params);
                return format.apply(null, args);
            };
        }
        else {  // multiple variables
            formatter = function () {
                var values = getValuesForVariables(varNames);
                var args = values.concat(params);
                return format.apply(null, args);
            };
        }
        return formatter;
    }
    
    function getFormatterForSprintfFormat(formatAttribute, varNames) {
        if (!sprintf || !formatAttribute.test(/\%/)) { return null; }

        var formatter;
        if (varNames.length &lt;= 1) {  // one variable
            formatter = function (value) {
                return sprintf(formatAttribute, value);
            };
        }
        else {
            formatter = function (value) {  // multiple variables
                var values = getValuesForVariables(varNames);
                var args = [ formatAttribute ].concat(values);
                return sprintf.apply(null, args);
            };
        }
        return formatter;
    }

    
    //----------------------------------------------------------
    //
    // setters
    
    function addViewSettersForElement(element, varNames, view) {   // element has a class with an update method
        var setter;
        if (varNames.length &lt;= 1) {
            setter = function (value) { view.update(element, value); };
        }
        else {
            setter = function () {
                var values = getValuesForVariables(varNames);
                var args = [ element ].concat(values);
                view.update.apply(view,args);
            };
        }

        addSetterForVariables(setter, varNames);
    }

    function addFormatSettersForElement(element, varNames, formatter) {  // tangle is injecting a formatted value itself
        var span = null;
        var setter = function (value) {
            if (!span) { 
                span = document.createElement(&quot;span&quot;);
                element.insertBefore(span, element.firstChild);
            }
            span.innerHTML = formatter(value);
        };

        addSetterForVariables(setter, varNames);
    }
    
    function addSetterForVariables(setter, varNames) {
        var setterInfo = { setterID:_nextSetterID, setter:setter };
        _nextSetterID++;

        for (var i = 0; i &lt; varNames.length; i++) {
            var varName = varNames[i];
            if (!_setterInfosByVariableName[varName]) { _setterInfosByVariableName[varName] = []; }
            _setterInfosByVariableName[varName].push(setterInfo);
        }
    }

    function applySettersForVariables(varNames) {
        var appliedSetterIDs = {};  // remember setterIDs that we&apos;ve applied, so we don&apos;t call setters twice
    
        for (var i = 0, ilength = varNames.length; i &lt; ilength; i++) {
            var varName = varNames[i];
            var setterInfos = _setterInfosByVariableName[varName];
            if (!setterInfos) { continue; }
            
            var value = _model[varName];
            
            for (var j = 0, jlength = setterInfos.length; j &lt; jlength; j++) {
                var setterInfo = setterInfos[j];
                if (setterInfo.setterID in appliedSetterIDs) { continue; }  // if we&apos;ve already applied this setter, move on
                appliedSetterIDs[setterInfo.setterID] = true;
                
                setterInfo.setter(value);
            }
        }
    }
    

    //----------------------------------------------------------
    //
    // variables

    function getValue(varName) {
        var value = _model[varName];
        if (value === undefined) { log(&quot;Tangle: unknown variable: &quot; + varName);  return 0; }
        return value;
    }

    function setValue(varName, value) {
        var obj = {};
        obj[varName] = value;
        setValues(obj);
    }

    function setValues(obj) {
        var changedVarNames = [];

        for (var varName in obj) {
            var value = obj[varName];
            var oldValue = _model[varName];
            if (oldValue === undefined) { log(&quot;Tangle: setting unknown variable: &quot; + varName);  continue; }
            if (oldValue === value) { continue; }  // don&apos;t update if new value is the same

            _model[varName] = value;
            changedVarNames.push(varName);
        }
        
        if (changedVarNames.length) {
            applySettersForVariables(changedVarNames);
            updateModel();
        }
    }
    
    function getValuesForVariables(varNames) {
        var values = [];
        for (var i = 0, length = varNames.length; i &lt; length; i++) {
            values.push(getValue(varNames[i]));
        }
        return values;
    }

                    
    //----------------------------------------------------------
    //
    // model

    function setModel(modelClass) {
        var ModelClass = function () { };
        ModelClass.prototype = modelClass;
        _model = new ModelClass;

        updateModel(true);  // initialize and update
    }
    
    function updateModel(shouldInitialize) {
        var ShadowModel = function () {};  // make a shadow object, so we can see exactly which properties changed
        ShadowModel.prototype = _model;
        var shadowModel = new ShadowModel;
        
        if (shouldInitialize) { shadowModel.initialize(); }
        shadowModel.update();
        
        var changedVarNames = [];
        for (var varName in shadowModel) {
            if (!shadowModel.hasOwnProperty(varName)) { continue; }
            if (_model[varName] === shadowModel[varName]) { continue; }
            
            _model[varName] = shadowModel[varName];
            changedVarNames.push(varName);
        }
        
        applySettersForVariables(changedVarNames);
    }


    //----------------------------------------------------------
    //
    // debug

    function log (msg) {
        if (window.console) { window.console.log(msg); }
    }

};  // end of Tangle


//----------------------------------------------------------
//
// components

Tangle.classes = {};
Tangle.formats = {};

Tangle.formats[&quot;default&quot;] = function (value) { return &quot;&quot; + value; };

&lt;/script&gt;</pre>]]></content>
  </entry>
  
</feed>
