Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Smarter coordinate plane #944

Open
cdsmith opened this issue May 20, 2019 · 6 comments
Open

Smarter coordinate plane #944

cdsmith opened this issue May 20, 2019 · 6 comments
Labels
discussion Needs more information or major design decisions

Comments

@cdsmith
Copy link
Collaborator

cdsmith commented May 20, 2019

@alphalambda I'm creating this issue to capture thoughts about the coordinate plane.

Trying to sum up the current state of things: the built-in coordinatePlane picture extends from -10 to 10 in each direction. It is useful when it's added as-is with no transformations. However, attempting to scale it, translate it, etc. doesn't work so great, because (a) it's a fixed size, (b) the granularity of guidelines is fixed, and (c) the labels don't stay readable if you stretch them, especially non-uniformly. It would be great for a lot of educational purposes if one could instead get a coordinate plane of any dimension, and trust it to look right (the right bounds, readable labels, and a reasonable choice of guides).

The rest of this document is brainstorming how to make this work.

Option 0

Do nothing. CodeWorld doesn't need to solve all problems, and sometimes the complexity cost of adding a solution outweighs the benefit.

Option 1

Create a new function that draws a coordinate plane with specific parameters. Looks like this could be an internal size (range of values in x and y that can be represented on the plane), and maybe also external size (amount of screen space to draw it in).

Option 2

Make the coordinatePlane picture "smart", so it reacts to being transformed. It would be somewhat unique among pictures, both in that it's infinite in size (of course, the implementation would be to just draw the visible part), and would actually adjust the picture when it's transformed, so that for instance if it's scaled down, the granularity of the guidelines would change, and the labels would be drawn at the correct place, but without other transformations.

I don't think there's anything wrong with an infinite picture. None of the built-in pictures are infinite, but one could imagine changing that in some future world. It makes semantic sense. (Some students already complain about having to choose some arbitrarily large size to make a background for their programs, and would like a everywhere :: Picture that's infinite and solid, so they could write background = colored(everywhere, blue). I'm not proposing to add this, but it's not unreasonable.

The latter difference, though, definitely breaks the denotational semantics of the Picture type as Point -> Color (see #456) which I don't like if it's avoidable. Then again, coordinatePlane is kind of a debugging feature anyway, and debugging often breaks abstractions.

Option 3

(just brainstorming here; I don't really like this one.)

Deprecate coordinatePlane as a picture entirely. Instead, add a debug control to draw a coordinatePlane that can be toggled at runtime. In conjunction with #938, students could get more flexibility in the abstract size of their canvas, and that may satisfy many of the use cases for coordinate planes. On the other hand, it's bad for students who might want to use a coordinate plane as part of a bigger animation or drawing. Here's an example of that: https://code.world/#PrjRYVwRnctivph7EKKInwg I could never have made that animation had coordinatePlane been a toggle button in the UI instead of part of the code.

@cdsmith cdsmith added the discussion Needs more information or major design decisions label May 20, 2019
@alphalambda
Copy link
Contributor

How about this:

Option 4

Adopt the approach illustrated by Extras.Cw.graphed and Extras.Cw.wideGraphed, i.e., graphed(picture,scalex,scaley) works like scaled(picture,scalex,scaley) but it also frames the picture inside a graph that covers the whole output and shows labels and guides that correspond to the given scales. With this function, we could just define:

coordinatePlane = graphed(blank,1,1)

@cdsmith
Copy link
Collaborator Author

cdsmith commented May 20, 2019

Sure, this is basically option 1a, right? Unless I misunderstand, you could do either:

graphed(pic, x, y) = ...
sizedCoordinatePlane(x, y) = graphed(blank, x, y)

or:

sizedCoordinatePlane(x, y) = ...
graphed(pic, x, y) = pic & sizedCoordinatePlane(x, y)

I like the second of these options a little better, because it does a little better in keeping the primitives simple and composing them in familiar ways. Or am I misunderstanding this graphed function?

@alphalambda
Copy link
Contributor

There is a small difference in the point of view.

The way I see it, you define a picture at a fixed size, and then you can zoom into it without changing the actual definition of the picture:

https://code.world/#PRaCjCIjqE20tb_uFqByWeQ

The way you see it, if I understood you correctly, you define a fixed coordinate plane, and then you can resize your picture to fit it:

https://code.world/#Pf8e4Z7BkGpkxnUGV01yp9Q

Both approaches accomplish the same thing, but depending on what you want to do, one may be
simpler than the other. My claim is that what people usually want is the first, not the second, and so that case should be as simple as possible.

@alphalambda
Copy link
Contributor

Actually, maybe people want both approaches. The first is useful for graphing and the second is useful for debugging. I'm just not too fond of having students paint by number, so I try to discourage it.

@cdsmith
Copy link
Collaborator Author

cdsmith commented May 21, 2019

Okay, I see one thing I was missing. To amend my earlier comment, the options are actually:

graphed(pic, x, y) = primitive
sizedCoordinatePlane(x, y) = graphed(blank, x, y)

or

graphed(pic, x, y) = scaled(pic, x, y) & sizedCoordinatePlane(x, y)
sizedCoordinatePlane(x, y) = primitive

Now it makes more sense why you'd choose graphed as a cohesive operation, because it's important for the scaling factor to be the same between the picture and coordinate plane.

Now, if we implemented option 2, then we'd get this:

smartCoordinatePlane = primitive
graphed(pic, x, y) = scaled(pic & smartCoordinatePlane, x, y)
sizedCoordinatePlane = scaled(smartCoordinatePlane, x, y)

(but at the cost of weakening the simple model where Picture ~ Point -> Color)

cdsmith added a commit that referenced this issue May 28, 2019
not implement the CanvasRenderingContext2d.canvas property, I
decided to plumb the size through the CanvasM monad for the JS
implementation.  That could be changed.

This currently doesn't change anything.  However, it does pave the
road to make use of this information when drawing.  One use case is
an infinite coordinate plane a la #944, which would need to use the
screen size and current transformation to decide which subset of
the infinite plane to draw.
@cdsmith
Copy link
Collaborator Author

cdsmith commented May 28, 2019

I'm playing around with this in my mind. I like the idea of option 2 more as I think about it. Technically, it's a violation of picture semantics; but I don't really care because coordinate planes are kind of like a debugging feature anyway.

The math isn't so hard. You just invert the transformation matrix to find the pre-image of all four corners of the screen, and then look for the minimum and maximum x and y coordinates among those four points. That works out to something like:

coordinateBounds screenW screenH transform@(a, b, c, d, e, f)
  | det == 0 = (0, 0, 0, 0)
  | otherwise = (minimum [x1, x2, x3, x4], maximum [x1, x2, x3, x4],
                 minimum [y1, y2, y3, y4], maximum [y1, y2, y3, y4])
  where det = a * e - b * d
        preimage x y = (( e * (x - c) - b * (y - f)) / det,
                        (-d * (x - c) + a * (y - f)) / det)
        (x1, y1) = preImage ( screenW / 2) ( screenH / 2)
        (x2, y2) = preImage (-screenW / 2) ( screenH / 2)
        (x3, y3) = preImage ( screenW / 2) (-screenH / 2)
        (x4, y4) = preImage (-screenW / 2) (-screenH / 2)

That gives you the region of the source coordinate plane to actually draw. Any point on the screen is inside a convex polygon with these vertices, and this fact is preserved by affine transformations, so this is always enough. If there's rotation or shear, it will occasionally draw too much, but it's not clear how to avoid that!
The resolution of guide lines can be deduced from there. Then the only remaining change is to draw the lettering for the labels without the transformation in effect (but project the location manually as a point so they are in the right place).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion Needs more information or major design decisions
Projects
None yet
Development

No branches or pull requests

2 participants