Skip to content

Instantly share code, notes, and snippets.

@philippeback
Created April 5, 2016 12:57
Show Gist options
  • Save philippeback/ef4d128e953de226cf40639641f83e04 to your computer and use it in GitHub Desktop.
Save philippeback/ef4d128e953de226cf40639641f83e04 to your computer and use it in GitHub Desktop.

Basic abstractions behind Athens.

Since Athens is about drawing graphics, we need a media where all drawing operations will appear. We call that media a surface. The surface is abstract. It can have set dimensions, or don't. We don't define if it representing some kind of physical surface (like part of the display screen), or how it storing the data inside. We leaving an introduction of such details to concrete surface implementation. All that matters is that surface is a final target of all our drawing operations. Therefore, in Athens, a surface is usually a starting point where all begins from, and you doing so by creating a specific surface. It is surface's responsibility then, to provide user a means how he can draw on it, and therefore there is a number of factory methods, that allowing you to create a canvas, paints and shapes. All those three are specific implementation of AthensCanvas, AthensPaint and AthensShape protocols, suitable to be used with specific surface implementation that you using.

Canvas

Canvas represents a basic drawing context. We don't allow a direct operations with surface, but instead we provide a context, that contains and carries all information that represents a current stage of drawing operations. This includes things like, current coordinate transformation(s), currently selected paint and shape, and paint mode.

In order to obtain canvas, one must use #drawDuring: message sent to surface with block as argument. The given block receives an instance of AthensCanvas as a single parameter. We are intentionally enclosing all possible drawing operations within a block to make sure that when we leave, we can safely release all resources that was allocated, required to hold the drawing context state. By exposing it in such form, we also making sure that nothing can alter the surface outside a given block. That way, it gives users a definitive answer, whether he finished drawing operations or not, and if it safe to operate with surface for things like saving it to file, or using it as a source for more complex operations, like acting as a paint to fill area(s) inside another surface etc.

Paints and shapes

A starting point is answering a question, how we can represent a simplest, elementary drawing operation on a surface without putting too much constraints. We doing so by postulating that any elementary drawing operation can be expressed by a function:

fill(paint, shape)

Please, note that fill here is not a literally fill given shape with given paint. We call it fill for simplicity reasons. It can anything that altering the surface, but always taking into account given parameters: paint and shape.

Then, from that perspective we can clearly say what are the roles and responsibility of shapes and paints.

The shape defines the affected region, its geometry and location, while paint defines how that region will be altered. In this way, most of more complex operations can be expressed as a series of such function invocations by using various paints and shapes.

Such a representation also gives us a minimal set of roles, a building brick, that we need to introduce in order to represent any kind of drawing operation we may need, as well as a minimal functionality in order to implement such function(s). And therefore a minimal protocol(s), that all paints and shapes should implement.

Since there potentially infinite number of various paint kinds and shape kinds, we cannot make a single function that will implement all possible permutations in order to fill shape with concrete paint. To solve that we introducing a straight dispatch mechanism, where we delegate the responsibility of implementing a concrete case, first to shape, and then to paint.

The API representing this function in canvas by #draw protocol. It takes currently selected paint and currently selected shape and starting dispatch:

draw
	 "Fill the currently selected shape with currently selected paint"
	
	 ^ shape paintFillsUsing: paint on: self

So, first it goes to the shape, by sending #paintFillsUsing:on:,then shape dispatching it further to paint by sending appropriate message (be it #athensFillPath:on: or #athensFillRectangle:on: or anything else, if you want to introduce new kind of shape representation and implement it accordingly). Such dispatch gives us an ability to easily extend the framework by introducing new kind of shapes and paints , by implementing new kind of fill() functions for them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment