mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-01-11 10:39:56 -05:00
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.
This commit is contained in:
parent
d33af47a87
commit
369c02b5d5
7 changed files with 382 additions and 6 deletions
231
src/blocks/scratch3_pen.js
Normal file
231
src/blocks/scratch3_pen.js
Normal file
|
@ -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;
|
|
@ -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'),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -563,7 +563,7 @@ var specMap = {
|
|||
]
|
||||
},
|
||||
'setPenShadeTo:': {
|
||||
opcode: 'pen_changepenshadeby',
|
||||
opcode: 'pen_setpenshadeto',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue