|
| 1 | +package sketch |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "image" |
| 6 | + "image/color" |
| 7 | + "math" |
| 8 | + "math/rand" |
| 9 | + |
| 10 | + "github.com/fogleman/gg" |
| 11 | + "github.com/teacat/noire" |
| 12 | +) |
| 13 | + |
| 14 | +var ( |
| 15 | + andersonColors = [...][3]uint8{ |
| 16 | + {224, 105, 99}, |
| 17 | + {119, 194, 169}, |
| 18 | + {45, 225, 100}, |
| 19 | + {135, 174, 99}, |
| 20 | + {232, 223, 104}, |
| 21 | + {56, 125, 179}, |
| 22 | + } |
| 23 | + sky = [3]int{46, 59, 75} |
| 24 | + water = [3]int{52, 56, 57} |
| 25 | + dark = [3]int{36, 47, 62} |
| 26 | +) |
| 27 | + |
| 28 | +// AndersonParams contains externally-provided parameters |
| 29 | +type AndersonParams struct { |
| 30 | + // tweakable parameters for the cli |
| 31 | + DestWidth int |
| 32 | + DestHeight int |
| 33 | + Iterations int |
| 34 | +} |
| 35 | + |
| 36 | +// AndersonSketch wraps all the components needed to draw the spiral sketch |
| 37 | +type AndersonSketch struct { |
| 38 | + AndersonParams |
| 39 | + DC *gg.Context |
| 40 | + currentR float64 |
| 41 | + horizon int // also max height of each slot |
| 42 | + slot float64 |
| 43 | + slotOffsets [6]int |
| 44 | +} |
| 45 | + |
| 46 | +// NewAndersonSketch initializes the canvas and AndersonSketch |
| 47 | +// The sketch has a top sky zone and a bottom water zone made of a blue-ish color on top and a gray-blue color on bottom |
| 48 | +// Each color fits into a "slot" of equal possible width. |
| 49 | +// The edges on the left and right can be different sizes than the rest of the slots. |
| 50 | +// The water rectangle mirrors the sky rectangle but gets muted out. |
| 51 | +// The sky rectangle has progressively more opaque and vibrant colored squares with paint marks and fades. |
| 52 | +// The progression of colors can be offset to either side of the slot. |
| 53 | +// The water rectangle contains aberations that look vaguely like waves. |
| 54 | +func NewAndersonSketch(params AndersonParams) *AndersonSketch { |
| 55 | + fmt.Println("Starting Sketch") |
| 56 | + |
| 57 | + s := &AndersonSketch{AndersonParams: params, currentR: 2.0} |
| 58 | + s.horizon = randIntRangeFrom(s.DestHeight/5, s.DestHeight*4/5) |
| 59 | + //s.horizon = 300 |
| 60 | + s.slot = float64(s.DestWidth) / 8.0 |
| 61 | + |
| 62 | + // canvas is a gg image context and contains what gets drawn to the screen |
| 63 | + canvas := gg.NewContext(s.DestWidth, s.DestHeight) |
| 64 | + canvas.SetLineWidth(0.0) |
| 65 | + // set sky rectangle |
| 66 | + canvas.SetRGBA255(sky[0], sky[1], sky[2], 255) |
| 67 | + canvas.DrawRectangle(0, 0, float64(s.DestWidth), float64(s.horizon)) |
| 68 | + canvas.FillPreserve() |
| 69 | + canvas.Stroke() |
| 70 | + // set water rectangle |
| 71 | + canvas.SetRGBA255(water[0], water[1], water[2], 255) |
| 72 | + canvas.DrawRectangle(0, float64(s.horizon), float64(s.DestWidth), float64(s.DestHeight)) |
| 73 | + canvas.FillPreserve() |
| 74 | + canvas.Stroke() |
| 75 | + s.DC = canvas |
| 76 | + |
| 77 | + rand.Shuffle(len(andersonColors), func(i, j int) { |
| 78 | + andersonColors[i], andersonColors[j] = andersonColors[j], andersonColors[i] |
| 79 | + }) |
| 80 | + |
| 81 | + // slot offsets, 1 for left and -1 for right |
| 82 | + slotOffsets := [6]int{} |
| 83 | + for j := 0; j < len(andersonColors); j++ { |
| 84 | + if rand.Intn(100) > 50 { |
| 85 | + slotOffsets[j] = 1 |
| 86 | + } else { |
| 87 | + slotOffsets[j] = -1 |
| 88 | + } |
| 89 | + } |
| 90 | + s.slotOffsets = slotOffsets |
| 91 | + |
| 92 | + return s |
| 93 | +} |
| 94 | + |
| 95 | +// Output produces an image output of the current state of the sketch |
| 96 | +func (s *AndersonSketch) Output() image.Image { |
| 97 | + return s.DC.Image() |
| 98 | +} |
| 99 | + |
| 100 | +// Update makes a logical step into generation |
| 101 | +func (s *AndersonSketch) Update(i int) { |
| 102 | + for j := 0; j < len(andersonColors); j++ { |
| 103 | + acolor := andersonColors[j] |
| 104 | + |
| 105 | + x := s.slot + float64(j)*s.slot |
| 106 | + y := float64(s.horizon) |
| 107 | + |
| 108 | + maxWidth := float64(s.slot) / float64(i+1) |
| 109 | + nextStepWidth := float64(s.slot) / float64(i+2) |
| 110 | + w := maxWidth |
| 111 | + if i != 0 { |
| 112 | + w = randFloat64RangeFrom(nextStepWidth+(maxWidth-nextStepWidth)/2, maxWidth) |
| 113 | + } |
| 114 | + // push the starting place to the right if selected at initilization |
| 115 | + if s.slotOffsets[j] == -1 { |
| 116 | + x += s.slot - w |
| 117 | + } |
| 118 | + |
| 119 | + maxHeight := y / float64(i+1) |
| 120 | + nextStepHeight := y / float64(i+2) |
| 121 | + h := randFloat64RangeFrom(nextStepHeight, maxHeight) |
| 122 | + |
| 123 | + // gradient is two circles: first is the solid color and is the smaller of the two |
| 124 | + // second is the transparent color and is larger |
| 125 | + // solid color is 100% within the first circle and transitions between the smaller and larger circles |
| 126 | + // transparent color is 100% outside the second circle. |
| 127 | + // Ensure the first circle is entirely below the horizon, so that the base is a solid color. |
| 128 | + // Jitter left and right and radius of larger circle for some variation |
| 129 | + grad := gg.NewRadialGradient(x+w/2, y+5, 5, x+w/2+randFloat64Range(5), y+5, h+randFloat64Range(5)) |
| 130 | + |
| 131 | + alpha := minFloat64(0.2+0.2*float64(i), 1.0) |
| 132 | + solid := color.RGBA{} |
| 133 | + solid.R = uint8(alpha * float64(acolor[0])) |
| 134 | + solid.G = uint8(alpha * float64(acolor[1])) |
| 135 | + solid.B = uint8(alpha * float64(acolor[2])) |
| 136 | + solid.A = uint8(alpha * 255) |
| 137 | + |
| 138 | + transparent := color.RGBA{} |
| 139 | + transparent.R = 0 |
| 140 | + transparent.G = 0 |
| 141 | + transparent.B = 0 |
| 142 | + transparent.A = 0 |
| 143 | + |
| 144 | + grad.AddColorStop(0, solid) |
| 145 | + grad.AddColorStop(1, transparent) |
| 146 | + |
| 147 | + s.DC.SetFillStyle(grad) |
| 148 | + s.DC.DrawRectangle(x, y, w, -h) |
| 149 | + s.DC.Fill() |
| 150 | + s.DC.Stroke() |
| 151 | + |
| 152 | + // water mirror |
| 153 | + wgrad := gg.NewRadialGradient(x+w/2, y-5, 5, x+w/2, y-5, h) |
| 154 | + wcolor := noire.NewRGB(float64(acolor[0]), float64(acolor[1]), float64(acolor[2])) |
| 155 | + r, g, b := wcolor.Darken(0.33).RGB() |
| 156 | + |
| 157 | + walpha := minFloat64(0.2+0.2*float64(i), 1.0) |
| 158 | + wsolid := color.RGBA{} |
| 159 | + wsolid.R = uint8(walpha * float64(r)) |
| 160 | + wsolid.G = uint8(walpha * float64(g)) |
| 161 | + wsolid.B = uint8(walpha * float64(b)) |
| 162 | + wsolid.A = uint8(walpha * 255) |
| 163 | + |
| 164 | + wtransparent := color.RGBA{} |
| 165 | + wtransparent.R = 0 |
| 166 | + wtransparent.G = 0 |
| 167 | + wtransparent.B = 0 |
| 168 | + wtransparent.A = 0 |
| 169 | + |
| 170 | + wgrad.AddColorStop(0, wsolid) |
| 171 | + wgrad.AddColorStop(1, wtransparent) |
| 172 | + |
| 173 | + waterScale := 1.4 |
| 174 | + s.DC.SetFillStyle(wgrad) |
| 175 | + s.DC.DrawRectangle(x, y, w, h*waterScale) |
| 176 | + s.DC.Fill() |
| 177 | + s.DC.Stroke() |
| 178 | + |
| 179 | + // if this is the last iteration, we are going to put some full strength color and near black blocks in the front |
| 180 | + // and draw the sides |
| 181 | + if i == s.Iterations { |
| 182 | + // shuffle, we'll care about the first 3 only |
| 183 | + // 0 = darken |
| 184 | + // 1 = brighten |
| 185 | + // 2 = near-black |
| 186 | + // 3 = same |
| 187 | + options := [...]int{0, 1, 2, 2, 2, 2, 3} |
| 188 | + rand.Shuffle(len(options), func(i, j int) { |
| 189 | + options[i], options[j] = options[j], options[i] |
| 190 | + }) |
| 191 | + |
| 192 | + lcolor := noire.NewRGB(float64(acolor[0]), float64(acolor[1]), float64(acolor[2])) |
| 193 | + // reset |
| 194 | + x = s.slot + float64(j)*s.slot |
| 195 | + h := nextStepHeight |
| 196 | + down := y |
| 197 | + wi := math.Round(s.slot / 3) |
| 198 | + he := -h / 2 |
| 199 | + hj := 0.0 |
| 200 | + |
| 201 | + for t := 0; t < 3; t++ { |
| 202 | + left := math.Round(x + float64(t)*s.slot/3) |
| 203 | + if t == 1 { |
| 204 | + hj = randFloat64Range(he / 5) |
| 205 | + } else { |
| 206 | + hj = 0.0 |
| 207 | + } |
| 208 | + switch options[t] { |
| 209 | + case 0: |
| 210 | + dr, dg, db := lcolor.Darken(0.15).RGB() |
| 211 | + s.DC.SetRGBA255(int(dr), int(dg), int(db), 255) |
| 212 | + s.DC.DrawRectangle(left, down, wi, he-hj) |
| 213 | + s.DC.Fill() |
| 214 | + s.DC.Stroke() |
| 215 | + case 1: |
| 216 | + br, bg, bb := lcolor.Brighten(0.15).RGB() |
| 217 | + s.DC.SetRGBA255(int(br), int(bg), int(bb), 255) |
| 218 | + s.DC.DrawRectangle(left, down, wi, he-hj) |
| 219 | + s.DC.Fill() |
| 220 | + s.DC.Stroke() |
| 221 | + case 2: |
| 222 | + s.DC.SetRGBA255(dark[0], dark[1], dark[2], 255) |
| 223 | + s.DC.DrawRectangle(left, down, wi, he-hj) |
| 224 | + s.DC.Fill() |
| 225 | + s.DC.Stroke() |
| 226 | + case 3: |
| 227 | + nr, ng, nb := lcolor.RGB() |
| 228 | + s.DC.SetRGBA255(int(nr), int(ng), int(nb), 255) |
| 229 | + s.DC.DrawRectangle(left, down, wi, he-hj) |
| 230 | + s.DC.Fill() |
| 231 | + s.DC.Stroke() |
| 232 | + } |
| 233 | + } |
| 234 | + |
| 235 | + // draw the shapes on the far edges of the sketch |
| 236 | + // left |
| 237 | + s.DC.SetRGBA255(dark[0], dark[1], dark[2], 255) |
| 238 | + s.DC.MoveTo(0, float64(s.horizon)) |
| 239 | + s.DC.LineTo(s.slot, float64(s.horizon)) |
| 240 | + s.DC.LineTo(s.slot, float64(s.horizon)+he) |
| 241 | + s.DC.LineTo(0, float64(s.horizon)+2*he) |
| 242 | + s.DC.ClosePath() |
| 243 | + s.DC.Fill() |
| 244 | + s.DC.Stroke() |
| 245 | + // right |
| 246 | + s.DC.SetRGBA255(dark[0], dark[1], dark[2], 255) |
| 247 | + s.DC.MoveTo(float64(s.DestWidth)-s.slot, float64(s.horizon)) |
| 248 | + s.DC.LineTo(float64(s.DestWidth), float64(s.horizon)) |
| 249 | + s.DC.LineTo(float64(s.DestWidth), float64(s.horizon)+2*he) |
| 250 | + s.DC.LineTo(float64(s.DestWidth)-s.slot, float64(s.horizon)+he) |
| 251 | + s.DC.ClosePath() |
| 252 | + s.DC.Fill() |
| 253 | + s.DC.Stroke() |
| 254 | + |
| 255 | + // add ripples |
| 256 | + ripples := (s.DestHeight - s.horizon) / 100 |
| 257 | + for k := 0; k < ripples; k++ { |
| 258 | + s.DC.SetRGBA255(dark[0], dark[1], dark[2], 10) |
| 259 | + s.DC.DrawRectangle(0, float64(s.horizon+100*k)+randFloat64Range(5.0), float64(s.DestWidth), 10+randFloat64Range(3)) |
| 260 | + s.DC.Fill() |
| 261 | + s.DC.Stroke() |
| 262 | + } |
| 263 | + } |
| 264 | + } |
| 265 | +} |
0 commit comments