summaryrefslogtreecommitdiff
path: root/tex/context/base/mkxl/mlib-ran.lmt
blob: cb8645e8d373b7b0d24018ac44fba6e2ddb22ee4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
if not modules then modules = { } end modules ['mlib-ran'] = {
    version   = 1.001,
    comment   = "companion to mlib-ctx.mkiv",
    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
    copyright = "PRAGMA ADE / ConTeXt Development Team",
    license   = "see context related readme files",
}

local next = next
local ceil, floor, random, sqrt, cos, sin, pi, max, min = math.ceil, math.floor, math.random, math.sqrt, math.cos, math.sin, math.pi, math.min, math.max
local remove = table.remove

-- Below is a bit of rainy saturday afternoon hobyism, while listening to Judith
-- Owens redisCOVERed (came there via Leland Sklar who I have on a few live blurays;
-- and who is also on YT). (Also nice: https://www.youtube.com/watch?v=GXqasIRaxlA)

-- When Aditya pointed me to an article on mazes I ended up at poison distributions
-- which to me looks nicer than what I normally do, fill a grid and then randomize
-- the resulting positions. With some hooks this can be used for interesting patterns
-- too. A few links:
--
-- https://bost.ocks.org/mike/algorithms/#maze-generation
-- https://extremelearning.com.au/
-- https://www.jasondavies.com/maps/random-points/
-- http://devmag.org.za/2009/05/03/poisson-disk-sampling

-- The next function is quite close to what us discribed in the poisson-disk-sampling
-- link mentioned before. One can either use a one dimensional grid array or a two
-- dimensional one. The example code uses some classes dealing with points. In the
-- process I added some more control.

-- we could do without the samplepoints list

local function poisson(width, height, mindist, newpointscount, initialx, initialy)
    local starttime       = os.clock()
    local cellsize        = mindist / sqrt(2)
    local nofwidth        = ceil(width // cellsize)
    local nofheight       = ceil(height // cellsize)
    local grid            = lua.newtable(nofwidth,0) -- table.setmetatableindex("table")
    local firstx          = initialx or random() * width
    local firsty          = initialy or random() * height
    local firstpoint      = { firstx, firsty, 1 }
 -- local samplepoints    = { firstpoint }
    local processlist     = { firstpoint }
    local nofprocesslist  = 1
    local nofsamplepoints = 1
    local twopi           = 2 * pi

    for i=1,nofwidth do
        local g = lua.newindex(nofheight,false)
        grid[i] = g
    end

    local x = floor(firstx // cellsize) + 1 -- lua indices
    local y = floor(firsty // cellsize) + 1 -- lua indices

    x = max(1, min(x, width  - 1))
    y = max(1, min(y, height - 1))

    grid[x][y] = firstpoint

    -- The website shows graphic for this 5*5 grid snippet, if we use a one dimentional
    -- array then we could have one loop; a first version used a metatable trick so we
    -- had grid[i+gx][j+gy] but we no we also return the grid, so ... we now just check.

    -- There is no need for the samplepoints list as we can get that from the grid but
    -- instead we can store the index with the grid.

    while nofprocesslist > 0 do
        local point = remove(processlist,random(1,nofprocesslist))
        nofprocesslist = nofprocesslist - 1
        for i=1,newpointscount do -- we start at 1
            local radius = mindist * (random() + 1)
            local angle  = twopi * random()
            local nx     = point[1] + radius * cos(angle)
            local ny     = point[2] + radius * sin(angle)
            if nx > 1 and ny > 1 and nx <= width and ny <= height then -- lua indices
                local gx = floor(nx // cellsize)
                local gy = floor(ny // cellsize)
                -- the 5x5 cells around the point
                for i=-2,2 do
                    for j=-2,2 do
                        local cell = grid[i + gx]
                        if cell then
                            cell = cell[j + gy]
                            if cell and sqrt((cell[1] - nx)^2 + (cell[2] - ny)^2) < mindist then
                                goto next
                            end
                        end
                    end
                end
             -- local newpoint  = { nx, ny }
                nofprocesslist  = nofprocesslist + 1
                nofsamplepoints = nofsamplepoints + 1
                local newpoint  = { nx, ny, nofsamplepoints }
                processlist [nofprocesslist]  = newpoint
             -- samplepoints[nofsamplepoints] = newpoint
                grid[gx][gy] = newpoint
            end
            ::next::
        end
    end

    return {
        count  = nofsamplepoints,
     -- points = samplepoints,
        grid   = grid,
        time   = os.clock() - starttime,
    }
end

-- For now:

local randomizers     = utilities.randomizers or { }
utilities.randomizers = randomizers
randomizers.poisson   = poisson

-- The MetaFun interface:

local formatters = string.formatters
local concat = table.concat

local f_macro = formatters["%s(%N,%N);"]

local f_macros = {
    [2] = formatters["%s(%N,%N);"],
    [3] = formatters["%s(%N,%N,%i);"],
    [4] = formatters["%s(%N,%N,%i,%i);"],
}

function grid_to_mp(t,f,n)
    local grid   = t.grid
    local count  = t.count
    local result = { }
    local r      = 0
    local macro  = f or "draw"
    local runner = f_macros[n or 2] or f_macros[2]
    for i=1,#grid do
        local g = grid[i]
        if g then
            for j=1,#g do
                local v = g[j]
                if v then
                    r = r + 1
                    result[r] = runner(macro,v[1],v[2],v[3],count)
                end
            end
        end
    end
    return concat(result, " ")
end

local getparameter = metapost.getparameter

local function lmt_poisson()
    local initialx = getparameter { "initialx" }
    local initialy = getparameter { "initialy" }
    local width    = getparameter { "width" }
    local height   = getparameter { "height" }
    local distance = getparameter { "distance" }
    local count    = getparameter { "count" }

    local result = poisson (
        width, height, distance, count,
        initialx > 0 and initialx or false,
        initialy > 0 and initialy or false
    )

    if result then
        logs.report("poisson","w=%N, h=%N, d=%N, c=%N, n=%i, runtime %.3f",
            width, height, distance, count, result.count, result.time
        )
    end

    return result
end

function mp.lmt_poisson_generate()
    local result = lmt_poisson()
    if result then
        return grid_to_mp (
            result,
            getparameter { "macro" },
            getparameter { "arguments" }
        )
    end
end

-- -- some playing around showed no benefit
--
-- function points_to_mp(t,f)
--     local points = t.points
--     local count  = t.count
--     local result = { }
--     local r      = 0
--     local macro  = f or "draw"
--     local runner = f_macros[n or 2] or f_macros[2]
--     for i=1,count do
--         local v = points[i]
--         r = r + 1
--         result[r] = runner(macro,v[1],v[2],v[3],count)
--     end
--     return concat(result, " ")
-- end
--
-- local result  = false
-- local i, j, n = 0, 0, 0
--
-- function mp.lmt_poison_start()
--     result = lmt_poisson()
-- end
--
-- function mp.lmt_poisson_stop()
--     result = false
-- end
--
-- function mp.lmt_poisson_count()
--     return result and result.count or 0
-- end
--
-- function mp.lmt_poisson_get(i)
--     if result then
--         return mp.pair(result.points[i])
--     end
-- end
--
-- function mp.lmt_poisson_generate()
--     mp.lmt_poisson_start()
--     if result then
--         return grid_to_mp (
--             result,
--             getparameter { "macro" },
--             getparameter { "arguments" }
--         )
--     end
--     mp.lmt_poisson_stop()
-- end