Skip to content

Commit 87e646d

Browse files
CZX123notnotmaxRichDom2185martin-henz
authored
CSE UI/UX: Animations & UI improvements (#2931)
* display array indices * animate moving numbers from agenda to stash * add code to read from previous agenda * Add animation components and some abstraction * Change test cases * Fix bug Properly disable animations when control and stash option is not enabled * display array indices * animate moving numbers from agenda to stash * add code to read from previous agenda * Add animation components and some abstraction * Change test cases * Fix bug Properly disable animations when control and stash option is not enabled * Fix issues with names after rebase * Fix animation bugs and refactoring of animation classes and logic * Fix mistake in test snapshot * Revert "Merge branch 'cse-uiux' of https://github.com/source-academy/frontend into cse-uiux" This reverts commit 7ef87d8, reversing changes made to efa8c57. * Restructure animation classes * Add binary operator animation * Add unary operator animation * Begin work on block separation animation * Improve binary operation animation, and improve the versatility of the base animation components * Improve the unary operation and block animations * Update test cases and remove block animation conditions * Add pop animation (linear movement) * Improve pop animation, and cleanup code for pull request * Revert envVisualizer test snapshot changes * Add assignment animation * Work on binding lookup function * Improve assignment animation and touch up on other animations, added experimental Column component * Add lookup animation (Identifier) * Improve lookup animation * Hide arrows for lookup animation and show them when it's finished * Remove AnimationUtils.tsx file replaced with AnimationUtils.ts * Slow down assignment and lookup animations * Fix issues with merging * Add environment animation * Refactor setDestination and animate to new method animateTo * Improve env animation and rename a utility function * Added animated arrows and also modified GenericArrow and improve assignment animation * Update test snapshot and formatting changes * Add arrow animation to lookup animation, and fix animateTo function behavior * Add FrameCreationAnimation * Rewrote all animation components for greater flexibility and performance * Shorten duration and delay names, and add listener functionality * Move experimental file * Fix issues with `this` keyword * Rewrite AnimatedTextbox to make it easier for both Text and Rect within it to be individually animated * Improve FrameCreationAnimation and bunch of other fixes * Fix some issues with `undefined` inside the control and stash * Make compact components the new default and remove any mentions to the old components. Also removes the experimental button toggle. * Update test snapshots * Clean up testing code a little * Formatting changes * Fix issues after merge * Revert some incorrect merges * Add FunctionFrameCreationAnimation if possible, reusing AssignmentAnimation would make handling fade-in of values easier * Add getNodeDimensions and getNodeLocation * Add ArrowFunctionExpressionAnimation * Add BranchAnimation simple animation for replacing a branch item in the control with the correct code block * Sort instr types in alphabetical order * Add ArrayLiteralAnimation * Added cases for block splitting for/while loops and conditional expressions * Improve animations for arrow functions and branch instruction * Update function application animation * Special changes for js-slang branch: add new objectCount property and filter program bindings * Change dummy binding behavior to match js-slang update * Fix array references being lost in frames due to cloning property descriptors * Simplify code * Add ArrayAccessAnimation * Begin work on ArrayAssignmentAnimation todo: animate arrows for object assignment * Fix application animation for predeclared funcs * Changes to solve issue 2700 and some fixes regarding global frame * Re-add animations * Fix many issues regarding displaying objects on global frame * Formatting * Simplify merging of environment heaps and drawing of bindings * Fix infinite loop in `findObjects` * Disable animations if control is truncated Could allow such functionality in the future, but currently animations break with a truncated control. * Fix AssignmentAnimation previous use of binding.height() causes positioning issues with nested array assignment * Increase space between closure and frame * Change for/while instr to use BranchAnimation * Initial Commit * More fixes and added faded gc objects * Disable variadic function animation * Increase spacing for global closure between closure circles and global frame border * Add missing case 'FunctionExpression' animates the moving of a functionexpression to the stash as a closure * Fix variadic function checker * Improve FunctionApplicationAnimation for nullary functions, allow the closure stash item to fade away * Improve assignment animation * Run format * Simplify check frame creation * Fix params text and added SourceObject to display runes correctly * bumping js-slang * Revamp unreferenced behavior and update snapshots * Run format * Update snapshot * Bump js-slang * Fix issues after merge * Fix frames creeping to the left * Improve rune display, revamp color system and add color interpolation * UI & animation improvements * Big improvements for many animations * More improvements, better transitions between border colors * More improvements and update snapshot * Update types * Remove explicit fragment * Remove explicit fragment again * Update typings * Merge branch 'cse-uiux2' of https://github.com/source-academy/frontend into cse-uiux2 * Fix findObjects * Add docs * Re-add color dependencies for Java CSE * Re-add color dependencies for Java CSE * Fix format * Array access & asgn animation improvements, general polish * Cleanup and update java cse machine colors to use new functions * Move type helpers from animation utils to global type helpers file * Run format * Fix fn to frame arrow, and cleanup arrow code --------- Co-authored-by: notnotmax <156508404+notnotmax@users.noreply.github.com> Co-authored-by: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Co-authored-by: henz <henz@comp.nus.edu.sg>
1 parent 4f6be6b commit 87e646d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+3129
-1152
lines changed

src/commons/utils/JsSlangHelper.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export function visualizeCseMachine({ context }: { context: Context }) {
9595
CseMachine.drawCse(context);
9696
} catch (err) {
9797
console.error(err);
98+
throw new Error('CSE machine is not enabled');
9899
}
99100
}
100101

src/commons/utils/TypeHelper.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,22 @@ export type ActionType<T extends Record<string, any>> = {
1515
[k in keyof T]: ReturnType<T[k]>;
1616
}[keyof T];
1717

18+
/** Omits the index signature `[key: string]: any;` from type `T` */
19+
export type RemoveIndex<T> = {
20+
[K in keyof T as string extends K
21+
? never
22+
: number extends K
23+
? never
24+
: symbol extends K
25+
? never
26+
: K]: T[K];
27+
};
28+
29+
/** A true intersection of the properties of types `A` and `B`, unlike the confusingly named
30+
* "Intersection Types" in TypeScript which uses the `&` operator and are actually unions.
31+
* This also excludes the index signature from both `A` and `B` automatically. */
32+
export type SharedProperties<A, B> = Pick<A, Extract<keyof RemoveIndex<A>, keyof RemoveIndex<B>>>;
33+
1834
/* =========================================
1935
* Utility types for tuple type manipulation
2036
* ========================================= */
Lines changed: 244 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,44 @@
1-
import { InstrType } from 'js-slang/dist/cse-machine/types';
1+
import { AppInstr, ArrLitInstr, AssmtInstr, InstrType } from 'js-slang/dist/cse-machine/types';
2+
import { Node } from 'js-slang/dist/types';
3+
import { Layer } from 'konva/lib/Layer';
24
import { Easings } from 'konva/lib/Tween';
5+
import React from 'react';
36

4-
import { Animatable } from './animationComponents/AnimationComponents';
7+
import { ArrayAccessAnimation } from './animationComponents/ArrayAccessAnimation';
8+
import { ArrayAssignmentAnimation } from './animationComponents/ArrayAssignmentAnimation';
9+
import { AssignmentAnimation } from './animationComponents/AssignmentAnimation';
10+
import { Animatable } from './animationComponents/base/Animatable';
11+
import { lookupBinding } from './animationComponents/base/AnimationUtils';
512
import { BinaryOperationAnimation } from './animationComponents/BinaryOperationAnimation';
6-
import { BlockAnimation } from './animationComponents/BlockAnimation';
7-
import { LiteralAnimation } from './animationComponents/LiteralAnimation';
13+
import { BranchAnimation } from './animationComponents/BranchAnimation';
14+
import { ControlExpansionAnimation } from './animationComponents/ControlExpansionAnimation';
15+
import { ControlToStashAnimation } from './animationComponents/ControlToStashAnimation';
16+
import { EnvironmentAnimation } from './animationComponents/EnvironmentAnimation';
17+
import { FrameCreationAnimation } from './animationComponents/FrameCreationAnimation';
18+
import { FunctionApplicationAnimation } from './animationComponents/FunctionApplicationAnimation';
19+
import { InstructionApplicationAnimation } from './animationComponents/InstructionApplicationAnimation';
20+
import { LookupAnimation } from './animationComponents/LookupAnimation';
821
import { PopAnimation } from './animationComponents/PopAnimation';
922
import { UnaryOperationAnimation } from './animationComponents/UnaryOperationAnimation';
10-
import { isInstr } from './components/ControlStack';
23+
import { isNode } from './components/ControlStack';
24+
import { Frame } from './components/Frame';
25+
import { ArrayValue } from './components/values/ArrayValue';
1126
import CseMachine from './CseMachine';
1227
import { Layout } from './CseMachineLayout';
28+
import { isBuiltInFn, isStreamFn } from './CseMachineUtils';
1329

1430
export class CseAnimation {
15-
private static animationEnabled = false;
16-
static readonly animationComponents: Animatable[] = [];
17-
static readonly defaultDuration = 0.3;
31+
static readonly animations: Animatable[] = [];
32+
static readonly defaultDuration = 300;
1833
static readonly defaultEasing = Easings.StrongEaseInOut;
34+
private static animationEnabled = false;
35+
private static currentFrame: Frame;
36+
private static previousFrame: Frame;
37+
38+
static layerRef = React.createRef<Layer>();
39+
static getLayer(): Layer | null {
40+
return this.layerRef.current;
41+
}
1942

2043
static enableAnimations(): void {
2144
CseAnimation.animationEnabled = true;
@@ -25,99 +48,258 @@ export class CseAnimation {
2548
CseAnimation.animationEnabled = false;
2649
}
2750

51+
static setCurrentFrame(frame: Frame) {
52+
CseAnimation.previousFrame = CseAnimation.currentFrame;
53+
CseAnimation.currentFrame = frame;
54+
}
55+
2856
private static clearAnimationComponents(): void {
29-
CseAnimation.animationComponents.length = 0;
57+
CseAnimation.animations.length = 0;
58+
}
59+
60+
private static getNewControlItems() {
61+
const currentControlSize = Layout.controlComponent.control.size();
62+
const previousControlSize = Layout.previousControlComponent.control.size();
63+
const numOfItems = currentControlSize - previousControlSize + 1;
64+
if (numOfItems <= 0) return [];
65+
return Layout.controlComponent.stackItemComponents.slice(-numOfItems);
66+
}
67+
68+
private static handleNode(node: Node) {
69+
const lastControlComponent = Layout.previousControlComponent.stackItemComponents.at(-1)!;
70+
const currStashComponent = Layout.stashComponent.stashItemComponents.at(-1)!;
71+
switch (node.type) {
72+
case 'Program':
73+
CseAnimation.animations.push(
74+
new ControlExpansionAnimation(lastControlComponent, CseAnimation.getNewControlItems())
75+
);
76+
if (CseMachine.getCurrentEnvId() !== '-1') {
77+
CseAnimation.animations.push(
78+
new FrameCreationAnimation(lastControlComponent, CseAnimation.currentFrame)
79+
);
80+
}
81+
break;
82+
case 'BlockStatement':
83+
CseAnimation.animations.push(
84+
new ControlExpansionAnimation(lastControlComponent, CseAnimation.getNewControlItems()),
85+
new FrameCreationAnimation(lastControlComponent, CseAnimation.currentFrame)
86+
);
87+
break;
88+
case 'Literal':
89+
CseAnimation.animations.push(
90+
new ControlToStashAnimation(lastControlComponent, currStashComponent!)
91+
);
92+
break;
93+
case 'ArrowFunctionExpression':
94+
CseAnimation.animations.push(
95+
new ControlToStashAnimation(lastControlComponent, currStashComponent!)
96+
);
97+
break;
98+
case 'Identifier':
99+
// Special case for 'undefined' identifier
100+
if (node.name === 'undefined') {
101+
CseAnimation.animations.push(
102+
new ControlToStashAnimation(lastControlComponent, currStashComponent!)
103+
);
104+
} else {
105+
CseAnimation.animations.push(
106+
new LookupAnimation(
107+
lastControlComponent,
108+
currStashComponent!,
109+
...lookupBinding(CseAnimation.currentFrame, node.name)
110+
)
111+
);
112+
}
113+
break;
114+
case 'AssignmentExpression':
115+
case 'ArrayExpression':
116+
case 'BinaryExpression':
117+
case 'CallExpression':
118+
case 'ConditionalExpression':
119+
case 'ForStatement':
120+
case 'IfStatement':
121+
case 'MemberExpression':
122+
case 'ReturnStatement':
123+
case 'StatementSequence':
124+
case 'UnaryExpression':
125+
case 'VariableDeclaration':
126+
case 'FunctionDeclaration':
127+
case 'WhileStatement':
128+
CseAnimation.animations.push(
129+
new ControlExpansionAnimation(lastControlComponent, CseAnimation.getNewControlItems())
130+
);
131+
break;
132+
case 'ExpressionStatement':
133+
CseAnimation.handleNode(node.expression);
134+
break;
135+
}
30136
}
31137

32138
static updateAnimation() {
33-
CseAnimation.animationComponents.forEach(a => a.destroy());
139+
CseAnimation.animations.forEach(a => a.destroy());
34140
CseAnimation.clearAnimationComponents();
35141

36142
if (!Layout.previousControlComponent) return;
37143
const lastControlItem = Layout.previousControlComponent.control.peek();
38144
const lastControlComponent = Layout.previousControlComponent.stackItemComponents.at(-1);
145+
const currStashComponent = Layout.stashComponent.stashItemComponents.at(-1);
39146
if (
40147
!CseAnimation.animationEnabled ||
41148
!lastControlItem ||
42149
!lastControlComponent ||
43-
!CseMachine.getControlStash() // TODO: handle cases where there are environment animations
150+
!CseMachine.getControlStash() // TODO: handle cases where there are only environment animations
44151
) {
45152
return;
46153
}
47-
let animation: Animatable | undefined;
48-
if (!isInstr(lastControlItem)) {
49-
switch (lastControlItem.type) {
50-
case 'Literal':
51-
animation = new LiteralAnimation(
52-
lastControlComponent,
53-
Layout.stashComponent.stashItemComponents.at(-1)!
154+
if (isNode(lastControlItem)) {
155+
CseAnimation.handleNode(lastControlItem);
156+
} else {
157+
switch (lastControlItem.instrType) {
158+
case InstrType.APPLICATION:
159+
const appInstr = lastControlItem as AppInstr;
160+
const fnStashItem = Layout.previousStashComponent.stashItemComponents.at(
161+
-appInstr.numOfArgs - 1
162+
)!;
163+
const fn = fnStashItem.value;
164+
if (isBuiltInFn(fn) || isStreamFn(fn)) {
165+
CseAnimation.animations.push(
166+
new InstructionApplicationAnimation(
167+
lastControlComponent,
168+
Layout.previousStashComponent.stashItemComponents.slice(-appInstr.numOfArgs - 1),
169+
currStashComponent!
170+
)
171+
);
172+
break;
173+
}
174+
const frameCreated = appInstr.numOfArgs > 0;
175+
176+
CseAnimation.animations.push(
177+
new FunctionApplicationAnimation(
178+
lastControlComponent,
179+
CseAnimation.getNewControlItems(),
180+
fnStashItem,
181+
Layout.previousStashComponent.stashItemComponents.slice(-appInstr.numOfArgs),
182+
frameCreated ? CseAnimation.currentFrame : undefined
183+
)
54184
);
55185
break;
56-
case 'Program':
57-
case 'ExpressionStatement':
58-
case 'VariableDeclaration':
59-
const currentControlSize = Layout.controlComponent.control.size();
60-
const previousControlSize = Layout.previousControlComponent.control.size();
61-
const numOfItems = currentControlSize - previousControlSize + 1;
62-
if (numOfItems <= 0) break;
63-
const targetItems = Array.from({ length: numOfItems }, (_, i) => {
64-
return Layout.controlComponent.stackItemComponents[previousControlSize + i - 1];
65-
});
66-
animation = new BlockAnimation(lastControlComponent, targetItems);
186+
case InstrType.ARRAY_ACCESS:
187+
CseAnimation.animations.push(
188+
new ArrayAccessAnimation(
189+
lastControlComponent,
190+
Layout.previousStashComponent.stashItemComponents.at(-2)!,
191+
Layout.previousStashComponent.stashItemComponents.at(-1)!,
192+
Layout.stashComponent.stashItemComponents.at(-1)!
193+
)
194+
);
67195
break;
68-
}
69-
} else {
70-
switch (lastControlItem.instrType) {
71-
case InstrType.RESET:
72-
case InstrType.WHILE:
73-
case InstrType.FOR:
74-
case InstrType.ASSIGNMENT:
196+
case InstrType.ARRAY_ASSIGNMENT:
197+
const arrayItem = Layout.previousStashComponent.stashItemComponents.at(-3)!;
198+
CseAnimation.animations.push(
199+
new ArrayAssignmentAnimation(
200+
lastControlComponent,
201+
arrayItem,
202+
Layout.values.get(arrayItem.value.id) as ArrayValue,
203+
Layout.previousStashComponent.stashItemComponents.at(-2)!,
204+
Layout.previousStashComponent.stashItemComponents.at(-1)!,
205+
Layout.stashComponent.stashItemComponents.at(-1)!
206+
)
207+
);
75208
break;
76-
case InstrType.UNARY_OP:
77-
animation = new UnaryOperationAnimation(
78-
lastControlComponent,
79-
Layout.previousStashComponent.stashItemComponents.at(-1)!,
80-
Layout.stashComponent.stashItemComponents.at(-1)!
209+
case InstrType.ARRAY_LITERAL:
210+
const arrSize = (lastControlItem as ArrLitInstr).arity;
211+
CseAnimation.animations.push(
212+
new InstructionApplicationAnimation(
213+
lastControlComponent,
214+
Layout.previousStashComponent.stashItemComponents.slice(-arrSize),
215+
currStashComponent!
216+
)
81217
);
82218
break;
83-
case InstrType.BINARY_OP:
84-
animation = new BinaryOperationAnimation(
85-
lastControlComponent,
86-
Layout.previousStashComponent.stashItemComponents.at(-2)!,
87-
Layout.previousStashComponent.stashItemComponents.at(-1)!,
88-
Layout.stashComponent.stashItemComponents.at(-1)!
219+
case InstrType.ASSIGNMENT:
220+
CseAnimation.animations.push(
221+
new AssignmentAnimation(
222+
lastControlComponent,
223+
currStashComponent!,
224+
...lookupBinding(CseAnimation.currentFrame, (lastControlItem as AssmtInstr).symbol)
225+
)
89226
);
90227
break;
91-
case InstrType.POP:
92-
animation = new PopAnimation(
93-
lastControlComponent,
94-
Layout.previousStashComponent.stashItemComponents.at(-1)!
228+
case InstrType.BINARY_OP:
229+
CseAnimation.animations.push(
230+
new BinaryOperationAnimation(
231+
lastControlComponent,
232+
Layout.previousStashComponent.stashItemComponents.at(-2)!,
233+
Layout.previousStashComponent.stashItemComponents.at(-1)!,
234+
currStashComponent!
235+
)
95236
);
96237
break;
97-
case InstrType.APPLICATION:
98238
case InstrType.BRANCH:
239+
case InstrType.FOR:
240+
case InstrType.WHILE:
241+
CseAnimation.animations.push(
242+
new BranchAnimation(
243+
lastControlComponent,
244+
Layout.previousStashComponent.stashItemComponents.at(-1)!,
245+
CseAnimation.getNewControlItems()
246+
)
247+
);
248+
break;
99249
case InstrType.ENVIRONMENT:
100-
case InstrType.ARRAY_LITERAL:
101-
case InstrType.ARRAY_ACCESS:
102-
case InstrType.ARRAY_ASSIGNMENT:
250+
CseAnimation.animations.push(
251+
new EnvironmentAnimation(CseAnimation.previousFrame, CseAnimation.currentFrame)
252+
);
253+
break;
254+
case InstrType.POP:
255+
const currentStashSize = Layout.stashComponent.stash.size();
256+
const previousStashSize = Layout.previousStashComponent.stash.size();
257+
const lastStashIsUndefined =
258+
currentStashSize === 1 &&
259+
currStashComponent!.text === 'undefined' &&
260+
currentStashSize === previousStashSize;
261+
CseAnimation.animations.push(
262+
new PopAnimation(
263+
lastControlComponent,
264+
Layout.previousStashComponent.stashItemComponents.at(-1)!,
265+
lastStashIsUndefined ? currStashComponent : undefined
266+
)
267+
);
268+
break;
269+
case InstrType.UNARY_OP:
270+
CseAnimation.animations.push(
271+
new UnaryOperationAnimation(
272+
lastControlComponent,
273+
Layout.previousStashComponent.stashItemComponents.at(-1)!,
274+
currStashComponent!
275+
)
276+
);
277+
break;
103278
case InstrType.ARRAY_LENGTH:
104-
case InstrType.CONTINUE_MARKER:
105279
case InstrType.BREAK:
106280
case InstrType.BREAK_MARKER:
281+
case InstrType.CONTINUE_MARKER:
107282
case InstrType.MARKER:
283+
case InstrType.RESET:
284+
break;
108285
}
109286
}
110-
if (animation) CseAnimation.animationComponents.push(animation);
111287
}
112288

113-
static playAnimation(): void {
289+
static async playAnimation() {
114290
if (!CseAnimation.animationEnabled) {
115-
CseAnimation.disableAnimations();
291+
CseAnimation.animations.forEach(a => a.destroy());
292+
CseAnimation.clearAnimationComponents();
116293
return;
117294
}
118295
CseAnimation.disableAnimations();
119-
for (const animationComponent of this.animationComponents) {
120-
animationComponent.animate();
121-
}
296+
// Get the actual HTML <canvas> element and set the pointer events to none, to allow for
297+
// mouse events to pass through the animation layer, and be handled by the actual CSE Machine.
298+
// Setting the listening property to false on the Konva Layer does not seem to work, so
299+
// this a workaround.
300+
const canvasElement = CseAnimation.getLayer()?.getCanvas()._canvas;
301+
if (canvasElement) canvasElement.style.pointerEvents = 'none';
302+
// Play all the animations
303+
await Promise.all(this.animations.map(a => a.animate()));
122304
}
123305
}

0 commit comments

Comments
 (0)