Bitmap Transformation

Same again. 6.2.0 introduces new features, but no examples. Everyone has to reinvent the wheel (except senior SW architects around, but I can imagine, also they have difficulties in case of Bitmap Tranformation feature). Bitmap Tranformation seems to be very interesting. Scalable Fonts are only avalable on some devices, so not interesting (only in some years if majority of devices in the field are supporting).

  • Thanks  Ok hand tone1

    these scalable fonts is apparently just usable on the newest devices?

  • Correct.  From the post in News & Announcements, these devices right now:
    epix(gen 2)
    fēnix® 7 Series
    Forerunner® 265 Series
    Forerunner 955
    Forerunner 965
    MARQ® (Gen 2) Series

  • Ok, so the Affine transformations stuff is heaps of fun!!

    I'll be making some trippy watch faces for you folks Joy

    Here's a snippet of my code;

      // define our bitmap offset
      var x_off = 0;
      var y_off = 0;
    
      // define our scale factors 
      var scaleY = 1.0+(Toybox.Math.cos(count/18.0)*2.8)+2.8;
      var scaleX = 1.0+(Toybox.Math.cos(count/18.0)*2.8)+2.8;
    
      // define our rotation angle
      var angle = count/12.0;
    
      // calculate center of the "ground" bitmap resource
      var centerX = x_off + ground.getWidth() / 2;
      var centerY = y_off + ground.getHeight() / 2;
    
      // init three different affine transformations
      var rotateScaleMatrix = new Gfx.AffineTransform();
      var translateMatrix = new Gfx.AffineTransform();
      var inverseTranslateMatrix = new Gfx.AffineTransform();
    
      // let's translate it so the origin is in the middle of the bitmap
      translateMatrix.setMatrix([1, 0, centerX,
                                 0, 1, centerY]);
    
      // let's set the matrix, where we simultanously rotate and scale our bitmap
      rotateScaleMatrix.setMatrix([scaleX * Toybox.Math.cos(angle), -scaleY * Toybox.Math.sin(angle), 0,
                        scaleX * Toybox.Math.sin(angle), scaleY * Toybox.Math.cos(angle), 0]);
    
      // we need to translate it back so the origin is back up the top left
      inverseTranslateMatrix.setMatrix([1, 0, -centerX,
                                        0, 1, -centerY]);
    
      // combine the rotation and inverse with the translate matrix, order is important
      translateMatrix.concatenate(rotateScaleMatrix);
      translateMatrix.concatenate(inverseTranslateMatrix);
    
      // let's draw the bitmap
      dc.drawBitmap2(0, 0, ground, {
        :transform => translateMatrix,
        :filterMode => Gfx.FILTER_MODE_BILINEAR
      });
    
      // increment our count
      count++;

  • Cool again  Thumbsup tone1 btw, do you have a sample how to make rounded fonts with your tilemapper tool. It has been a great help to the whole community Muscle tone1

  • Took me a while and not sure if there is a more efficient way of doing this, but thanks to , I figured out how to rotate a watch hand so that there is a small tail with the hand.  The key was to rotate before translation.  Below, I created a watch hand bitmap that is 20px X 210px and have it spinning about the centre of the watch, with a 30px tail (210-180).  This assumes you have created a bitmap of the watchhand with the Point of the hand at the top.

    	var angle = (clockTime.sec / 60.0f) * TWO_PI;
    	var transform = new Graphics.AffineTransform();
    	transform.rotate(angle);
    	transform.translate(-10, -180);
    	dc.drawBitmap2(CX, CY, bmp, { :transform => transform, :tintColor => 0xFFFFFF });

    Please enable Safe Browsing to warn against unsafe URLs

    Go to help

    Please enable Safe Browsing to warn against unsafe URLs

    Go to help

  • Yes, composing transforms is backward from the order you expect.

    In case anyone finds it helpful, here is a cut down version of the test app I used when working on the feature. I used a greyscale version of the normal monkey.png that we include with our samples, but you can easily use an image of your choosing. The transforms are specified in the order you'd expect, and they are applied in reverse order.

    import Toybox.Application;
    import Toybox.Lang;
    import Toybox.WatchUi;
    import Toybox.Graphics;
    
    (:typecheck(false))
    function defaultValue(value, fallback as Float) as Float {
        if (value has :toFloat) {
            return value.toFloat();
        }
    
        return fallback;
    }
    
    function formatFloat(value as Float) as String {
        return value.format("%0.2f");
    }
    
    
    class NothingView extends WatchUi.View {
    
        typedef ScaleTransform as {
            :scaleX as Float,
            :scaleY as Float,
        };
    
        typedef OffsetTransform as {
            :offsetX as Float,
            :offsetY as Float,
        };
    
        typedef ShearTransform as {
            :shearX as Float,
            :shearY as Float,
        };
    
        typedef RotateTransform as {
            :theta as Float,
        };
    
        typedef Transform as ScaleTransform or OffsetTransform or ShearTransform or RotateTransform;
    
        typedef Scenario as {
            :locX as Number,
            :locY as Number,
            :bitmapX as Number,
            :bitmapY as Number,
            :filterMode as FilterMode,
            :tintColor as ColorType,
            :transforms as Array<Transform>,
        };
    
        hidden const SCENARIOS as Array<Scenario> = [
            {
                :tintColor => Graphics.COLOR_GREEN,
                :transforms => [
                    {
                        // image is 101x116px, and we want to center it
                        :offsetX => -50.0f,
                        :offsetY => -58.0f
                    },
                    {
                        :theta => 30.0f
                    },
                ]
            },
            {
                :tintColor => Graphics.COLOR_RED,
                :transforms => [
                    {
                        :theta => 30.0f
                    },
                    {
                        // image is 101x116px, and we want to center it
                        :offsetX => -50.0f,
                        :offsetY => -58.0f
                    },
                ]
            },
    
            {
                // draw a yellow tinted mini-monkey near the 3 o'clock position
                :tintColor => Graphics.COLOR_YELLOW,
                :transforms => [
                    {
                        // image is 101x116px, and we want to center it
                        :offsetX => -50.0f,
                        :offsetY => -58.0f
                    },
                    {
                        :theta => 30.0f
                    },
                    {
                        :scaleX => 0.2f,
                        :scaleY => 0.2f,
                    },
                    {
                        :offsetX => 100.0f,
                        :offsetY => 0.0f,
                    },
                ]
            }
        ] as Array<Scenario>;
    
        hidden var mIndex as Number;
        hidden var mBitmap as BitmapType or Null;
        hidden var mFontHeight as Number = 0;
        hidden var mFont as FontDefinition = Graphics.FONT_XTINY;
        hidden var mJustification as Number = Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER;
    
        // cached values
        hidden var mTransform as AffineTransform or Null;
        hidden var mLines as Array<String> = [] as Array<String>;
    
        public function initialize() {
            View.initialize();
    
            mIndex = 0;
        }
    
        public function onLayout(dc as Dc) as Void {
            mFontHeight = dc.getFontHeight(mFont);
        }
    
        public function onShow() as Void {
            mBitmap = WatchUi.loadResource(Rez.Drawables.Monkey) as BitmapType;
    
            // hack.. force cached values to be loaded
            changePage(0);
        }
    
        public function onHide() as Void {
            mBitmap = null;
        }
    
        public function changePage(direction as Number) as Void {
            if (direction > 0) {
                mIndex = (mIndex + 1) % SCENARIOS.size();
            } else if (direction < 0) {
                mIndex = (mIndex + (SCENARIOS.size() - 1)) % SCENARIOS.size();
            }
    
            //mTransform = null;
            mLines = [] as Array<String>;
    
            // update the transform and the text and cache it for multiple redraws
            var transforms = SCENARIOS[mIndex][:transforms];
            if (transforms != null) {
                var xform = new Graphics.AffineTransform();
                var lines = [] as Array<String>;
    
                var count = transforms.size();
                for (var i = 0; i < count; ++i) {
                    var transform = transforms[count - i - 1];
    
                    var theta = transform[:theta];
                    if (theta != null) {
                        theta = defaultValue(theta, 0.0f);
    
                        lines.add(Lang.format("ROT $1$", [
                            formatFloat(theta)
                        ]));
                        xform.rotate(Math.toRadians(theta).toFloat());
                    }
    
                    var scaleX = transform[:scaleX];
                    var scaleY = transform[:scaleY];
                    if (scaleX != null || scaleY != null) {
                        scaleX = defaultValue(scaleX, 1.0f);
                        scaleY = defaultValue(scaleY, 1.0f);
    
                        lines.add(Lang.format("SCL $1$, $2$", [
                            formatFloat(scaleX),
                            formatFloat(scaleY)
                        ]));
                        xform.scale(scaleX, scaleY);
                    }
    
                    var offsetX = transform[:offsetX];
                    var offsetY = transform[:offsetY];
                    if (offsetX != null || offsetY != null) {
                        offsetX = defaultValue(offsetX, 0.0f);
                        offsetY = defaultValue(offsetY, 0.0f);
    
                        lines.add(Lang.format("OFF $1$, $2$", [
                            formatFloat(offsetX),
                            formatFloat(offsetY)
                        ]));
                        xform.translate(offsetX, offsetY);
                    }
    
                    var shearX = transform[:shearX];
                    var shearY = transform[:shearY];
                    if (shearX != null || shearY != null) {
                        shearX = defaultValue(shearX, 0.0f);
                        shearY = defaultValue(shearY, 0.0f);
    
                        lines.add(Lang.format("SHR $1$, $2$", [
                            formatFloat(shearX),
                            formatFloat(shearY)
                        ]));
                        xform.shear(shearX, shearY);
                    }
                }
    
                mTransform = xform;
                mLines = lines;
            }
    
            WatchUi.requestUpdate();
        }
    
        public function onUpdate(dc as Dc) as Void {
            dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK);
            dc.clear();
    
            var cx = dc.getWidth() / 2;
            var cy = dc.getHeight() / 2;
    
            var scenario = SCENARIOS[mIndex];
            var bitmap = mBitmap;
    
            if (bitmap != null) {
    
                var locX = scenario[:locX];
                if (locX == null) {
                    // bitmap drawn at center of screen if no location specified
                    locX = cx;
    
                    // this would default the bitmap to be drawn centered on the
                    // screen
                    //cx - bitmap.getWidth() / 2;
                }
    
                var locY = scenario[:locY];
                if (locY == null) {
                    // bitmap drawn at center of screen if no location specified
                    locY = cy;
    
                    // this would default the bitmap to be drawn centered on the
                    // screen
                    // cy - bitmap.getHeight() / 2;
                }
    
                var options = {};
    
                var bitmapX = scenario[:bitmapX];
                if (bitmapX != null) {
                    options[:bitmapX] = bitmapX;
                }
    
                var bitmapY = scenario[:bitmapY];
                if (bitmapY != null) {
                    options[:bitmapY] = bitmapY;
                }
    
                var tintColor = scenario[:tintColor];
                if (tintColor != null) {
                    options[:tintColor] = tintColor;
                }
    
                var filterMode = scenario[:filterMode];
                if (filterMode != null) {
                    options[:filterMode] = filterMode;
                }
    
                if (mTransform != null) {
                    options[:transform] = mTransform;
                }
    
                dc.drawBitmap2(locX, locY, bitmap, options);
    
                // draw crosshairs on top as a reference point
                dc.setColor(Graphics.COLOR_RED, Graphics.COLOR_RED);
                dc.drawLine(cx - 10, cy, cx + 10, cy);
                dc.drawLine(cx, cy - 10, cx, cy + 10);
            }
    
            dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
    
            // display a summary of the options
            var tx = 10;
            var ty = cx - (mLines.size() / 2) * mFontHeight;
    
            for (var i = 0; i < mLines.size(); ++i) {
                dc.drawText(tx, ty, mFont, mLines[i], mJustification);
                ty += mFontHeight;
            }
        }
    }
    
    class NothingDelegate extends WatchUi.BehaviorDelegate {
    
        hidden var mView as NothingView;
    
        public function initialize(aView as NothingView) {
            BehaviorDelegate.initialize();
            mView = aView;
        }
    
        public function onNextPage() as Boolean {
            return changePage(+1);
        }
    
        public function onPreviousPage() as Boolean {
            return changePage(-1);
        }
    
        hidden function changePage(direction as Number) as Boolean {
            mView.changePage(direction);
            return true;
        }
    }
    
    class NothingApp extends Application.AppBase {
    
        function initialize() {
            AppBase.initialize();
        }
    
        function getInitialView() {
            var view = new NothingView();
            return [ view, new NothingDelegate(view) ] as Array<Views or InputDelegates>;
        }
    }
    
    

  • - You shouldn't need to manually calculate the fields of the matrices. The setTranslation/setRotation/setScale/setShear methods overwrite the entire matrix, and the translate/rotate/scale/shear methods concatenate.

    //  translateMatrix.setMatrix([1, 0, centerX,
    //                             0, 1, centerY]);
    translateMatrix.setTranslation(centerX, centerY);
    
    //  rotateScaleMatrix.setMatrix([scaleX * Toybox.Math.cos(angle), -scaleY * Toybox.Math.sin(angle), 0,
    //                    scaleX * Toybox.Math.sin(angle), scaleY * Toybox.Math.cos(angle), 0]);
    rotateScaleMatrix.setRotation(angle);
    rotateScaleMatrix.scale(scaleX, scaleY); // apply scale on top of rotation

    You should also be able to directly compose the matrices, instead of creating multiple local temporaries.

  • Yes, right!

    And finally we can now draw perfectly centered circles and disks, without the need to draw 4 circles. We just need to draw a circle and to translate it by 0.5 pixel.

    // Draw a circle on a buffered bitmap

    var watchCenterBuffer = Graphics.createBufferedBitmap({ :width=>30, :height=>30 });
    var tempCenter = watchCenterBuffer.get().getDc();

    tempCenter.setColor(Graphics.COLOR_TRANSPARENT, Graphics.COLOR_TRANSPARENT);

    tempCenter.clear();

    tempCenter.setAntiAlias(true);

    tempCenter.setColor(0xaaaaaa, Graphics.COLOR_TRANSPARENT);

    tempCenter.fillCircle(15, 15, 10);

    // Affine transform to translate by half pixel to get perfect centered circle
    var translateMatrix = new Graphis.AffineTransform();

    translateMatrix.initialize();

    translateMatrix.translate(-0.5,-0.5);

    // let's draw the bitmap

    dc.setAntiAlias(true);

    dc.drawBitmap2(dc.getWidth()/2-15, dc.getWidth()/2-15, watchCenterBuffer, {

    :transform => translateMatrix,

    :filterMode => Graphics.FILTER_MODE_BILINEAR

    });

  • Some rotating text. I was hoping to use it to draw curved text, which is possible with added calculations, but judging by the simulator results this would be a battery drainer. Guess I'm waiting for a device that supports vector fonts or figuring out the tilemapper tool ito fonts.

    var buffBmp = Graphics.createBufferedBitmap({:width => 100, :height => 100}).get();
    var buffDc = buffBmp.getDc();
    buffDc.setColor(Graphics.COLOR_YELLOW, -1);
    buffDc.drawText(50, 50, Graphics.FONT_TINY, "BMP", Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER);
    
    var transform = new Graphics.AffineTransform();
    transform.rotate(Math.toRadians(time.sec * 6).toFloat());
    transform.translate(-50.0, -50.0);
    
    dc.drawBitmap2(centerX, centerY, buffBmp, {:transform => transform});





  • As stated in my other thread ("Rotating an image (FR965)") this stuff is a bit too much for me to handle right now.

    I "simply" (well, obviously it isn't simple) want to rotate an image by 30° per (current) hour. The image is an anolg dial.

            var currentHour = System.getClockTime().hour;
    
            // calculate rotation
            var rotationDegrees = (currentHour * 30) % 360;
            var rotationRadians = rotationDegrees * (Math.PI / 180);
    
            // create affine transform matrix
            var transform = new AffineTransform();
    
            // do the rotation
            transform.rotate(rotationRadians);
    
            // create parameters
            var parameters = {
                :bitmapX => 6,  // X-Pos on screen
                :bitmapY => 6,  // Y-Pos on screen
                :bitmapWidth => 443,  // width of bitmap
                :bitmapHeight => 443,  // height of bitmap
                :transform => transform  // Affine Transformation
            };
    
            // Call the parent onUpdate function to redraw the layout
            View.onUpdate(dc);
    
            // draw bitmap
            dc.drawBitmap2(6, 6, dialBitmap, parameters);

    When I run the simulator the only thing I see is an IQ sign with an exclamation mark. I'm sure I am missing something, that should be really obvious but I just don't get it.