09-02-2023, 02:01 AM (This post was last modified: 09-02-2023, 02:05 AM by grymmjack.)
So for a compound shape (like boolean operation Add, Subtract, etc.)
Operation will be destructive (compound shape is composited on new image)
We will freeimage away our source shapes that compose the compound shape
What defines a compound shape?
The shapes are all the same colors
They do not have to touch each other
For a boolean operation of Add:
Compound shape members which overlap other compound shape members and draw on top of each other are kept and merged together.
For a boolean operation of Subtract:
Compound shape members which overlap other compound shape members are cut out.
In vector drawing programs there are typically operations like "Minus Front" and "Minus Back".
We will have only Minus, and it will always be minus front. That is, our boolean operation will only take 2 shapes, and the last shape passed in will be the front-most.
So therefore a mouth could be composed like so:
Shape: Ellipse
Shape: Rectangle
Position Rectangle half over ellipse
Subtract Rectangle from Ellipse
New shape is a half circle
This is "mouth" shape
Now for the teeth:
Use "mouth" shape as base
Set shape area to rect for mouth shape
Randomize rectangles on top
Add rectangle to Mouth shape
continue for n teeth
This is another edge case - we will also want some kind of shape area to define "undrawable" areas.
For example, in a mouth the teeth shouldn't overlap, but be separate. So once we draw the teeth we should change the bounding rectangle or shape area to prevent a random tooth being drawn on another one.
The idea is that we will use separate image handles for each of our building blocks, and approximate a vector approach.
We won't support transparency for now, because that's going to be a bit much. But, since cutting just means making something a background color, and superimposing it on top of something else that shares that background color, it will appear as transparent in the master compositing of the final image.
Then, using the same shape areas, and shape stuff we outlined previously, and creation of a compound shape with destructive booleans, we can assemble and randomize what we need.
A final thought - since we're using image handles, we do not want to closely-couple our shape and boolean and compound/composite stuff to our shape area stuff - we want loose binding between these because we can use the same logic/functions for images that aren't single colors or faking to be vectors. There is no reason we couldn't use the add or subtract to cut or add full images to each other. Since we're just defining images, positions, and destinations in a layout of rectangles..
09-02-2023, 02:07 AM (This post was last modified: 09-02-2023, 02:14 AM by grymmjack.)
Knowing what we know now we can add a property to our shape area where we define a base image to use as a background/base image.
A shape area
shape_image - long - optional handle of the shape image we want to use as a base. and if we have this we should automatically set the shape rect_w, and rect_h to the images width and height.
This would allow us to generate a compound shape, and use it in a new image as a shape_area background rect, and overlay more shapes on top of it per offset, etc.
This is essentially getting into layer territory, and z-indexes would be the only thing that would distinguish one layer from another and the stacking order is where z-index comes into play.
In our code, it's FIFO order (first in first out)
If we first draw a circle
Then we draw a box
and the two happen to overlap
The box will be on top of the circle
So the z-index is really a human convenience - an abstraction - but all we would really need for this would be a shape area modification again with a zindex property.
This could be useful so let's add it:
A shape area
shape_zindex - integer - optional zindex for a shape area - useful for looping through a collection of shapes and determining order to draw for the stacking order
With these 2 additional properties to a shape area, we have a start of something very powerful.
Having a full on layer object doesn't seem like it makes a lot of sense unless we want to make our code more descriptive and self documenting. That is we declare what we want and QB64 makes it so.
Because QB64 lacks some basic stuff like dictionaries though, we're going to stay procedural.
Think as a designer using a graphics program:
New Layer
Draw a box
Create a new layer (it goes on top)
Draw a ellipse over the box but let the box under it still peek through
Think as a programmer:
New image
Draw a box (x,y)-(w,h), color, etc. BF line
New image
Draw a ellipse over the box... etc.
There is a 1:1 with what we're doing in code already so a further layer based approach would be overkill. Shape areas are layers when they are drawn in a sequence or stacking order. Shape areas are images so we can position them wherever we want and draw them WHENEVER we want (the zindex).
09-02-2023, 02:33 AM (This post was last modified: 09-02-2023, 03:37 AM by grymmjack.)
Thinking a bit deeper it appeared to me that alignment of images would be ideal.
In drawing programs I use, we have the ability to align on the horizontal and vertical as well as distribute shapes within a bounding box (distributing makes the shapes equal spacing on horizontal or vertical axis).
`align(x_type$, y_type$, shapes())`
x_type = "empty, left, middle, right" (where empty = "")
y_type = "empty, top, middle, bottom" (where empty = "")
align is self explanatory - but distribute needs some further discussion.
If you passed: `align("left", "top", shapes())` it would align every shape in `shapes()` to the left and top at the same time. overlapping them all on top of each other.
If you passed: `align("left", "", shapes())` it would left align shape in `shapes()` to the left and if the shapes were already on different y positions, they may overlap, partially, but if they were at least height apart, they would not. but they would be flush left aligned.
If you passed: `align("", "top", shapes())` it would top align shape in `shapes()` to the top and if the shapes were already on different x positions, they may overlap, partially, but if they were at least width apart, they would not. but they would be flush top aligned.
Suppose you have 4 rectangles and they are identical sizes and positioned exactly (x and y all the same) on top of each other...
If you pass: `distribute("left", "", shapes())` nothing is going to happen - because they are already aligned like that.
If you pass: `distribute("", "top", shapes())` nothing is going to happen - because they are already aligned like that.
However, suppose you have 4 rectangles and they are different sizes scattered across the x axis randomly with no equi-distant spacing between them....
If you pass: `distribute("left", "", shapes())` each shape, except the left most, will be spaced equidistant to the extents of all the shapes combined width...
all the shapes combined width = combined width = combined width + shape().x + shape().width
However, suppose you have 4 rectangles and they are different sizes scattered across the y axis randomly with no equi-distant spacing between them....
If you pass: `distribute("", "top", shapes())` each shape, except the top most, will be spaced equidistant to the extents of all the shapes combined height...
all the shapes combined height = combined height = combined height + shape().y + shape().height
So this could be very useful for our shape layer but also useful for ANY kind of rectangle alignment or distribution. So we won't closely couple this either.
But we will want it.
So now we have
align, distribute, shape area, compound shape (add/subtract), that is a good start.
Something we could do extra though, regarding align and distribute is add that element of randomization...
So maybe we could have additional functions:
`align_random()` and `distribute_random` -- why would you want to use align and distribute randomly when they are meant to be precise things? because it could get boring is why.
we'd need 4 new arguments here. why? start_x and end_x, start_y, and end_y -- random x and y in that range applied AFTER the alignment.
So: `align_random(x_type$, y_type$, shapes(), x_start%, x_end%, y_start%, y_end%)`
Same for distribute:
`distribute(x_type$, y_type$, shapes(), x_start%, x_end%, y_start%, y_end%)` -- random x, y ranges.
Here's where optional arguments would be ideal for QB64... but alas we don't have those. However, in our code, we can just pass 0's for stuff we don't care about. Since the math will be using offsets, adding 0 to something is going to do nothing. We can also make sure we check for a range > 0 because randomizing 0 isn't helpful and it's wasteful.
Now we could have used the same funcs/subs for align and distribute by just passing in these other parameters, but this is a messy idea because most of the time align and distribute are going to be used without random jitter.
So instead, we will have align_random and distribute_random call on align and distribute then just add the rand ranges.
These high level ways of thinking will be much easier for us too, as humans. Because we won't be confronted by endless 0's all over the place lol.
09-02-2023, 02:49 AM (This post was last modified: 09-02-2023, 03:40 AM by grymmjack.)
Previous align and random discussion is also a compositing operation. We don't have x and y positions when we are doing stuff, we have separate image handles, and then we have a destination handle where all of this stuff is going to be drawn to.
So at this point I think we have enough plans to start writing some real code and iteratively discover edge cases and identify places where we can create higher level subs and funcs to make things easier and DRY (Don't Repeat Yourself)
for example, thinking about align and distribute we also need to pass an image handle for the destination image. we could just depend on _DEST and write to whatever it's using, that's definitely one way to do it, but it might be better to be more explicit. so let's add arguments to our pseudo:
I started writing and realized that we're going to want to create the _NEWIMAGE INSIDE the sub and here's why. We don't know the width or height of the image, and while we could definitely create something and resize it I supposed, it makes more sense to do it as just a empty `LONG` perhaps... Let's follow this train of thought.
```
'pretend we have a bunch of shape areas in shapes for now we're just seeing if this needs any more higher level stuff
DIM tmp_image AS LONG
align("left", "", shapes(), tmp_image&) ' align them left
_DEST 0 : _SOURCE tmp_image& : _PUTIMAGE
```
So we have 1 line where we simply `_PUTIMAGE` onto another image, in this case `_DEST 0`.
This seems OK by me and having a wrapper that is something silly like "add_to_stage" might not really be helpful here. we need high-level for complexity on the outside, and low-level on the inside. in this case we're doing low-level on the outside, but the level of complexity here is extremely minor.
Maybe something like:
`hcir(x, y, w, h, fill_color, stroke_color, stroke_width)` which would use negative radians to make half a circle (presuming i can figure it out)
Further, a nice shape to add would be a polygon
`poly(x, y, w, h, fill_color, stroke_color, stroke_width, angle, origin_x, origin_y, points)`
If we got poly working we could use it for line, rectangles, triangles, diamonds, hexes, etc.
While it would be nice, I'm going to skip it for now. maybe we will add that later.
We would still use all the default shape types, lin, tri, rec, cir, hcir, etc. even if we had `poly`. it would just use poly to do it's work and wrap poly for those simpler ones.