Spaces:
Build error
Build error
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; | |