From a5ace20fe0a714801d6bf09d64e5c268c3bb4143 Mon Sep 17 00:00:00 2001 From: Omikhleia Date: Sat, 16 Dec 2023 13:56:11 +0100 Subject: [PATCH 1/4] feat: Port more roughjs features to Lua for extra framebox options Refactoring the rough-lua port and adding lots of missing bits. --- examples/framebox.sil | 16 +- examples/tables.sil | 2 +- packages/framebox/graphics/renderer.lua | 49 +- packages/framebox/graphics/rough.lua | 454 +------------- packages/framebox/init.lua | 39 +- .../rough-lua/fillers/dashed-filler.lua | 53 ++ .../framebox/rough-lua/fillers/dot-filler.lua | 58 ++ .../framebox/rough-lua/fillers/filler.lua | 42 ++ .../rough-lua/fillers/hachure-fill.lua | 187 ++++++ .../rough-lua/fillers/hachure-filler.lua | 35 ++ .../rough-lua/fillers/hatch-filler.lua | 25 + .../rough-lua/fillers/scan-line-hachure.lua | 36 ++ .../rough-lua/fillers/zigzag-filler.lua | 47 ++ .../rough-lua/fillers/zigzag-line-filler.lua | 61 ++ packages/framebox/rough-lua/generator.lua | 325 ++++++++++ packages/framebox/rough-lua/geometry.lua | 18 + packages/framebox/rough-lua/jsshims.lua | 44 ++ packages/framebox/rough-lua/renderer.lua | 572 ++++++++++++++++++ 18 files changed, 1601 insertions(+), 462 deletions(-) create mode 100644 packages/framebox/rough-lua/fillers/dashed-filler.lua create mode 100644 packages/framebox/rough-lua/fillers/dot-filler.lua create mode 100644 packages/framebox/rough-lua/fillers/filler.lua create mode 100644 packages/framebox/rough-lua/fillers/hachure-fill.lua create mode 100644 packages/framebox/rough-lua/fillers/hachure-filler.lua create mode 100644 packages/framebox/rough-lua/fillers/hatch-filler.lua create mode 100644 packages/framebox/rough-lua/fillers/scan-line-hachure.lua create mode 100644 packages/framebox/rough-lua/fillers/zigzag-filler.lua create mode 100644 packages/framebox/rough-lua/fillers/zigzag-line-filler.lua create mode 100644 packages/framebox/rough-lua/generator.lua create mode 100644 packages/framebox/rough-lua/geometry.lua create mode 100644 packages/framebox/rough-lua/jsshims.lua create mode 100644 packages/framebox/rough-lua/renderer.lua diff --git a/examples/framebox.sil b/examples/framebox.sil index d2616ce..855fa00 100644 --- a/examples/framebox.sil +++ b/examples/framebox.sil @@ -75,4 +75,18 @@ sherlock.holmes@sile.org\par \end{roughbox} \end{center} -\end{document} \ No newline at end of file +\bigskip + +\begin{center} +\begin[padding=1em, bordercolor=#ff7f50, fillcolor=#ecebbd, fillstyle=cross-hatch, enlarge=true]{roughbox} +\begin[valign=middle,width=80%fw,minimize=true]{parbox} +\begin{center} +\font[weight=700]{Sherlock Holmes}\par +221b Baker Street — London NW1 6XE\par +sherlock.holmes@sile.org\par +\end{center} +\end{parbox} +\end{roughbox} +\end{center} + +\end{document} diff --git a/examples/tables.sil b/examples/tables.sil index 94f0b4d..81910e3 100644 --- a/examples/tables.sil +++ b/examples/tables.sil @@ -156,4 +156,4 @@ \font[features=+smcp]{Table 3.} \end{center} -\end{document} \ No newline at end of file +\end{document} diff --git a/packages/framebox/graphics/renderer.lua b/packages/framebox/graphics/renderer.lua index 12e1319..b0b7fe4 100644 --- a/packages/framebox/graphics/renderer.lua +++ b/packages/framebox/graphics/renderer.lua @@ -11,7 +11,7 @@ -- one. -- -local RoughGenerator = require("packages.framebox.graphics.rough").RoughGenerator +local RoughGenerator = require("packages.framebox.rough-lua.generator").RoughGenerator -- HELPERS @@ -247,6 +247,18 @@ function DefaultPainter.curlyBrace (_, x1, y1 , x2 , y2, width, thickness, curvy } end +function DefaultPainter.ellipse () + SU.error("Ellipse not implemented in DefaultPainter") +end + +function DefaultPainter.circle () + SU.error("Circle not implemented in DefaultPainter") +end + +function DefaultPainter.arc () + SU.error("Arc not implemented in DefaultPainter") +end + function DefaultPainter.draw (_, drawable, clippable) local o = drawable.options local path @@ -306,6 +318,18 @@ function RoughPainter:rectangle (x, y , w , h, options) return self.gen:rectangle(x, y , w , h, options) end +function RoughPainter:ellipse (x, y , w , h, options) + return self.gen:ellipse(x, y , w , h, options) +end + +function RoughPainter:circle (x, y , diameter, options) + return self.gen:circle(x, y , diameter, options) +end + +function RoughPainter:arc (x, y , w , h, start, stop, closed, options) + return self.gen:arc(x, y , w , h, start, stop, closed, options) +end + function RoughPainter.rectangleShadow () SU.error("Rectangle shadow not implemented in RoughPainter") end @@ -337,7 +361,13 @@ function RoughPainter:draw (drawable) "S" }, " ") elseif drawing.type == "fillPath" then - SU.error("Path filling not yet implemented.") + print("fillPath") + path = table.concat({ + self:opsToPath(drawing, precision), + makeColorHelper(o.fill, false), + _r(o.strokeWidth), "w", + "f" + }, " ") elseif drawing.type == "fillSketch" then path = table.concat({ self:opsToPath(drawing, precision), @@ -386,6 +416,21 @@ function PathRenderer:rectangle (x, y , w , h, options) return self.adapter:draw(drawable) end +function PathRenderer:ellipse (x, y , w , h, options) + local drawable = self.adapter:ellipse(x, y, w, h, options) + return self.adapter:draw(drawable) +end + +function PathRenderer:circle (x, y , w , h, options) + local drawable = self.adapter:circle(x, y, w, h, options) + return self.adapter:draw(drawable) +end + +function PathRenderer:arc (x, y , w , h, start, stop, closed, options) + local drawable = self.adapter:arc(x, y, w, h, start, stop, closed, options) + return self.adapter:draw(drawable) +end + function PathRenderer:rectangleShadow (x, y , w , h, s, options) local drawable = self.adapter:rectangle(x + s, y + s, w, h, options) local cliparea = self.adapter:rectangleClip(x, y, w, h, s) diff --git a/packages/framebox/graphics/rough.lua b/packages/framebox/graphics/rough.lua index 18e5ae6..7fd7908 100644 --- a/packages/framebox/graphics/rough.lua +++ b/packages/framebox/graphics/rough.lua @@ -1,455 +1,7 @@ --- --- Core logic for rough hand-drawn-like sketchs --- Implemented: lines and rectangles --- License: MIT --- +local RoughGenerator = require("packages.framebox.rough-lua.generator").RoughGenerator -local PRNG = require("packages.framebox.graphics.prng") -local prng = PRNG() - --- This is a partial straightforward port of the rough.js JavaScript library. --- (https://github.com/rough-stuff/rough) - --- From renderer.ts (private helpers) - -local function _offset(min, max, ops, roughnessGain) - return ops.roughness * (roughnessGain or 1) * ((prng:random() * (max - min)) + min) -end - -local function _offsetOpt(x, ops, roughnessGain) - return _offset(-x, x, ops, roughnessGain or 1) -end - -local function _line(x1, y1, x2, y2, o, move, overlay) -- returns an array of operations - local lengthSq = (x1 - x2)^2 + (y1 - y2)^2 - local length = math.sqrt(lengthSq) - local roughnessGain - if length < 200 then - roughnessGain = 1 - elseif length > 500 then - roughnessGain = 0.4 - else - roughnessGain = (-0.0016668) * length + 1.233334 - end - - local offset = o.maxRandomnessOffset or 0 - if (offset * offset * 100) > lengthSq then - offset = length / 10 - end - local halfOffset = offset / 2 - local divergePoint = 0.2 + prng:random() * 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) - midDispY = _offsetOpt(midDispY, o, roughnessGain) - local ops = {} - local randomHalf = function() return _offsetOpt(halfOffset, o, roughnessGain) end - local randomFull = function() return _offsetOpt(offset, o, roughnessGain) end - local preserveVertices = o.preserveVertices - if move then - if overlay then - local t = { - op = 'move', - data = { - x1 + (preserveVertices and 0 or randomHalf()), - y1 + (preserveVertices and 0 or randomHalf()), - } - } - ops[#ops+1] = t - else - local t = { - op = 'move', - data = { - x1 + (preserveVertices and 0 or _offsetOpt(offset, o, roughnessGain)), - y1 + (preserveVertices and 0 or _offsetOpt(offset, o, roughnessGain)), - }, - } - ops[#ops+1] = t - end - end - if overlay then - local t = { - op = 'bcurveTo', - data = { - midDispX + x1 + (x2 - x1) * divergePoint + randomHalf(), - midDispY + y1 + (y2 - y1) * divergePoint + randomHalf(), - midDispX + x1 + 2 * (x2 - x1) * divergePoint + randomHalf(), - midDispY + y1 + 2 * (y2 - y1) * divergePoint + randomHalf(), - x2 + (preserveVertices and 0 or randomHalf()), - y2 + (preserveVertices and 0 or randomHalf()), - } - } - ops[#ops+1] = t - else - local t = { - op = 'bcurveTo', - data = { - midDispX + x1 + (x2 - x1) * divergePoint + randomFull(), - midDispY + y1 + (y2 - y1) * divergePoint + randomFull(), - midDispX + x1 + 2 * (x2 - x1) * divergePoint + randomFull(), - midDispY + y1 + 2 * (y2 - y1) * divergePoint + randomFull(), - x2 + (preserveVertices and 0 or randomFull()), - y2 + (preserveVertices and 0 or randomFull()), - } - } - ops[#ops+1] = t - end - return ops -end - -local function _doubleLine(x1, y1, x2, y2, o, filling) - local singleStroke = filling and o.disableMultiStrokeFill or o.disableMultiStroke - local o1 = _line(x1, y1, x2, y2, o, true, false) - if singleStroke then - return o1 - end - local o2 = _line(x1, y1, x2, y2, o, true, true) - -- fusing arrays - local t = {} - local n = 0 - for _, v in ipairs(o1) do - n = n + 1 - t[n] = v - end - for _, v in ipairs(o2) do - n = n + 1 - t[n] = v - end - return t -end - --- From renderer.ts (public functions) - -local function line(x1, y1, x2, y2, o) - return { type = 'path', ops = _doubleLine(x1, y1, x2, y2, o) } -end - -local function linearPath(points, close, o) - local len = #(points or {}) - if len >= 2 then - local ops = {} - for i = 1, len - 1 do - local t = _doubleLine(points[i][1], points[i][2], points[i + 1][1], points[i + 1][2], o) - for k = 1, #t do - ops[#ops+1] = t[k] - end - end - if close then - local t = _doubleLine(points[len][1], points[len][2], points[1][1], points[1][2], o) - for k = 1, #t do - ops[#ops+1] = t[k] - end - end - return { type = 'path', ops = ops } - elseif len == 2 then - return line(points[1][1], points[1][2], points[2][1], points[2][2], o) - end - return { type = 'path', ops = {} } -end - -local function polygon(points, o) - return linearPath(points, true, o) -end - -local function rectangle(x, y, width, height, o) - local points = { - {x, y}, - {x + width, y}, - {x + width, y + height}, - {x, y + height} - } - return polygon(points, o) -end - --- from geometry.ts - -local function rotatePoints(points, center, degrees) - if points and #points ~= 0 then - local cx = center[1] - local cy = center[2] - local angle = (math.pi / 180) * degrees - local cos = math.cos(angle) - local sin = math.sin(angle) - for _, p in ipairs(points) do - local x = p[1] - local y = p[2] - p[1] = ((x - cx) * cos) - ((y - cy) * sin) + cx - p[2] = ((x - cx) * sin) + ((y - cy) * cos) + cy - end - end -end - -local function rotateLines(lines, center, degrees) - local points = {} - for _, linea in ipairs(lines) do - for _, l in ipairs(linea) do - points[#points + 1] = l - end - end - rotatePoints(points, center, degrees); -end - --- from fillers/scan-line-hachure.ts - --- quick Lua shim... -local function table_splice(tbl, start, length) -- from xlua - length = length or 1 - start = start or 1 - local endd = start + length - local spliced = {} - local remainder = {} - for i, elt in ipairs(tbl) do - if i < start or i >= endd then - table.insert(spliced, elt) - else - table.insert(remainder, elt) - end - end - return spliced, remainder -end -local function math_round (x) -- quick Lua shim - return x>=0 and math.floor(x+0.5) or math.ceil(x-0.5) -end - -local function straightHachureLines (polygonList, gap) - local vertexArray = {} - for _, poly in ipairs(polygonList) do - -- local vertices = [...polygon] - local vertices = poly -- NOTE Should we make a copy? Why the spreading in JS ? - if vertices[1][1] ~= vertices[#vertices][1] and vertices[1][2] ~= vertices[#vertices][2] then - vertices[#vertices + 1] = { vertices[1][1], vertices[1][2] } - end - if #vertices > 2 then - vertexArray[#vertexArray + 1] = vertices - end - end - - local lines = {} - gap = math.max(gap, 0.1) - - -- Create sorted edges table - local edges = {} - - for _, vertices in ipairs(vertexArray) do - for i = 1, #vertices - 1 do - local p1 = vertices[i] - local p2 = vertices[i + 1] - if p1[2] ~= p2[2] then - local ymin = math.min(p1[2], p2[2]) - edges[#edges + 1] = { - ymin = ymin, - ymax = math.max(p1[2], p2[2]), - x = (ymin == p1[2]) and p1[1] or p2[1], - islope = (p2[1] - p1[1]) / (p2[2] - p1[2]), - } - end - end - end - - local f = function (e1, e2) - if e1.ymin < e2.ymin then - return true - end - if e1.ymin > e2.ymin then - return false - end - if e1.x < e2.x then - return true - end - if e1.x > e2.x then - return false - end - if (e1.ymax < e2.ymax) then - return true - end - if (e1.ymax > e2.ymax) then - return false - end - return true - end - table.sort(edges, f) - if #edges == 0 then - return lines - end - - -- Start scanning - local activeEdges = {} - local y = edges[1].ymin; - while (#activeEdges > 0 or #edges > 0) do - if #edges ~= 0 then - local ix = 0 - for i = 1, #edges do - if edges[i].ymin > y then - break; - end - ix = i - end - local removed - edges, removed = table_splice(edges, 1, ix) - for _, e in ipairs(removed) do - activeEdges[#activeEdges + 1] = { s = y, edge = e } - end - end - activeEdges = pl.tablex.filter(activeEdges, function (ae) - if ae.edge.ymax <= y then - return false - end - return true; - end) - table.sort(activeEdges, function (ae1, ae2) - if ae1.edge.x < ae2.edge.x then - return true - end - return false - end) - - -- fill between the edges - if (#activeEdges > 1) then - for i = 1, #activeEdges, 2 do - local nexti = i + 1 - if nexti > #activeEdges then - break - end - local ce = activeEdges[i].edge; - local ne = activeEdges[nexti].edge; - lines[#lines + 1] = { - { math_round(ce.x), y }, - { math_round(ne.x), y }, - } - end - end - - y = y + gap - for _, ae in ipairs(activeEdges) do - ae.edge.x = ae.edge.x + (gap * ae.edge.islope) - end - end - return lines -end - - -local function polygonHachureLines (polygonList, o) - local angle = o.hachureAngle + 90; - local gap = o.hachureGap - if gap < 0 then - gap = o.strokeWidth * 4 - end - gap = math.max(gap, 0.1) - - local rotationCenter = {0, 0} - if angle then - for _, poly in ipairs(polygonList) do - rotatePoints(poly, rotationCenter, angle) - end - end - local lines = straightHachureLines(polygonList, gap) - if angle then - -- NOTE: This code was in rough.js but is not needed, right? - -- for _, poly in ipairs(polygonList) do - -- rotatePoints(poly, rotationCenter, -angle) - -- end - rotateLines(lines, rotationCenter, -angle) - end - return lines -end - --- from fillers/harchure-filler.ts - -local HachureFiller = pl.class({ - fillPolygons = function (self, polygonList, o) - local lines = polygonHachureLines(polygonList, o); - local ops = self:renderLines(lines, o); - return { type = 'fillSketch', ops = ops } - end, - - renderLines = function (_, lines, o) - local ops = {} - for _, lin in ipairs(lines) do - -- NOTE rough.js used a helper here. - local t = _doubleLine(lin[1][1], lin[1][2], lin[2][1], lin[2][2], o, true) - for _, v in ipairs(t) do - ops[#ops + 1] = v - end - end - return ops; - end, -}) - --- a bit of hack renderer.ts --- ... - -local filler = HachureFiller() -local function patternFillPolygons(polygonList, o) - return filler:fillPolygons(polygonList, o); -end - --- From generator.ts - -local RoughGenerator = pl.class({ - defaultOptions = { - maxRandomnessOffset = 2, - roughness = 1, - bowing = 1, - stroke = { l = 0 }, -- COMPAT WITH SILE PARSED COLORS - strokeWidth = 1, - -- curveTightness = 0, - -- curveFitting = 0.95, - -- curveStepCount = 9, - fillStyle = 'hachure', - -- fillWeight = -1, - hachureAngle = -41, - hachureGap = -1, - -- dashOffset = -1, - -- dashGap = -1, - -- zigzagOffset = -1, - -- seed = 0, - disableMultiStroke = false, - disableMultiStrokeFill = false, - preserveVertices = false, - }, - - _init = function (self, options) - if options then - self.defaultOptions = self:_o(options) - end - end, - - _d = function (self, shape, sets, options) - return { shape = shape, sets = sets or {}, options = options or self.defaultOptions } - end, - - _o = function (self, options) - return options and pl.tablex.union(self.defaultOptions, options) or self.defaultOptions - end, - - line = function (self, x1, y1, x2, y2, options) - local o = self:_o(options) - return self:_d('line', { line(x1, y1, x2, y2, o) }, o) - end, - - rectangle = function (self, x, y, width, height, options) - local o = self:_o(options) - - local paths = {} - local outline = rectangle(x, y, width, height, o) - if o.fill then - local points = { {x, y}, {x + width, y}, {x + width, y + height}, {x, y + height} } - if o.fillStyle == 'solid' then - SU.error("Rough fill (solid) not yet implemented.") - -- paths[#paths + 1] = solidFillPolygon({ points }, o) - else - paths[#paths + 1] = patternFillPolygons({ points }, o) - end - end - if o.stroke ~= 'none' then - paths[#paths+1] = outline - end - return self:_d('rectangle', paths, o) - end, -}) - --- Exports +SU.warn("The rough.lua module is deprecated. Please use the rough-lua module instead.") return { RoughGenerator = RoughGenerator, -} \ No newline at end of file +} diff --git a/packages/framebox/init.lua b/packages/framebox/init.lua index de5d0e5..5896bf6 100644 --- a/packages/framebox/init.lua +++ b/packages/framebox/init.lua @@ -182,7 +182,14 @@ function package:registerCommands () self:registerCommand("roughbox", function(options, content) local padding = SU.cast("measurement", options.padding or SILE.settings:get("framebox.padding")):tonumber() + local border = SU.boolean(options.border, true) local borderwidth = SU.cast("measurement", options.borderwidth or SILE.settings:get("framebox.borderwidth")):tonumber() + if borderwidth <= 0 then + border = false + -- This is actually a stroke width, so we need to make sure it is not 0 + -- (for hachures etc.) + borderwidth = SILE.settings:get("framebox.borderwidth"):tonumber() + end local bordercolor = SILE.color(options.bordercolor or "black") local fillcolor = options.fillcolor and SILE.color(options.fillcolor) local enlarge = SU.boolean(options.enlarge, false) @@ -198,8 +205,9 @@ function package:registerCommands () roughOpts.preserveVertices = SU.boolean(options.preserve, false) roughOpts.disableMultiStroke = SU.boolean(options.singlestroke, false) roughOpts.strokeWidth = borderwidth - roughOpts.stroke = bordercolor + roughOpts.stroke = border and bordercolor or 'none' roughOpts.fill = fillcolor + roughOpts.fillStyle = options.fillstyle or 'hachure' frameHbox(hbox, hlist, nil, function(w, h, d) local H = h + d @@ -331,14 +339,31 @@ As above, the \autodoc:parameter{padding}, \autodoc:parameter{borderwidth} and \ options all apply, as well as \autodoc:parameter{fillcolor}, so one can make a \roughbox[bordercolor=#b94051,fillcolor=220]{rough \em{hachured} box.} -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 +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) and \autodoc:parameter{singlestroke} (defaults to false; if set to true, a single stroke is applied +\autodoc:parameter{preserve} (defaults to false; when set to true, the 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.} +For instance, here is a single-stroked \roughbox[bordercolor=#59b24c, singlestroke=true]{rough box,} +and a cross-hatched \roughbox[border=false, fillcolor=#ecebbd, fillstyle=cross-hatch]{rough box.} + +The last example also shows the \autodoc:parameter{fillstyle} option (defaults to “hachure”). +It can also be set to “solid”, “zigzag”, “cross-hatch”, “dashed”, “zigzag-line” or “dots”. + +\smallskip +\roughbox[bordercolor=150, fillcolor=#bcc6d7, singlestroke=true]{Hachure,}\kern[width=0.8em] +\roughbox[bordercolor=150, fillcolor=#bcc6d7, singlestroke=true, fillstyle=solid]{solid,}\kern[width=0.8em] +\roughbox[bordercolor=150, fillcolor=#bcc6d7, singlestroke=true, fillstyle=zigzag]{zigzag,}\kern[width=0.8em] +\roughbox[bordercolor=150, fillcolor=#bcc6d7, singlestroke=true, fillstyle=cross-hatch]{cross-hatch,}\kern[width=0.8em] +\roughbox[bordercolor=150, fillcolor=#bcc6d7, singlestroke=true, fillstyle=dashed]{dashed,}\kern[width=0.8em] +\roughbox[bordercolor=150, fillcolor=#bcc6d7, singlestroke=true, fillstyle=zigzag-line]{zigag-line.}\kern[width=0.8em] +\roughbox[bordercolor=150, fillcolor=#bcc6d7, singlestroke=true, fillstyle=dots]{and dots.} + +\smallskip +The border width is actually the stroke width, also used for the hachures, etc. +Use \autodoc:parameter{border=false} if you want to disable the border and only keep the fill. Compared to the previous box framing commands, rough boxes by default do not take up more horizontal and vertical space due to their padding, as if the sketchy box was indeed manually added diff --git a/packages/framebox/rough-lua/fillers/dashed-filler.lua b/packages/framebox/rough-lua/fillers/dashed-filler.lua new file mode 100644 index 0000000..2772d45 --- /dev/null +++ b/packages/framebox/rough-lua/fillers/dashed-filler.lua @@ -0,0 +1,53 @@ +-- +-- License: MIT +-- Copyright (c) 2023, Didier Willis +-- +-- This is a straightforward port of the rough.js JavaScript library. +-- (https://github.com/rough-stuff/rough) +-- License MIT +-- Copyright (c) 2019 Preet Shihn +-- +local polygonHachureLines = require("packages.framebox.rough-lua.fillers.scan-line-hachure").polygonHachureLines +local lineLength = require("packages.framebox.rough-lua.geometry").lineLength + +local DashedFiller = pl.class() + +function DashedFiller:_init (helper) + self.helper = helper +end + +function DashedFiller:fillPolygons (polygonList, o) + local lines = polygonHachureLines(polygonList, o) + return { type = 'fillSketch', ops = self:dashedLine(lines, o) } +end + +function DashedFiller:dashedLine (lines, o) + local offset = o.dashOffset < 0 and (o.hachureGap < 0 and (o.strokeWidth * 4) or o.hachureGap) or o.dashOffset + local gap = o.dashGap < 0 and (o.hachureGap < 0 and (o.strokeWidth * 4) or o.hachureGap) or o.dashGap + local ops = {} + for _, line in ipairs(lines) do + local length = lineLength(line) + local count = math.floor(length / (offset + gap)) + local startOffset = (length + gap - (count * (offset + gap))) / 2 + local p1 = line[1] + local p2 = line[2] + if p1[1] > p2[1] then + p1 = line[2] + p2 = line[1] + end + local alpha = math.atan((p2[2] - p1[2]) / (p2[1] - p1[1])) + for i = 0, count - 1 do + local lstart = i * (offset + gap) + local lend = lstart + offset + local start = { p1[1] + (lstart * math.cos(alpha)) + (startOffset * math.cos(alpha)), p1[2] + lstart * math.sin(alpha) + (startOffset * math.sin(alpha)) } + local end_ = { p1[1] + (lend * math.cos(alpha)) + (startOffset * math.cos(alpha)), p1[2] + (lend * math.sin(alpha)) + (startOffset * math.sin(alpha)) } + local t = self.helper.doubleLineOps(start[1], start[2], end_[1], end_[2], o) + pl.tablex.insertvalues(ops, t) -- = JS ops.push(...t) + end + end + return ops +end + +return { + DashedFiller = DashedFiller +} diff --git a/packages/framebox/rough-lua/fillers/dot-filler.lua b/packages/framebox/rough-lua/fillers/dot-filler.lua new file mode 100644 index 0000000..7b0f7a4 --- /dev/null +++ b/packages/framebox/rough-lua/fillers/dot-filler.lua @@ -0,0 +1,58 @@ +-- +-- License: MIT +-- Copyright (c) 2023, Didier Willis +-- +-- This is a straightforward port of the rough.js JavaScript library. +-- (https://github.com/rough-stuff/rough) +-- License MIT +-- Copyright (c) 2019 Preet Shihn +-- +local polygonHachureLines = require("packages.framebox.rough-lua.fillers.scan-line-hachure").polygonHachureLines +local lineLength = require("packages.framebox.rough-lua.geometry").lineLength + +local DotFiller = pl.class() + +function DotFiller:_init (helper) + self.helper = helper +end + +function DotFiller:fillPolygons (polygonList, o) + o = pl.tablex.deepcopy(o) + o.hachureAngle = 0 + local lines = polygonHachureLines(polygonList, o) + return self:dotsOnLines(lines, o) +end + +function DotFiller:dotsOnLines (lines, o) + local ops = {} + local gap = o.hachureGap + if gap < 0 then + gap = o.strokeWidth * 4 + end + gap = math.max(gap, 0.1) + local fweight = o.fillWeight + if fweight < 0 then + fweight = o.strokeWidth / 2 + end + local ro = gap / 4 + for _, line in ipairs(lines) do + local length = lineLength(line) + local dl = length / gap + local count = math.ceil(dl) - 1 + local offset = length - (count * gap) + local x = ((line[1][1] + line[2][1]) / 2) - (gap / 4) + local minY = math.min(line[1][2], line[2][2]) + for i = 0, count - 1 do + local y = minY + offset + (i * gap) + local cx = (x - ro) + math.random() * 2 * ro + local cy = (y - ro) + math.random() * 2 * ro + local el = self.helper.ellipse(cx, cy, fweight, fweight, o) + pl.tablex.insertvalues(ops, el.ops) -- = JS ops.push(...el.ops) + end + end + return { type = 'fillSketch', ops = ops } +end + +return { + DotFiller = DotFiller +} diff --git a/packages/framebox/rough-lua/fillers/filler.lua b/packages/framebox/rough-lua/fillers/filler.lua new file mode 100644 index 0000000..205a268 --- /dev/null +++ b/packages/framebox/rough-lua/fillers/filler.lua @@ -0,0 +1,42 @@ +-- +-- License: MIT +-- Copyright (c) 2023, Didier Willis +-- +-- This is a straightforward port of the rough.js JavaScript library. +-- (https://github.com/rough-stuff/rough) +-- License MIT +-- Copyright (c) 2019 Preet Shihn +-- +local HachureFiller = require("packages.framebox.rough-lua.fillers.hachure-filler").HachureFiller +local ZigZagFiller = require("packages.framebox.rough-lua.fillers.zigzag-filler").ZigZagFiller +local HatchFiller = require("packages.framebox.rough-lua.fillers.hatch-filler").HatchFiller +local DotFiller = require("packages.framebox.rough-lua.fillers.dot-filler").DotFiller +local DashedFiller = require("packages.framebox.rough-lua.fillers.dashed-filler").DashedFiller +local ZigZagLineFiller = require("packages.framebox.rough-lua.fillers.zigzag-line-filler").ZigZagLineFiller + +local fillers = {} + +local function getFiller (o, helper) + local fillerName = o.fillStyle or 'hachure' + if not fillers[fillerName] then + if fillerName == 'zigzag' then + fillers[fillerName] = ZigZagFiller(helper) + elseif fillerName == 'cross-hatch' then + fillers[fillerName] = HatchFiller(helper) + elseif fillerName == 'dots' then + fillers[fillerName] = DotFiller(helper) + elseif fillerName == 'dashed' then + fillers[fillerName] = DashedFiller(helper) + elseif fillerName == 'zigzag-line' then + fillers[fillerName] = ZigZagLineFiller(helper) + else + fillerName = 'hachure' + fillers[fillerName] = HachureFiller(helper) + end + end + return fillers[fillerName] +end + +return { + getFiller = getFiller, +} diff --git a/packages/framebox/rough-lua/fillers/hachure-fill.lua b/packages/framebox/rough-lua/fillers/hachure-fill.lua new file mode 100644 index 0000000..6b5a92a --- /dev/null +++ b/packages/framebox/rough-lua/fillers/hachure-fill.lua @@ -0,0 +1,187 @@ +-- +-- License: MIT +-- Copyright (c) 2023, Didier Willis +-- +-- This is a straightforward port of the rough.js JavaScript library. +-- (https://github.com/rough-stuff/rough) +-- License MIT +-- Copyright (c) 2019 Preet Shihn +-- +-- https://github.com/pshihn/hachure-fill +-- MIT License +-- Copyright (c) 2023 Preet Shihn +-- + +local jsshims = require("packages.framebox.rough-lua.jsshims") +local array_splice = jsshims.array_splice +local math_round = jsshims.math_round + +local rotatePoints = function (points, center, degrees) + if points and #points > 0 then + local cx, cy = center[1], center[2] + local angle = (math.pi / 180) * degrees + local cos = math.cos(angle) + local sin = math.sin(angle) + for _, p in ipairs(points) do + local x, y = p[1], p[2] + p[1] = ((x - cx) * cos) - ((y - cy) * sin) + cx + p[2] = ((x - cx) * sin) + ((y - cy) * cos) + cy + end + end +end + +local function rotateLines (lines, center, degrees) + local points = {} + for _, line in ipairs(lines) do + for _, p in ipairs(line) do + points[#points + 1] = p + end + end + rotatePoints(points, center, degrees) +end + +local function areSamePoints(p1, p2) + return p1[1] == p2[1] and p1[2] == p2[2] +end + +local function straightHachureLines (polygons, gap, hachureStepOffset) + local vertexArray = {} + for _, polygon in ipairs(polygons) do + local vertices = polygon + if not areSamePoints(vertices[1], vertices[#vertices]) then + vertices[#vertices + 1] = { vertices[1][1], vertices[1][2] } + end + if #vertices > 2 then + vertexArray[#vertexArray + 1] = vertices + end + end + + local lines = {} + gap = math.max(gap, 0.1) + + -- Create sorted edges table + local edges = {} + + for _, vertices in ipairs(vertexArray) do + for i = 1, #vertices - 1 do + local p1 = vertices[i] + local p2 = vertices[i + 1] + if p1[2] ~= p2[2] then + local ymin = math.min(p1[2], p2[2]) + edges[#edges + 1] = { + ymin = ymin, + ymax = math.max(p1[2], p2[2]), + x = (ymin == p1[2]) and p1[1] or p2[1], + islope = (p2[1] - p1[1]) / (p2[2] - p1[2]), + } + end + end + end + + local f = function (e1, e2) + if e1.ymin < e2.ymin then + return true + end + if e1.ymin > e2.ymin then + return false + end + if e1.x < e2.x then + return true + end + if e1.x > e2.x then + return false + end + if (e1.ymax < e2.ymax) then + return true + end + -- PORTING NOTE: + -- Lua sorting differs from JS + -- Not so sure about the correctness here! + return false + end + table.sort(edges, f) + if #edges == 0 then + return lines + end + + -- Start scanning + local activeEdges = {} + local y = edges[1].ymin + local iteration = 0 + while #activeEdges > 0 or #edges > 0 do + if #edges > 0 then + local ix = -1 + for i = 1, #edges do + if edges[i].ymin > y then + break + end + ix = i + end + local removed + edges, removed = array_splice(edges, 1, ix) + for _, edge in ipairs(removed) do + activeEdges[#activeEdges + 1] = { s = y, edge = edge } + end + end + activeEdges = pl.tablex.filter(activeEdges, function (ae) + if ae.edge.ymax <= y then + return false + end + return true + end) + table.sort(activeEdges, function (ae1, ae2) + if ae1.edge.x < ae2.edge.x then + return true + end + return false + end) + + -- fill between the edges + if (hachureStepOffset ~= 1) or (iteration % gap == 0) then + if #activeEdges > 1 then + for i = 1, #activeEdges, 2 do + local nexti = i + 1 + if nexti > #activeEdges then + break + end + local ce = activeEdges[i].edge + local ne = activeEdges[nexti].edge + lines[#lines + 1] = { + { math_round(ce.x), y }, + { math_round(ne.x), y }, + } + end + end + end + y = y + hachureStepOffset + for _, ae in ipairs(activeEdges) do + ae.edge.x = ae.edge.x + (hachureStepOffset * ae.edge.islope) + end + iteration = iteration + 1 + end + return lines +end + +local function hachureLines (polygons, hachureGap, hachureAngle, hachureStepOffset) + local angle = hachureAngle + local gap = math.max(hachureGap, 0.1) + local polygonList = (polygons[1] and polygons[1][1] and type(polygons[1][1]) == 'number') and { polygons } or polygons + local rotationCenter = { 0, 0 } + if angle then + for _, polygon in ipairs(polygonList) do + rotatePoints(polygon, rotationCenter, angle) + end + end + local lines = straightHachureLines(polygonList, gap, hachureStepOffset) + if angle then + for _, polygon in ipairs(polygonList) do + rotatePoints(polygon, rotationCenter, -angle) + end + rotateLines(lines, rotationCenter, -angle) + end + return lines +end + +return { + hachureLines = hachureLines, +} diff --git a/packages/framebox/rough-lua/fillers/hachure-filler.lua b/packages/framebox/rough-lua/fillers/hachure-filler.lua new file mode 100644 index 0000000..d113aa7 --- /dev/null +++ b/packages/framebox/rough-lua/fillers/hachure-filler.lua @@ -0,0 +1,35 @@ +-- +-- License: MIT +-- Copyright (c) 2023, Didier Willis +-- +-- This is a straightforward port of the rough.js JavaScript library. +-- (https://github.com/rough-stuff/rough) +-- License MIT +-- Copyright (c) 2019 Preet Shihn +-- +local polygonHachureLines = require("packages.framebox.rough-lua.fillers.scan-line-hachure").polygonHachureLines + +local HachureFiller = pl.class() + +function HachureFiller:_init (helper) + self.helper = helper +end + +function HachureFiller:fillPolygons (polygonList, o) + local lines = polygonHachureLines(polygonList, o) + local ops = self:renderLines(lines, o) + return { type = 'fillSketch', ops = ops } +end + +function HachureFiller:renderLines (lines, o) + local ops = {} + for _, line in ipairs(lines) do + local t = self.helper.doubleLineOps(line[1][1], line[1][2], line[2][1], line[2][2], o) + pl.tablex.insertvalues(ops, t) -- = JS ops.push(...t) + end + return ops +end + +return { + HachureFiller = HachureFiller, +} diff --git a/packages/framebox/rough-lua/fillers/hatch-filler.lua b/packages/framebox/rough-lua/fillers/hatch-filler.lua new file mode 100644 index 0000000..299a7a1 --- /dev/null +++ b/packages/framebox/rough-lua/fillers/hatch-filler.lua @@ -0,0 +1,25 @@ +-- +-- License: MIT +-- Copyright (c) 2023, Didier Willis +-- +-- This is a straightforward port of the rough.js JavaScript library. +-- (https://github.com/rough-stuff/rough) +-- License MIT +-- Copyright (c) 2019 Preet Shihn +-- +local HachureFiller = require("packages.framebox.rough-lua.fillers.hachure-filler").HachureFiller + +local HatchFiller = pl.class(HachureFiller) + +function HatchFiller:fillPolygons (polygonList, o) + local set = self._base.fillPolygons(self, polygonList, o) + local o2 = pl.tablex.copy(o) + o2.hachureAngle = o.hachureAngle + 90 + local set2 = self._base.fillPolygons(self, polygonList, o2) + pl.tablex.insertvalues(set.ops, set2.ops) + return set +end + +return { + HatchFiller = HatchFiller +} diff --git a/packages/framebox/rough-lua/fillers/scan-line-hachure.lua b/packages/framebox/rough-lua/fillers/scan-line-hachure.lua new file mode 100644 index 0000000..f4e284a --- /dev/null +++ b/packages/framebox/rough-lua/fillers/scan-line-hachure.lua @@ -0,0 +1,36 @@ +-- +-- License: MIT +-- Copyright (c) 2023, Didier Willis +-- +-- This is a straightforward port of the rough.js JavaScript library. +-- (https://github.com/rough-stuff/rough) +-- License MIT +-- Copyright (c) 2019 Preet Shihn +-- +local hachureLines = require("packages.framebox.rough-lua.fillers.hachure-fill").hachureLines + +local jsshims = require("packages.framebox.rough-lua.jsshims") +local math_round = jsshims.math_round + +local PRNG = require("packages.framebox.graphics.prng") +local prng = PRNG() + +local function polygonHachureLines (polygonList, o) + local angle = o.hachureAngle + 90 + local gap = o.hachureGap + if gap < 0 then + gap = o.strokeWidth * 4 + end + gap = math_round(math.max(gap, 0.1)) + local skipOffset = 1 + if o.roughness >= 1 then + if prng:random() > 0.7 then + skipOffset = gap + end + end + return hachureLines(polygonList, gap, angle, skipOffset or 1) +end + +return { + polygonHachureLines = polygonHachureLines, +} diff --git a/packages/framebox/rough-lua/fillers/zigzag-filler.lua b/packages/framebox/rough-lua/fillers/zigzag-filler.lua new file mode 100644 index 0000000..d7552fb --- /dev/null +++ b/packages/framebox/rough-lua/fillers/zigzag-filler.lua @@ -0,0 +1,47 @@ +-- +-- License: MIT +-- Copyright (c) 2023, Didier Willis +-- +-- This is a straightforward port of the rough.js JavaScript library. +-- (https://github.com/rough-stuff/rough) +-- License MIT +-- Copyright (c) 2019 Preet Shihn +-- +local HachureFiller = require("packages.framebox.rough-lua.fillers.hachure-filler").HachureFiller +local polygonHachureLines = require("packages.framebox.rough-lua.fillers.scan-line-hachure").polygonHachureLines +local lineLength = require("packages.framebox.rough-lua.geometry").lineLength + +local ZigZagFiller = pl.class(HachureFiller) + +function ZigZagFiller:fillPolygons (polygonList, o) + local gap = o.hachureGap + if gap < 0 then + gap = o.strokeWidth * 4 + end + gap = math.max(gap, 0.1) + local o2 = pl.tablex.deepcopy(o) + o2.hachureGap = gap + local lines = polygonHachureLines(polygonList, o2) + local zigZagAngle = (math.pi / 180) * o.hachureAngle + local zigzagLines = {} + local dgx = gap * 0.5 * math.cos(zigZagAngle) + local dgy = gap * 0.5 * math.sin(zigZagAngle) + for _, line in ipairs(lines) do + if lineLength(line) then + zigzagLines[#zigzagLines + 1] = { + { line[1][1] - dgx, line[1][2] + dgy }, + { line[2][1], line[2][2] }, + } + zigzagLines[#zigzagLines + 1] = { + { line[1][1] + dgx, line[1][2] - dgy }, + { line[2][1], line[2][2] }, + } + end + end + local ops = self:renderLines(zigzagLines, o) + return { type = 'fillSketch', ops = ops } +end + +return { + ZigZagFiller = ZigZagFiller +} diff --git a/packages/framebox/rough-lua/fillers/zigzag-line-filler.lua b/packages/framebox/rough-lua/fillers/zigzag-line-filler.lua new file mode 100644 index 0000000..071ff52 --- /dev/null +++ b/packages/framebox/rough-lua/fillers/zigzag-line-filler.lua @@ -0,0 +1,61 @@ +-- +-- License: MIT +-- Copyright (c) 2023, Didier Willis +-- +-- This is a straightforward port of the rough.js JavaScript library. +-- (https://github.com/rough-stuff/rough) +-- License MIT +-- Copyright (c) 2019 Preet Shihn +-- +local polygonHachureLines = require("packages.framebox.rough-lua.fillers.scan-line-hachure").polygonHachureLines +local lineLength = require("packages.framebox.rough-lua.geometry").lineLength + +local jsshims = require("packages.framebox.rough-lua.jsshims") +local math_round = jsshims.math_round + +local ZigZagLineFiller = pl.class() + +function ZigZagLineFiller:_init (helper) + self.helper = helper +end + +function ZigZagLineFiller:fillPolygons (polygonList, o) + local gap = o.hachureGap < 0 and (o.strokeWidth * 4) or o.hachureGap + local zo = o.zigzagOffset < 0 and gap or o.zigzagOffset + o = pl.tablex.copy(o) + o.hachureGap = gap + zo + local lines = polygonHachureLines(polygonList, o) + return { type = 'fillSketch', ops = self:zigzagLines(lines, zo, o) } +end + +function ZigZagLineFiller:zigzagLines (lines, zo, o) + local ops = {} + for _, line in ipairs(lines) do + local length = lineLength(line) + local count = math_round(length / (2 * zo)) + local p1 = line[1] + local p2 = line[2] + if p1[1] > p2[1] then + p1 = line[2] + p2 = line[1] + end + local alpha = math.atan((p2[2] - p1[2]) / (p2[1] - p1[1])) + for i = 0, count - 1 do + local lstart = i * 2 * zo + local lend = (i + 1) * 2 * zo + local dz = math.sqrt(2 * zo^2) -- = JS Math.sqrt(2 * Math.pow(zo, 2)) dubious? + local start = { p1[1] + (lstart * math.cos(alpha)), p1[2] + lstart * math.sin(alpha) } + local end_ = { p1[1] + (lend * math.cos(alpha)), p1[2] + (lend * math.sin(alpha)) } + local middle = { start[1] + dz * math.cos(alpha + math.pi / 4), start[2] + dz * math.sin(alpha + math.pi / 4) } + local t = self.helper.doubleLineOps(start[1], start[2], middle[1], middle[2], o) + pl.tablex.insertvalues(ops, t) -- = JS ops.push(...t) + t = self.helper.doubleLineOps(middle[1], middle[2], end_[1], end_[2], o) + pl.tablex.insertvalues(ops, t) -- = JS ops.push(...t) + end + end + return ops +end + +return { + ZigZagLineFiller = ZigZagLineFiller, +} diff --git a/packages/framebox/rough-lua/generator.lua b/packages/framebox/rough-lua/generator.lua new file mode 100644 index 0000000..33ccc36 --- /dev/null +++ b/packages/framebox/rough-lua/generator.lua @@ -0,0 +1,325 @@ +-- +-- License: MIT +-- Copyright (c) 2023, Didier Willis +-- +-- This is a straightforward port of the rough.js JavaScript library. +-- (https://github.com/rough-stuff/rough) +-- License MIT +-- Copyright (c) 2019 Preet Shihn +-- +local renderer = require("packages.framebox.rough-lua.renderer") +local line, rectangle, + ellipseWithParams, generateEllipseParams, + arc, curve, linearPath, + svgPath, + patternFillArc, patternFillPolygons, solidFillPolygon + = renderer.line, renderer.rectangle, + renderer.ellipseWithParams, renderer.generateEllipseParams, + renderer.arc, renderer.curve, renderer.linearPath, + renderer.svgPath, + 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 (_, _, _) + SU.error("Not implemented") +end +local curveToBezier = function (_) + SU.error("Not implemented") +end +local pointsOnBezierCurves = function (_, _, _) + SU.error("Not implemented") +end + + +local RoughGenerator = pl.class({ + defaultOptions = { + maxRandomnessOffset = 2, + roughness = 1, + bowing = 1, + stroke = { l = 0 }, -- PORTING NOTE: COMPAT WITH SILE PARSED COLORS + strokeWidth = 1, + curveTightness = 0, + curveFitting = 0.95, + curveStepCount = 9, + fillStyle = 'hachure', + fillWeight = -1, + hachureAngle = -41, + hachureGap = -1, + dashOffset = -1, + dashGap = -1, + zigzagOffset = -1, + seed = 0, + disableMultiStroke = false, + disableMultiStrokeFill = false, + preserveVertices = false, + }, + + _init = function (self, options) + if options then + self.defaultOptions = self:_o(options) + end + end, + + _d = function (self, shape, sets, options) + return { shape = shape, sets = sets or {}, options = options or self.defaultOptions } + end, + + _o = function (self, options) + return options and pl.tablex.union(self.defaultOptions, options) or self.defaultOptions + end, + + line = function (self, x1, y1, x2, y2, options) + local o = self:_o(options) + return self:_d('line', { line(x1, y1, x2, y2, o) }, o) + end, + + rectangle = function (self, x, y, width, height, options) + local o = self:_o(options) + local paths = {} + local outline = rectangle(x, y, width, height, o) + if o.fill then + local points = { {x, y}, {x + width, y}, {x + width, y + height}, {x, y + height} } + if o.fillStyle == 'solid' then + paths[#paths + 1] = solidFillPolygon({ points }, o) + else + paths[#paths + 1] = patternFillPolygons({ points }, o) + end + end + if o.stroke ~= 'none' then + paths[#paths+1] = outline + end + return self:_d('rectangle', paths, o) + end, + + ellipse = function (self, x, y, width, height, options) + local o = self:_o(options) + local paths = {} + local ellipseParams = generateEllipseParams(width, height, o) + local ellipseResponse = ellipseWithParams(x, y, o, ellipseParams) + if o.fill then + if o.fillStyle == 'solid' then + local shape = ellipseWithParams(x, y, o, ellipseParams).opset + shape.type = 'fillPath' + paths[#paths + 1] = shape + else + paths[#paths + 1] = patternFillPolygons({ ellipseResponse.estimatedPoints }, o) + end + end + if o.stroke ~= 'none' then + paths[#paths + 1] = ellipseResponse.opset + end + return self:_d('ellipse', paths, o) + end, + + circle = function (self, x, y, diameter, options) + local ret = self:ellipse(x, y, diameter, diameter, options) + ret.shape = 'circle' + return ret + end, + + linearPath = function (self, points, options) + local o = self:_o(options) + return self:_d('linearPath', { linearPath(points, false, o) }, o) + end, + + arc = function (self, x, y, width, height, start, stop, closed, options) + local o = self:_o(options) + local paths = {} + local outline = arc(x, y, width, height, start, stop, closed, true, o) + if closed and o.fill then + if o.fillStyle == 'solid' then + local fillOptions = pl.tablex.copy(o) + fillOptions.disableMultiStroke = true + local shape = arc(x, y, width, height, start, stop, true, false, fillOptions) + shape.type = 'fillPath' + paths[#paths + 1] = shape + else + paths[#paths + 1] = patternFillArc(x, y, width, height, start, stop, o) + end + end + if o.stroke ~= 'none' then + paths[#paths + 1] = outline + end + return self:_d('arc', paths, o) + end, + + curve = function (self, pointOrPoints, options) + local o = self:_o(options) + local paths = {} + local outline = curve(pointOrPoints, o) + if o.fill and o.fill ~= 'none' then + if o.fillStyle == 'solid' then + local fillShape = curve( + pointOrPoints, + pl.tablex.union(o, { + disableMultiStroke = true, + roughness = o.roughness and (o.roughness + o.fillShapeRoughnessGain) or 0 + } + )) + paths[#paths + 1] = { + type = 'fillPath', + ops = self:_mergedShape(fillShape.ops), + } + else + local polyPoints = {} + local inputPoints = pointOrPoints + if #inputPoints > 0 then + local p1 = inputPoints[1] + local pointsList = type(p1[1]) == 'number' and { inputPoints } or inputPoints + for _, points in ipairs(pointsList) do + if #points < 3 then + pl.tablex.insertvalues(polyPoints, points) -- = JS polyPoints.push(...points) + elseif #points == 3 then + local t = pointsOnBezierCurves(curveToBezier({ points[1], points[1], points[2], points[3] }), 10, (1 + o.roughness) / 2) + pl.tablex.insertvalues(polyPoints, t) -- = JS polyPoints.push(...t) + else + local t = pointsOnBezierCurves(curveToBezier(points), 10, (1 + o.roughness) / 2) + pl.tablex.insertvalues(polyPoints, t) -- = JS polyPoints.push(...t) + end + end + end + if #polyPoints > 0 then + paths[#paths + 1] = patternFillPolygons({ polyPoints }, o) + end + end + end + if o.stroke ~= 'none' then + paths[#paths + 1] = outline + end + return self:_d('curve', paths, o) + end, + + polygon = function (self, points, options) + local o = self:_o(options) + local paths = {} + local outline = linearPath(points, true, o) + if o.fill then + if o.fillStyle == 'solid' then + paths[#paths + 1] = solidFillPolygon({ points }, o) + else + paths[#paths + 1] = patternFillPolygons({ points }, o) + end + end + if o.stroke ~= 'none' then + paths[#paths + 1] = outline + end + return self:_d('polygon', paths, o) + end, + + path = function (self, d, options) + local o = self:_o(options) + local paths = {} + if not d then + return self:_d('path', paths, o) + end + d = d:gsub('\n', ' '):gsub('(-%s)', '-'):gsub('(%s%s)', ' ') + + local hasFill = o.fill and o.fill ~= 'transparent' and o.fill ~= 'none' + local hasStroke = o.stroke ~= 'none' + local simplified = o.simplification and o.simplification < 1 + local distance = simplified and (4 - 4 * (o.simplification or 1)) or ((1 + o.roughness) / 2) + local sets = pointsOnPath(d, 1, distance) + local shape = svgPath(d, o) + + if hasFill then + if o.fillStyle == 'solid' then + if #sets == 1 then + local fillShape = svgPath(d, pl.tablex.union(o, { disableMultiStroke = true, roughness = o.roughness and (o.roughness + o.fillShapeRoughnessGain) or 0 })) + paths[#paths + 1] = { + type = 'fillPath', + ops = self:_mergedShape(fillShape.ops), + } + else + paths[#paths + 1] = solidFillPolygon(sets, o) + end + else + paths[#paths + 1] = patternFillPolygons(sets, o) + end + end + if hasStroke then + if simplified then + for _, set in ipairs(sets) do + paths[#paths + 1] = linearPath(set, false, o) + end + else + paths[#paths + 1] = shape + end + end + + return self:_d('path', paths, o) + end, + + opsToPath = function (_, drawing, fixedDecimals) + local path = '' + for _, item in ipairs(drawing.ops) do + local data = fixedDecimals and pl.tablex.map(item.data, function (d) return tonumber(string.format('%.' .. fixedDecimals .. 'f', d)) end) or item.data + if item.op == 'move' then + path = path .. 'M' .. data[1] .. ' ' .. data[2] .. ' ' + elseif item.op == 'bcurveTo' then + path = path .. 'C' .. data[1] .. ' ' .. data[2] .. ', ' .. data[3] .. ' ' .. data[4] .. ', ' .. data[5] .. ' ' .. data[6] .. ' ' + elseif item.op == 'lineTo' then + path = path .. 'L' .. data[1] .. ' ' .. data[2] .. ' ' + end + end + return path:trim() + end, + + toPaths = function (self, drawable) + local sets = drawable.sets or {} + local o = drawable.options or self.defaultOptions + local paths = {} + for _, drawing in ipairs(sets) do + local path = nil + if drawing.type == 'path' then + path = { + d = self:opsToPath(drawing), + stroke = o.stroke, + strokeWidth = o.strokeWidth, + fill = 'none', + } + elseif drawing.type == 'fillPath' then + path = self:fillSketch(drawing, o) + elseif drawing.type == 'fillSketch' then + path = self:fillSketch(drawing, o) + end + if path then + paths[#paths + 1] = path + end + end + return paths + end, + + fillSketch = function (self, drawing, o) + local fweight = o.fillWeight + if fweight < 0 then + fweight = o.strokeWidth / 2 + end + return { + d = self:opsToPath(drawing), + stroke = 'none', + strokeWidth = fweight, + fill = o.fill or 'none', + } + end, + + _mergedShape = function (_, input) + return pl.tablex.filter(input, function (d, i) + if i == 1 then + return true + end + if d.op == 'move' then + return false + end + return true + end) + end, +}) + +-- Exports + +return { + RoughGenerator = RoughGenerator, +} diff --git a/packages/framebox/rough-lua/geometry.lua b/packages/framebox/rough-lua/geometry.lua new file mode 100644 index 0000000..9c8a209 --- /dev/null +++ b/packages/framebox/rough-lua/geometry.lua @@ -0,0 +1,18 @@ +-- +-- License: MIT +-- Copyright (c) 2023, Didier Willis +-- +-- This is a straightforward port of the rough.js JavaScript library. +-- (https://github.com/rough-stuff/rough) +-- License MIT +-- Copyright (c) 2019 Preet Shihn +-- +local function lineLength (line) + local p1 = line[1] + local p2 = line[2] + return math.sqrt((p1[1] - p2[1])^2 + (p1[2] - p2[2])^2) +end + +return { + lineLength = lineLength +} diff --git a/packages/framebox/rough-lua/jsshims.lua b/packages/framebox/rough-lua/jsshims.lua new file mode 100644 index 0000000..07da419 --- /dev/null +++ b/packages/framebox/rough-lua/jsshims.lua @@ -0,0 +1,44 @@ +-- +-- License: MIT +-- Copyright (c) 2023, Didier Willis +-- +-- Some convenience JavaScript-like functions to make the porting easier. +-- (So the code looks more like the original JavaScript code.) +-- +-- + +-- JS Math.round +local function math_round (x) + return x >= 0 and math.floor(x + 0.5) or math.ceil(x - 0.5) +end + +-- JS Array.splice +local function array_splice (t, start, length) -- from xlua + length = length or 1 + start = start or 1 + local ending = start + length + local spliced = {} + local remainder = {} + for i, item in ipairs(t) do + if i < start or i >= ending then + table.insert(spliced, item) + else + table.insert(remainder, item) + end + end + return spliced, remainder +end + +-- JS Array.code +local function array_concat (t1, t2) + local t = {} + pl.tablex.insertvalues(t, t1) + pl.tablex.insertvalues(t, t2) + return t +end + +return { + math_round = math_round, + array_splice = array_splice, + array_concat = array_concat, +} diff --git a/packages/framebox/rough-lua/renderer.lua b/packages/framebox/rough-lua/renderer.lua new file mode 100644 index 0000000..969d022 --- /dev/null +++ b/packages/framebox/rough-lua/renderer.lua @@ -0,0 +1,572 @@ +-- +-- License: MIT +-- Copyright (c) 2023, Didier Willis +-- +-- This is a straightforward port of the rough.js JavaScript library. +-- (https://github.com/rough-stuff/rough) +-- License MIT +-- Copyright (c) 2019 Preet Shihn +-- + +local jsshims = require("packages.framebox.rough-lua.jsshims") +local array_concat = jsshims.array_concat + +local PRNG = require("packages.framebox.graphics.prng") +local prng = PRNG() + +-- PORTING NOTE: +-- I ported path-data-parser but haven't tested it for now +-- local pathDataParser = require("packages.framebox.path-data-parser") +-- local parsePath, normalize, absolutize = pathDataParser.parsePath, pathDataParser.normalize, pathDataParser.absolutize +local normalize = function () SU.error("Not yet implemented") end +local absolutize = function () SU.error("Not yet implemented") end +local parsePath = function () SU.error("Not yet implemented") end + +local getFiller = require("packages.framebox.rough-lua.fillers.filler").getFiller + +local function cloneOptionsAlterSeed (o) + -- PORTING NOTE: + -- Option to alter seed no implemented. + return o +end + +local function _offset (min, max, ops, roughnessGain) + return ops.roughness * (roughnessGain or 1) * ((prng:random() * (max - min)) + min) +end + +local function _offsetOpt (x, ops, roughnessGain) + return _offset(-x, x, ops, roughnessGain or 1) +end + +local function _line (x1, y1, x2, y2, o, move, overlay) + local lengthSq = (x1 - x2)^2 + (y1 - y2)^2 + local length = math.sqrt(lengthSq) + local roughnessGain + if length < 200 then + roughnessGain = 1 + elseif length > 500 then + roughnessGain = 0.4 + else + roughnessGain = (-0.0016668) * length + 1.233334 + end + + local offset = o.maxRandomnessOffset or 0 + if (offset * offset * 100) > lengthSq then + offset = length / 10 + end + local halfOffset = offset / 2 + local divergePoint = 0.2 + prng:random() * 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) + midDispY = _offsetOpt(midDispY, o, roughnessGain) + local ops = {} + local randomHalf = function() return _offsetOpt(halfOffset, o, roughnessGain) end + local randomFull = function() return _offsetOpt(offset, o, roughnessGain) end + local preserveVertices = o.preserveVertices + if move then + if overlay then + local t = { + op = 'move', + data = { + x1 + (preserveVertices and 0 or randomHalf()), + y1 + (preserveVertices and 0 or randomHalf()), + } + } + ops[#ops+1] = t + else + local t = { + op = 'move', + data = { + x1 + (preserveVertices and 0 or _offsetOpt(offset, o, roughnessGain)), + y1 + (preserveVertices and 0 or _offsetOpt(offset, o, roughnessGain)), + }, + } + ops[#ops+1] = t + end + end + if overlay then + local t = { + op = 'bcurveTo', + data = { + midDispX + x1 + (x2 - x1) * divergePoint + randomHalf(), + midDispY + y1 + (y2 - y1) * divergePoint + randomHalf(), + midDispX + x1 + 2 * (x2 - x1) * divergePoint + randomHalf(), + midDispY + y1 + 2 * (y2 - y1) * divergePoint + randomHalf(), + x2 + (preserveVertices and 0 or randomHalf()), + y2 + (preserveVertices and 0 or randomHalf()), + } + } + ops[#ops+1] = t + else + local t = { + op = 'bcurveTo', + data = { + midDispX + x1 + (x2 - x1) * divergePoint + randomFull(), + midDispY + y1 + (y2 - y1) * divergePoint + randomFull(), + midDispX + x1 + 2 * (x2 - x1) * divergePoint + randomFull(), + midDispY + y1 + 2 * (y2 - y1) * divergePoint + randomFull(), + x2 + (preserveVertices and 0 or randomFull()), + y2 + (preserveVertices and 0 or randomFull()), + } + } + ops[#ops+1] = t + end + return ops +end + +local function _doubleLine (x1, y1, x2, y2, o, filling) + local singleStroke = filling and o.disableMultiStrokeFill or o.disableMultiStroke + local o1 = _line(x1, y1, x2, y2, o, true, false) + if singleStroke then + return o1 + end + local o2 = _line(x1, y1, x2, y2, o, true, true) + return array_concat(o1, o2) +end + +local function _curve (points, closePoint, o) + local len = #points + local ops = {} + if len > 3 then + local b = { 0,0,0,0 } + local s = 1 - o.curveTightness + ops[#ops+1] = { + op = 'move', + data = { + points[1][1], + points[1][2] + } + } + for i = 2, len - 2 do + local cachedVertArray = points[i] + b[1] = { cachedVertArray[1], cachedVertArray[2] } + b[2] = { cachedVertArray[1] + (s * points[i + 1][1] - s * points[i - 1][1]) / 6, cachedVertArray[2] + (s * points[i + 1][2] - s * points[i - 1][2]) / 6 } + b[3] = { points[i + 1][1] + (s * points[i][1] - s * points[i + 2][1]) / 6, points[i + 1][2] + (s * points[i][2] - s * points[i + 2][2]) / 6 } + b[4] = { points[i + 1][1], points[i + 1][2] } + ops[#ops+1] = { + op = 'bcurveTo', + data = { b[2][1], b[2][2], b[3][1], b[3][2], b[4][1], b[4][2] } + } + end + if closePoint and #closePoint == 2 then + local ro = o.maxRandomnessOffset + ops[#ops+1] = { + op = 'lineTo', + data = { + closePoint[1] + _offsetOpt(ro, o), + closePoint[2] + _offsetOpt(ro, o) + } + } + end + elseif len == 3 then + ops[#ops+1] = { + op = 'move', + data = { points[1][1], points[1][2] } + } + ops[#ops+1] = { + op = 'bcurveTo', + data = { points[1][1], points[1][2], points[2][1], points[2][2], points[3][1], points[3][2] } + } + elseif len == 2 then + local t = _line(points[1][1], points[1][2], points[2][1], points[2][2], o, true, true) + pl.tablex.insertvalues(ops, t) -- = JS ops.push(...t) + end + return ops +end + +local function _curveWithOffset (points, offset, o) + if #points == 0 then + return {} + end + local ps = {} + ps[1] = { + points[1][1] + _offsetOpt(offset, o), + points[1][2] + _offsetOpt(offset, o) + } + ps[2] = { + points[1][1] + _offsetOpt(offset, o), + points[1][2] + _offsetOpt(offset, o) + } + for i = 2, #points do + ps[#ps+1] = { + points[i][1] + _offsetOpt(offset, o), + points[i][2] + _offsetOpt(offset, o) + } + if i == #points then + ps[#ps+1] = { + points[i][1] + _offsetOpt(offset, o), + points[i][2] + _offsetOpt(offset, o) + } + end + end + return _curve(ps, nil, o) +end + +local function _computeEllipsePoints (increment, cx, cy, rx, ry, offset, overlap, o) + local coreOnly = o.roughness == 0 + local corePoints = {} + local allPoints = {} + if coreOnly then + increment = increment / 4 + allPoints[1] = { + cx + rx * math.cos(-increment), + cy + ry * math.sin(-increment) + } + for angle = 0, math.pi * 2, increment do + local p = { + cx + rx * math.cos(angle), + cy + ry * math.sin(angle) + } + corePoints[#corePoints+1] = p + allPoints[#allPoints+1] = p + end + allPoints[#allPoints+1] = { + cx + rx * math.cos(0), + cy + ry * math.sin(0) + } + allPoints[#allPoints+1] = { + cx + rx * math.cos(increment), + cy + ry * math.sin(increment) + } + else + local radOffset = _offsetOpt(0.5, o) - (math.pi / 2) + allPoints[1] = { + _offsetOpt(offset, o) + cx + 0.9 * rx * math.cos(radOffset - increment), + _offsetOpt(offset, o) + cy + 0.9 * ry * math.sin(radOffset - increment) + } + local endAngle = math.pi * 2 + radOffset - 0.01 + for angle = radOffset, endAngle, increment do + local p = { + _offsetOpt(offset, o) + cx + rx * math.cos(angle), + _offsetOpt(offset, o) + cy + ry * math.sin(angle) + } + corePoints[#corePoints+1] = p + allPoints[#allPoints+1] = p + end + allPoints[#allPoints+1] = { + _offsetOpt(offset, o) + cx + rx * math.cos(radOffset + math.pi * 2 + overlap * 0.5), + _offsetOpt(offset, o) + cy + ry * math.sin(radOffset + math.pi * 2 + overlap * 0.5) + } + allPoints[#allPoints+1] = { + _offsetOpt(offset, o) + cx + 0.98 * rx * math.cos(radOffset + overlap), + _offsetOpt(offset, o) + cy + 0.98 * ry * math.sin(radOffset + overlap) + } + allPoints[#allPoints+1] = { + _offsetOpt(offset, o) + cx + 0.9 * rx * math.cos(radOffset + overlap * 0.5), + _offsetOpt(offset, o) + cy + 0.9 * ry * math.sin(radOffset + overlap * 0.5) + } + end + return allPoints, corePoints +end + +local function _arc (increment, cx, cy, rx, ry, strt, stp, offset, o) + local radOffset = strt + _offsetOpt(0.1, o) + local points = {} + points[1] = { + _offsetOpt(offset, o) + cx + 0.9 * rx * math.cos(radOffset - increment), + _offsetOpt(offset, o) + cy + 0.9 * ry * math.sin(radOffset - increment) + } + for angle = radOffset, stp, increment do + points[#points+1] = { + _offsetOpt(offset, o) + cx + rx * math.cos(angle), + _offsetOpt(offset, o) + cy + ry * math.sin(angle) + } + end + points[#points+1] = { + cx + rx * math.cos(stp), + cy + ry * math.sin(stp) + } + points[#points+1] = { + cx + rx * math.cos(stp), + cy + ry * math.sin(stp) + } + return _curve(points, nil, o) +end + +local function _bezierTo (x1, y1, x2, y2, x, y, current, o) + local ops = {} + local ros = {o.maxRandomnessOffset or 1, (o.maxRandomnessOffset or 1) + 0.3} + local iterations = o.disableMultiStroke and 1 or 2 + local preserveVertices = o.preserveVertices + for i = 1, iterations do + if i == 1 then + ops[#ops+1] = { + op = 'move', + data = { current[1], current[2] } + } + else + ops[#ops+1] = { + op = 'move', + data = { + current[1] + (preserveVertices and 0 or _offsetOpt(ros[1], o)), + current[2] + (preserveVertices and 0 or _offsetOpt(ros[1], o)) + } + } + end + local f = preserveVertices and { x, y } or { x + _offsetOpt(ros[i], o), y + _offsetOpt(ros[i], o) } + ops[#ops+1] = { + op = 'bcurveTo', + data = { + x1 + _offsetOpt(ros[i], o), + y1 + _offsetOpt(ros[i], o), + x2 + _offsetOpt(ros[i], o), + y2 + _offsetOpt(ros[i], o), + f[1], + f[2] + } + } + end + return ops +end + +-- Public functions + +local function line (x1, y1, x2, y2, o) + return { type = 'path', ops = _doubleLine(x1, y1, x2, y2, o) } +end + +local function linearPath (points, close, o) + local len = #(points or {}) + if len >= 2 then + local ops = {} + for i = 1, len - 1 do + local t = _doubleLine(points[i][1], points[i][2], points[i + 1][1], points[i + 1][2], o) + pl.tablex.insertvalues(ops, t) -- = JS ops.push(...t) + end + if close then + local t = _doubleLine(points[len][1], points[len][2], points[1][1], points[1][2], o) + pl.tablex.insertvalues(ops, t) -- = JS ops.push(...t) + end + return { type = 'path', ops = ops } + elseif len == 2 then + return line(points[1][1], points[1][2], points[2][1], points[2][2], o) + end + return { type = 'path', ops = {} } +end + +local function polygon (points, o) + return linearPath(points, true, o) +end + +local function rectangle (x, y, width, height, o) + local points = { + {x, y}, + {x + width, y}, + {x + width, y + height}, + {x, y + height} + } + return polygon(points, o) +end + +local function curve (inputPoints, o) + if #inputPoints > 0 then + local p1 = inputPoints[1] + local pointsList = (type(p1[1]) == 'number') and {inputPoints} or inputPoints + local o1 = _curveWithOffset(pointsList[1], 1 * (1 + o.roughness * 0.2), o) + local o2 = o.disableMultiStroke and {} or _curveWithOffset(pointsList[1], 1.5 * (1 + o.roughness * 0.22), cloneOptionsAlterSeed(o)) + for i = 2, #pointsList do + local points = pointsList[i] + if #points > 0 then + local underlay = _curveWithOffset(points, 1 * (1 + o.roughness * 0.2), o) + local overlay = o.disableMultiStroke and {} or _curveWithOffset(points, 1.5 * (1 + o.roughness * 0.22), cloneOptionsAlterSeed(o)) + for _, item in ipairs(underlay) do + if item.op ~= 'move' then + o1[#o1+1] = item + end + end + for _, item in ipairs(overlay) do + if item.op ~= 'move' then + o2[#o2+1] = item + end + end + end + end + return { type = 'path', ops = array_concat(o1, o2) } + end + return { type = 'path', ops = {} } +end + +local function generateEllipseParams (width, height, o) + local psq = math.sqrt(math.pi * 2 * math.sqrt(((width / 2)^2 + (height / 2)^2) / 2)) + local stepCount = math.ceil(math.max(o.curveStepCount, (o.curveStepCount / math.sqrt(200)) * psq)) + local increment = (math.pi * 2) / stepCount + local rx = math.abs(width / 2) + local ry = math.abs(height / 2) + local curveFitRandomness = 1 - o.curveFitting + rx = rx + _offsetOpt(rx * curveFitRandomness, o) + ry = ry + _offsetOpt(ry * curveFitRandomness, o) + return { increment = increment, rx = rx, ry = ry } +end + +local function ellipseWithParams (x, y, o, ellipseParams) + local ap1, cp1 = _computeEllipsePoints(ellipseParams.increment, x, y, ellipseParams.rx, ellipseParams.ry, 1, ellipseParams.increment * _offset(0.1, _offset(0.4, 1, o), o), o) + local o1 = _curve(ap1, nil, o) + if not o.disableMultiStroke and o.roughness ~= 0 then + local ap2 = _computeEllipsePoints(ellipseParams.increment, x, y, ellipseParams.rx, ellipseParams.ry, 1.5, 0, o) + local o2 = _curve(ap2, nil, o) + pl.tablex.insertvalues(o1, o2) -- JS version used array_concat but seems avoidable here + end + return { estimatedPoints = cp1, opset = { type = 'path', ops = o1 } } +end + +local function ellipse (x, y, width, height, o) + local params = generateEllipseParams(width, height, o) + return ellipseWithParams(x, y, o, params).opset +end + +local function arc (x, y, width, height, start, stop, closed, roughClosure, o) + local cx = x + local cy = y + local rx = math.abs(width / 2) + local ry = math.abs(height / 2) + rx = rx + _offsetOpt(rx * 0.01, o) + ry = ry + _offsetOpt(ry * 0.01, o) + local strt = start + local stp = stop + while strt < 0 do + strt = strt + math.pi * 2 + stp = stp + math.pi * 2 + end + if (stp - strt) > (math.pi * 2) then + strt = 0 + stp = math.pi * 2 + end + local ellipseInc = (math.pi * 2) / o.curveStepCount + local arcInc = math.min(ellipseInc / 2, (stp - strt) / 2) + local ops = _arc(arcInc, cx, cy, rx, ry, strt, stp, 1, o) + if not o.disableMultiStroke then + local o2 = _arc(arcInc, cx, cy, rx, ry, strt, stp, 1.5, o) + pl.tablex.insertvalues(ops, o2) -- = JS ops.push(...o2) + end + if closed then + if roughClosure then + local t = _doubleLine(cx, cy, cx + rx * math.cos(strt), cy + ry * math.sin(strt), o) + pl.tablex.insertvalues(ops, t) -- = JS ops.push(...t) + t = _doubleLine(cx, cy, cx + rx * math.cos(stp), cy + ry * math.sin(stp), o) + pl.tablex.insertvalues(ops, t) -- = JS ops.push(...t) + else + ops[#ops+1] = { op = 'lineTo', data = {cx, cy} } + ops[#ops+1] = { op = 'lineTo', data = {cx + rx * math.cos(strt), cy + ry * math.sin(strt)} } + end + end + return { type = 'path', ops = ops } +end + +local function svgPath (path, o) + local segments = normalize(absolutize(parsePath(path))) -- FIXME + local ops = {} + local first = {0, 0} + local current = {0, 0} + for _, item in ipairs(segments) do + local key = item.key + local data = item.data + if key == 'M' then + current = {data[1], data[2]} + first = {data[1], data[2]} + elseif key == 'L' then + local t = _doubleLine(current[1], current[2], data[1], data[2], o) + pl.tablex.insertvalues(ops, t) -- = JS ops.push(...t) + current = { data[1], data[2] } + elseif key == 'C' then + local x1, y1, x2, y2, x, y = data[1], data[2], data[3], data[4], data[5], data[6] + local t = _bezierTo(x1, y1, x2, y2, x, y, current, o) + pl.tablex.insertvalues(ops, t) -- = JS ops.push(...t) + current = {x, y} + elseif key == 'Z' then + local t = _doubleLine(current[1], current[2], first[1], first[2], o) + pl.tablex.insertvalues(ops, t) -- = JS ops.push(...t) + current = {first[1], first[2]} + end + end + return { type = 'path', ops = ops } +end + +-- helpers + +local function doubleLineFillOps (x1, y1, x2, y2, o) + return _doubleLine(x1, y1, x2, y2, o, true) +end + +local helper = { + randOffset = _offsetOpt, + randOffsetWithRange =_offset, + ellipse = ellipse, + doubleLineOps = doubleLineFillOps, +} + +-- Fills + +local function solidFillPolygon (polygonList, o) + local ops = {} + for _, points in ipairs(polygonList) do + if #points > 0 then + local offset = o.maxRandomnessOffset or 0 + local len = #points + if len > 2 then + ops[#ops+1] = { + op = 'move', + data = { + points[1][1] + _offsetOpt(offset, o), + points[1][2] + _offsetOpt(offset, o) + } + } + for i = 2, len do + ops[#ops+1] = { + op = 'lineTo', + data = { + points[i][1] + _offsetOpt(offset, o), + points[i][2] + _offsetOpt(offset, o) + } + } + end + end + end + end + return { type = 'fillPath', ops = ops } +end + +local function patternFillPolygons (polygonList, o) + return getFiller(o, helper):fillPolygons(polygonList, o) +end + +local function patternFillArc (x, y, width, height, start, stop, o) + local cx = x + local cy = y + local rx = math.abs(width / 2) + local ry = math.abs(height / 2) + rx = rx + _offsetOpt(rx * 0.01, o) + ry = ry + _offsetOpt(ry * 0.01, o) + local strt = start + local stp = stop + while strt < 0 do + strt = strt + math.pi * 2 + stp = stp + math.pi * 2 + end + if (stp - strt) > (math.pi * 2) then + strt = 0 + stp = math.pi * 2 + end + local increment = (stp - strt) / o.curveStepCount + local points = {} + for angle = strt, stp, increment do + points[#points+1] = {cx + rx * math.cos(angle), cy + ry * math.sin(angle)} + end + points[#points+1] = {cx + rx * math.cos(stp), cy + ry * math.sin(stp)} + points[#points+1] = {cx, cy} + return patternFillPolygons({ points }, o) +end + +return { + line = line, + rectangle = rectangle, + ellipseWithParams = ellipseWithParams, + generateEllipseParams = generateEllipseParams, + arc = arc, + curve = curve, + linearPath = linearPath, + svgPath = svgPath, + patternFillArc = patternFillArc, + patternFillPolygons = patternFillPolygons, + solidFillPolygon = solidFillPolygon, +} From 7288d008731fdb487e4f0a09939b852d60b81153 Mon Sep 17 00:00:00 2001 From: Omikhleia Date: Sat, 13 Jan 2024 18:54:58 +0100 Subject: [PATCH 2/4] refactor: Move rough stuff outside framebox and independent from SILE --- packages/framebox/graphics/renderer.lua | 2 +- packages/framebox/graphics/rough.lua | 2 +- .../rough}/fillers/dashed-filler.lua | 4 ++-- .../rough}/fillers/dot-filler.lua | 4 ++-- .../rough-lua => rough-lua/rough}/fillers/filler.lua | 12 ++++++------ .../rough}/fillers/hachure-fill.lua | 2 +- .../rough}/fillers/hachure-filler.lua | 2 +- .../rough}/fillers/hatch-filler.lua | 2 +- .../rough}/fillers/scan-line-hachure.lua | 4 ++-- .../rough}/fillers/zigzag-filler.lua | 6 +++--- .../rough}/fillers/zigzag-line-filler.lua | 6 +++--- .../rough-lua => rough-lua/rough}/generator.lua | 8 ++++---- .../rough-lua => rough-lua/rough}/geometry.lua | 0 .../rough-lua => rough-lua/rough}/jsshims.lua | 0 .../rough-lua => rough-lua/rough}/renderer.lua | 10 +++++----- 15 files changed, 32 insertions(+), 32 deletions(-) rename {packages/framebox/rough-lua => rough-lua/rough}/fillers/dashed-filler.lua (90%) rename {packages/framebox/rough-lua => rough-lua/rough}/fillers/dot-filler.lua (88%) rename {packages/framebox/rough-lua => rough-lua/rough}/fillers/filler.lua (62%) rename {packages/framebox/rough-lua => rough-lua/rough}/fillers/hachure-fill.lua (98%) rename {packages/framebox/rough-lua => rough-lua/rough}/fillers/hachure-filler.lua (88%) rename {packages/framebox/rough-lua => rough-lua/rough}/fillers/hatch-filler.lua (86%) rename {packages/framebox/rough-lua => rough-lua/rough}/fillers/scan-line-hachure.lua (83%) rename {packages/framebox/rough-lua => rough-lua/rough}/fillers/zigzag-filler.lua (80%) rename {packages/framebox/rough-lua => rough-lua/rough}/fillers/zigzag-line-filler.lua (88%) rename {packages/framebox/rough-lua => rough-lua/rough}/generator.lua (98%) rename {packages/framebox/rough-lua => rough-lua/rough}/geometry.lua (100%) rename {packages/framebox/rough-lua => rough-lua/rough}/jsshims.lua (100%) rename {packages/framebox/rough-lua => rough-lua/rough}/renderer.lua (98%) diff --git a/packages/framebox/graphics/renderer.lua b/packages/framebox/graphics/renderer.lua index b0b7fe4..0731d7b 100644 --- a/packages/framebox/graphics/renderer.lua +++ b/packages/framebox/graphics/renderer.lua @@ -11,7 +11,7 @@ -- one. -- -local RoughGenerator = require("packages.framebox.rough-lua.generator").RoughGenerator +local RoughGenerator = require("rough-lua.rough.generator").RoughGenerator -- HELPERS diff --git a/packages/framebox/graphics/rough.lua b/packages/framebox/graphics/rough.lua index 7fd7908..3338621 100644 --- a/packages/framebox/graphics/rough.lua +++ b/packages/framebox/graphics/rough.lua @@ -1,4 +1,4 @@ -local RoughGenerator = require("packages.framebox.rough-lua.generator").RoughGenerator +local RoughGenerator = require("rough-lua.rough.generator").RoughGenerator SU.warn("The rough.lua module is deprecated. Please use the rough-lua module instead.") diff --git a/packages/framebox/rough-lua/fillers/dashed-filler.lua b/rough-lua/rough/fillers/dashed-filler.lua similarity index 90% rename from packages/framebox/rough-lua/fillers/dashed-filler.lua rename to rough-lua/rough/fillers/dashed-filler.lua index 2772d45..d5105fe 100644 --- a/packages/framebox/rough-lua/fillers/dashed-filler.lua +++ b/rough-lua/rough/fillers/dashed-filler.lua @@ -7,8 +7,8 @@ -- License MIT -- Copyright (c) 2019 Preet Shihn -- -local polygonHachureLines = require("packages.framebox.rough-lua.fillers.scan-line-hachure").polygonHachureLines -local lineLength = require("packages.framebox.rough-lua.geometry").lineLength +local polygonHachureLines = require("rough-lua.rough.fillers.scan-line-hachure").polygonHachureLines +local lineLength = require("rough-lua.rough.geometry").lineLength local DashedFiller = pl.class() diff --git a/packages/framebox/rough-lua/fillers/dot-filler.lua b/rough-lua/rough/fillers/dot-filler.lua similarity index 88% rename from packages/framebox/rough-lua/fillers/dot-filler.lua rename to rough-lua/rough/fillers/dot-filler.lua index 7b0f7a4..d69d75f 100644 --- a/packages/framebox/rough-lua/fillers/dot-filler.lua +++ b/rough-lua/rough/fillers/dot-filler.lua @@ -7,8 +7,8 @@ -- License MIT -- Copyright (c) 2019 Preet Shihn -- -local polygonHachureLines = require("packages.framebox.rough-lua.fillers.scan-line-hachure").polygonHachureLines -local lineLength = require("packages.framebox.rough-lua.geometry").lineLength +local polygonHachureLines = require("rough-lua.rough.fillers.scan-line-hachure").polygonHachureLines +local lineLength = require("rough-lua.rough.geometry").lineLength local DotFiller = pl.class() diff --git a/packages/framebox/rough-lua/fillers/filler.lua b/rough-lua/rough/fillers/filler.lua similarity index 62% rename from packages/framebox/rough-lua/fillers/filler.lua rename to rough-lua/rough/fillers/filler.lua index 205a268..ed967ba 100644 --- a/packages/framebox/rough-lua/fillers/filler.lua +++ b/rough-lua/rough/fillers/filler.lua @@ -7,12 +7,12 @@ -- License MIT -- Copyright (c) 2019 Preet Shihn -- -local HachureFiller = require("packages.framebox.rough-lua.fillers.hachure-filler").HachureFiller -local ZigZagFiller = require("packages.framebox.rough-lua.fillers.zigzag-filler").ZigZagFiller -local HatchFiller = require("packages.framebox.rough-lua.fillers.hatch-filler").HatchFiller -local DotFiller = require("packages.framebox.rough-lua.fillers.dot-filler").DotFiller -local DashedFiller = require("packages.framebox.rough-lua.fillers.dashed-filler").DashedFiller -local ZigZagLineFiller = require("packages.framebox.rough-lua.fillers.zigzag-line-filler").ZigZagLineFiller +local HachureFiller = require("rough-lua.rough.fillers.hachure-filler").HachureFiller +local ZigZagFiller = require("rough-lua.rough.fillers.zigzag-filler").ZigZagFiller +local HatchFiller = require("rough-lua.rough.fillers.hatch-filler").HatchFiller +local DotFiller = require("rough-lua.rough.fillers.dot-filler").DotFiller +local DashedFiller = require("rough-lua.rough.fillers.dashed-filler").DashedFiller +local ZigZagLineFiller = require("rough-lua.rough.fillers.zigzag-line-filler").ZigZagLineFiller local fillers = {} diff --git a/packages/framebox/rough-lua/fillers/hachure-fill.lua b/rough-lua/rough/fillers/hachure-fill.lua similarity index 98% rename from packages/framebox/rough-lua/fillers/hachure-fill.lua rename to rough-lua/rough/fillers/hachure-fill.lua index 6b5a92a..465a53e 100644 --- a/packages/framebox/rough-lua/fillers/hachure-fill.lua +++ b/rough-lua/rough/fillers/hachure-fill.lua @@ -12,7 +12,7 @@ -- Copyright (c) 2023 Preet Shihn -- -local jsshims = require("packages.framebox.rough-lua.jsshims") +local jsshims = require("rough-lua.rough.jsshims") local array_splice = jsshims.array_splice local math_round = jsshims.math_round diff --git a/packages/framebox/rough-lua/fillers/hachure-filler.lua b/rough-lua/rough/fillers/hachure-filler.lua similarity index 88% rename from packages/framebox/rough-lua/fillers/hachure-filler.lua rename to rough-lua/rough/fillers/hachure-filler.lua index d113aa7..32991af 100644 --- a/packages/framebox/rough-lua/fillers/hachure-filler.lua +++ b/rough-lua/rough/fillers/hachure-filler.lua @@ -7,7 +7,7 @@ -- License MIT -- Copyright (c) 2019 Preet Shihn -- -local polygonHachureLines = require("packages.framebox.rough-lua.fillers.scan-line-hachure").polygonHachureLines +local polygonHachureLines = require("rough-lua.rough.fillers.scan-line-hachure").polygonHachureLines local HachureFiller = pl.class() diff --git a/packages/framebox/rough-lua/fillers/hatch-filler.lua b/rough-lua/rough/fillers/hatch-filler.lua similarity index 86% rename from packages/framebox/rough-lua/fillers/hatch-filler.lua rename to rough-lua/rough/fillers/hatch-filler.lua index 299a7a1..6a7e0b2 100644 --- a/packages/framebox/rough-lua/fillers/hatch-filler.lua +++ b/rough-lua/rough/fillers/hatch-filler.lua @@ -7,7 +7,7 @@ -- License MIT -- Copyright (c) 2019 Preet Shihn -- -local HachureFiller = require("packages.framebox.rough-lua.fillers.hachure-filler").HachureFiller +local HachureFiller = require("rough-lua.rough.fillers.hachure-filler").HachureFiller local HatchFiller = pl.class(HachureFiller) diff --git a/packages/framebox/rough-lua/fillers/scan-line-hachure.lua b/rough-lua/rough/fillers/scan-line-hachure.lua similarity index 83% rename from packages/framebox/rough-lua/fillers/scan-line-hachure.lua rename to rough-lua/rough/fillers/scan-line-hachure.lua index f4e284a..62462c6 100644 --- a/packages/framebox/rough-lua/fillers/scan-line-hachure.lua +++ b/rough-lua/rough/fillers/scan-line-hachure.lua @@ -7,9 +7,9 @@ -- License MIT -- Copyright (c) 2019 Preet Shihn -- -local hachureLines = require("packages.framebox.rough-lua.fillers.hachure-fill").hachureLines +local hachureLines = require("rough-lua.rough.fillers.hachure-fill").hachureLines -local jsshims = require("packages.framebox.rough-lua.jsshims") +local jsshims = require("rough-lua.rough.jsshims") local math_round = jsshims.math_round local PRNG = require("packages.framebox.graphics.prng") diff --git a/packages/framebox/rough-lua/fillers/zigzag-filler.lua b/rough-lua/rough/fillers/zigzag-filler.lua similarity index 80% rename from packages/framebox/rough-lua/fillers/zigzag-filler.lua rename to rough-lua/rough/fillers/zigzag-filler.lua index d7552fb..62216c2 100644 --- a/packages/framebox/rough-lua/fillers/zigzag-filler.lua +++ b/rough-lua/rough/fillers/zigzag-filler.lua @@ -7,9 +7,9 @@ -- License MIT -- Copyright (c) 2019 Preet Shihn -- -local HachureFiller = require("packages.framebox.rough-lua.fillers.hachure-filler").HachureFiller -local polygonHachureLines = require("packages.framebox.rough-lua.fillers.scan-line-hachure").polygonHachureLines -local lineLength = require("packages.framebox.rough-lua.geometry").lineLength +local HachureFiller = require("rough-lua.rough.fillers.hachure-filler").HachureFiller +local polygonHachureLines = require("rough-lua.rough.fillers.scan-line-hachure").polygonHachureLines +local lineLength = require("rough-lua.rough.geometry").lineLength local ZigZagFiller = pl.class(HachureFiller) diff --git a/packages/framebox/rough-lua/fillers/zigzag-line-filler.lua b/rough-lua/rough/fillers/zigzag-line-filler.lua similarity index 88% rename from packages/framebox/rough-lua/fillers/zigzag-line-filler.lua rename to rough-lua/rough/fillers/zigzag-line-filler.lua index 071ff52..3146c30 100644 --- a/packages/framebox/rough-lua/fillers/zigzag-line-filler.lua +++ b/rough-lua/rough/fillers/zigzag-line-filler.lua @@ -7,10 +7,10 @@ -- License MIT -- Copyright (c) 2019 Preet Shihn -- -local polygonHachureLines = require("packages.framebox.rough-lua.fillers.scan-line-hachure").polygonHachureLines -local lineLength = require("packages.framebox.rough-lua.geometry").lineLength +local polygonHachureLines = require("rough-lua.rough.fillers.scan-line-hachure").polygonHachureLines +local lineLength = require("rough-lua.rough.geometry").lineLength -local jsshims = require("packages.framebox.rough-lua.jsshims") +local jsshims = require("rough-lua.rough.jsshims") local math_round = jsshims.math_round local ZigZagLineFiller = pl.class() diff --git a/packages/framebox/rough-lua/generator.lua b/rough-lua/rough/generator.lua similarity index 98% rename from packages/framebox/rough-lua/generator.lua rename to rough-lua/rough/generator.lua index 33ccc36..5dcb2d1 100644 --- a/packages/framebox/rough-lua/generator.lua +++ b/rough-lua/rough/generator.lua @@ -7,7 +7,7 @@ -- License MIT -- Copyright (c) 2019 Preet Shihn -- -local renderer = require("packages.framebox.rough-lua.renderer") +local renderer = require("rough-lua.rough.renderer") local line, rectangle, ellipseWithParams, generateEllipseParams, arc, curve, linearPath, @@ -24,13 +24,13 @@ local line, rectangle, -- local pointsOnPath = require("packages.framebox.points-on-curve").pointsOnPath -- local pointsOnBezierCurves = require("packages.framebox.points-on-curve").pointsOnBezierCurves local pointsOnPath = function (_, _, _) - SU.error("Not implemented") + error("Not implemented") end local curveToBezier = function (_) - SU.error("Not implemented") + error("Not implemented") end local pointsOnBezierCurves = function (_, _, _) - SU.error("Not implemented") + error("Not implemented") end diff --git a/packages/framebox/rough-lua/geometry.lua b/rough-lua/rough/geometry.lua similarity index 100% rename from packages/framebox/rough-lua/geometry.lua rename to rough-lua/rough/geometry.lua diff --git a/packages/framebox/rough-lua/jsshims.lua b/rough-lua/rough/jsshims.lua similarity index 100% rename from packages/framebox/rough-lua/jsshims.lua rename to rough-lua/rough/jsshims.lua diff --git a/packages/framebox/rough-lua/renderer.lua b/rough-lua/rough/renderer.lua similarity index 98% rename from packages/framebox/rough-lua/renderer.lua rename to rough-lua/rough/renderer.lua index 969d022..4bd63f5 100644 --- a/packages/framebox/rough-lua/renderer.lua +++ b/rough-lua/rough/renderer.lua @@ -8,7 +8,7 @@ -- Copyright (c) 2019 Preet Shihn -- -local jsshims = require("packages.framebox.rough-lua.jsshims") +local jsshims = require("rough-lua.rough.jsshims") local array_concat = jsshims.array_concat local PRNG = require("packages.framebox.graphics.prng") @@ -18,11 +18,11 @@ local prng = PRNG() -- I ported path-data-parser but haven't tested it for now -- local pathDataParser = require("packages.framebox.path-data-parser") -- local parsePath, normalize, absolutize = pathDataParser.parsePath, pathDataParser.normalize, pathDataParser.absolutize -local normalize = function () SU.error("Not yet implemented") end -local absolutize = function () SU.error("Not yet implemented") end -local parsePath = function () SU.error("Not yet implemented") end +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("packages.framebox.rough-lua.fillers.filler").getFiller +local getFiller = require("rough-lua.rough.fillers.filler").getFiller local function cloneOptionsAlterSeed (o) -- PORTING NOTE: From 15fcfd8e4b12d67a7cfa7f9841e68351d6d342bf Mon Sep 17 00:00:00 2001 From: Omikhleia Date: Sat, 13 Jan 2024 21:38:49 +0100 Subject: [PATCH 3/4] refactor: Move PRNG outside framebox and improve usage --- packages/framebox/graphics/renderer.lua | 9 +++++- packages/framebox/init.lua | 2 +- .../prng.lua => prng-prigarin/init.lua | 26 +++++++++-------- rough-lua/rough/fillers/scan-line-hachure.lua | 8 ++++-- rough-lua/rough/generator.lua | 18 ++++-------- rough-lua/rough/renderer.lua | 28 ++++++++++++------- 6 files changed, 52 insertions(+), 39 deletions(-) rename packages/framebox/graphics/prng.lua => prng-prigarin/init.lua (64%) 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/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..c5d235f 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) From ae1435300aad04c080a1ce50441c48b65acce304 Mon Sep 17 00:00:00 2001 From: Omikhleia Date: Sun, 14 Jan 2024 19:07:31 +0100 Subject: [PATCH 4/4] feat: Add untested parts of the roughjs port These are not needed (yet) in our framebox use, but let's have them in an "untested" folder. This will help to get started if we later decide to extract the port as a separate Lua rocks. --- ptable.sile-dev-1.rockspec | 21 +- rough-lua/rough/generator.lua | 11 +- rough-lua/rough/renderer.lua | 10 +- .../untested/path-data-parser/absolutize.lua | 95 ++++++++ rough-lua/untested/path-data-parser/init.lua | 20 ++ .../untested/path-data-parser/normalize.lua | 204 ++++++++++++++++++ .../untested/path-data-parser/parser.lua | 152 +++++++++++++ .../points-on-curve/curve-to-bezier.lua | 56 +++++ rough-lua/untested/points-on-curve/init.lua | 148 +++++++++++++ 9 files changed, 701 insertions(+), 16 deletions(-) create mode 100644 rough-lua/untested/path-data-parser/absolutize.lua create mode 100644 rough-lua/untested/path-data-parser/init.lua create mode 100644 rough-lua/untested/path-data-parser/normalize.lua create mode 100644 rough-lua/untested/path-data-parser/parser.lua create mode 100644 rough-lua/untested/points-on-curve/curve-to-bezier.lua create mode 100644 rough-lua/untested/points-on-curve/init.lua diff --git a/ptable.sile-dev-1.rockspec b/ptable.sile-dev-1.rockspec index cf45ae3..705f866 100644 --- a/ptable.sile-dev-1.rockspec +++ b/ptable.sile-dev-1.rockspec @@ -22,8 +22,27 @@ build = { ["sile.packages.parbox"] = "packages/parbox/init.lua", ["sile.packages.ptable"] = "packages/ptable/init.lua", ["sile.packages.framebox"] = "packages/framebox/init.lua", - ["sile.packages.framebox.graphics.prng"] = "packages/framebox/graphics/prng.lua", ["sile.packages.framebox.graphics.renderer"] = "packages/framebox/graphics/renderer.lua", ["sile.packages.framebox.graphics.rough"] = "packages/framebox/graphics/rough.lua", + ["prng-prigarin"] = "prng-prigarin/init.lua", + ["rough-lua.rough.jsshims"] = "rough-lua/rough/jsshims.lua", + ["rough-lua.rough.generator"] = "rough-lua/rough/generator.lua", + ["rough-lua.rough.renderer"] = "rough-lua/rough/renderer.lua", + ["rough-lua.rough.fillers.hachure-filler"] = "rough-lua/rough/fillers/hachure-filler.lua", + ["rough-lua.rough.fillers.zigzag-filler"] = "rough-lua/rough/fillers/zigzag-filler.lua", + ["rough-lua.rough.fillers.zigzag-line-filler"] = "rough-lua/rough/fillers/zigzag-line-filler.lua", + ["rough-lua.rough.fillers.dot-filler"] = "rough-lua/rough/fillers/dot-filler.lua", + ["rough-lua.rough.fillers.dashed-filler"] = "rough-lua/rough/fillers/dashed-filler.lua", + ["rough-lua.rough.fillers.scan-line-hachure"] = "rough-lua/rough/fillers/scan-line-hachure.lua", + ["rough-lua.rough.fillers.hatch-filler"] = "rough-lua/rough/fillers/hatch-filler.lua", + ["rough-lua.rough.fillers.hachure-fill"] = "rough-lua/rough/fillers/hachure-fill.lua", + ["rough-lua.rough.fillers.filler"] = "rough-lua/rough/fillers/filler.lua", + ["rough-lua.rough.geometry"] = "rough-lua/rough/geometry.lua", + ["rough-lua.untested.path-data-parser"] = "rough-lua/untested/path-data-parser/init.lua", + ["rough-lua.untested.path-data-parser.parser"] = "rough-lua/untested/path-data-parser/parser.lua", + ["rough-lua.untested.path-data-parser.normalize"] = "rough-lua/untested/path-data-parser/normalize.lua", + ["rough-lua.untested.path-data-parser.absolutize"] = "rough-lua/untested/path-data-parser/absolutize.lua", + ["rough-lua.untested.points-on-curve"] = "rough-lua/untested/points-on-curve/init.lua", + ["rough-lua.untested.points-on-curve.curve-to-bezier"] = "rough-lua/untested/points-on-curve/curve-to-bezier.lua", } } diff --git a/rough-lua/rough/generator.lua b/rough-lua/rough/generator.lua index d442a2f..4873d94 100644 --- a/rough-lua/rough/generator.lua +++ b/rough-lua/rough/generator.lua @@ -18,14 +18,9 @@ local line, rectangle, renderer.arc, renderer.curve, renderer.linearPath, renderer.svgPath, renderer.patternFillArc, renderer.patternFillPolygons, renderer.solidFillPolygon --- PORTING NOTE: --- I ported the module but haven't tested it for now --- 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 curveToBezier = require("rough-lua.untested.points-on-curve.curve-to-bezier").curveToBezier +local pointsOnPath = require("rough-lua.untested.points-on-curve").pointsOnPath +local pointsOnBezierCurves = require("rough-lua.untested.points-on-curve").pointsOnBezierCurves local RoughGenerator = pl.class({ diff --git a/rough-lua/rough/renderer.lua b/rough-lua/rough/renderer.lua index 176c0ab..e8760b6 100644 --- a/rough-lua/rough/renderer.lua +++ b/rough-lua/rough/renderer.lua @@ -12,13 +12,9 @@ local jsshims = require("rough-lua.rough.jsshims") local array_concat = jsshims.array_concat local PRNG = require("prng-prigarin") --- PORTING NOTE: --- I ported path-data-parser but haven't tested it for now --- 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 pathDataParser = require("rough-lua.untested.path-data-parser") +local parsePath, normalize, absolutize + = pathDataParser.parsePath, pathDataParser.normalize, pathDataParser.absolutize local getFiller = require("rough-lua.rough.fillers.filler").getFiller diff --git a/rough-lua/untested/path-data-parser/absolutize.lua b/rough-lua/untested/path-data-parser/absolutize.lua new file mode 100644 index 0000000..3f2d429 --- /dev/null +++ b/rough-lua/untested/path-data-parser/absolutize.lua @@ -0,0 +1,95 @@ +-- +-- License: MIT +-- Copyright (c) 2023, Didier Willis +-- +-- This is a straightforward port of the path-data-parser JavaScript library. +-- (https://github.com/pshihn/path-data-parser/) +-- License: MIT +-- Copyright (c) 2019 Preet Shihn +-- +local function absolutize(segments) + local cx, cy = 0, 0 + local subx, suby = 0, 0 + local out = {} + for _, segment in ipairs(segments) do + local key, data = segment.key, segment.data + if key == 'M' then + out[#out + 1] = { key = 'M', data = pl.tablex.copy(data) } + cx, cy = data[1], data[2] + subx, suby = data[1], data[2] + elseif key == 'm' then + cx = cx + data[1] + cy = cy + data[2] + out[#out + 1] = { key = 'M', data = { cx, cy } } + subx, suby = cx, cy + elseif key == 'L' then + out[#out + 1] = { key = 'L', data = pl.tablex.copy(data) } + cx, cy = data[1], data[2] + elseif key == 'l' then + cx = cx + data[1] + cy = cy + data[2] + out[#out + 1] = { key = 'L', data = { cx, cy } } + elseif key == 'C' then + out[#out + 1] = { key = 'C', data = pl.tablex.copy(data) } + cx, cy = data[5], data[6] + elseif key == 'c' then + local newdata = pl.tablex.map(data, function (d, i) + return (i % 2) == 0 and (d + cx) or (d + cy) + end) + out[#out + 1] = { key = 'C', data = newdata } + cx, cy = newdata[5], newdata[6] + elseif key == 'Q' then + out[#out + 1] = { key = 'Q', data = pl.tablex.copy(data) } + cx, cy = data[2], data[3] + elseif key == 'q' then + local newdata = pl.tablex.map(data, function (d, i) + return (i % 2) == 0 and (d + cx) or (d + cy) + end) + out[#out + 1] = { key = 'Q', data = newdata } + cx, cy = newdata[2], newdata[3] + elseif key == 'A' then + out[#out + 1] = { key = 'A', data = pl.tablex.copy(data) } + cx, cy = data[5], data[6] + elseif key == 'a' then + cx = cx + data[5] + cy = cy + data[6] + out[#out + 1] = { key = 'A', data = { data[1], data[2], data[3], data[4], data[5], cx, cy } } + elseif key == 'H' then + out[#out + 1] = { key = 'H', data = pl.tablex.copy(data) } + cx = data[1] + elseif key == 'h' then + cx = cx + data[1] + out[#out + 1] = { key = 'H', data = { cx } } + elseif key == 'V' then + out[#out + 1] = { key = 'V', data = pl.tablex.copy(data) } + cy = data[1] + elseif key == 'v' then + cy = cy + data[1] + out[#out + 1] = { key = 'V', data = { cy } } + elseif key == 'S' then + out[#out + 1] = { key = 'S', data = pl.tablex.copy(data) } + cx, cy = data[2], data[3] + elseif key == 's' then + local newdata = pl.tablex.map(data, function (d, i) + return (i % 2) == 0 and (d + cx) or (d + cy) + end) + out[#out + 1] = { key = 'S', data = newdata } + cx, cy = newdata[2], newdata[3] + elseif key == 'T' then + out[#out + 1] = { key = 'T', data = pl.tablex.copy(data) } + cx, cy = data[1], data[2] + elseif key == 't' then + cx = cx + data[1] + cy = cy + data[2] + out[#out + 1] = { key = 'T', data = { cx, cy } } + elseif key == 'Z' or key == 'z' then + out[#out + 1] = { key = 'Z', data = {} } + cx, cy = subx, suby + end + end + return out +end + +return { + absolutize = absolutize, +} diff --git a/rough-lua/untested/path-data-parser/init.lua b/rough-lua/untested/path-data-parser/init.lua new file mode 100644 index 0000000..165b80c --- /dev/null +++ b/rough-lua/untested/path-data-parser/init.lua @@ -0,0 +1,20 @@ +-- +-- License: MIT +-- Copyright (c) 2023, Didier Willis +-- +-- This is a straightforward port of the path-data-parser JavaScript library. +-- (https://github.com/pshihn/path-data-parser/) +-- License: MIT +-- Copyright (c) 2019 Preet Shihn +-- +local normalize = require("rough-lua.untested.path-data-parser.normalize").normalize +local absolutize = require("rough-lua.untested.path-data-parser.absolutize").absolutize +local parsePath = require("rough-lua.untested.path-data-parser.parser").parsePath +local serialize = require("rough-lua.untested.path-data-parser.parser").serialize + +return { + parsePath = parsePath, + serialize = serialize, + absolutize = absolutize, + normalize = normalize, +} diff --git a/rough-lua/untested/path-data-parser/normalize.lua b/rough-lua/untested/path-data-parser/normalize.lua new file mode 100644 index 0000000..afa6b12 --- /dev/null +++ b/rough-lua/untested/path-data-parser/normalize.lua @@ -0,0 +1,204 @@ +-- License: MIT +-- Copyright (c) 2023, Didier Willis +-- +-- This is a straightforward port of the path-data-parser JavaScript library. +-- (https://github.com/pshihn/path-data-parser/) +-- License: MIT +-- Copyright (c) 2019 Preet Shihn +-- +local jsshims = require("rough-lua.rough.jsshims") +local array_concat = jsshims.array_concat + +local function degToRad (degrees) + return (math.pi * degrees) / 180 +end + +local function rotate (x, y, angleRad) + local X = x * math.cos(angleRad) - y * math.sin(angleRad) + local Y = x * math.sin(angleRad) + y * math.cos(angleRad) + return { X, Y } +end + +local function arcToCubicCurves (x1, y1, x2, y2, r1, r2, angle, largeArcFlag, sweepFlag, recursive) + local angleRad = degToRad(angle) + local params = {} + local f1, f2, cx, cy + if recursive then + f1, f2, cx, cy = recursive[1], recursive[2], recursive[3], recursive[4] + else + x1, y1 = rotate(x1, y1, -angleRad) + x2, y2 = rotate(x2, y2, -angleRad) + local x = (x1 - x2) / 2 + local y = (y1 - y2) / 2 + local h = (x * x) / (r1 * r1) + (y * y) / (r2 * r2) + if h > 1 then + h = math.sqrt(h) + r1 = h * r1 + r2 = h * r2 + end + local sign = (largeArcFlag == sweepFlag) and -1 or 1 + local r1Pow = r1 * r1 + local r2Pow = r2 * r2 + local left = r1Pow * r2Pow - r1Pow * y * y - r2Pow * x * x + local right = r1Pow * y * y + r2Pow * x * x + local k = sign * math.sqrt(math.abs(left / right)) + cx = k * r1 * y / r2 + (x1 + x2) / 2 + cy = k * -r2 * x / r1 + (y1 + y2) / 2 + f1 = math.asin(((y1 - cy) / r2)) + f2 = math.asin(((y2 - cy) / r2)) + if x1 < cx then + f1 = math.pi - f1 + end + if x2 < cx then + f2 = math.pi - f2 + end + if f1 < 0 then + f1 = math.pi * 2 + f1 + end + if f2 < 0 then + f2 = math.pi * 2 + f2 + end + if sweepFlag and f1 > f2 then + f1 = f1 - math.pi * 2 + end + if not sweepFlag and f2 > f1 then + f2 = f2 - math.pi * 2 + end + end + local df = f2 - f1 + if math.abs(df) > (math.pi * 120 / 180) then + local f2old = f2 + local x2old = x2 + local y2old = y2 + if sweepFlag and f2 > f1 then + f2 = f1 + (math.pi * 120 / 180) * (1) + else + f2 = f1 + (math.pi * 120 / 180) * (-1) + end + x2 = cx + r1 * math.cos(f2) + y2 = cy + r2 * math.sin(f2) + params = arcToCubicCurves(x2, y2, x2old, y2old, r1, r2, angle, 0, sweepFlag, { f2, f2old, cx, cy }) + end + df = f2 - f1 + local c1 = math.cos(f1) + local s1 = math.sin(f1) + local c2 = math.cos(f2) + local s2 = math.sin(f2) + local t = math.tan(df / 4) + local hx = 4 / 3 * r1 * t + local hy = 4 / 3 * r2 * t + local m1 = { x1, y1 } + local m2 = { x1 + hx * s1, y1 - hy * c1 } + local m3 = { x2 + hx * s2, y2 - hy * c2 } + local m4 = { x2, y2 } + m2[1] = 2 * m1[1] - m2[1] + m2[2] = 2 * m1[2] - m2[2] + if recursive then + return array_concat({ m2, m3, m4 }, params) + else + params = array_concat({ m2, m3, m4 }, params) + local curves = {} + for i = 1, #params, 3 do + local ro1 = rotate(params[i][1], params[i][2], angleRad) + local ro2 = rotate(params[i + 1][1], params[i + 1][2], angleRad) + local ro3 = rotate(params[i + 2][1], params[i + 2][2], angleRad) + curves[#curves + 1] = { ro1[1], ro1[2], ro2[1], ro2[2], ro3[1], ro3[2] } + end + return curves + end +end + +local function normalize(segments) + local out = {} + local lastType = '' + local cx, cy = 0, 0 + local subx, suby = 0, 0 + local lcx, lcy = 0, 0 + for _, segment in ipairs(segments) do + local key, data = segment.key, segment.data + if key == 'M' then + out[#out + 1] = { key = 'M', data = pl.tablex.copy(data) } + cx, cy = data[1], data[2] + subx, suby = data[1], data[2] + elseif key == 'C' then + out[#out + 1] = { key = 'C', data = pl.tablex.copy(data) } + cx, cy = data[5], data[6] + lcx, lcy = data[3], data[4] + elseif key == 'L' then + out[#out + 1] = { key = 'L', data = pl.tablex.copy(data) } + cx, cy = data[1], data[2] + elseif key == 'H' then + cx = data[1] + out[#out + 1] = { key = 'L', data = { cx, cy } } + elseif key == 'V' then + cy = data[1] + out[#out + 1] = { key = 'L', data = { cx, cy } } + elseif key == 'S' then + local cx1, cy1 + if lastType == 'C' or lastType == 'S' then + cx1 = cx + (cx - lcx) + cy1 = cy + (cy - lcy) + else + cx1 = cx + cy1 = cy + end + out[#out + 1] = { key = 'C', data = { cx1, cy1, pl.tablex.copy(data) } } + lcx, lcy = data[1], data[2] + cx, cy = data[3], data[4] + elseif key == 'T' then + local x, y = data[1], data[2] + local x1, y1 + if lastType == 'Q' or lastType == 'T' then + x1 = cx + (cx - lcx) + y1 = cy + (cy - lcy) + else + x1 = cx + y1 = cy + end + local cx1 = cx + 2 * (x1 - cx) / 3 + local cy1 = cy + 2 * (y1 - cy) / 3 + local cx2 = x + 2 * (x1 - x) / 3 + local cy2 = y + 2 * (y1 - y) / 3 + out[#out + 1] = { key = 'C', data = { cx1, cy1, cx2, cy2, x, y } } + lcx, lcy = x1, y1 + cx, cy = x, y + elseif key == 'Q' then + local x1, y1, x, y = data[1], data[2], data[3], data[4] + local cx1 = cx + 2 * (x1 - cx) / 3 + local cy1 = cy + 2 * (y1 - cy) / 3 + local cx2 = x + 2 * (x1 - x) / 3 + local cy2 = y + 2 * (y1 - y) / 3 + out[#out + 1] = { key = 'C', data = { cx1, cy1, cx2, cy2, x, y } } + lcx, lcy = x1, y1 + cx, cy = x, y + elseif key == 'A' then + local r1, r2 = math.abs(data[1]), math.abs(data[2]) + local angle = data[3] + local largeArcFlag = data[4] + local sweepFlag = data[5] + local x, y = data[6], data[7] + if r1 == 0 or r2 == 0 then + out[#out + 1] = { key = 'C', data = { cx, cy, x, y, x, y } } + cx, cy = x, y + else + if cx ~= x or cy ~= y then + local curves = arcToCubicCurves(cx, cy, x, y, r1, r2, angle, largeArcFlag, sweepFlag) + for _, curve in ipairs(curves) do + out[#out + 1] = { key = 'C', data = curve } + end + cx, cy = x, y + end + end + elseif key == 'Z' then + out[#out + 1] = { key = 'Z', data = {} } + cx, cy = subx, suby + end + lastType = key + end + return out +end + +return { + normalize = normalize, + arcToCubicCurves = arcToCubicCurves, +} diff --git a/rough-lua/untested/path-data-parser/parser.lua b/rough-lua/untested/path-data-parser/parser.lua new file mode 100644 index 0000000..374552f --- /dev/null +++ b/rough-lua/untested/path-data-parser/parser.lua @@ -0,0 +1,152 @@ +-- +-- License: MIT +-- Copyright (c) 2023, Didier Willis +-- +-- This is a straightforward port of the path-data-parser JavaScript library. +-- (https://github.com/pshihn/path-data-parser/) +-- License: MIT +-- Copyright (c) 2019 Preet Shihn +-- +local COMMAND = 0 +local NUMBER = 1 +local EOD = 2 + +local PARAMS = { + A = 7, + a = 7, + C = 6, + c = 6, + H = 1, + h = 1, + L = 2, + l = 2, + M = 2, + m = 2, + Q = 4, + q = 4, + S = 4, + s = 4, + T = 2, + t = 2, + V = 1, + v = 1, + Z = 0, + z = 0, +} + +local function tokenize (d) + local tokens = {} + while d ~= '' do + local i, j = d:find("^[ \t\r\n,]+") + if i then + d = d:sub(j + 1) + else + i, j = d:find("^[aAcChHlLmMqQsStTvVzZ]") + if i then + tokens[#tokens + 1] = { type = COMMAND, text = d:sub(i, j) } + d = d:sub(j + 1) + else + i, j = d:find("^[+-]?%.?[0-9]+%.?[eE0-9]*") + -- PORTING NOTE: + -- JS has "^(([-+]?[0-9]+(\.[0-9]*)?|[-+]?^.[0-9]+)([eE][-+]?[0-9]+)?)") + -- Lua does not support such complex regexps, so we use a simpler one + -- but it will not catch some malformed numbers. + if i then + tokens[#tokens + 1] = { type = NUMBER, text = tostring(tonumber(d:sub(i, j))) } + d = d:sub(j + 1) + else + return {} + end + end + end + end + tokens[#tokens + 1] = { type = EOD, text = '' } + return tokens +end + +local function isType (token, type) + return token.type == type +end + +local function parsePath (d) + local segments = {} + local tokens = tokenize(d) + local mode = 'BOD' + local index = 1 + local token = tokens[index] + while not isType(token, EOD) do + local paramsCount + local params = {} + if mode == 'BOD' then + if token.text == 'M' or token.text == 'm' then + index = index + 1 + paramsCount = PARAMS[token.text] + mode = token.text + else + return parsePath('M0,0' .. d) + end + elseif isType(token, NUMBER) then + paramsCount = PARAMS[mode] + else + index = index + 1 + paramsCount = PARAMS[token.text] + mode = token.text + end + if (index + paramsCount) <= #tokens then + for i = index, index + paramsCount - 1 do + local numbeToken = tokens[i] + if isType(numbeToken, NUMBER) then + params[#params + 1] = tonumber(numbeToken.text) + else + error('Param not a number: ' .. mode .. ',' .. numbeToken.text) + end + end + if type(PARAMS[mode]) == 'number' then + local segment = { key = mode, data = params } + segments[#segments + 1] = segment + index = index + paramsCount + token = tokens[index] + if mode == 'M' then + mode = 'L' + end + if mode == 'm' then + mode = 'l' + end + else + error('Bad segment: ' .. mode) + end + else + error('Path data ended short') + end + end + return segments +end + +local function serialize (segments) + local tokens = {} + for _, segment in ipairs(segments) do + tokens[#tokens + 1] = segment.key + if segment.key == 'C' or segment.key == 'c' then + tokens[#tokens + 1] = segment.data[1] + tokens[#tokens + 1] = tostring(segment.data[2]) .. ',' + tokens[#tokens + 1] = segment.data[3] + tokens[#tokens + 1] = tostring(segment.data[4]) .. ',' + tokens[#tokens + 1] = segment.data[5] + tokens[#tokens + 1] = segment.data[6] + elseif segment.key == 'S' or segment.key == 's' + or segment.key == 'Q' or segment.key == 'q' then + tokens[#tokens + 1] = segment.data[1] + tokens[#tokens + 1] = tostring(segment.data[2]) .. ',' + tokens[#tokens + 1] = segment.data[3] + tokens[#tokens + 1] = segment.data[4] + else + pl.tablex.insertvalues(tokens, segment.data) + end + end + return table.concat(tokens, ' ') +end + +return { + parsePath = parsePath, + serialize = serialize, +} \ No newline at end of file diff --git a/rough-lua/untested/points-on-curve/curve-to-bezier.lua b/rough-lua/untested/points-on-curve/curve-to-bezier.lua new file mode 100644 index 0000000..2ab8f22 --- /dev/null +++ b/rough-lua/untested/points-on-curve/curve-to-bezier.lua @@ -0,0 +1,56 @@ +-- +-- 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 +-- +local function clone (p) + return { p[1], p[2] } +end + +local function curveToBezier (pointsIn, curveTightness) + local len = #pointsIn + if len < 3 then + error('A curve must have at least three points.') + end + local out = {} + if len == 3 then + out[#out + 1] = clone(pointsIn[1]) + out[#out + 1] = clone(pointsIn[2]) + out[#out + 1] = clone(pointsIn[3]) + out[#out + 1] = clone(pointsIn[3]) + else + local points = {} + points[#points + 1] = pointsIn[1] + points[#points + 1] = pointsIn[1] + for i = 2, #pointsIn do + points[#points + 1] = pointsIn[i] + if i == (#pointsIn - 1) then + points[#points + 1] = pointsIn[i] + end + end + local b = {} + local s = 1 - curveTightness + out[#out + 1] = clone(points[1]) + for i = 2, (#points - 2) do + local cachedVertArray = points[i] + b[1] = { cachedVertArray[1], cachedVertArray[2] } + b[2] = { cachedVertArray[1] + (s * points[i + 1][1] - s * points[i - 1][1]) / 6, cachedVertArray[2] + (s * points[i + 1][2] - s * points[i - 1][2]) / 6 } + b[3] = { points[i + 1][1] + (s * points[i][1] - s * points[i + 2][1]) / 6, points[i + 1][2] + (s * points[i][2] - s * points[i + 2][2]) / 6 } + b[4] = { points[i + 1][1], points[i + 1][2] } + out[#out + 1] = b[2] + out[#out + 1] = b[3] + out[#out + 1] = b[4] + end + end + return out +end + +-- Exports + +return { + curveToBezier = curveToBezier, +} diff --git a/rough-lua/untested/points-on-curve/init.lua b/rough-lua/untested/points-on-curve/init.lua new file mode 100644 index 0000000..9085609 --- /dev/null +++ b/rough-lua/untested/points-on-curve/init.lua @@ -0,0 +1,148 @@ +-- +-- 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 +-- + +local function lerp (a, b, t) + return {a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t} +end + +-- Distance between 2 points squared +local function distanceSq (p1, p2) + return (p1[1] - p2[1]) ^ 2 + (p1[2] - p2[2]) ^ 2 +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 + +-- 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 = math.sqrt(distanceSq(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 + +-- 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 simplify (points, distance) + return simplifyPoints(points, 1, #points, distance) +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, +}