Skip to content

Remove _layerOrder and just store sprites in-order #210

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 5 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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"scripts": {
"build": "rollup -c",
"dev": "rollup -c --watch",
"lint": "eslint \"./src/**.ts\"",
"lint": "eslint \"./src/**/*.ts\"",
"prepare": "npm run build"
},
"devDependencies": {
Expand Down
110 changes: 80 additions & 30 deletions src/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Renderer from "./Renderer";
import Input from "./Input";
import LoudnessHandler from "./Loudness";
import Sound from "./Sound";
import type { Stage, Sprite } from "./Sprite";
import { Stage, Sprite } from "./Sprite";

type TriggerWithTarget = {
target: Sprite | Stage;
Expand All @@ -13,6 +13,13 @@ type TriggerWithTarget = {
export default class Project {
public stage: Stage;
public sprites: Partial<Record<string, Sprite>>;
/**
* All rendered targets (the stage, sprites, and clones), in layer order.
* This is kept private so that nobody can improperly modify it. The only way
* to add or remove targets is via the appropriate methods, and iteration can
* be done with {@link forEachTarget}.
*/
private targets: (Sprite | Stage)[];
public renderer: Renderer;
public input: Input;

Expand All @@ -34,12 +41,29 @@ export default class Project {
this.stage = stage;
this.sprites = sprites;

Object.freeze(sprites); // Prevent adding/removing sprites while project is running
this.targets = [
stage,
...Object.values(this.sprites as Record<string, Sprite>),
];
this.targets.sort((a, b) => {
// There should only ever be one stage, but it's best to maintain a total
// ordering to avoid weird sorting-algorithm stuff from happening if
// there's more than one
if (a instanceof Stage && !(b instanceof Stage)) {
return -1;
}
if (b instanceof Stage && !(a instanceof Stage)) {
return 1;
}

for (const sprite of this.spritesAndClones) {
sprite._project = this;
return a.getInitialLayerOrder() - b.getInitialLayerOrder();
});
for (const target of this.targets) {
target.clearInitialLayerOrder();
target._project = this;
}
this.stage._project = this;

Object.freeze(sprites); // Prevent adding/removing sprites while project is running

this.renderer = new Renderer(this, null);
this.input = new Input(this.stage, this.renderer.stage, (key) => {
Expand Down Expand Up @@ -77,7 +101,7 @@ export default class Project {
void Sound.audioContext.resume();
}

let clickedSprite = this.renderer.pick(this.spritesAndClones, {
let clickedSprite = this.renderer.pick(this.targets, {
x: this.input.mouse.x,
y: this.input.mouse.y,
});
Expand Down Expand Up @@ -113,8 +137,9 @@ export default class Project {
triggerMatches: (tr: Trigger, target: Sprite | Stage) => boolean
): TriggerWithTarget[] {
const matchingTriggers: TriggerWithTarget[] = [];
const targets = this.spritesAndStage;
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];
const matchingTargetTriggers = target.triggers.filter((tr) =>
triggerMatches(tr, target)
);
Expand All @@ -133,10 +158,11 @@ export default class Project {
let predicate;
switch (trigger.trigger) {
case Trigger.TIMER_GREATER_THAN:
predicate = this.timer > trigger.option("VALUE", target)!;
predicate = this.timer > (trigger.option("VALUE", target) as number);
break;
case Trigger.LOUDNESS_GREATER_THAN:
predicate = this.loudness > trigger.option("VALUE", target)!;
predicate =
this.loudness > (trigger.option("VALUE", target) as number);
break;
default:
throw new Error(`Unimplemented trigger ${String(trigger.trigger)}`);
Expand Down Expand Up @@ -197,15 +223,13 @@ export default class Project {
this.stopAllSounds();
this.runningTriggers = [];

for (const spriteName in this.sprites) {
const sprite = this.sprites[spriteName]!;
sprite.clones = [];
}
this.filterSprites((sprite) => {
if (!sprite.isOriginal) return false;

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

const matchingTriggers = this._matchingTriggers((tr, target) =>
Expand Down Expand Up @@ -236,22 +260,49 @@ export default class Project {
);
}

public get spritesAndClones(): Sprite[] {
return Object.values(this.sprites)
.flatMap((sprite) => sprite!.andClones())
.sort((a, b) => a._layerOrder - b._layerOrder);
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);
}
}

public removeSprite(sprite: Sprite): void {
const index = this.targets.indexOf(sprite);
if (index === -1) return;

this.targets.splice(index, 1);
this.cleanupSprite(sprite);
}

public filterSprites(predicate: (sprite: Sprite) => boolean): void {
let nextKeptSpriteIndex = 0;
for (let i = 0; i < this.targets.length; i++) {
const target = this.targets[i];
if (target instanceof Stage || predicate(target)) {
this.targets[nextKeptSpriteIndex] = target;
nextKeptSpriteIndex++;
} else {
this.cleanupSprite(target);
}
}
this.targets.length = nextKeptSpriteIndex;
}

public get spritesAndStage(): (Sprite | Stage)[] {
return [...this.spritesAndClones, this.stage];
private cleanupSprite(sprite: Sprite): void {
this.runningTriggers = this.runningTriggers.filter(
({ target }) => target !== sprite
);
}

public changeSpriteLayer(
sprite: Sprite,
layerDelta: number,
relativeToSprite = sprite
): void {
const spritesArray = this.spritesAndClones;
const spritesArray = this.targets;

const originalIndex = spritesArray.indexOf(sprite);
const relativeToIndex = spritesArray.indexOf(relativeToSprite);
Expand All @@ -263,17 +314,16 @@ export default class Project {
// Remove sprite from originalIndex and insert at newIndex
spritesArray.splice(originalIndex, 1);
spritesArray.splice(newIndex, 0, sprite);
}

// spritesArray is sorted correctly, but to influence
// the actual order of the sprites we need to update
// each one's _layerOrder property.
spritesArray.forEach((sprite, index) => {
sprite._layerOrder = index + 1;
});
public forEachTarget(callback: (target: Sprite | Stage) => void): void {
for (const target of this.targets) {
callback(target);
}
}

public stopAllSounds(): void {
for (const target of this.spritesAndStage) {
for (const target of this.targets) {
target.stopAllOfMySounds();
}
}
Expand Down
54 changes: 24 additions & 30 deletions src/Renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,7 @@ export default class Renderer {
public _createFramebufferInfo(
width: number,
height: number,
filtering:
| WebGLRenderingContext["NEAREST"]
| WebGLRenderingContext["LINEAR"],
filtering: number,
stencil = false
): FramebufferInfo {
// Create an empty texture with this skin's dimensions.
Expand Down Expand Up @@ -305,37 +303,33 @@ export default class Renderer {
(filter && !filter(layer))
);

// Stage
if (shouldIncludeLayer(this.project.stage)) {
this.renderSprite(this.project.stage, options);
}
this.project.forEachTarget((target) => {
// TODO: just make a `visible` getter for Stage to avoid this rigmarole
const visible = "visible" in target ? target.visible : true;

// Pen layer
if (shouldIncludeLayer(this._penSkin)) {
const penMatrix = Matrix.create();
Matrix.scale(
penMatrix,
penMatrix,
this._penSkin.width,
-this._penSkin.height
);
Matrix.translate(penMatrix, penMatrix, -0.5, -0.5);
if (shouldIncludeLayer(target) && visible) {
this.renderSprite(target, options);
}

this._renderSkin(
this._penSkin,
options.drawMode,
penMatrix,
1 /* scale */
);
}
// Draw the pen layer in front of the stage
if (target instanceof Stage && shouldIncludeLayer(this._penSkin)) {
const penMatrix = Matrix.create();
Matrix.scale(
penMatrix,
penMatrix,
this._penSkin.width,
-this._penSkin.height
);
Matrix.translate(penMatrix, penMatrix, -0.5, -0.5);

// Sprites + clones
for (const sprite of this.project.spritesAndClones) {
// Stage doesn't have "visible" defined, so check if it's strictly false
if (shouldIncludeLayer(sprite) && sprite.visible !== false) {
this.renderSprite(sprite, options);
this._renderSkin(
this._penSkin,
options.drawMode,
penMatrix,
1 /* scale */
);
}
}
});
}

private _updateStageSize(): void {
Expand Down
Loading