├── tests ├── nim.cfg ├── gridpngs.nim ├── islands.nim └── zoom.nim ├── islands.png ├── .gitignore ├── src ├── nile.nim ├── io.nim ├── canvas.nim ├── vector.nim ├── noise.nim ├── distance.nim ├── image.nim ├── filter.nim └── grid.nim ├── LICENSE └── README.md /tests/nim.cfg: -------------------------------------------------------------------------------- 1 | --path:"../src/" 2 | -------------------------------------------------------------------------------- /islands.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prideout/nile/HEAD/islands.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache 2 | *.png 3 | .vscode 4 | tests/* 5 | !tests/*.nim 6 | default.profraw 7 | -islands.png 8 | -------------------------------------------------------------------------------- /src/nile.nim: -------------------------------------------------------------------------------- 1 | include grid 2 | include noise 3 | include distance 4 | include image 5 | include io 6 | include vector 7 | include canvas 8 | include filter 9 | -------------------------------------------------------------------------------- /src/io.nim: -------------------------------------------------------------------------------- 1 | import grid 2 | import image 3 | import nimPNG 4 | 5 | proc savePNG*(g: Grid, filename: string): void = 6 | discard savePNG(filename, g.toDataString(), LCT_GREY, 8, g.width, g.height) 7 | 8 | proc savePNG*(img: Image, filename: string): void = 9 | discard savePNG(filename, img.toDataString(), LCT_RGBA, 8, img.width, img.height) 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Philip Rideout 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/canvas.nim: -------------------------------------------------------------------------------- 1 | import cairo 2 | import image 3 | import math 4 | import vector 5 | 6 | type 7 | Canvas* = ref object 8 | data*: string 9 | width*, height*: int 10 | surface: PSurface 11 | context: PContext 12 | 13 | proc newCanvas*(width, height: int): Canvas = 14 | new(result) 15 | result.data = newString(width * height * 4) 16 | result.width = width 17 | result.height = height 18 | let 19 | w32 = int32(width) 20 | h32 = int32(height) 21 | stride = int32(width * 4) 22 | result.surface = image_surface_create(cstring(result.data), FORMAT_ARGB32, w32, h32, stride) 23 | result.context = result.surface.create() 24 | result.context.scale float64(width), float64(width) 25 | result.context.setLineWidth(0.005) 26 | 27 | proc toImage*(c: Canvas): Image = newImageFromDataString(c.data, c.width, c.height) 28 | 29 | proc scale*(c: Canvas; x, y: float): auto = 30 | c.context.scale(x, y); c 31 | 32 | proc setLineWidth*(c: Canvas; width: float): auto = 33 | c.context.set_line_width(width); c 34 | 35 | proc setColor*(c: Canvas; red, grn, blu, alp: float): auto = 36 | c.context.setSourceRgba(red, grn, blu, alp); c 37 | 38 | proc stroke*(c: Canvas): auto {.discardable.} = 39 | c.context.stroke; c 40 | 41 | proc fill*(c: Canvas): auto {.discardable.} = 42 | c.context.fill; c 43 | 44 | proc moveTo*(c: Canvas; pt: Vec2f): auto = 45 | c.context.move_to(pt.x, pt.y); c 46 | 47 | proc lineTo*(c: Canvas; pt: Vec2f): auto = 48 | c.context.line_to(pt.x, pt.y); c 49 | 50 | proc circle*(c: Canvas; pt: Vec2f, radius: float): auto = 51 | let ctx = c.context; ctx.save() 52 | ctx.translate(pt.x, pt.y) 53 | ctx.scale(radius, radius) 54 | ctx.arc(0, 0, 1, 0, 2 * PI) 55 | ctx.restore(); c 56 | -------------------------------------------------------------------------------- /tests/gridpngs.nim: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nim c --run 2 | 3 | import nile 4 | 5 | const diagramScale = 16 6 | 7 | let original = newGrid(""" 8 | 00000000 9 | 00111100 10 | 00100100 11 | 00111100 12 | 00100000 13 | 00100000 14 | 00100000 15 | 00000000""") 16 | original.setPixel(7, 7, 0.5) 17 | var mag = original.resizeBoxFilter(11, 11).resizeNearestFilter(diagramScale).drawGrid(11, 11) 18 | var ide = original.resizeBoxFilter(8, 8).resizeNearestFilter(diagramScale).drawGrid(8, 8) 19 | var min = original.resizeBoxFilter(5, 5).resizeNearestFilter(diagramScale).drawGrid(5, 5) 20 | min.savePNG("min0.png") 21 | mag.savePNG("mag0.png") 22 | ide.savePNG("ide0.png") 23 | 24 | let tiny = newGrid("000 010 000") 25 | min = tiny.resizeBoxFilter(1, 1).resizeNearestFilter(diagramScale).drawGrid(1, 1) 26 | mag = tiny.resizeBoxFilter(5, 5).resizeNearestFilter(diagramScale).drawGrid(5, 5) 27 | ide = tiny.resizeBoxFilter(9, 9).resizeNearestFilter(diagramScale).drawGrid(9, 9) 28 | min.savePNG("min1.png") 29 | mag.savePNG("mag1.png") 30 | ide.savePNG("ide1.png") 31 | 32 | let row = newGrid("010") 33 | mag = row.resize(5, 1, FilterHermite).resizeNearestFilter(diagramScale).drawGrid(5, 1, 1) 34 | mag.savePNG("mag2.png") 35 | mag = tiny.resize(5, 5, FilterHermite).resizeNearestFilter(diagramScale).drawGrid(5, 5, 1) 36 | mag.savePNG("mag3.png") 37 | mag = tiny.resize(128, 128, FilterHermite).drawGrid(1, 1, 1) 38 | mag.savePNG("mag4.png") 39 | 40 | let nearest = original.resizeNearestFilter(1000, 1000).resizeBoxFilter(100, 100) 41 | let hermite = original.resize(1000, 1000, FilterHermite).resizeBoxFilter(100, 100) 42 | let gauss = original.resize(1000, 1000, FilterGaussian).resizeBoxFilter(100, 100) 43 | let triangle = original.resize(1000, 1000, FilterTriangle).resizeBoxFilter(100, 100) 44 | (1 - hstack(nearest, hermite, gauss, triangle)).drawGrid(4, 1, 1).savePNG("min2.png") 45 | (0.2 + 0.5 * vstack(nearest, hermite, gauss, triangle)).drawGrid(1, 4, 1).savePNG("min3.png") 46 | 47 | let grads = 0.5f + generateGradientNoise(42, 256, 256, 2.0f) * 0.5f 48 | grads.drawGrid(1, 1, 1).savePNG("grads1.png") 49 | -------------------------------------------------------------------------------- /src/vector.nim: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | type 4 | Vec2f* = tuple[x, y: float32] 5 | Vec2i* = tuple[x, y: int32] 6 | Vec3f* = tuple[x, y, z: float32] 7 | Vec3ii* = tuple[x, y, z: int64] 8 | Viewport* = tuple[left, top, right, bottom: float32] 9 | 10 | const ENTIRE* = (0.0f, 0.0f, 1.0f, 1.0f) 11 | 12 | proc `*`*(a: Vec2f, b: Vec2f): Vec2f = (a.x * b.x, a.y * b.y) 13 | proc `*`*(a: Vec2f, b: float32): Vec2f = (a.x * b, a.y * b) 14 | proc `*`*(a: float32, b: Vec2f): Vec2f = (a * b.x, a * b.y) 15 | proc `/`*(a: Vec2f, b: float32): Vec2f = (a.x / b, a.y / b) 16 | proc `+`*(a: Vec2f, b: float32): Vec2f = (a.x + b, a.y + b) 17 | proc `-`*(a: Vec2f, b: float32): Vec2f = (a.x - b, a.y - b) 18 | proc `+`*(a: Vec2f, b: Vec2f): Vec2f = (a.x + b.x, a.y + b.y) 19 | proc `-`*(a: Vec2f, b: Vec2f): Vec2f = (a.x - b.x, a.y - b.y) 20 | proc `+`*(a: Vec2i, b: int32): Vec2i = (a.x + b, a.y + b) 21 | proc `-`*(a: Vec2i, b: int32): Vec2i = (a.x - b, a.y - b) 22 | proc `+`*(a: Vec2i, b: Vec2i): Vec2i = (a.x + b.x, a.y + b.y) 23 | proc `-`*(a: Vec2i, b: Vec2i): Vec2i = (a.x - b.y, a.y - b.y) 24 | 25 | proc `+`*(a: Vec3f, b: Vec3f): Vec3f = (a.x + b.x, a.y + b.y, a.z + b.z) 26 | proc `-`*(a: Vec3f, b: Vec3f): Vec3f = (a.x - b.x, a.y - b.y, a.z - b.z) 27 | proc `/`*(a: Vec3f, b: float32): Vec3f = (a.x / b, a.y / b, a.z / b) 28 | 29 | 30 | proc `+=`*(a: var Vec2f, b: Vec2f): void = 31 | a.x += b.x 32 | a.y += b.y 33 | 34 | proc `+=`*(a: var Vec3f, b: Vec3f): void = 35 | a.x += b.x 36 | a.y += b.y 37 | a.z += b.z 38 | 39 | proc dot*(a: Vec2f, b: Vec2f): auto = a.x * b.x + a.y * b.y 40 | proc len*(v: Vec2f): auto = sqrt(dot(v, v)) 41 | proc hat*(v: Vec2f): auto = v / v.len() 42 | 43 | proc lower*(vp: Viewport): Vec2f = (x: vp.left, y: vp.top) 44 | proc upper*(vp: Viewport): Vec2f = (x: vp.right, y: vp.bottom) 45 | proc center*(vp: Viewport): Vec2f = (vp.lower() + vp.upper()) / 2 46 | proc size*(vp: Viewport): Vec2f = vp.upper() - vp.lower() 47 | proc viewport*(lower, upper: Vec2f): Viewport = (lower.x, lower.y, upper.x, upper.y) 48 | 49 | proc `*`*(vp: Viewport, scale: float32): Viewport = 50 | (vp.left * scale, vp.top * scale, vp.right * scale, vp.bottom * scale) 51 | -------------------------------------------------------------------------------- /src/noise.nim: -------------------------------------------------------------------------------- 1 | import math 2 | import hashes 3 | import random 4 | import grid 5 | import vector 6 | 7 | type 8 | GradientNoiseTable = ref object 9 | seed: int 10 | size: int 11 | mask: int32 12 | gradients: seq[Vec2f] 13 | indices: seq[int32] 14 | 15 | proc fract(f: float32): float32 = f - floor(f) 16 | 17 | proc newGradientNoiseTable*(seed: int): GradientNoiseTable = 18 | result = new GradientNoiseTable 19 | result.seed = seed 20 | result.size = 256 21 | result.mask = int32(result.size - 1) 22 | result.gradients = newSeq[Vec2f](result.size) 23 | result.indices = newSeq[int32](result.size) 24 | for i in 0..result.mask: 25 | result.indices[i] = int32(i) 26 | result.gradients[i].x = cos(float32(i) * 2 * PI / float32(result.size)) 27 | result.gradients[i].y = sin(float32(i) * 2 * PI / float32(result.size)) 28 | var rnd = initRand(seed) 29 | rnd.shuffle(result.indices) 30 | 31 | proc getGradient(table: GradientNoiseTable, p: Vec2i): Vec2f = 32 | var h: Hash = 0 33 | h = h !& p.x 34 | h = h !& p.y 35 | h = (!$h) 36 | table.gradients[table.indices[h and table.mask]] 37 | 38 | proc computeNoiseValue*(table: GradientNoiseTable, x, y: float32): float32 = 39 | let 40 | i = (x: int32(floor(x)), y: int32(floor(y))) 41 | f = (fract(x), fract(y)) 42 | u = f*f*f*(f*(f*6.0f - 15.0f) + 10.0f) 43 | ga = table.getGradient(i + (0'i32,0'i32) ) 44 | gb = table.getGradient(i + (1'i32,0'i32) ) 45 | gc = table.getGradient(i + (0'i32,1'i32) ) 46 | gd = table.getGradient(i + (1'i32,1'i32) ) 47 | va = dot(ga, f - (0'f32,0'f32)) 48 | vb = dot(gb, f - (1'f32,0'f32)) 49 | vc = dot(gc, f - (0'f32,1'f32)) 50 | vd = dot(gd, f - (1'f32,1'f32)) 51 | va + u.x * (vb-va) + u.y * (vc-va) + u.x * u.y * (va-vb-vc+vd) 52 | 53 | # Creates a scalar field with C1 continuity whose values are roughly in [-0.8, +0.8] 54 | # The viewport that spans from [-1,-1] to [+1,+1] is a 2x2 grid of surflets. 55 | proc generateGradientNoise*(seed: int; width, height: int; viewport: Viewport): Grid = 56 | result = newGrid(width, height) 57 | let 58 | table = newGradientNoiseTable(seed) 59 | vpwidth = viewport.right - viewport.left 60 | vpheight = viewport.bottom - viewport.top 61 | dx = vpwidth / float32(width) 62 | sx = viewport.left + dx / 2 63 | dy = vpheight / float32(height) 64 | sy = viewport.top + dy / 2 65 | var i = 0 66 | for row in 0.. 2 | 3 | **Nile** generates height maps and images for imaginary islands. It includes: 4 | 5 | - Efficient high-quality resampling of floating-point images. (`filter.nim`) 6 | - Efficient computation of signed distance fields. (`distance.nim`) 7 | - Tweakable generation of gradient noise. (`noise.nim`) 8 | 9 | To try it out, do: 10 | 11 | `nim c --run tests/islands.nim` 12 | 13 | Alternatively, simply invoke `islands.nim` directly from your shell since it has a shebang. This 14 | will enable the release flag, creating a very fast native executable. 15 | 16 | 119 | -------------------------------------------------------------------------------- /src/image.nim: -------------------------------------------------------------------------------- 1 | import grid 2 | import strformat 3 | import vector 4 | 5 | type 6 | Image* = ref object 7 | width*, height*: int 8 | red*: Grid 9 | grn*: Grid 10 | blu*: Grid 11 | alp*: Grid 12 | ColorGradient* = ref object 13 | red*: array[256, float32] 14 | grn*: array[256, float32] 15 | blu*: array[256, float32] 16 | alp*: array[256, float32] 17 | 18 | proc addOverlay*(a, b: Image): Image = 19 | new(result) 20 | assert(a.width == b.width and a.height == b.height) 21 | let invalp = 1 - b.alp 22 | result.red = a.red * invalp 23 | result.grn = a.grn * invalp 24 | result.blu = a.blu * invalp 25 | result.alp = a.alp * invalp 26 | result.red += b.red 27 | result.grn += b.grn 28 | result.blu += b.blu 29 | result.alp += b.alp 30 | result.width = a.width 31 | result.height = a.height 32 | 33 | proc newImageFromLuminance*(grid: Grid): Image = 34 | new(result) 35 | result.red = newGrid(grid) 36 | result.grn = newGrid(grid) 37 | result.blu = newGrid(grid) 38 | result.alp = newGrid(grid.width, grid.height, 1.0f) 39 | result.width = grid.width 40 | result.height = grid.height 41 | 42 | # TODO: rename to newImageFromBGRA8 43 | proc newImageFromDataString*(data: string; width, height: int): Image = 44 | new(result) 45 | result.red = newGrid(width, height) 46 | result.grn = newGrid(width, height) 47 | result.blu = newGrid(width, height) 48 | result.alp = newGrid(width, height) 49 | result.width = width 50 | result.height = height 51 | var i = 0; var j = 0 52 | for row in 0.. 2 89 | assert colors[0] == 0 90 | assert colors[colors.len() - 2] == 255 91 | var i = 0 92 | while i < colors.len() - 2: 93 | let 94 | currval = colors[i] 95 | nextval = colors[i + 2] 96 | currrgb = colorToFloat3(colors[i + 1]) 97 | nextrgb = colorToFloat3(colors[i + 3]) 98 | assert(currval >= 0 and currval < 256) 99 | assert(nextval >= 0 and nextval < 256) 100 | assert(nextval >= currval) 101 | let 102 | ncols = nextval - currval 103 | del = (nextrgb - currrgb) / float(ncols) 104 | var col = currrgb 105 | for j in currval..nextval: 106 | result.red[j] = col.x 107 | result.grn[j] = col.y 108 | result.blu[j] = col.z 109 | col += del 110 | i += 2 111 | 112 | proc applyColorGradient*(image: Image, colors: ColorGradient): void = 113 | var j = 0 114 | for row in 0..= 1.0: return 0 11 | 2 * x * x * x - 3 * x * x + 1) 12 | 13 | let FilterTriangle* = Filter(radius: 1, function: proc (x: float32): float32 = 14 | if x >= 1.0: return 0 15 | 1.0 - x) 16 | 17 | let FilterGaussian* = Filter(radius: 2, function: proc (x: float32): float32 = 18 | const scale = 1.0f / sqrt(0.5f * math.PI) 19 | if x >= 2.0: return 0 20 | exp(-2 * x * x) * scale) 21 | 22 | # Computes an average value over a viewport in [0,+1] and accounts for pixel squares that are 23 | # only partially covered by the viewport. 24 | proc computeAverage*(g: Grid; left, top, right, bottom: float32): float32 = 25 | let 26 | x0 = left * float32(g.width) 27 | y0 = top * float32(g.height) 28 | x1 = right * float32(g.width) 29 | y1 = bottom * float32(g.height) 30 | inner_col0 = int(ceil(x0)).max(0) 31 | inner_row0 = int(ceil(y0)).max(0) 32 | inner_col1 = int(floor(x1)).min(g.width) - 1 33 | inner_row1 = int(floor(y1)).min(g.height) - 1 34 | outer_col0 = int(floor(x0)).max(0) 35 | outer_row0 = int(floor(y0)).max(0) 36 | outer_col1 = int(ceil(x1)).min(g.width) - 1 37 | outer_row1 = int(ceil(y1)).min(g.height) - 1 38 | var 39 | area = 0.0f 40 | total = 0.0f 41 | # First add up the pixel squares that lie completely inside the viewport. 42 | for col in inner_col0..inner_col1: 43 | for row in inner_row0..inner_row1: 44 | area += 1.0 45 | total += g.getPixel(col, row) 46 | # Determine the amount of fractional overhang on all 4 sides. 47 | let 48 | w = float32(inner_col0) - x0 49 | e = x1 - float32(outer_col1) 50 | n = float32(inner_row0) - y0 51 | s = y1 - float32(outer_row1) 52 | # Left column of pixels with fractional coverage. 53 | if w > 0: 54 | for row in inner_row0..inner_row1: 55 | area += w; total += w * g.getPixel(outer_col0, row) 56 | # Right column of pixels with fractional coverage. 57 | if e > 0: 58 | for row in inner_row0..inner_row1: 59 | area += e; total += e * g.getPixel(outer_col1, row) 60 | # Top row of pixels with fractional coverage. 61 | if n > 0: 62 | for col in inner_col0..inner_col1: 63 | area += n; total += n * g.getPixel(col, outer_row0) 64 | # Bottom row of pixels with fractional coverage. 65 | if s > 0: 66 | for col in inner_col0..inner_col1: 67 | area += s; total += s * g.getPixel(col, outer_row1) 68 | # Northwest corner. 69 | if w > 0 and n > 0: 70 | area += w * n; total += w * n * g.getPixel(outer_col0, outer_row0) 71 | # Southwest corner. 72 | if w > 0 and s > 0: 73 | area += w * s; total += w * s * g.getPixel(outer_col0, outer_row1) 74 | # Northeast corner. 75 | if e > 0 and n > 0: 76 | area += e * n; total += e * n * g.getPixel(outer_col1, outer_row0) 77 | # Southeast corner. 78 | if s > 0 and e > 0: 79 | area += s * e; total += s * e * g.getPixel(outer_col1, outer_row1) 80 | total / area 81 | 82 | # Resamples the given image using a technique sometimes called "pixel mixing". 83 | # This is the same as a classic box filter when magnifying or minifying by a multiple of 2. 84 | proc resizeBoxFilter*(g: Grid, width, height: int): Grid = 85 | result = newGrid(width, height) 86 | let dx = 1.0f / float32(width) 87 | let dy = 1.0f / float32(height) 88 | var x = 0.0f 89 | var y = 0.0f 90 | var i = 0 91 | for row in 0..= sourceLen: continue 148 | let 149 | sx = (0.5 + float32(si)) * sourceDelta 150 | t = filterDomain * abs(sx - x) 151 | weight = filter.function(t) 152 | if weight != 0: 153 | result.add (targetIndex, si, weight) 154 | weightSum += weight 155 | inc nsamples 156 | if weightSum > 0: 157 | while nsamples > 0: 158 | result[result.len() - nsamples].filterWeight /= weightSum 159 | dec nsamples 160 | x += targetDelta 161 | 162 | # Resizes an image with the given filter. 163 | proc resize*(source: Grid, width, height: int, filter: Filter): Grid = 164 | # First resize horizontally. 165 | let horizontal = newGrid(width, source.height) 166 | var ops = computeMaccOps(width, source.width, filter) 167 | for ty in 0.. 0.5: 209 | if nw: vp.right -= amt; vp.bottom -= amt 210 | if ne: vp.left += amt; vp.bottom -= amt 211 | if sw: vp.right -= amt; vp.top += amt 212 | if se: vp.left += amt; vp.top += amt 213 | renderPartial(tile, fmt"frame-{frame:03}.png", vp) 214 | inc frame 215 | return frame 216 | 217 | if isMainModule: 218 | var tile = generateRootTile(TILE_RESOLUTION, SEED) 219 | echo tile.index 220 | var frame = 0 221 | renderEntire(tile, fmt"frame-{frame:03}.png") 222 | inc frame 223 | for zoom in 1..NFRAMES: 224 | let childIndex = chooseChild(tile) 225 | echo childIndex 226 | frame = smoothZoom(tile, childIndex, frame) 227 | tile = generateChild(tile, childIndex) 228 | renderEntire(tile, fmt"frame-{frame:03}.png") 229 | inc frame 230 | 231 | discard execShellCmd "ffmpeg -i frame-%03d.png -c:v mpeg4 -vb 150M video.mp4" 232 | discard execShellCmd "rm frame*.png" 233 | -------------------------------------------------------------------------------- /src/grid.nim: -------------------------------------------------------------------------------- 1 | import math 2 | import sequtils 3 | import strutils 4 | import vector 5 | 6 | ## Simple two-dimensional image with floating-point luminance data. 7 | ## Values are stored in row-major (scanline) order, but coordinates and dimensions use 8 | ## X Y notation (i.e. columns ⨯ rows) with 0,0 being the top-left. 9 | type 10 | Grid* = ref object 11 | data*: seq[float32] 12 | width*, height*: int 13 | 14 | # Creates a grid full of zeros. 15 | proc newGrid*(width, height: int): Grid = 16 | new(result) 17 | result.data = newSeq[float32](width * height) 18 | result.width = width 19 | result.height = height 20 | 21 | # Creates a grid filled with the given value. 22 | proc newGrid*(width, height: int, value: float32): Grid = 23 | new(result) 24 | result.data = repeat(value, width * height) 25 | result.width = width 26 | result.height = height 27 | 28 | # Clone a grid. 29 | proc newGrid*(grid: Grid): Grid = 30 | new(result) 31 | result.data = grid.data 32 | result.width = grid.width 33 | result.height = grid.height 34 | 35 | # Consumes a multiline string composed of 0's and 1's. 36 | proc newGrid*(pattern: string): Grid = 37 | new(result) 38 | for iter in tokenize(pattern): 39 | if not iter.isSep: 40 | inc result.height 41 | let ncols = iter.token.len() 42 | if result.width == 0: 43 | result.width = ncols 44 | elif result.width != ncols: 45 | doAssert false 46 | result.data = newSeq[float32](result.width * result.height) 47 | var n = 0 48 | for iter in tokenize(pattern): 49 | if not iter.isSep: 50 | for c in iter.token: 51 | result.data[n] = float32(ord(c) - ord('0')) 52 | inc n 53 | 54 | # Returns a new grid with the results of op applied to every value. 55 | proc map*(g: Grid, op: proc (x: float32): float32): Grid = 56 | new(result) 57 | result.data = map(g.data, op) 58 | result.width = g.width 59 | result.height = g.height 60 | 61 | # Convenience template around the map proc to reduce typing. 62 | template mapIt*(g: Grid, op: untyped): Grid = 63 | new(result) 64 | result.data = newSeq[float32](g.data.len) 65 | result.width = g.width 66 | result.height = g.height 67 | var i = 0 68 | let t = g.data 69 | for it {.inject.} in t: 70 | result.data[i] = op 71 | inc i 72 | result 73 | 74 | # Applies op to every item in g modifying it directly. 75 | proc apply*(g:Grid, op: proc (x: var float32): void): void = g.data.apply(op) 76 | 77 | # Addition. 78 | proc `+`*(k: float32, g: Grid): Grid = g.map(proc(f: float32): float32 = k + f) 79 | proc `+`*(g: Grid, k: float32): Grid = k + g 80 | proc `+=`*(g: Grid, k: float32): void = g.apply(proc(f: var float32): void = f += k) 81 | proc `+=`*(a: Grid, b: Grid): void = 82 | assert(a.data.len() == b.data.len()) 83 | for i in 0..= 0 and x < g.width and y >= 0 and y < g.height) 145 | g.data[y * g.width + x] = k 146 | 147 | # Adds the given value into the texel 148 | proc addPixel*(g: Grid, x, y: int, k: float32 = 1): void = 149 | assert(x >= 0 and x < g.width and y >= 0 and y < g.height) 150 | g.data[y * g.width + x] += k 151 | 152 | # Gets the pixel value at the given column and row. 153 | proc getPixel*(g: Grid, x, y: int): float32 = 154 | assert(x >= 0 and x < g.width and y >= 0 and y < g.height) 155 | g.data[y * g.width + x] 156 | 157 | # Takes floating point coordinates in [0,+1] and returns the nearest pixel value. 158 | proc sampleNearest*(g: Grid, x, y: float32): float32 = 159 | let col = max(0, min(int(float32(g.width) * x), g.width - 1)) 160 | let row = max(0, min(int(float32(g.height) * y), g.height - 1)) 161 | g.data[row * g.width + col] 162 | 163 | # Copies a region of pixels from the source grid. 164 | proc blitFrom*(dst: Grid, src: Grid; dstx, dsty: int; left, top, right, bottom: int): void = 165 | var dstrow = dsty; 166 | for srcrow in top..