Fireworks!

Enjoy the fireworks! Watch them being launched in the background, and even create some of your own by pressing buttons.

/**
 * particle effects
 *
 * The particle effects you see in arcade are formed through the combination
 * of several objects:
 *
 * **Particles** are the individual elements you see on the screen -
 * they contain a small amount of information that conveys where and how they are represented.
 *
 * **ParticleFactories** describe how the particles are created
 * and how they should be be displayed on the screen. This sets things like initial speed,
 * color, and how long the particle survives
 *
 * **ParticleSources** control when Particles are allowed to be created,
 * they apply updates to the particles, they display them to the screen,
 * and make sure the state is clean (e.g. getting rid of particles when their lifespan runs out).
 * They use ParticleFactories to handle the creation of the sprites involved, so a single source
 * can have a wide variety of actual behaviors by attaching different types of factories to it.
 * Most effects in Arcade use the standard particle source, but some use slightly modified versions
 * to add special behaviors. In particular, the Fire effect has a special source that makes
 * particles move toward each other to get a flame-like 'wave'. Bubbles though, have a source
 * that makes the individual bubbles oscillate left and right, and change state (grow bigger / smaller)
 *
 * Finally, **ParticleEffects** create a combination of a ParticleSource and a ParticleFactory,
 * which display on the screen - typically attached to a sprite or other screen element.
 *
 * Many of these are derived from particle effects that have already been created.
 * If you're interested, you can find those effects in
 * pxt-common-packages/libs/game/particleeffects.ts,
 * with some factories defined in
 * pxt-common-packages/libs/game/particlefactories.ts,
 * and other logic (e.g. the ParticleSources) defined in
 * pxt-common-packages/libs/game/particles.ts
 */
const fireworkEffects: effects.ParticleEffect[] = [
    /** small spinner effect **/
    createEffect(
        1000,
        300,
        () => {
            /**
             * this extends the radial factory used in the warm radial, cool radial,
             * and halo effects to shorten the lifespan of the particles, so they will
             * form a smaller radius
             */
            class ShortRadial extends particles.RadialFactory {
                createParticle(anchor: particles.ParticleAnchor) {
                    const p = super.createParticle(anchor);
                    p.lifespan = randint(200, 450);
                    return p;
                }
            }

            return new ShortRadial(
                2,
                50,
                5,
                randomPalette(randint(2, 5))
            );
        }
    ),
    /** Brocade: forms an 'umbrella like' pattern. I started building this off of the 'fountain' particle **/
    new effects.ParticleEffect(600, 500, function (anchor: particles.ParticleAnchor, particlesPerSecond: number) {
        class BrocadeFactory extends particles.SprayFactory {
            galois: Math.FastRandom;
            palette: number[];

            constructor() {
                super(110, 180, 359);
                this.galois = new Math.FastRandom();
                this.palette = randomPalette(2);
            }

            createParticle(anchor: particles.ParticleAnchor) {
                const p = super.createParticle(anchor);

                if (this.galois.percentChance(25)) {
                    p.color = this.palette[0];
                    p.lifespan = randint(50, 150);
                } else {
                    p.color = this.palette[1];
                    p.lifespan = randint(50, 350);
                }
                return p;
            }

            drawParticle(p: particles.Particle, x: Fx8, y: Fx8) {
                // always just fill a pixel if color is first color, otherwise single pixel 3/4 the time
                if (p.color == this.palette[0] || this.galois.percentChance(85)) {
                    screen.setPixel(Fx.toInt(x), Fx.toInt(y), p.color);
                } else {
                    const toPrint = this.galois.randomBool()
                        ? img`
                            . 1 .
                            1 1 1
                            . 1 .
                        `
                        : img`
                            1 . 1
                            . 1 .
                            1 . 1
                        `;
                    toPrint.replace(0x1, p.color);
                    screen.drawTransparentImage(
                        toPrint,
                        Fx.toInt(x),
                        Fx.toInt(y)
                    );
                }
            }
        }

        const factory = new BrocadeFactory();
        const source = new particles.ParticleSource(anchor, particlesPerSecond, factory);
        source.setAcceleration(0, 600);
        return source;
    }),
    /** Sparkler like effect**/
    createEffect(
        600,
        600,
        () => {
            class SparklerFactory extends particles.SprayFactory {
                galois: Math.FastRandom;
                palette: number[];

                constructor() {
                    super(50, 180, 359);
                    this.galois = new Math.FastRandom();
                    this.palette = randomPalette(2);
                }

                createParticle(anchor: particles.ParticleAnchor) {
                    const p = super.createParticle(anchor);
                    p.data = randint(0, 10);

                    if (this.galois.percentChance(25)) {
                        p.color = this.palette[0];
                        p.lifespan = randint(250, 450);
                    } else {
                        p.color = this.palette[2];
                        p.lifespan = randint(500, 750);
                    }

                    return p;
                }

                drawParticle(p: particles.Particle, x: Fx8, y: Fx8) {
                    ++p.data;
                    // this condition will make the particles flicker;
                    // p.data >> 1 is equivalent to dividing by 2,
                    // and % 2 evaluates to 1 or 0 (effectively, odd or even)
                    // this condition then executes if it evaluates to 1,
                    // which javascript considers to be 'truthy'
                    if ((p.data >> 1) % 2) {
                        // mostly print single dots, but potentially also print small shapes
                        const toPrint = this.galois.percentChance(90)
                            ? img`1`
                            : this.galois.randomBool()
                                ? img`
                                    . 1 .
                                    1 . 1
                                    . 1 .
                                `
                                : img`
                                    1 . 1
                                    . 1 .
                                `;
                        toPrint.replace(1, p.color);
                        screen.drawTransparentImage(
                            toPrint,
                            Fx.toInt(x),
                            Fx.toInt(y)
                        );
                    }
                }
            }

            return new SparklerFactory();
        }
    ),
    /** Crossette: straight lines that fly straight out, with small 'branches' **/
    createEffect(
        100,
        600,
        () => {
            class CrossetteFactory extends particles.SprayFactory {
                galois: Math.FastRandom;
                anchor: particles.ParticleAnchor;
                particlesRemaining: number
                palette: number[];

                constructor() {
                    super(40, 180, 359);
                    this.galois = new Math.FastRandom();
                    this.particlesRemaining = 8;
                    this.palette = randomPalette(2);
                }

                createParticle(anchor: particles.ParticleAnchor) {
                    if (--this.particlesRemaining < 0) {
                        return undefined;
                    }
                    if (!this.anchor)
                        this.anchor = anchor;
                    const p = super.createParticle(anchor);
                    const particleRateMultiple = Fx8(randint(60, 100) / 100);
                    p.vx = Fx.mul(p.vx, particleRateMultiple);
                    p.vy = Fx.mul(p.vy, particleRateMultiple);
                    p.color = this.palette[this.galois.randomRange(0, 1)];;

                    p.lifespan = randint(600, 800);
                    return p;
                }

                drawParticle(p: particles.Particle, x: Fx8, y: Fx8) {
                    // double line with offset x to make the current position of the particle
                    // slightly 'thicker'
                    for (let i = 0; i < 2; i++) {
                        screen.drawLine(
                            Fx.toInt(x) + i,
                            Fx.toInt(y),
                            this.anchor.x,
                            this.anchor.y,
                            p.color
                        );
                    }
                    if (this.galois.randomBool()) {
                        screen.drawTransparentImage(
                            this.galois.randomBool()
                                ? img`
                                    4 . 4
                                    . 4 .
                                    4 . 4
                                `
                                : img`
                                    . 4 .
                                    4 . 4
                                    . 4 .
                                `,
                            Fx.toInt(x) - 1,
                            Fx.toInt(y) - 1
                        );
                    }
                }
            }

            return new CrossetteFactory();
        }
    ),
]

/**
 * This is copied from my original definition for it in
 * pxt-common-packages/libs/game/particleeffects.ts, as that isn't currently exported.
 *
 * It is used to wrap simple particle factories that are created with a standard source
 * into effects that can be easily used
 */
function createEffect(
    defaultParticlesPerSecond: number,
    defaultLifespan: number,
    factoryFactory: (anchor?: particles.ParticleAnchor) => particles.ParticleFactory
): effects.ParticleEffect {
    return new effects.ParticleEffect(defaultParticlesPerSecond, defaultLifespan,
        (anchor: particles.ParticleAnchor, pps: number) =>
            new particles.ParticleSource(anchor, pps, factoryFactory()));
}

// stars that don't twinkle - focus should be on fireworks, not the random
// changes in the background
new effects.ScreenEffect(
    2,
    5,
    5000,
    function (anchor: particles.ParticleAnchor, particlesPerSecond: number) {
        class NoTwinkleStarFactory extends particles.StarFactory {
            constructor() {
                super();
                this.possibleColors = [0xE, 0xB, 0xC, 0xD];
            }

            drawParticle(p: particles.Particle, x: Fx8, y: Fx8) {
                const rest = 0x7FFF;
                const selected = this.images[rest & p.data].clone();
                selected.replace(0x1, p.color);
                screen.drawTransparentImage(
                    selected,
                    Fx.toInt(x),
                    Fx.toInt(y)
                );
            }
        }
        const factory = new NoTwinkleStarFactory();
        return new particles.ParticleSource(
            anchor,
            particlesPerSecond,
            new NoTwinkleStarFactory()
        );
    }
).startScreenEffect();

const fireworkTrail = createEffect(
    25,
    50,
    a => {
        class FireworkTrail extends particles.ParticleFactory {
            constructor() {
                super();
            }

            createParticle(anchor: particles.ParticleAnchor) {
                const p = super.createParticle(anchor);
                p.vx = Fx.neg(Fx8(anchor.vx + randint(-10, 10)));
                p.vy = Fx.neg(Fx8(anchor.vy >> 1));
                p.lifespan = randint(50, 500);
                p.color = Math.percentChance(90) ? 0xE : randint(0x1, 0xD);
                return p;
            }

            drawParticle(p: particles.Particle, x: Fx8, y: Fx8) {
                screen.setPixel(
                    Fx.toInt(x),
                    Fx.toInt(y),
                    p.color
                );
            }
        }
        return new FireworkTrail;
    }
);

// disable the menu button - menus shouldn't get in the way of the demonstration!
controller.menu.onEvent(ControllerButtonEvent.Pressed, undefined);
controller.anyButton.onEvent(
    ControllerButtonEvent.Pressed,
    tryToFire
);

const TIMEOUT = 200;
let lastFired = game.runtime();
function tryToFire() {
    const time = game.runtime();
    if (lastFired + TIMEOUT < time) {
        const vx = randint(-35, 35);
        const firework = sprites.createProjectileFromSide(
            img`
                e
                e
            `,
            vx,
            randint(-150, -125)
        );

        if (!firework.vx || Math.percentChance(70)) {
            firework.x = randint(25, screen.width - 25);
        } else {
            firework.y -= 20;
            firework.vy *= .8;
            if (Math.abs(firework.vx) < 10) {
                firework.vx = randint(30, 40) * (firework.vx < 0 ? -1 : 1);
            } else {
                firework.vx *= 2;
            }
        }

        firework.startEffect(fireworkTrail);
        firework.ay = 100;
        firework.lifespan = randint(800, 1200);
        lastFired = time;
    }
}

game.onUpdate(function () {
    if (lastFired + (3 * TIMEOUT) < game.runtime()) {
        // auto fire if there hasn't been any for a while
        tryToFire();
    }
});


sprites.onDestroyed(SpriteKind.Projectile, s => {
    Math.pickRandom(fireworkEffects).start(s, 500);
});

/**
 * Color stuff
 *
 * this uses the pxt-color extension to change the color palette at runtime.
 * To make all the fireworks unique, this generates a random palette of pastel-ish colors,
 * with the exception of 0xE (set to white) and 0xF (left as black).
 * It also continuously changes the colors from 0x1 to 0xA, fading between different palettes
 * for those colors at random.
 */
const p = color.currentPalette();
p.setColor(0xB, generatePastel().hexValue());
p.setColor(0xC, generatePastel().hexValue());
p.setColor(0xD, generatePastel().hexValue());
p.setColor(0xE, 0xFFFFFF);
color.setPalette(p);

forever(() => {
    new color.Fade()
        .mapEndHSL(
            generatePastel,
            0x1,
            0xA
        )
        .startUntilDone(500);
})

function generatePastel() {
    // generate a random pastel-adjacent color:
    // pastels have 100% saturation and high luminosity ('brightness')
    return new color.HSL(
        randint(0, 359),
        1,
        randint(75, 95) / 100
    );
}

/**
 * Generates a value to be used to specify the colors for each firework,
 * so that the colors aren't always the same between fireworks that run
 * at the same time (value between 1 and 8, so there will always be )
 */
function randomPalette(len: number) {
    if (len > 8) {
        len = 8;
    }
    const palette: number[] = [];
    for (let i = 0; i < len; i++) {
        while (palette.length == i) {
            const selected = randint(1, 0xA);
            if (palette.indexOf(selected) < 0) {
                palette.push(selected);
            }
        }
    }

    return palette;
}
pxt-color=github:jwunderl/pxt-color#v0.0.29