diff --git a/src/blocks/scratch3_pen.js b/src/blocks/scratch3_pen.js
new file mode 100644
index 000000000..5d652531b
--- /dev/null
+++ b/src/blocks/scratch3_pen.js
@@ -0,0 +1,333 @@
+var Cast = require('../util/cast');
+var Clone = require('../util/clone');
+var Color = require('../util/color');
+var MathUtil = require('../util/math-util');
+var RenderedTarget = require('../sprites/rendered-target');
+
+/**
+ * @typedef {object} PenState - the pen state associated with a particular target.
+ * @property {Boolean} penDown - tracks whether the pen should draw for this target.
+ * @property {number} hue - the current hue of the pen.
+ * @property {number} shade - the current shade of the pen.
+ * @property {PenAttributes} penAttributes - cached pen attributes for the renderer. This is the authoritative value for
+ *   diameter but not for pen color.
+ */
+
+/**
+ * Host for the Pen-related blocks in Scratch 3.0
+ * @param {Runtime} runtime - the runtime instantiating this block package.
+ * @constructor
+ */
+var Scratch3PenBlocks = function (runtime) {
+    /**
+     * The runtime instantiating this block package.
+     * @type {Runtime}
+     */
+    this.runtime = runtime;
+
+    /**
+     * The ID of the renderer Drawable corresponding to the pen layer.
+     * @type {int}
+     * @private
+     */
+    this._penDrawableId = -1;
+
+    /**
+     * The ID of the renderer Skin corresponding to the pen layer.
+     * @type {int}
+     * @private
+     */
+    this._penSkinId = -1;
+
+    this._onTargetMoved = this._onTargetMoved.bind(this);
+};
+
+/**
+ * The default pen state, to be used when a target has no existing pen state.
+ * @type {PenState}
+ */
+Scratch3PenBlocks.DEFAULT_PEN_STATE = {
+    penDown: false,
+    hue: 120,
+    shade: 50,
+    penAttributes: {
+        color4f: [0, 0, 1, 1],
+        diameter: 1
+    }
+};
+
+/**
+ * Place the pen layer in front of the backdrop but behind everything else.
+ * We should probably handle this somewhere else... somewhere central that knows about pen, backdrop, video, etc.
+ * Maybe it should be in the GUI?
+ * @type {int}
+ */
+Scratch3PenBlocks.PEN_ORDER = 1;
+
+/**
+ * The minimum and maximum allowed pen size.
+ * @type {{min: number, max: number}}
+ */
+Scratch3PenBlocks.PEN_SIZE_RANGE = {min: 1, max: 255};
+
+/**
+ * The key to load & store a target's pen-related state.
+ * @type {string}
+ */
+Scratch3PenBlocks.STATE_KEY = 'Scratch.pen';
+
+/**
+ * Clamp a pen size value to the range allowed by the pen.
+ * @param {number} requestedSize - the requested pen size.
+ * @returns {number} the clamped size.
+ * @private
+ */
+Scratch3PenBlocks.prototype._clampPenSize = function (requestedSize) {
+    return MathUtil.clamp(requestedSize, Scratch3PenBlocks.PEN_SIZE_RANGE.min, Scratch3PenBlocks.PEN_SIZE_RANGE.max);
+};
+
+/**
+ * Retrieve the ID of the renderer "Skin" corresponding to the pen layer. If the pen Skin doesn't yet exist, create it.
+ * @returns {int} the Skin ID of the pen layer, or -1 on failure.
+ * @private
+ */
+Scratch3PenBlocks.prototype._getPenLayerID = function () {
+    if (this._penSkinId < 0 && this.runtime.renderer) {
+        this._penSkinId = this.runtime.renderer.createPenSkin();
+        this._penDrawableId = this.runtime.renderer.createDrawable();
+        this.runtime.renderer.setDrawableOrder(this._penDrawableId, Scratch3PenBlocks.PEN_ORDER);
+        this.runtime.renderer.updateDrawableProperties(this._penDrawableId, {skinId: this._penSkinId});
+    }
+    return this._penSkinId;
+};
+
+/**
+ * @param {Target} target - collect pen state for this target. Probably, but not necessarily, a RenderedTarget.
+ * @returns {PenState} the mutable pen state associated with that target. This will be created if necessary.
+ * @private
+ */
+Scratch3PenBlocks.prototype._getPenState = function (target) {
+    var penState = target.getCustomState(Scratch3PenBlocks.STATE_KEY);
+    if (!penState) {
+        penState = Clone.simple(Scratch3PenBlocks.DEFAULT_PEN_STATE);
+        target.setCustomState(Scratch3PenBlocks.STATE_KEY, penState);
+    }
+    return penState;
+};
+
+/**
+ * Handle a target which has moved. This only fires when the pen is down.
+ * @param {RenderedTarget} target - the target which has moved.
+ * @param {number} oldX - the previous X position.
+ * @param {number} oldY - the previous Y position.
+ * @private
+ */
+Scratch3PenBlocks.prototype._onTargetMoved = function (target, oldX, oldY) {
+    var penSkinId = this._getPenLayerID();
+    if (penSkinId >= 0) {
+        var penState = this._getPenState(target);
+        this.runtime.renderer.penLine(penSkinId, penState.penAttributes, oldX, oldY, target.x, target.y);
+        this.runtime.requestRedraw();
+    }
+};
+
+/**
+ * Update the cached RGB color from the hue & shade values in the provided PenState object.
+ * @param {PenState} penState - the pen state to update.
+ * @private
+ */
+Scratch3PenBlocks.prototype._updatePenColor = function (penState) {
+    var rgb = Color.hsvToRgb({h: penState.hue * 180 / 100, s: 1, v: 1});
+    var shade = (penState.shade > 100) ? 200 - penState.shade : penState.shade;
+    if (shade < 50) {
+        rgb = Color.mixRgb(Color.RGB_BLACK, rgb, (10 + shade) / 60);
+    } else {
+        rgb = Color.mixRgb(rgb, Color.RGB_WHITE, (shade - 50) / 60);
+    }
+    penState.penAttributes.color4f[0] = rgb.r / 255.0;
+    penState.penAttributes.color4f[1] = rgb.g / 255.0;
+    penState.penAttributes.color4f[2] = rgb.b / 255.0;
+};
+
+/**
+ * Wrap a pen hue or shade values to the range [0,200).
+ * @param {number} value - the pen hue or shade value to the proper range.
+ * @returns {number} the wrapped value.
+ * @private
+ */
+Scratch3PenBlocks.prototype._wrapHueOrShade = function (value) {
+    value = value % 200;
+    if (value < 0) value += 200;
+    return value;
+};
+
+/**
+ * Retrieve the block primitives implemented by this package.
+ * @return {Object.<string, Function>} Mapping of opcode to Function.
+ */
+Scratch3PenBlocks.prototype.getPrimitives = function () {
+    return {
+        pen_clear: this.clear,
+        pen_stamp: this.stamp,
+        pen_pendown: this.penDown,
+        pen_penup: this.penUp,
+        pen_setpencolortocolor: this.setPenColorToColor,
+        pen_changepencolorby: this.changePenHueBy,
+        pen_setpencolortonum: this.setPenHueToNumber,
+        pen_changepenshadeby: this.changePenShadeBy,
+        pen_setpenshadeto: this.setPenShadeToNumber,
+        pen_changepensizeby: this.changePenSizeBy,
+        pen_setpensizeto: this.setPenSizeTo
+    };
+};
+
+/**
+ * The pen "clear" block clears the pen layer's contents.
+ */
+Scratch3PenBlocks.prototype.clear = function () {
+    var penSkinId = this._getPenLayerID();
+    if (penSkinId >= 0) {
+        this.runtime.renderer.penClear(penSkinId);
+        this.runtime.requestRedraw();
+    }
+};
+
+/**
+ * The pen "stamp" block stamps the current drawable's image onto the pen layer.
+ * @param {object} args - the block arguments.
+ * @param {object} util - utility object provided by the runtime.
+ */
+Scratch3PenBlocks.prototype.stamp = function (args, util) {
+    var penSkinId = this._getPenLayerID();
+    if (penSkinId >= 0) {
+        var target = util.target;
+        this.runtime.renderer.penStamp(penSkinId, target.drawableID);
+        this.runtime.requestRedraw();
+    }
+};
+
+/**
+ * The pen "pen down" block causes the target to leave pen trails on future motion.
+ * @param {object} args - the block arguments.
+ * @param {object} util - utility object provided by the runtime.
+ */
+Scratch3PenBlocks.prototype.penDown = function (args, util) {
+    var target = util.target;
+    var penState = this._getPenState(target);
+
+    if (!penState.penDown) {
+        penState.penDown = true;
+        target.addListener(RenderedTarget.EVENT_TARGET_MOVED, this._onTargetMoved);
+    }
+
+    var penSkinId = this._getPenLayerID();
+    if (penSkinId >= 0) {
+        this.runtime.renderer.penPoint(penSkinId, penState.penAttributes, target.x, target.y);
+        this.runtime.requestRedraw();
+    }
+};
+
+/**
+ * The pen "pen up" block stops the target from leaving pen trails.
+ * @param {object} args - the block arguments.
+ * @param {object} util - utility object provided by the runtime.
+ */
+Scratch3PenBlocks.prototype.penUp = function (args, util) {
+    var target = util.target;
+    var penState = this._getPenState(target);
+
+    if (penState.penDown) {
+        penState.penDown = false;
+        target.removeListener(RenderedTarget.EVENT_TARGET_MOVED, this._onTargetMoved);
+    }
+};
+
+/**
+ * The pen "set pen color to {color}" block sets the pen to a particular RGB color.
+ * @param {object} args - the block arguments.
+ *  @property {int} COLOR - the color to set, expressed as a 24-bit RGB value (0xRRGGBB).
+ * @param {object} util - utility object provided by the runtime.
+ */
+Scratch3PenBlocks.prototype.setPenColorToColor = function (args, util) {
+    var penState = this._getPenState(util.target);
+    var rgb = Cast.toRgbColorObject(args.COLOR);
+    var hsv = Color.rgbToHsv(rgb);
+
+    penState.hue = 200 * hsv.h / 360;
+    penState.shade = 50 * hsv.v;
+    penState.penAttributes.color4f[0] = rgb.r / 255.0;
+    penState.penAttributes.color4f[1] = rgb.g / 255.0;
+    penState.penAttributes.color4f[2] = rgb.b / 255.0;
+};
+
+/**
+ * The pen "change pen color by {number}" block rotates the hue of the pen by the given amount.
+ * @param {object} args - the block arguments.
+ *  @property {number} COLOR - the amount of desired hue rotation.
+ * @param {object} util - utility object provided by the runtime.
+ */
+Scratch3PenBlocks.prototype.changePenHueBy = function (args, util) {
+    var penState = this._getPenState(util.target);
+    penState.hue = this._wrapHueOrShade(penState.hue + Cast.toNumber(args.COLOR));
+    this._updatePenColor(penState);
+};
+
+/**
+ * The pen "set pen color to {number}" block sets the hue of the pen.
+ * @param {object} args - the block arguments.
+ *  @property {number} COLOR - the desired hue.
+ * @param {object} util - utility object provided by the runtime.
+ */
+Scratch3PenBlocks.prototype.setPenHueToNumber = function (args, util) {
+    var penState = this._getPenState(util.target);
+    penState.hue = this._wrapHueOrShade(Cast.toNumber(args.COLOR));
+    this._updatePenColor(penState);
+};
+
+/**
+ * The pen "change pen shade by {number}" block changes the "shade" of the pen, related to the HSV value.
+ * @param {object} args - the block arguments.
+ *  @property {number} SHADE - the amount of desired shade change.
+ * @param {object} util - utility object provided by the runtime.
+ */
+Scratch3PenBlocks.prototype.changePenShadeBy = function (args, util) {
+    var penState = this._getPenState(util.target);
+    penState.shade = this._wrapHueOrShade(penState.shade + Cast.toNumber(args.SHADE));
+    this._updatePenColor(penState);
+};
+
+/**
+ * The pen "set pen shade to {number}" block sets the "shade" of the pen, related to the HSV value.
+ * @param {object} args - the block arguments.
+ *  @property {number} SHADE - the amount of desired shade change.
+ * @param {object} util - utility object provided by the runtime.
+ */
+Scratch3PenBlocks.prototype.setPenShadeToNumber = function (args, util) {
+    var penState = this._getPenState(util.target);
+    penState.shade = this._wrapHueOrShade(Cast.toNumber(args.SHADE));
+    this._updatePenColor(penState);
+};
+
+/**
+ * The pen "change pen size by {number}" block changes the pen size by the given amount.
+ * @param {object} args - the block arguments.
+ *  @property {number} SIZE - the amount of desired size change.
+ * @param {object} util - utility object provided by the runtime.
+ */
+Scratch3PenBlocks.prototype.changePenSizeBy = function (args, util) {
+    var penAttributes = this._getPenState(util.target).penAttributes;
+    penAttributes.diameter = this._clampPenSize(penAttributes.diameter + Cast.toNumber(args.SIZE));
+};
+
+/**
+ * The pen "set pen size to {number}" block sets the pen size to the given amount.
+ * @param {object} args - the block arguments.
+ *  @property {number} SIZE - the amount of desired size change.
+ * @param {object} util - utility object provided by the runtime.
+ */
+Scratch3PenBlocks.prototype.setPenSizeTo = function (args, util) {
+    var penAttributes = this._getPenState(util.target).penAttributes;
+    penAttributes.diameter = this._clampPenSize(Cast.toNumber(args.SIZE));
+};
+
+module.exports = Scratch3PenBlocks;
diff --git a/src/engine/runtime.js b/src/engine/runtime.js
index 80b86d926..3a1967baa 100644
--- a/src/engine/runtime.js
+++ b/src/engine/runtime.js
@@ -15,6 +15,7 @@ var defaultBlockPackages = {
     scratch3_looks: require('../blocks/scratch3_looks'),
     scratch3_motion: require('../blocks/scratch3_motion'),
     scratch3_operators: require('../blocks/scratch3_operators'),
+    scratch3_pen: require('../blocks/scratch3_pen'),
     scratch3_sound: require('../blocks/scratch3_sound'),
     scratch3_sensing: require('../blocks/scratch3_sensing'),
     scratch3_data: require('../blocks/scratch3_data'),
diff --git a/src/engine/target.js b/src/engine/target.js
index e36790b9b..947df53d3 100644
--- a/src/engine/target.js
+++ b/src/engine/target.js
@@ -1,3 +1,6 @@
+var EventEmitter = require('events');
+var util = require('util');
+
 var Blocks = require('./blocks');
 var Variable = require('../engine/variable');
 var List = require('../engine/list');
@@ -14,6 +17,8 @@ var uid = require('../util/uid');
  * @constructor
  */
 var Target = function (blocks) {
+    EventEmitter.call(this);
+
     if (!blocks) {
         blocks = new Blocks(this);
     }
@@ -39,8 +44,20 @@ var Target = function (blocks) {
      * @type {Object.<string,*>}
      */
     this.lists = {};
+    /**
+     * Dictionary of custom state for this target.
+     * This can be used to store target-specific custom state for blocks which need it.
+     * TODO: do we want to persist this in SB3 files?
+     * @type {Object.<string,*>}
+     */
+    this._customState = {};
 };
 
+/**
+ * Inherit from EventEmitter
+ */
+util.inherits(Target, EventEmitter);
+
 /**
  * Called when the project receives a "green flag."
  * @abstract
@@ -112,10 +129,30 @@ Target.prototype.lookupOrCreateList = function (name) {
  */
 Target.prototype.postSpriteInfo = function () {};
 
+/**
+ * Retrieve custom state associated with this target and the provided state ID.
+ * @param {string} stateId - specify which piece of state to retrieve.
+ * @returns {*} the associated state, if any was found.
+ */
+Target.prototype.getCustomState = function (stateId) {
+    return this._customState[stateId];
+};
+
+/**
+ * Store custom state associated with this target and the provided state ID.
+ * @param {string} stateId - specify which piece of state to store on this target.
+ * @param {*} newValue - the state value to store.
+ */
+Target.prototype.setCustomState = function (stateId, newValue) {
+    this._customState[stateId] = newValue;
+};
+
 /**
  * Call to destroy a target.
  * @abstract
  */
-Target.prototype.dispose = function () {};
+Target.prototype.dispose = function () {
+    this._customState = {};
+};
 
 module.exports = Target;
diff --git a/src/import/sb2specmap.js b/src/import/sb2specmap.js
index 204f3c543..95aad5019 100644
--- a/src/import/sb2specmap.js
+++ b/src/import/sb2specmap.js
@@ -563,7 +563,7 @@ var specMap = {
         ]
     },
     'setPenShadeTo:': {
-        opcode: 'pen_changepenshadeby',
+        opcode: 'pen_setpenshadeto',
         argMap: [
             {
                 type: 'input',
diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js
index dc7c25d2f..36affd7b1 100644
--- a/src/sprites/rendered-target.js
+++ b/src/sprites/rendered-target.js
@@ -125,6 +125,12 @@ RenderedTarget.prototype.size = 100;
  */
 RenderedTarget.prototype.currentCostume = 0;
 
+/**
+ * Event which fires when a target moves.
+ * @type {string}
+ */
+RenderedTarget.EVENT_TARGET_MOVED = 'TARGET_MOVED';
+
 /**
  * Rotation style for "all around"/spinning.
  * @enum
@@ -160,6 +166,8 @@ RenderedTarget.prototype.setXY = function (x, y) {
     if (this.isStage) {
         return;
     }
+    var oldX = this.x;
+    var oldY = this.y;
     this.x = x;
     this.y = y;
     if (this.renderer) {
@@ -170,6 +178,7 @@ RenderedTarget.prototype.setXY = function (x, y) {
             this.runtime.requestRedraw();
         }
     }
+    this.emit(RenderedTarget.EVENT_TARGET_MOVED, this, oldX, oldY);
     this.runtime.spriteInfoReport(this);
 };
 
diff --git a/src/util/cast.js b/src/util/cast.js
index f14e97df5..0723b5ecd 100644
--- a/src/util/cast.js
+++ b/src/util/cast.js
@@ -66,18 +66,28 @@ Cast.toString = function (value) {
 };
 
 /**
- * Cast any Scratch argument to an RGB color object to be used for the renderer.
- * @param {*} value Value to convert to RGB color object.
+ * Cast any Scratch argument to an RGB color array to be used for the renderer.
+ * @param {*} value Value to convert to RGB color array.
  * @return {Array.<number>} [r,g,b], values between 0-255.
  */
 Cast.toRgbColorList = function (value) {
+    var color = Cast.toRgbColorObject(value);
+    return [color.r, color.g, color.b];
+};
+
+/**
+ * Cast any Scratch argument to an RGB color object to be used for the renderer.
+ * @param {*} value Value to convert to RGB color object.
+ * @return {RGBOject} [r,g,b], values between 0-255.
+ */
+Cast.toRgbColorObject = function (value) {
     var color;
     if (typeof value === 'string' && value.substring(0, 1) === '#') {
         color = Color.hexToRgb(value);
     } else {
         color = Color.decimalToRgb(Cast.toNumber(value));
     }
-    return [color.r, color.g, color.b];
+    return color;
 };
 
 /**
diff --git a/src/util/clone.js b/src/util/clone.js
new file mode 100644
index 000000000..5e1011a29
--- /dev/null
+++ b/src/util/clone.js
@@ -0,0 +1,17 @@
+/**
+ * Methods for cloning JavaScript objects.
+ * @type {object}
+ */
+var Clone = {};
+
+/**
+ * Deep-clone a "simple" object: one which can be fully expressed with JSON.
+ * Non-JSON values, such as functions, will be stripped from the clone.
+ * @param {object} original - the object to be cloned.
+ * @returns {object} a deep clone of the original object.
+ */
+Clone.simple = function (original) {
+    return JSON.parse(JSON.stringify(original));
+};
+
+module.exports = Clone;
diff --git a/src/util/color.js b/src/util/color.js
index 3e5054652..951cb270a 100644
--- a/src/util/color.js
+++ b/src/util/color.js
@@ -1,5 +1,25 @@
 var Color = function () {};
 
+/**
+ * @typedef {object} RGBObject - An object representing a color in RGB format.
+ * @property {number} r - the red component, in the range [0, 255].
+ * @property {number} g - the green component, in the range [0, 255].
+ * @property {number} b - the blue component, in the range [0, 255].
+ */
+
+/**
+ * @typedef {object} HSVObject - An object representing a color in HSV format.
+ * @property {number} h - hue, in the range [0-359).
+ * @property {number} s - saturation, in the range [0,1].
+ * @property {number} v - value, in the range [0,1].
+ */
+
+/** @type {RGBObject} */
+Color.RGB_BLACK = {r: 0, g: 0, b: 0};
+
+/** @type {RGBObject} */
+Color.RGB_WHITE = {r: 255, g: 255, b: 255};
+
 /**
  * Convert a Scratch decimal color to a hex string, #RRGGBB.
  * @param {number} decimal RGB color as a decimal.
@@ -17,7 +37,7 @@ Color.decimalToHex = function (decimal) {
 /**
  * Convert a Scratch decimal color to an RGB color object.
  * @param {number} decimal RGB color as decimal.
- * @returns {Object} {r: R, g: G, b: B}, values between 0-255
+ * @return {RGBObject} rgb - {r: red [0,255], g: green [0,255], b: blue [0,255]}.
  */
 Color.decimalToRgb = function (decimal) {
     var r = (decimal >> 16) & 0xFF;
@@ -31,7 +51,7 @@ Color.decimalToRgb = function (decimal) {
  * CC-BY-SA Tim Down:
  * https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
  * @param {!string} hex Hex representation of the color.
- * @return {Object} {r: R, g: G, b: B}, 0-255, or null.
+ * @return {RGBObject} null on failure, or rgb: {r: red [0,255], g: green [0,255], b: blue [0,255]}.
  */
 Color.hexToRgb = function (hex) {
     var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
@@ -48,7 +68,7 @@ Color.hexToRgb = function (hex) {
 
 /**
  * Convert an RGB color object to a hex color.
- * @param {Object} rgb {r: R, g: G, b: B}, values between 0-255.
+ * @param {RGBObject} rgb - {r: red [0,255], g: green [0,255], b: blue [0,255]}.
  * @return {!string} Hex representation of the color.
  */
 Color.rgbToHex = function (rgb) {
@@ -57,7 +77,7 @@ Color.rgbToHex = function (rgb) {
 
 /**
  * Convert an RGB color object to a Scratch decimal color.
- * @param {Object} rgb {r: R, g: G, b: B}, values between 0-255.
+ * @param {RGBObject} rgb - {r: red [0,255], g: green [0,255], b: blue [0,255]}.
  * @return {!number} Number representing the color.
  */
 Color.rgbToDecimal = function (rgb) {
@@ -73,4 +93,109 @@ Color.hexToDecimal = function (hex) {
     return Color.rgbToDecimal(Color.hexToRgb(hex));
 };
 
+/**
+ * Convert an HSV color to RGB format.
+ * @param {HSVObject} hsv - {h: hue [0,360), s: saturation [0,1], v: value [0,1]}
+ * @return {RGBObject} rgb - {r: red [0,255], g: green [0,255], b: blue [0,255]}.
+ */
+Color.hsvToRgb = function (hsv) {
+    var h = hsv.h % 360;
+    if (h < 0) h += 360;
+    var s = Math.max(0, Math.min(hsv.s, 1));
+    var v = Math.max(0, Math.min(hsv.v, 1));
+
+    var i = Math.floor(h / 60);
+    var f = (h / 60) - i;
+    var p = v * (1 - s);
+    var q = v * (1 - (s * f));
+    var t = v * (1 - (s * (1 - f)));
+
+    var r;
+    var g;
+    var b;
+
+    switch (i) {
+    default:
+    case 0:
+        r = v;
+        g = t;
+        b = p;
+        break;
+    case 1:
+        r = q;
+        g = v;
+        b = p;
+        break;
+    case 2:
+        r = p;
+        g = v;
+        b = t;
+        break;
+    case 3:
+        r = p;
+        g = q;
+        b = v;
+        break;
+    case 4:
+        r = t;
+        g = p;
+        b = v;
+        break;
+    case 5:
+        r = v;
+        g = p;
+        b = q;
+        break;
+    }
+
+    return {
+        r: Math.floor(r * 255),
+        g: Math.floor(g * 255),
+        b: Math.floor(b * 255)
+    };
+};
+
+/**
+ * Convert an RGB color to HSV format.
+ * @param {RGBObject} rgb - {r: red [0,255], g: green [0,255], b: blue [0,255]}.
+ * @return {HSVObject} hsv - {h: hue [0,360), s: saturation [0,1], v: value [0,1]}
+ */
+Color.rgbToHsv = function (rgb) {
+    var r = rgb.r / 255;
+    var g = rgb.g / 255;
+    var b = rgb.b / 255;
+    var x = Math.min(Math.min(r, g), b);
+    var v = Math.max(Math.max(r, g), b);
+
+    // For grays, hue will be arbitrarily reported as zero. Otherwise, calculate
+    var h = 0;
+    var s = 0;
+    if (x !== v) {
+        var f = (r === x) ? g - b : ((g === x) ? b - r : r - g);
+        var i = (r === x) ? 3 : ((g === x) ? 5 : 1);
+        h = ((i - (f / (v - x))) * 60) % 360;
+        s = (v - x) / v;
+    }
+
+    return {h: h, s: s, v: v};
+};
+
+/**
+ * Linear interpolation between rgb0 and rgb1.
+ * @param {RGBObject} rgb0 - the color corresponding to fraction1 <= 0.
+ * @param {RGBObject} rgb1 - the color corresponding to fraction1 >= 1.
+ * @param {number} fraction1 - the interpolation parameter. If this is 0.5, for example, mix the two colors equally.
+ * @return {RGBObject} the interpolated color.
+ */
+Color.mixRgb = function (rgb0, rgb1, fraction1) {
+    if (fraction1 <= 0) return rgb0;
+    if (fraction1 >= 1) return rgb1;
+    var fraction0 = 1 - fraction1;
+    return {
+        r: (fraction0 * rgb0.r) + (fraction1 * rgb1.r),
+        g: (fraction0 * rgb0.g) + (fraction1 * rgb1.g),
+        b: (fraction0 * rgb0.b) + (fraction1 * rgb1.b)
+    };
+};
+
 module.exports = Color;
diff --git a/test/fixtures/pen.sb2 b/test/fixtures/pen.sb2
new file mode 100644
index 000000000..5d68aa0c5
Binary files /dev/null and b/test/fixtures/pen.sb2 differ
diff --git a/test/integration/pen.js b/test/integration/pen.js
new file mode 100644
index 000000000..8256e13cb
--- /dev/null
+++ b/test/integration/pen.js
@@ -0,0 +1,34 @@
+var path = require('path');
+var test = require('tap').test;
+var extract = require('../fixtures/extract');
+var VirtualMachine = require('../../src/index');
+
+var uri = path.resolve(__dirname, '../fixtures/pen.sb2');
+var project = extract(uri);
+
+test('pen', function (t) {
+    var vm = new VirtualMachine();
+
+    // Evaluate playground data and exit
+    vm.on('playgroundData', function () {
+        // @todo Additional tests
+        t.end();
+        process.nextTick(process.exit);
+    });
+
+    // Start VM, load project, and run
+    t.doesNotThrow(function () {
+        vm.start();
+        vm.clear();
+        vm.setCompatibilityMode(false);
+        vm.setTurboMode(false);
+        vm.loadProject(project);
+        vm.greenFlag();
+    });
+
+    // After two seconds, get playground data and stop
+    setTimeout(function () {
+        vm.getPlaygroundData();
+        vm.stopAll();
+    }, 2000);
+});
diff --git a/test/unit/util_cast.js b/test/unit/util_cast.js
index 147ba2575..4f15d16bb 100644
--- a/test/unit/util_cast.js
+++ b/test/unit/util_cast.js
@@ -91,6 +91,24 @@ test('toRbgColorList', function (t) {
     t.end();
 });
 
+test('toRbgColorObject', function (t) {
+    // Hex (minimal, see "color" util tests)
+    t.deepEqual(cast.toRgbColorObject('#000'), {r: 0, g: 0, b: 0});
+    t.deepEqual(cast.toRgbColorObject('#000000'), {r: 0, g: 0, b: 0});
+    t.deepEqual(cast.toRgbColorObject('#fff'), {r: 255, g: 255, b: 255});
+    t.deepEqual(cast.toRgbColorObject('#ffffff'), {r: 255, g: 255, b: 255});
+
+    // Decimal (minimal, see "color" util tests)
+    t.deepEqual(cast.toRgbColorObject(0), {r: 0, g: 0, b: 0});
+    t.deepEqual(cast.toRgbColorObject(1), {r: 0, g: 0, b: 1});
+    t.deepEqual(cast.toRgbColorObject(16777215), {r: 255, g: 255, b: 255});
+
+    // Malformed
+    t.deepEqual(cast.toRgbColorObject('ffffff'), {r: 0, g: 0, b: 0});
+    t.deepEqual(cast.toRgbColorObject('foobar'), {r: 0, g: 0, b: 0});
+    t.end();
+});
+
 test('compare', function (t) {
     // Numeric
     t.strictEqual(cast.compare(0, 0), 0);
diff --git a/test/unit/util_color.js b/test/unit/util_color.js
index c0db8ee90..1ffb737eb 100644
--- a/test/unit/util_color.js
+++ b/test/unit/util_color.js
@@ -1,6 +1,42 @@
 var test = require('tap').test;
 var color = require('../../src/util/color');
 
+/**
+ * Assert that two HSV colors are similar to each other, within a tolerance.
+ * @param {Test} t - the Tap test object.
+ * @param {HSVObject} actual - the first HSV color to compare.
+ * @param {HSVObject} expected - the other HSV color to compare.
+ */
+var hsvSimilar = function (t, actual, expected) {
+    if ((Math.abs(actual.h - expected.h) >= 1) ||
+        (Math.abs(actual.s - expected.s) >= 0.01) ||
+        (Math.abs(actual.v - expected.v) >= 0.01)
+    ) {
+        t.fail('HSV colors not similar enough', {
+            actual: actual,
+            expected: expected
+        });
+    }
+};
+
+/**
+ * Assert that two RGB colors are similar to each other, within a tolerance.
+ * @param {Test} t - the Tap test object.
+ * @param {RGBObject} actual - the first RGB color to compare.
+ * @param {RGBObject} expected - the other RGB color to compare.
+ */
+var rgbSimilar = function (t, actual, expected) {
+    if ((Math.abs(actual.r - expected.r) >= 1) ||
+        (Math.abs(actual.g - expected.g) >= 1) ||
+        (Math.abs(actual.b - expected.b) >= 1)
+    ) {
+        t.fail('RGB colors not similar enough', {
+            actual: actual,
+            expected: expected
+        });
+    }
+};
+
 test('decimalToHex', function (t) {
     t.strictEqual(color.decimalToHex(0), '#000000');
     t.strictEqual(color.decimalToHex(1), '#000001');
@@ -60,3 +96,37 @@ test('hexToDecimal', function (t) {
     t.strictEqual(color.hexToDecimal('#00ffaa'), 65450);
     t.end();
 });
+
+test('hsvToRgb', function (t) {
+    rgbSimilar(t, color.hsvToRgb({h: 0, s: 0, v: 0}), {r: 0, g: 0, b: 0});
+    rgbSimilar(t, color.hsvToRgb({h: 123, s: 0.1234, v: 0}), {r: 0, g: 0, b: 0});
+    rgbSimilar(t, color.hsvToRgb({h: 0, s: 0, v: 1}), {r: 255, g: 255, b: 255});
+    rgbSimilar(t, color.hsvToRgb({h: 321, s: 0, v: 1}), {r: 255, g: 255, b: 255});
+    rgbSimilar(t, color.hsvToRgb({h: 0, s: 1, v: 1}), {r: 255, g: 0, b: 0});
+    rgbSimilar(t, color.hsvToRgb({h: 120, s: 1, v: 1}), {r: 0, g: 255, b: 0});
+    rgbSimilar(t, color.hsvToRgb({h: 240, s: 1, v: 1}), {r: 0, g: 0, b: 255});
+    t.end();
+});
+
+test('rgbToHsv', function (t) {
+    hsvSimilar(t, color.rgbToHsv({r: 0, g: 0, b: 0}), {h: 0, s: 0, v: 0});
+    hsvSimilar(t, color.rgbToHsv({r: 64, g: 64, b: 64}), {h: 0, s: 0, v: 0.25});
+    hsvSimilar(t, color.rgbToHsv({r: 128, g: 128, b: 128}), {h: 0, s: 0, v: 0.5});
+    hsvSimilar(t, color.rgbToHsv({r: 192, g: 192, b: 192}), {h: 0, s: 0, v: 0.75});
+    hsvSimilar(t, color.rgbToHsv({r: 255, g: 255, b: 255}), {h: 0, s: 0, v: 1});
+    hsvSimilar(t, color.rgbToHsv({r: 255, g: 0, b: 0}), {h: 0, s: 1, v: 1});
+    hsvSimilar(t, color.rgbToHsv({r: 0, g: 255, b: 0}), {h: 120, s: 1, v: 1});
+    hsvSimilar(t, color.rgbToHsv({r: 0, g: 0, b: 255}), {h: 240, s: 1, v: 1});
+    t.end();
+});
+
+test('mixRgb', function (t) {
+    rgbSimilar(t, color.mixRgb({r: 10, g: 20, b: 30}, {r: 30, g: 40, b: 50}, -1), {r: 10, g: 20, b: 30});
+    rgbSimilar(t, color.mixRgb({r: 10, g: 20, b: 30}, {r: 30, g: 40, b: 50}, 0), {r: 10, g: 20, b: 30});
+    rgbSimilar(t, color.mixRgb({r: 10, g: 20, b: 30}, {r: 30, g: 40, b: 50}, 0.25), {r: 15, g: 25, b: 35});
+    rgbSimilar(t, color.mixRgb({r: 10, g: 20, b: 30}, {r: 30, g: 40, b: 50}, 0.5), {r: 20, g: 30, b: 40});
+    rgbSimilar(t, color.mixRgb({r: 10, g: 20, b: 30}, {r: 30, g: 40, b: 50}, 0.75), {r: 25, g: 35, b: 45});
+    rgbSimilar(t, color.mixRgb({r: 10, g: 20, b: 30}, {r: 30, g: 40, b: 50}, 1), {r: 30, g: 40, b: 50});
+    rgbSimilar(t, color.mixRgb({r: 10, g: 20, b: 30}, {r: 30, g: 40, b: 50}, 2), {r: 30, g: 40, b: 50});
+    t.end();
+});