 ###
 
 class Keyboard extends Sprite
+  ###
+  The constructor accepts an optional object with any
+  subset of the following properties:
+    keys: the number of keys (for example, 88 or 36 or 12)
+    width: the pixel width of the whole keyboard
+    height: the pixel height of the whole keyboard
+    lowest: the midi number of the lowest key
+    lineWidth: the pixel width of keyboard outline strokes
+    color: the color to use for the white keys
+    blackColor: the color to use for the black keys
+    lineColor: the color to use for outlines
+  If any subset of the options are chosen, then reasonable
+  defaults are chosen for everything else.
+  ###
   constructor: (options = {}) ->
-    # default key width
-    defwidth = if options.kh?
-        (options.kh / 5)
-      else
-        10
+    @lineWidth = options.lineWidth ? 1.6
+    @color = options.color ? 'white'
+    @blackColor = options.blackColor ? 'black'
+    @lineColor = options.lineColor ? 'black'
+    extra = Math.ceil(@lineWidth)
+    defwidth = 422
+    # If you just specify a number of keys, we will pick a default size.
+    if options.keys?
+      numwhite = Math.ceil(options.keys / 12 * 7)
+      aspect = numwhite / 5
+      defwidth = Math.sqrt(42000 * aspect) + extra
+    # Default aspect ratio is figurd from the keys requested, or 4.2
+    # if no keys were requested. If all is given is an overall sprite
+    # width or height, then the aspect ratio is used to pick the other.
+    # If both are given, and keys are not, then the number of keys is
+    # picked based on shape.  If all three are specified, then the
+    # keys are deformed to fit.
+    width = options.width ? if options.height?
+       Math.round((options.height - extra) * (aspect ? 4.2) + extra)
+    else
+       defwidth
+    height = options.height ? Math.round((width - extra) / (aspect ? 4.2) + extra)
+    if not options.keys?
+      numwhite = Math.max(1, Math.round((width - extra) / (height - extra) * 5))
+    # Get midi key numbers for the key range, defaulting to centered at 'F'.
+    firstwhite = Math.floor(42 - (numwhite - 1) / 2)
+    lastwhite = Math.ceil(firstwhite + numwhite - 1)
+    @lowest = options.lowest ? Keyboard.mcp(firstwhite)
+    if options.keys?
+      @highest = @lowest + options.keys - 1
+    else
+      @highest = Math.max(@lowest, Keyboard.mcp(Keyboard.wcp(@lowest) + numwhite - 1))
+    # OK, now after applying options, how many white keys?
+    firstwhite = Keyboard.wcp(@lowest)
+    lastwhite = Keyboard.wcp(@highest)
+    if (Keyboard.keyshape(@highest) >= 8)
+      lastwhite += 1
+    numwhite = lastwhite - firstwhite + 1
+
     # Calculate some geometry constants for the keyboard:
-    @kw = options.kw ? defwidth          # white key width
-    @kh = options.kh ? @kw * 5           # white key height
-    @bkw = options.bkw ? @kw * 3 / 5     # black key width
-    @bkh = options.bkh ? @kh * 3 / 5     # black key height
-    @lowest = options.lowest ? 21        # midi num for lowest key
-    @highest = options.highest ? 108     # midi num for highest key
-    @hlw = (options.lw ? 1.6) / 2        # half line width
-    @horiz = @calcHoriz()                # {left, right} boundaries
-    @ckw = (3 * @kw - 2 * @bkw) / 3      # top width of the c key
-    @fkw = (4 * @kw - 3 * @bkw) / 4      # top width of the f key
+    @kw = (width - extra) / numwhite    # white key width, height
+    @kh = (options.height ? @kw * 5 + extra) - extra
+    @bkw = @kw * 4 / 7                  # black key width
+    @bkh = @kh * 3 / 5                  # black key height
+    @halfex = extra / 2                 # offset to center lines
+    @horiz =
+      left: firstwhite * @kw            # left boundary
+      right: (lastwhite + 1) * @kw      # right boundary
+    @ckw = (3 * @kw - 2 * @bkw) / 3     # top width of the c key
+    @fkw = (4 * @kw - 3 * @bkw) / 4     # top width of the f key
     # Render the keyboard as its own sprite
     # that can be moved around.
     super
-      width: Math.ceil(@horiz.right - @horiz.left + @hlw * 2)
-      height: Math.ceil(@kh + @hlw * 2)
+      width: Math.ceil(@horiz.right - @horiz.left + extra)
+      height: Math.ceil(@kh + extra)
     do @drawall
 
   # Calculates the bounding left and right pixel positions

  @wcp: (n) ->
    floor((n + 7) / 12 * 7)

  # A code for the shape of the key number.
  # C,D,E,F,G,A,B are 1,2,3,4,5,6,7 and
  # C#,D#,F#,G#,A# are 8,9,10,11,12.
  @keyshape: (n) ->
    [1,8,2,9,3,4,10,5,11,6,12,7][n % 12]

  # A colorful color to use for the key number.
  @keycolor: (n) ->

      return
    if not fillcolor?
      if Keyboard.keyshape(n) >= 8
        fillcolor = @blackColor
      else
        fillcolor = @color
    ctx = @canvas().getContext '2d'
    ctx.save()
    ctx.beginPath()
    @keyoutline(ctx, n)
    ctx.fillStyle = fillcolor
    ctx.strokeStyle = @lineColor
    ctx.lineWidth = @lineWidth
    ctx.fill()
    ctx.stroke()
    ctx.restore()
