Transforms
Each View
is a flat, 2D surface bound to a 2D grid. This means Views can only have an x
/y
position, and an area within the plane. However, Views can also be transformed using Affine Transformations (and perspective) that alter the way they are displayed. These transforms allow you to change a View's position and shape in the full 3D volume.
Use the right slider to change the camera distance from the View, which changes the intensity of the perspective used to render it.
Affine transforms
Each View has a transform
that can be set at any time to change how it is rendered. Changes to this property will update the View on screen if it is visible
. Transforms are very lightweight and do not require re-rendering or trigger layouts when modified. This makes them a great option for animations and other UI treatments.
import io.nacular.doodle.core.View
import io.nacular.doodle.core.center
import io.nacular.doodle.drawing.AffineTransform.Companion.Identity
import io.nacular.measured.units.Angle.Companion.degrees
import io.nacular.measured.units.times
fun affineTransform(view: View) {
//sampleStart
view.transform = Identity
view.transform = Identity.rotateX(around = view.center, by = 45 * degrees)
//sampleEnd
}
Multiple AffineTransform
instances can also be combined to produce more complex operations. The end result is a single transform that will behave as though each operation were done in succession.
import io.nacular.doodle.drawing.AffineTransform
import io.nacular.doodle.geometry.Point
import io.nacular.measured.units.Angle.Companion.degrees
import io.nacular.measured.units.Angle.Companion.radians
import io.nacular.measured.units.times
import kotlin.math.PI
fun affineTransformChaining(transform: AffineTransform) {
//sampleStart
transform.
rotateY(by = 45 * degrees, around = Point( 2, 4)).
rotateX(by = PI * radians, around = Point(10, 3)).
//...
flipHorizontally()
//sampleEnd
}
Identity transform
Views default to the Identity
transform, which does not change the View at all. Set this to effectively clear a View's transform.
import io.nacular.doodle.core.View
import io.nacular.doodle.drawing.AffineTransform.Companion.Identity
fun identityTransform(view: View) {
//sampleStart
// Scale view by 2 in x and y
view.transform = Identity.scale(2.0, 2.0)
// ...
// Remove all transform effects
view.transform = Identity
//sampleEnd
}
Cameras
AffineTransform
s warp an object while preserving collinearity (points on a line remain on a line) and distance ratios. This means they are insufficient to produce perspective. Perspective requires a transformation that preserves collinearity, but not distance ratios. Doodle supports this via Camera
s. Each Camera has a position
(relative to a View's parent) and distance
, which define where vanishing points will be and the intensity their parallel warping effect. Each View has a camera
that will combine with its transform
to produce a final perspective transformation.
Drag the camera to change its position. Resize it to change its distance from the plane.
Sharing cameras
Views can share a single Camera, which allows them to share the same perspective when they have the same parent (or are all top-level). The result, as you can see below is the effect of the Views sharing the same 3D space.
A View's Camera position is relative to its parent, so shared Cameras only produce the single 3D space for Views with the same parent.
2-sided views
Views render onto an infinitely flat plane that is similar to a sheet of glass. This means you see the contents of a View that is flipped or rotated as though you are looking through the back of that pane of glass. You can have Views with 2-sides of course, as you see with the playing cards. But that requires logic to decide when the View's back is visible. The playing card does this by recomputing its face direction whenever its transform
or camera
change, and repainting if needed.
import io.nacular.doodle.core.View
fun View.twoSidedView() {
//sampleStart
// Example of how one might determine whether a View is facing the user.
// This takes the cross product of 2 vectors on the View's surface and
// applies its transformation and camera.
val points = (camera.projection * transform).invoke(bounds.points.take(3))
val faceUp = (points[1] - points[0] cross points[2] - points[1]).z > 0.0
//sampleEnd
}