Compare commits

...

23 Commits

Author SHA1 Message Date
b97737f670 add blobs 2024-10-27 21:43:34 +01:00
651473975f new scheme 2024-10-27 20:46:54 +01:00
050c1da950 fix: background colour 2024-10-27 20:46:39 +01:00
3220b6aa64 jpg and png works 2024-10-27 20:21:22 +01:00
b4e70c2245 fix: dot needed before suffix 2024-10-27 20:12:44 +01:00
94489b5da2 enable other image formats API 2024-10-27 20:09:57 +01:00
beb81771a8 as API 2024-10-27 19:46:19 +01:00
0ee3e39c3c save as different image formats (pyvips) 2024-10-27 19:28:02 +01:00
9f5bb724cb add posibility for including emojis 2024-10-27 16:42:02 +01:00
62e7aa0e07 working with some emojis and card game characters 2024-10-25 22:54:46 +02:00
dfaef09ccb add filename option 2024-10-21 13:32:10 +02:00
dc3503a407 add Perlin noise as distribution 2024-08-22 15:52:32 +02:00
ba0834d729 add custom markers 2024-08-14 14:19:48 +00:00
93e4e9453f format 2024-08-14 15:28:49 +02:00
06cf912490 add more marker symbols 2024-08-14 13:27:35 +00:00
cbaf9bc071 add custom symbols 2024-08-14 14:14:41 +02:00
eaa2541596 feat: markers + adjusted density 2024-08-14 12:55:22 +02:00
5941ed7909 feat: choose marker 2024-08-13 16:14:29 +02:00
juvilius
01fb94c9eb add some popular colourschemes 2023-07-24 16:22:55 +02:00
juvilius
bfd1ecabeb background local helper script 2023-07-24 16:18:41 +02:00
b8011837e9 orientation option 2023-07-10 07:14:05 +00:00
c8444ed3b6 Density option 2023-06-14 09:05:25 +00:00
juvilius
d00fe9c315 Remove possibly licensed palettes 2023-05-18 16:36:28 +02:00
7 changed files with 661 additions and 1210 deletions

48
background.py Normal file
View 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
View 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)],
)

File diff suppressed because it is too large Load Diff

176
main.py Normal file
View File

@@ -0,0 +1,176 @@
import io
from perlin import CoordsGenerator
from typing import Literal
import itertools
import logging
import random
import matplotlib.pyplot as plt
import numpy as np
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
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=["*"],
)
MEDIA_TYPES = {
"png": "image/png",
"jpeg": "image/jpeg",
"jpg": "image/jpeg",
"svg": "image/svg+xml",
"pdf": "application/pdf",
}
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/")
def make_wallpaper(
speckle_colours: str,
density: float = 0.12,
size: float = 3,
fileformat: str = "svg",
orientation: str = "landscape",
filename: str = "",
markers: str = ".",
perlin: bool = True,
):
if fileformat not in MEDIA_TYPES:
return
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 / 120, y / 120), facecolor=background)
ax.set_facecolor(background)
[spine.set_color(background) for spine in ax.spines.values()]
ax.set_xticks([])
ax.set_yticks([])
ax.margins(0, 0)
if perlin:
gen = CoordsGenerator(y, x)
for color, marker, size in itertools.product(
speckle_colours,
markers,
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(
x_coords,
y_coords,
c=color,
s=size,
marker=marker,
)
fig.tight_layout()
# plt.xlim(0, x)
# plt.ylim(0, y)
# plt.axis("off")
buf = io.BytesIO()
fig.savefig(
buf,
format=fileformat,
dpi=128,
bbox_inches="tight",
pad_inches=0,
)
buf.seek(0)
if filename:
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__":
uvicorn.run("main:app", workers=3, port=8099, reload=True)

85
perlin.py Normal file
View 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
View 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}]

View File

@@ -1,109 +1,311 @@
import itertools
import json
import random
from collections import Counter
from datetime import datetime
from blob import blob
import io
from fastapi.responses import StreamingResponse
from pathlib import Path
import pyvips
import logging
from perlin import CoordsGenerator
import svg
import matplotlib.pyplot as plt
import numpy as np
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
palettes = [
{ "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 },
app = FastAPI(title="Speckles API", root_path="/images")
origins = [
"http://localhost",
"http://localhost:3000",
"https://localhost",
"https://0124816.xyz",
"http://0124816.xyz:3001",
]
palette_files = ["colorhunt.json", "coolors.json"]
for palette_file in palette_files:
with open(palette_file, "rt") as f:
palettes += json.load(f)
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(
background: str,
speckle_colours: list[str],
filename: str | Path,
density: int = 20,
dimensions: tuple[float | int, float | int] = (1920, 1080),
) -> None:
x, y = dimensions
fig, ax = plt.subplots(figsize=(x / 100, y / 100), facecolor=background)
ax.set_facecolor(background)
[spine.set_color(background) for spine in ax.spines.values()]
plt.xticks([])
plt.yticks([])
plt.margins(0, 0)
speckle_colours: str,
density: float = 0.12,
size: float = 3,
fileformat: str = "svg",
orientation: str = "landscape",
filename: str = "",
markers: str = ".",
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))
for color, size in itertools.product(
if perlin:
gen = CoordsGenerator(x, y)
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,
np.logspace(1, 6, 8, base=2),
markers,
np.logspace(0, size, 10, base=np.exp(2)),
):
plt.scatter(
[random.random() * x / 8 for _ in range(density)],
[random.random() * y / 8 for _ in range(density)],
c=color,
s=size,
if perlin:
xs, ys = gen.pick(speckles_per_colour)
else:
xs, ys = zip(
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()
# plt.xlim(0, x)
# plt.ylim(0, y)
# plt.axis("off")
plt.savefig(
filename,
dpi=128,
bbox_inches="tight",
pad_inches=0,
)
# plt.show()
plt.close()
content = svg.SVG(width=x, height=y, elements=elements)
if filename:
if Path(filename).suffix == ".svg":
with open(filename, "wt") as f:
f.write(content.as_str())
else:
with pyvips.Image.svgload_buffer(content.as_str().encode("utf-8")) as image:
image.write_to_file(filename)
return filename
elif fileformat:
if fileformat == "svg":
buf = io.BytesIO(content.as_str().encode("utf-8"))
buf.seek(0)
elif fileformat in ["jpg", "png"]:
with pyvips.Image.svgload_buffer(content.as_str().encode("utf-8")) as image:
data = image.write_to_buffer("." + fileformat)
buf = io.BytesIO(data)
buf.seek(0)
return StreamingResponse(content=buf, media_type=MEDIA_TYPES[fileformat])
# palette = random.choice(palettes)
def speckles(palette):
for i, background in enumerate(palette):
speckle_colours = palette[:i] + palette[i + 1 :]
_id = "_".join(speckle_colours).replace("#", "")
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",
)
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)
if __name__ == "__main__":
uvicorn.run("speckles:app", workers=3, port=8099)