     @buildPanel @script[2..3], (err, panel) -> finishPanel(err, 1, panel)
     @buildPanel @script[4..5], (err, panel) -> finishPanel(err, 2, panel)
 
-  # Load avatars for `names` and invoke `cb` as:
-  # `cb(err, {"Nickname": avatarImg, ...})`
-  # mapping each name to an avatar image that
-  # can be used to represent it.
-  #
-  # `err` is only set on failure
-  loadAvatars: (names, cb) =>
-    # Prepare requirements
-    names = names.map (n) -> {name: n, img: null, err: null}
-    failed = false
-
-    fail = (err) ->
-      cb(err, null)
-      return faliled = true
-
-    charPathForNick = (nick) ->
-      potential = path.resolve(AVATAR_BASE, "#{nick.toLowerCase()}.png")
-      return potential if (try fs.statSync(potential))
-      return path.resolve(AVATAR_BASE, "default.png")
-
-    for nameObj in names
-      do (nameObj) ->
-        GD.openPng charPathForNick(nameObj.name), (err, img) ->
-          return if failed
-          return fail(err) if err
-          nameObj.img = img unless err
-
-          # Are we done? Then build up a dictionary
-          # and tell the caller.
-          if names.every((o) -> o.img)
-            avatars = {}
-            for obj in names
-              avatars[obj.name] = obj.img
-            cb(null, avatars)
+  # Build a single panel out of (UP TO) two lines of dialog
+  # cb invoked as `cb(err, image)`. `err` is only set on failure
+  buildPanel: (lines, cb) =>
+    # Setup a transparant frame that we'll composite characters and text into.
+    frame = GD.createTrueColor(AYP_PANEL_WIDTH, AYP_PANEL_WIDTH)
+    frame.saveAlpha(1)
+    clear = frame.colorAllocateAlpha(0, 0, 0, 127)
+    frame.fill(0, 0, clear)
+
+    # We can just return the empty transperant
+    # frame if we get no lines
+    return cb(false, frame) unless lines.length > 0
+
+    names = (l[0] for l in lines).
+      filter((v, i, a) -> a.indexOf(v) == i) # De-dupe
+
+    @loadAvatars names, (err, avatars) =>
+      return cb(err, null) if err
+
+      if names.length == 1
+        # The only person speaking is centered in the frame
+        char = avatars[names[0]]
+        left = (frame.width / 2) - (char.width / 2)
+        top = (frame.height - char.height)
+        @compositeImage frame, char, left, top
+      else
+        # We have two speakers
+        first = true
+        for line in lines
+          [who, what] = line
+          char = avatars[who]
+          top = (frame.height - char.height)
+
+          if first
+            left = 0
+            first = false
+          else
+            left = frame.width - char.width
+
+          @compositeImage frame, char, left, top
+
+      # Add the text after all the avatars are painted on
+      @drawPanelText frame, lines
+
+      # Return the frame to the caller
+      return cb(false, frame)
 
   # Draw the text into a `frame` described by
   # the `@script`

    else
      msg

  # Produce a text bubble that contains `msg` printed in the
  # font `font` in the size `size`. The bubble will be at most
  # `max` pixels wide, and will be padded according to `AYP_TEXT_PADDING`
  #
  # The return value will be a GD image.
  textBubble: (msg, font=FONT_PATH, size=AYP_FONT_SIZE, max=AYP_BUBBLE_MAX_WIDTH) =>
    msg = @formatForTextBubble(msg, font, size, max)
    [w, h] = @textSize(msg, font, size)

    frame = GD.createTrueColor(
      w + (AYP_TEXT_PADDING * 2),
      h + (AYP_TEXT_PADDING * 2)
    )
    frame.saveAlpha(1)
    white = frame.colorAllocate(0xff, 0xff, 0xff)
    black = frame.colorAllocate(0x00, 0x00, 0x00)
    frame.fill(0, 0, white)

    frame.stringFT(black, font, size,
      0,                           # Rotation angle
      AYP_TEXT_PADDING,            # x
      AYP_TEXT_PADDING + size + 1, # y
      msg
    )
    return frame

  # Composite `sprite` onto `dst` in full.
  # Offsets `sprite` `+left` from the left
  # and `+top` from the top
  compositeImage: (dst, sprite, left, top) ->

              return msg.reply "Woooops! Failed to upload: #{err}" if err
              msg.reply "I made a thing: http://s3.amazonaws.com/#{AYP_AWS_BUCKET}/#{name}"

# This wraps up everything that builds the image strips of the comic
#
# Usage:
# `new AYPStrip script, (err, image) -> ...`
# Will invoke the callback with the completed
# strip when it's ready.
class AYPStrip
  # Constructor just stores the script and callback
  # and passes flow to the builder.
  constructor: (@script, @ready) ->
    do @buildComic

