Compare commits
20 Commits
b8011837e9
...
main
Author | SHA1 | Date | |
---|---|---|---|
b97737f670
|
|||
651473975f
|
|||
050c1da950
|
|||
3220b6aa64
|
|||
b4e70c2245
|
|||
94489b5da2
|
|||
beb81771a8
|
|||
0ee3e39c3c
|
|||
9f5bb724cb
|
|||
62e7aa0e07
|
|||
dfaef09ccb
|
|||
dc3503a407
|
|||
ba0834d729 | |||
93e4e9453f
|
|||
06cf912490 | |||
cbaf9bc071
|
|||
eaa2541596
|
|||
5941ed7909
|
|||
|
01fb94c9eb
|
||
|
bfd1ecabeb
|
48
background.py
Normal file
48
background.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import json
|
||||||
|
import random
|
||||||
|
|
||||||
|
from speckles import make_wallpaper
|
||||||
|
|
||||||
|
palettes = [
|
||||||
|
{"colours": ["#DE4A21", "#6F04F2", "#490B1C", "#4a9c9e"], "votes": 1000000},
|
||||||
|
{"colours": ["#eee4ab", "#e5cb9f", "#99c4c8", "#68a7ad"], "votes": 100000},
|
||||||
|
{"colours": ["#e4d192", "#cba0ae", "#af7ab3", "#80558c"], "votes": 100000},
|
||||||
|
{"colours": ["#eeeeee", "#e1d4bb", "#cbb279", "#537188"], "votes": 100000},
|
||||||
|
{"colours": ["#c0dbea", "#ba90c6", "#e8a0bf", "#fdf4f5"], "votes": 100000},
|
||||||
|
{"colours": ["#f5ffc9", "#b3e5be", "#a86464", "#804674"], "votes": 100000},
|
||||||
|
{"colours": ["#ffde7d", "#f6416c", "#f8f3d4", "#00b8a9"], "votes": 100000},
|
||||||
|
{"colours": ["#53354a", "#903749", "#e84545", "#2b2e4a"], "votes": 100000},
|
||||||
|
{"colours": ["#967e76", "#d7c0ae", "#eee3cb", "#b7c4cf"], "votes": 100000},
|
||||||
|
{"colours": ["#fc5185", "#f5f5f5", "#3fc1c9", "#364f6b"], "votes": 100000},
|
||||||
|
{"colours": ["#eaeaea", "#ff2e63", "#252a34", "#08d9d6"], "votes": 100000},
|
||||||
|
{"colours": ["#eeeeee", "#00adb5", "#393e46", "#222831"], "votes": 100000},
|
||||||
|
{"colours": ["#2cd3e1", "#a459d1", "#f266ab", "#ffb84c"], "votes": 100000},
|
||||||
|
{"colours": ["#ffe194", "#e8f6ef", "#1b9c85", "#4c4c6d"], "votes": 100000},
|
||||||
|
{"colours": ["#146c94", "#19a7ce", "#b0daff", "#feff86"], "votes": 100000},
|
||||||
|
{"colours": ["#4c3d3d", "#c07f00", "#ffd95a", "#fff7d4"], "votes": 100000},
|
||||||
|
{"colours": ["#8bacaa", "#b04759", "#e76161", "#f99b7d"], "votes": 100000},
|
||||||
|
{"colours": ["#146c94", "#19a7ce", "#afd3e2", "#f6f1f1"], "votes": 100000},
|
||||||
|
{"colours": ["#9ba4b5", "#212a3e", "#394867", "#f1f6f9"], "votes": 100000},
|
||||||
|
{"colours": ["#00ffca", "#05bfdb", "#088395", "#0a4d68"], "votes": 100000},
|
||||||
|
{"colours": ["#7c9070", "#9ca777", "#fee8b0", "#f97b22"], "votes": 100000},
|
||||||
|
{"colours": ["#002a19", "#000000", "#ffffff", "#e0512f"], "votes": 100000},
|
||||||
|
{"colours": ["#212248", "#EBAE36", "#C7C9F0", "#11088A"], "votes": 100000},
|
||||||
|
]
|
||||||
|
with open("popular.json", "r") as f:
|
||||||
|
palettes += json.load(f)
|
||||||
|
|
||||||
|
palette = random.choice(palettes)
|
||||||
|
enabled_markers = "otY+xP*b"
|
||||||
|
markers = random.choices(enabled_markers, k=random.randint(1, len(enabled_markers)))
|
||||||
|
for i, palette in enumerate(palettes):
|
||||||
|
colours = ["none"] + palette["colours"]
|
||||||
|
b = make_wallpaper(
|
||||||
|
",".join(colours),
|
||||||
|
fileformat="png",
|
||||||
|
size=2.1,
|
||||||
|
density=0.2,
|
||||||
|
filename=f"wallpapers/speckles{i}_2880x1800.png",
|
||||||
|
perlin=True,
|
||||||
|
markers=enabled_markers,
|
||||||
|
orientation="2880x1800",
|
||||||
|
)
|
57
blob.py
Normal file
57
blob.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import svg
|
||||||
|
from typing import Iterator, NamedTuple
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
RGen = np.random.default_rng()
|
||||||
|
|
||||||
|
|
||||||
|
class Point(NamedTuple):
|
||||||
|
x: float
|
||||||
|
y: float
|
||||||
|
|
||||||
|
|
||||||
|
def iter_body_points(size: int) -> Iterator[Point]:
|
||||||
|
n_points = RGen.integers(3, 12) # how many points do we want?
|
||||||
|
angle_step = (
|
||||||
|
np.pi * 2 / n_points
|
||||||
|
) # step used to place each point at equal distances
|
||||||
|
for point_number in range(1, n_points + 1):
|
||||||
|
pull = 0.75 * RGen.random() * 0.25
|
||||||
|
angle = point_number * angle_step
|
||||||
|
x = size + np.cos(angle) * size * pull
|
||||||
|
y = size + np.sin(angle) * size * pull
|
||||||
|
yield Point(x, y)
|
||||||
|
|
||||||
|
|
||||||
|
def spline(points: list[Point], body_tension: int) -> Iterator[svg.PathData]:
|
||||||
|
"""
|
||||||
|
https://github.com/georgedoescode/splinejs
|
||||||
|
"""
|
||||||
|
yield svg.MoveTo(*points[-1])
|
||||||
|
first_point = points[0]
|
||||||
|
second_point = points[1]
|
||||||
|
points.insert(0, points[-1])
|
||||||
|
points.insert(0, points[-2])
|
||||||
|
points.append(first_point)
|
||||||
|
points.append(second_point)
|
||||||
|
for p0, p1, p2, p3 in zip(points, points[1:], points[2:], points[3:]):
|
||||||
|
yield svg.CubicBezier(
|
||||||
|
x1=p1.x + (p2.x - p0.x) / 6 * body_tension,
|
||||||
|
y1=p1.y + (p2.y - p0.y) / 6 * body_tension,
|
||||||
|
x2=p2.x - (p3.x - p1.x) / 6 * body_tension,
|
||||||
|
y2=p2.y - (p3.y - p1.y) / 6 * body_tension,
|
||||||
|
x=p2.x,
|
||||||
|
y=p2.y,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def blob(x, y, c, s, body_tension: int = 1) -> svg.Path:
|
||||||
|
points = list(iter_body_points(s))
|
||||||
|
return svg.Path(
|
||||||
|
d=list(spline(points, body_tension)),
|
||||||
|
fill=c,
|
||||||
|
stroke="#000000",
|
||||||
|
stroke_width=2,
|
||||||
|
transform=[svg.Translate(x-s, y-s)],
|
||||||
|
)
|
107
main.py
107
main.py
@@ -1,17 +1,14 @@
|
|||||||
import io
|
import io
|
||||||
import logging
|
from perlin import CoordsGenerator
|
||||||
|
from typing import Literal
|
||||||
import itertools
|
import itertools
|
||||||
import json
|
import logging
|
||||||
import random
|
import random
|
||||||
from collections import Counter
|
|
||||||
from copy import deepcopy
|
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi import FastAPI, Response
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
|
|
||||||
@@ -41,15 +38,65 @@ MEDIA_TYPES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Marker = Literal[
|
||||||
|
".",
|
||||||
|
",",
|
||||||
|
"o",
|
||||||
|
"v",
|
||||||
|
"^",
|
||||||
|
"<",
|
||||||
|
">",
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
"4",
|
||||||
|
"8",
|
||||||
|
"s",
|
||||||
|
"p",
|
||||||
|
"P",
|
||||||
|
"*",
|
||||||
|
"h",
|
||||||
|
"H",
|
||||||
|
"+",
|
||||||
|
"x",
|
||||||
|
"X",
|
||||||
|
"D",
|
||||||
|
"d",
|
||||||
|
"|",
|
||||||
|
"_",
|
||||||
|
]
|
||||||
|
|
||||||
|
marker_subs = {
|
||||||
|
"=": "$=$",
|
||||||
|
"/": "$/$",
|
||||||
|
"\\": "$\\setminus$",
|
||||||
|
"«": "$«$",
|
||||||
|
"»": "$»$",
|
||||||
|
"~": "$\\sim$",
|
||||||
|
"♪": "$♪$",
|
||||||
|
"♫": "$♫$",
|
||||||
|
"∞": "$\\infty$",
|
||||||
|
"♡": "$♡$",
|
||||||
|
"o": "$\\bigcirc$",
|
||||||
|
"♠": "$♠$",
|
||||||
|
"♣": "$♣$",
|
||||||
|
"♥": "$♥$",
|
||||||
|
"♦": "$♦$",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/speckles/")
|
@app.get("/speckles/")
|
||||||
def make_wallpaper(
|
def make_wallpaper(
|
||||||
speckle_colours: str,
|
speckle_colours: str,
|
||||||
density: float | None = 0.12,
|
density: float = 0.12,
|
||||||
size: float | None = 3,
|
size: float = 3,
|
||||||
fileformat: str = "svg",
|
fileformat: str = "svg",
|
||||||
orientation: str | None = "landscape",
|
orientation: str = "landscape",
|
||||||
|
filename: str = "",
|
||||||
|
markers: str = ".",
|
||||||
|
perlin: bool = True,
|
||||||
):
|
):
|
||||||
if not fileformat in MEDIA_TYPES:
|
if fileformat not in MEDIA_TYPES:
|
||||||
return
|
return
|
||||||
speckle_colours = speckle_colours.split(",")
|
speckle_colours = speckle_colours.split(",")
|
||||||
background = speckle_colours.pop(0)
|
background = speckle_colours.pop(0)
|
||||||
@@ -59,17 +106,17 @@ def make_wallpaper(
|
|||||||
x, y = (1920, 1080)
|
x, y = (1920, 1080)
|
||||||
elif "x" in orientation:
|
elif "x" in orientation:
|
||||||
resolution = orientation.split("x")
|
resolution = orientation.split("x")
|
||||||
if len(resolution) !=2 :
|
if len(resolution) != 2:
|
||||||
logging.critical("input resolution has more or less than 2 dimensions")
|
logging.critical("input resolution has more or less than 2 dimensions")
|
||||||
return
|
return
|
||||||
x,y = resolution
|
x, y = resolution
|
||||||
if all([x.isdigit(), y.isdigit()]):
|
if all([x.isdigit(), y.isdigit()]):
|
||||||
x,y = int(x), int(y)
|
x, y = int(x), int(y)
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
x, y = (1920, 1080)
|
x, y = (1920, 1080)
|
||||||
speckles_per_colour = int(x / 128 * y / 128 * density)
|
speckles_per_colour = int(x / 128 * y / 128 * density / len(markers))
|
||||||
|
|
||||||
fig, ax = plt.subplots(figsize=(x / 120, y / 120), facecolor=background)
|
fig, ax = plt.subplots(figsize=(x / 120, y / 120), facecolor=background)
|
||||||
ax.set_facecolor(background)
|
ax.set_facecolor(background)
|
||||||
@@ -78,15 +125,28 @@ def make_wallpaper(
|
|||||||
ax.set_yticks([])
|
ax.set_yticks([])
|
||||||
ax.margins(0, 0)
|
ax.margins(0, 0)
|
||||||
|
|
||||||
for color, size in itertools.product(
|
if perlin:
|
||||||
|
gen = CoordsGenerator(y, x)
|
||||||
|
|
||||||
|
for color, marker, size in itertools.product(
|
||||||
speckle_colours,
|
speckle_colours,
|
||||||
|
markers,
|
||||||
np.logspace(0, size, 10, base=np.exp(2)),
|
np.logspace(0, size, 10, base=np.exp(2)),
|
||||||
):
|
):
|
||||||
|
marker = marker_subs.get(marker, marker)
|
||||||
|
if perlin:
|
||||||
|
x_coords, y_coords = gen.pick(speckles_per_colour)
|
||||||
|
else:
|
||||||
|
x_coords, y_coords = (
|
||||||
|
[random.random() * x / 8 for _ in range(speckles_per_colour)],
|
||||||
|
[random.random() * y / 8 for _ in range(speckles_per_colour)],
|
||||||
|
)
|
||||||
ax.scatter(
|
ax.scatter(
|
||||||
[random.random() * x / 8 for _ in range(speckles_per_colour)],
|
x_coords,
|
||||||
[random.random() * y / 8 for _ in range(speckles_per_colour)],
|
y_coords,
|
||||||
c=color,
|
c=color,
|
||||||
s=size,
|
s=size,
|
||||||
|
marker=marker,
|
||||||
)
|
)
|
||||||
|
|
||||||
fig.tight_layout()
|
fig.tight_layout()
|
||||||
@@ -103,9 +163,14 @@ def make_wallpaper(
|
|||||||
pad_inches=0,
|
pad_inches=0,
|
||||||
)
|
)
|
||||||
buf.seek(0)
|
buf.seek(0)
|
||||||
return StreamingResponse(content=buf, media_type=MEDIA_TYPES[fileformat])
|
if filename:
|
||||||
buf.close()
|
with open(filename, "wb") as f:
|
||||||
|
f.write(buf.getbuffer())
|
||||||
|
buf.close()
|
||||||
|
return filename
|
||||||
|
else:
|
||||||
|
return StreamingResponse(content=buf, media_type=MEDIA_TYPES[fileformat])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
uvicorn.run("main:app", workers=2, port=8099, reload=False)
|
uvicorn.run("main:app", workers=3, port=8099, reload=True)
|
||||||
|
85
perlin.py
Normal file
85
perlin.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import numpy as np
|
||||||
|
from scipy.special import softmax
|
||||||
|
|
||||||
|
|
||||||
|
def interpolant(t):
|
||||||
|
return t * t * t * (t * (t * 6 - 15) + 10)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_perlin_noise_2d(
|
||||||
|
shape, res, tileable=(False, False), interpolant=interpolant
|
||||||
|
):
|
||||||
|
"""Generate a 2D numpy array of perlin noise.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
shape: The shape of the generated array (tuple of two ints).
|
||||||
|
This must be a multple of res.
|
||||||
|
res: The number of periods of noise to generate along each
|
||||||
|
axis (tuple of two ints). Note shape must be a multiple of
|
||||||
|
res.
|
||||||
|
tileable: If the noise should be tileable along each axis
|
||||||
|
(tuple of two bools). Defaults to (False, False).
|
||||||
|
interpolant: The interpolation function, defaults to
|
||||||
|
t*t*t*(t*(t*6 - 15) + 10).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A numpy array of shape shape with the generated noise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If shape is not a multiple of res.
|
||||||
|
"""
|
||||||
|
delta = (res[0] / shape[0], res[1] / shape[1])
|
||||||
|
d = (shape[0] // res[0], shape[1] // res[1])
|
||||||
|
grid = np.mgrid[0 : res[0] : delta[0], 0 : res[1] : delta[1]].transpose(1, 2, 0) % 1
|
||||||
|
# Gradients
|
||||||
|
angles = 2 * np.pi * np.random.rand(res[0] + 1, res[1] + 1)
|
||||||
|
gradients = np.dstack((np.cos(angles), np.sin(angles)))
|
||||||
|
if tileable[0]:
|
||||||
|
gradients[-1, :] = gradients[0, :]
|
||||||
|
if tileable[1]:
|
||||||
|
gradients[:, -1] = gradients[:, 0]
|
||||||
|
gradients = gradients.repeat(d[0], 0).repeat(d[1], 1)
|
||||||
|
g00 = gradients[: -d[0], : -d[1]]
|
||||||
|
g10 = gradients[d[0] :, : -d[1]]
|
||||||
|
g01 = gradients[: -d[0], d[1] :]
|
||||||
|
g11 = gradients[d[0] :, d[1] :]
|
||||||
|
# Ramps
|
||||||
|
n00 = np.sum(np.dstack((grid[:, :, 0], grid[:, :, 1])) * g00, 2)
|
||||||
|
n10 = np.sum(np.dstack((grid[:, :, 0] - 1, grid[:, :, 1])) * g10, 2)
|
||||||
|
n01 = np.sum(np.dstack((grid[:, :, 0], grid[:, :, 1] - 1)) * g01, 2)
|
||||||
|
n11 = np.sum(np.dstack((grid[:, :, 0] - 1, grid[:, :, 1] - 1)) * g11, 2)
|
||||||
|
# Interpolation
|
||||||
|
t = interpolant(grid)
|
||||||
|
n0 = n00 * (1 - t[:, :, 0]) + t[:, :, 0] * n10
|
||||||
|
n1 = n01 * (1 - t[:, :, 0]) + t[:, :, 0] * n11
|
||||||
|
return np.sqrt(2) * ((1 - t[:, :, 1]) * n0 + t[:, :, 1] * n1)
|
||||||
|
|
||||||
|
|
||||||
|
class CoordsGenerator:
|
||||||
|
def __init__(self, x: int, y: int, factor: int = 500):
|
||||||
|
print(x, y, x // factor, y // factor)
|
||||||
|
self.noise = generate_perlin_noise_2d((x, y), (x // factor, y // factor))
|
||||||
|
self.noise_distribution = softmax(self.noise, axis=1)
|
||||||
|
|
||||||
|
def pick(self, n):
|
||||||
|
x, y = self.noise.shape
|
||||||
|
x = np.random.choice(x, size=n, replace=False)
|
||||||
|
y = [
|
||||||
|
np.random.choice(y, size=1, p=self.noise_distribution[x_, :], replace=False)
|
||||||
|
for x_ in x
|
||||||
|
]
|
||||||
|
return x, np.array(y).flatten()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
# gen = CoordsGenerator(1920, 1080, threshold=0.85)
|
||||||
|
# x, y = gen.pick(1000)
|
||||||
|
# plt.scatter(x, y)
|
||||||
|
factor = 500
|
||||||
|
noise = generate_perlin_noise_2d((1080, 1920), (1080 // factor, 1920 // factor))
|
||||||
|
|
||||||
|
plt.matshow(noise, cmap="bwr")
|
||||||
|
plt.colorbar()
|
||||||
|
plt.show()
|
1
popular.json
Normal file
1
popular.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[{"colours": ["#264653", "#2a9d8f", "#e9c46a", "#f4a261", "#e76f51"], "votes": 99400}, {"colours": ["#ffcdb2", "#ffb4a2", "#e5989b", "#b5838d", "#6d6875"], "votes": 64500}, {"colours": ["#e63946", "#f1faee", "#a8dadc", "#457b9d", "#1d3557"], "votes": 64099}, {"colours": ["#cb997e", "#ddbea9", "#ffe8d6", "#b7b7a4", "#a5a58d", "#6b705c"], "votes": 63700}, {"colours": ["#606c38", "#283618", "#fefae0", "#dda15e", "#bc6c25"], "votes": 56400}, {"colours": ["#ccd5ae", "#e9edc9", "#fefae0", "#faedcd", "#d4a373"], "votes": 54200}, {"colours": ["#fec5bb", "#fcd5ce", "#fae1dd", "#f8edeb", "#e8e8e4", "#d8e2dc", "#ece4db", "#ffe5d9", "#ffd7ba", "#fec89a"], "votes": 51800}, {"colours": ["#cdb4db", "#ffc8dd", "#ffafcc", "#bde0fe", "#a2d2ff"], "votes": 50700}, {"colours": ["#ffadad", "#ffd6a5", "#fdffb6", "#caffbf", "#9bf6ff", "#a0c4ff", "#bdb2ff", "#ffc6ff", "#fffffc"], "votes": 42800}, {"colours": ["#006d77", "#83c5be", "#edf6f9", "#ffddd2", "#e29578"], "votes": 40900}, {"colours": ["#000000", "#14213d", "#fca311", "#e5e5e5", "#ffffff"], "votes": 40500}, {"colours": ["#8ecae6", "#219ebc", "#023047", "#ffb703", "#fb8500"], "votes": 39700}, {"colours": ["#f4f1de", "#e07a5f", "#3d405b", "#81b29a", "#f2cc8f"], "votes": 39300}, {"colours": ["#03045e", "#023e8a", "#0077b6", "#0096c7", "#00b4d8", "#48cae4", "#90e0ef", "#ade8f4", "#caf0f8"], "votes": 38300}, {"colours": ["#003049", "#d62828", "#f77f00", "#fcbf49", "#eae2b7"], "votes": 36300}, {"colours": ["#eeeeee", "#00adb5", "#393e46", "#222831"], "votes": 53077}, {"colours": ["#71c9ce", "#a6e3e9", "#cbf1f5", "#e3fdfd"], "votes": 30343}, {"colours": ["#6a2c70", "#b83b5e", "#f08a5d", "#f9ed69"], "votes": 27275}, {"colours": ["#8785a2", "#f6f6f6", "#ffe2e2", "#ffc7c7"], "votes": 27218}, {"colours": ["#95e1d3", "#eaffd0", "#fce38a", "#f38181"], "votes": 26925}, {"colours": ["#eaeaea", "#ff2e63", "#252a34", "#08d9d6"], "votes": 26832}, {"colours": ["#112d4e", "#3f72af", "#dbe2ef", "#f9f7f7"], "votes": 26219}, {"colours": ["#ffffd2", "#fcbad3", "#aa96da", "#a8d8ea"], "votes": 24588}, {"colours": ["#424874", "#a6b1e1", "#dcd6f7", "#f4eeff"], "votes": 23767}, {"colours": ["#61c0bf", "#bbded6", "#fae3d9", "#ffb6b9"], "votes": 23214}, {"colours": ["#fc5185", "#f5f5f5", "#3fc1c9", "#364f6b"], "votes": 22036}, {"colours": ["#bbe1fa", "#3282b8", "#0f4c75", "#1b262c"], "votes": 21806}, {"colours": ["#fffbe9", "#e3caa5", "#ceab93", "#ad8b73"], "votes": 21786}, {"colours": ["#ff9494", "#ffd1d1", "#ffe3e1", "#fff5e4"], "votes": 21497}, {"colours": ["#cca8e9", "#c3bef0", "#cadefc", "#defcf9"], "votes": 21348}]
|
395
speckles.py
395
speckles.py
@@ -1,116 +1,311 @@
|
|||||||
import itertools
|
import itertools
|
||||||
import json
|
from blob import blob
|
||||||
import random
|
import io
|
||||||
from collections import Counter
|
from fastapi.responses import StreamingResponse
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import pyvips
|
||||||
|
import logging
|
||||||
|
from perlin import CoordsGenerator
|
||||||
|
import svg
|
||||||
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import uvicorn
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
app = FastAPI(title="Speckles API", root_path="/images")
|
||||||
|
origins = [
|
||||||
|
"http://localhost",
|
||||||
|
"http://localhost:3000",
|
||||||
|
"https://localhost",
|
||||||
|
"https://0124816.xyz",
|
||||||
|
"http://0124816.xyz:3001",
|
||||||
|
]
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=origins,
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
RGen = np.random.default_rng()
|
||||||
|
|
||||||
|
MEDIA_TYPES = {
|
||||||
|
"png": "image/png",
|
||||||
|
"jpeg": "image/jpeg",
|
||||||
|
"jpg": "image/jpeg",
|
||||||
|
"svg": "image/svg+xml",
|
||||||
|
"pdf": "application/pdf",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def emoji(marker: str, source="twemoji"):
|
||||||
|
if len(marker) != 1:
|
||||||
|
return
|
||||||
|
if source == "openmoji":
|
||||||
|
scaler = 512
|
||||||
|
file = hex(ord(marker))[2:].upper()
|
||||||
|
elif source == "twemoji":
|
||||||
|
scaler = 768
|
||||||
|
file = hex(ord(marker))[2:]
|
||||||
|
file = Path(f"{source}/{file}.svg")
|
||||||
|
if file.is_file():
|
||||||
|
return svg.G(
|
||||||
|
elements=[Include(text=file.read_text())],
|
||||||
|
id=marker,
|
||||||
|
transform=[svg.Scale(100 / scaler)],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Include(svg.Element):
|
||||||
|
element_name = "svg"
|
||||||
|
transform: list[svg.Transform] | None = None
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.text
|
||||||
|
|
||||||
|
|
||||||
|
DEFINED = ".o+tYP-|sSex*b"
|
||||||
|
|
||||||
|
|
||||||
|
def markertoSVG(marker, x, y, c, s):
|
||||||
|
if marker == ".":
|
||||||
|
return svg.Circle(cx=x, cy=y, fill=c, r=np.sqrt(s))
|
||||||
|
elif marker == "o":
|
||||||
|
return svg.Circle(
|
||||||
|
cx=x,
|
||||||
|
cy=y,
|
||||||
|
stroke=c,
|
||||||
|
fill="none",
|
||||||
|
stroke_width=np.sqrt(s / 4),
|
||||||
|
r=np.sqrt(s),
|
||||||
|
)
|
||||||
|
elif marker == "+":
|
||||||
|
shift = np.sqrt(s)
|
||||||
|
return svg.Path(
|
||||||
|
stroke=c,
|
||||||
|
stroke_width=np.sqrt(s) / 4,
|
||||||
|
stroke_linecap="round",
|
||||||
|
d=[
|
||||||
|
svg.M(x - shift, y), # horizontal line
|
||||||
|
svg.L(x + shift, y),
|
||||||
|
svg.M(x, y - shift), # vertical line
|
||||||
|
svg.L(x, y + shift),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
elif marker == "t":
|
||||||
|
shift = np.sqrt(s)
|
||||||
|
return svg.Polygon(
|
||||||
|
stroke=c,
|
||||||
|
stroke_width=np.sqrt(s) / 4,
|
||||||
|
stroke_linejoin="round",
|
||||||
|
points=[
|
||||||
|
x + shift * np.sqrt(3) / 2,
|
||||||
|
y - shift / 2,
|
||||||
|
x - shift * np.sqrt(3) / 2,
|
||||||
|
y - shift / 2,
|
||||||
|
x,
|
||||||
|
y + shift,
|
||||||
|
],
|
||||||
|
transform=[svg.Rotate(RGen.random() * 360, x, y)],
|
||||||
|
)
|
||||||
|
elif marker == "Y":
|
||||||
|
shift = np.sqrt(s)
|
||||||
|
return svg.Path(
|
||||||
|
stroke=c,
|
||||||
|
stroke_width=np.sqrt(s) / 4,
|
||||||
|
stroke_linecap="round",
|
||||||
|
d=[
|
||||||
|
svg.M(x + shift * np.sqrt(3) / 2, y - shift / 2), # / line
|
||||||
|
svg.L(x, y),
|
||||||
|
svg.M(x - shift * np.sqrt(3) / 2, y - shift / 2), # / line
|
||||||
|
svg.L(x, y),
|
||||||
|
svg.M(x, y + shift), # / line
|
||||||
|
svg.L(x, y),
|
||||||
|
],
|
||||||
|
transform=[svg.Rotate(RGen.random() * 360, x, y)],
|
||||||
|
)
|
||||||
|
elif marker == "P":
|
||||||
|
shift = np.sqrt(s)
|
||||||
|
return svg.Path(
|
||||||
|
stroke=c,
|
||||||
|
stroke_width=np.sqrt(s) * 2 / 3,
|
||||||
|
d=[
|
||||||
|
svg.M(x - shift, y), # horizontal line
|
||||||
|
svg.L(x + shift, y),
|
||||||
|
svg.M(x, y - shift), # vertical line
|
||||||
|
svg.L(x, y + shift),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
elif marker == "-":
|
||||||
|
shift = np.sqrt(s)
|
||||||
|
return svg.Path(
|
||||||
|
stroke=c,
|
||||||
|
stroke_width=np.sqrt(s) / 4,
|
||||||
|
d=[
|
||||||
|
svg.M(x - shift, y), # horizontal line
|
||||||
|
svg.L(x + shift, y),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
elif marker == "|":
|
||||||
|
shift = np.sqrt(s)
|
||||||
|
return svg.Path(
|
||||||
|
stroke=c,
|
||||||
|
stroke_width=np.sqrt(s) / 4,
|
||||||
|
d=[
|
||||||
|
svg.M(x, y - shift), # vertical line
|
||||||
|
svg.L(x, y + shift),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
elif marker == "s":
|
||||||
|
shift = np.sqrt(s) * 2
|
||||||
|
return svg.Rect(x=x, y=y, width=shift, height=shift, fill=c)
|
||||||
|
elif marker == "S":
|
||||||
|
s = np.sqrt(s) * 2
|
||||||
|
return svg.Rect(x=x, y=y, width=s, height=s, fill=c, rx=s / 3, ry=s / 3)
|
||||||
|
elif marker == "e":
|
||||||
|
s = np.sqrt(s * 2)
|
||||||
|
return svg.Ellipse(
|
||||||
|
cx=x,
|
||||||
|
cy=y,
|
||||||
|
fill=c,
|
||||||
|
rx=s,
|
||||||
|
ry=s / 2,
|
||||||
|
transform=[svg.Rotate(RGen.random() * 360, x, y)],
|
||||||
|
)
|
||||||
|
elif marker == "x":
|
||||||
|
shift = np.sqrt(s)
|
||||||
|
scale = np.sqrt(2) / 2
|
||||||
|
return svg.Path(
|
||||||
|
stroke=c,
|
||||||
|
stroke_width=np.sqrt(s) / 4,
|
||||||
|
stroke_linecap="round",
|
||||||
|
d=[
|
||||||
|
svg.M(x - shift * scale, y - shift * scale), # / line
|
||||||
|
svg.L(x + shift * scale, y + shift * scale),
|
||||||
|
svg.M(x - shift * scale, y + shift * scale),
|
||||||
|
svg.L(x + shift * scale, y - shift * scale), # / line
|
||||||
|
],
|
||||||
|
)
|
||||||
|
elif marker == "*":
|
||||||
|
shift = np.sqrt(s)
|
||||||
|
return [
|
||||||
|
svg.Path(
|
||||||
|
stroke=c,
|
||||||
|
stroke_width=np.sqrt(s) / 4,
|
||||||
|
stroke_linecap="round",
|
||||||
|
transform=[svg.Rotate(angle, x, y)],
|
||||||
|
d=[
|
||||||
|
svg.M(x - shift, y),
|
||||||
|
svg.L(x + shift, y),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
for angle in (0, 60, -60)
|
||||||
|
]
|
||||||
|
elif marker == "b":
|
||||||
|
return blob(x, y, c, np.sqrt(s)*10)
|
||||||
|
else:
|
||||||
|
return svg.Use(
|
||||||
|
href=f"#{marker}",
|
||||||
|
transform=[svg.Translate(x=x, y=y), svg.Scale(np.sqrt(s) / 100)],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/speckles/")
|
||||||
def make_wallpaper(
|
def make_wallpaper(
|
||||||
background: str,
|
speckle_colours: str,
|
||||||
speckle_colours: list[str],
|
density: float = 0.12,
|
||||||
filename: str | Path,
|
size: float = 3,
|
||||||
density: int = 0.6,
|
fileformat: str = "svg",
|
||||||
dimensions: tuple[float | int, float | int] = (1920, 1080),
|
orientation: str = "landscape",
|
||||||
) -> None:
|
filename: str = "",
|
||||||
x, y = dimensions
|
markers: str = ".",
|
||||||
speckles_per_colour = x / 100 * y / 100 * density
|
perlin: bool = True,
|
||||||
|
):
|
||||||
|
speckle_colours = speckle_colours.split(",")
|
||||||
|
background = speckle_colours.pop(0)
|
||||||
|
if orientation == "portrait":
|
||||||
|
x, y = (1080, 1920)
|
||||||
|
elif orientation == "landscape":
|
||||||
|
x, y = (1920, 1080)
|
||||||
|
elif "x" in orientation:
|
||||||
|
resolution = orientation.split("x")
|
||||||
|
if len(resolution) != 2:
|
||||||
|
logging.critical("input resolution has more or less than 2 dimensions")
|
||||||
|
return
|
||||||
|
x, y = resolution
|
||||||
|
if all([x.isdigit(), y.isdigit()]):
|
||||||
|
x, y = int(x), int(y)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
x, y = (1920, 1080)
|
||||||
|
speckles_per_colour = int(x / 128 * y / 128 * density / len(markers))
|
||||||
|
|
||||||
fig, ax = plt.subplots(figsize=(x / 100, y / 100), facecolor=background)
|
if perlin:
|
||||||
ax.set_facecolor(background)
|
gen = CoordsGenerator(x, y)
|
||||||
[spine.set_color(background) for spine in ax.spines.values()]
|
|
||||||
plt.xticks([])
|
|
||||||
plt.yticks([])
|
|
||||||
plt.margins(0, 0)
|
|
||||||
|
|
||||||
for color, size in itertools.product(
|
elements = [svg.Rect(width=x, height=y, fill=background)]
|
||||||
|
style = svg.Style(
|
||||||
|
text="\n".join(
|
||||||
|
[f".c{colour[1:]} {{ fill: {colour}; }}" for colour in speckle_colours]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elements.append(style)
|
||||||
|
style = svg.Style(
|
||||||
|
text="\n".join(
|
||||||
|
[
|
||||||
|
f'.s{hex(int(size*100))[2:]} {{ font-family: "OpenMoji"; font-size: {int(np.sqrt(size)*16)}px; }}'
|
||||||
|
for size in np.logspace(0, size, 10, base=np.exp(2))
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elements.append(style)
|
||||||
|
|
||||||
|
elements.append(
|
||||||
|
svg.Defs(
|
||||||
|
elements=[emoji(marker) for marker in markers if marker not in DEFINED]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for colour, marker, size in itertools.product(
|
||||||
speckle_colours,
|
speckle_colours,
|
||||||
np.logspace(1, 6, 8, base=2),
|
markers,
|
||||||
|
np.logspace(0, size, 10, base=np.exp(2)),
|
||||||
):
|
):
|
||||||
plt.scatter(
|
if perlin:
|
||||||
[random.random() * x / 8 for _ in range(speckles_per_colour)],
|
xs, ys = gen.pick(speckles_per_colour)
|
||||||
[random.random() * y / 8 for _ in range(speckles_per_colour)],
|
else:
|
||||||
c=color,
|
xs, ys = zip(
|
||||||
s=size,
|
RGen.random(speckles_per_colour) * x,
|
||||||
|
RGen.random(speckles_per_colour) * y,
|
||||||
|
)
|
||||||
|
elements.extend(
|
||||||
|
[markertoSVG(marker, x, y, colour, size) for x, y in zip(xs, ys)]
|
||||||
)
|
)
|
||||||
|
|
||||||
plt.tight_layout()
|
content = svg.SVG(width=x, height=y, elements=elements)
|
||||||
# plt.xlim(0, x)
|
if filename:
|
||||||
# plt.ylim(0, y)
|
if Path(filename).suffix == ".svg":
|
||||||
# plt.axis("off")
|
with open(filename, "wt") as f:
|
||||||
plt.savefig(
|
f.write(content.as_str())
|
||||||
filename,
|
else:
|
||||||
dpi=128,
|
with pyvips.Image.svgload_buffer(content.as_str().encode("utf-8")) as image:
|
||||||
bbox_inches="tight",
|
image.write_to_file(filename)
|
||||||
pad_inches=0,
|
return filename
|
||||||
)
|
elif fileformat:
|
||||||
# plt.show()
|
if fileformat == "svg":
|
||||||
plt.close()
|
buf = io.BytesIO(content.as_str().encode("utf-8"))
|
||||||
|
buf.seek(0)
|
||||||
|
elif fileformat in ["jpg", "png"]:
|
||||||
# palette = random.choice(palettes)
|
with pyvips.Image.svgload_buffer(content.as_str().encode("utf-8")) as image:
|
||||||
def speckles(palette):
|
data = image.write_to_buffer("." + fileformat)
|
||||||
for i, background in enumerate(palette):
|
buf = io.BytesIO(data)
|
||||||
speckle_colours = palette[:i] + palette[i + 1 :]
|
buf.seek(0)
|
||||||
_id = "_".join(speckle_colours).replace("#", "")
|
return StreamingResponse(content=buf, media_type=MEDIA_TYPES[fileformat])
|
||||||
speckle_colours += ["#000000", "#000000", "#ffffff"]
|
|
||||||
make_wallpaper(
|
|
||||||
background,
|
|
||||||
speckle_colours,
|
|
||||||
f"speckles/speckles_{_id}.png",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def night_sky(palette):
|
|
||||||
speckle_colours = palette + ["#ffffff"]
|
|
||||||
_id = "_".join(palette).replace("#", "")
|
|
||||||
make_wallpaper(
|
|
||||||
"#000000",
|
|
||||||
speckle_colours,
|
|
||||||
f"speckles/night_sky_{_id}.png",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
palettes = [
|
uvicorn.run("speckles:app", workers=3, port=8099)
|
||||||
{"colours": ["#eee4ab", "#e5cb9f", "#99c4c8", "#68a7ad"], "votes": 100000},
|
|
||||||
{"colours": ["#e4d192", "#cba0ae", "#af7ab3", "#80558c"], "votes": 100000},
|
|
||||||
{"colours": ["#eeeeee", "#e1d4bb", "#cbb279", "#537188"], "votes": 100000},
|
|
||||||
{"colours": ["#c0dbea", "#ba90c6", "#e8a0bf", "#fdf4f5"], "votes": 100000},
|
|
||||||
{"colours": ["#f5ffc9", "#b3e5be", "#a86464", "#804674"], "votes": 100000},
|
|
||||||
{"colours": ["#ffde7d", "#f6416c", "#f8f3d4", "#00b8a9"], "votes": 100000},
|
|
||||||
{"colours": ["#53354a", "#903749", "#e84545", "#2b2e4a"], "votes": 100000},
|
|
||||||
{"colours": ["#967e76", "#d7c0ae", "#eee3cb", "#b7c4cf"], "votes": 100000},
|
|
||||||
{"colours": ["#fc5185", "#f5f5f5", "#3fc1c9", "#364f6b"], "votes": 100000},
|
|
||||||
{"colours": ["#eaeaea", "#ff2e63", "#252a34", "#08d9d6"], "votes": 100000},
|
|
||||||
{"colours": ["#eeeeee", "#00adb5", "#393e46", "#222831"], "votes": 100000},
|
|
||||||
{"colours": ["#2cd3e1", "#a459d1", "#f266ab", "#ffb84c"], "votes": 100000},
|
|
||||||
{"colours": ["#ffe194", "#e8f6ef", "#1b9c85", "#4c4c6d"], "votes": 100000},
|
|
||||||
{"colours": ["#146c94", "#19a7ce", "#b0daff", "#feff86"], "votes": 100000},
|
|
||||||
{"colours": ["#4c3d3d", "#c07f00", "#ffd95a", "#fff7d4"], "votes": 100000},
|
|
||||||
{"colours": ["#8bacaa", "#b04759", "#e76161", "#f99b7d"], "votes": 100000},
|
|
||||||
{"colours": ["#146c94", "#19a7ce", "#afd3e2", "#f6f1f1"], "votes": 100000},
|
|
||||||
{"colours": ["#9ba4b5", "#212a3e", "#394867", "#f1f6f9"], "votes": 100000},
|
|
||||||
{"colours": ["#00ffca", "#05bfdb", "#088395", "#0a4d68"], "votes": 100000},
|
|
||||||
{"colours": ["#7c9070", "#9ca777", "#fee8b0", "#f97b22"], "votes": 100000},
|
|
||||||
{"colours": ["#002a19", "#000000", "#ffffff", "#e0512f"], "votes": 100000},
|
|
||||||
]
|
|
||||||
|
|
||||||
palette_files = ["colorhunt.json"]
|
|
||||||
for palette_file in palette_files:
|
|
||||||
with open(palette_file, "rt") as f:
|
|
||||||
palettes += json.load(f)
|
|
||||||
|
|
||||||
print(len(palettes))
|
|
||||||
palettes = [
|
|
||||||
palette["colours"]
|
|
||||||
for palette in palettes
|
|
||||||
if palette["votes"] > 30000 or len(palette["colours"]) > 5
|
|
||||||
]
|
|
||||||
print(len(palettes))
|
|
||||||
for palette in palettes:
|
|
||||||
night_sky(palette)
|
|
||||||
speckles(palette)
|
|
||||||
|
Reference in New Issue
Block a user