-
-
Notifications
You must be signed in to change notification settings - Fork 49
GraphX tips and random[nextInt()] stuffs.
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™.
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 byDisplayObject
s subclasses... Therefore, anyDisplayObject
needs to exists as a child, or descendant, of any otherDisplayObject
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.
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.
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.