Dendro 0000
Policy: 2e9b24...420c
Owner:
Metadata:
{ "name": "Dendro 0000", "files": [ { "src": [ "# DendroRithms (c) 2022 Wout Fierens - https://dendrorithms.com/", "# License: AGPL v3 https://www.gnu.org/licenses/agpl-3.0.en.html", "# Source code: https://github.com/wout/dendro/", "# To run, install Crystal: https://crystal-lang.org/install/", "# Or paste into the playground: https://play.crystal-lang.org/", "# This code is written for Crystal 1.3 and has no dependendies.", "", "require %(json)", "require %(xml)", "", "struct Dendro", " CORE_ANGLES = (8..18) # min/max number growth angles", " CORE_DIST = 7.0 # min distance between cores", " CORE_LIFETIME = (15..25) # min/max lifetime of a core", " CORE_R_INIT = 2.0 # initial core radius", " CORE_R_MAX = (5..8) # variable max core radius", " CORE_RATE = {0.8, 1.5} # min/max growth rate", " CORE_TRIES = 500 # max tries to find new position", " FILL = %(#111111) # background colour", " LAYER_DIST = 0.4 # distance between layers", " LAYER_FLOW = 400 # the max number of layers", " LAYER_FALLOFF = 60 # number of layers for spacing", " LAYER_MAX = 0.5 # max usable layer distance", " LAYER_REDUCE = 20 # flow reduction step per core", " LAYER_SMOOTH = 0.15 # smoothing of bezier curve", " LAYER_SPACING = {1.1, 0.8} # start/end layer spacing", " POINT_DIST = 1.25 # distance between layer points", " PREDICT_DIST = 2.0 # predicted growth deactivation", " SECTOR_NUM = 360 * 10 # growth deactivation sectors", " SIZE_DOC = 4096 # document size for export", " SIZE_VB = 100 # viewbox size", " STROKE_OPACITY = {0.85, 0.95} # min/max stroke opacity", " STROKE_WIDTH = {0.12, 0.18} # min/max stroke width", "", " SECTOR_RES = 360 / SECTOR_NUM", "", " COLORS = {", " %(#BAF4DE), # 0: verdigris", " %(#FFFFFA), # 1: porcelain", " %(#CAEFF7), # 2: frost", " %(#DFEDAE), # 3: phosphor", " %(#DEDAF1), # 4: lilac", " %(#E5B7A5), # 5: copper", " %(#ff6d6a), # 6: coral", " %(#42515D), # 7: slate", " %(#f9c33b), # 8: saffron", " %(#6667AB), # 9: veryperi", " %(#ff8200), # 10: goldfish", " %(#fc8eac), # 11: flamingo", " %(#05c3dd), # 12: azure", " %(#C71A2F), # 13: amaranth", " %(#800080), # 14: royalpurple", " %(#9DEE00), # 15: neongreen", " %(#504416), # 16: olive", " %(#fb48c4), # 17: neonpink", " %(#EBCE88), # 18: napelsyellow", " }", "", " alias Point = Tuple(Float64, Float64)", " alias KeyPoint = Tuple(Int32, Float64)", "", " private getter color : Int32", " private getter previous_cores : Array(Core)", " private getter current_cores : Array(Core)", " private getter prng : Random::PCG32", " private getter seed : Int32", "", " def initialize(@seed, @color, previous_cores, add_core : Bool)", " @prng = Random.new(@seed)", " @previous_cores = Array(Core).from_json(previous_cores)", " @current_cores = evolve_cores(@previous_cores)", " add_new_core if @current_cores.empty?", " add_new_core if add_core", " end", "", " def to_svg : String", " to_svg(LAYER_FLOW - (number_of_cores - 1) * LAYER_REDUCE)", " end", "", " def to_svg(layer_flow : Int32) : String", " Svg.build(color, current_cores, prng, layer_flow)", " end", "", " def previous_cores_as_json : String", " previous_cores.to_json", " end", "", " def current_cores_as_json : String", " current_cores.to_json", " end", "", " def number_of_cores : Int32", " current_cores.size", " end", "", " private def evolve_cores(cs) : Array(Core)", " cs.compact_map do |c|", " c.evolve", " c unless c.dead?", " end", " end", "", " private def add_new_core : Void", " max_r = prng.rand(CORE_R_MAX).to_f", " if cp = Core.position(max_r, current_cores, prng)", " l = prng.rand(CORE_LIFETIME)", " core = Core.new(cp: cp, max_r: max_r, life: l, age: 1)", " current_cores << core", " end", " end", "", " struct Core", " include JSON::Serializable", "", " getter age : Int32", " @[JSON::Field(converter: Dendro::PointFromArray)]", " getter cp : Point", " getter life : Int32", " @[JSON::Field(converter: Dendro::FloatFromInt)]", " getter max_r : Float64", "", " @[JSON::Field(ignore: true)]", " getter growth = Array(Float64).new", " @[JSON::Field(ignore: true)]", " getter layers = Array(Array(Tuple(Float64, Float64))).new", " @[JSON::Field(ignore: true)]", " getter sectors = Array(Float64?).new(SECTOR_NUM, nil)", "", " def initialize(@cp, @max_r, @life, @age = 1)", " end", "", " def evolve", " @age += 1", " end", "", " def r", " pos = -Math.cos(age / life * Math::PI * 2) / 2 + 0.5", " CORE_R_INIT + pos * (max_r - CORE_R_INIT)", " end", "", " def dead?", " age > life", " end", "", " def self.position(r, cs, prng, i = 0) : Point?", " return if i >= CORE_TRIES", " p = Geo.random_point_on_circle(prng)", " overlapping?(cs, p, r) ? position(r, cs, prng, i + 1) : p", " end", "", " def self.overlapping?(cs, p, r) : Bool", " cs.any? do |c|", " Geo.point_distance(p, c.cp) <= r + c.max_r + CORE_DIST", " end", " end", "", " def self.growth_rates(prng) : Array(Float64)", " ks = key_points(prng.rand(CORE_ANGLES), 360, prng)", " Blend.key_points(ks, 360)", " end", "", " def self.key_points(n, t, prng) : Array(KeyPoint)", " s = t / n", " min, max = CORE_RATE", " (0..(n - 1)).map do |i|", " {(i * s + prng.rand(s)).to_i,", " (min + prng.rand(max - min)).round(2)}", " end", " end", " end", "", " module Layer", " extend self", "", " def build(cs, prng, layer_flow) : Void", " cs.each { |c| c.growth.concat(Core.growth_rates(prng)) }", " layer_flow.times do |l|", " cs.each do |core|", " r = core.r + scaled_dist(l)", " n = (Geo.r_to_length(r) / POINT_DIST).round", " core.layers << (0..(n - 1)).map do |i|", " build_point(core, r, n, i, prng)", " end", " end", " Deactivation.update(cs)", " end", " end", "", " def scaled_dist(i) : Float64", " s, e = LAYER_SPACING", " step = Blend.step(s, e, Blend.pos(i, LAYER_FALLOFF))", " LAYER_DIST * i * step", " end", "", " def build_point(c, r, n, i, prng) : Point", " rad = Geo.deg_to_rad(360 / n) * i", " rn = r + LAYER_DIST", " x, y = c.cp", " p = {x + rn * Math.cos(rad), y + rn * Math.sin(rad)}", " rn = next_radius(r, sector_angle(c.cp, p), c, prng)", " {(x + rn * Math.cos(rad)).round(1),", " (y + rn * Math.sin(rad)).round(1)}", " end", "", " def next_radius(r, a, c, prng) : Float64", " d = adjacent_dead_angles(a, c.sectors, SECTOR_NUM)", " unless d.empty?", " return average_radius(d) + prng.rand(LAYER_DIST * 2)", " end", " r * growth_rate(a, c) + LAYER_DIST * prng.rand(LAYER_MAX)", " end", "", " def average_radius(d) : Float64", " d.sum(0) / d.size", " end", "", " def growth_rate(a, c) : Float64", " n = (a * SECTOR_RES).round.to_i % 360", " Blend.step(1, c.growth[n], Blend.pos(c.layers.size, 20))", " end", "", " def sector_angle(p1, p2) : Int32", " d = Geo.rad_to_deg(Geo.angle_from_points(p1, p2)) + 180", " (d / SECTOR_RES).to_i", " end", "", " def adjacent_dead_angles(a, s, n) : Array(Float64)", " o = (10 + n / 360).round.to_i", " (-o..o).compact_map { |i| s[(n + a + i).to_i % n]? }", " end", " end", "", " module Blend", " extend self", "", " def step(f, t, p) : Float64", " return f.to_f if p <= 0", " return t.to_f if p >= 1", " f + (t - f) * p", " end", "", " def pos(c, t) : Float64", " (1.0 / t) * c", " end", "", " def linear(s, e, n) : Array(Float64)", " (0..n).map { |i| (s + ((e - s) / n) * i).round(2) }", " end", "", " def key_points(ks, n) : Array(Float64)", " b = (0..(ks.size - 1)).reduce([] of Float64) do |m, i|", " if k = ks[i.succ]?", " (m += linear(ks[i][1], k[1], k[0] - ks[i][0])[..-2])", " else", " m", " end", " end", " t = (n - ks.last[0]).to_i", " w = linear(ks.last[1], ks[0][1], t + ks[0][0])[..-2]", " w[t..] + b + w[..(t - 1)]", " end", " end", "", " module Deactivation", " extend self", "", " def update(cs) : Void", " predicted_growth(cs).each.with_index do |g, i|", " next unless gs = g", " cs.each.with_index do |c, j|", " next unless (ps = c.layers.last) && i != j", " ps.each { |p| register_death(c, p, gs) }", " end", " end", " end", "", " def register_death(c, p, ps) : Void", " a = angle(c.cp, p) % SECTOR_NUM", " if c.sectors[a].nil? && Geo.point_in_polygon?(p, ps)", " c.sectors[a] = Geo.point_distance(c.cp, p)", " end", " end", "", " def angle(p1, p2) : Int32", " d = Geo.rad_to_deg(Geo.angle_from_points(p1, p2)) + 180", " (d / SECTOR_RES).to_i", " end", "", " def predicted_growth(cs) : Array(Array(Point)?)", " cs.map do |c|", " if ps = c.layers.last", " resample_polygon(ps).map do |p|", " d = Geo.point_distance(c.cp, p) + PREDICT_DIST", " a = Geo.angle_from_points(c.cp, p)", " Geo.point_on_circle(c.cp, d, a)", " end", " end", " end", " end", "", " def resample_polygon(ps, f = 3)", " ps.map_with_index { |a, i| a if i % f == 0 }.compact", " end", " end", "", " module Path", " extend self", "", " def from_points(ps)", " ps.map_with_index do |p, i|", " i == 0 ? start(p) : bezier(p, i, ps)", " end.join(' ') + stop", " end", "", " def start(p) : String", " %(M #{p[0]},#{p[1]})", " end", "", " def stop : String", " %(z)", " end", "", " def bezier(p, i, a) : String", " s = a[i - 1]", " cs_x, cs_y = control_point(s, a[i - 2], p, false)", " ce_x, ce_y = control_point(p, s, a[(i + 1) % a.size])", " %(C #{cs_x},#{cs_y} #{ce_x},#{ce_y} #{p[0]},#{p[1]})", " end", "", " def control_point(c, p, n, r = true) : Point", " l, a = line_prop(p || c, n || c)", " a += Math::PI if r", " {(c[0] + Math.cos(a) * l * LAYER_SMOOTH).round(1),", " (c[1] + Math.sin(a) * l * LAYER_SMOOTH).round(1)}", " end", "", " def line_prop(p1, p2) : Tuple(Float64, Float64)", " lx, ly = p2[0] - p1[0], p2[1] - p1[1]", " {Math.hypot(lx, ly), Math.atan2(ly, lx)}", " end", " end", "", " module Geo", " extend self", "", " def angle_from_points(p1, p2) : Float64", " Math.atan2(p2[1] - p1[1], p2[0] - p1[0])", " end", "", " def r_to_length(r) : Float64", " r * 2 * Math::PI", " end", "", " def deg_to_rad(deg) : Float64", " deg * Math::PI / 180", " end", "", " def rad_to_deg(rad) : Float64", " rad * 180 / Math::PI", " end", "", " def point_distance(p1, p2) : Float64", " Math.hypot(p1[0] - p2[0], p1[1] - p2[1])", " end", "", " def point_on_circle(cp, r, a) : Point", " {(cp[0] + r * Math.cos(a)).round(2),", " (cp[1] + r * Math.sin(a)).round(2)}", " end", "", " def random_point_on_circle(prng) : Point", " r, rad = SIZE_VB / 2, prng.rand(Math::TAU)", " {(r + Math.cos(rad) * r * prng.rand).round(2),", " (r + Math.sin(rad) * r * prng.rand).round(2)}", " end", "", " def point_in_polygon?(p, ps) : Bool", " inside, prev = false, ps.size - 1", " ps.each.with_index do |pt, i|", " inside = !inside if point_intersect?(p, pt, ps[prev])", " prev = i", " end", " inside", " end", "", " def point_intersect?(p, p1, p2) : Bool", " ((p1[1] > p[1]) != (p2[1] > p[1])) &&", " (p[0] < (p2[0] - p1[0]) *", " (p[1] - p1[1]) / (p2[1] - p1[1]) + p1[0])", " end", " end", "", " module Svg", " extend self", "", " def build(color, cores, prng, layer_flow)", " XML.build_fragment(indent: %( )) do |svg|", " Layer.build(cores, prng, layer_flow)", " d, v, h = SIZE_DOC, SIZE_VB, SIZE_VB / 2", "", " svg.element(%(svg),", " xmlns: %(http://www.w3.org/2000/svg),", " version: %(1.1), stroke: COLORS[color], fill: FILL,", " width: d, height: d, viewBox: %(0 0 #{v} #{v})) do", " svg.element(%(defs)) do", " svg.element(%(clipPath), id: %(clip)) do", " svg.element(%(circle), r: h, cx: h, cy: h)", " end", " end", " svg.element(%(g), %(clip-path): %(url(#clip))) do", " svg.element(%(rect), width: v, height: v)", " cores.each do |core|", " o = 1 / core.layers.size", " core.layers.reverse.each.with_index do |points, i|", " swmin, swmax = STROKE_WIDTH", " somin, somax = STROKE_OPACITY", " sw = (swmin + prng.rand(swmax - swmin)).round(2)", " so = (somin + prng.rand(somax = somin)).round(2)", " fo = Math.max(0.5 - i * o, 0)", " svg.element(%(path),", " d: Path.from_points(points),", " %(fill-opacity): fo,", " %(stroke-width): sw, %(stroke-opacity): so)", " end", " end", " end", " end", " end", " end", " end", "", " struct FloatFromInt", " def self.from_json(pull : JSON::PullParser)", " pull.read_int.to_f", " end", "", " def self.to_json(value : Float64, json : JSON::Builder)", " json.scalar(value.to_i)", " end", " end", "", " struct PointFromArray", " def self.from_json(pull : JSON::PullParser)", " pull.read_begin_array", " x, y = pull.read_int, pull.read_int", " pull.read_end_array", " {x.to_f, y.to_f}", " end", "", " def self.to_json(value : Point, json : JSON::Builder)", " json.start_array", " json.scalar(value[0].to_i)", " json.scalar(value[1].to_i)", " json.end_array", " end", " end", "end", "", "# Uncomment lines 453-465 to generate a Dendro. The output will", "# be an SVG-formatted string. Copy and paste it into a new file,", "# and call is for example 'dendro.svg'.", "#", "# Replace the values below with the values of your Dendro. They", "# can be found in the metadata of your NFT.", "#", "# number = 1 # integer value (1-3000)", "# color_id = 0 # integer value (0-5)", "# previous_cores = %([]) # JSON string (array of cores)", "# add_core = false # boolean value (true/false)", "", "# dendro = Dendro.new(", "# seed: number,", "# color: color_id,", "# previous_cores: previous_cores,", "# add_core: add_core", "# )", "", "# puts dendro.to_svg" ], "name": "dendro.cr", "mediaType": "text/crystal" } ], "image": "ipfs://QmPj3DHtH754FvcDEJUbodCe2zNptSN5o1rSnYz78ygttu", "title": "Dendro 0000", "series": "DendroRithms", "mediaType": "image/jpeg", "description": "Genesis Dendro - Source Code" }