diff --git a/packages/framebox/graphics/renderer.lua b/packages/framebox/graphics/renderer.lua index 0731d7b..2f14186 100644 --- a/packages/framebox/graphics/renderer.lua +++ b/packages/framebox/graphics/renderer.lua @@ -12,6 +12,7 @@ -- local RoughGenerator = require("rough-lua.rough.generator").RoughGenerator +local PRNG = require("prng-prigarin") -- HELPERS @@ -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) diff --git a/packages/framebox/init.lua b/packages/framebox/init.lua index 5896bf6..aab9936 100644 --- a/packages/framebox/init.lua +++ b/packages/framebox/init.lua @@ -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,} diff --git a/packages/framebox/points-on-curve/init.lua b/packages/framebox/points-on-curve/init.lua new file mode 100644 index 0000000..91af7bb --- /dev/null +++ b/packages/framebox/points-on-curve/init.lua @@ -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, +} diff --git a/packages/framebox/graphics/prng.lua b/prng-prigarin/init.lua similarity index 64% rename from packages/framebox/graphics/prng.lua rename to prng-prigarin/init.lua index 932da60..0d51a90 100644 --- a/packages/framebox/graphics/prng.lua +++ b/prng-prigarin/init.lua @@ -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. @@ -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 @@ -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 diff --git a/rough-lua/rough/fillers/scan-line-hachure.lua b/rough-lua/rough/fillers/scan-line-hachure.lua index 62462c6..5b607a0 100644 --- a/rough-lua/rough/fillers/scan-line-hachure.lua +++ b/rough-lua/rough/fillers/scan-line-hachure.lua @@ -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 @@ -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 diff --git a/rough-lua/rough/generator.lua b/rough-lua/rough/generator.lua index 5dcb2d1..d442a2f 100644 --- a/rough-lua/rough/generator.lua +++ b/rough-lua/rough/generator.lua @@ -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({ diff --git a/rough-lua/rough/renderer.lua b/rough-lua/rough/renderer.lua index 4bd63f5..176c0ab 100644 --- a/rough-lua/rough/renderer.lua +++ b/rough-lua/rough/renderer.lua @@ -10,13 +10,11 @@ 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 @@ -24,14 +22,24 @@ 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) @@ -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)