#
# Created by Boyd Multerer on 2021-04-19.
# Copyright © 2021 Kry10 Limited. All rights reserved.
#
defmodule Scenic.Assets.Stream.Bitmap do
@moduledoc """
This module helps you to prepare images, in the form of a bitmap, that are to be streamed
and displayed through the `Scenic.Assets.Stream` module.
A bitmap is a rectangular field of pixels. Each pixel can be addressed and assigned a color.
When the bitmap is put into `Scenic.Assets.Stream` it becomes an image that can be displayed
in a scene via `Scenic.Primitive.Style.Paint.Stream`.
### Committed vs. Mutable
Bitmaps are interesting because a typical pattern is to change the color of many pixels in
a rapid burst, then send the image up. The bitmaps can become quite large tho, so if we
were to make a copy of it every time a single pixel was changed, that could become quite
slow.
Unfortunately, writing a NIF that manipulates individual pixels quickly and without making
a copy, breaks the immutable, functional model of Erlang/Elixir.
The compromise is that a Bitmap can be either in a "commited" state, which can be put
into `Scenic.Assets.Stream`, but not changed, or in a "mutable" state, which can be
manipulated rapidly, but not streamed to scenes.
When a new bitmap is built, it starts in the mutable state, unless the `commit: true` option is set.
```elixir
alias Scenic.Assets.Stream.Bitmap
bitmap = Bitmap.build( :rgb, 20, 10, clear: :blue )
|> Bitmap.put( 2, 3, :red )
|> Bitmap.put( 9, 10, :yellow )
|> Bitmap.commit()
Scenic.Assets.Stream.put( "stream_id", bitmap )
```
In the above example, a new bitmap is created, that can hold an rgb color in every pixel,
is 20 pixels wide, 10 pixels high, and starts with the entire image set to the color `:blue`.
The `:commit` option is not set, so it is mutable.
Then two of the pixels are set to other colors. One `:red` and the other `:yellow`.
Finally, the image is committed, making it usable, but no longer mutable. After the image is
completed, it is sent to `Scenic.Assets.Stream`, which makes it available for use in a scene.
### Color Depth
Bitmaps can be one of four depths. Each consumes a different amount of memory per pixel.
If you are running on a constrained memory device, or are worried about bandwidth when remoting
the UI, then you should choose the depth that you actually use. If you have lots of memory,
then `:rgba` is usually the fastest format.
| Depth | Bytes per pixel | Notes |
|---------------|------------------------|-----------|
| `:g` | 1 | Simple Greyscale. 256 shades of grey |
| `:ga` | 2 | Greyscale plus an alhpa channel |
| `:rgb` | 3 | Red/Green/Blue Millions of colors |
| `:rgba` | 4 | Red/Green/Blue/Alpha |
"""
alias Scenic.Assets.Stream.Bitmap
alias Scenic.Color
@app Mix.Project.config()[:app]
# load the NIF
@compile {:autoload, false}
@on_load :load_nifs
@doc false
def load_nifs do
:ok =
@app
|> :code.priv_dir()
|> :filename.join('bitmap')
|> :erlang.load_nif(0)
end
@type depth ::
:g
| :ga
| :rgb
| :rgba
@type meta :: {width :: pos_integer, height :: pos_integer, depth :: depth()}
@bitmap __MODULE__
@mutable :mutable_bitmap
@type t :: {__MODULE__, meta :: meta(), data :: binary}
@type m :: {:mutable_bitmap, meta :: meta(), data :: binary}
# --------------------------------------------------------
@doc """
Build a new bitmap with a given depth, width and height.
Build creates a new bitmap in memory. It begins in a mutable state
and will be set to transparent black unless the :clear option is specified.
The valid depths are :g, :ga, :rgb, :rgba as explained in the following table
| Depth | Bytes per pixel | Notes |
|---------------|------------------------|-----------|
| `:g` | 1 | Simple Greyscale. 256 shades of grey |
| `:ga` | 2 | Greyscale plus an alhpa channel |
| `:rgb` | 3 | Red/Green/Blue Millions of colors |
| `:rgba` | 4 | Red/Green/Blue/Alpha |
### Options
* `:clear` Set the new bitmap so that every pixel is the specified color.
* `:commit` Set to true to start the bitmap committed. Set to false for mutable. The default if not specified is mutable.
"""
@spec build(
depth :: Bitmap.depth(),
width :: pos_integer,
height :: pos_integer,
opts :: Keyword.t()
) :: t()
def build(format, width, height, opts \\ [])
def build(format, width, height, opts) do
bits =
case format do
:g -> 8 * width * height
:ga -> 8 * width * height * 2
:rgb -> 8 * width * height * 3
:rgba -> 8 * width * height * 4
end
m = {@mutable, {width, height, format}, <<0::size(bits)>>}
m =
case opts[:clear] do
nil -> m
color -> clear(m, color)
end
case opts[:commit] do
nil -> m
false -> m
true -> commit(m)
end
end
# --------------------------------------------------------
@doc """
Change a bitmap from committed to mutable.
This makes a copy of the bitmap's memory to preserve the Erlang model.
Mutable bitmaps are not usable by `Scenic.Assets.Stream`.
"""
@spec mutable(texture :: t()) :: mutable :: m()
def mutable({@bitmap, meta, bin}), do: {@mutable, meta, :binary.copy(bin)}
# --------------------------------------------------------
@doc """
Change a bitmap from mutable to committed.
Committed bitmaps can be used by `Scenic.Assets.Stream`. They will not
work with the `put` and `clear` functions in this module.
"""
@spec commit(mutable :: m()) :: texture :: t()
def commit({@mutable, meta, bin}), do: {@bitmap, meta, bin}
# --------------------------------------------------------
@doc """
Get the color value of a single pixel in a bitmap.
Works with either committed or mutable bitmaps.
"""
@spec get(t_or_m :: t() | m(), x :: pos_integer, y :: pos_integer) :: Color.explicit()
def get(texture, x, y)
def get({@mutable, meta, bin}, x, y), do: do_get(meta, bin, x, y)
def get({@bitmap, meta, bin}, x, y), do: do_get(meta, bin, x, y)
defp do_get({w, h, :g}, p, x, y)
when is_integer(x) and x >= 0 and x <= w and
is_integer(y) and y >= 0 and y <= h do
skip = y * w + x
<<_::binary-size(skip), g::8, _::binary>> = p
Color.to_g(g)
end
defp do_get({w, h, :ga}, p, x, y)
when is_integer(x) and x >= 0 and x <= w and
is_integer(y) and y >= 0 and y <= h do
skip = y * w * 2 + x * 2
<<_::binary-size(skip), g::8, a::8, _::binary>> = p
Color.to_ga({g, a})
end
defp do_get({w, h, :rgb}, p, x, y)
when is_integer(x) and x >= 0 and x <= w and
is_integer(y) and y >= 0 and y <= h do
skip = y * w * 3 + x * 3
<<_::binary-size(skip), r::8, g::8, b::8, _::binary>> = p
Color.to_rgb({r, g, b})
end
defp do_get({w, h, :rgba}, p, x, y)
when is_integer(x) and x >= 0 and x <= w and
is_integer(y) and y >= 0 and y <= h do
skip = y * w * 4 + x * 4
<<_::binary-size(skip), r::8, g::8, b::8, a::8, _::binary>> = p
Color.to_rgba({r, g, b, a})
end
# --------------------------------------------------------
@doc """
Set the color value of a single pixel in a bitmap.
Only works with mutable bitmaps.
The color you provide can be any valid value from the `Scenic.Color` module.
If the color you provide doesn't match the depth of the bitmap, this will
transform the color as appropriate to fit. For example, putting an `:rgb`
color into a `:g` (greyscale) bit map, will set the level of grey to be the average
value of the red, green, and blue channels of the supplied color
"""
@spec put(mutable :: m(), x :: pos_integer, y :: pos_integer, color :: Color.t()) ::
mutable :: m()
def put(mutable, x, y, color)
def put({@mutable, {w, h, :g}, p}, x, y, color)
when is_integer(x) and x >= 0 and x <= w and
is_integer(y) and y >= 0 and y <= h do
{:color_g, g} = Color.to_g(color)
nif_put(p, y * w + x, g)
{@mutable, {w, h, :g}, p}
end
def put({@mutable, {w, h, :ga}, p}, x, y, color)
when is_integer(x) and x >= 0 and x <= w and
is_integer(y) and y >= 0 and y <= h do
{:color_ga, {g, a}} = Color.to_ga(color)
nif_put(p, y * w + x, g, a)
{@mutable, {w, h, :ga}, p}
end
def put({@mutable, {w, h, :rgb}, p}, x, y, color)
when is_integer(x) and x >= 0 and x <= w and
is_integer(y) and y >= 0 and y <= h do
{:color_rgb, {r, g, b}} = Color.to_rgb(color)
nif_put(p, y * w + x, r, g, b)
{@mutable, {w, h, :rgb}, p}
end
def put({@mutable, {w, h, :rgba}, p}, x, y, color)
when is_integer(x) and x >= 0 and x <= w and
is_integer(y) and y >= 0 and y <= h do
{:color_rgba, {r, g, b, a}} = Color.to_rgba(color)
nif_put(p, y * w + x, r, g, b, a)
{@mutable, {w, h, :rgba}, p}
end
defp nif_put(_, _, _), do: :erlang.nif_error("Did not find nif_put_g")
defp nif_put(_, _, _, _), do: :erlang.nif_error("Did not find nif_put_ga")
defp nif_put(_, _, _, _, _), do: :erlang.nif_error("Did not find nif_put_rgb")
defp nif_put(_, _, _, _, _, _), do: :erlang.nif_error("Did not find nif_put_rgba")
# --------------------------------------------------------
@doc """
Set the color value of all pixels in a bitmap. This effectively erases the bitmap,
replacing it with a solid field of the supplied color.
Only works with mutable bitmaps.
The color you provide can be any valid value from the `Scenic.Color` module.
If the color you provide doesn't match the depth of the bitmap, this will
transform the color as appropriate to fit. For example, putting an `:rgb`
color into a `:g` (greyscale) bit map, will set the level of grey to be the average
value of the red, green, and blue channels of the supplied color
"""
@spec clear(mutable :: m(), color :: Color.t()) :: mutable :: m()
def clear(mutable, color)
def clear({@mutable, {w, h, :g}, p}, color) do
{:color_g, g} = Color.to_g(color)
nif_clear(p, g)
{@mutable, {w, h, :g}, p}
end
def clear({@mutable, {w, h, :ga}, p}, color) do
{:color_ga, {g, a}} = Color.to_ga(color)
nif_clear(p, g, a)
{@mutable, {w, h, :ga}, p}
end
def clear({@mutable, {w, h, :rgb}, p}, color) do
{:color_rgb, {r, g, b}} = Color.to_rgb(color)
nif_clear(p, r, g, b)
{@mutable, {w, h, :rgb}, p}
end
def clear({@mutable, {w, h, :rgba}, p}, color) do
{:color_rgba, {r, g, b, a}} = Color.to_rgba(color)
nif_clear(p, r, g, b, a)
{@mutable, {w, h, :rgba}, p}
end
def clear({@mutable, {_, _, :file}, _p}, _c) do
raise "Texture.clear(...) is not supported for file encoded data"
end
defp nif_clear(_, _), do: :erlang.nif_error("Did not find nif_clear_g")
defp nif_clear(_, _, _), do: :erlang.nif_error("Did not find nif_clear_ga")
defp nif_clear(_, _, _, _), do: :erlang.nif_error("Did not find nif_clear_rgb")
defp nif_clear(_, _, _, _, _), do: :erlang.nif_error("Did not find nif_clear_rgba")
# --------------------------------------------------------
@doc false
# @impl Scenic.Assets.Stream
@spec valid?(bitmap :: t()) :: boolean
def valid?(bitmap)
def valid?({@bitmap, {w, h, :g}, p}), do: byte_size(p) == w * h
def valid?({@bitmap, {w, h, :ga}, p}), do: byte_size(p) == w * h * 2
def valid?({@bitmap, {w, h, :rgb}, p}), do: byte_size(p) == w * h * 3
def valid?({@bitmap, {w, h, :rgba}, p}), do: byte_size(p) == w * h * 4
def valid?(_), do: false
end