Dynamic Follower Goal Bar with Phaser - Stream Elements Widget Deep Dive
  1. Create the goal bar assets with midjourney, photoshop and texture packer

Midjourney Prompt: neon progress bar, black tube with neon fill -s 25

Seperate the different parts of your goal bar such as the bar and fill using masking, use smoothing, feathering and shift edges as needed. Use the content-aware-fill feature in photoshop to clean up the bar after you seperate the fill layer.

Use the photoshop to spline script to export all of your layers. You can download it from here: https://github.com/EsotericSoftware/spine-scripts/tree/master/photoshop

Open Texture Packer and drag and drop a folder with the assets you just exported from photoshop. Make sure its set to Phaser 3 and click publish sprite sheet. You will get a png image and a json file.

Host your png image and JSON file on a server somewhere. I'm hosting mine in a public directory on a personal site on netlify. You may want to set the access-control-origin in your headers to avoid CORS issues.

/neon-bar/*
  Access-Control-Allow-Origin: *

Create a custom widget on stream elements and write the goal bar logic

HTML

<link href="https://fonts.googleapis.com/css?family=Montserrat:400,700" rel="stylesheet">
<script src="
https://cdn.jsdelivr.net/npm/phaser@3.70.0/dist/phaser.min.js
" unsafe-inline></script>
<div id="game-container"></div>

JS

// Global state object
let state = {
    "goal": {
        "value": 0, // Goal amount set in custom field by user
    },
    "desiredWidth": 0, // Track the desired width
  	"count": 0
};

// Configuration for the Phaser game
const config = {
    type: Phaser.AUTO,
    width: 400,
    height: 300,
    parent: 'game-container',
    transparent: true,
    scene: {
        preload: preload,
        create: create,
        update: update
    }
};

// Initialize the Phaser game
const game = new Phaser.Game(config);

function preload() {
    //load the goal bar assets
    this.load.atlas('ui', 'https://rowberry.dev/neon-bar/neon-bar.png', 'https://rowberry.dev/neon-bar/neon-bar.json');
}

function create() {
  	// Get center position of phaser screen
    const centerX = this.cameras.main.width / 2;
    const centerY = this.cameras.main.height / 2;
	// Create the goal bar
    const block = this.add.nineslice(centerX, centerY, 'ui', 'neon-bar-block.png', 402, 60, 10, 10, 10, 10);
    this.fill = this.add.nineslice(centerX, centerY, 'ui', 'neon-bar-fill.png', 339, 29, 10, 10, 10, 10);
    this.fill.setOrigin(0, 0.5);
    this.fill.x = centerX - this.fill.width / 2 - 10;

 // Create a Graphics object for the background
    let bg = this.add.graphics();
    bg.fillStyle(0x000000, 0.5); // Black with some transparency
    bg.fillRect(0, 0, 200, 90); // Adjust the size as needed

    // Create text objects
    this.goalText = this.add.text(10, 10, `Goal: ${state.goal.value}`, { font: '16px Arial', fill: '#ffffff' });
    this.countText = this.add.text(10, 30, `Count: ${state.count}`, { font: '16px Arial', fill: '#ffffff' });
    this.widthText = this.add.text(10, 50, `Width: ${state.desiredWidth}`, { font: '16px Arial', fill: '#ffffff' });

    // Create a container and add the background and text objects
    this.infoGroup = this.add.container(0, 0);
    this.infoGroup.add(bg);
    this.infoGroup.add(this.goalText);
    this.infoGroup.add(this.countText);
    this.infoGroup.add(this.widthText);

    // Position the container as needed
    this.infoGroup.setPosition(0, 0);
}

function update() {
    if (this.fill && this.fill.width !== state.desiredWidth) {
          this.tweens.add({
            targets: this.fill,
            width: state.desiredWidth,
            duration: 500,
            ease: 'Linear',
        });
        this.widthText.setText(`Width: ${state.desiredWidth}`);
       this.countText.setText(`Count: ${state.count}`);
    }
}

window.addEventListener('onWidgetLoad', function (obj) {
    let data = obj.detail.session.data;
    let fieldData = obj.detail.fieldData;
    state.goal.value = fieldData.goal_total;
    progress(data);
});

window.addEventListener('onSessionUpdate', function (obj) {
    let data = obj.detail.session;
  	progress(data);
});

const progress = (data) => {
    let count = data["follower-goal"]["amount"];
  	state.count = count;
    let p = (count) / (state.goal.value) * 100;
    state.desiredWidth = (p / 100) * 339; // Assuming 339 is the full width
    state.desiredWidth = Math.max(0, Math.min(state.desiredWidth, 339)); 
    console.log(`Count: ${count}, Goal: ${state.goal.value}, p: ${p}, newWidth: ${state.desiredWidth}`);
};

FIELDS

{
  "goal_total": {
    "type": "number",
    "label": "Goal amount",
    "value": 100
  }
}