Current section
Files
Jump to
Current section
Files
lib/noise_map.ex
defmodule Isotope.NoiseMap do
@moduledoc """
A compact representation of a 2D noise map backed by a flat binary of
little-endian `f32` values in row-major order.
## Memory Layout
The `data` field is a binary where each value is a 32-bit float in
little-endian byte order. Values are stored row by row (row-major).
A 100x100 map uses exactly 40,000 bytes (~40 KB), compared to ~3.2 MB
for the equivalent nested Elixir list.
## Accessing Values
{:ok, noise} = Isotope.Noise.new()
nm = Isotope.Noise.noise_map(noise, {100, 100})
# Single value at column 10, row 5
Isotope.NoiseMap.get(nm, 10, 5)
# Entire row as a list of floats
Isotope.NoiseMap.row(nm, 0)
# Convert to nested list (for compatibility or serialization)
Isotope.NoiseMap.to_list(nm)
# Dimensions
{width, height} = Isotope.NoiseMap.size(nm)
## Enumerable
The `Enumerable` protocol is implemented, iterating over rows (each row
is a `[float()]`). This means standard `Enum` functions work directly:
# Average value per row
Enum.map(nm, fn row -> Enum.sum(row) / length(row) end)
# Find the row with the highest peak
Enum.max_by(nm, fn row -> Enum.max(row) end)
# First 5 rows
Enum.take(nm, 5)
## Migration from `[[float()]]`
If you have code that expects nested lists, use `Isotope.NoiseMap.to_list/1`:
nm = Isotope.Noise.noise_map(noise, {100, 100})
nested_list = Isotope.NoiseMap.to_list(nm)
"""
@enforce_keys [:data, :width, :height]
defstruct [:data, :width, :height]
@type t :: %__MODULE__{
data: binary(),
width: non_neg_integer(),
height: non_neg_integer()
}
@doc """
Returns the noise value at column `x`, row `y`.
Raises `ArgumentError` if the coordinates are out of bounds.
iex> nm = %Isotope.NoiseMap{data: <<1.0::float-little-32, 2.0::float-little-32, 3.0::float-little-32, 4.0::float-little-32>>, width: 2, height: 2}
iex> Isotope.NoiseMap.get(nm, 1, 0)
2.0
"""
@spec get(t(), non_neg_integer(), non_neg_integer()) :: float()
def get(%__MODULE__{data: data, width: w, height: h}, x, y)
when x >= 0 and x < w and y >= 0 and y < h do
offset = (y * w + x) * 4
<<_::binary-size(offset), val::float-little-32, _::binary>> = data
val
end
def get(%__MODULE__{width: w, height: h}, x, y) do
raise ArgumentError,
"coordinates (#{x}, #{y}) out of bounds for #{w}x#{h} noise map"
end
@doc """
Returns row `y` as a list of floats.
Raises `ArgumentError` if the row index is out of bounds.
iex> nm = %Isotope.NoiseMap{data: <<1.0::float-little-32, 2.0::float-little-32, 3.0::float-little-32, 4.0::float-little-32>>, width: 2, height: 2}
iex> Isotope.NoiseMap.row(nm, 0)
[1.0, 2.0]
"""
@spec row(t(), non_neg_integer()) :: [float()]
def row(%__MODULE__{data: data, width: w, height: h}, y)
when y >= 0 and y < h do
offset = y * w * 4
row_bytes = w * 4
<<_::binary-size(offset), row_data::binary-size(row_bytes), _::binary>> =
data
decode_row(row_data, [])
end
def row(%__MODULE__{height: h}, y) do
raise ArgumentError, "row #{y} out of bounds for noise map with #{h} rows"
end
@doc """
Converts the noise map to a nested list `[[float()]]` (list of rows).
iex> nm = %Isotope.NoiseMap{data: <<1.0::float-little-32, 2.0::float-little-32, 3.0::float-little-32, 4.0::float-little-32>>, width: 2, height: 2}
iex> Isotope.NoiseMap.to_list(nm)
[[1.0, 2.0], [3.0, 4.0]]
"""
@spec to_list(t()) :: [[float()]]
def to_list(%__MODULE__{height: h} = nm) do
for y <- 0..(h - 1), do: row(nm, y)
end
@doc """
Returns `{width, height}` of the noise map.
iex> nm = %Isotope.NoiseMap{data: <<0::128>>, width: 2, height: 2}
iex> Isotope.NoiseMap.size(nm)
{2, 2}
"""
@spec size(t()) :: {non_neg_integer(), non_neg_integer()}
def size(%__MODULE__{width: w, height: h}), do: {w, h}
defp decode_row(<<>>, acc), do: Enum.reverse(acc)
defp decode_row(<<val::float-little-32, rest::binary>>, acc),
do: decode_row(rest, [val | acc])
defimpl Enumerable do
def count(%Isotope.NoiseMap{height: h}), do: {:ok, h}
def member?(_nm, _element), do: {:error, __MODULE__}
def reduce(%Isotope.NoiseMap{height: h} = nm, acc, fun) do
reduce_rows(nm, 0, h, acc, fun)
end
defp reduce_rows(_nm, _y, _h, {:halt, acc}, _fun), do: {:halted, acc}
defp reduce_rows(nm, y, h, {:suspend, acc}, fun),
do: {:suspended, acc, &reduce_rows(nm, y, h, &1, fun)}
defp reduce_rows(_nm, h, h, {:cont, acc}, _fun), do: {:done, acc}
defp reduce_rows(nm, y, h, {:cont, acc}, fun) do
row = Isotope.NoiseMap.row(nm, y)
reduce_rows(nm, y + 1, h, fun.(row, acc), fun)
end
def slice(%Isotope.NoiseMap{height: h} = nm) do
{:ok, h, &slice_fun(nm, &1, &2, &3)}
end
defp slice_fun(nm, start, length, step) do
start..(start + (length - 1) * step)//step
|> Enum.map(&Isotope.NoiseMap.row(nm, &1))
end
end
end