Creating a React based Webinterface for [jweb] similar to n4m.monitor

maybites's icon

maybites

9月 29 2024 | 4:36 午後

Hi

I am exploring the possibilities for the object [jweb]. specifically the generation of React-js based static webinterfaces as used for the node 4 max monitor window.

I am able to compile a react webapp that loads statically from a local file in [jweb], but i am struggling now to integrate hooks to max inlets and outlets.

I know this must be possible, since it has been realized with the n4m.monitor. The n4m.monitor.min.js file is not giving away lots of its secreets, but I was able to find the function calls for max.outlet and max.bindInlet:

they look something like:

{key:"revealFile",value:function(A){this._max.outlet("reveal",A)}}

or

A._max.bindInlet("npm",A._onNPM.bind(mw(A)))


I assume [jweb] provides these functionalies to interact with the max environment by means of code injection? similar to the 'executejavascript' message?

but I would like to know how it could be implemented in the development of a react app.

maybites's icon

maybites

10月 01 2024 | 8:15 午後

I found a way to use https://github.com/flyover/imgui-js and implement the Javascript API for jweb into the typescript source file of the example that comes with the repo.

For reference (and yeah - an AI has done the job - after some coaxing):
1. First, let's declare the `max` property on the `Window` interface:
declare global {
    interface Window {
        max?: {
            bindInlet: (message: string, callback: (...args: any[]) => void) => void;
            outlet: (message: string, ...args: any[]) => void;
        };
    }
}
Note the `?` after `max`, which makes it an optional property. This is because `max` might not always be present (e.g., when not running in the Max/MSP environment).
2. Then, we need to ensure that the `window.max` object is available. We can do this by checking for its existence and waiting if necessary:
function waitForMax(): Promise<void> {
    return new Promise((resolve) => {
        if (window.max) {
            resolve();
        } else {
            const checkInterval = setInterval(() => {
                if (window.max) {
                    clearInterval(checkInterval);
                    resolve();
                }
            }, 100);
        }
    });
}
3. Modify the `main` function to wait for the Max environment:
export default async function main(): Promise<void> {
    await ImGui.default();
    await waitForMax();
    
    if (typeof(window) !== "undefined") {
        window.requestAnimationFrame(_init);
    } else {
        // ... existing code for non-browser environments ...
    }
}
4. In the `_init` function, set up the Max message bindings:
async function _init(): Promise<void> {
    // ... existing ImGui setup code ...

    if (window.max) {
        window.max.bindInlet("setValue", (v: number) => {
            f = Math.min(Math.max(v, 0), 1);  // Assuming f is in 0-1 range
        });

        window.max.bindInlet("toggleDemo", () => {
            show_demo_window = !show_demo_window;
        });
    }

    // ... rest of existing code ...
}
5. Create a helper function to send messages to Max:
function sendToMax(message: string, ...args: any[]): void {
    if (window.max) {
        window.max.outlet(message, ...args);
    }
}
6. Use this helper function in your ImGui controls:
function _loop(time: number): void {
    // ... existing ImGui setup code ...

    ImGui.Begin("Hello, world!");

    if (ImGui.SliderFloat("float", (value = f) => f = value, 0.0, 1.0)) {
        if (window.max) {
            sendToMax("value", f * 127);  // Scale to 0-127 range for Max
        }
    }

    if (ImGui.Button("Toggle Demo")) {
        show_demo_window = !show_demo_window;
        if (window.max) {
            sendToMax("demoToggled", show_demo_window ? 1 : 0);
        }
    }

    // ... rest of existing code ...
}
make build-example

and patch: