File size: 7,292 Bytes
30c32c8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
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
};