Add Rectangle.fit(Rectangle):Matrix44, FitMethod.Fill, FitMethod.None (#253)
This commit is contained in:
@@ -2,27 +2,44 @@
|
|||||||
|
|
||||||
Draws the given image making sure it fits (`contain`) or it covers (`cover`) the specified area.
|
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
|
## 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
|
or
|
||||||
- `contain`
|
|
||||||
- `cover`
|
```
|
||||||
|
drawer.imageFit(
|
||||||
horizontal values
|
img: ColorBuffer,
|
||||||
- left ... right
|
bounds: Rectangle,
|
||||||
- `-1.0` ... `1.0`
|
fitMethod: FitMethod,
|
||||||
|
horizontalPosition: Double,
|
||||||
vertical values
|
verticalPosition: Double)
|
||||||
- top ... bottom
|
```
|
||||||
- `-1.0` ... `1.0`
|
|
||||||
|
- `img`: the image to draw
|
||||||
## Example
|
- `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
|
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.
|
`imageFit` uses the cover mode, which fills the target rectangle with an image.
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import ScreenshotsHelper.collectScreenshots
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("multiplatform")
|
kotlin("multiplatform")
|
||||||
kotlin("plugin.serialization")
|
kotlin("plugin.serialization")
|
||||||
@@ -10,7 +12,8 @@ kotlin {
|
|||||||
defaultSourceSet {
|
defaultSourceSet {
|
||||||
kotlin.srcDir("src/demo")
|
kotlin.srcDir("src/demo")
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":orx-camera"))
|
implementation(project(":orx-shapes"))
|
||||||
|
implementation(project(":orx-image-fit"))
|
||||||
implementation(libs.openrndr.application)
|
implementation(libs.openrndr.application)
|
||||||
implementation(libs.openrndr.extensions)
|
implementation(libs.openrndr.extensions)
|
||||||
runtimeOnly(libs.openrndr.gl3.core)
|
runtimeOnly(libs.openrndr.gl3.core)
|
||||||
@@ -18,6 +21,9 @@ kotlin {
|
|||||||
implementation(compilations["main"]!!.output.allOutputs)
|
implementation(compilations["main"]!!.output.allOutputs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
collectScreenshots {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
testRuns["test"].executionTask.configure {
|
testRuns["test"].executionTask.configure {
|
||||||
@@ -74,4 +80,4 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,120 +2,142 @@ package org.openrndr.extra.imageFit
|
|||||||
|
|
||||||
import org.openrndr.draw.ColorBuffer
|
import org.openrndr.draw.ColorBuffer
|
||||||
import org.openrndr.draw.Drawer
|
import org.openrndr.draw.Drawer
|
||||||
|
import org.openrndr.math.Matrix44
|
||||||
import org.openrndr.math.Vector2
|
import org.openrndr.math.Vector2
|
||||||
import org.openrndr.math.map
|
import org.openrndr.math.transforms.transform
|
||||||
import org.openrndr.shape.Rectangle
|
import org.openrndr.shape.Rectangle
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Available `object-fit` methods (borrowed from CSS)
|
||||||
|
*/
|
||||||
enum class FitMethod {
|
enum class FitMethod {
|
||||||
|
/** Cover target area. Crop the source image if needed. */
|
||||||
Cover,
|
Cover,
|
||||||
Contain
|
|
||||||
|
/** Fit image in target area. Add margins if needed. */
|
||||||
|
Contain,
|
||||||
|
|
||||||
|
/** Deform source image to match the target area. */
|
||||||
|
Fill,
|
||||||
|
|
||||||
|
/** Maintain original image scale, crop to target area size. */
|
||||||
|
None
|
||||||
|
|
||||||
|
/** Not implemented */
|
||||||
|
// ScaleDown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(
|
fun fitRectangle(
|
||||||
src: Rectangle,
|
src: Rectangle,
|
||||||
dest: Rectangle,
|
dest: Rectangle,
|
||||||
horizontalPosition: Double = 0.0,
|
horizontalPosition: Double = 0.0,
|
||||||
verticalPosition: Double = 0.0,
|
verticalPosition: Double = 0.0,
|
||||||
fitMethod: FitMethod = FitMethod.Cover
|
fitMethod: FitMethod = FitMethod.Cover
|
||||||
): Pair<Rectangle, Rectangle> {
|
): Pair<Rectangle, Rectangle> {
|
||||||
val sourceWidth = src.width
|
val positionNorm = Vector2(horizontalPosition, verticalPosition) * 0.5 + 0.5
|
||||||
val sourceHeight = src.height
|
val (scaleX, scaleY) = dest.dimensions / src.dimensions
|
||||||
|
|
||||||
val targetX: Double
|
return when (fitMethod) {
|
||||||
val targetY: Double
|
FitMethod.Cover -> {
|
||||||
var targetWidth: Double
|
val actualDimensions = dest.dimensions / max(scaleX, scaleY)
|
||||||
var targetHeight: Double
|
val actualSrc = Rectangle(
|
||||||
|
src.corner + (src.dimensions - actualDimensions) * positionNorm,
|
||||||
val source: Rectangle
|
actualDimensions.x, actualDimensions.y
|
||||||
val target: Rectangle
|
)
|
||||||
|
Pair(actualSrc, dest)
|
||||||
val (x, y) = dest.corner
|
|
||||||
val width = dest.width
|
|
||||||
val height = dest.height
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
source = Rectangle(0.0, 0.0, sourceWidth, sourceHeight)
|
|
||||||
target = Rectangle(targetX, targetY, targetWidth, targetHeight)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FitMethod.Cover -> {
|
FitMethod.Contain -> {
|
||||||
targetWidth = sourceWidth
|
val actualDimensions = src.dimensions * min(scaleX, scaleY)
|
||||||
targetHeight = sourceHeight
|
val actualDest = Rectangle(
|
||||||
|
dest.corner + (dest.dimensions - actualDimensions) * positionNorm,
|
||||||
|
actualDimensions.x, actualDimensions.y
|
||||||
|
)
|
||||||
|
Pair(src, actualDest)
|
||||||
|
}
|
||||||
|
|
||||||
if (sourceWidth <= targetWidth) {
|
FitMethod.Fill -> Pair(src, dest)
|
||||||
targetWidth = sourceWidth
|
FitMethod.None -> {
|
||||||
targetHeight = (height / width) * sourceWidth
|
val actualSrc = Rectangle(
|
||||||
}
|
src.corner + (src.dimensions - dest.dimensions) * positionNorm,
|
||||||
|
dest.width, dest.height
|
||||||
if (sourceHeight <= targetHeight) {
|
)
|
||||||
targetHeight = sourceHeight
|
Pair(actualSrc, dest)
|
||||||
targetWidth = (width / height) * sourceHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
val left = 0.0
|
|
||||||
val right = sourceWidth - targetWidth
|
|
||||||
val top = 0.0
|
|
||||||
val bottom = sourceHeight - targetHeight
|
|
||||||
|
|
||||||
targetX = map(-1.0, 1.0, left, right, horizontalPosition)
|
|
||||||
targetY = map(-1.0, 1.0, top, bottom, verticalPosition)
|
|
||||||
|
|
||||||
source = Rectangle(targetX, targetY, targetWidth, targetHeight)
|
|
||||||
target = Rectangle(x, y, width, height)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Pair(source, target)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Drawer.imageFit(
|
/**
|
||||||
img: ColorBuffer,
|
* Helper function that calls [fitRectangle] and returns a [Matrix44] instead
|
||||||
x: Double = 0.0,
|
* of a `Pair<Rectangle, Rectangle>`. The returned matrix can be used to draw
|
||||||
y: Double = 0.0,
|
* scaled `Shape` or `ShapeContour` objects.
|
||||||
width: Double = img.width.toDouble(),
|
*
|
||||||
height: Double = img.height.toDouble(),
|
* Example scaling and centering a collection of ShapeContours inside
|
||||||
horizontalPosition: Double = 0.0,
|
* `drawer.bounds` leaving a margin of 50 pixels:
|
||||||
verticalPosition: Double = 0.0,
|
*
|
||||||
fitMethod: FitMethod = FitMethod.Cover
|
* val src = shapeContours.map { it.bounds }.bounds
|
||||||
): Pair<Rectangle, Rectangle> {
|
* 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(
|
val (source, target) = fitRectangle(
|
||||||
img.bounds,
|
this,
|
||||||
Rectangle(x, y, width, height),
|
dest,
|
||||||
horizontalPosition,
|
horizontalPosition,
|
||||||
verticalPosition,
|
verticalPosition,
|
||||||
fitMethod
|
fitMethod
|
||||||
)
|
)
|
||||||
|
return transform {
|
||||||
image(img, source, target)
|
translate(target.corner)
|
||||||
return Pair(source, target)
|
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(
|
fun Drawer.imageFit(
|
||||||
img: ColorBuffer,
|
img: ColorBuffer,
|
||||||
bounds: Rectangle = Rectangle(Vector2.ZERO, img.width * 1.0, img.height * 1.0),
|
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 = img.bounds,
|
||||||
horizontalPosition: Double = 0.0,
|
horizontalPosition: Double = 0.0,
|
||||||
verticalPosition: Double = 0.0,
|
verticalPosition: Double = 0.0,
|
||||||
fitMethod: FitMethod = FitMethod.Cover
|
fitMethod: FitMethod = FitMethod.Cover
|
||||||
|
|||||||
70
orx-image-fit/src/demo/kotlin/DemoImageFit01.kt
Normal file
70
orx-image-fit/src/demo/kotlin/DemoImageFit01.kt
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import org.openrndr.application
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import org.openrndr.draw.ColorBuffer
|
||||||
|
import org.openrndr.draw.isolatedWithTarget
|
||||||
|
import org.openrndr.draw.loadFont
|
||||||
|
import org.openrndr.draw.renderTarget
|
||||||
|
import org.openrndr.extra.imageFit.FitMethod
|
||||||
|
import org.openrndr.extra.imageFit.imageFit
|
||||||
|
import org.openrndr.extra.shapes.grid
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests `drawer.imageFit()` with all FitMethods for portrait and landscape images.
|
||||||
|
*/
|
||||||
|
fun main() = application {
|
||||||
|
configure {
|
||||||
|
width = 1600
|
||||||
|
height = 900
|
||||||
|
}
|
||||||
|
|
||||||
|
program {
|
||||||
|
val font = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 18.0)
|
||||||
|
|
||||||
|
// Create a test image with circles
|
||||||
|
fun makeImage(cols: Int, rows: Int, side: Int = 400): ColorBuffer {
|
||||||
|
val rt = renderTarget(cols * side, rows * side) {
|
||||||
|
colorBuffer()
|
||||||
|
}
|
||||||
|
drawer.isolatedWithTarget(rt) {
|
||||||
|
clear(ColorRGBa.WHITE)
|
||||||
|
stroke = null
|
||||||
|
ortho(rt)
|
||||||
|
bounds.grid(cols, rows).flatten().forEachIndexed { i, it ->
|
||||||
|
fill = if (i % 2 == 0) ColorRGBa.PINK else ColorRGBa.GRAY
|
||||||
|
circle(it.center, side / 2.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rt.colorBuffer(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
val layouts = mapOf(
|
||||||
|
"portrait" to makeImage(1, 2),
|
||||||
|
"landscape" to makeImage(2, 1)
|
||||||
|
)
|
||||||
|
val fitMethods = FitMethod.values()
|
||||||
|
|
||||||
|
val grid = drawer.bounds.grid(fitMethods.size, layouts.size, 30.0, 30.0, 30.0, 30.0)
|
||||||
|
|
||||||
|
extend {
|
||||||
|
drawer.fontMap = font
|
||||||
|
drawer.stroke = null
|
||||||
|
fitMethods.forEachIndexed { y, fitMethod ->
|
||||||
|
layouts.entries.forEachIndexed { x, (layoutName, img) ->
|
||||||
|
val cell = grid[x][y]
|
||||||
|
// In each grid cell draw 9 fitted images combining
|
||||||
|
// [left, center, right] and [top, center, bottom] alignment
|
||||||
|
val subgrid = cell.grid(3, 3, 0.0, 0.0, 4.0, 4.0)
|
||||||
|
subgrid.forEachIndexed { yy, rects ->
|
||||||
|
rects.forEachIndexed { xx, rect ->
|
||||||
|
drawer.fill = ColorRGBa.WHITE.shade(0.25)
|
||||||
|
drawer.rectangle(rect)
|
||||||
|
drawer.imageFit(img, rect, xx - 1.0, yy - 1.0, fitMethod)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drawer.fill = ColorRGBa.WHITE
|
||||||
|
drawer.text("${fitMethod.name}, $layoutName", cell.position(0.0, 1.038).toInt().vector2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user