Speed up "aspect" reporter for hyper inputs

There exists a block in Snap!, the “aspect” block (coined by the selector name):
untitled script pic(19)

But also, it supports hyper ops:

However, its slow! The normal block takes around 20 or so milliseconds, so this function takes around 100. It would be nice if it was faster (and don’t worry, I know how it can be fixed)

How...

In threads.js

Process.prototype.reportAspect = function (aspect, location) {
    // sense colors and sprites anywhere,
    // use sprites to read/write data encoded in colors.
    //
    // usage:
    // ------
    // left input selects color/saturation/brightness/transparency or "sprites".
    // right input selects "mouse-pointer", "myself" or name of another sprite.
    // you can also embed a a reporter with a reference to a sprite itself
    // or a list of two items representing x- and y- coordinates.
    //
    // what you'll get:
    // ----------------
    // left input (aspect):
    //
    //      'color'         - a COLOR object
    //      'hue'           - hsv HUE on a scale of 0 - 100
    //      'saturation'    - hsv SATURATION on a scale of 0 - 100
    //      'brightness'    - hsv BRIGHTNESS on a scale of 0 - 100
    //      'transparency'  - rgba ALPHA on a reversed (!) scale of 0 - 100
    //      'r-g-b-a'       - list of rgba values on a scale of 0 - 255 each
    //      'sprites'       - a list of sprites at the location, empty if none
    //
    // right input (location):
    //
    //      'mouse-pointer' - color/sprites at mouse-pointer anywhere in Snap
    //      'myself'        - sprites at or color UNDERNEATH the rotation center
    //      sprite-name     - sprites at or color UNDERNEATH sprites's rot-ctr.
    //      two-item-list   - color/sprites at x-/y- coordinates on the Stage
    //
    // what does "underneath" mean?
    // ----------------------------
    // the not-fully-transparent color of the top-layered sprite at the given
    // location excluding the receiver sprite's own layer and all layers above
    // it gets reported.
    //
    // color-aspect "underneath" a sprite means that the sprite's layer is
    // relevant for what gets reported. Sprites can only sense colors in layers
    // below themselves, not their own color and not colors in sprites above
    // their own layer.

    if (this.enableHyperOps) {
        if (location instanceof List && !this.isCoordinate(location)) {
            return location.map(each => this.reportAspect(aspect, each));
        }
    }

    var choice = this.inputOption(aspect),
        target = this.inputOption(location),
        options = ['hue', 'saturation', 'brightness', 'transparency'],
        idx = options.indexOf(choice),
        thisObj = this.blockReceiver(),
        thatObj,
        stage = thisObj.parentThatIsA(StageMorph),
        world = thisObj.world(),
        point,
        clr;

    if (target === 'myself') {
        if (choice === 'sprites') {
            if (thisObj instanceof StageMorph) {
                point = thisObj.center();
            } else {
                point = thisObj.rotationCenter();
            }
            return this.spritesAtPoint(point, stage);
        } else {
            clr = this.colorAtSprite(thisObj);
        }
    } else if (target === 'mouse-pointer') {
        if (choice === 'sprites') {
            return this.spritesAtPoint(world.hand.position(), stage);
        } else {
            clr = world.getGlobalPixelColor(world.hand.position());
        }
    } else if (target instanceof List) {
        point = new Point(
            target.at(1) * stage.scale + stage.center().x,
            stage.center().y - (target.at(2) * stage.scale)
        );
        if (choice === 'sprites') {
            return this.spritesAtPoint(point, stage);
        } else {
            clr = world.getGlobalPixelColor(point);
        }
    } else {
        if (!target) {return; }
        thatObj = this.getOtherObject(target, thisObj, stage);
        if (thatObj) {
            if (choice === 'sprites') {
                point = thatObj instanceof SpriteMorph ?
                    thatObj.rotationCenter() : thatObj.center();
                return this.spritesAtPoint(point, stage);
            } else {
                clr = this.colorAtSprite(thatObj);
            }
        } else {
            return;
        }

    }

    if (choice === 'color') {
        return clr.copy();
    }
    if (choice === 'r-g-b-a') {
        return new List([clr.r, clr.g, clr.b, Math.round(clr.a * 255)]);
    }
    if (idx < 0 || idx > 3) {
        return;
    }
    if (idx === 3) {
        return (1 - clr.a) * 100;
    }
    return clr[SpriteMorph.prototype.penColorModel]()[idx] * 100;
};

As you can see, if hyper-ops are enabled and the location is a list (with coords), it maps it with the same reportAspect function. Now, note the multiple uses of getGlobalPixelColor, how about we check the code for that? (in morphic.js)

WorldMorph.prototype.getGlobalPixelColor = function (point) {
    // answer the color at the given point.
    // first, create a new temporary canvas representing the fullImage
    // and sample that one instead of the actual world canvas
    // this slows things down but keeps Chrome from crashing
    // in v119 in the Fall of 2023
    var dta = Morph.prototype.fullImage.call(this)
            .getContext('2d')
            .getImageData(point.x, point.y, 1, 1)
            .data;
    return new Color(dta[0], dta[1], dta[2]);
};

Notice the problem here? For every coordinate in the location list, it renders everything on the world over again! The fix would be, in the reportAspect function–instead of calling itself over and over– do something a bit more complicated but faster. If the block should be a hyperop, instead run its own code that will get the rendered world canvas and use that to get the coordinate colors. Here’s the (hopefully) fixed code!

if (this.enableHyperOps) {
    if (location instanceof List && !this.isCoordinate(location)) {
        // old code: return location.map(each => this.reportAspect(aspect, each));

        var thisObj = this.blockReceiver(),
            world = thisObj.world(),
            ctx = Morph.prototype.fullImage.call(world)
                      .getContext('2d');

        return location.map(each => {
            var data = ctx.getImageData(each.at(1), each.at(2), 1, 1)
                          .data;
            return new Color(data[0], data[1], data[2]);
        });
    }
}

Hope this gets fixed!

considering you know exactly how and why to fix it, I think you should submit a PR on github.

I can’t create a fork of Snap! since I have one for Split..

try using the edit button, github will create a branch for you

Not how it works, in GitHub you can only have one fork of a repo on your account, even if you have a fork of a fork of the repo..

I got the branches and forks mixed up

This is just not true?

It is? If you already have a fork of the source repo or even a fork of a fork of the repo, it wont work. You might be getting it confused with branches (which I could make one on my Split! PR repo, but it doesn’t seem right commiting to Snap! from that). I could also make a temporary GitHub organization, and do it from there, but besides I’m not home right now…

I think on GitHub you can create a new branch in your fork for this fix even if you already have a fork. You don’t necessarily need a second fork of the same upstream repo.

btw if you didn’t know I’m pretty sure the previous guy is an AI bot so don’t respond to it

What did it even say?? I’m curious.

don’t remember, but it was some generic AI stuff with AI bolding and italics

Im going to create a branch for my split PR, get it to Snap!, add the changes, and finally make a PR. Coming soon!

I use a lot of bold and italics

Here it is!