const { translateForCamera } = require('../util/pos-math'); /** * Prepare the targets of a runtime for interpolation. * @param {Runtime} runtime The Runtime with targets to prepare for interpolation. */ const setupInitialState = runtime => { const renderer = runtime.renderer; for (const target of runtime.targets) { const directionAndScale = target._getRenderedDirectionAndScale(); let camData = { ...runtime.getCamera(target.cameraBound) }; camData.dir = camData.dir / 180; camData.scale = 1 + ((camData.scale - 1) / 100); // If sprite may have been interpolated in the previous frame, reset its renderer state. if (renderer && target.interpolationData) { const drawableID = target.drawableID; renderer.updateDrawablePosition(drawableID, [target.x - camData.pos[0], target.y - camData.pos[1]]); renderer.updateDrawableDirectionScale( drawableID, directionAndScale.direction - camData.dir, [directionAndScale.scale[0] * camData.scale, directionAndScale.scale[1] * camData.scale] ); renderer.updateDrawableEffect(drawableID, 'ghost', target.effects.ghost); } if (target.visible && !target.isStage) { target.interpolationData = { x: target.x - camData.pos[0], y: target.y - camData.pos[1], direction: directionAndScale.direction - camData.dir, scale: [directionAndScale.scale[0] * camData.scale, directionAndScale.scale[1] * camData.scale], costume: target.currentCostume, ghost: target.effects.ghost }; } else { target.interpolationData = null; } } }; /** * Interpolate the position of targets. * @param {Runtime} runtime The Runtime with targets to interpolate. * @param {number} time Relative time in the frame in [0-1]. */ const interpolate = (runtime, time) => { const renderer = runtime.renderer; if (!renderer) { return; } for (const target of runtime.targets) { // interpolationData is the initial state at the start of the frame (time 0) // the state on the target itself is the state at the end of the frame (time 1) const interpolationData = target.interpolationData; if (!interpolationData) { continue; } // Don't waste time interpolating sprites that are hidden. if ( !target.visible || /* special thanks to CST and Cubester for this new check */ (target.effects.ghost === 100 && interpolationData.ghost === 100) ) { continue; } runtime.emit(runtime.constructor.BEFORE_INTERPOLATE, target); let camData = { ...runtime.getCamera(target.cameraBound) }; camData.scale = 1 + ((camData.scale - 1) / 100); const drawableID = target.drawableID; // Position interpolation. const xDistance = target.x - interpolationData.x - camData.pos[0]; const yDistance = target.y - interpolationData.y - camData.pos[1]; const absoluteXDistance = Math.abs(xDistance); const absoluteYDistance = Math.abs(yDistance); if (absoluteXDistance > 0.1 || absoluteYDistance > 0.1) { const drawable = renderer._allDrawables[drawableID]; // Large movements are likely intended to be instantaneous. // getAABB is less accurate than getBounds, but it's much faster const bounds = drawable.getAABB(); const tolerance = Math.min(240, Math.max(50, 1.5 * (bounds.width + bounds.height))); const distance = Math.sqrt((absoluteXDistance ** 2) + (absoluteYDistance ** 2)); if (distance < tolerance) { const newX = interpolationData.x + (xDistance * time); const newY = interpolationData.y + (yDistance * time); renderer.updateDrawablePosition(drawableID, [newX, newY]); } } // Effect interpolation. const ghostChange = target.effects.ghost - interpolationData.ghost; const absoluteGhostChange = Math.abs(ghostChange); // Large changes are likely intended to be instantaneous. if (absoluteGhostChange > 0 && absoluteGhostChange < 25) { const newGhost = target.effects.ghost + (ghostChange * time); renderer.updateDrawableEffect(drawableID, 'ghost', newGhost); } // Interpolate scale and direction. const costumeUnchanged = interpolationData.costume === target.currentCostume; if (costumeUnchanged) { let {direction, scale} = target._getRenderedDirectionAndScale(); direction = direction - (camData.dir / 180); let updateDrawableDirectionScale = false; // Interpolate direction. if (direction !== interpolationData.direction) { // Perfect 90 degree angles should not be interpolated. // eg. the foreground tile clones in https://scratch.mit.edu/projects/60917032/ if (direction % 90 !== 0 || interpolationData.direction % 90 !== 0) { const currentRadians = direction * Math.PI / 180; const startingRadians = interpolationData.direction * Math.PI / 180; direction = Math.atan2( (Math.sin(currentRadians) * time) + (Math.sin(startingRadians) * (1 - time)), (Math.cos(currentRadians) * time) + (Math.cos(startingRadians) * (1 - time)) ) * 180 / Math.PI; updateDrawableDirectionScale = true; } } // Interpolate scale. const startingScale = interpolationData.scale; scale[0] = scale[0] * camData.scale; scale[1] = scale[1] * camData.scale; if (scale[0] !== startingScale[0] || scale[1] !== startingScale[1]) { // Do not interpolate size when the sign of either scale differs. if ( Math.sign(scale[0]) === Math.sign(startingScale[0]) && Math.sign(scale[1]) === Math.sign(startingScale[1]) ) { const changeX = scale[0] - startingScale[0]; const changeY = scale[1] - startingScale[1]; const absoluteChangeX = Math.abs(changeX); const absoluteChangeY = Math.abs(changeY); // Large changes are likely intended to be instantaneous. if (absoluteChangeX < 100 && absoluteChangeY < 100) { scale[0] = (startingScale[0] + (changeX * time)); scale[1] = (startingScale[1] + (changeY * time)); updateDrawableDirectionScale = true; } } } if (updateDrawableDirectionScale) { renderer.updateDrawableDirectionScale(drawableID, direction, scale); } } runtime.emit(runtime.constructor.AFTER_INTERPOLATE, target); } }; module.exports = { setupInitialState, interpolate };