Compare commits

...

10 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
4 changed files with 371 additions and 107 deletions

View File

@@ -1,9 +1,10 @@
import random
import json import json
import random
from main import make_wallpaper from speckles import make_wallpaper
palettes = [ palettes = [
{"colours": ["#DE4A21", "#6F04F2", "#490B1C", "#4a9c9e"], "votes": 1000000},
{"colours": ["#eee4ab", "#e5cb9f", "#99c4c8", "#68a7ad"], "votes": 100000}, {"colours": ["#eee4ab", "#e5cb9f", "#99c4c8", "#68a7ad"], "votes": 100000},
{"colours": ["#e4d192", "#cba0ae", "#af7ab3", "#80558c"], "votes": 100000}, {"colours": ["#e4d192", "#cba0ae", "#af7ab3", "#80558c"], "votes": 100000},
{"colours": ["#eeeeee", "#e1d4bb", "#cbb279", "#537188"], "votes": 100000}, {"colours": ["#eeeeee", "#e1d4bb", "#cbb279", "#537188"], "votes": 100000},
@@ -25,15 +26,23 @@ palettes = [
{"colours": ["#00ffca", "#05bfdb", "#088395", "#0a4d68"], "votes": 100000}, {"colours": ["#00ffca", "#05bfdb", "#088395", "#0a4d68"], "votes": 100000},
{"colours": ["#7c9070", "#9ca777", "#fee8b0", "#f97b22"], "votes": 100000}, {"colours": ["#7c9070", "#9ca777", "#fee8b0", "#f97b22"], "votes": 100000},
{"colours": ["#002a19", "#000000", "#ffffff", "#e0512f"], "votes": 100000}, {"colours": ["#002a19", "#000000", "#ffffff", "#e0512f"], "votes": 100000},
{"colours": ["#212248", "#EBAE36", "#C7C9F0", "#11088A"], "votes": 100000},
] ]
with open("popular.json", "r") as f: with open("popular.json", "r") as f:
palettes += json.load(f) palettes += json.load(f)
palette = random.choice(palettes) palette = random.choice(palettes)
colours = ["black"] + palette["colours"] enabled_markers = "otY+xP*b"
with open("current_speckles.png", "wb") as f: 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( b = make_wallpaper(
",".join(colours), fileformat="png", size=2.1, density=0.3, local=True ",".join(colours),
fileformat="png",
size=2.1,
density=0.2,
filename=f"wallpapers/speckles{i}_2880x1800.png",
perlin=True,
markers=enabled_markers,
orientation="2880x1800",
) )
# for body in b.body_iterator:
# print(body)

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)],
)

View File

@@ -65,9 +65,10 @@ class CoordsGenerator:
x, y = self.noise.shape x, y = self.noise.shape
x = np.random.choice(x, size=n, replace=False) x = np.random.choice(x, size=n, replace=False)
y = [ y = [
np.random.choice(y, size=1, p=self.noise_distribution[x_, :], replace=False) for x_ in x np.random.choice(y, size=1, p=self.noise_distribution[x_, :], replace=False)
for x_ in x
] ]
return x, y return x, np.array(y).flatten()
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -1,114 +1,311 @@
import itertools import itertools
import json from blob import blob
import random import io
from fastapi.responses import StreamingResponse
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,
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)
for color, size in itertools.product(
speckle_colours,
np.logspace(1, 6, 8, base=2),
): ):
plt.scatter( speckle_colours = speckle_colours.split(",")
[random.random() * x / 8 for _ in range(speckles_per_colour)], background = speckle_colours.pop(0)
[random.random() * y / 8 for _ in range(speckles_per_colour)], if orientation == "portrait":
c=color, x, y = (1080, 1920)
s=size, 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))
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]
)
) )
plt.tight_layout() for colour, marker, size in itertools.product(
# 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()
# 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, speckle_colours,
f"speckles/speckles_{_id}.png", markers,
np.logspace(0, size, 10, base=np.exp(2)),
):
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)]
) )
content = svg.SVG(width=x, height=y, elements=elements)
def night_sky(palette): if filename:
speckle_colours = palette + ["#ffffff"] if Path(filename).suffix == ".svg":
_id = "_".join(palette).replace("#", "") with open(filename, "wt") as f:
make_wallpaper( f.write(content.as_str())
"#000000", else:
speckle_colours, with pyvips.Image.svgload_buffer(content.as_str().encode("utf-8")) as image:
f"speckles/night_sky_{_id}.png", 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])
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)