Skip to content

Commit

Permalink
Add Rectangle.fit(Rectangle):Matrix44, FitMethod.Fill, FitMethod.None (
Browse files Browse the repository at this point in the history
  • Loading branch information
hamoid committed Aug 17, 2022
1 parent 7936e5d commit 1cdb387
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 109 deletions.
63 changes: 46 additions & 17 deletions orx-image-fit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,44 @@

Draws the given image making sure it fits (`contain`) or it covers (`cover`) the specified area.

Similar to CSS object-fit (https://www.w3schools.com/css/css3_object-fit.asp)
Similar to CSS object-fit (https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit)

`orx-image-fit` provides an extension function `imageFit` for `Drawer`.
`orx-image-fit` provides the `Drawer.imageFit` extension function.

## Usage

`imageFit(img: ColorBuffer, x: Double, y: Double, w: Double, h: Double, fitMethod, horizontalPosition:Double, verticalPosition:Double)`
```
drawer.imageFit(
img: ColorBuffer,
x: Double, y: Double, w: Double, h: Double,
fitMethod: FitMethod,
horizontalPosition: Double,
verticalPosition: Double)
```

fitMethod
- `contain`
- `cover`

horizontal values
- left ... right
- `-1.0` ... `1.0`

vertical values
- top ... bottom
- `-1.0` ... `1.0`

## Example
or

```
drawer.imageFit(
img: ColorBuffer,
bounds: Rectangle,
fitMethod: FitMethod,
horizontalPosition: Double,
verticalPosition: Double)
```

- `img`: the image to draw
- `x`, `y`, `w`, `h` or `bounds`: the target area where to draw the image
- `fitMethod`:
- `FitMethod.Contain`: fits `img` in the target area. If the aspect ratio of `img` and `bounds` differ it leaves blank horizontal or vertical margins to avoid deforming the image.
- `FitMethod.Cover`: covers the target area. . If the aspect ratio of `img` and `bounds` differ part of the image will be cropped away.
- `FitMethod.Fill`: deforms the image to exactly match the target area.
- `FitMethod.None`: draws the image on the target area without scaling it.
- `horizontalPosition` and `verticalPosition`: controls which part of the image is visible (`Cover`, `None`) or the alignment of the image (`Contain`).
- `horizontalPosition`: `-1.0` = left, `0.0` = center, `1.0` = right.
- `verticalPosition`: `-1.0` = top, `0.0` = center, `1.0` = bottom.

## Examples

A quick example that fits an image to the window rectangle with a 10 pixel margin. By default
`imageFit` uses the cover mode, which fills the target rectangle with an image.
Expand All @@ -37,4 +54,16 @@ fun main() = application {
}
}
```


or

```kotlin
fun main() = application {
program {
val image = loadImage("data/images/pm5544.png")
extend {
drawer.imageFit(image, drawer.bounds.offsetEdges(-10.0))
}
}
}
```
10 changes: 8 additions & 2 deletions orx-image-fit/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import ScreenshotsHelper.collectScreenshots

plugins {
kotlin("multiplatform")
kotlin("plugin.serialization")
Expand All @@ -10,14 +12,18 @@ kotlin {
defaultSourceSet {
kotlin.srcDir("src/demo")
dependencies {
implementation(project(":orx-camera"))
implementation(project(":orx-shapes"))
implementation(project(":orx-image-fit"))
implementation(libs.openrndr.application)
implementation(libs.openrndr.extensions)
runtimeOnly(libs.openrndr.gl3.core)
runtimeOnly(libs.openrndr.gl3.natives)
implementation(compilations["main"]!!.output.allOutputs)
}
}
collectScreenshots {

}
}
}
testRuns["test"].executionTask.configure {
Expand Down Expand Up @@ -74,4 +80,4 @@ kotlin {
}
}
}
}
}
202 changes: 112 additions & 90 deletions orx-image-fit/src/commonMain/kotlin/ImageFit.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,120 +2,142 @@ package org.openrndr.extra.imageFit

import org.openrndr.draw.ColorBuffer
import org.openrndr.draw.Drawer
import org.openrndr.math.Matrix44
import org.openrndr.math.Vector2
import org.openrndr.math.map
import org.openrndr.math.transforms.transform
import org.openrndr.shape.Rectangle
import kotlin.math.max
import kotlin.math.min


/**
* Available `object-fit` methods (borrowed from CSS)
*/
enum class FitMethod {
/** Cover target area. Crop the source image if needed. */
Cover,
Contain
}

fun fitRectangle(
src: Rectangle,
dest: Rectangle,
horizontalPosition: Double = 0.0,
verticalPosition: Double = 0.0,
fitMethod: FitMethod = FitMethod.Cover
): Pair<Rectangle, Rectangle> {
val sourceWidth = src.width
val sourceHeight = src.height

val targetX: Double
val targetY: Double
var targetWidth: Double
var targetHeight: Double
/** Fit image in target area. Add margins if needed. */
Contain,

val source: Rectangle
val target: Rectangle
/** Deform source image to match the target area. */
Fill,

val (x, y) = dest.corner
val width = dest.width
val height = dest.height
/** Maintain original image scale, crop to target area size. */
None

when (fitMethod) {
FitMethod.Contain -> {
targetWidth = width
targetHeight = height

if (width <= targetWidth) {
targetWidth = width
targetHeight = (sourceHeight / sourceWidth) * width
}

if (height <= targetHeight) {
targetHeight = height
targetWidth = (sourceWidth / sourceHeight) * height
}

val left = x
val right = x + width - targetWidth
val top = y
val bottom = y + height - targetHeight

targetX = map(-1.0, 1.0, left, right, horizontalPosition)
targetY = map(-1.0, 1.0, top, bottom, verticalPosition)
/** Not implemented */
// ScaleDown
}

source = Rectangle(0.0, 0.0, sourceWidth, sourceHeight)
target = Rectangle(targetX, targetY, targetWidth, targetHeight)
}
/**
* Transforms [src] and [dest] into a Pair in which one of the
* two rectangles is modified to conform with the [fitMethod]. It uses
* [horizontalPosition] and [verticalPosition] to control positioning / cropping.
*/
fun fitRectangle(
src: Rectangle,
dest: Rectangle,
horizontalPosition: Double = 0.0,
verticalPosition: Double = 0.0,
fitMethod: FitMethod = FitMethod.Cover
): Pair<Rectangle, Rectangle> {
val positionNorm = Vector2(horizontalPosition, verticalPosition) * 0.5 + 0.5
val (scaleX, scaleY) = dest.dimensions / src.dimensions

return when (fitMethod) {
FitMethod.Cover -> {
targetWidth = sourceWidth
targetHeight = sourceHeight

if (sourceWidth <= targetWidth) {
targetWidth = sourceWidth
targetHeight = (height / width) * sourceWidth
}

if (sourceHeight <= targetHeight) {
targetHeight = sourceHeight
targetWidth = (width / height) * sourceHeight
}

val left = 0.0
val right = sourceWidth - targetWidth
val top = 0.0
val bottom = sourceHeight - targetHeight
val actualDimensions = dest.dimensions / max(scaleX, scaleY)
val actualSrc = Rectangle(
src.corner + (src.dimensions - actualDimensions) * positionNorm,
actualDimensions.x, actualDimensions.y
)
Pair(actualSrc, dest)
}

targetX = map(-1.0, 1.0, left, right, horizontalPosition)
targetY = map(-1.0, 1.0, top, bottom, verticalPosition)
FitMethod.Contain -> {
val actualDimensions = src.dimensions * min(scaleX, scaleY)
val actualDest = Rectangle(
dest.corner + (dest.dimensions - actualDimensions) * positionNorm,
actualDimensions.x, actualDimensions.y
)
Pair(src, actualDest)
}

source = Rectangle(targetX, targetY, targetWidth, targetHeight)
target = Rectangle(x, y, width, height)
FitMethod.Fill -> Pair(src, dest)
FitMethod.None -> {
val actualSrc = Rectangle(
src.corner + (src.dimensions - dest.dimensions) * positionNorm,
dest.width, dest.height
)
Pair(actualSrc, dest)
}
}

return Pair(source, target)
}

fun Drawer.imageFit(
img: ColorBuffer,
x: Double = 0.0,
y: Double = 0.0,
width: Double = img.width.toDouble(),
height: Double = img.height.toDouble(),
horizontalPosition: Double = 0.0,
verticalPosition: Double = 0.0,
fitMethod: FitMethod = FitMethod.Cover
): Pair<Rectangle, Rectangle> {
/**
* Helper function that calls [fitRectangle] and returns a [Matrix44] instead
* of a `Pair<Rectangle, Rectangle>`. The returned matrix can be used to draw
* scaled `Shape` or `ShapeContour` objects.
*
* Example scaling and centering a collection of ShapeContours inside
* `drawer.bounds` leaving a margin of 50 pixels:
*
* val src = shapeContours.map { it.bounds }.bounds
* val dest = drawer.bounds.offsetEdges(-50.0)
* val mat = src.fit(dest, fitMethod = FitMethod.Contain)
* drawer.view *= mat
* drawer.contours(shapeContours)
*/
fun Rectangle.fit(
dest: Rectangle,
horizontalPosition: Double = 0.0,
verticalPosition: Double = 0.0,
fitMethod: FitMethod = FitMethod.Cover
): Matrix44 {
val (source, target) = fitRectangle(
img.bounds,
Rectangle(x, y, width, height),
horizontalPosition,
verticalPosition,
fitMethod
this,
dest,
horizontalPosition,
verticalPosition,
fitMethod
)

image(img, source, target)
return Pair(source, target)
return transform {
translate(target.corner)
scale((target.dimensions / source.dimensions).vector3(z = 1.0))
translate(-source.corner)
}
}

/**
* Draws [img] into the bounding box defined by [x], [y], [width] and [height]
* using the specified [fitMethod]
* and aligned or cropped using [horizontalPosition] and [verticalPosition].
*/
fun Drawer.imageFit(
img: ColorBuffer,
x: Double = 0.0,
y: Double = 0.0,
width: Double = img.width.toDouble(),
height: Double = img.height.toDouble(),
horizontalPosition: Double = 0.0,
verticalPosition: Double = 0.0,
fitMethod: FitMethod = FitMethod.Cover
) = imageFit(
img,
Rectangle(x, y, width, height),
horizontalPosition,
verticalPosition,
fitMethod
)

/**
* Draws [img] into the bounding box defined by [bounds]
* using the specified [fitMethod]
* and aligned or cropped using [horizontalPosition] and [verticalPosition].
*/
fun Drawer.imageFit(
img: ColorBuffer,
bounds: Rectangle = Rectangle(Vector2.ZERO, img.width * 1.0, img.height * 1.0),
bounds: Rectangle = img.bounds,
horizontalPosition: Double = 0.0,
verticalPosition: Double = 0.0,
fitMethod: FitMethod = FitMethod.Cover
Expand Down
Loading

0 comments on commit 1cdb387

Please sign in to comment.