Skip to content

Conversation

@adroitwhiz
Copy link
Collaborator

Depends on #207.

This gets rid of the clones hierarchy and _layerOrder property, and stores every rendered target (sprite, stage, or clone) in one array kept in layer order. Clones now refer directly to the "original" sprite instead of their parent.

While this does require some more management of that array, it means we don't have to:

  • Maintain the _layerOrder property
  • Construct and sort a bunch of new arrays every time we want to loop through all the rendered targets (slow)
  • Traverse upwards through the clone hierarchy to find the "original" sprite

Starting triggers is also done in top-down order now (reverse layer order), which matches how Scratch does it.

@PullJosh
Copy link
Collaborator

Nice! Glad to see compatibility improvements and simpler code.

I've been meeting with some of the good folks working on Patch, a "Python Scratch" that serves a similar purpose as Leopard but for Python rather than JavaScript. They are updating their codebase to use Leopard as their underlying VM rather than their forked version of scratch-vm. (scratch-vm was quite messy to integrate with and Leopard is much simpler.)

If they want to provide a "live" editor experience like Scratch, where sprites perpetually "exist" even when no scripts are running, and the instances appear on the stage immediately when creating or modifying sprites in the editor, it would be nice if they could add/remove sprites to/from a running project. We currently disallow that, mostly as a historical decision. As far as I recall, the reasoning for disallowing it was basically we don't know if the behavior will work correctly and nobody would ever want to do that anyway.

I'm wondering, now that you've just been digging into this code, if you have any sense of how difficult it would be to enable adding/deleting sprites (which are not necessarily clones) while a project is running.

@adroitwhiz
Copy link
Collaborator Author

adroitwhiz commented Jul 17, 2024

I don't think it would be that difficult. In fact, this PR adds addSprite and removeSprite methods that could probably be used as-is.

EDIT: Although removeSprite doesn't currently handle removing all of a sprite's clones, as it is currently only used for removing individual clones themselves. That would probably not be too difficult to add, though.

Object.freeze(sprites); // Prevent adding/removing sprites while project is running
this.targets = [
stage,
...Object.values(this.sprites as Record<string, Sprite>),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the as phrase here? Can the sprites parameter just be typed to this within the constructor itself? Does the Leopard editor actually interact with Leopard's TypeScript definitions, and would typing Project constructor params cause problems there, or nah? (Do any of the other public classes ex. Sprite have any typed parameters yet?)

for (const target of targets) {
// Iterate over targets in top-down order, as Scratch does
for (let i = this.targets.length - 1; i >= 0; i--) {
const target = this.targets[i];
Copy link
Member

@towerofnix towerofnix Aug 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We looked this over in Discord—the old behavior was broken!
Simple layer sequence test

clones have ghost effect and are numbered left to right
(so it's 1, 2, 3, original, for both sprites)

Scratch:
giga clone 3, giga clone 2, giga clone 1, giga sprite, cat sprite, cat clone 1, cat clone 2, cat clone 3, stage (matches rendered layer order)

Leopard (before this PR):
cat clone 3, cat clone 2, cat clone 1, cat sprite, giga sprite, giga clone 1, giga clone 2, giga clone 3, stage

So this is a (very) substantial behavior change, but brings closer to Scratch compat in an important way. Ostensibly related to #163 / #212 but those issues don't really touch on how layers have to do with script execution. And Leopard did attempt to execute scripts in a reliable order (spritesAndClones did sort by layer order)... it was just the wrong order (executing bottom to top, then stage, instead of top to bottom, then stage).

src/Project.ts Outdated
Comment on lines 226 to 232
this.filterSprites((sprite) => {
if (!sprite.isOriginal) return false;

for (const sprite of this.spritesAndStage) {
sprite.effects.clear();
sprite.audioEffects.clear();
}
return true;
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kinda dull, can we separate clearing the effects to happen after filterSprites + does this still affect the stage?

this.filterSprites(sprite => sprite.isOriginal);
this.forEachTarget(target => {
  target.effects.clear();
  target.audioEffects.clear();
});

Comment on lines 263 to 275
public addSprite(sprite: Sprite, behind?: Sprite): void {
if (behind) {
const currentIndex = this.targets.indexOf(behind);
this.targets.splice(currentIndex, 0, sprite);
} else {
this.targets.push(sprite);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removeSprite below doesn't remove a sprite if it's already not in the list, but this function will double-up adding a sprite. This feels like a bad idea, even if addSprite is made not actually public atm lol

This matches how Scratch behaves, and means we don't have to mess around
with _layerOrder anymore.
This matches how Scratch does it.
@adroitwhiz adroitwhiz merged commit d1b0017 into leopard-js:master Aug 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants