soegaard / sketching

A Racket library for creative drawings and animations. Inspired by Processing.
110 stars 10 forks source link

Shape rotation #70

Open soegaard opened 2 years ago

soegaard commented 2 years ago

@eXodiquas

When shapes are rotated the x and y the draw method shouldn't transform the placement coordinates.

In the example below I was surprised by the direction of the second shape.

#lang sketching

(define (setup)
  (size 600 400)
  (background 230))

(define (draw)
  (background 230)

  (define x frame-count)

  (define s (new Shape))
  (s.begin-shape)
  ; (s.fill 0 0 255)
  ; (s.no-stroke)
  (s.vertex  0  0)
  (s.vertex 50 50)
  (s.vertex 50  0)
  (s.end-shape)

  (s.draw    x     25)
  (s.rotate 45)
  (s.draw (+ x 50) 25))
soegaard commented 2 years ago

@eXodiquas

The odd direction was caused by an old bug in translation-matrix where x and y were flipped.

However, I have changed the transformation in draw-shape to also use the current transformation.

I still need to check that the behaviour matches Processing.

Here is an example program:

#lang sketching

(define (setup)
  (size 600 400)
  (background 230)
  ; (no-loop)
  )

(define (draw)
  (background 230)

  (define x frame-count)

  (define s (new Shape))
  (s.begin-shape)
  ; (s.fill 0 0 255)
  ; (s.no-stroke)
  (s.vertex  0  0)
  (s.vertex 50 50)
  (s.vertex 50  0)
  (s.end-shape)

  (fill "white")
  (s.draw  (+ 300 x)        200)
  (s.rotate (* (/ 22.5 180) 3.1415))
  (fill "red")
  ; now move the underlying coordinate system 50
  (translate 50 0)
  ; the red triangle should appear to the right of the white
  (s.draw  (+ 300 x)        200))
Ecsodikas commented 2 years ago

Oh now I see why my draw method had the strange behaviour of moving the shape around each frame. Coordinate systems are hard. :brain:

I tried a program in sketching and in processing:

void setup() {
  size(600, 400);
  background(230);
}

void draw() {
  background(230);
  int fc = frameCount;
  PShape p = createShape();
  p.beginShape();
  p.vertex(0, 0);
  p.vertex(50, 50);
  p.vertex(50, 0);
  p.endShape();

  fill(255, 255, 0, 255);
  shape(p, 300 + fc, 200);
  p.rotate((22.5/180) * 3.1415);
  translate(50, 0);
  fill(255, 0, 0, 255);
  shape(p, 300 + fc, 200);
}
#lang sketching

(define (setup)
  (size 600 400)
  (background 230)
  )

(define (draw)
  (background 230)

  (define x frame-count)

  (define s (new Shape))
  (s.begin-shape)
  (s.vertex  0  0)
  (s.vertex 50 50)
  (s.vertex 50  0)
  (s.end-shape)

  (fill 255 255 0 255)
  (s.draw  (+ 300 x)        200)
  (s.rotate (* (/ 22.5 180) 3.1415))
  (translate 50 0)
  (fill 255 0 0 255)
  (s.draw  (+ 300 x)        200))

and both programs are behaving differently here, but none feels "wrong". The processing one only draws red shapes, based on the last fill that was called. The sketching one draws two differently colored triangles, based on the fill that was set before the call. I did not find any difference between them as long as set-fill / setFill was specified, but this search probably was not exhaustive.

Is there anything else left to do in regards of bug fixing of my code?

soegaard commented 2 years ago

@eXodiquas

Yep. Coordinate systems are hard. It didn't help that I was a long time to discover that Cairo uses the reverser of the normal order in the when doing matrix multiplication. None of my tests made sense...

There is one thing, I thought of when reading the source code. You will need to make a test program to see if everything works already.

The question is whether the children of a group are rotated when the group is rotated.

If it is simpler, then test the above with translation instead of rotation.

soegaard commented 2 years ago

I'll check out your example in Processing. Let's see if we can figure out what causes the difference.

Btw - do you know wheter P5.js has shapes as objects? I looked for them, but didn't find them.

soegaard commented 2 years ago

Your Processing examples is interesting.

If I add the line

  fill(0, 0, 255, 255);

right before the line

PShape p = createShape();

then the shape gets blue as the fill color. The subsequence fill statements doesn't affect the how the shape p is drawn at all.

Now it sort of makes sense P. uses the current fill color when the shape is created. But in your original example where the shape is created with no fill -- I don't get why both triangles get the same (the last) fill color.

soegaard commented 2 years ago

I think, I figured it out. The processing documentation is scattered in several locations, so it is hard to get an overview.

Each shape stores a "style" which (sigh) "include attributes such as colors, stroke weight, and stroke joints". The default is that a shape has "style enabled" which means that drawing a shape ignores the current stroke and fill, and instead uses the style stored inside the shape.

Calling "disableStyle()" makes shape drawing uses the standard current fill and stroke.

The documentation of setFill() says:

When a shape is created with beginShape() and endShape(), its attributes may be changed with fill() and stroke() within beginShape() and endShape(). However, after the shape is created, only the setFill() method can define a new fill value for the PShape.

That is, when endShape() is called, the current stroke and fill is stored as the style inside the shape object. This explains why fill() and stroke() "works" inside beginShape() and endShape().

It also explains why both triangles in your example get the same color (except for the very first frame).

It even explains why the color is the last one. The last fill color is still active when draw is called for a frame. Thus that color is used as the style color for the shape.

There are still some details to work out: Which attributes are stored as part of the "style". I think, we need to find the relevant part of the P. source code.

soegaard commented 2 years ago

Turns out a style contains almost all settings of the drawing context.

https://github.com/processing/processing4/blob/master/core/src/processing/core/PShape.java#L319

Ecsodikas commented 2 years ago

Regarding the test program, I'll give it a shot tomorrow. I try to replicate as many edge cases as possible and compare it to the Processing version of the same code. Do you want me to push it to the Repo under a test directory or just a link to a gist in this issue?

Turns out a style contains almost all settings of the drawing context.

So that's what the style is all about. Should this be a thing in Sketching aswell? It seems like it is just a shortcut for "I don't want to define the style specifically for each shape, so I use the global style." But Sketching is already doing this if no specific value is set for a style (at least for fill as stated above).

Btw - do you know wheter P5.js has shapes as objects? I looked for them, but didn't find them.

I'm pretty sure p5.js does not have shape objects, I tried to create those examples in an web editor first and I did not find anything about shape objects, only workarounds. I had to install Processing to check the behaviour.

soegaard commented 2 years ago

Yes, give it a shot.

I think the best way to do it, is to make a new branch in your repo. Then push that new branch to sketching. When we have worked out the details, we can merge the branch into main.

Wrt to style: You already have some of the state of a style (the fill and stroke). I am not 100% sure this is a complete solution: but a copy of the current pen and the current brush and use them for drawing the shape might work.

I think you are right about P5.js not having a Shape class.

Ecsodikas commented 2 years ago

I am currently writing the test program and I found a big difference between Sketching and Processing. In Processing it is only useful to add child shapes to parent shapes that are defined as createShape(GROUP).

Before I start fixing this, I'll compile a list of differences I found while writing the test programm, so we can generate a list of issues.

ericcervin commented 1 year ago

I feel like I've been beating my head against this tonight.

In my Processing examples, I can translate then rotate.

tr

I can also rotate then translate.

rt

I can do the translate/rotate in Sketching but the rotate/translate breaks. I see where Sketching's Transform documentation acknowledges this saying, "use translate to change the origin to the point of revolution before calling rotate".

soegaard commented 1 year ago

@ericcervin Thanks for the bug report!

Turns out there was a mistake in translate.

Note that the "use translate to change the origin to the point of revolution before calling rotate" is part of the processing manual too. When rotate is called it is called with only the angle, so which point should he the rotation point? The rotation point is set by translate. If translate isn't called before the first rotate, then the rotation point used is (0,0).

I have commited a bug fix. Let me know if it works.

ericcervin commented 1 year ago

Thanks. That looks like what I wanted.

image