Skip to content

GraphX tips and random[nextInt()] stuffs.

Roi Peker edited this page Oct 22, 2020 · 3 revisions

intro

This is my first time doing wikis, so please, bear with me, I will try to organize things later as the lib takes shape. For now, we will use this section to add random tips about the progress of GraphX™.

GraphX™ general tips:

In order to "compose" complex graphics scenes, we might wanna split up our GraphX™ code a little.

If you ever played with Flash (meaning, ActionScript 3.0 code) in the past, and did some custom code outside the Flash/Animate IDE, you probably remember that you had to extend Sprite or some other DisplayObject if you wanted to create reusable components. In order to be faithful to that philosophy, GraphX™ encourages the same approach. So, you can think of it, as creating a Stateless or StatefulWidget in your Flutter code, to encapsulate your UI. Yet, GraphX™ doesn't "rebuild" those DisplayObject Instances in every frame, so it follows a more classical imperative paradigm... it is also imperative to clarify this now :) , as might lead to memory leaks easily if you create new instances on every "enterFrame".

Let's now see how to make a custom (simplistic) button by using the "Primary Button" design in Windows Fluent UI, with GraphX™ (in it's current dev state):

First, let's create a Scene, the starting point of our display list:

class MyButtonDemoScene extends RootScene {
  @override
  void init() {
    owner.core.config.usePointer = true;
    owner.core.config.painterWillChange = true;
  }

  @override
  void ready() {
    super.ready();
    var btn = MyButton();
    addChild(btn);


    /// center button on the stage.
    /// as it has it's registration point centered by `alignPivot()`
    btn.setPosition(
      stage.stageWidth / 2,
      stage.stageHeight / 2,
    );
  }
}
....

If you wanna setup any "initial" property for the SceneController or the ScenePainter, you should do it by overriding the init() method. You might initialise some objects there as well, if u want, but if you need access to the stage or the size of the current scene, you must wait for the ready() state. It will be called on the first frame, as soon as the root scene gets added to the stage. It's the safest place to start your GraphX™ code.

Now, take a look at our button code:

class MyButton extends Sprite {
  static int pressedColor = 0x005A9E;
  static int releasedColor = 0x0078D4;
  int _currentColor = releasedColor;

  /// how many times we clicked the button.
  int _pressCount = 0;

  double _w = 120, _h = 32;
  StaticText _stext;

  MyButton() {
    draw();
    onAddedToStage.add(_onAddedToStage);
  }

  bool _isPointTouching(GxPoint p) => hitTouch(globalToLocal(p));

  void _onAddedToStage() {
    onRemovedFromStage.addOnce(() {
      /// ubsubscribe when we remove from stage.
      stage.pointer.onDown.remove(_onMouseDown);
    });
    stage.pointer.onDown.add(_onMouseDown);
    _stext = StaticText(
      text: 'Hello GraphX',
      paragraphStyle: ParagraphStyle(textAlign: TextAlign.center),
      textStyle: StaticText.getStyle(
        color: Colors.white,
        fontSize: 14,
        fontWeight: FontWeight.w300,
      ),
      width: _w,
    );
    _stext.alignPivot();
    alignPivot();
    _stext.setPosition(_w / 2, _h / 2 - 1);
    addChild(_stext);
  }

  void draw() {
    if (inStage) stage.scene.requestRepaint();

    /// we draw a "focus" border around the button.
    final borderOffset = 2.0;
    graphics.clear()
      ..lineStyle(2, 0x0, 1, true, StrokeCap.square, StrokeJoin.bevel)
      ..drawRect(
        -borderOffset,
        -borderOffset,
        _w + borderOffset * 2,
        _h + borderOffset * 2,
      )
      ..endFill()

      /// we draw the fill of the button with the current color in a
      /// roundRect with "border radius" 2
      ..beginFill(_currentColor)
      ..drawRoundRect(0, 0, _w, _h, 2)
      ..endFill();
  }

  void _onMouseDown(PointerEventData e) {
    if (_isPointTouching(e.stagePosition)) {
      /// register for 1 time callback each time we press.
      stage.pointer.onUp.addOnce((e) => _pressing(false));
      _pressing(true);
    }
  }

  void _pressing(bool flag) {
    _currentColor = flag ? pressedColor : releasedColor;
    _stext.scale = flag ? .95 : 1;
    draw();
    if (!flag) {
      ++_pressCount;
      print("You clicked the button $_pressCount times");
      if (_pressCount >= 5) {
        /// same as `parent.removeChild(this);`
        removeFromParent(true);
        print(
            "$runtimeType instance has been removed from rendering and disposed.");
      }
    }
  }
}

It might seem overcomplicated, coming from the Widgets world, but it give you some hints of GraphX™ lifecycle:

As we add a child (the StaticText) into the button, we need to use a DisplayObjectContainer, but as it's an abstract class, we will use a subclass of it, Sprite().

NOTE: The rendering pipeline starts from the Stage down to the "display list tree", composed by DisplayObjects subclasses... Therefore, any DisplayObject needs to exists as a child, or descendant, of any other DisplayObject that is on the Stage to render something on the screen!

As your initial class extends RootScene (which extends Sprite), that's the first child of the stage. So the hierarchy in this case will be:

.stage             # Stage
└── root           # RootScene (your entry point class)     
    └── button     # MyButton   

As we are building a button, we need access to some touch/mouse input, and that's what stage.pointer provides. In GraphX™, the only way to access that now is through the Stage. So, if we wanna listen to pointer events, we need to be on the stage first. Thankfully, every DisplayObject has a set of "signals" that can notify you about things.

NOTE: I choose the concept of signal callbacks, because it's simpler, easier to implement, potentially much more memory friendly (lazy instantiation), and easier to understand compared to the original EventDispatcher system in AS3.0, which can bubble events through the display list, stop subscriptions, etc... but it always brought some performance hit with it.

A DisplayObject can "dispatch" some signal states like onAdded, onAddedToStage, onRemoved, onRemovedFromStage ... So, in order to be "self notified" of when it gets added to the stage to listen for mouse inputs, we use onAddedToStage.add( myCallback ).

The object has no idea you are subscribing to some listener, that's why is a good idea here to also listen when we get removedFromStage, so we can remove the signal subscription from stage.pointer.onDown. As you see, we used onRemovedFromStage.addOnce(()=> ) for that, which will call the function once, and remove it from memory. It is a common behaviour that gets repeated with the pointer signals as well. In order for a "mouse up" to exist, there must be a "mouse down" before... So the same addOnce() concept is used there.

One thing to notice in the demo code:

void draw() {
if (inStage) stage.scene.requestRepaint();

We are manually requesting a new frame to Flutter/SKIA, if you check Flutter's Performance Overlay, you will see the ticker is not running, but only when we request a new frame. This gives GraphX™ a powerful flexibility to control how and when you screen get refreshed. With a proper usage (and maybe some custom APIs in the future, as GraphX™ becomes "smarter") it will help greatly to improve performance, save battery life and CPU cycles.

the ticking frame

GraphX™ aims at simplifying your code for Canvas drawing, but, as we've seen, it gives you other small features like auto Ticker creation. You probably know what Ticker is in Flutter. You probably used SingleTickerProviderStateMixin more than once in your State classes for an AnimationController or as a vsync: this argument for a TabController. Well, Ticker is the object that Flutter provides you to control WHEN to update something on screen. Internally it uses SchedulerBinding.instance, the layer that communicates directly with the low level dart:window. And it's what GraphX™ uses to request Frames ... like an AnimationController without duration bounds.

To tell our scene that it needs to re-paint on each frame (60 frames per second), we need to set it up in our SceneController.config:

@override
init(){
    owner.core.config.useTicker=true;
}

That will create a Ticker for you, but it will not start it.

If you wanna control the Ticker you can access it through:

    owner.core.isTicking
    owner.core.resumeTicker()
    owner.core.pauseTicker()

And every time it requests a new "frame" you will get a notification through stage.onEnterFrame(callback). It is that simple.

So, if you wanna move a red box on the screen from left to right with GraphX™:

@override
void ready() {
  super.ready();

  /// starts the Ticker, required for onEnterFrame()
  owner.core.resumeTicker();

  /// clips the stage area.
  stage.maskBounds = true;

  var box = Shape();
  addChild(box);

  /// drawing api to make a red square, 30x30 points.
  box.graphics.beginFill(0xff0000).drawRect(0, 0, 30, 30).endFill();

  /// position the box at x=10, y=100.
  box.setPosition(10, 100);
  stage.onEnterFrame.add(() {
    /// every frame (~16ms), increase by 2 points the position
    /// of the box in the X axis, would be like doing `Positioned(left+:2)`
    /// if it "exists"
    box.x += 2;

    /// if we go beyond our stage bounds, move the box back to the start...
    if (box.x > stage.stageWidth) {
      box.x = -box.width;
    }
  });

/// to be continued.


text rendering

StaticText is the current way to deal with SKIA Text, and it's a DisplayObject, so it inherits all the usual transformation properties. As flutter and dart:ui packages share several classes names (like TextStyle), we will try to create some custom types, like TextFormat to avoid potential conflicts in the future, and to keep the API more separated. But in the meantime, this is how we deal with Texts in GraphX™.

/// to be continued.

Clone this wiki locally