From 369c02b5d54e643eceedcda0885e3e72b1a4fcf3 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <cwillisf@media.mit.edu> Date: Thu, 19 Jan 2017 11:26:38 -0800 Subject: [PATCH 1/6] Implement pen blocks These blocks implement pen features as found in Scratch 2.0 Supporting changes include: - `Target` is now an event emitter - `RenderedTarget` now emits an event when it moves - `Target` can now store arbitrary "extra" data, called "custom state" in the code, using a `Target`'s `setCustomState` and `getCustomState` methods. This is used to store per-target pen state without requiring `Target` or `RenderedTarget` to know anything about the pen. - `Cast` can now cast to an RGB color object. - `Color` now has functions to convert between RGB and HSV, constants for for black & white, and a `mixRgb` function to lerp between two colors. --- src/blocks/scratch3_pen.js | 231 +++++++++++++++++++++++++++++++++ src/engine/runtime.js | 1 + src/engine/target.js | 29 ++++- src/import/sb2specmap.js | 2 +- src/sprites/rendered-target.js | 8 ++ src/util/cast.js | 16 ++- src/util/color.js | 101 +++++++++++++- 7 files changed, 382 insertions(+), 6 deletions(-) create mode 100644 src/blocks/scratch3_pen.js diff --git a/src/blocks/scratch3_pen.js b/src/blocks/scratch3_pen.js new file mode 100644 index 000000000..ff74a48d0 --- /dev/null +++ b/src/blocks/scratch3_pen.js @@ -0,0 +1,231 @@ +var Cast = require('../util/cast'); +var Color = require('../util/color'); +var MathUtil = require('../util/math-util'); +var RenderedTarget = require('../sprites/rendered-target'); + +// 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? +var penOrder = 1; + +var stateKey = 'Scratch.pen'; + +/** + * @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. + */ + +/** + * The default pen state, to be used when a target has no existing pen state. + * @type {PenState} + */ +var defaultPenState = { + penDown: false, + hue: 120, + shade: 50, + penAttributes: { + color4f: [0, 0, 1, 1], + diameter: 1 + } +}; + +var Scratch3PenBlocks = function (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + + this._penSkinId = -1; + + this._onTargetMoved = this._onTargetMoved.bind(this); +}; + +/** + * 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, 1, 255); +}; + +Scratch3PenBlocks.prototype._getPenLayerID = function () { + if (this._penSkinId < 0) { + this._penSkinId = this.runtime.renderer.createPenSkin(); + this._penDrawableId = this.runtime.renderer.createDrawable(); + this.runtime.renderer.setDrawableOrder(this._penDrawableId, penOrder); + 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(stateKey); + if (!penState) { + penState = JSON.parse(JSON.stringify(defaultPenState)); + target.setCustomState(stateKey, penState); + } + return penState; +}; + +/** + * Handle a target which has moved. This only fires when the pen is down. + * @param {RenderedTarget} target + * @param {number} oldX + * @param {number} oldY + * @private + */ +Scratch3PenBlocks.prototype._onTargetMoved = function (target, oldX, oldY) { + var penState = this._getPenState(target); + var penSkinId = this._getPenLayerID(); + 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 + }; +}; + +Scratch3PenBlocks.prototype.clear = function () { + var penSkinId = this._getPenLayerID(); + this.runtime.renderer.penClear(penSkinId); + this.runtime.requestRedraw(); +}; + +Scratch3PenBlocks.prototype.stamp = function (args, util) { + var penSkinId = this._getPenLayerID(); + var target = util.target; + this.runtime.renderer.penStamp(penSkinId, target.drawableID); + this.runtime.requestRedraw(); +}; + +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(); + this.runtime.renderer.penPoint(penSkinId, penState.penAttributes, target.x, target.y); + this.runtime.requestRedraw(); +}; + +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); + } +}; + +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; + + return rgb; +}; + +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); +}; + +Scratch3PenBlocks.prototype.setPenHueToNumber = function (args, util) { + var penState = this._getPenState(util.target); + penState.hue = this._wrapHueOrShade(Cast.toNumber(args.COLOR)); + this._updatePenColor(penState); +}; + +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); +}; + +Scratch3PenBlocks.prototype.setPenShadeToNumber = function (args, util) { + var penState = this._getPenState(util.target); + penState.shade = this._wrapHueOrShade(Cast.toNumber(args.SHADE)); + this._updatePenColor(penState); +}; + +Scratch3PenBlocks.prototype.changePenSizeBy = function (args, util) { + var penAttributes = this._getPenState(util.target).penAttributes; + penAttributes.diameter = this._clampPenSize(penAttributes.diameter + Cast.toNumber(args.SIZE)); +}; + +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..d5e379413 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,20 @@ Target.prototype.lookupOrCreateList = function (name) { */ Target.prototype.postSpriteInfo = function () {}; +Target.prototype.getCustomState = function (stateId) { + return this._customState[stateId]; +}; + +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..40e4ed4b0 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,7 @@ RenderedTarget.prototype.setXY = function (x, y) { if (this.isStage) { return; } + var oldX = this.x, oldY = this.y; this.x = x; this.y = y; if (this.renderer) { @@ -170,6 +177,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/color.js b/src/util/color.js index 3e5054652..2a3b1d338 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 + * @returns {RGBObject} {r: R, g: G, b: B}, values between 0-255 */ Color.decimalToRgb = function (decimal) { var r = (decimal >> 16) & 0xFF; @@ -73,4 +93,83 @@ 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]} + * @returns {RGBObject} rgb - {r: red [0,255], g: green [0,255], b: blue [0,255]}. + */ +Color.hsvToRgb = function (hsv) { + var h = hsv.h; + var s = hsv.s; + var v = hsv.v; + + h = h % 360; + if (h < 0) h += 360; + s = Math.max(0, Math.min(s, 1)); + v = Math.max(0, Math.min(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, g, b; + if (i == 0) { r = v; g = t; b = p; } + else if (i == 1) { r = q; g = v; b = p; } + else if (i == 2) { r = p; g = v; b = t; } + else if (i == 3) { r = p; g = q; b = v; } + else if (i == 4) { r = t; g = p; b = v; } + else if (i == 5) { r = v; g = p; b = q; } + + 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]}. + * @returns {HSVObject} hsv - {h: hue [0,360), s: saturation [0,1], v: value [0,1]} + */ +Color.rgbToHsv = function (rgb) { + var v, x, f, i; + var h = 0, s = 0; + var r = rgb.r; + var g = rgb.g; + var b = rgb.b; + x = Math.min(Math.min(r, g), b); + v = Math.max(Math.max(r, g), b); + + // For grays, hue will be arbitrarily reported as zero. Otherwise, calculate + if (x != v) { + f = (r == x) ? g - b : ((g == x) ? b - r : r - g); + 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. + * @returns {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; From a6190da774b1e1181e9fcc1e6b5ce26750ca955a Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <cwillisf@media.mit.edu> Date: Thu, 19 Jan 2017 12:50:46 -0800 Subject: [PATCH 2/6] Lint fixes and related cleanup --- src/blocks/scratch3_pen.js | 6 +-- src/sprites/rendered-target.js | 3 +- src/util/color.js | 74 +++++++++++++++++++++++----------- 3 files changed, 55 insertions(+), 28 deletions(-) diff --git a/src/blocks/scratch3_pen.js b/src/blocks/scratch3_pen.js index ff74a48d0..7f1f6a285 100644 --- a/src/blocks/scratch3_pen.js +++ b/src/blocks/scratch3_pen.js @@ -81,9 +81,9 @@ Scratch3PenBlocks.prototype._getPenState = function (target) { /** * Handle a target which has moved. This only fires when the pen is down. - * @param {RenderedTarget} target - * @param {number} oldX - * @param {number} oldY + * @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) { diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index 40e4ed4b0..36affd7b1 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -166,7 +166,8 @@ RenderedTarget.prototype.setXY = function (x, y) { if (this.isStage) { return; } - var oldX = this.x, oldY = this.y; + var oldX = this.x; + var oldY = this.y; this.x = x; this.y = y; if (this.renderer) { diff --git a/src/util/color.js b/src/util/color.js index 2a3b1d338..057b68196 100644 --- a/src/util/color.js +++ b/src/util/color.js @@ -99,14 +99,10 @@ Color.hexToDecimal = function (hex) { * @returns {RGBObject} rgb - {r: red [0,255], g: green [0,255], b: blue [0,255]}. */ Color.hsvToRgb = function (hsv) { - var h = hsv.h; - var s = hsv.s; - var v = hsv.v; - - h = h % 360; + var h = hsv.h % 360; if (h < 0) h += 360; - s = Math.max(0, Math.min(s, 1)); - v = Math.max(0, Math.min(v, 1)); + 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; @@ -114,13 +110,43 @@ Color.hsvToRgb = function (hsv) { var q = v * (1 - (s * f)); var t = v * (1 - (s * (1 - f))); - var r, g, b; - if (i == 0) { r = v; g = t; b = p; } - else if (i == 1) { r = q; g = v; b = p; } - else if (i == 2) { r = p; g = v; b = t; } - else if (i == 3) { r = p; g = q; b = v; } - else if (i == 4) { r = t; g = p; b = v; } - else if (i == 5) { r = v; g = p; b = q; } + 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), @@ -135,18 +161,18 @@ Color.hsvToRgb = function (hsv) { * @returns {HSVObject} hsv - {h: hue [0,360), s: saturation [0,1], v: value [0,1]} */ Color.rgbToHsv = function (rgb) { - var v, x, f, i; - var h = 0, s = 0; var r = rgb.r; var g = rgb.g; var b = rgb.b; - x = Math.min(Math.min(r, g), b); - v = Math.max(Math.max(r, g), b); + 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 - if (x != v) { - f = (r == x) ? g - b : ((g == x) ? b - r : r - g); - i = (r == x) ? 3 : ((g == x) ? 5 : 1); + 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; } @@ -166,9 +192,9 @@ Color.mixRgb = function (rgb0, rgb1, fraction1) { 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 + r: (fraction0 * rgb0.r) + (fraction1 * rgb1.r), + g: (fraction0 * rgb0.g) + (fraction1 * rgb1.g), + b: (fraction0 * rgb0.b) + (fraction1 * rgb1.b) }; }; From d0845728ee57ba8198b176c6daf660237c71c54e Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <cwillisf@media.mit.edu> Date: Thu, 19 Jan 2017 15:19:06 -0800 Subject: [PATCH 3/6] Add unit tests for new color-related functionality Also, fix a math error in `Color.rgbToHsv`. Newly covered functions: - `toRbgColorObject` from `util/cast.js` - `hsvToRgb` from `util/colors.js` - `rgbToHsv` from `util/colors.js` - `mixRgb` from `util/colors.js` --- src/util/color.js | 20 +++++------ test/unit/util_cast.js | 18 ++++++++++ test/unit/util_color.js | 73 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 100 insertions(+), 11 deletions(-) diff --git a/src/util/color.js b/src/util/color.js index 057b68196..951cb270a 100644 --- a/src/util/color.js +++ b/src/util/color.js @@ -37,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 {RGBObject} {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; @@ -51,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; @@ -68,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) { @@ -77,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) { @@ -96,7 +96,7 @@ Color.hexToDecimal = function (hex) { /** * Convert an HSV color to RGB format. * @param {HSVObject} hsv - {h: hue [0,360), s: saturation [0,1], v: value [0,1]} - * @returns {RGBObject} rgb - {r: red [0,255], g: green [0,255], b: blue [0,255]}. + * @return {RGBObject} rgb - {r: red [0,255], g: green [0,255], b: blue [0,255]}. */ Color.hsvToRgb = function (hsv) { var h = hsv.h % 360; @@ -158,12 +158,12 @@ Color.hsvToRgb = function (hsv) { /** * Convert an RGB color to HSV format. * @param {RGBObject} rgb - {r: red [0,255], g: green [0,255], b: blue [0,255]}. - * @returns {HSVObject} hsv - {h: hue [0,360), s: saturation [0,1], v: value [0,1]} + * @return {HSVObject} hsv - {h: hue [0,360), s: saturation [0,1], v: value [0,1]} */ Color.rgbToHsv = function (rgb) { - var r = rgb.r; - var g = rgb.g; - var b = rgb.b; + 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); @@ -185,7 +185,7 @@ Color.rgbToHsv = function (rgb) { * @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. - * @returns {RGBObject} the interpolated color. + * @return {RGBObject} the interpolated color. */ Color.mixRgb = function (rgb0, rgb1, fraction1) { if (fraction1 <= 0) return rgb0; 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..ac8761acc 100644 --- a/test/unit/util_color.js +++ b/test/unit/util_color.js @@ -1,6 +1,43 @@ -var test = require('tap').test; +var tap = require('tap'); +var test = 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 +97,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(); +}); From 72f17d6dc488cf1408673144e284dbd652320ba1 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <cwillisf@media.mit.edu> Date: Thu, 19 Jan 2017 15:58:00 -0800 Subject: [PATCH 4/6] Add pen integration test See `pen.sb2` for details --- test/fixtures/pen.sb2 | Bin 0 -> 55038 bytes test/integration/pen.js | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 test/fixtures/pen.sb2 create mode 100644 test/integration/pen.js diff --git a/test/fixtures/pen.sb2 b/test/fixtures/pen.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..ac4c780d71592eed1cf52c78ae8e0add4ea8df4e GIT binary patch literal 55038 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 literal 0 HcmV?d00001 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); +}); From 88cc50aa18bacf9f97d5f8f9cd8deba997b453b8 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <cwillisf@media.mit.edu> Date: Fri, 20 Jan 2017 11:26:18 -0800 Subject: [PATCH 5/6] Code review: more docs, move constants, clone util Changes include: - Added missing JSDoc for items in `scratch3_pen.js` and `target.js`. - Moved constants used by `Scratch3PenBlocks` into the class. - Created a constant for minimum and maximum pen size. - Added `util/clone.js` to host cloning functionality. - Pen blocks now check for the renderer before trying to use it. - The pen integration test covers all blocks, though `clear`, `stamp`, and `pen down` will skip some of their functionality when there is no renderer. --- src/blocks/scratch3_pen.js | 172 +++++++++++++++++++++++++++++-------- src/engine/target.js | 10 +++ src/util/clone.js | 17 ++++ test/fixtures/pen.sb2 | Bin 55038 -> 54853 bytes 4 files changed, 164 insertions(+), 35 deletions(-) create mode 100644 src/util/clone.js diff --git a/src/blocks/scratch3_pen.js b/src/blocks/scratch3_pen.js index 7f1f6a285..5d652531b 100644 --- a/src/blocks/scratch3_pen.js +++ b/src/blocks/scratch3_pen.js @@ -1,15 +1,9 @@ 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'); -// 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? -var penOrder = 1; - -var stateKey = 'Scratch.pen'; - /** * @typedef {object} PenState - the pen state associated with a particular target. * @property {Boolean} penDown - tracks whether the pen should draw for this target. @@ -19,11 +13,40 @@ var stateKey = 'Scratch.pen'; * 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} */ -var defaultPenState = { +Scratch3PenBlocks.DEFAULT_PEN_STATE = { penDown: false, hue: 120, shade: 50, @@ -33,17 +56,25 @@ var defaultPenState = { } }; -var Scratch3PenBlocks = function (runtime) { - /** - * The runtime instantiating this block package. - * @type {Runtime} - */ - this.runtime = runtime; +/** + * 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; - this._penSkinId = -1; +/** + * The minimum and maximum allowed pen size. + * @type {{min: number, max: number}} + */ +Scratch3PenBlocks.PEN_SIZE_RANGE = {min: 1, max: 255}; - this._onTargetMoved = this._onTargetMoved.bind(this); -}; +/** + * 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. @@ -52,14 +83,19 @@ var Scratch3PenBlocks = function (runtime) { * @private */ Scratch3PenBlocks.prototype._clampPenSize = function (requestedSize) { - return MathUtil.clamp(requestedSize, 1, 255); + 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) { + 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, penOrder); + this.runtime.renderer.setDrawableOrder(this._penDrawableId, Scratch3PenBlocks.PEN_ORDER); this.runtime.renderer.updateDrawableProperties(this._penDrawableId, {skinId: this._penSkinId}); } return this._penSkinId; @@ -71,10 +107,10 @@ Scratch3PenBlocks.prototype._getPenLayerID = function () { * @private */ Scratch3PenBlocks.prototype._getPenState = function (target) { - var penState = target.getCustomState(stateKey); + var penState = target.getCustomState(Scratch3PenBlocks.STATE_KEY); if (!penState) { - penState = JSON.parse(JSON.stringify(defaultPenState)); - target.setCustomState(stateKey, penState); + penState = Clone.simple(Scratch3PenBlocks.DEFAULT_PEN_STATE); + target.setCustomState(Scratch3PenBlocks.STATE_KEY, penState); } return penState; }; @@ -87,10 +123,12 @@ Scratch3PenBlocks.prototype._getPenState = function (target) { * @private */ Scratch3PenBlocks.prototype._onTargetMoved = function (target, oldX, oldY) { - var penState = this._getPenState(target); var penSkinId = this._getPenLayerID(); - this.runtime.renderer.penLine(penSkinId, penState.penAttributes, oldX, oldY, target.x, target.y); - this.runtime.requestRedraw(); + if (penSkinId >= 0) { + var penState = this._getPenState(target); + this.runtime.renderer.penLine(penSkinId, penState.penAttributes, oldX, oldY, target.x, target.y); + this.runtime.requestRedraw(); + } }; /** @@ -143,19 +181,36 @@ Scratch3PenBlocks.prototype.getPrimitives = function () { }; }; +/** + * The pen "clear" block clears the pen layer's contents. + */ Scratch3PenBlocks.prototype.clear = function () { var penSkinId = this._getPenLayerID(); - this.runtime.renderer.penClear(penSkinId); - this.runtime.requestRedraw(); + 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(); - var target = util.target; - this.runtime.renderer.penStamp(penSkinId, target.drawableID); - this.runtime.requestRedraw(); + 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); @@ -166,10 +221,17 @@ Scratch3PenBlocks.prototype.penDown = function (args, util) { } var penSkinId = this._getPenLayerID(); - this.runtime.renderer.penPoint(penSkinId, penState.penAttributes, target.x, target.y); - this.runtime.requestRedraw(); + 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); @@ -180,6 +242,12 @@ Scratch3PenBlocks.prototype.penUp = function (args, util) { } }; +/** + * 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); @@ -190,39 +258,73 @@ Scratch3PenBlocks.prototype.setPenColorToColor = function (args, util) { penState.penAttributes.color4f[0] = rgb.r / 255.0; penState.penAttributes.color4f[1] = rgb.g / 255.0; penState.penAttributes.color4f[2] = rgb.b / 255.0; - - return rgb; }; +/** + * 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)); diff --git a/src/engine/target.js b/src/engine/target.js index d5e379413..947df53d3 100644 --- a/src/engine/target.js +++ b/src/engine/target.js @@ -129,10 +129,20 @@ 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; }; 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/test/fixtures/pen.sb2 b/test/fixtures/pen.sb2 index ac4c780d71592eed1cf52c78ae8e0add4ea8df4e..5d68aa0c54a0eae94f28c9e35415ef6a1f855162 100644 GIT binary patch delta 1218 zcmeyjmig!!=K26{W)?061_lm>;7C<3=Y6p<{>%&vO&kmiJV4QcqWrAX<PyEC;{3d= zp{FM;HWP{6z5meBU5}YsCcL=oQ?6Ecbx)R!XVs~vM_FTYgdC<!xP5u%tNMPG0~hL2 z%)hbC{e8jm#j6jG)1ICD^1eS{VY&JDFsW@X?n-T#`a5b1TXg;Z9W&4V`a0p-iDPQK zA3hyyaAXf@y2azCkT|DP!~5}^B|2(}Qk)Z$XKn5{l2h2Qd?9OyZ|C=A6V0Y7K9ybL zaC>jl0shSPCqbbJ#Wv^H#G6e^Q;XiazQAdVJKHMFl>Z#6vlragZ56wJ*jUSb)|>`; zIhks=NmG(I5?wOnk{cpt)T^|pI!d*wbUbnm&Tp@4UAN$8(^{SHyC!`7p*H18!o=z| zrmNgn>weO2-FH*!UH`0aUNaBWs;5M9|H(7TS$RFd_Nk|_9D`yZpQisrBY~OzLJtI% zwWLhovFR`M+wywJ#;hl|&&!{&DmxIY7_d)sw(N~5tff9>u9`g4Kfd)#Um957dhFre zZ8No`-kwX8Eip>p{P=LyTiLRR|H}kkm#gON^_8EocJCUOfcYE$ty-XQJ9CL^?uYx| ze}vwv++JKSWYqL7Rdec5&Hf~}8<7vQ*ca(9Ro}Y$QT&z)gT~fBO}w)hr58wk?eBMU z*w$RtExU00`E^xqUbXOWZmXVOsCv+7_l^3+H>01`o7~eb(cW6Kr}$@fv0;|t>T<8J z@>`w1kDi+<yYB4M4Kvq9TPMuswzfUt{P*+stgSmA|Fx->{yzK0@rp|S+GS3a5`D=@ zriVB7q)na}QD83?DKu%qQcfd#uZCkL-Is0T&ptbo$0S#5*HU$w)A@hJC-r5Q<1gN1 zeQddXUVWXU%=Wsswg)Be+|RNXlzeXUHCXMF!pWKX6%Ll{DJopGF<bm^Z+ve0f9DGa z!QC>O#auGgH=O=a@B2{H_o1n;ieHjb@(0d>i2{C1=OTGb6sNS>DA`9|F31R<yS3Qh z!p(Odd48I?E;w@Wk?TT(*OF@L_x4>nuXlCzTb8MNeZ}6^H#fZ%3lcxQFlQrwgwg3= zwG*`h&Oa9Oca1JN8TIz}ooklwH=SGBc-L{SjJ@2^j&GI61GgW&x6k!!S~uUvu(<~+ zcYJt0|I;3wtU3R+xOAs3e!aE*kAFz6`i1H)jh7$xw3L~5{@MB`!{*^N^ZyO5wQGMX z?74TRHreiK^C#)Uzn|Lk!tx&jDF1D?XW?gM1~CFTxuw92c;zAqFyo4~07UA!gU=+e zh}q>JCO*_$J9*kwolKC>hKoXv-?K0<fG{gCBO^d1Fnb&5738G{cr!9_F@W+u*lw?% zkB-lKEy=(D!l+uhfu(_=UU3<^mdV=Jq$Or5F)$cmlZ*pO@>w%57-O+)^2BS>riu<A jmy{=#p}S=M$JTWQOh8E>Ms-QSrO6MkiL)KP0&+9}hcp2( delta 1407 zcmX@QhWX!G=K26{W)?061_llWfqE6M{x|z>a<DKkq;fMb@Bl>%it@8klS}lniu3c{ zhMg^5Y$jrFUC(-WsrY(sj>Z$Y<}bI+ySe^m&h57rmK<LWlsG1-BwoLJ@_(FXL!r>} z?O#Hw-!$I;{mb^{OWWH05kHjve`d}-xc|!^X<zLR)7e56J+JqX%=!E`X`{q_^{sv- zVVO<Wue0&6FG#(SCE&7o$`T#DPfJ`XXD!g^m(*6&zbv8pGRwbwGWV@A-WW;uTM=$E zcs4A#b&Tt<!Xf4$uA4!tAOC*#_usioIxUxKxz%0VQx?s-{78E8BJSfEmZ2Z|<Q%Ln zy%7BI@76~Dz-h@H86wIt^=bl6f~k^Tn+^JvMUI_Z_4w+O_1ARY+BFOLuFpRBeCh<J z;O-d4>k9jC-Fft2&I|r*=O)YLabC(hq=X`i+3F>EvtN}T@UMK-)FQHQ$r8(xjLWBp zGM>^{7}yu!xViKB#dE4}Z89^gKR&x${dt~6Lb$QO`q_EjYfo`mtKHs}*mk;p&#vWL zFP)4!yk*_038l;L+T|QlJbU!fj}13}{MP+2UzMT$_JvdXrCGas*E`fR{qyFJKeBZH zmnPwf3u{%S6m|PN=Pc2vO{&<uG{@v<e@_0Z{RbaC<UDgE{l$SP7g=-*3TjMDx^<t5 z^0eHOne+9{RVxdvi1&++8FNj$87Uu|Kl6Y6t)KiCyfQw1z4S8Q=V!6nt?fZurr%Qi zek!bAcj4~ONx_cZ(Qf7Xk1p4-?f?94k;csJ+cO%xCSMWveC_sk|Nj3M|M5*+cXZzV z8l9!S%jI@Yst*!+d~?50ZuYHf7nmNIm@rQG*!0#p;op`w2Xp>kD(Rag{@|r<#j#1R zl744huiBbf-#WD<_u}ORThh1fe{ZAa@KJt?x!KWY4_5Ab-Et*veO8>MzS{wZCKJPL zTR-ufe{@^YS@*1eh0mqE?mrK{_>}eZ?y2aVdinKR{+_(u>@F!S8}&WdKv_e>ylLIS zzTbxD_FaoU?o{xhDC0=oQ+e@c4+Uy%+@b}3uk`a*dl|C+vyN_kz#)$b8<e^OraY5e z;y5R8&I6^Br;Fw-^<S3hA>^cX(IRlMhJ{?GyK(6phi>EIuP;+%;}0jlXkTnv!_@Dr zeImk<<7Bf<YU+%snFl;NI2TS>WWklNwCw7e&pSU>l^r<tvU29!4HEpEd{Y1C1%L7J zyI5?wP{muoFQY*tQBp}mrC!BX<D7t>ve07jbD6icr0eq?R@~}-_O5f&8^*NQy<U$w z7xSLkE)nLf;Wq8&;@78p?JPp)-`2`HHrrwCI@PsWv#cXMrCApr5Q_R-UaGg!=8R(5 z)>Z7O*Ol1B-W6?pw$xz5?KQt2|6MWfV&MCTci!!9Pv3V~)wp?ow_(wlKbsb2xmPRw zto?iN%YoE{eTh>1R(qL#?qYg=cE9Ab<K1D6-#6;azBT8={D`%!GE<iCE0?+xyV&lN z=4bB1zyHa@N)854$+5YOnV*#z#F)XsEd^#QQ!0`GGo-BrAX0h`K9j&AwU>gJ_)yE6 z$#<{lfaE6gT$PqE0v16AdIfpu0p5&ETnwP{32e(``>WFFE}RSuhI++i=#pM_T|bP1 zBp4V#m=&xAMg%D{Fc@LeQlVY2>I2X+5JuH<eDcAoB4!{rI(~dQM`scP0|=w4x#a+I jW_e;6x-+@2Ny}?q200Tf$;t+D2s;qwFfuUITm|s}hiYLG From f8088be127a86e19384508f092ff0f51968452fe Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <cwillisf@media.mit.edu> Date: Fri, 20 Jan 2017 11:37:54 -0800 Subject: [PATCH 6/6] Undo accidental harmless but unnecessary change --- test/unit/util_color.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/unit/util_color.js b/test/unit/util_color.js index ac8761acc..1ffb737eb 100644 --- a/test/unit/util_color.js +++ b/test/unit/util_color.js @@ -1,5 +1,4 @@ -var tap = require('tap'); -var test = tap.test; +var test = require('tap').test; var color = require('../../src/util/color'); /**