Add more extension documentation

How do you prepare a extension? I mean, i know how to code a extension, but not how to actually “perpare it”… … Take a look:


Now, how do you add those helper blocks and descriptions? There’s not much docementation for making extensions in general. I did find pxt/banana but that didnt explain how it worked.

If anybody on the makecode team sees this, i would suggest documenting making a extension more.

3 Likes

Here is the documentation for Defining blocks (makecode.com) that should answer your questions.

Here are the Building your own extension (makecode.com) docs as well :slight_smile:.

1 Like

I do have one question though, how do you slot a image in like this?
skillissuegemtree

Thanks! This should help alot. Didnt realize it was under “defining blocks”.

@hasanchik you need to set the “shadow” block for that argument!

Do it like this:

//% block="squish image x $toSquish $amount $time"
//% toSquish.shadow=screen_image_picker
function squishImageX(toSquish: Image, amount: number, time: number) {

}

Note that it’s important that the names match in all three places: the function definition, inside the block definition, and in the line that sets the shadow.

What does shadow do exactly?

Shadow sets the “shadow block” for a given parameter.

To explain a bit further, let me define some blockly terms:

  • Field - An argument in the block that cannot have another block placed inside it. The dropdown in your gif is an example of a field
  • Input - An argument slot in which a block can be placed. Your two number arguments in the gif are examples of inputs
  • Shadow Block - The block that is attached to an input by default. For number inputs, this is the number block “math_number”. Shadow blocks cannot be removed from inputs, but they can be covered up by other blocks (e.g. a variable)

By default, MakeCode tries to guess whether an argument should be a field or an input. It does this based on the type; some types like numbers get shadow blocks, others like enums and images get dropdown fields.

When you set the shadow parameter, you are telling MakeCode to make this argument an input and attach the block with the ID you specify as a shadow (in this example, screen_image_picker).

Another useful shadow block for your example is the time picker. You can see it on the “pause” block in loops. Try this for the time argument:

//% time.shadow=timePicker

(please forgive the mixing of camel case and snake case :stuck_out_tongue:)

1 Like

image
Yeah, i tried doing this with the on/off block at heatx and heaty and loopimage, but this happened:
No clue.

namespace Math {
    /**
     * Loops a number between another number. 16 mod 5 would be 1.
     */
    //% block="%a mod %n"
    export function mod(a: number, n: number): number {
        if (n == 0) {
            return a
        }
        return (((a % n) + n) % n);
    }

    /**
     * Generates a new number that this function will only return with the two numbers you put in.
     */
    //% block="%x pair %y"
    export function cantorPair(x : number, y : number) {
        return (0.5 * (x + y) * (x + y + 1)) + y;
    }

    /**
     * Generates a new number that this function will only return with the two numbers you put in.
     * Works with Negative numbers.
     */
    //% block="%x pair %y"
    export function cantorPairSigned(x : number, y : number) {
        const a = (x >= 0.0 ? 2.0 * x : (-2.0 * x) - 1.0);
        const b = (y >= 0.0 ? 2.0 * y : (-2.0 * y) - 1.0);
        return Math.cantorPair(a,b)
    }
}

interface Image {
    //% helper=getColumns
    getColumns(y: number, dst: Buffer): void;
    //% helper=setColumns
    setColumns(y: number, dst: Buffer, offset: Number): void;
    //% helper=blitColumn
    blitColumn(x: number, y: number, from: Image, fromY: number, fromW: number): void;
    //% helper=getBufferFromPalette
    getBufferFromPalette(y: number): Buffer;
}

namespace helpers {
    //declare function _getColumns(img: Image, y: number, dst: Buffer): void;

    //declare function _setColumns(img: Image, y: number, dst: Buffer): void;

    export function getColumns(img: Image, y : number, dst: Buffer): void {
        let sp = 0
        let w = img.width
        let h = img.height
        if (y >= h || y < 0) {
            return
        }

        dst.setUint8(1, img.getPixel(0, y))
        let n = Math.min(dst.length, (w - y) * h)
        //uint8_t * dp = dst.data;
        //let n = min(dst.length, (w - x) * h) >> 1;

        while (n--) {
            dst.setUint8(sp, img.getPixel(sp, y))
            sp++;
        }
        return
    }

    export function setColumns(img: Image, y: number, src: Buffer, offset: number): void {
        let sp = 0
        offset = offset || 0
        let w = img.width
        let h = img.height
        if (y >= h || y < 0) {
            return
        }

        let n = Math.min(src.length, (w - y) * h)
        //uint8_t * dp = dst.data;
        //let n = min(dst.length, (w - x) * h) >> 1;

        while (n--) {
            img.setPixel(Math.mod((sp + offset), screen.width), y, src[sp])
            sp++;
        }
        return
    }

    function numberToHex(n: number) {
        let hex: string[] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "F", "G"]
        return hex[Math.clamp(1, 16, n)]
    }

    function hexStringToBuffer(hex: string) {
        let buff = control.createBuffer(hex.length);
        for (let i = 0; i < hex.length; i += 1) {
            buff.setUint8(i, parseInt(hex.substr(i, 1), 16));
        }
        return buff;
    }

    /**
     * Gets a palette buffer from a column in a image.
     */
    export function getBufferFromPalette(img: Image, y: number): Buffer {
        let palleteB = "";
        let buffer: number[] = []
        for (let i = 0; i < 16; i++) {
            buffer.push(img.getPixel(i, y));
        }
        for (let i = 0; i < 16; i++) {
            palleteB = palleteB + numberToHex(buffer[i])
        }
        return hexStringToBuffer(palleteB)
    }
}

//% color=190 weight=10 icon="\uff1fb" block="Image effects" advanced=true
namespace imgfx {
    /*
    const bayerThresholdMap = [
        [15, 135, 45, 165],
        [195, 75, 225, 105],
        [60, 180, 30, 150],
        [240, 120, 210, 90]
    ];
    */
    const bayerThresholdMap = [
        [1, 49, 13, 61, 4, 52, 16, 64],
        [33, 17, 45, 29, 36, 20, 48, 32],
        [9, 57, 5, 53, 12, 60, 8, 56],
        [41, 25, 37, 21, 44, 28, 40, 24],
        [3, 51, 15, 63, 2, 50, 14, 62],
        [35, 19, 47, 31, 34, 18, 46, 30],
        [11, 59, 7, 55, 10, 58, 6, 54],
        [43, 27, 39, 23, 42, 26, 38, 22]
    ];
    /*
    const bayerThresholdMap = [
        [0, 2],
        [3, 1],
    ];
    */
    //const ditherStepX = [0, 2, 0, 2, 1, 1, 3, 3, 0, 2, 0, 2, 1, 3, 1, 3]
    //const ditherStepY = [0, 2, 2, 0, 1, 3, 3, 1, 1, 3, 3, 1, 0, 2, 2, 0]

    function ditherRow(buff: Buffer, x: number, threshold: number, col: number = 0, buff2: Buffer = null): void {
        let y = 0
        let dithering = Math.floor(Math.mod(threshold, 65))
        while (y <= buff.length) {
            let map = bayerThresholdMap[x % 8][y % 8]
            if (map < dithering + 1) {
                if (buff2 != null) {
                    buff.setUint8(y, buff2[y])
                } else {
                    buff.setUint8(y, col)
                }
            }
            y += 1
        }
    }

    /**
        *Makes a squishy effect on any image in the X axis.
        *@param image: The image that gets affected.
        *@param stretch: How much the image gets stretched.
        *@param time: Put game.runtime in time, or put any number you want in it. Acts as the frame of the animation.
    */
    //% blockId=squish_image_x
    //% block="squish image x %img=screen_image_picker scretch %stretch time %time"
    //% img.shadow=screen_image_picker
    //% inlineInputMode=inline
    export function squishImageX(img : Image, stretch: number, time: number) {
        let w = img.width
        let h = img.height
        let out = image.create(w, h)
        let buf: Buffer = Buffer.create(h)
        for (let x = 0; x < w; x++) {
            let sin = (Math.sin(x / 10 + (time / 1000)) * stretch)
            img.getRows(Math.mod(Math.mod(sin + x, w), w), buf)
            out.setRows(x, buf)
        }

        return out
    }


    /**
        *Makes a squishy effect on any image in the Y axis.
        *@param image: The image that gets affected.
        *@param stretch: How much the image gets stretched.
        *@param time: Put game.runtime in time, or put any number you want in it. Acts as the frame of the animation.
    */
    //% blockId=squish_image_y
    //% block="squish image y %img=screen_image_picker scretch %stretch time %time"
    //% img.shadow=screen_image_picker
    //% inlineInputMode=inline
    export function squishImageY(img: Image, stretch: number, time: number) {
        let w = img.width
        let h = img.height
        let out = image.create(w, h)
        //let og2 = img.clone()

        /*
        let out = null
        if (w > h) {
            out = image.create(w, w)
        } else if (h > w) {
            out = image.create(h, h)
        } else {
            out = image.create(h, h)
        }
        out.drawTransparentImage(og, 0, 0)
        out = out.transposed()
        out = imgfx.squishImageX(out, stretch, time)
        out = out.transposed()
        og = image.create(w, h)
        og.drawTransparentImage(out, 0, 0)
        */
        let buf: Buffer = Buffer.create(w)
        for (let y = 0; y < h; y++) {
            let sin = Math.sin(y / 10 + (time / 1000)) * stretch
            out.blit(0, y, w, h, img, 0, y - sin, w, h, false, false)
        }
        return out
    }

    /**
        *Makes a heat effect on any image in the Y axis. Very laggy, not suitable on hardware.
        *@param image: The image that gets affected.
        *@param stretch: The width/amplitude of the heat effect.
        *@param time: Put game.runtime in time, or put any number you want in it. Acts as the frame of the animation.
        *@param height: How high the heat effect's max height and min height is.
        *@param oscillate: Whetever to oscillate on all even rows.
    */
    //% blockId=heat_image_y
    //% block="heat image y %img=screen_image_picker scretch %stretch height %height time %time oscillate? %oscillate="toggleTrueFalse""
    //% img.shadow=screen_image_picker
    //% oscillate.shadow="toggleTrueFalse"
    //% inlineInputMode=inline
    export function heatY(img: Image, stretch: number, height: number, time: number, oscillate : Boolean) {
        let w = img.width
        let h = img.height
        const og = img.clone()
        let out = image.create(w, h)
        for (let x = 0; x < w; x++) {
            let sin = (Math.sin((time / 1000) + (stretch * x)) * height)
            if (oscillate == true && x % 2 == 0) {
                sin *= -1
            }
            out.blitRow(x, sin*1, og, x, w)
        } 
        return out
    }

    /**
        *Makes a heat effect on any image in the X axis. Very laggy, not suitable on hardware.
        *@param image: The image that gets affected.
        *@param stretch: The height/amplitude of the heat effect.
        *@param time: Put game.runtime in time, or put any number you want in it. Acts as the frame of the animation.
        *@param height: How high the heat effect's max width and min width is.
        *@param oscillate: Whetever to oscillate on all even columns.
    */
    //% blockId=heat_image_x
    //% block="heat image x %img=screen_image_picker scretch %stretch width %width time %time oscillate? %oscillate="toggleTrueFalse""
    //% img.shadow=screen_image_picker
    //% oscillate.shadow="toggleTrueFalse"
    //% inlineInputMode=inline
    export function heatX(img: Image, stretch: number, width: number, time: number, oscillate: Boolean) {
        let w = img.width
        let h = img.height
        const og = img.clone()
        let out = image.create(w, h)
        //let buf: Buffer = Buffer.create(w)
        for (let y = 0; y < h; y++) {
            let sin = (Math.sin((time / 1000) + (y * stretch)) * width)
            if (oscillate == true && y % 2 == 0) {
                sin *= -1
            }
            out.blit(sin, y, w, h, img, 0, y, w, h, false, false)
            //out.blit(0, y+sin, w, h, img, 0, y, w, h, false, false)
        }
        return out
    }


    /**
        *Does a true dither. Works best with small images. When doing this effect on hardware at fullscreen, use optimized dither instead.
        *@param imgFrom: The image that gets affected.
        *@param threshold: 1-16, decides how much to dither.
        *@param color: Color to dither to if imgTo is null.
        *@param imgTo: Image to dither to if there is one.
    */
    //% blockId=true_dither
    //% block="true dither image imgFrom %imgFrom=screen_image_picker threshold %threshold color %color imgTo %imgTo=screen_image_picker"
    //% imgFrom.shadow=screen_image_picker
    //% imgTo.shadow=screen_image_picker
    //% inlineInputMode=inline
    export function trueDither(imgFrom: Image, threshold: number, color: number = 0, imgTo: Image = null) {
        let w = imgFrom.width
        let h = imgFrom.height
        //let imageData : number[] = []
        let out = imgFrom.clone()
        let buff = Buffer.create(h)
        let buff2 = null
        if (imgTo != null) {
            buff2 = Buffer.create(h)
        }
        for (let x = 0; x < w; x++) {
            out.getRows(x, buff)
            if (buff2 != null) {
                imgTo.getRows(x, buff2)
            }
            ditherRow(buff, x, threshold, color, buff2)
            out.setRows(x, buff)
        }
        return out
    }

    /**
        *Does a optimized dither. To do this, the white color of imgFrom is sacrificed and replaced with white gray. If imgFrom is transparent, imgTo will show trough it.
        *@param imgFrom: The image that gets affected.
        *@param threshold: 1-16, decides how much to dither.
        *@param color: Color to dither to if imgTo is null.
        *@param imgTo: Image to dither to if there is one.
    */
    //% blockId=optimized_dither
    //% block="optimized dither image imgFrom %imgFrom=screen_image_picker threshold %threshold color %color imgTo %imgTo=screen_image_picker"
    //% imgFrom.shadow=screen_image_picker
    //% imgTo.shadow=screen_image_picker
    //% inlineInputMode=inline
    export function optimizedDither(imgFrom: Image, threshold: number, color: number = 0, imgTo: Image = null) {
        let w = imgFrom.width
        let h = imgFrom.height
        let out = imgFrom.clone()
        if (imgTo) {
            color = 0
        }
        //let whiteOnlyImg : Image = imgFrom.clone()
        ///whiteOnlyImg.mapRect(0, 0, w, h, optimizedDitherBuffer)
        let ditherPattern = image.create(8,8)
        if (color == 0) {
            ditherPattern = trueDither(image.create(16, 16), threshold, 1)
        } else {
            ditherPattern = trueDither(image.create(16, 16), threshold, color)
        }
        let repeatedPattern = imgfx.repeatImage(ditherPattern, w, h, offx, offy)
        if (color == 0) {
            out.replace(1, 13)
        }
        out.drawTransparentImage(repeatedPattern, 0, 0)
        if (color == 0) {
            //whiteOnlyImg = imgFrom.clone()
            out.replace(1, 0)
        }
        if (imgTo) {
            let temp = imgTo.clone()
            //let temp2 = imgFrom.clone()
            temp.drawTransparentImage(out,0,0)
            //temp.drawTransparentImage(whiteOnlyImg, 0, 0)
            out = temp
        }
        return out
    }


    /**
        *Repeats a image in X and Y with scroll scrollx and scrolly and a rectangle with maxwidth and maxheight.
        *@param img: The image that gets affected.
        *@param maxwidth: Max width of the frame that the image scrolls trough.
        *@param maxheight: Max height of the frame that the image scrolls trough.
        *@param scrollx: Scroll x.
        *@param scrolly: Scroll y.
        *@param scrollable: Whetever to scroll infinitely.
    */
    //% blockId=optimized_dither
    //% block="repeat image %img=screen_image_picker maxwidth %maxwidth maxheight %maxheight scrollx %scrollx scrolly %scrolly scrollable %scrollable="toggleTrueFalse""
    //% img.shadow=screen_image_picker
    //% scrollable.shadow="toggleTrueFalse"
    //% inlineInputMode=inline
    export function repeatImage(img: Image, maxwidth: number, maxheight: number, scrollx: number = 0, scrolly: number = 0, scrollable : boolean = false) {
        let w = img.width
        let h = img.height
        let out = image.create(maxwidth, maxheight)

        let x: number;
        let y: number;
        if (scrollx >= 0) {
            x = -Math.floor(scrollx % w);
        }
        else {
            x = -(w - Math.floor(Math.abs(scrollx) % w));
        }

        if (scrolly >= 0) {
            y = -Math.floor(scrolly % h);
        }
        else {
            y = -(h - Math.floor(Math.abs(scrolly) % h));
        }

        if (scrollable == false) {
            x = scrollx
            y = scrolly
        }

        for (y; y < maxheight + h; y += h) {
            for (let x2 = x; x2 < maxwidth + w; x2 += w) {
                out.drawTransparentImage(img, x2, y)
            }
        }
        
        return out
    }
}

Ah, don’t use the %param=whatever syntax when defining blocks. That’s the old way of doing things and it can be pretty buggy; the new way of doing it is to do $param and then param.shadow=whatever on a separate line

In other words, change this:

//% blockId=heat_image_y
//% block="heat image y %img=screen_image_picker scretch %stretch height %height time %time oscillate? %oscillate="toggleTrueFalse""
//% img.shadow=screen_image_picker
//% oscillate.shadow="toggleTrueFalse"
//% inlineInputMode=inline
function heatY(img: Image, stretch: number, height: number, time: number, oscillate : Boolean) {
}

to this:

//% blockId=heat_image_y
//% block="heat image y $img scretch $stretch height $height time $time oscillate? $oscillate"
//% img.shadow=screen_image_picker
//% time.shadow=timePicker
//% oscillate.shadow=toggleTrueFalse
function heatY(img: Image, stretch: number, height: number, time: number, oscillate : Boolean) {
}
1 Like

Whenever you see the blocks all go on top of each other like that, it means that there’s a bug in your block definition.

Thanks!

Time actually isnt a still value, its like delta.

One last thing though, why does it display 2 functions?
image
Searched my code for two of the same functions but there is only one.

    /**
        *Does a optimized dither. To do this, the white color of imgFrom is sacrificed and replaced with white gray. If imgFrom is transparent, imgTo will show trough it.
        *@param imgFrom: The image that gets affected.
        *@param threshold: 1-16, decides how much to dither.
        *@param color: Color to dither to if imgTo is null.
        *@param imgTo: Image to dither to if there is one.
    */
    //% blockId=optimized_dither
    //% block="optimized dither image imgFrom $imgFrom=screen_image_picker threshold $threshold color $color imgTo $imgTo=screen_image_picker"
    //% imgFrom.shadow=screen_image_picker
    //% imgTo.shadow=screen_image_picker
    //% inlineInputMode=inline
    export function optimizedDither(imgFrom: Image, threshold: number, color: number = 0, imgTo: Image = null, offx: number = 0, offy: number = 0) {
        let w = imgFrom.width
        let h = imgFrom.height
        let out = imgFrom.clone()
        if (imgTo) {
            color = 0
        }
        //let whiteOnlyImg : Image = imgFrom.clone()
        ///whiteOnlyImg.mapRect(0, 0, w, h, optimizedDitherBuffer)
        let ditherPattern = image.create(8,8)
        if (color == 0) {
            ditherPattern = trueDither(image.create(16, 16), threshold, 1)
        } else {
            ditherPattern = trueDither(image.create(16, 16), threshold, color)
        }
        let repeatedPattern = imgfx.repeatImage(ditherPattern, w, h, offx, offy)
        if (color == 0) {
            out.replace(1, 13)
        }
        out.drawTransparentImage(repeatedPattern, 0, 0)
        if (color == 0) {
            //whiteOnlyImg = imgFrom.clone()
            out.replace(1, 0)
        }
        if (imgTo) {
            let temp = imgTo.clone()
            //let temp2 = imgFrom.clone()
            temp.drawTransparentImage(out,0,0)
            //temp.drawTransparentImage(whiteOnlyImg, 0, 0)
            out = temp
        }
        return out
    }

are you sure you didn’t duplicate the block definition somewhere in the file?

That’s answered one of my questions in my mind for a long time.
I really hope these docs can have a renew, espicially:
Defining blocks (makecode.com)
In which most of examples are using “%”. Some of them differrent with playgound, and seems not compatible each other. And some of usage I found in source codes are not included in this doc.
I did some extensions but alway feeling have not master them exactly, still learning with it, sometimes very confused really.
Thanks a lot if it can be updated, and really appreciate all efforts of you all!
Thanks again!

1 Like

Hey, @richard, regarding the shadow blocks, is there any documentation listing all possible shadow blocks, or atleast the syntax that it uses (perhaps the shadow is formated as {module name}_{block} or is there a defined list of constants that can be used as shadow blocks?

1 Like

@S0m3_random_guy I’m afraid that doesn’t exist. We make a ton of one-off shadow blocks so the list would be quite long and we don’t have a standardized syntax (though we really should!).

The easiest way to figure out how to use a particular shadow block is this:

  1. Drag out a block that uses that shadow and put it in on start
  2. Switch to javascript
  3. See what the javascript version of the block is (e.g. controller.moveSprite())
  4. Inside the pxt-common-packages repo, search for where the function is defined. You can usually get there by search for something like export function moveSprites
  5. Look for where the shadow block is referenced in the block definition. If it’s a new block, it will look like param.shadow=shadow_id. If it’s an old block, you will see it in the block definition string like %param=shadow_id
2 Likes

and, when in doubt, ask questions! I try to answer all extension related questions in a timely manner :slight_smile:

1 Like

Thanks!