mirror of
https://github.com/OpenVGLab/OmniLottie.git
synced 2026-09-16 23:26:26 +00:00
276 lines
9.4 KiB
Python
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)
|