Skip to content

Commit 2f5bc86

Browse files
CSE Machine: Added animation for spread instruction (#3114)
* spread animation * spread animation * fixed bug with empty array, and animated control instr * fixed bug with empty array, and animated control instr * added animation for array access out of range * add highlight to change in call arity * remove console log * Update index.html * Update craco.config.js * Update ArrayAccessAnimation.tsx * Update craco.config.js --------- Co-authored-by: Martin Henz <[email protected]>
1 parent 1ccd0d2 commit 2f5bc86

File tree

12 files changed

+227
-20
lines changed

12 files changed

+227
-20
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ env:
1515
concurrency:
1616
group: ${{ github.workflow }}-${{ github.ref }}
1717
cancel-in-progress: true
18-
18+
1919
jobs:
2020
lint:
2121
if: github.event.pull_request.draft == false

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ To start contributing, create a fork from our repo and send a PR. Refer to [this
1616

1717
The frontend comes with an extensive test suite. To run the tests after you made your modifications, run
1818
`yarn test`. Regression tests are run automatically when you want to push changes to this repository.
19-
The regression tests are generated using `jest` and stored as snapshots in `src/\_\_tests\_\_`. After modifying the frontend, carefully inspect any failing regression tests reported in red in the command line. If you are convinced that the regression tests and not your changes are at fault, you can update the regression tests by running:
19+
The regression tests are generated using `jest` and stored as snapshots in `src/\_\_tests\_\_`. After modifying the frontend, carefully inspect any failing regression tests reported in red in the command line. If you are convinced that the regression tests and not your changes are at fault, you can update the regression tests by running:
2020

2121
```bash
2222
yarn test --updateSnapshot

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ The Source Academy (<https://sourceacademy.org/>) is an immersive online experie
2424

2525
1. Clone this repository and navigate to it using your command line
2626

27-
1. Install the version of `yarn` as specified in `package.json`, `packageManager`.
27+
1. Install the version of `yarn` as specified in `package.json`, `packageManager`.
2828

2929
> We recommend using `corepack` to manage the version of `yarn`, you may simply run `corepack enable` to complete this step.
3030

_config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
theme: jekyll-theme-cayman
1+
theme: jekyll-theme-cayman

eslint.config.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import { config, configs } from 'typescript-eslint';
55
import reactPlugin from 'eslint-plugin-react';
66
import reactHooksPlugin from 'eslint-plugin-react-hooks';
7-
import simpleImportSort from 'eslint-plugin-simple-import-sort'
7+
import simpleImportSort from 'eslint-plugin-simple-import-sort';
88
// import reactRefresh from 'eslint-plugin-react-refresh';
99

1010
export default config(
@@ -24,7 +24,7 @@ export default config(
2424
files: ['**/*.ts*'],
2525
plugins: {
2626
'react-hooks': reactHooksPlugin,
27-
'react': reactPlugin,
27+
react: reactPlugin,
2828
'simple-import-sort': simpleImportSort
2929
},
3030
rules: {

public/externalLibs/sound/soundToneMatrix.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ var timeout_matrix;
3636
// for coloring the matrix accordingly while it's being played
3737
var timeout_color;
3838

39-
var timeout_objects = new Array();
39+
var timeout_objects = [];
4040

4141
// vector_to_list returns a list that contains the elements of the argument vector
4242
// in the given order.
@@ -54,7 +54,7 @@ function vector_to_list(vector) {
5454
function x_y_to_row_column(x, y) {
5555
var row = Math.floor((y - margin_length) / (square_side_length + distance_between_squares));
5656
var column = Math.floor((x - margin_length) / (square_side_length + distance_between_squares));
57-
return Array(row, column);
57+
return [row, column];
5858
}
5959

6060
// given the row number of a square, return the leftmost coordinate
@@ -365,5 +365,5 @@ function clear_all_timeout() {
365365
clearTimeout(timeout_objects[i]);
366366
}
367367

368-
timeout_objects = new Array();
368+
timeout_objects = [];
369369
}

public/manifest.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,16 @@
1313
"type": "image/png"
1414
},
1515
{
16-
"src": "icons/android-chrome-256x256.png",
17-
"sizes": "256x256",
18-
"type": "image/png"
16+
"src": "icons/android-chrome-256x256.png",
17+
"sizes": "256x256",
18+
"type": "image/png"
1919
},
2020
{
2121
"src": "icons/maskable.png",
2222
"sizes": "196x196",
2323
"type": "image/png",
2424
"purpose": "maskable"
25-
}
25+
}
2626
],
2727
"start_url": "./",
2828
"display": "standalone",

src/features/cseMachine/CseMachineAnimation.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import React from 'react';
66

77
import { ArrayAccessAnimation } from './animationComponents/ArrayAccessAnimation';
88
import { ArrayAssignmentAnimation } from './animationComponents/ArrayAssignmentAnimation';
9+
import { ArraySpreadAnimation } from './animationComponents/ArraySpreadAnimation';
910
import { AssignmentAnimation } from './animationComponents/AssignmentAnimation';
1011
import { Animatable } from './animationComponents/base/Animatable';
1112
import { lookupBinding } from './animationComponents/base/AnimationUtils';
@@ -112,6 +113,11 @@ export class CseAnimation {
112113
);
113114
}
114115
break;
116+
case 'SpreadElement':
117+
CseAnimation.animations.push(
118+
new ControlExpansionAnimation(lastControlComponent, CseAnimation.getNewControlItems())
119+
);
120+
break;
115121
case 'AssignmentExpression':
116122
case 'ArrayExpression':
117123
case 'BinaryExpression':
@@ -276,6 +282,35 @@ export class CseAnimation {
276282
)
277283
);
278284
break;
285+
case InstrType.SPREAD:
286+
const control = Layout.controlComponent.stackItemComponents;
287+
const array = Layout.previousStashComponent.stashItemComponents.at(-1)!.arrow!
288+
.target! as ArrayValue;
289+
290+
let currCallInstr;
291+
292+
for (let i = 1; control.at(-i) != undefined; i++) {
293+
if (control.at(-i)?.text.includes('call ')) {
294+
// find call instr above
295+
currCallInstr = control.at(-i);
296+
break;
297+
}
298+
}
299+
300+
const resultItems =
301+
array.data.length !== 0
302+
? Layout.stashComponent.stashItemComponents.slice(-array.data.length)
303+
: [];
304+
305+
CseAnimation.animations.push(
306+
new ArraySpreadAnimation(
307+
lastControlComponent,
308+
Layout.previousStashComponent.stashItemComponents.at(-1)!,
309+
resultItems!,
310+
currCallInstr!
311+
)
312+
);
313+
break;
279314
case InstrType.ARRAY_LENGTH:
280315
case InstrType.BREAK:
281316
case InstrType.BREAK_MARKER:

src/features/cseMachine/animationComponents/ArrayAccessAnimation.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export class ArrayAccessAnimation extends Animatable {
2828
private resultAnimation: AnimatedTextbox;
2929
private resultArrowAnimation?: AnimatedGenericArrow<StashItemComponent, Visible>;
3030
private arrayUnit: ArrayUnit;
31+
private outOfRange: boolean;
3132

3233
constructor(
3334
private accInstr: ControlItemComponent,
@@ -46,12 +47,29 @@ export class ArrayAccessAnimation extends Animatable {
4647
rectProps: { stroke: defaultDangerColor() }
4748
});
4849
this.arrayArrowAnimation = new AnimatedGenericArrow(arrayItem.arrow!);
50+
// if index is out of range
51+
this.outOfRange = false;
4952
// the target should always be an array value
5053
const array = arrayItem.arrow!.target! as ArrayValue;
51-
this.arrayUnit = array.units[parseInt(indexItem.text)];
54+
55+
// if index access is out of range. if index access is negative, error should be thrown from js-slang at this point
56+
const arraylen = array.data.length;
57+
58+
if (parseInt(indexItem.text) >= arraylen) {
59+
this.outOfRange = true;
60+
this.arrayUnit = array.units[arraylen - 1];
61+
} else {
62+
this.arrayUnit = array.units[parseInt(indexItem.text)];
63+
}
64+
5265
this.resultAnimation = new AnimatedTextbox(resultItem.text, {
5366
...getNodeDimensions(resultItem),
54-
x: this.arrayUnit.x() + this.arrayUnit.width() / 2 - this.resultItem.width() / 2,
67+
// if array index out of range, animate to one unit beyond array
68+
x:
69+
this.arrayUnit.x() +
70+
this.arrayUnit.width() / 2 -
71+
this.resultItem.width() / 2 +
72+
(this.outOfRange ? this.arrayUnit.width() : 0),
5573
y: this.arrayUnit.y() + this.arrayUnit.height() / 2 - this.resultItem.height() / 2,
5674
opacity: 0
5775
});
@@ -79,7 +97,12 @@ export class ArrayAccessAnimation extends Animatable {
7997
const minInstrItemWidth =
8098
getTextWidth(this.accInstr.text) + ControlStashConfig.ControlItemTextPadding * 2;
8199
const indexAboveArrayLocation = {
82-
x: this.arrayUnit.x() + this.arrayUnit.width() / 2 - this.indexItem.width() / 2,
100+
// if array index out of range, animate to one unit beyond array
101+
x:
102+
this.arrayUnit.x() +
103+
this.arrayUnit.width() / 2 -
104+
this.indexItem.width() / 2 +
105+
(this.outOfRange ? this.arrayUnit.width() : 0),
83106
y: this.arrayUnit.y() - this.indexItem.height() - 8
84107
};
85108
const indexInArrayLocation = {
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
//import { Easings } from 'konva/lib/Tween';
2+
import React from 'react';
3+
import { Group } from 'react-konva';
4+
5+
import { ControlItemComponent } from '../components/ControlItemComponent';
6+
import { StashItemComponent } from '../components/StashItemComponent';
7+
import { Visible } from '../components/Visible';
8+
import { ControlStashConfig } from '../CseMachineControlStashConfig';
9+
import {
10+
defaultActiveColor,
11+
defaultDangerColor,
12+
defaultStrokeColor,
13+
getTextWidth
14+
} from '../CseMachineUtils';
15+
import { Animatable, AnimationConfig } from './base/Animatable';
16+
import { AnimatedGenericArrow } from './base/AnimatedGenericArrow';
17+
import { AnimatedTextbox } from './base/AnimatedTextbox';
18+
import { getNodePosition } from './base/AnimationUtils';
19+
20+
/**
21+
* Adapted from InstructionApplicationAnimation, but changed resultAnimation to [], among others
22+
*/
23+
export class ArraySpreadAnimation extends Animatable {
24+
private controlInstrAnimation: AnimatedTextbox; // the array literal control item
25+
private stashItemAnimation: AnimatedTextbox;
26+
private resultAnimations: AnimatedTextbox[];
27+
private arrowAnimation?: AnimatedGenericArrow<StashItemComponent, Visible>;
28+
private currCallInstrAnimation: AnimatedTextbox;
29+
30+
private endX: number;
31+
32+
constructor(
33+
private controlInstrItem: ControlItemComponent,
34+
private stashItem: StashItemComponent,
35+
private resultItems: StashItemComponent[],
36+
private currCallInstrItem: ControlItemComponent
37+
) {
38+
super();
39+
40+
this.endX = stashItem!.x() + stashItem!.width();
41+
this.controlInstrAnimation = new AnimatedTextbox(
42+
controlInstrItem.text,
43+
getNodePosition(controlInstrItem),
44+
{ rectProps: { stroke: defaultActiveColor() } }
45+
);
46+
this.stashItemAnimation = new AnimatedTextbox(stashItem.text, getNodePosition(stashItem), {
47+
rectProps: {
48+
stroke: defaultDangerColor()
49+
}
50+
});
51+
52+
// call instr above
53+
this.currCallInstrAnimation = new AnimatedTextbox(
54+
this.currCallInstrItem.text,
55+
getNodePosition(this.currCallInstrItem),
56+
{ rectProps: { stroke: defaultActiveColor() } }
57+
);
58+
59+
this.resultAnimations = resultItems.map(item => {
60+
return new AnimatedTextbox(item.text, {
61+
...getNodePosition(item),
62+
opacity: 0
63+
});
64+
});
65+
if (stashItem.arrow) {
66+
this.arrowAnimation = new AnimatedGenericArrow(stashItem.arrow, { opacity: 0 });
67+
}
68+
}
69+
70+
draw(): React.ReactNode {
71+
return (
72+
<Group ref={this.ref} key={Animatable.key--}>
73+
{this.controlInstrAnimation.draw()}
74+
{this.stashItemAnimation.draw()}
75+
{this.currCallInstrAnimation.draw()}
76+
{this.resultAnimations.map(a => a.draw())}
77+
{this.arrowAnimation?.draw()}
78+
</Group>
79+
);
80+
}
81+
82+
async animate(animationConfig?: AnimationConfig) {
83+
this.resultItems?.map(a => a.ref.current?.hide());
84+
this.resultItems?.map(a => a.arrow?.ref.current?.hide());
85+
const minInstrWidth =
86+
getTextWidth(this.controlInstrItem.text) + ControlStashConfig.ControlItemTextPadding * 2;
87+
const resultX = (idx: number) => this.resultItems[idx]?.x() ?? this.stashItem.x();
88+
const resultY = this.resultItems[0]?.y() ?? this.stashItem.y();
89+
const startX = resultX(0);
90+
const fadeDuration = ((animationConfig?.duration ?? 1) * 3) / 4;
91+
const fadeInDelay = (animationConfig?.delay ?? 0) + (animationConfig?.duration ?? 1) / 4;
92+
93+
// Move spread instruction next to stash item (array pointer)
94+
await Promise.all([
95+
...this.resultAnimations.flatMap(a => [
96+
a.animateTo(
97+
{ x: startX + (this.endX - startX) / 2 - this.resultItems[0]?.width() / 2 },
98+
{ duration: 0 }
99+
)
100+
]),
101+
this.controlInstrAnimation.animateRectTo({ stroke: defaultStrokeColor() }, animationConfig),
102+
this.controlInstrAnimation.animateTo(
103+
{
104+
x: startX,
105+
y: resultY + (this.resultItems[0]?.height() ?? this.stashItem.height()),
106+
width: minInstrWidth
107+
},
108+
animationConfig
109+
),
110+
this.stashItemAnimation.animateRectTo({ stroke: defaultDangerColor() }, animationConfig)
111+
]);
112+
113+
animationConfig = { ...animationConfig, delay: 0 };
114+
// Merge all elements together to form the result
115+
await Promise.all([
116+
this.controlInstrAnimation.animateTo({ x: resultX(0), y: resultY }, animationConfig),
117+
this.controlInstrAnimation.animateTo(
118+
{ opacity: 0 },
119+
{ ...animationConfig, duration: fadeDuration }
120+
),
121+
this.stashItemAnimation.animateTo({ x: resultX(0) }, animationConfig),
122+
this.stashItemAnimation.animateTo(
123+
{ opacity: 0 },
124+
{ ...animationConfig, duration: fadeDuration }
125+
),
126+
127+
...this.resultAnimations.flatMap((a, idx) => [
128+
a.animateTo({ x: resultX(idx) }, animationConfig),
129+
a.animateRectTo({ stroke: defaultDangerColor() }, animationConfig),
130+
a.animateTo(
131+
{ opacity: 1 },
132+
{ ...animationConfig, duration: fadeDuration, delay: fadeInDelay }
133+
)
134+
])
135+
]);
136+
137+
this.destroy();
138+
}
139+
140+
destroy() {
141+
this.ref.current?.hide();
142+
this.resultItems.map(a => a.ref.current?.show());
143+
this.resultItems.map(a => a.arrow?.ref.current?.show());
144+
this.controlInstrAnimation.destroy();
145+
this.stashItemAnimation.destroy();
146+
this.resultAnimations.map(a => a.destroy());
147+
this.arrowAnimation?.destroy();
148+
}
149+
}

src/i18n/locales/en/login.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"Logging In": "Logging In...",
3-
"Log in with": "Log in with {{name}}"
2+
"Logging In": "Logging In...",
3+
"Log in with": "Log in with {{name}}"
44
}

src/i18n/locales/pseudo/login.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"Logging In": "Lògging Ìn...",
3-
"Log in with": "Lòg in with {{name}}"
2+
"Logging In": "Lògging Ìn...",
3+
"Log in with": "Lòg in with {{name}}"
44
}

0 commit comments

Comments
 (0)