Skip to content

CSE Machine: Added animation for spread instruction #3114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ env:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
lint:
if: github.event.pull_request.draft == false
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ To start contributing, create a fork from our repo and send a PR. Refer to [this

The frontend comes with an extensive test suite. To run the tests after you made your modifications, run
`yarn test`. Regression tests are run automatically when you want to push changes to this repository.
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:
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:

```bash
yarn test --updateSnapshot
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ The Source Academy (<https://sourceacademy.org/>) is an immersive online experie

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

1. Install the version of `yarn` as specified in `package.json`, `packageManager`.
1. Install the version of `yarn` as specified in `package.json`, `packageManager`.

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

Expand Down
2 changes: 1 addition & 1 deletion _config.yml
Original file line number Diff line number Diff line change
@@ -1 +1 @@
theme: jekyll-theme-cayman
theme: jekyll-theme-cayman
4 changes: 2 additions & 2 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { config, configs } from 'typescript-eslint';
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
import simpleImportSort from 'eslint-plugin-simple-import-sort'
import simpleImportSort from 'eslint-plugin-simple-import-sort';
// import reactRefresh from 'eslint-plugin-react-refresh';

export default config(
Expand All @@ -24,7 +24,7 @@ export default config(
files: ['**/*.ts*'],
plugins: {
'react-hooks': reactHooksPlugin,
'react': reactPlugin,
react: reactPlugin,
'simple-import-sort': simpleImportSort
},
rules: {
Expand Down
6 changes: 3 additions & 3 deletions public/externalLibs/sound/soundToneMatrix.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ var timeout_matrix;
// for coloring the matrix accordingly while it's being played
var timeout_color;

var timeout_objects = new Array();
var timeout_objects = [];

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

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

timeout_objects = new Array();
timeout_objects = [];
}
8 changes: 4 additions & 4 deletions public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@
"type": "image/png"
},
{
"src": "icons/android-chrome-256x256.png",
"sizes": "256x256",
"type": "image/png"
"src": "icons/android-chrome-256x256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "icons/maskable.png",
"sizes": "196x196",
"type": "image/png",
"purpose": "maskable"
}
}
],
"start_url": "./",
"display": "standalone",
Expand Down
35 changes: 35 additions & 0 deletions src/features/cseMachine/CseMachineAnimation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import React from 'react';

import { ArrayAccessAnimation } from './animationComponents/ArrayAccessAnimation';
import { ArrayAssignmentAnimation } from './animationComponents/ArrayAssignmentAnimation';
import { ArraySpreadAnimation } from './animationComponents/ArraySpreadAnimation';
import { AssignmentAnimation } from './animationComponents/AssignmentAnimation';
import { Animatable } from './animationComponents/base/Animatable';
import { lookupBinding } from './animationComponents/base/AnimationUtils';
Expand Down Expand Up @@ -112,6 +113,11 @@ export class CseAnimation {
);
}
break;
case 'SpreadElement':
CseAnimation.animations.push(
new ControlExpansionAnimation(lastControlComponent, CseAnimation.getNewControlItems())
);
break;
case 'AssignmentExpression':
case 'ArrayExpression':
case 'BinaryExpression':
Expand Down Expand Up @@ -276,6 +282,35 @@ export class CseAnimation {
)
);
break;
case InstrType.SPREAD:
const control = Layout.controlComponent.stackItemComponents;
const array = Layout.previousStashComponent.stashItemComponents.at(-1)!.arrow!
.target! as ArrayValue;

let currCallInstr;

for (let i = 1; control.at(-i) != undefined; i++) {
if (control.at(-i)?.text.includes('call ')) {
// find call instr above
currCallInstr = control.at(-i);
break;
}
}

const resultItems =
array.data.length !== 0
? Layout.stashComponent.stashItemComponents.slice(-array.data.length)
: [];

CseAnimation.animations.push(
new ArraySpreadAnimation(
lastControlComponent,
Layout.previousStashComponent.stashItemComponents.at(-1)!,
resultItems!,
currCallInstr!
)
);
break;
case InstrType.ARRAY_LENGTH:
case InstrType.BREAK:
case InstrType.BREAK_MARKER:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export class ArrayAccessAnimation extends Animatable {
private resultAnimation: AnimatedTextbox;
private resultArrowAnimation?: AnimatedGenericArrow<StashItemComponent, Visible>;
private arrayUnit: ArrayUnit;
private outOfRange: boolean;

constructor(
private accInstr: ControlItemComponent,
Expand All @@ -46,12 +47,29 @@ export class ArrayAccessAnimation extends Animatable {
rectProps: { stroke: defaultDangerColor() }
});
this.arrayArrowAnimation = new AnimatedGenericArrow(arrayItem.arrow!);
// if index is out of range
this.outOfRange = false;
// the target should always be an array value
const array = arrayItem.arrow!.target! as ArrayValue;
this.arrayUnit = array.units[parseInt(indexItem.text)];

// if index access is out of range. if index access is negative, error should be thrown from js-slang at this point
const arraylen = array.data.length;

if (parseInt(indexItem.text) >= arraylen) {
this.outOfRange = true;
this.arrayUnit = array.units[arraylen - 1];
} else {
this.arrayUnit = array.units[parseInt(indexItem.text)];
}

this.resultAnimation = new AnimatedTextbox(resultItem.text, {
...getNodeDimensions(resultItem),
x: this.arrayUnit.x() + this.arrayUnit.width() / 2 - this.resultItem.width() / 2,
// if array index out of range, animate to one unit beyond array
x:
this.arrayUnit.x() +
this.arrayUnit.width() / 2 -
this.resultItem.width() / 2 +
(this.outOfRange ? this.arrayUnit.width() : 0),
y: this.arrayUnit.y() + this.arrayUnit.height() / 2 - this.resultItem.height() / 2,
opacity: 0
});
Expand Down Expand Up @@ -79,7 +97,12 @@ export class ArrayAccessAnimation extends Animatable {
const minInstrItemWidth =
getTextWidth(this.accInstr.text) + ControlStashConfig.ControlItemTextPadding * 2;
const indexAboveArrayLocation = {
x: this.arrayUnit.x() + this.arrayUnit.width() / 2 - this.indexItem.width() / 2,
// if array index out of range, animate to one unit beyond array
x:
this.arrayUnit.x() +
this.arrayUnit.width() / 2 -
this.indexItem.width() / 2 +
(this.outOfRange ? this.arrayUnit.width() : 0),
y: this.arrayUnit.y() - this.indexItem.height() - 8
};
const indexInArrayLocation = {
Expand Down
149 changes: 149 additions & 0 deletions src/features/cseMachine/animationComponents/ArraySpreadAnimation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
//import { Easings } from 'konva/lib/Tween';
import React from 'react';
import { Group } from 'react-konva';

import { ControlItemComponent } from '../components/ControlItemComponent';
import { StashItemComponent } from '../components/StashItemComponent';
import { Visible } from '../components/Visible';
import { ControlStashConfig } from '../CseMachineControlStashConfig';
import {
defaultActiveColor,
defaultDangerColor,
defaultStrokeColor,
getTextWidth
} from '../CseMachineUtils';
import { Animatable, AnimationConfig } from './base/Animatable';
import { AnimatedGenericArrow } from './base/AnimatedGenericArrow';
import { AnimatedTextbox } from './base/AnimatedTextbox';
import { getNodePosition } from './base/AnimationUtils';

/**
* Adapted from InstructionApplicationAnimation, but changed resultAnimation to [], among others
*/
export class ArraySpreadAnimation extends Animatable {
private controlInstrAnimation: AnimatedTextbox; // the array literal control item
private stashItemAnimation: AnimatedTextbox;
private resultAnimations: AnimatedTextbox[];
private arrowAnimation?: AnimatedGenericArrow<StashItemComponent, Visible>;
private currCallInstrAnimation: AnimatedTextbox;

private endX: number;

constructor(
private controlInstrItem: ControlItemComponent,
private stashItem: StashItemComponent,
private resultItems: StashItemComponent[],
private currCallInstrItem: ControlItemComponent
) {
super();

this.endX = stashItem!.x() + stashItem!.width();
this.controlInstrAnimation = new AnimatedTextbox(
controlInstrItem.text,
getNodePosition(controlInstrItem),
{ rectProps: { stroke: defaultActiveColor() } }
);
this.stashItemAnimation = new AnimatedTextbox(stashItem.text, getNodePosition(stashItem), {
rectProps: {
stroke: defaultDangerColor()
}
});

// call instr above
this.currCallInstrAnimation = new AnimatedTextbox(
this.currCallInstrItem.text,
getNodePosition(this.currCallInstrItem),
{ rectProps: { stroke: defaultActiveColor() } }
);

this.resultAnimations = resultItems.map(item => {
return new AnimatedTextbox(item.text, {
...getNodePosition(item),
opacity: 0
});
});
if (stashItem.arrow) {
this.arrowAnimation = new AnimatedGenericArrow(stashItem.arrow, { opacity: 0 });
}
}

draw(): React.ReactNode {
return (
<Group ref={this.ref} key={Animatable.key--}>
{this.controlInstrAnimation.draw()}
{this.stashItemAnimation.draw()}
{this.currCallInstrAnimation.draw()}
{this.resultAnimations.map(a => a.draw())}
{this.arrowAnimation?.draw()}
</Group>
);
}

async animate(animationConfig?: AnimationConfig) {
this.resultItems?.map(a => a.ref.current?.hide());
this.resultItems?.map(a => a.arrow?.ref.current?.hide());
const minInstrWidth =
getTextWidth(this.controlInstrItem.text) + ControlStashConfig.ControlItemTextPadding * 2;
const resultX = (idx: number) => this.resultItems[idx]?.x() ?? this.stashItem.x();
const resultY = this.resultItems[0]?.y() ?? this.stashItem.y();
const startX = resultX(0);
const fadeDuration = ((animationConfig?.duration ?? 1) * 3) / 4;
const fadeInDelay = (animationConfig?.delay ?? 0) + (animationConfig?.duration ?? 1) / 4;

// Move spread instruction next to stash item (array pointer)
await Promise.all([
...this.resultAnimations.flatMap(a => [
a.animateTo(
{ x: startX + (this.endX - startX) / 2 - this.resultItems[0]?.width() / 2 },
{ duration: 0 }
)
]),
this.controlInstrAnimation.animateRectTo({ stroke: defaultStrokeColor() }, animationConfig),
this.controlInstrAnimation.animateTo(
{
x: startX,
y: resultY + (this.resultItems[0]?.height() ?? this.stashItem.height()),
width: minInstrWidth
},
animationConfig
),
this.stashItemAnimation.animateRectTo({ stroke: defaultDangerColor() }, animationConfig)
]);

animationConfig = { ...animationConfig, delay: 0 };
// Merge all elements together to form the result
await Promise.all([
this.controlInstrAnimation.animateTo({ x: resultX(0), y: resultY }, animationConfig),
this.controlInstrAnimation.animateTo(
{ opacity: 0 },
{ ...animationConfig, duration: fadeDuration }
),
this.stashItemAnimation.animateTo({ x: resultX(0) }, animationConfig),
this.stashItemAnimation.animateTo(
{ opacity: 0 },
{ ...animationConfig, duration: fadeDuration }
),

...this.resultAnimations.flatMap((a, idx) => [
a.animateTo({ x: resultX(idx) }, animationConfig),
a.animateRectTo({ stroke: defaultDangerColor() }, animationConfig),
a.animateTo(
{ opacity: 1 },
{ ...animationConfig, duration: fadeDuration, delay: fadeInDelay }
)
])
]);

this.destroy();
}

destroy() {
this.ref.current?.hide();
this.resultItems.map(a => a.ref.current?.show());
this.resultItems.map(a => a.arrow?.ref.current?.show());
this.controlInstrAnimation.destroy();
this.stashItemAnimation.destroy();
this.resultAnimations.map(a => a.destroy());
this.arrowAnimation?.destroy();
}
}
4 changes: 2 additions & 2 deletions src/i18n/locales/en/login.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"Logging In": "Logging In...",
"Log in with": "Log in with {{name}}"
"Logging In": "Logging In...",
"Log in with": "Log in with {{name}}"
}
4 changes: 2 additions & 2 deletions src/i18n/locales/pseudo/login.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"Logging In": "Lògging Ìn...",
"Log in with": "Lòg in with {{name}}"
"Logging In": "Lògging Ìn...",
"Log in with": "Lòg in with {{name}}"
}