soiz1's picture
Upload 811 files
30c32c8 verified
raw
history blame
19.9 kB
const formatMessage = require('format-message');
const BlockType = require('../../extension-support/block-type');
const ArgumentType = require('../../extension-support/argument-type');
const Cast = require('../../util/cast');
/**
* @param {number} time should be 0-1
* @param {number} a value at 0
* @param {number} b value at 1
* @returns {number}
*/
const interpolate = (time, a, b) => {
// don't restrict range of time as some easing functions are expected to go outside the range
const multiplier = b - a;
return time * multiplier + a;
};
const linear = x => x;
const sine = (x, dir) => {
switch (dir) {
case "in": return 1 - Math.cos((x * Math.PI) / 2);
case "out": return Math.sin((x * Math.PI) / 2);
case "in out": return -(Math.cos(Math.PI * x) - 1) / 2;
default: return 0;
}
};
const quad = (x, dir) => {
switch (dir) {
case "in": return x * x;
case "out": return 1 - (1 - x) * (1 - x);
case "in out": return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2;
default: return 0;
}
};
const cubic = (x, dir) => {
switch (dir) {
case "in": return x * x * x;
case "out": return 1 - Math.pow(1 - x, 3);
case "in out": return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
default: return 0;
}
};
const quart = (x, dir) => {
switch (dir) {
case "in": return x * x * x * x;
case "out": return 1 - Math.pow(1 - x, 4);
case "in out": return x < 0.5 ? 8 * x * x * x * x : 1 - Math.pow(-2 * x + 2, 4) / 2;
default: return 0;
}
};
const quint = (x, dir) => {
switch (dir) {
case "in": return x * x * x * x * x;
case "out": return 1 - Math.pow(1 - x, 5);
case "in out": return x < 0.5 ? 16 * x * x * x * x * x : 1 - Math.pow(-2 * x + 2, 5) / 2;
default:
return 0;
}
};
const expo = (x, dir) => {
switch (dir) {
case "in": return x === 0 ? 0 : Math.pow(2, 10 * x - 10);
case "out": return x === 1 ? 1 : 1 - Math.pow(2, -10 * x);
case "in out": return x === 0 ? 0 : x === 1 ? 1 : x < 0.5
? Math.pow(2, 20 * x - 10) / 2 : (2 - Math.pow(2, -20 * x + 10)) / 2;
default: return 0;
}
};
const circ = (x, dir) => {
switch (dir) {
case "in": return 1 - Math.sqrt(1 - Math.pow(x, 2));
case "out": return Math.sqrt(1 - Math.pow(x - 1, 2));
case "in out": return x < 0.5 ? (1 - Math.sqrt(1 - Math.pow(2 * x, 2))) / 2
: (Math.sqrt(1 - Math.pow(-2 * x + 2, 2)) + 1) / 2;
default: return 0;
}
};
const back = (x, dir) => {
const c1 = 1.70158;
const c2 = c1 * 1.525;
const c3 = c1 + 1;
switch (dir) {
case "in": return c3 * x * x * x - c1 * x * x;
case "out": return 1 + c3 * Math.pow(x - 1, 3) + c1 * Math.pow(x - 1, 2);
case "in out": return x < 0.5 ? (Math.pow(2 * x, 2) * ((c2 + 1) * 2 * x - c2)) / 2
: (Math.pow(2 * x - 2, 2) * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2;
default: return 0;
}
};
const elastic = (x, dir) => {
const c4 = (2 * Math.PI) / 3;
const c5 = (2 * Math.PI) / 4.5;
switch (dir) {
case "in": return x === 0 ? 0 : x === 1 ? 1 : -Math.pow(2, 10 * x - 10) * Math.sin((x * 10 - 10.75) * c4);
case "out": return x === 0 ? 0 : x === 1 ? 1 : Math.pow(2, -10 * x) * Math.sin((x * 10 - 0.75) * c4) + 1;
case "in out": return x === 0 ? 0 : x === 1 ? 1 : x < 0.5
? -(Math.pow(2, 20 * x - 10) * Math.sin((20 * x - 11.125) * c5)) / 2
: (Math.pow(2, -20 * x + 10) * Math.sin((20 * x - 11.125) * c5)) / 2 + 1;
default: return 0;
}
};
const bounce = (x, dir) => {
switch (dir) {
case "in": return 1 - bounce(1 - x, "out");
case "out": {
const n1 = 7.5625, d1 = 2.75;
if (x < 1 / d1) return n1 * x * x;
else if (x < 2 / d1) return n1 * (x -= 1.5 / d1) * x + 0.75;
else if (x < 2.5 / d1) return n1 * (x -= 2.25 / d1) * x + 0.9375;
return n1 * (x -= 2.625 / d1) * x + 0.984375;
}
case "in out": return x < 0.5 ? (1 - bounce(1 - 2 * x, "out")) / 2 : (1 + bounce(2 * x - 1, "out")) / 2;
default: return 0;
}
};
const EasingMethods = {
linear, sine, quad, cubic, quart,
quint, expo, circ, back, elastic, bounce
};
class Tween {
constructor(runtime) {
/**
* The runtime instantiating this block package.
* @type {Runtime}
*/
this.runtime = runtime;
}
getInfo() {
return {
id: "jgTween",
name: "Tweening",
blocks: [
{
opcode: "tweenValue",
text: formatMessage({
id: 'jgTween.blocks.tweenValue',
default: '[MODE] ease [DIRECTION] [START] to [END] by [AMOUNT]%',
description: 'Block for easing a value with a certain mode and direction by a certain amount.'
}),
disableMonitor: true,
blockType: BlockType.REPORTER,
arguments: {
MODE: {
type: ArgumentType.STRING,
menu: "modes"
},
DIRECTION: {
type: ArgumentType.STRING,
menu: "direction"
},
START: {
type: ArgumentType.NUMBER,
defaultValue: 0
},
END: {
type: ArgumentType.NUMBER,
defaultValue: 100
},
AMOUNT: {
type: ArgumentType.NUMBER,
defaultValue: 50
}
}
},
{
opcode: "tweenVariable",
text: "tween variable [VAR] to [VALUE] over [SEC] seconds using [MODE] ease [DIRECTION]",
blockType: BlockType.COMMAND,
arguments: {
VAR: {
type: ArgumentType.STRING,
menu: "vars"
},
VALUE: {
type: ArgumentType.NUMBER,
defaultValue: 100
},
SEC: {
type: ArgumentType.NUMBER,
defaultValue: 1
},
MODE: {
type: ArgumentType.STRING,
menu: "modes"
},
DIRECTION: {
type: ArgumentType.STRING,
menu: "direction"
}
}
},
{
opcode: "tweenXY",
text: "tween to x: [X] y: [Y] over [SEC] seconds using [MODE] ease [DIRECTION]",
blockType: BlockType.COMMAND,
arguments: {
PROPERTY: {
type: ArgumentType.STRING,
menu: "properties"
},
X: {
type: ArgumentType.NUMBER,
defaultValue: 100
},
Y: {
type: ArgumentType.NUMBER,
defaultValue: 100
},
SEC: {
type: ArgumentType.NUMBER,
defaultValue: 1
},
MODE: {
type: ArgumentType.STRING,
menu: "modes"
},
DIRECTION: {
type: ArgumentType.STRING,
menu: "direction"
}
}
},
{
opcode: "tweenProperty",
text: "tween [PROPERTY] to [VALUE] over [SEC] seconds using [MODE] ease [DIRECTION]",
blockType: BlockType.COMMAND,
arguments: {
PROPERTY: {
type: ArgumentType.STRING,
menu: "properties"
},
VALUE: {
type: ArgumentType.NUMBER,
defaultValue: 100
},
SEC: {
type: ArgumentType.NUMBER,
defaultValue: 1
},
MODE: {
type: ArgumentType.STRING,
menu: "modes"
},
DIRECTION: {
type: ArgumentType.STRING,
menu: "direction"
}
}
},
"---",
{
opcode: "tweenVariableCancel",
text: "cancel tween for variable [VAR]",
blockType: BlockType.COMMAND,
arguments: {
VAR: {
type: ArgumentType.STRING,
menu: "vars"
}
}
},
{
opcode: "tweenPropertyCancel",
text: "cancel tween for [PROPERTY]",
blockType: BlockType.COMMAND,
arguments: {
PROPERTY: {
type: ArgumentType.STRING,
menu: "properties"
}
}
},
"---",
{
opcode: "tweenC", blockType: BlockType.LOOP,
text: "[MODE] ease [DIRECTION] [CHANGE] [START] to [END] in [SEC] secs",
arguments: {
MODE: {
type: ArgumentType.STRING,
menu: "modes",
},
DIRECTION: {
type: ArgumentType.STRING,
menu: "direction",
},
CHANGE: {
type: ArgumentType.STRING,
fillIn: "tweenVal"
},
START: {
type: ArgumentType.NUMBER,
defaultValue: 0,
},
END: {
type: ArgumentType.NUMBER,
defaultValue: 100,
},
SEC: {
type: ArgumentType.NUMBER,
defaultValue: 1,
},
}
},
{
opcode: "tweenVal", blockType: BlockType.REPORTER,
text: "tween value", canDragDuplicate: true, hideFromPalette: true
},
],
menus: {
modes: {
acceptReporters: true,
items: Object.keys(EasingMethods)
},
direction: {
acceptReporters: true,
items: ["in", "out", "in out"]
},
vars: {
acceptReporters: false, // for Scratch parity
items: "getVariables"
},
properties: {
acceptReporters: true,
items: ["x position", "y position", "direction", "size"]
}
}
};
}
getVariables() {
const variables =
// @ts-expect-error
typeof Blockly === "undefined" ? [] :
// @ts-expect-error
Blockly.getMainWorkspace()
.getVariableMap().getVariablesOfType("")
.map(model => ({ text: model.name, value: model.getId() }));
if (variables.length > 0) return variables;
return [{ text: "", value: "" }];
}
tweenValue(args) {
const easeMethod = Cast.toString(args.MODE);
const easeDirection = Cast.toString(args.DIRECTION);
const start = Cast.toNumber(args.START);
const end = Cast.toNumber(args.END);
const progress = Cast.toNumber(args.AMOUNT) / 100;
if (!Object.prototype.hasOwnProperty.call(EasingMethods, easeMethod)) {
// Unknown method
return start;
}
const easingFunction = EasingMethods[easeMethod];
const tweened = easingFunction(progress, easeDirection);
return interpolate(tweened, start, end);
}
_tweenValue(args, util, id, valueArgName, currentValue, propertyName) {
// Only use args on first run. For later executions grab everything from stackframe.
// This ensures that if the arguments change, the tweening won't change. This matches
// the vanilla Scratch glide blocks.
const state = util.stackFrame[id];
if (!state) {
// First run, need to start timer
util.yield();
if (util.stackTimerNeedsInit()) {
const durationMS = Math.max(0, 1000 * Cast.toNumber(args.SEC));
util.startStackTimer(durationMS);
}
const easeMethod = Cast.toString(args.MODE);
const easeDirection = Cast.toString(args.DIRECTION);
const start = currentValue;
const end = Cast.toNumber(args[valueArgName]);
let easingFunction;
if (Object.prototype.hasOwnProperty.call(EasingMethods, easeMethod)) easingFunction = EasingMethods[easeMethod];
else easingFunction = EasingMethods.linear;
util.stackFrame[id] = {
easingFunction, easeDirection,
start, end, propertyName
};
return start;
} else if (util.stackTimerFinished()) {
// Done
return util.stackFrame[id].end;
}
// Still running
util.yield();
const progress = util.stackFrame.timer.timeElapsed() / util.stackFrame.duration;
const tweened = state.easingFunction(progress, state.easeDirection);
return interpolate(tweened, state.start, state.end);
}
tweenVariable(args, util) {
const variable = util.target.lookupVariableById(args.VAR);
if (util.stackFrame[""] && util.stackFrame[""].cancelled) {
return;
}
const value = this._tweenValue(args, util, "", "VALUE", variable.value, args.VAR);
if (variable && variable.type === "") variable.value = value;
}
tweenXY(args, util) {
const stateX = util.stackFrame["x"] || {};
const stateY = util.stackFrame["y"] || {};
const x = stateX.cancelled ? util.target.x : this._tweenValue(args, util, "x", "X", util.target.x, "x position");
const y = stateY.cancelled ? util.target.y : this._tweenValue(args, util, "y", "Y", util.target.y, "y position");
util.target.setXY(x, y);
}
tweenProperty(args, util) {
let currentValue = 0;
if (args.PROPERTY === "x position") currentValue = util.target.x;
else if (args.PROPERTY === "y position") currentValue = util.target.y;
else if (args.PROPERTY === "direction") currentValue = util.target.direction;
else if (args.PROPERTY === "size") currentValue = util.target.size;
if (util.stackFrame[""] && util.stackFrame[""].cancelled) {
return;
}
const value = this._tweenValue(args, util, "", "VALUE", currentValue, args.PROPERTY);
if (args.PROPERTY === "x position") util.target.setXY(value, util.target.y);
else if (args.PROPERTY === "y position") util.target.setXY(util.target.x, value);
else if (args.PROPERTY === "direction") util.target.setDirection(value);
else if (args.PROPERTY === "size") util.target.setSize(value);
}
tweenVariableCancel(args, util) {
const property = args.VAR;
this.tweenPropertyCancel({
PROPERTY: property
}, util);
}
tweenPropertyCancel(args, util) {
const property = args.PROPERTY;
const id = util.target.id;
// supposedly for i loop is faster (garbo seemed to say this before too?)
for (let i = 0; i < this.runtime.threads.length; i++) {
const thread = this.runtime.threads[i];
if (!thread.target) continue;
if (thread.target.id !== id) continue;
// some threads dont have a stackFrame from util
if (!thread.compatibilityStackFrame) continue;
// x position and y position should also cancel the tweenXY block
const propertyFrame = thread.compatibilityStackFrame[""] ||
(property === "x position" ? thread.compatibilityStackFrame["x"] : null) ||
(property === "y position" ? thread.compatibilityStackFrame["y"] : null);
// this thread did not have a property tween
if (!propertyFrame) continue;
// check if the property being tweened is the one we are cancelling
if (propertyFrame.propertyName !== property) continue;
propertyFrame.cancelled = true;
}
}
tweenC(args, util) {
const id = "loopedVal";
const state = util.stackFrame[id];
if (!state) {
if (util.stackTimerNeedsInit()) {
const durationMS = Math.max(0, 1000 * Cast.toNumber(args.SEC));
util.startStackTimer(durationMS);
}
const easeMethod = Cast.toString(args.MODE);
const easeDirection = Cast.toString(args.DIRECTION);
const start = Cast.toNumber(args.START);
const end = Cast.toNumber(args.END);
const params = util.thread.tweenValue;
if (typeof params === "undefined") util.thread.stackFrames[0].tweenValue = start;
let easingFunction;
if (Object.prototype.hasOwnProperty.call(EasingMethods, easeMethod)) easingFunction = EasingMethods[easeMethod];
else easingFunction = EasingMethods.linear;
util.stackFrame[id] = {
easingFunction, easeDirection,
start, end,
};
util.startBranch(1, true);
} else if (util.stackTimerFinished()) {
util.thread.stackFrames[0].tweenValue = util.stackFrame[id].end;
if (util.stackFrame[id].canContinue !== "stop") {
util.stackFrame[id].canContinue = "stop";
util.startBranch(1, true);
}
} else {
const progress = util.stackFrame.timer.timeElapsed() / util.stackFrame.duration;
const tweened = state.easingFunction(progress, state.easeDirection);
util.thread.stackFrames[0].tweenValue = interpolate(tweened, state.start, state.end);
if (util.stackFrame[id].canContinue !== "stop") util.startBranch(1, true);
}
}
tweenVal(_, util) {
return util.thread.stackFrames[0].tweenValue ?? "";
}
}
module.exports = Tween;