scratch-vm/src/blocks/scratch3_pen.js

494 lines
17 KiB
JavaScript
Raw Normal View History

const ArgumentType = require('../extension-support/argument-type');
const BlockType = require('../extension-support/block-type');
2017-04-17 15:10:04 -04:00
const Cast = require('../util/cast');
const Clone = require('../util/clone');
const Color = require('../util/color');
const MathUtil = require('../util/math-util');
const RenderedTarget = require('../sprites/rendered-target');
/**
* Enum for pen color parameters.
* @readonly
* @enum {string}
*/
const ColorParam = {
HUE: 'hue',
SATURATION: 'saturation',
BRIGHTNESS: 'brightness',
TRANSPARENCY: 'transparency'
};
/**
* @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
*/
2017-04-17 19:42:48 -04:00
class Scratch3PenBlocks {
constructor (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._onTargetCreated = this._onTargetCreated.bind(this);
2017-04-17 19:42:48 -04:00
this._onTargetMoved = this._onTargetMoved.bind(this);
runtime.on('targetWasCreated', this._onTargetCreated);
2017-04-17 19:42:48 -04:00
}
/**
2017-04-17 19:42:48 -04:00
* The default pen state, to be used when a target has no existing pen state.
* @type {PenState}
*/
2017-04-17 19:42:48 -04:00
static get DEFAULT_PEN_STATE () {
return {
penDown: false,
hue: 33,
saturation: 100,
brightness: 100,
2017-04-17 19:42:48 -04:00
shade: 50,
2017-09-05 18:00:34 -04:00
transparency: 0,
2017-04-17 19:42:48 -04:00
penAttributes: {
color4f: [0, 0, 1, 1],
diameter: 1
}
};
}
/**
2017-04-17 19:42:48 -04:00
* 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}
*/
2017-04-17 19:42:48 -04:00
static get PEN_ORDER () {
return 1;
}
/**
2017-04-17 19:42:48 -04:00
* The minimum and maximum allowed pen size.
* @type {{min: number, max: number}}
*/
2017-04-17 19:42:48 -04:00
static get PEN_SIZE_RANGE () {
return {min: 1, max: 255};
}
2017-04-17 19:42:48 -04:00
/**
* The key to load & store a target's pen-related state.
* @type {string}
*/
static get STATE_KEY () {
return 'Scratch.pen';
}
2017-04-17 19:42:48 -04:00
/**
* 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
*/
_clampPenSize (requestedSize) {
return MathUtil.clamp(
requestedSize,
Scratch3PenBlocks.PEN_SIZE_RANGE.min,
Scratch3PenBlocks.PEN_SIZE_RANGE.max
);
}
2017-04-17 19:42:48 -04:00
/**
* 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
*/
_getPenLayerID () {
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;
}
2017-04-17 19:42:48 -04:00
/**
* @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
*/
_getPenState (target) {
let penState = target.getCustomState(Scratch3PenBlocks.STATE_KEY);
if (!penState) {
penState = Clone.simple(Scratch3PenBlocks.DEFAULT_PEN_STATE);
target.setCustomState(Scratch3PenBlocks.STATE_KEY, penState);
}
return penState;
}
/**
* When a pen-using Target is cloned, clone the pen state.
* @param {Target} newTarget - the newly created target.
* @param {Target} [sourceTarget] - the target used as a source for the new clone, if any.
* @listens Runtime#event:targetWasCreated
* @private
*/
_onTargetCreated (newTarget, sourceTarget) {
if (sourceTarget) {
const penState = sourceTarget.getCustomState(Scratch3PenBlocks.STATE_KEY);
if (penState) {
newTarget.setCustomState(Scratch3PenBlocks.STATE_KEY, Clone.simple(penState));
if (penState.penDown) {
newTarget.addListener(RenderedTarget.EVENT_TARGET_MOVED, this._onTargetMoved);
}
}
}
}
2017-04-17 19:42:48 -04:00
/**
* 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
*/
_onTargetMoved (target, oldX, oldY) {
const penSkinId = this._getPenLayerID();
if (penSkinId >= 0) {
const penState = this._getPenState(target);
this.runtime.renderer.penLine(penSkinId, penState.penAttributes, oldX, oldY, target.x, target.y);
this.runtime.requestRedraw();
}
}
2017-04-17 19:42:48 -04:00
/**
* Wrap a pen hue or shade values to the range (0,200).
2017-04-17 19:42:48 -04:00
* @param {number} value - the pen hue or shade value to the proper range.
* @returns {number} the wrapped value.
* @private
*/
_wrapHueOrShade (value) {
value = value % 200;
if (value < 0) value += 200;
return value;
}
2017-09-05 18:00:34 -04:00
_wrapHue (value) {
return MathUtil.wrapClamp(value, 0, 100);
}
/**
* Clamp a pen color parameter to the range (0,100).
* @param {number} value - the value to be clamped.
2017-09-05 18:00:34 -04:00
* @returns {number} the clamped value.
* @private
*/
_clampColorParam (value) {
2017-09-05 18:00:34 -04:00
return MathUtil.clamp(value, 0, 100);
}
/**
* Convert an alpha value to a pen transparency value.
* Alpha ranges from 0 to 1, where 0 is transparent and 1 is opaque.
* Transparency ranges from 0 to 100, where 0 is opaque and 100 is transparent.
* @param {number} alpha - the input alpha value.
* @returns {number} the transparency value.
* @private
*/
_alphaToTransparency (alpha) {
return (1.0 - alpha) * 100.0;
}
/**
* Convert a pen transparency value to an alpha value.
* Alpha ranges from 0 to 1, where 0 is transparent and 1 is opaque.
* Transparency ranges from 0 to 100, where 0 is opaque and 100 is transparent.
* @param {number} transparency - the input transparency value.
* @returns {number} the alpha value.
* @private
*/
_transparencyToAlpha (transparency) {
return 1.0 - (transparency / 100.0);
}
2017-04-17 19:42:48 -04:00
/**
* @returns {object} metadata for this extension and its blocks.
2017-04-17 19:42:48 -04:00
*/
getInfo () {
2017-04-17 19:42:48 -04:00
return {
id: 'pen',
name: 'Pen',
blocks: [
{
opcode: 'clear',
blockType: BlockType.COMMAND
},
{
opcode: 'stamp',
blockType: BlockType.COMMAND
},
{
opcode: 'penDown',
blockType: BlockType.COMMAND,
text: 'pen down'
},
{
opcode: 'penUp',
blockType: BlockType.COMMAND,
text: 'pen up'
},
{
opcode: 'setPenColorToColor',
blockType: BlockType.COMMAND,
text: 'set pen color to [COLOR]',
arguments: {
COLOR: {
type: ArgumentType.COLOR
}
}
},
{
opcode: 'changePenColorParamBy',
blockType: BlockType.COMMAND,
text: 'change pen [COLOR_PARAM] by [VALUE]',
arguments: {
COLOR_PARAM: {
type: ArgumentType.STRING,
menu: 'colorParam',
defaultValue: ColorParam.HUE
},
VALUE: {
type: ArgumentType.NUMBER,
defaultValue: 10
}
}
},
{
opcode: 'setPenColorParamTo',
blockType: BlockType.COMMAND,
text: 'set pen [COLOR_PARAM] to [VALUE]',
arguments: {
COLOR_PARAM: {
type: ArgumentType.STRING,
menu: 'colorParam',
defaultValue: ColorParam.HUE
},
VALUE: {
type: ArgumentType.NUMBER,
defaultValue: 50
}
}
},
{
opcode: 'changePenSizeBy',
blockType: BlockType.COMMAND,
text: 'change pen size by [SIZE]',
arguments: {
SIZE: {
type: ArgumentType.NUMBER,
defaultValue: 1
}
}
},
{
opcode: 'setPenSizeTo',
blockType: BlockType.COMMAND,
text: 'set pen size to [SIZE]',
arguments: {
SIZE: {
type: ArgumentType.NUMBER,
defaultValue: 1
}
}
}
],
menus: {
colorParam:
[ColorParam.HUE, ColorParam.SATURATION,
ColorParam.BRIGHTNESS, ColorParam.TRANSPARENCY]
}
2017-04-17 19:42:48 -04:00
};
}
2017-04-17 19:42:48 -04:00
/**
* The pen "clear" block clears the pen layer's contents.
*/
clear () {
const penSkinId = this._getPenLayerID();
if (penSkinId >= 0) {
this.runtime.renderer.penClear(penSkinId);
this.runtime.requestRedraw();
}
}
2017-04-17 19:42:48 -04:00
/**
* 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.
*/
stamp (args, util) {
const penSkinId = this._getPenLayerID();
if (penSkinId >= 0) {
const target = util.target;
this.runtime.renderer.penStamp(penSkinId, target.drawableID);
this.runtime.requestRedraw();
}
}
2017-04-17 19:42:48 -04:00
/**
* 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.
*/
penDown (args, util) {
const target = util.target;
const penState = this._getPenState(target);
2017-04-17 19:42:48 -04:00
if (!penState.penDown) {
penState.penDown = true;
target.addListener(RenderedTarget.EVENT_TARGET_MOVED, this._onTargetMoved);
}
2017-04-17 19:42:48 -04:00
const penSkinId = this._getPenLayerID();
if (penSkinId >= 0) {
this.runtime.renderer.penPoint(penSkinId, penState.penAttributes, target.x, target.y);
this.runtime.requestRedraw();
}
}
2017-04-17 19:42:48 -04:00
/**
* 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.
*/
penUp (args, util) {
2017-04-17 15:10:04 -04:00
const target = util.target;
2017-04-17 19:42:48 -04:00
const penState = this._getPenState(target);
2017-04-17 19:42:48 -04:00
if (penState.penDown) {
penState.penDown = false;
target.removeListener(RenderedTarget.EVENT_TARGET_MOVED, this._onTargetMoved);
}
}
2017-04-17 19:42:48 -04:00
/**
* 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.
*/
setPenColorToColor (args, util) {
const penState = this._getPenState(util.target);
const rgb = Cast.toRgbColorObject(args.COLOR);
const hsv = Color.rgbToHsv(rgb);
penState.hue = (hsv.h / 360) * 100;
penState.saturation = hsv.s * 100;
penState.brightness = hsv.v * 100;
2017-04-17 19:42:48 -04:00
this._updatePenColor(penState);
}
2017-04-17 19:42:48 -04:00
/**
* Update the cached color from the hue, saturation, brightness and transparency values
* in the provided PenState object.
* @param {PenState} penState - the pen state to update.
* @private
2017-04-17 19:42:48 -04:00
*/
_updatePenColor (penState) {
let rgb = Color.hsvToRgb({
h: penState.hue * 360 / 100,
s: penState.saturation / 100,
v: penState.brightness / 100
});
penState.penAttributes.color4f[0] = rgb.r / 255.0;
penState.penAttributes.color4f[1] = rgb.g / 255.0;
penState.penAttributes.color4f[2] = rgb.b / 255.0;
penState.penAttributes.color4f[3] = this._transparencyToAlpha(penState.transparency);
}
changePenColorParamBy (args, util) {
2017-04-17 19:42:48 -04:00
const penState = this._getPenState(util.target);
switch (args.COLOR_PARAM) {
case ColorParam.HUE:
penState.hue = this._wrapHue(penState.hue + Cast.toNumber(args.VALUE));
break;
case ColorParam.SATURATION:
penState.saturation = this._clampColorParam(penState.saturation + Cast.toNumber(args.VALUE));
break;
case ColorParam.BRIGHTNESS:
penState.brightness = this._clampColorParam(penState.brightness + Cast.toNumber(args.VALUE));
break;
case ColorParam.TRANSPARENCY:
penState.transparency = this._clampColorParam(penState.transparency + Cast.toNumber(args.VALUE));
break;
}
2017-04-17 19:42:48 -04:00
this._updatePenColor(penState);
}
setPenColorParamTo (args, util) {
2017-04-17 19:42:48 -04:00
const penState = this._getPenState(util.target);
switch (args.COLOR_PARAM) {
case ColorParam.HUE:
penState.hue = this._wrapHue(Cast.toNumber(args.VALUE));
break;
case ColorParam.SATURATION:
penState.saturation = this._clampColorParam(Cast.toNumber(args.VALUE));
break;
case ColorParam.BRIGHTNESS:
penState.brightness = this._clampColorParam(Cast.toNumber(args.VALUE));
break;
case ColorParam.TRANSPARENCY:
penState.transparency = this._clampColorParam(Cast.toNumber(args.VALUE));
break;
}
2017-04-17 19:42:48 -04:00
this._updatePenColor(penState);
}
2017-04-17 19:42:48 -04:00
/**
* 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.
*/
changePenSizeBy (args, util) {
const penAttributes = this._getPenState(util.target).penAttributes;
penAttributes.diameter = this._clampPenSize(penAttributes.diameter + Cast.toNumber(args.SIZE));
}
2017-04-17 19:42:48 -04:00
/**
* 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.
*/
setPenSizeTo (args, util) {
const penAttributes = this._getPenState(util.target).penAttributes;
penAttributes.diameter = this._clampPenSize(Cast.toNumber(args.SIZE));
}
}
module.exports = Scratch3PenBlocks;