Images
Images are rendered directly to the Canvas
as primitives just like text and shapes. This means transformations and other rendering capabilities apply to images as well. You load Image
s into your app using the ImageLoader
. This interface provides an async API for fetching images from different sources.
You must include the ImageModule
(Web, Desktop) in your application in order to use these features.
package rendering
import io.nacular.doodle.application.Application
import io.nacular.doodle.application.Modules.Companion.ImageModule
import io.nacular.doodle.application.application
import io.nacular.doodle.core.Display
import io.nacular.doodle.image.ImageLoader
import org.kodein.di.instance
class ImageLoaderApp(display: Display, images: ImageLoader): Application {
override fun shutdown() {}
}
fun imageLoader() {
//sampleStart
application(modules = listOf(ImageModule)) {
ImageLoaderApp(display = instance(), images = instance())
}
//sampleEnd
}
Doodle uses opt-in modules like this to improve bundle size.
Loading from resource
ImageLoader
provides APIs for loading images from various sources, like urls, file-paths, and LocalFiles
that are obtained during drag-drop or via a FileSelector.
The following examples shows how loading works. Notice that ImageLoader.load
returns Image?
, which is null
when the image fails to load for some reason. Fetching is also async, so it must be done from a suspend
method or a CoroutineScope
.
package rendering
import io.nacular.doodle.image.Image
import io.nacular.doodle.image.ImageLoader
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
fun loadImage(imageLoader: ImageLoader, scope: CoroutineScope) {
//sampleStart
scope.launch {
val image: Image? = imageLoader.load("some_image_path")
// won't get here until load resolves
image?.let {
// ...
}
}
//sampleEnd
}
See here for an example of how you might handle time-outs.
Rendering
Image
s are treated like primitive elements of the rendering pipeline. They are rendered directly to a Canvas
like other shapes and text.
You are able to define the rectangular region within an image that will be put onto the Canvas
, as well as where on the Canvas
that region will be placed. These two values allow you to zoom and scale images as you draw them.
- Image
- Cropped
- Aspect Cropped
package io.nacular.doodle.docs.apps
import io.nacular.doodle.application.Application
import io.nacular.doodle.application.Modules.Companion.ImageModule
import io.nacular.doodle.application.Modules.Companion.PointerModule
import io.nacular.doodle.application.application
import io.nacular.doodle.core.Display
import io.nacular.doodle.core.center
import io.nacular.doodle.core.height
import io.nacular.doodle.core.view
import io.nacular.doodle.core.width
import io.nacular.doodle.docs.utils.controlBackgroundColor
import io.nacular.doodle.drawing.paint
import io.nacular.doodle.geometry.Rectangle
import io.nacular.doodle.geometry.Size
import io.nacular.doodle.geometry.centered
import io.nacular.doodle.image.ImageLoader
import io.nacular.doodle.utils.Resizer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.kodein.di.instance
import org.w3c.dom.HTMLElement
import kotlin.math.min
class ImageApp(
display : Display,
imageLoader: ImageLoader,
appScope : CoroutineScope
): Application {
init {
appScope.launch {
//sampleStart
val image = imageLoader.load("/doodle/images/photo.jpg") ?: return@launch
display.children += view {
render = {
image(image, destination = bounds.atOrigin)
}
Resizer(this)
}
//sampleEnd
display.fill(controlBackgroundColor.paint)
when {
display.size.empty -> display.sizeChanged += { _,_,_ -> setInitialBounds(display, image.size) }
else -> setInitialBounds(display, image.size)
}
}
}
private fun setInitialBounds(display: Display, imageSize: Size) {
with(display.first()) {
if (size.empty && !display.size.empty) {
var w = min(display.width / 2, display.height - 40)
var h = w * imageSize.height / imageSize.width
if (h > display.height - 40) {
h = display.height - 40
w = h * imageSize.width / imageSize.height
}
display.first().bounds = Rectangle(w, h).centered(display.center)
}
}
}
override fun shutdown() {}
companion object {
private val appModules = listOf(PointerModule, ImageModule)
operator fun invoke(root: HTMLElement) = application(root, modules = appModules) {
ImageApp(instance(), instance(), CoroutineScope(SupervisorJob() + Dispatchers.Default))
}
}
}
package io.nacular.doodle.docs.apps
import io.nacular.doodle.application.Application
import io.nacular.doodle.application.Modules.Companion.ImageModule
import io.nacular.doodle.application.Modules.Companion.PointerModule
import io.nacular.doodle.application.application
import io.nacular.doodle.core.Display
import io.nacular.doodle.core.center
import io.nacular.doodle.core.height
import io.nacular.doodle.core.view
import io.nacular.doodle.core.width
import io.nacular.doodle.docs.utils.controlBackgroundColor
import io.nacular.doodle.drawing.paint
import io.nacular.doodle.geometry.Rectangle
import io.nacular.doodle.geometry.centered
import io.nacular.doodle.geometry.div
import io.nacular.doodle.image.ImageLoader
import io.nacular.doodle.utils.Resizer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.kodein.di.instance
import org.w3c.dom.HTMLElement
import kotlin.math.min
class CroppedImageApp(
display : Display,
imageLoader: ImageLoader,
appScope : CoroutineScope
): Application {
init {
appScope.launch {
//sampleStart
imageLoader.load("/doodle/images/photo.jpg")?.let { image ->
display.children += view {
render = {
image(image, source = Rectangle(image.size / 2), destination = bounds.atOrigin)
}
Resizer(this)
}
}
//sampleEnd
display.fill(controlBackgroundColor.paint)
when {
display.size.empty -> display.sizeChanged += { _,_,_ -> setInitialBounds(display) }
else -> setInitialBounds(display)
}
}
}
private fun setInitialBounds(display: Display) {
with(display.first()) {
if (size.empty && !display.size.empty) {
display.first().bounds = Rectangle(min(display.width / 2, display.height - 80)).centered(display.center)
}
}
}
override fun shutdown() {}
companion object {
private val appModules = listOf(PointerModule, ImageModule)
operator fun invoke(root: HTMLElement) = application(root, modules = appModules) {
CroppedImageApp(instance(), instance(), CoroutineScope(SupervisorJob() + Dispatchers.Default))
}
}
}
package io.nacular.doodle.docs.apps
import io.nacular.doodle.application.Application
import io.nacular.doodle.application.Modules.Companion.ImageModule
import io.nacular.doodle.application.Modules.Companion.PointerModule
import io.nacular.doodle.application.application
import io.nacular.doodle.core.Display
import io.nacular.doodle.core.center
import io.nacular.doodle.core.height
import io.nacular.doodle.core.view
import io.nacular.doodle.core.width
import io.nacular.doodle.docs.utils.controlBackgroundColor
import io.nacular.doodle.drawing.Color.Companion.White
import io.nacular.doodle.drawing.opacity
import io.nacular.doodle.drawing.paint
import io.nacular.doodle.geometry.Point
import io.nacular.doodle.geometry.Rectangle
import io.nacular.doodle.geometry.centered
import io.nacular.doodle.geometry.div
import io.nacular.doodle.image.ImageLoader
import io.nacular.doodle.image.height
import io.nacular.doodle.image.width
import io.nacular.doodle.utils.Resizer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.kodein.di.instance
import org.w3c.dom.HTMLElement
import kotlin.math.min
class AspectCroppedImageApp(
display : Display,
imageLoader: ImageLoader,
appScope : CoroutineScope
): Application {
init {
appScope.launch {
//sampleStart
imageLoader.load("/doodle/images/photo.jpg")?.let { image ->
display.children += view {
val aspect = image.width / image.height
render = {
var h = height
var w = h * aspect
if (w > width) {
w = width
h = w / aspect
}
rect(bounds.atOrigin, fill = (White opacity 0.75f).paint)
image(
image = image,
source = Rectangle(image.size / 2),
destination = Rectangle(w, h).centered(Point(width / 2, height / 2))
)
}
Resizer(this)
}
}
//sampleEnd
display.fill(controlBackgroundColor.paint)
when {
display.size.empty -> display.sizeChanged += { _,_,_ -> setInitialBounds(display) }
else -> setInitialBounds(display)
}
}
}
private fun setInitialBounds(display: Display) {
with(display.first()) {
if (size.empty && !display.size.empty) {
display.first().bounds = Rectangle(min(display.width / 2, display.height - 40)).centered(display.center)
}
}
}
override fun shutdown() {}
companion object {
private val appModules = listOf(PointerModule, ImageModule)
operator fun invoke(root: HTMLElement) = application(root, modules = appModules) {
AspectCroppedImageApp(instance(), instance(), CoroutineScope(SupervisorJob() + Dispatchers.Default))
}
}
}
You can also control the corner radius, and opacity of the image being drawn.