Skip to content

Fancybox with fancier product zooming

Product Zooming using FancyboxFancybox is a popular open-source javascript lightboxing library, used in many contexts, including product presentation. I recently had a client request image zooming functionality to be included in their site’s shopping cart. The pages already used Fancybox to show the products, but Fancybox’s default behavior is to open new images and resize them to fit the containing element, whose size is in turn determined by the size of the viewport. The client required a detailed view instead, in a way that would allow the user to navigate around the image. After looking around the internets I did not find a library or addon that satisfied that criteria, but I did manage to extend Fancybox easily, thanks to the decently designed API it offers. Here is how I did it:

Step 1: Overriding default styles

We will redefine a few styles, overriding the default resizing behavior. To avoid screwing up things elsewhere in the application, we will prepend a custom selector in our special zooming lightbox. Thankfully, Fancybox gives us an easy way to do that as you can see in the next section.

/* we want the close icon inside the container */
.productviewer .fancybox-close {top:2px; right:2px;}
/* the transition smooths image navigation, or our eyes would water */
.productviewer .fancybox-image {
  width: auto;
  height: auto;
  cursor:move;
  transition: top 0.2s, left 0.2s;
  -webkit-transition: top 0.2s, left 0.2s;
}
.productviewer .fancybox-inner {overflow: hidden;}

Don’t forget that these styles should be loaded after the default stylesheets, or else the overrides will fail.

Step 2: Customize the javascript call

We will use the afterShow attribute to hook our custom logic to each lightboxed image.

$(document).ready(function() {
  if ($('.my-image-class'.length != 0)) {
    $(".my-image-class").fancybox({
      prevEffect: 'none',
      nextEffect: 'none',
      padding: 0, // padding and curvy borders are, like, so 2009
      margin: 0,
      minWidth: 480, // You may want to tweak this depending on your application
      autoScale: false,
      wrapCSS: 'productviewer', // allows us to use custom styles
      // Initialize extended product view
      afterShow : function() {
        // make image nagivation smaller, takes too much space
        $(".fancybox-next").css('width',40);
        $(".fancybox-prev").css('width',40);
        
        // determine original image size
        var img = $("img.fancybox-image");
        // We determine the actual image size by initializing a new image element
        // and retrieving its dimensions
        var t = new Image();
        t.src = (img.getAttribute ? img.getAttribute("src") : false) || img.src;
        var fullW = img.width();
        var fullH = img.height();
        
        // determine visible container height
        var containerW = $('.fancybox-inner').width();
        var containerH = $('.fancybox-inner').height();
        
        // Position the element in the centre of the container
        img.css({
          position: 'absolute',
          top: -((img.height() / 2)) + (containerH / 2),
          left: -((img.width() / 2)) + (containerW / 2)
        });
        
        
        // Hook up mousemove to scroll the image accordingly
        $(".fancybox-image").mousemove(function(e){
          var mouseX = e.pageX - img.offset().left; 
          var mouseY = e.pageY - img.offset().top;
          // determine the new position based on cursor position
          var posX = (Math.round((mouseX/fullW)*100)/100) *  (fullW-containerW);
          var posY = (Math.round((mouseY/fullH)*100)/100) * (fullHeight-containerH);
          // perform a scroll if we are within bounds.
          if (posX + containerW <= fullW)
            img.css({'left': '-' + posX + 'px'})
          if (posY + containerH <= fullHeight)
            img.css({'top': '-' + posY + 'px'})
        });
      }
    });
  };
});

You can view a working example of this script on this page.

Categories: Code.

Tags: , , ,

Chamber Music by Ari Benjamin Meyers

Chamber Music Build Snapshot

Early build stages: A bunch of XBees are happily chattering away while I am freaking out over interrupt-driven code going sour.

Today was the first on-site test for the system I am building for Ari’s piece ‘Chamber Music‘, opening on April 27th at the Berlinische Gallerie Museum For Modern Art.

Thankfully everything went smoothly, and I was once again surprised by just how much more rewarding it feels to  build something tangible that interacts with the world in a real way rather than writing code that runs solely in an abstract ‘cloud’.

The piece’s infrastructure includes an Arduino-based media player, sensors and radio transmitters. If you are in Berlin on the opening day, come by and say hi.

The piece will be on display until 28/04/2014.

 

Categories: Uncategorized.

Tags: , ,

Writing an XMPP Component in Ruby

I am currently involved in a project that makes extensive use of the XMPP protocol. During the design stages we were considering which language to use to write a robust XMPP server component that would encapsulate our business logic and make it available through the XMPP protocol.

My first choice is of course always Ruby, and after a few Googles and SO searches, I realised that there aren’t many examples of how to write a ruby component, which is kinda surprising, and some of the information out there is actually plain wrong. So, here is what I found out during that process.

xmpp4r

At first I decided to give XMPP4R a spin. I did get a component up and running, although the API is not that tasty, and the component kept crashing without explanation. Nevertheless, here is a simple component using the library for posterity’s sake:

require 'xmpp4r'
include Jabber
class MySpecialMessage < Message
  # Extend default message to do more stuff
end
component = Component.new 'ian'
component.connect('ian.kilminster.me', 5311)
component.auth('lemmygetin')
component.on_exception { |e|
  $stderr.puts "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
}
component.add_message_callback { |msg| handle_message(msg) }
component.add_iq_callback { |iq| handle_iq(iq) }
component.add_presence_callback { |msg| handle_presence(msg) }

def handle_message(msg)
  message = MySpecialMessage.import(msg)
  if message.type == :error
    raise message.error.to_s
  else
    message.ask_for_a_quid if message.is_broke?
    message.play_fruit_machines if message.has_a_quid?
    true
  end
end

def handle_presence(msg)
  if msg.error
    raise XMPPError.new(msg.error.to_s)
  else
    # do something with the presence
    true
  end
end

def handle_iq(msg)
  if msg.type == :error
    raise XMPPError.new(msg.error.to_s)
  else
    # do something with the query
    true
  end
end

skates

As I wasn’t generally impressed with how xmpp4r worked (or had me working), and was also a bit worried about the last commit on xmpp4r being 4 years ago, I moved on.
I gave skates a shot, but this project is a good example of how you can kill a gem by not documenting it right. After spending an hour going through the code, I finally figured out how it works and decided it is not the way to go. This Rails-inspired MVC framework just didn’t make sense to me and the routing abstraction is counterintuitive and hard to use.

blather

Blather does indeed allow you to create XMPP components, but it is not documented anywhere. I actually found an SO answer saying that it doesn’t, and that is why I tried it last. Docs didn’t mention components anywhere, but, after looking around the code again, I figured it out:

#!/usr/bin/env ruby
# filename: lemmy.rb
# Run me by opening a terminal and hitting ./lemmy.rb
require 'rubygems'
require 'bundler/setup'
require 'blather/client/dsl'
require 'nutrun-string'
$stdout.sync = true

module Lemmy
  extend Blather::DSL
  def self.run; client.run; end

  setup 'ian.kilminster.me', 'lemmygetin', 'kilminster.me', 5311

  message :chat?, :body do |m|
    begin
      puts "#{m.from.to_s.yellow} #{m.body.white}"
      if m.body =~ /have a quid/
        say m.from, 'Cheers mai'
        say m.from, '* heads off to the fruit machine'
      else
        say m.from, 'I said, lemmy a dam quid'
      end
    rescue => e
      say m.from, e.inspect
    end
  end

  subscription(:request?) do |subs|
    write(subs.approve!)
    write(subs.request!)
  end
end

[:INT, :TERM].each do |sig|
  trap(sig) { 
    print 'Shutting down component...'.white
    Lemmy.shutdown
    puts 'done.'.green

    EM.stop 
  }
end

EM.run do
  Lemmy.run
end

Categories: Code.

Tags: , ,

Fighting ejabberd startup errors on OSX

Posting in case anyone else falls into this pit:

Say you just installed ejabberd through Homebrew.
You happily go on to issue an ejabberdctl start command, and you get no feedback.
That’s normal, ejabberdctl is actually very laconic by nature.

Still, it looks like nothing happened, as you can’t see any new processes spawned.
You then tail /usr/local/var/log/ejabberd/ejabberd.log and it tells you that

E(:ejabberd_config:554) : Error reading Mnesia database spool files:
The Mnesia database couldn't read the spool file for the table 'config'.
ejabberd needs read and write access in the directory:
   /usr/local/var/lib/ejabberd
Maybe the problem is a change in the computer hostname,
or a change in the Erlang node name, which is currently:
   ejabberd@localhost
Check the ejabberd guide for details about changing the
computer hostname or Erlang node name.

You try out a couple of Googles on that error, and on the 10th hit you finally bump into the solution:
rm -rf /usr/local/var/lib/ejabberd/*

This basically removes the existing Mnesia database, forcing the software to create a new one on the next startup.
The reason the whole thing happened seems to have something to do with machine names, and how the internal database is bound to them.

Categories: Tricks.

Tags: ,

Redis and Ruby on Rails environments

We are currently developing a Ruby on Rails application that makes heavy use of the Redis key/value store in its models. Traditionally, key/value storages are mostly used for caching in Rails, but there are so much more to them than that.

After you get started with redis, you might decide that you need different Redis databases depending on your environment, allowing you to freely flush your test database for example, just as you do with the Rails database.

There are a couple of ways you can do this but here is what we came up with:

# Location: config/initializers/redis.rb
conf_file = File.join('config','redis.yml')

$redis = if File.exists?(conf_file)
  conf = YAML.load(File.read(conf_file))
  conf[Rails.env.to_s].blank? ? Redis.new : Redis.new(conf[Rails.env.to_s])
else
  Redis.new
end
# Location: config/redis.yml
development:
  url: redis://127.0.0.1:6379/1
test:
  url: redis://127.0.0.1:6379/15
production:
  host: data.myapp.com
  port: 6333
  db: 0

This approach allows for a Redis configuration very similar to the native Rails database configuration. You can have environment specific databases or even Redis installs, and you can pass any configuration the Redis gem understands.

Categories: Code.

Tags: , ,

autometal-piwik gem gone all official

autometal-piwik is now the official Piwik ruby api client. The github repo has been taken under Piwik team’s wing, has been renamed to piwik-ruby-api and has been relocated here. The gem itself has not been renamed yet and I will still be the main maintainer and gem publisher, so direct and bug reports requests etc to me.

Categories: Uncategorized.

Tags: ,