Files
2026-03-01 21:36:54 +08:00

276 lines
9.4 KiB
Python

from PIL import Image
from .. import objects
from .. import NVector, Color
from ..utils import color
class Polygen:
def __init__(self, x, y):
self.vertices = [
NVector(x, y),
NVector(x+1, y),
NVector(x+1, y+1),
NVector(x, y+1),
]
self._has_x = False
self._has_y = False
def add_pixel_x(self, x, y):
i = self.vertices.index(NVector(x, y))
if len(self.vertices) > i and self.vertices[i+1] == NVector(x, y+1):
self._has_x = True
self.vertices.insert(i+1, NVector(x+1, y))
self.vertices.insert(i+2, NVector(x+1, y+1))
else:
raise ValueError()
def add_pixel_x_neg(self, x, y):
i = self.vertices.index(NVector(x+1, y))
if i > 0 and self.vertices[i-1] == NVector(x+1, y+1):
self._has_x = True
self.vertices.insert(i, NVector(x, y))
self.vertices.insert(i, NVector(x, y+1))
else:
raise ValueError()
def add_pixel_y(self, x, y):
i = self.vertices.index(NVector(x, y))
if i > 0 and self.vertices[i-1] == NVector(x+1, y):
self._has_y = True
if i > 1 and self.vertices[i-2] == NVector(x+1, y+1):
self.vertices[i-1] = NVector(x, y+1)
else:
self.vertices.insert(i, NVector(x, y+1))
self.vertices.insert(i, NVector(x+1, y+1))
else:
raise ValueError()
def _to_rect(self, id1, id2):
p1 = self.vertices[id1]
p2 = self.vertices[id2]
return objects.Rect((p1+p2)/2, p2-p1)
def to_shape(self):
if not self._has_x or not self._has_y:
return self._to_rect(0, int(len(self.vertices)/2))
bez = objects.Bezier()
bez.closed = True
for point in self.vertices:
if len(bez.vertices) > 1 and (
bez.vertices[-1].x == bez.vertices[-2].x == point.x or
bez.vertices[-1].y == bez.vertices[-2].y == point.y
):
bez.vertices[-1] = point
else:
bez.add_point(point)
if len(bez.vertices) > 2 and bez.vertices[0].x == bez.vertices[-1].x == bez.vertices[-2].x:
bez.vertices.pop()
bez.out_tangents.pop()
bez.in_tangents.pop()
return objects.Path(bez)
def pixel_add_layer_paths(animation, raster):
layer = animation.add_layer(objects.ShapeLayer())
groups = {}
processed = set()
xneg_candidates = set()
def avail(x, y):
rid = (x, y)
return not (
x < 0 or x >= raster.width or y >= raster.height or
rid in processed or raster.getpixel(rid) != colort
)
def recurse(gen, x, y, xneg):
processed.add((x, y))
if avail(x+1, y):
gen.add_pixel_x(x+1, y)
recurse(gen, x+1, y, False)
if avail(x, y+1):
gen.add_pixel_y(x, y+1)
recurse(gen, x, y+1, True)
if xneg and avail(x-1, y):
xneg_candidates.add((x-1, y))
for y in range(raster.height):
for x in range(raster.width):
pid = (x, y)
colort = raster.getpixel(pid)
if colort[-1] == 0 or pid in processed:
continue
gen = Polygen(x, y)
xneg_candidates = set()
recurse(gen, x, y, False)
xneg_candidates -= processed
while xneg_candidates:
p = next(iter(sorted(xneg_candidates, key=lambda t: (t[1], t[0]))))
gen.add_pixel_x_neg(*p)
recurse(gen, p[0], p[1], True)
processed.add(p)
xneg_candidates -= processed
g = groups.setdefault(colort, set())
g.add(gen.to_shape())
for colort, rects in groups.items():
g = layer.add_shape(objects.Group())
g.shapes = list(rects) + g.shapes
g.name = "".join("%02x" % c for c in colort)
fill = g.add_shape(objects.Fill())
fill.color.value = color.from_uint8(*colort[:3])
fill.opacity.value = colort[-1] / 255 * 100
stroke = g.add_shape(objects.Stroke(fill.color.value, 0.1))
stroke.opacity.value = fill.opacity.value
return layer
def pixel_add_layer_rects(animation, raster):
layer = animation.add_layer(objects.ShapeLayer())
last_rects = {}
groups = {}
def merge_up():
if last_rect and last_rect._start in last_rects:
yrect = last_rects[last_rect._start]
if yrect.size.value.x == last_rect.size.value.x and yrect._color == last_rect._color:
groups[last_rect._color].remove(last_rect)
yrect.position.value.y += 0.5
yrect.size.value.y += 1
rects[last_rect._start] = yrect
def group(colort):
return groups.setdefault(colort, set())
for y in range(raster.height):
rects = {}
last_color = None
last_rect = None
for x in range(raster.width):
colort = raster.getpixel((x, y))
if colort[-1] == 0:
continue
yrect = last_rects.get(x, None)
if colort == last_color:
last_rect.position.value.x += 0.5
last_rect.size.value.x += 1
elif yrect and colort == yrect._color and yrect.size.value.x == 1:
yrect.position.value.y += 0.5
yrect.size.value.y += 1
rects[x] = yrect
last_color = last_rect = colort = None
else:
merge_up()
g = group(colort)
last_rect = objects.Rect()
g.add(last_rect)
last_rect.size.value = NVector(1, 1)
last_rect.position.value = NVector(x + 0.5, y + 0.5)
rects[x] = last_rect
last_rect._start = x
last_rect._color = colort
last_color = colort
merge_up()
last_rects = rects
for colort, rects in groups.items():
g = layer.add_shape(objects.Group())
g.shapes = list(rects) + g.shapes
g.name = "".join("%02x" % c for c in colort)
fill = g.add_shape(objects.Fill())
fill.color.value = color.from_uint8(*colort[:3])
fill.opacity.value = colort[-1] / 255 * 100
stroke = g.add_shape(objects.Stroke(fill.color.value, 0.1))
stroke.opacity.value = fill.opacity.value
return layer
def _vectorizing_func(filenames, frame_delay, framerate, callback):
if not isinstance(filenames, list):
filenames = [filenames]
animation = objects.Animation(0, framerate)
nframes = 0
for filename in filenames:
raster = Image.open(filename)
if nframes == 0:
animation.width = raster.width
animation.height = raster.height
if not hasattr(raster, "is_animated"):
raster.n_frames = 1
raster.seek = lambda x: None
for frame in range(raster.n_frames):
raster.seek(frame)
new_im = Image.new("RGBA", raster.size)
new_im.paste(raster)
callback(animation, new_im, nframes + frame)
new_im.close()
nframes += raster.n_frames
animation.out_point = frame_delay * nframes
#animation._nframes = nframes
return animation
def raster_to_embedded_assets(filenames, frame_delay=1, framerate=60, embed_format=None):
"""!
@brief Loads external assets
"""
def callback(animation, raster, frame):
asset = objects.assets.Image.embedded(raster, embed_format)
animation.assets.append(asset)
layer = animation.add_layer(objects.ImageLayer(asset.id))
layer.in_point = frame * frame_delay
layer.out_point = layer.in_point + frame_delay
return _vectorizing_func(filenames, frame_delay, framerate, callback)
def raster_to_linked_assets(filenames, frame_delay=1, framerate=60):
"""!
@brief Loads external assets
"""
animation = objects.Animation(frame_delay * len(filenames), framerate)
for frame, filename in enumerate(filenames):
asset = objects.assets.Image.linked(filename)
animation.assets.append(asset)
layer = animation.add_layer(objects.ImageLayer(asset.id))
layer.in_point = frame * frame_delay
layer.out_point = layer.in_point + frame_delay
return animation
def pixel_to_animation(filenames, frame_delay=1, framerate=60):
"""!
@brief Converts pixel art to vector
"""
def callback(animation, raster, frame):
layer = pixel_add_layer_rects(animation, raster.convert("RGBA"))
layer.in_point = frame * frame_delay
layer.out_point = layer.in_point + frame_delay
return _vectorizing_func(filenames, frame_delay, framerate, callback)
def pixel_to_animation_paths(filenames, frame_delay=1, framerate=60):
"""!
@brief Converts pixel art to vector paths
Slower and yields larger files compared to pixel_to_animation,
but it produces a single shape for each area with the same color.
Mostly useful when you want to add your own animations to the loaded image
"""
def callback(animation, raster, frame):
layer = pixel_add_layer_paths(animation, raster.convert("RGBA"))
layer.in_point = frame * frame_delay
layer.out_point = layer.in_point + frame_delay
return _vectorizing_func(filenames, frame_delay, framerate, callback)