views:

265

answers:

2

Need some advice about a best-approach for this problem:

I've fallen madly in love with RaphaëlJS, as it has made SVG realizable for me and my coding since it has managed to bring IE into the fold.

However, I don't like having to include a .js file for every SVG graphic I want to render on the page.

So, I poked around on the internet to see if I could find something that was more "dynamic", and I found this: (the below is my edited version of code I found here: http://groups.google.com/group/raphaeljs/msg/ce59df3d01736a6f)

function parseXML(xml) { 
  if (window.ActiveXObject && window.GetObject) { 
    var dom = new ActiveXObject('Microsoft.XMLDOM'); 
    dom.loadXML(xml); 
    return dom; 
  } 
  if (window.DOMParser) {
    return new DOMParser().parseFromString(xml, 'text/xml');
    throw new Error('No XML parser available');
  }
}

(function($) {

  $.fn.render_raphaels = function(options) {
    var defaults, options, counter, img_path, doc, root, vb, dims, img, node, path, atts, container, new_svg, inline;
    defaults = {};
    options = $.extend(defaults, options);

    counter = 0;
    inline = false;
    // find all the img's that point to SVGs
    $('img[src*="\.svg"]').each(function() {
      $(this).fadeOut(1000);
      img_path = $(this).attr('src');
      if (!$(this).attr('id')) new_svg = true;
      if ($(this).hasClass('inline')) inline = true;
      container = jQuery('<div/>', {
        id: $(this).attr('id') ? $(this).attr('id') : 'svg-' + (counter + 1),
        'class': $(this).attr('class') ? $(this).attr('class') : 'svg'
      }).hide().insertBefore($(this));

      $.get(img_path, null, function(doc) { 

        doc = parseXML(doc);
        root = $(doc).find('svg')[0];
        dims = [root.getAttribute('width'), root.getAttribute('height')];

        if(new_svg) container.css({ width: dims[0], height: dims[1] });
        if(inline) container.css('display', 'inline-block');

        img = Raphael(container.attr('id'), parseInt(dims[0]), parseInt(dims[1]));

        $(root).find('path').each(function() {
          node = this;
          path = img.path($(this).attr('d'));


          $(['stroke-linejoin','stroke','stroke-miterlimit','stroke-width','fill','stroke-linecap']).each(function() { 
            if($(node).attr(this.toString())) {
              path.attr(this, $(node).attr(this.toString()));
            } else {
              path.attr(this, 0);
            }
          });


          if($(node).attr('style')) {
            atts = $(node).attr('style').split(';');
            for(var i=0; i < atts.length; i++) { 
              bits = atts[i].split(':');
              path.attr(bits[0],bits[1]);
            }
          }

        });

      }, 'text');

      $(this).remove(); // removes the original image after the new one has been redrawn
      container.fadeIn(2000);
    });

  };

})(jQuery);

In essence, this allows me to just write a normal image tag with the .svg graphic, and the jQuery plugin will automatically replace it with the Raphaël-rendered version.

This works great in non-SVG compliant browsers, like IE, but in modern browsers that actually already support SVG graphics, the image tag works as-is (without Raphaël), so when Raphaël loads, it unloads the existing image, then fades in the Raphaël version... essentially creating a flicker. I tried to downplay this by fading in the new version, but I'm still faced with the problem that the old one shows, is hidden, then is shown again.

I need a way to reconcile the desired behavior in the problematic browsers like IE and the undesired behavior in the modern, standards-compliant browsers like Safari 4 and Firefox 3. But, I want to do this in a manner that I don't have to significantly change the way I code (why I use the plugin in the first place).

I know that SVG is still a bit cutting edge, but does anyone have any thoughts about how I can get around this?

DISCLAIMER: If at all possible, I'd like to stay away from browser targeting... I'm looking for a manageable and functional workflow solution, not a browser hack.

2ND DISCLAIMER: I don't want a solution based on Flash; I want to be as "native" as possible, and I regard javascript to be much more so than Flash. (This is why I'm so excited about Raphaël, because I can stay away from Flash).

A: 

Have you tried svgweb?

If there is no native support, a flash plugin will be triggered to render the SVG.

Davy Landman
Sorry, should have clarified that I don't want a Flash-based solution. Call me crazy. :)
neezer
A: 

Right, I came up with this... (using Ruby on Rails, btw)

  1. Export SVG graphics to public/images/svg/
  2. Run rake svg:parse:to_json (my rake task source below)
  3. Redraw the Raphaël graphic using the JSON path data.
  4. Call my SVG graphic using only <span class="svg">name_of_svg_file</span> (hiding the span by default using CSS and redrawing using modified jQuery plugin below).

This means that my workflow from Illustrator to HTML is very clean (I have my rake task fire during my Capistrano deploy). This satisfies my needs outlined in my question in that this renders as quickly and flicker-free in nearly every browser I've tested in so far (that Raphaël supports), does not require Flash, and involves writing only one line of code per SVG graphic (the <span> tag with the name of the graphic).

Refactorings welcome.

ToDo: At the moment, if the user has javascript turned off, there is no replacement. I think a simple <noscript> block might help me out there...


parse_svg.rake

require 'hpricot' # >= 0.8.2
require 'json/pure'

namespace :svg do
  namespace :parse do
    desc "Parse all SVG graphics in '/public/images/svg' into JSON libraries in '/public/javascripts/raphael/svg-js/' usable by Raphaël"
    task :to_json do
      FileList['public/images/svg/*.svg'].each do |svg|
        name = File.basename(svg).split('.')[0]
        doc = open(svg) { |f| Hpricot.XML(f) } # parse the SVG
        js = {} # container
        js[:name] = name
        js[:width] = doc.at('svg')['width'].to_i
        js[:height] = doc.at('svg')['height'].to_i
        js[:paths] = [] # all paths
        doc.search("/svg/g//path").each do |p|
          path = {} # our path hash
          path[:path] = p['d'].gsub(/(?:[\r\n\t])+/, ',')
          path[:stroke_width] = p['stroke-width'] || 0
          path[:stroke] = p['stroke'] || 0
          path[:fill] = p['fill'] || 0
          js[:paths] << path
        end
        File.open("public/javascripts/raphael/svg-js/#{name}.js", 'w') { |f| f.write(js.to_json) }
      end
      puts "Done!"
    end
  end
end

render_raphaels.jquery.js

(function($) {
  $.fn.render_raphaels = function(options) {
    var defaults, options, name, container, raphael;
    defaults = {
      span_id: 'svg-ref'
    };
    options = $.extend(defaults, options);
    // find all the spans that point to SVGs, based on default or passed-in identifier
    $('span.'+options.span_id).each(function() {
      name = $(this).text();
      $.getJSON('/javascripts/raphael/svg-js/' + name + '.js', function(data) {
        paper = Raphael(document.getElementById(data.name), data.width, data.height);
        for (var p in data.paths) {
          paper.path(data.paths[p].path).attr({
            fill: data.paths[p].fill,
            stroke: data.paths[p].stroke,
            'stroke-width': data.paths[p].stroke_width
          });
        }
      });
      // remove the span
      $(this).remove();
    });
  };
})(jQuery);

call on all pages with SVGs

$.fn.render_raphaels({ span_id: 'svg' });
neezer