Skip to content

Commit

Permalink
refactor: Move PRNG outside framebox and improve usage
Browse files Browse the repository at this point in the history
  • Loading branch information
Omikhleia authored and Didier Willis committed Jan 13, 2024
1 parent 5903e7b commit 8b812f2
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 39 deletions.
9 changes: 8 additions & 1 deletion packages/framebox/graphics/renderer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
--

local RoughGenerator = require("rough-lua.rough.generator").RoughGenerator
local PRNG = require("prng-prigarin")

-- HELPERS

Expand Down Expand Up @@ -305,9 +306,15 @@ function DefaultPainter.draw (_, drawable, clippable)
end

local RoughPainter = pl.class()
local prng = PRNG()

function RoughPainter:_init (options)
self.gen = RoughGenerator(options)
local o = options or {}
if not o.randomizer then
o.randomizer = prng -- use common 'static' PRNG instance
-- so that all sketchy drawings look random but reproducible
end
self.gen = RoughGenerator(o)
end

function RoughPainter:line (x1, y1, x2, y2, options)
Expand Down
2 changes: 1 addition & 1 deletion packages/framebox/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ Sketching options are
\autodoc:parameter{roughness} (numerical value indicating how rough the drawing is; 0 would be a perfect rectangle, the default value is 1 and there is no upper limit to this value but a value over 10 is mostly useless),
\autodoc:parameter{bowing} (numerical value indicating how curvy the lines are when
drawing a sketch; a value of 0 will cause straight lines and the default value is 1),
\autodoc:parameter{preserve} (defaults to false; when set to true, the locations of the end points are not randomized),
\autodoc:parameter{preserve} (defaults to false; when set to true, the \roughbox[bordercolor=#22427c, preserve=true]{locations} of the end points are not randomized),
\autodoc:parameter{singlestroke} (defaults to false; if set to true, a single stroke is applied
to sketch the shape instead of multiple strokes).
For instance, here is a single-stroked \roughbox[bordercolor=#59b24c, singlestroke=true]{rough box,}
Expand Down
153 changes: 153 additions & 0 deletions packages/framebox/points-on-curve/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
--
-- License: MIT
-- Copyright (c) 2023, Didier Willis
--
-- This is a straightforward port of the bezier-points JavaScript library.
-- (https://github.com/pshihn/bezier-points)
-- License: MIT
-- Copyright (c) 2020 Preet Shihn
--

-- Distance between 2 points squared
local function distanceSq (p1, p2)
return (p1[1] - p2[1]) ^ 2 + (p1[2] - p2[2]) ^ 2
end

-- Distance between 2 points
local function distance (p1, p2)
return math.sqrt(distanceSq(p1, p2))
end

-- Distance squared from a point p to the line segment vw
local function distanceToSegmentSq (p, v, w)
local l2 = distanceSq(v, w)
if l2 == 0 then
return distanceSq(p, v)
end
local t = ((p[1] - v[1]) * (w[1] - v[1]) + (p[2] - v[2]) * (w[2] - v[2])) / l2
t = math.max(0, math.min(1, t))
return distanceSq(p, lerp(v, w, t))
end

local function lerp (a, b, t)
return {a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t}
end

-- Adapted from https://seant23.wordpress.com/2010/11/12/offset-bezier-curves/
local function flatness (points, offset)
local p1 = points[offset + 1]
local p2 = points[offset + 2]
local p3 = points[offset + 3]
local p4 = points[offset + 4]

local ux = 3 * p2[1] - 2 * p1[1] - p4[1]
ux = ux * ux
local uy = 3 * p2[2] - 2 * p1[2] - p4[2]
uy = uy * uy
local vx = 3 * p3[1] - 2 * p4[1] - p1[1]
vx = vx * vx
local vy = 3 * p3[2] - 2 * p4[2] - p1[2]
vy = vy * vy

if ux < vx then
ux = vx
end
if uy < vy then
uy = vy
end
return ux + uy
end

local function getPointsOnBezierCurveWithSplitting (points, offset, tolerance, newPoints)
local outPoints = newPoints or {}
if flatness(points, offset) < tolerance then
local p0 = points[offset + 1]
if #outPoints > 0 then
local d = distance(outPoints[#outPoints], p0)
if d > 1 then
table.insert(outPoints, p0)
end
else
table.insert(outPoints, p0)
end
table.insert(outPoints, points[offset + 4])
else
-- subdivide
local t = .5
local p1 = points[offset + 1]
local p2 = points[offset + 2]
local p3 = points[offset + 3]
local p4 = points[offset + 4]

local q1 = lerp(p1, p2, t)
local q2 = lerp(p2, p3, t)
local q3 = lerp(p3, p4, t)

local r1 = lerp(q1, q2, t)
local r2 = lerp(q2, q3, t)

local red = lerp(r1, r2, t)

getPointsOnBezierCurveWithSplitting({p1, q1, r1, red}, 0, tolerance, outPoints)
getPointsOnBezierCurveWithSplitting({red, r2, q3, p4}, 0, tolerance, outPoints)
end
return outPoints
end

local function simplify (points, distance)
return simplifyPoints(points, 1, #points, distance)
end

-- Ramer–Douglas–Peucker algorithm
-- https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
local function simplifyPoints (points, start, finish, distance)
local outPoints = {}
local s = points[start]
local e = points[finish]
local maxDistSq = 0
local maxNdx = 1
for i = start + 1, finish - 1 do
local distSq = distanceToSegmentSq(points[i], s, e)
if distSq > maxDistSq then
maxDistSq = distSq
maxNdx = i
end
end
if math.sqrt(maxDistSq) > distance then
local t1 = simplifyPoints(points, start, maxNdx + 1, distance)
local t2 = simplifyPoints(points, maxNdx, finish, distance)
for _, v in ipairs(t1) do
table.insert(outPoints, v)
end
for _, v in ipairs(t2) do
table.insert(outPoints, v)
end
else
if #outPoints == 0 then
table.insert(outPoints, s)
end
table.insert(outPoints, e)
end
return outPoints
end

local function pointsOnBezierCurves (points, tolerance, distance)
local newPoints = {}
local numSegments = (#points - 1) / 3
for i = 0, numSegments - 1 do
local offset = i * 3
getPointsOnBezierCurveWithSplitting(points, offset, tolerance, newPoints)
end
if distance and distance > 0 then
return simplifyPoints(newPoints, 1, #newPoints, distance)
end
return newPoints
end

-- Exports

return {
simplify = simplify,
simplifyPoints = simplifyPoints,
pointsOnBezierCurves = pointsOnBezierCurves,
}
26 changes: 14 additions & 12 deletions packages/framebox/graphics/prng.lua → prng-prigarin/init.lua
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
--
-- Pseudo-Random Number Generator (PRNG)
-- License: MIT
-- 2022, 2023 Didier Willis
--
-- Why would a text processing software such as SILE need a PRNG,
-- where one would expect the reproduceability of the output?
--
-- Well, there are algorithms were a bit of randomness is expected
-- e.g. the rough "hand-drawn-like" drawing style, where one would
-- expect all rough graphics to look different.
-- But using math.random() there would yield always different results...
-- There are algorithms were a bit of randomness is expected, but
-- where one would expect a reproducible output.
-- Using math.random() there would yield always different results...
-- and using math.randomseed() is also problematic: it's global and could be
-- affected elsewhere, etc.
-- So one may need instead a "fake" PRNG, that spits out a seemingly uniform
-- distribution of "random" numbers.

-- (didier.willis@gmail.com) The algorithm below was just found on the
-- Internet, where it was stated to be common in Monte Carlo randomizations.
--
-- The algorithm below was just found on the Internet, where it was stated to
-- be "common in Monte Carlo randomizations."
--
-- I am not so lazy not to check, and traced it back to Sergei M. Prigarin,
-- _Spectral Models of Random Fields in Monte Carlo Methods_, 2001.
Expand All @@ -25,8 +22,8 @@
-- This derivation, if I read correctly, has a 2^40 module and 5^17 mutiplier
-- (cycle length 2^38).
-- For information; the seeds are (X1, X2), here set to (0, 1). The algorithm
-- could be seeded with other values. It's not clear to me which variant was
-- used (I didn't check the whole book...), but it seems the constraints are
-- can be seeded with other values.
-- I didn't check the whole book...), but it seems the constraints are
-- 0 < X1, X2 <= 2^20 and X2 being odd.

local A1, A2 = 727595, 798405 -- 5^17=D20*A1+A2
Expand All @@ -35,6 +32,11 @@ local D20, D40 = 1048576, 1099511627776 -- 2^20, 2^40
local PRNG = pl.class({
X1 = 0,
X2 = 1,
_init = function (self, seed)
if seed then -- Just seeding X1
self.X1 = math.abs(seed) % D20
end
end,
random = function (self)
local U = self.X2 * A2
local V = (self.X1 * A2 + self.X2 * A1) % D20
Expand Down
8 changes: 5 additions & 3 deletions rough-lua/rough/fillers/scan-line-hachure.lua
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ local hachureLines = require("rough-lua.rough.fillers.hachure-fill").hachureLine
local jsshims = require("rough-lua.rough.jsshims")
local math_round = jsshims.math_round

local PRNG = require("packages.framebox.graphics.prng")
local prng = PRNG()
local PRNG = require("prng-prigarin")

local function polygonHachureLines (polygonList, o)
local angle = o.hachureAngle + 90
Expand All @@ -24,7 +23,10 @@ local function polygonHachureLines (polygonList, o)
gap = math_round(math.max(gap, 0.1))
local skipOffset = 1
if o.roughness >= 1 then
if prng:random() > 0.7 then
-- PORTING NOTE: Slightly different approach to randomization.
-- We never rely on math.random() but always use our PRNG.
local rand = o.randomizer and o.randomizer:random() or PRNG(o.seed or 0):random()
if rand > 0.7 then
skipOffset = gap
end
end
Expand Down
18 changes: 6 additions & 12 deletions rough-lua/rough/generator.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,12 @@ local line, rectangle,
renderer.patternFillArc, renderer.patternFillPolygons, renderer.solidFillPolygon
-- PORTING NOTE:
-- I ported the module but haven't tested it for now
-- local curveToBezier = require("packages.framebox.points-on-curve.curve-to-bezier").curveToBezier
-- local pointsOnPath = require("packages.framebox.points-on-curve").pointsOnPath
-- local pointsOnBezierCurves = require("packages.framebox.points-on-curve").pointsOnBezierCurves
local pointsOnPath = function (_, _, _)
error("Not implemented")
end
local curveToBezier = function (_)
error("Not implemented")
end
local pointsOnBezierCurves = function (_, _, _)
error("Not implemented")
end
-- local curveToBezier = require("rough-lua.points-on-curve.curve-to-bezier").curveToBezier
-- local pointsOnPath = require("rough-lua.points-on-curve").pointsOnPath
-- local pointsOnBezierCurves = require("rough-lua.points-on-curve").pointsOnBezierCurves
local pointsOnPath = function () error("Not implemented") end
local curveToBezier = function () error("Not implemented") end
local pointsOnBezierCurves = function () error("Not implemented") end


local RoughGenerator = pl.class({
Expand Down
28 changes: 18 additions & 10 deletions rough-lua/rough/renderer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,36 @@

local jsshims = require("rough-lua.rough.jsshims")
local array_concat = jsshims.array_concat

local PRNG = require("packages.framebox.graphics.prng")
local prng = PRNG()
local PRNG = require("prng-prigarin")

-- PORTING NOTE:
-- I ported path-data-parser but haven't tested it for now
-- local pathDataParser = require("packages.framebox.path-data-parser")
-- local pathDataParser = require("rough-lua.path-data-parser")
-- local parsePath, normalize, absolutize = pathDataParser.parsePath, pathDataParser.normalize, pathDataParser.absolutize
local normalize = function () error("Not yet implemented") end
local absolutize = function () error("Not yet implemented") end
local parsePath = function () error("Not yet implemented") end

local getFiller = require("rough-lua.rough.fillers.filler").getFiller

local function cloneOptionsAlterSeed (o)
-- PORTING NOTE:
-- Option to alter seed no implemented.
return o
local function cloneOptionsAlterSeed (ops)
local result = pl.tablex.copy(ops)
result.randomizer = nil
if ops.seed then
result.seed = ops.seed + 1
end
return result
end

local function random (ops)
if not ops.randomizer then
ops.randomizer = PRNG(ops.seed or 0)
end
return ops.randomizer:random()
end

local function _offset (min, max, ops, roughnessGain)
return ops.roughness * (roughnessGain or 1) * ((prng:random() * (max - min)) + min)
return ops.roughness * (roughnessGain or 1) * ((random(ops) * (max - min)) + min)
end

local function _offsetOpt (x, ops, roughnessGain)
Expand All @@ -55,7 +63,7 @@ local function _line (x1, y1, x2, y2, o, move, overlay)
offset = length / 10
end
local halfOffset = offset / 2
local divergePoint = 0.2 + prng:random() * 0.2
local divergePoint = 0.2 + random(o) * 0.2
local midDispX = o.bowing * o.maxRandomnessOffset * (y2 - y1) / 200
local midDispY = o.bowing * o.maxRandomnessOffset * (x1 - x2) / 200
midDispX = _offsetOpt(midDispX, o, roughnessGain)
Expand Down

0 comments on commit 8b812f2

Please sign in to comment.