Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Fall Banner 2023 Idea - Dev Diary
#1
Brick 
[Image: Screen-Shot-2023-09-01-at-8-55-40-PM.png]

I want to make this programmatically and random but with refinement.

Here is the layer structure in my Aseprite doc:

[Image: Screen-Shot-2023-09-01-at-8-58-17-PM.png]

And here is my ANSI32 palette I'm going to use:

[Image: ansi32-32x.png]

More on this palette here.
grymmjack (gj!)
GitHubYouTube | Soundcloud | 16colo.rs
Reply
#2
Attached Aseprite doc, and exported layers.


Attached Files
.zip   GJ-QB64PE-FALL-2023-BANNER.zip (Size: 39.15 KB / Downloads: 71)
grymmjack (gj!)
GitHubYouTube | Soundcloud | 16colo.rs
Reply
#3
So I figure I need the following random maker routines:
  • Pumpkin - fill color, light color, eyes, nose, mouth, teeth, positions for eyes, nose, mouth, teeth
  • Cloud - fill color, size(small, med, large), position (x-start-range, x-end-range, y-start-range, y-end-range)
  • House - fill color, window color, window type, position (like cloud), window position (like cloud)
  • Porch - fill color, position (like cloud), min-width, min-height, max-width, max-height
  • Sidewalk - fill color, min-width, max-width, origin point, destination point (point=x,y)
  • Moon - fill color, halo color1, halo color2, max-radius, min-radius, halo-offfset
  • Tree - fill color, num branches, max width, max height

Even if I don't complete it in time it will be a fun experiment.
grymmjack (gj!)
GitHubYouTube | Soundcloud | 16colo.rs
Reply
#4
It makes sense to start with the pumpkin.

But before I do that, I want to take a hot minute to think about modular design and reusability.

I think by listing out what I need, I have identified a few things that could work for modularity and reuse:
  • A shape area
    • rect_x, rect_y, rect_w, rect_h - determines where the shape can be drawn. (the bounding box for the entire random shape)
    • shape - one of line, box, triangle, or ellipse
    • shape_jitter - 0-100 range in which points are humanized or randomized for each point in a shape, or line.
    • shape_rect_offset_x - determines where to draw the shape within the rect - x coord
    • shape_rect_offset_y - same but y coord
    • shape_min_w - minimum width of shape
    • shape_max_w - maximum width of shape
    • shape_fill_color
    • shape_line_color
    • shape_is_filled - boolean
    • shape_is_outlined - boolean
    • shape_visibility - string - "always", "sometimes", "never", "25%" - when do we draw the shape? random % 25%, 50%, etc.

Now if I look at just a pumpkin, it could be composed of the main shape area (the ellipse for the body), the eyes, the nose, the mouth

pumpkin-body
  • shape area: rect_x:0, rect_y:0, rect_w:200, rect_h:100 (because rect_w is 2x height - ellipse could be short and wide)
  • shape: "ellipse"
  • shape_jitter: 0
  • shape_rect_offset_x: 0 (body has no offset)
  • shape_rect_offset_y: 0 (body has no offset)
  • shape_min_w: rect_w\3
  • shape_max_w: rect_w-2
  • shape_min_h: rect_h\3
  • shape_max_h: rect_h-2
  • shape_fill_color: orange
  • shape_line_color: n/a
  • shape_is_filled: true
  • shape_is_outlined: false
  • shape_visibility: "always"

pumpkin-eye-left
  • shape area: rect_x: pumpkin-body.rect_w \ 4, rect_y: pumpkin-body.rect_h\4, rect_w:20, rect_h:10 (wider triangle than taller)
  • shape: "triangle"
  • shape_rect_offset_x: 5 (eye can have offset 0 to n)
  • shape_rect_offset_y: 5 (eye can have offset 0 to n)
  • shape_min_w: rect_w\2
  • shape_max_w: rect_w
  • shape_min_h: rect_h\2
  • shape_max_h: rect_h
  • shape_fill_color: yellow
  • shape_line_color: n/a
  • shape_is_filled: true
  • shape_is_outlined: false
  • shape_visibility: "80%" (i don't want the eye always there - pumpkins could have 1 eye)

pumpkin-eye-right
  • shape area: rect_x: pumpkin-body.rect_w \ 2, rect_y: pumpkin-body.rect_h\4, rect_w:20, rect_h:10 (wider triangle than taller)
  • shape: "triangle"
  • shape_rect_offset_x: 5 (eye can have offset 0 to n)
  • shape_rect_offset_y: 5 (eye can have offset 0 to n)
  • shape_min_w: rect_w\2
  • shape_max_w: rect_w
  • shape_min_h: rect_h\2
  • shape_max_h: rect_h
  • shape_fill_color: yellow
  • shape_line_color: n/a
  • shape_is_filled: true
  • shape_is_outlined: false
  • shape_visibility: "80%" (i don't want the eye always there - pumpkins could have 1 eye)

pumpkin-mouth
So here is where the shape choices break down and we need a composite shape. I think I will need to put more thought into this.

I will also need a way to composite shapes in boolean so I could have a grin with a half circle, and some rects for teeth, etc.

WIP... Smile
grymmjack (gj!)
GitHubYouTube | Soundcloud | 16colo.rs
Reply
#5
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:

  1. Shape: Ellipse
  2. Shape: Rectangle
  3. Position Rectangle half over ellipse
  4. Subtract Rectangle from Ellipse
  5. New shape is a half circle
  6. This is "mouth" shape

Now for the teeth:
  1. Use "mouth" shape as base
  2. Set shape area to rect for mouth shape
  3. Randomize rectangles on top
  4. Add rectangle to Mouth shape
  5. 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..
grymmjack (gj!)
GitHubYouTube | Soundcloud | 16colo.rs
Reply
#6
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).
grymmjack (gj!)
GitHubYouTube | Soundcloud | 16colo.rs
Reply
#7
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.

`distribute(x_type$, y_type$, shapes())`
x_type = "empty, left, middle, right" (where empty = "")
y_type = "empty, top, middle, bottom" (where empty = "")

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.
grymmjack (gj!)
GitHubYouTube | Soundcloud | 16colo.rs
Reply
#8
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.

So what would be randomizable?

Looking at the previous pseudo:
`align(x_type$, y_type$, shapes())`
x_type = "empty, left, middle, right" (where empty = "")
y_type = "empty, top, middle, bottom" (where empty = "")

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 Smile 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.
grymmjack (gj!)
GitHubYouTube | Soundcloud | 16colo.rs
Reply
#9
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:


`align(x_type$, y_type$, shapes(), dest_img&)`
`distribute(x_type$, y_type$, shapes(), dest_img&)`
`align_random(x_type$, y_type$, shapes(), dest_img&, x_start%, x_end%, y_start%, y_end%)`
`distribute_random(x_type$, y_type$, shapes(), dest_img& , x_start%, x_end%, y_start%, y_end% )`

Now let's pretend to call them.

```
DIM tmp_image AS LONG
```

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.

I think we're ok with what we have now then.
grymmjack (gj!)
GitHubYouTube | Soundcloud | 16colo.rs
Reply
#10
So one lack that we have in QB64 is there is no shape with a stroke.

We can of course make our own, and it's not too hard.

We'll need to create some higher level classes for some primitives though.

`rec(x, y, w, h, fill_color, stroke_color, stroke_width)`
`cir(x, y, w, h, fill_color, stroke_color, stroke_width)`
`tri(x, y, w, h, fill_color, stroke_color, stroke_width)`

We will also have a single `shape(x, y, w, h, fill_color, stroke_color, stroke_width)` which will be used by the wrappers `rect, circ, tri`

Line also needs a wrapper because we want to draw thick lines.
`lin(start_x, start_y, end_x, end_y, stroke_color, stroke_width)`

Line will not be using shape. It will be it's own thing.

Thinking about this a little deeper, if we added rotation and an origin to rec, we could get thick lines.

For example:
`rec(x, y, w, y, fill_color, stroke_color, stroke_width, angle, origin_x, origin_y)`

We could use this for `lin`... assuming:
origin_x = start_x + width\2 (center point of the x)
origin_y = start_y + height\2 (center point of the y)

This depends on some rotation that can handle that, I don't have any libraries for that, but I have seen others using RotoZoom example in the wiki.

There are also some parameters in CIRCLE that we might want to consider separately as a new higher level shape. Like `halfcir` by using:
https://qb64phoenix.com/qb64wiki/index.php/CIRCLE Wrote:Negative radian values can be used to draw lines from the end of an arc or partial ellipse to the circle center.

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.
grymmjack (gj!)
GitHubYouTube | Soundcloud | 16colo.rs
Reply




Users browsing this thread: 1 Guest(s)