Skip to content

Instantly share code, notes, and snippets.

@hsjunnesson
Created January 23, 2024 21:25
Show Gist options
  • Save hsjunnesson/8bf66bd2bf63849130f6af44d7ab8efb to your computer and use it in GitHub Desktop.
Save hsjunnesson/8bf66bd2bf63849130f6af44d7ab8efb to your computer and use it in GitHub Desktop.
A Ruby script that outputs some Perlin perturbed SVGs
require 'victor'
require 'perlin_noise'
$width = 210
$height = 297
# $width = 279
# $height = 356
$pen_width = 0.8
$margin_left = 10
$margin_right = 10
$margin_top = 10
$margin_bottom = 30.0
$border = 2
$margin_rect_origin = Vector[$margin_left + $border * 0.5, $margin_top + $border * 0.5]
$margin_rect_size = Vector[$width - $margin_left - $margin_right - $border * 2 * 0.5, $height - $margin_top - $margin_bottom - $border * 2 * 0.5]
svg = Victor::SVG.new width: "100%", height: "100%", viewBox: "0 0 #{$width} #{$height}", style: {
stroke_width: $pen_width,
stroke: "#000",
fill: "none",
background: "white"
}
$seed = 446848
$noise = Perlin::Noise.new 2, :seed => $seed
$contrast = Perlin::Curve.contrast(Perlin::Curve::CUBIC, 3)
rand = Random.new($seed)
rand.rand()
rand.rand()
$TWO_PI = 2.0 * Math::PI
$offset_dir_scale = 0.025
$primitive_odds = 0.026
$perturb_offset = 10.0
def point_offset(p)
theta = 10.0 * Math::PI / 180 # Convert 10 degrees to radians
x_rotated = p[0] * Math.cos(theta) - p[1] * Math.sin(theta)
y_rotated = p[0] * Math.sin(theta) + p[1] * Math.cos(theta)
rotated_p = Vector[x_rotated, y_rotated]
a = $noise[p[0] * $offset_dir_scale, p[1] * $offset_dir_scale] * $TWO_PI
v = Vector[Math.sin(a), Math.cos(a)] * $perturb_offset
return rotated_p + v
end
$drawn_lines = Set.new
def inside_rect(point, rect_origin, rect_size)
point[0] >= rect_origin[0] && point[0] < rect_origin[0] + rect_size[0] &&
point[1] >= rect_origin[1] && point[1] < rect_origin[1] + rect_size[1]
end
# Draw perturbed line
def line(svg, p1, p2)
return if $drawn_lines.include? [p1, p2] or $drawn_lines.include? [p2, p1]
$drawn_lines << [p1, p2]
segments = 10
dir = (p2-p1) / segments.to_f
points = (0..segments)
.map { |i| p1 + dir * i.to_f }
.map { |p| p + point_offset(p) }
.filter { |p| inside_rect(p, $margin_rect_origin, $margin_rect_size) }
data = points.map.with_index { |p, i|
command = i.zero? ? 'M' : 'L'
"#{command} #{p[0]} #{p[1]}"
}.join(' ')
svg.path d: data, stroke: "black"
end
# Draw perturbed circle
def circ(svg, c, r)
segments = 36
for i in 1..segments.to_f do
a = $TWO_PI * i / segments
b = $TWO_PI * (i + 1) / segments
p1 = Vector[c[0] + r * Math.cos(a), c[1] + r * Math.sin(a)]
p2 = Vector[c[0] + r * Math.cos(b), c[1] + r * Math.sin(b)]
p1 = p1 + point_offset(p1)
p2 = p2 + point_offset(p2)
if inside_rect(p1, $margin_rect_origin, $margin_rect_size) and inside_rect(p2, $margin_rect_origin, $margin_rect_size)
svg.polyline points: [p1[0],p1[1], p2[0],p2[1]]
end
end
end
# Draw perturbed circle filled
def fill_circ(svg, c, r)
segments = 36
x_min = c[0] - r
x_max = c[0] + r
(0..segments).each do |i|
x = x_min + (x_max - x_min) * i / segments.to_f
y_delta = Math.sqrt(r**2 - (x - c[0])**2)
y_top = c[1] + y_delta
y_bottom = c[1] - y_delta
p1 = Vector[x, y_top]
p2 = Vector[x, y_bottom]
line(svg, p1, p2)
end
circ(svg, c, r)
end
# Draw perturbed triangle
def triangle(svg, p1, p2, p3)
segments = 10
dir = (p2-p1) / segments
points = (0..segments)
.map { |i| p1 + dir * i }
.map { |p| p + point_offset(p) }
dir = (p3-p2) / segments
points2 = (0..segments)
.map { |i| p2 + dir * i }
.map { |p| p + point_offset(p) }
points = points.concat(points2)
data = points.map.with_index { |p, i|
command = i.zero? ? 'M' : 'L'
"#{command} #{p[0]} #{p[1]}"
}
# data << "Z"
data = data.join(' ')
svg.path d: data, stroke: "black"
end
# Draw perturbed triangle filled
def fill_triangle(svg, p1, p2, p3)
segments = 36
dir = (p3-p2) / segments.to_f
(0..segments).each do |i|
line(svg, p1, p2 + dir * i)
end
end
puts "Processing"
grid_size = 4
(-20..$height + 20).step(grid_size) do |y|
(-30..$width + 20).step(grid_size) do |x|
p = Vector[x, y]
p1 = p + Vector[grid_size, 0]
p2 = p + Vector[grid_size, grid_size]
p3 = p + Vector[0, grid_size]
rng = rand.rand(100.0)
if rng >= 99.5
c = p + (p2 - p) / 2.0
fill_circ(svg, c, (grid_size / 2.0))
elsif rng >= 98.5
c = p + (p2 - p) / 2.0
circ(svg, c, (grid_size / 2.0))
elsif rng >= 97.5
fill_triangle(svg, p, p1, p2)
elsif rng >= 96.5
fill_triangle(svg, p1, p2, p3)
else
primitive_odds = $noise[p[0] * $primitive_odds, p[1] * $primitive_odds]
primitive_odds = $contrast.call(primitive_odds)
if primitive_odds >= 0.1
line(svg, p, p1)
line(svg, p1, p2)
line(svg, p2, p3)
line(svg, p3, p)
line(svg, p1, p3)
else
line(svg, p, p1)
line(svg, p1, p2)
line(svg, p2, p3)
line(svg, p3, p)
end
end
end
end
# Border
(0..$border.to_f).step($pen_width * 0.25) { |inset|
svg.rect x: $margin_left + inset, y: $margin_top + inset, width: $width-$margin_left-$margin_right-inset*2, height: $height-$margin_top-$margin_bottom-inset*2, stroke: "black", fill: "none"
}
puts "Saving vellum.svg"
svg.save 'vellum'
# crop #{$margin_left}mm #{$margin_top}mm #{$width-$margin_left-$margin_right}mm #{$height-$margin_top-$margin_bottom}mm \
vpype = "vpype read vellum.svg \
layout -m 0 -b #{$width}mmx#{$height}mm \
penwidth #{$pen_width}mm \
splitall linemerge \
linesimplify -t 0.01 \
linesort --two-opt \
write vellum_final.svg"
puts "Running #{vpype}"
system(vpype)
puts "Done"
@hsjunnesson
Copy link
Author

Looks something like this

vellum_final

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment