mirror of
https://github.com/OpenVGLab/OmniLottie.git
synced 2026-09-17 07:36:27 +00:00
Initial Commit
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
__all__ = ["animation", "ellipse", "ik", "linediff", "restructure", "script", "stripper"]
|
||||
|
||||
try:
|
||||
from . import font
|
||||
__all__ += ["font"]
|
||||
except ImportError:
|
||||
pass
|
||||
@@ -0,0 +1,544 @@
|
||||
import random
|
||||
import math
|
||||
from ..nvector import NVector
|
||||
from ..objects.shapes import Path
|
||||
from .. import objects
|
||||
from ..objects import easing
|
||||
from ..objects import properties
|
||||
|
||||
|
||||
def shake(position_prop, x_radius, y_radius, start_time, end_time, n_frames, interp=easing.Linear()):
|
||||
if not isinstance(position_prop, list):
|
||||
position_prop = [position_prop]
|
||||
|
||||
n_frames = int(round(n_frames))
|
||||
frame_time = (end_time - start_time) / n_frames
|
||||
startpoints = list(map(
|
||||
lambda pp: pp.get_value(start_time),
|
||||
position_prop
|
||||
))
|
||||
|
||||
for i in range(n_frames):
|
||||
x = (random.random() * 2 - 1) * x_radius
|
||||
y = (random.random() * 2 - 1) * y_radius
|
||||
for pp, start in zip(position_prop, startpoints):
|
||||
px = start[0] + x
|
||||
py = start[1] + y
|
||||
pp.add_keyframe(start_time + i * frame_time, NVector(px, py), interp)
|
||||
|
||||
for pp, start in zip(position_prop, startpoints):
|
||||
pp.add_keyframe(end_time, start, interp)
|
||||
|
||||
|
||||
def rot_shake(rotation_prop, angles, start_time, end_time, n_frames):
|
||||
frame_time = (end_time - start_time) / n_frames
|
||||
start = rotation_prop.get_value(start_time)
|
||||
|
||||
for i in range(0, n_frames):
|
||||
a = angles[i % len(angles)] * math.sin(i/n_frames * math.pi)
|
||||
rotation_prop.add_keyframe(start_time + i * frame_time, start + a)
|
||||
rotation_prop.add_keyframe(end_time, start)
|
||||
|
||||
|
||||
def spring_pull(position_prop, point, start_time, end_time, falloff=15, oscillations=7):
|
||||
start = position_prop.get_value(start_time)
|
||||
d = start-point
|
||||
|
||||
delta = (end_time - start_time) / oscillations
|
||||
|
||||
for i in range(oscillations):
|
||||
time_x = i / oscillations
|
||||
factor = math.cos(time_x * math.pi * oscillations) * (1-time_x**(1/falloff))
|
||||
p = point + d * factor
|
||||
position_prop.add_keyframe(start_time + delta * i, p)
|
||||
|
||||
position_prop.add_keyframe(end_time, point)
|
||||
|
||||
|
||||
def follow_path(position_prop, bezier, start_time, end_time, n_keyframes,
|
||||
reverse=False, offset=NVector(0, 0), start_t=0, rotation_prop=None, rotation_offset=0):
|
||||
delta = (end_time - start_time) / (n_keyframes-1)
|
||||
fact = start_t
|
||||
factd = 1 / (n_keyframes-1)
|
||||
|
||||
if rotation_prop:
|
||||
start_rot = rotation_prop.get_value(start_time) if rotation_offset is None else rotation_offset
|
||||
|
||||
for i in range(n_keyframes):
|
||||
time = start_time + i * delta
|
||||
|
||||
if fact > 1 + factd/2:
|
||||
fact -= 1
|
||||
if time != start_time:
|
||||
easing.Jump()(position_prop.keyframes[-1])
|
||||
if rotation_prop:
|
||||
easing.Jump()(rotation_prop.keyframes[-1])
|
||||
|
||||
f = 1 - fact if reverse else fact
|
||||
position_prop.add_keyframe(time, bezier.point_at(f)+offset)
|
||||
|
||||
if rotation_prop:
|
||||
rotation_prop.add_keyframe(time, bezier.tangent_angle_at(f) / math.pi * 180 + start_rot)
|
||||
|
||||
fact += factd
|
||||
|
||||
|
||||
def generate_path_appear(bezier, appear_start, appear_end, n_keyframes, reverse=False):
|
||||
obj = Path()
|
||||
beziers = []
|
||||
maxp = 0
|
||||
|
||||
time_delta = (appear_end - appear_start) / n_keyframes
|
||||
for i in range(n_keyframes+1):
|
||||
time = appear_start + i * time_delta
|
||||
t2 = (time - appear_start) / (appear_end - appear_start)
|
||||
|
||||
if reverse:
|
||||
t2 = 1 - t2
|
||||
segment = bezier.segment(t2, 1)
|
||||
segment.reverse()
|
||||
else:
|
||||
segment = bezier.segment(0, t2)
|
||||
|
||||
beziers.append(segment)
|
||||
if len(segment.vertices) > maxp:
|
||||
maxp = len(segment.vertices)
|
||||
|
||||
obj.shape.add_keyframe(time, segment)
|
||||
|
||||
for segment in beziers:
|
||||
deltap = maxp - len(segment.vertices)
|
||||
if deltap > 0:
|
||||
segment.vertices += [segment.vertices[-1]] * deltap
|
||||
segment.in_tangents += [NVector(0, 0)] * deltap
|
||||
segment.out_tangents += [NVector(0, 0)] * deltap
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
def generate_path_disappear(bezier, disappear_start, disappear_end, n_keyframes, reverse=False):
|
||||
obj = Path()
|
||||
beziers = []
|
||||
maxp = 0
|
||||
|
||||
time_delta = (disappear_end - disappear_start) / n_keyframes
|
||||
for i in range(n_keyframes+1):
|
||||
time = disappear_start + i * time_delta
|
||||
t1 = (time - disappear_start) / (disappear_end - disappear_start)
|
||||
if reverse:
|
||||
t1 = 1 - t1
|
||||
segment = bezier.segment(0, t1)
|
||||
else:
|
||||
segment = bezier.segment(1, t1)
|
||||
segment.reverse()
|
||||
|
||||
beziers.append(segment)
|
||||
if len(segment.vertices) > maxp:
|
||||
maxp = len(segment.vertices)
|
||||
|
||||
obj.shape.add_keyframe(time, segment)
|
||||
|
||||
for segment in beziers:
|
||||
deltap = maxp - len(segment.vertices)
|
||||
if deltap > 0:
|
||||
segment.vertices += [segment.vertices[-1]] * deltap
|
||||
segment.in_tangents += [NVector(0, 0)] * deltap
|
||||
segment.out_tangents += [NVector(0, 0)] * deltap
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
def generate_path_segment(bezier, appear_start, appear_end, disappear_start, disappear_end, n_keyframes, reverse=False):
|
||||
obj = Path()
|
||||
beziers = []
|
||||
maxp = 0
|
||||
|
||||
# HACK: For some reson reversed works better
|
||||
if not reverse:
|
||||
bezier.reverse()
|
||||
|
||||
time_delta = (appear_end - appear_start) / n_keyframes
|
||||
for i in range(n_keyframes+1):
|
||||
time = appear_start + i * time_delta
|
||||
t1 = (time - disappear_start) / (disappear_end - disappear_start)
|
||||
t2 = (time - appear_start) / (appear_end - appear_start)
|
||||
|
||||
t1 = max(0, min(1, t1))
|
||||
t2 = max(0, min(1, t2))
|
||||
|
||||
#if reverse:
|
||||
if True:
|
||||
t1 = 1 - t1
|
||||
t2 = 1 - t2
|
||||
segment = bezier.segment(t2, t1)
|
||||
segment.reverse()
|
||||
#else:
|
||||
#segment = bezier.segment(t1, t2)
|
||||
#segment.reverse()
|
||||
|
||||
beziers.append(segment)
|
||||
if len(segment.vertices) > maxp:
|
||||
maxp = len(segment.vertices)
|
||||
|
||||
obj.shape.add_keyframe(time, segment)
|
||||
|
||||
for segment in beziers:
|
||||
deltap = maxp - len(segment.vertices)
|
||||
if deltap > 0:
|
||||
segment.split_self_chunks(deltap+1)
|
||||
|
||||
# HACK: Restore
|
||||
if not reverse:
|
||||
bezier.reverse()
|
||||
return obj
|
||||
|
||||
|
||||
class PointDisplacer:
|
||||
def __init__(self, time_start, time_end, n_frames):
|
||||
"""!
|
||||
@param time_start When the animation shall start
|
||||
@param time_end When the animation shall end
|
||||
@param n_frames Number of frames in the animation
|
||||
"""
|
||||
## When the animation shall start
|
||||
self.time_start = time_start
|
||||
## When the animation shall end
|
||||
self.time_end = time_end
|
||||
## Number of frames in the animation
|
||||
self.n_frames = n_frames
|
||||
## Length of a frame
|
||||
self.time_delta = (time_end - time_start) / n_frames
|
||||
|
||||
def animate_point(self, prop):
|
||||
startpos = prop.get_value(self.time_start)
|
||||
for f in range(self.n_frames+1):
|
||||
p = self._on_displace(startpos, f)
|
||||
prop.add_keyframe(self.frame_time(f), startpos+p)
|
||||
|
||||
def _on_displace(self, startpos, f):
|
||||
raise NotImplementedError()
|
||||
|
||||
def animate_bezier(self, prop):
|
||||
initial = prop.get_value(self.time_start)
|
||||
|
||||
for f in range(self.n_frames+1):
|
||||
bezier = objects.Bezier()
|
||||
bezier.closed = initial.closed
|
||||
|
||||
for pi in range(len(initial.vertices)):
|
||||
startpos = initial.vertices[pi]
|
||||
dp = self._on_displace(startpos, f)
|
||||
t1sp = initial.in_tangents[pi] + startpos
|
||||
t1fin = initial.in_tangents[pi] + self._on_displace(t1sp, f) - dp
|
||||
t2sp = initial.out_tangents[pi] + startpos
|
||||
t2fin = initial.out_tangents[pi] + self._on_displace(t2sp, f) - dp
|
||||
|
||||
bezier.add_point(dp + startpos, t1fin, t2fin)
|
||||
|
||||
prop.add_keyframe(self.frame_time(f), bezier)
|
||||
|
||||
def frame_time(self, f):
|
||||
return f * self.time_delta + self.time_start
|
||||
|
||||
def _init_lerp(self, val_from, val_to, easing):
|
||||
self._kf = properties.OffsetKeyframe(0, NVector(val_from), NVector(val_to), easing)
|
||||
|
||||
def _lerp_get(self, offset):
|
||||
return self._kf.interpolated_value(offset / self.n_frames)[0]
|
||||
|
||||
|
||||
class SineDisplacer(PointDisplacer):
|
||||
def __init__(
|
||||
self,
|
||||
wavelength,
|
||||
amplitude,
|
||||
time_start,
|
||||
time_end,
|
||||
n_frames,
|
||||
speed=1,
|
||||
axis=90,
|
||||
):
|
||||
"""!
|
||||
Displaces points as if they were following a sine wave
|
||||
|
||||
@param wavelength Distance between consecutive peaks
|
||||
@param amplitude Distance from a peak to the original position
|
||||
@param time_start When the animation shall start
|
||||
@param time_end When the animation shall end
|
||||
@param n_frames Number of keyframes to add
|
||||
@param speed Number of peaks a point will go through in the given time
|
||||
If negative, it will go the other way
|
||||
@param axis Wave peak direction
|
||||
"""
|
||||
super().__init__(time_start, time_end, n_frames)
|
||||
|
||||
self.wavelength = wavelength
|
||||
self.amplitude = amplitude
|
||||
self.speed_f = math.pi * 2 * speed
|
||||
self.axis = axis / 180 * math.pi
|
||||
|
||||
def _on_displace(self, startpos, f):
|
||||
off = -math.sin(startpos[0]/self.wavelength*math.pi*2-f*self.speed_f/self.n_frames) * self.amplitude
|
||||
return NVector(off * math.cos(self.axis), off * math.sin(self.axis))
|
||||
|
||||
|
||||
class MultiSineDisplacer(PointDisplacer):
|
||||
def __init__(
|
||||
self,
|
||||
waves,
|
||||
time_start,
|
||||
time_end,
|
||||
n_frames,
|
||||
speed=1,
|
||||
axis=90,
|
||||
amplitude_scale=1,
|
||||
):
|
||||
"""!
|
||||
Displaces points as if they were following a sine wave
|
||||
|
||||
@param waves List of tuples (wavelength, amplitude)
|
||||
@param time_start When the animation shall start
|
||||
@param time_end When the animation shall end
|
||||
@param n_frames Number of keyframes to add
|
||||
@param speed Number of peaks a point will go through in the given time
|
||||
If negative, it will go the other way
|
||||
@param axis Wave peak direction
|
||||
@param amplitude_scale Multiplies the resulting amplitude by this factor
|
||||
"""
|
||||
super().__init__(time_start, time_end, n_frames)
|
||||
|
||||
self.waves = waves
|
||||
self.speed_f = math.pi * 2 * speed
|
||||
self.axis = axis / 180 * math.pi
|
||||
self.amplitude_scale = amplitude_scale
|
||||
|
||||
def _on_displace(self, startpos, f):
|
||||
off = 0
|
||||
for wavelength, amplitude in self.waves:
|
||||
off -= math.sin(startpos[0]/wavelength*math.pi*2-f*self.speed_f/self.n_frames) * amplitude
|
||||
|
||||
off *= self.amplitude_scale
|
||||
return NVector(off * math.cos(self.axis), off * math.sin(self.axis))
|
||||
|
||||
|
||||
class DepthRotationAxis:
|
||||
def __init__(self, x, y, keep):
|
||||
self.x = x / x.length
|
||||
self.y = y / y.length
|
||||
self.keep = keep / keep.length # should be the cross product
|
||||
|
||||
def rot_center(self, center, point):
|
||||
return (
|
||||
self.x * self.x.dot(center) +
|
||||
self.y * self.y.dot(center) +
|
||||
self.keep * self.keep.dot(point)
|
||||
)
|
||||
|
||||
def extract_component(self, vector, axis):
|
||||
return sum(vector.element_scaled(axis).components)
|
||||
|
||||
@classmethod
|
||||
def from_points(cls, keep_point, center=NVector(0, 0, 0)):
|
||||
keep = keep_point - center
|
||||
keep /= keep.length
|
||||
# Hughes-Moller to find x and y
|
||||
if abs(keep.x) > abs(keep.z):
|
||||
y = NVector(-keep.y, keep.x, 0)
|
||||
else:
|
||||
y = NVector(0, -keep.z, keep.y)
|
||||
y /= y.length
|
||||
x = y.cross(keep)
|
||||
return cls(x, y, keep)
|
||||
|
||||
|
||||
class DepthRotation:
|
||||
axis_x = DepthRotationAxis(NVector(0, 0, 1), NVector(0, 1, 0), NVector(1, 0, 0))
|
||||
axis_y = DepthRotationAxis(NVector(1, 0, 0), NVector(0, 0, 1), NVector(0, 1, 0))
|
||||
axis_z = DepthRotationAxis(NVector(1, 0, 0), NVector(0, 1, 0), NVector(0, 0, 1))
|
||||
|
||||
def __init__(self, center):
|
||||
self.center = center
|
||||
|
||||
def rotate3d_y(self, point, angle):
|
||||
return self.rotate3d(point, angle, self.axis_y)
|
||||
# Hard-coded version:
|
||||
#c = NVector(self.center.x, point.y, self.center.z)
|
||||
#rad = angle * math.pi / 180
|
||||
#delta = point - c
|
||||
#pol_l = delta.length
|
||||
#pol_a = math.atan2(delta.z, delta.x)
|
||||
#dest_a = pol_a + rad
|
||||
#return NVector(
|
||||
# c.x + pol_l * math.cos(dest_a),
|
||||
# point.y,
|
||||
# c.z + pol_l * math.sin(dest_a)
|
||||
#)
|
||||
|
||||
def rotate3d_x(self, point, angle):
|
||||
return self.rotate3d(point, angle, self.axis_x)
|
||||
# Hard-coded version:
|
||||
#c = NVector(point.x, self.center.y, self.center.z)
|
||||
#rad = angle * math.pi / 180
|
||||
#delta = point - c
|
||||
#pol_l = delta.length
|
||||
#pol_a = math.atan2(delta.y, delta.z)
|
||||
#dest_a = pol_a + rad
|
||||
#return NVector(
|
||||
# point.x,
|
||||
# c.y + pol_l * math.sin(dest_a),
|
||||
# c.z + pol_l * math.cos(dest_a),
|
||||
#)
|
||||
|
||||
def rotate3d_z(self, point, angle):
|
||||
return self.rotate3d(point, angle, self.axis_z)
|
||||
|
||||
def rotate3d(self, point, angle, axis):
|
||||
c = axis.rot_center(self.center, point)
|
||||
rad = angle * math.pi / 180
|
||||
delta = point - c
|
||||
pol_l = delta.length
|
||||
pol_a = math.atan2(
|
||||
axis.extract_component(delta, axis.y),
|
||||
axis.extract_component(delta, axis.x)
|
||||
)
|
||||
dest_a = pol_a + rad
|
||||
return c + axis.x * pol_l * math.cos(dest_a) + axis.y * pol_l * math.sin(dest_a)
|
||||
|
||||
|
||||
class DepthRotationDisplacer(PointDisplacer):
|
||||
axis_x = DepthRotation.axis_x
|
||||
axis_y = DepthRotation.axis_y
|
||||
axis_z = DepthRotation.axis_z
|
||||
|
||||
def __init__(self, center, time_start, time_end, n_frames, axis,
|
||||
depth=0, angle=360, anglestart=0, ease=easing.Linear()):
|
||||
super().__init__(time_start, time_end, n_frames)
|
||||
self.rotation = DepthRotation(center)
|
||||
if isinstance(axis, NVector):
|
||||
axis = DepthRotationAxis.from_points(axis)
|
||||
self.axis = axis
|
||||
self.depth = depth
|
||||
self._angle = angle
|
||||
self.anglestart = anglestart
|
||||
self.ease = ease
|
||||
self._init_lerp(0, angle, ease)
|
||||
|
||||
@property
|
||||
def angle(self):
|
||||
return self._angle
|
||||
|
||||
@angle.setter
|
||||
def angle(self, value):
|
||||
self._angle = value
|
||||
self._init_lerp(0, value, self.ease)
|
||||
|
||||
def _on_displace(self, startpos, f):
|
||||
angle = self.anglestart + self._lerp_get(f)
|
||||
if len(startpos) < 3:
|
||||
startpos = NVector(*(startpos.components + [self.depth]))
|
||||
return self.rotation.rotate3d(startpos, angle, self.axis) - startpos
|
||||
|
||||
|
||||
class EnvelopeDeformation(PointDisplacer):
|
||||
def __init__(self, topleft, bottomright):
|
||||
self.topleft = topleft
|
||||
self.size = bottomright - topleft
|
||||
self.keyframes = []
|
||||
|
||||
@property
|
||||
def time_start(self):
|
||||
return self.keyframes[0][0]
|
||||
|
||||
def add_reset_keyframe(self, time):
|
||||
self.add_keyframe(
|
||||
time,
|
||||
self.topleft.clone(),
|
||||
NVector(self.topleft.x + self.size.x, self.topleft.y),
|
||||
NVector(self.topleft.x + self.size.x, self.topleft.y + self.size.y),
|
||||
NVector(self.topleft.x, self.topleft.y + self.size.y),
|
||||
)
|
||||
|
||||
def add_keyframe(self, time, tl, tr, br, bl):
|
||||
self.keyframes.append([
|
||||
time,
|
||||
tl.clone(),
|
||||
tr.clone(),
|
||||
br.clone(),
|
||||
bl.clone()
|
||||
])
|
||||
|
||||
def _on_displace(self, startpos, f):
|
||||
_, tl, tr, br, bl = self.keyframes[f]
|
||||
relp = startpos - self.topleft
|
||||
relp.x /= self.size.x
|
||||
relp.y /= self.size.y
|
||||
|
||||
x1 = tl.lerp(tr, relp.x)
|
||||
x2 = bl.lerp(br, relp.x)
|
||||
|
||||
#return x1.lerp(x2, relp.y)
|
||||
return x1.lerp(x2, relp.y) - startpos
|
||||
|
||||
@property
|
||||
def n_frames(self):
|
||||
return len(self.keyframes)-1
|
||||
|
||||
def frame_time(self, f):
|
||||
return self.keyframes[f][0]
|
||||
|
||||
|
||||
class DisplacerDampener(PointDisplacer):
|
||||
"""!
|
||||
Given a displacer and a function that returns a factor for a point,
|
||||
multiplies the effect of the displacer by the factor
|
||||
"""
|
||||
def __init__(self, displacer, dampener):
|
||||
self.displacer = displacer
|
||||
self.dampener = dampener
|
||||
|
||||
@property
|
||||
def time_start(self):
|
||||
return self.displacer.time_start
|
||||
|
||||
def _on_displace(self, startpos, f):
|
||||
disp = self.displacer._on_displace(startpos, f)
|
||||
damp = self.dampener(startpos)
|
||||
return disp * damp
|
||||
|
||||
@property
|
||||
def n_frames(self):
|
||||
return self.displacer.n_frames
|
||||
|
||||
def frame_time(self, f):
|
||||
return self.displacer.frame_time(f)
|
||||
|
||||
|
||||
class FollowDisplacer(PointDisplacer):
|
||||
def __init__(
|
||||
self,
|
||||
origin,
|
||||
range,
|
||||
offset_func,
|
||||
time_start, time_end, n_frames,
|
||||
falloff_exp=1,
|
||||
):
|
||||
"""!
|
||||
@brief Uses a custom offset function, and applies a falloff to the displacement
|
||||
|
||||
@param origin Origin point for the falloff
|
||||
@param range Radius after which the points will not move
|
||||
@param offset_func Function returning an offset given a ratio of the time
|
||||
@param time_start When the animation shall start
|
||||
@param time_end When the animation shall end
|
||||
@param n_frames Number of frames in the animation
|
||||
@param falloff_exp Exponent for the falloff
|
||||
"""
|
||||
super().__init__(time_start, time_end, n_frames)
|
||||
self.origin = origin
|
||||
self.range = range
|
||||
self.offset_func = offset_func
|
||||
self.falloff_exp = falloff_exp
|
||||
|
||||
def _on_displace(self, startpos, f):
|
||||
influence = 1 - min(1, (startpos - self.origin).length / self.range) ** self.falloff_exp
|
||||
return self.offset_func(f / self.n_frames) * influence
|
||||
@@ -0,0 +1,447 @@
|
||||
import enum
|
||||
import math
|
||||
import colorsys
|
||||
from ..nvector import NVector
|
||||
|
||||
|
||||
def from_uint8(r, g, b, a=255):
|
||||
return Color(r, g, b, a) / 255
|
||||
|
||||
|
||||
class ColorMode(enum.Enum):
|
||||
## sRGB, Components in [0, 1]
|
||||
RGB = enum.auto()
|
||||
## HSV, components in [0, 1]
|
||||
HSV = enum.auto()
|
||||
## HSL, components in [0, 1]
|
||||
HSL = enum.auto()
|
||||
## CIE XYZ with Illuminant D65. Components in [0, 1]
|
||||
XYZ = enum.auto()
|
||||
## CIE L*u*v*
|
||||
LUV = enum.auto()
|
||||
## CIE Lch(uv), polar version of LUV where C is the radius and H an angle in radians
|
||||
LCH_uv = enum.auto()
|
||||
## CIE L*a*b*
|
||||
LAB = enum.auto()
|
||||
## CIE LCh(ab), polar version of LAB where C is the radius and H an angle in radians
|
||||
#LCH_ab = enum.auto()
|
||||
|
||||
|
||||
def _clamp(x):
|
||||
return max(0, min(1, x))
|
||||
|
||||
|
||||
class Conversion:
|
||||
_conv_paths = {
|
||||
(ColorMode.RGB, ColorMode.RGB): [],
|
||||
(ColorMode.RGB, ColorMode.HSV): [],
|
||||
(ColorMode.RGB, ColorMode.HSL): [],
|
||||
(ColorMode.RGB, ColorMode.XYZ): [],
|
||||
(ColorMode.RGB, ColorMode.LUV): [ColorMode.XYZ],
|
||||
(ColorMode.RGB, ColorMode.LAB): [ColorMode.XYZ],
|
||||
(ColorMode.RGB, ColorMode.LCH_uv): [ColorMode.XYZ, ColorMode.LUV],
|
||||
#(ColorMode.RGB, ColorMode.LCH_ab): [ColorMode.XYZ, ColorMode.LAB],
|
||||
|
||||
(ColorMode.HSV, ColorMode.RGB): [],
|
||||
(ColorMode.HSV, ColorMode.HSV): [],
|
||||
(ColorMode.HSV, ColorMode.HSL): [],
|
||||
(ColorMode.HSV, ColorMode.XYZ): [ColorMode.RGB],
|
||||
(ColorMode.HSV, ColorMode.LUV): [ColorMode.RGB, ColorMode.XYZ],
|
||||
(ColorMode.HSV, ColorMode.LAB): [ColorMode.RGB, ColorMode.XYZ],
|
||||
(ColorMode.HSV, ColorMode.LCH_uv): [ColorMode.RGB, ColorMode.XYZ, ColorMode.LUV],
|
||||
#(ColorMode.HSV, ColorMode.LCH_ab): [ColorMode.RGB, ColorMode.XYZ, ColorMode.LAB],
|
||||
|
||||
(ColorMode.HSL, ColorMode.RGB): [],
|
||||
(ColorMode.HSL, ColorMode.HSV): [],
|
||||
(ColorMode.HSL, ColorMode.HSL): [],
|
||||
(ColorMode.HSL, ColorMode.XYZ): [ColorMode.RGB],
|
||||
(ColorMode.HSL, ColorMode.LUV): [ColorMode.RGB, ColorMode.XYZ],
|
||||
(ColorMode.HSL, ColorMode.LAB): [ColorMode.RGB, ColorMode.XYZ],
|
||||
(ColorMode.HSL, ColorMode.LCH_uv): [ColorMode.RGB, ColorMode.XYZ, ColorMode.LUV],
|
||||
#(ColorMode.HSL, ColorMode.LCH_ab): [ColorMode.RGB, ColorMode.XYZ, ColorMode.LAB],
|
||||
|
||||
(ColorMode.XYZ, ColorMode.RGB): [],
|
||||
(ColorMode.XYZ, ColorMode.HSV): [ColorMode.RGB],
|
||||
(ColorMode.XYZ, ColorMode.HSL): [ColorMode.RGB],
|
||||
(ColorMode.XYZ, ColorMode.XYZ): [],
|
||||
(ColorMode.XYZ, ColorMode.LUV): [],
|
||||
(ColorMode.XYZ, ColorMode.LAB): [],
|
||||
(ColorMode.XYZ, ColorMode.LCH_uv): [ColorMode.LUV],
|
||||
#(ColorMode.XYZ, ColorMode.LCH_ab): [ColorMode.LAB],
|
||||
|
||||
(ColorMode.LCH_uv, ColorMode.RGB): [ColorMode.LUV, ColorMode.XYZ],
|
||||
(ColorMode.LCH_uv, ColorMode.HSV): [ColorMode.LUV, ColorMode.XYZ, ColorMode.RGB],
|
||||
(ColorMode.LCH_uv, ColorMode.HSL): [ColorMode.LUV, ColorMode.XYZ, ColorMode.RGB],
|
||||
(ColorMode.LCH_uv, ColorMode.XYZ): [ColorMode.LUV],
|
||||
(ColorMode.LCH_uv, ColorMode.LUV): [],
|
||||
(ColorMode.LCH_uv, ColorMode.LAB): [ColorMode.LUV, ColorMode.XYZ],
|
||||
(ColorMode.LCH_uv, ColorMode.LCH_uv): [],
|
||||
#(ColorMode.LCH_uv, ColorMode.LCH_ab): [ColorMode.LUV, ColorMode.XYZ, ColorMode.LAB],
|
||||
|
||||
(ColorMode.LUV, ColorMode.RGB): [ColorMode.XYZ],
|
||||
(ColorMode.LUV, ColorMode.HSV): [ColorMode.XYZ, ColorMode.RGB],
|
||||
(ColorMode.LUV, ColorMode.HSL): [ColorMode.XYZ, ColorMode.RGB],
|
||||
(ColorMode.LUV, ColorMode.XYZ): [],
|
||||
(ColorMode.LUV, ColorMode.LUV): [],
|
||||
(ColorMode.LUV, ColorMode.LAB): [ColorMode.XYZ],
|
||||
(ColorMode.LUV, ColorMode.LCH_uv): [],
|
||||
#(ColorMode.LUV, ColorMode.LCH_ab): [ColorMode.XYZ, ColorMode.LAB],
|
||||
|
||||
(ColorMode.LAB, ColorMode.RGB): [ColorMode.XYZ],
|
||||
(ColorMode.LAB, ColorMode.HSV): [ColorMode.XYZ, ColorMode.RGB],
|
||||
(ColorMode.LAB, ColorMode.HSL): [ColorMode.XYZ, ColorMode.RGB],
|
||||
(ColorMode.LAB, ColorMode.XYZ): [],
|
||||
(ColorMode.LAB, ColorMode.LUV): [ColorMode.XYZ],
|
||||
(ColorMode.LAB, ColorMode.LAB): [],
|
||||
(ColorMode.LAB, ColorMode.LCH_uv): [ColorMode.XYZ, ColorMode.LUV],
|
||||
#(ColorMode.LAB, ColorMode.LCH_ab): [],
|
||||
|
||||
#(ColorMode.LCH_ab, ColorMode.RGB): [ColorMode.LAB, ColorMode.XYZ],
|
||||
#(ColorMode.LCH_ab, ColorMode.HSV): [ColorMode.LAB, ColorMode.XYZ, ColorMode.RGB],
|
||||
#(ColorMode.LCH_ab, ColorMode.HSL): [ColorMode.LAB, ColorMode.XYZ, ColorMode.RGB],
|
||||
#(ColorMode.LCH_ab, ColorMode.XYZ): [ColorMode.LAB],
|
||||
#(ColorMode.LCH_ab, ColorMode.LUV): [ColorMode.LAB, ColorMode.XYZ],
|
||||
#(ColorMode.LCH_ab, ColorMode.LAB): [],
|
||||
#(ColorMode.LCH_ab, ColorMode.LCH_uv): [ColorMode.LAB, ColorMode.XYZ, ColorMode.LUV],
|
||||
#(ColorMode.LCH_ab, ColorMode.LCH_ab): [],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def rgb_to_hsv(r, g, b):
|
||||
return colorsys.rgb_to_hsv(r, g, b)
|
||||
|
||||
@staticmethod
|
||||
def hsv_to_rgb(r, g, b):
|
||||
return colorsys.hsv_to_rgb(r, g, b)
|
||||
|
||||
@staticmethod
|
||||
def hsl_to_hsv(h, s_hsl, l):
|
||||
v = l + s_hsl * min(l, 1 - l)
|
||||
s_hsv = 0 if v == 0 else 2 - 2 * l / v
|
||||
return (h, s_hsv, v)
|
||||
|
||||
@staticmethod
|
||||
def hsv_to_hsl(h, s_hsv, v):
|
||||
l = v - v * s_hsv / 2
|
||||
s_hsl = 0 if l in (0, 1) else (v - l) / min(l, 1 - l)
|
||||
return (h, s_hsl, l)
|
||||
|
||||
@staticmethod
|
||||
def rgb_to_hsl(r, g, b):
|
||||
h, l, s = colorsys.rgb_to_hls(r, g, b)
|
||||
return (h, s, l)
|
||||
|
||||
@staticmethod
|
||||
def hsl_to_rgb(h, s, l):
|
||||
return colorsys.hls_to_rgb(h, l, s)
|
||||
|
||||
# http://w3.uqo.ca/missaoui/Publications/TRColorSpace.zip
|
||||
#@staticmethod
|
||||
#def rgb_to_hcl(r, g, b, gamma=3, y0=100):
|
||||
#maxc = max(r, g, b)
|
||||
#minc = min(r, g, b)
|
||||
#if maxc > 0:
|
||||
#alpha = 1/y0 * minc / maxc
|
||||
#else:
|
||||
#alpha = 0
|
||||
#q = math.e ** (alpha * gamma)
|
||||
#h = math.atan2(g - b, r - g)
|
||||
#if h < 0:
|
||||
#h += 2*math.pi
|
||||
#h /= 2*math.pi
|
||||
#c = q / 3 * (abs(r-g) + abs(g-b) + abs(b-r))
|
||||
#l = (q * maxc + (q-1) * minc) / 2
|
||||
#return (h, c, l)
|
||||
|
||||
#@staticmethod
|
||||
#def hcl_to_rgb(h, c, l, gamma=3, y0=100):
|
||||
#h *= 2*math.pi
|
||||
|
||||
#q = math.e ** ((1 - 2*c / 4*l) * gamma / y0)
|
||||
#minc = (4*l - 3*c) / (4*q - 2)
|
||||
#maxc = minc + 3*c / 2*q
|
||||
|
||||
#if h <= math.pi * 1 / 3:
|
||||
#tan = math.tan(3/2*h)
|
||||
#r = maxc
|
||||
#b = minc
|
||||
#g = (r * tan + b) / (1 + tan)
|
||||
#elif h <= math.pi * 2 / 3:
|
||||
#tan = math.tan(3/4*(h-math.pi))
|
||||
#g = maxc
|
||||
#b = minc
|
||||
#r = (g * (1+tan) - b) / tan
|
||||
#elif h <= math.pi * 3 / 3:
|
||||
#tan = math.tan(3/4*(h-math.pi))
|
||||
#g = maxc
|
||||
#r = minc
|
||||
#b = g * (1+tan) - r * tan
|
||||
#elif h <= math.pi * 4 / 3:
|
||||
#tan = math.tan(3/2*(h+math.pi))
|
||||
#b = maxc
|
||||
#r = minc
|
||||
#g = (r * tan + b) / (1 + tan)
|
||||
#elif h <= math.pi * 5 / 3:
|
||||
#tan = math.tan(3/4*h)
|
||||
#b = maxc
|
||||
#g = minc
|
||||
#r = (g * (1+tan) - b) / tan
|
||||
#else:
|
||||
#tan = math.tan(3/4*h)
|
||||
#r = maxc
|
||||
#g = minc
|
||||
#b = g * (1+tan) - r * tan
|
||||
|
||||
#return _clamp(r), _clamp(g), _clamp(b)
|
||||
|
||||
@staticmethod
|
||||
def rgb_to_xyz(r, g, b):
|
||||
def _gamma(v):
|
||||
return v / 12.92 if v <= 0.04045 else ((v + 0.055) / 1.055) ** 2.4
|
||||
rgb = (_gamma(r), _gamma(g), _gamma(b))
|
||||
matrix = [
|
||||
[0.4124564, 0.3575761, 0.1804375],
|
||||
[0.2126729, 0.7151522, 0.0721750],
|
||||
[0.0193339, 0.1191920, 0.9503041],
|
||||
]
|
||||
return tuple(
|
||||
sum(rgb[i] * c for i, c in enumerate(row))
|
||||
for row in matrix
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def xyz_to_rgb(x, y, z):
|
||||
def _gamma1(v):
|
||||
return _clamp(v * 12.92 if v <= 0.0031308 else v ** (1/2.4) * 1.055 - 0.055)
|
||||
matrix = [
|
||||
[+3.2404542, -1.5371385, -0.4985314],
|
||||
[-0.9692660, +1.8760108, +0.0415560],
|
||||
[+0.0556434, -0.2040259, +1.0572252],
|
||||
]
|
||||
xyz = (x, y, z)
|
||||
return tuple(map(_gamma1, (
|
||||
sum(xyz[i] * c for i, c in enumerate(row))
|
||||
for row in matrix
|
||||
)))
|
||||
|
||||
@staticmethod
|
||||
def xyz_to_luv(x, y, z):
|
||||
u1r = 0.2009
|
||||
v1r = 0.4610
|
||||
yr = 100
|
||||
|
||||
kap = (29/3)**3
|
||||
eps = (6/29)**3
|
||||
|
||||
try:
|
||||
u1 = 4*x / (x + 15*y + 3*z)
|
||||
v1 = 9*y / (x + 15*y + 3*z)
|
||||
except ZeroDivisionError:
|
||||
return 0, 0, 0
|
||||
|
||||
y_r = y/yr
|
||||
l = 166 * y_r ** (1/3) - 16 if y_r > eps else kap * y_r
|
||||
u = 13 * l * (u1 - u1r)
|
||||
v = 13 * l * (v1 - v1r)
|
||||
return l, u, v
|
||||
|
||||
@staticmethod
|
||||
def luv_to_xyz(l, u, v):
|
||||
u1r = 0.2009
|
||||
v1r = 0.4610
|
||||
yr = 100
|
||||
|
||||
kap = (29/3)**3
|
||||
|
||||
if l == 0:
|
||||
u1 = u1r
|
||||
v1 = v1r
|
||||
else:
|
||||
u1 = u / (13 * l) + u1r
|
||||
v1 = v / (13 * l) + v1r
|
||||
|
||||
y = yr * l / kap if l <= 8 else yr * ((l + 16) / 116) ** 3
|
||||
x = y * 9*u1 / (4*v1)
|
||||
z = y * (12 - 3*u1 - 20*v1) / (4*v1)
|
||||
return x, y, z
|
||||
|
||||
@staticmethod
|
||||
def luv_to_lch_uv(l, u, v):
|
||||
c = math.hypot(u, v)
|
||||
h = math.atan2(v, u)
|
||||
if h < 0:
|
||||
h += math.tau
|
||||
return l, c, h
|
||||
|
||||
@staticmethod
|
||||
def lch_uv_to_luv(l, c, h):
|
||||
u = math.cos(h) * c
|
||||
v = math.sin(h) * c
|
||||
return l, u, v
|
||||
|
||||
@staticmethod
|
||||
def xyz_to_lab(x, y, z):
|
||||
# D65 Illuminant aka sRGB(1,1,1)
|
||||
xn = 0.950489
|
||||
yn = 1
|
||||
zn = 108.8840
|
||||
|
||||
delta = 6 / 29
|
||||
|
||||
def f(t):
|
||||
return t ** (1/3) if t > delta ** 3 else t / (3*delta**2) + 4/29
|
||||
|
||||
fy = f(y/yn)
|
||||
l = 116 * fy - 16
|
||||
a = 500 * (f(x/xn) - fy)
|
||||
b = 200 * (fy - f(z/zn))
|
||||
|
||||
return l, a, b
|
||||
|
||||
@staticmethod
|
||||
def lab_to_xyz(l, a, b):
|
||||
# D65 Illuminant aka sRGB(1,1,1)
|
||||
xn = 0.950489
|
||||
yn = 1
|
||||
zn = 108.8840
|
||||
|
||||
delta = 6 / 29
|
||||
|
||||
def f1(t):
|
||||
return t**3 if t > delta else 3*delta**2*(t-4/29)
|
||||
|
||||
l1 = (l+16) / 116
|
||||
x = xn * f1(l1+a/500)
|
||||
y = yn * f1(l1)
|
||||
z = zn * f1(l1-b/200)
|
||||
|
||||
return x, y, z
|
||||
|
||||
#@staticmethod
|
||||
#def lab_to_lch_ab(l, a, b):
|
||||
#c = math.hypot(a, b)
|
||||
#h = math.atan2(b, a)
|
||||
#if h < 0:
|
||||
#h += math.tau
|
||||
#return l, c, h
|
||||
|
||||
#@staticmethod
|
||||
#def lch_ab_to_lab(l, c, h):
|
||||
#a = math.cos(h) * c
|
||||
#b = math.sin(h) * c
|
||||
#return l, a, b
|
||||
|
||||
@staticmethod
|
||||
def conv_func(mode_from, mode_to):
|
||||
return getattr(Conversion, "%s_to_%s" % (mode_from.name.lower(), mode_to.name.lower()), None)
|
||||
|
||||
@staticmethod
|
||||
def convert(tuple, mode_from, mode_to):
|
||||
if mode_from == mode_to:
|
||||
return tuple
|
||||
|
||||
if len(tuple) == 4:
|
||||
alpha = tuple[3]
|
||||
tuple = tuple[:3]
|
||||
else:
|
||||
alpha = None
|
||||
|
||||
func = Conversion.conv_func(mode_from, mode_to)
|
||||
if func:
|
||||
return func(*tuple)
|
||||
|
||||
if (mode_from, mode_to) in Conversion._conv_paths:
|
||||
steps = Conversion._conv_paths[(mode_from, mode_to)] + [mode_to]
|
||||
for step in steps:
|
||||
func = Conversion.conv_func(mode_from, step)
|
||||
if not func:
|
||||
raise ValueError("Missing definition for conversion from %s to %s" % (mode_from, step))
|
||||
tuple = func(*tuple)
|
||||
mode_from = step
|
||||
if alpha is not None:
|
||||
tuple += (alpha,)
|
||||
return tuple
|
||||
|
||||
raise ValueError("No conversion path from %s to %s" % (mode_from, mode_to))
|
||||
|
||||
|
||||
class Color(NVector):
|
||||
Mode = ColorMode
|
||||
|
||||
def __init__(self, c1=0, c2=0, c3=0, a=1, *, mode=ColorMode.RGB):
|
||||
if isinstance(a, ColorMode):
|
||||
raise TypeError("Please update the Color constructor")
|
||||
super().__init__(c1, c2, c3, a)
|
||||
self._mode = mode
|
||||
|
||||
@property
|
||||
def mode(self):
|
||||
return self._mode
|
||||
|
||||
def convert(self, v):
|
||||
if v == self._mode:
|
||||
return self
|
||||
|
||||
self.components = list(Conversion.convert(self.components, self._mode, v))
|
||||
|
||||
self._mode = v
|
||||
return self
|
||||
|
||||
def clone(self):
|
||||
return Color(*self.components, mode=self._mode)
|
||||
|
||||
def converted(self, mode):
|
||||
return self.clone().convert(mode)
|
||||
|
||||
def to_rgb(self):
|
||||
return self.converted(ColorMode.RGB)
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s %s [%.3f, %.3f, %.3f, %.3f]>" % (
|
||||
(self.__class__.__name__, self.mode.name) + tuple(self.components)
|
||||
)
|
||||
|
||||
def component_names(self):
|
||||
comps = None
|
||||
|
||||
if self._mode == ColorMode.RGB:
|
||||
comps = ({"r", "red"}, {"g", "green"}, {"b", "blue"})
|
||||
elif self._mode == ColorMode.HSV:
|
||||
comps = ({"h", "hue"}, {"s", "saturation"}, {"v", "value"})
|
||||
elif self._mode == ColorMode.HSL:
|
||||
comps = ({"h", "hue"}, {"s", "saturation"}, {"l", "lightness"})
|
||||
elif self._mode == ColorMode.LCH_uv: # in (ColorMode.LCH_uv, ColorMode.LCH_ab):
|
||||
comps = ({"l", "luma", "luminance"}, {"c", "choma"}, {"h", "hue"})
|
||||
elif self._mode == ColorMode.XYZ:
|
||||
comps = "xyz"
|
||||
elif self._mode == ColorMode.LUV:
|
||||
comps = "luv"
|
||||
elif self._mode == ColorMode.LAB:
|
||||
comps = "lab"
|
||||
|
||||
return comps
|
||||
|
||||
def _attrindex(self, name):
|
||||
comps = self.component_names()
|
||||
|
||||
if comps:
|
||||
for i, vals in enumerate(comps):
|
||||
if name in vals:
|
||||
return i
|
||||
|
||||
return None
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name not in vars(self) and name not in {"_mode", "components"}:
|
||||
i = self._attrindex(name)
|
||||
if i is not None:
|
||||
return self.components[i]
|
||||
raise AttributeError(name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name not in vars(self) and name not in {"_mode", "components"}:
|
||||
i = self._attrindex(name)
|
||||
if i is not None:
|
||||
self.components[i] = value
|
||||
return
|
||||
return super().__setattr__(name, value)
|
||||
@@ -0,0 +1,124 @@
|
||||
import math
|
||||
|
||||
from ..nvector import NVector
|
||||
from ..objects.bezier import BezierPoint
|
||||
|
||||
|
||||
## @todo Just output a Bezier object
|
||||
class Ellipse:
|
||||
def __init__(self, center, radii, xrot):
|
||||
"""
|
||||
@param center 2D vector, center of the ellipse
|
||||
@param radii 2D vector, x/y radius of the ellipse
|
||||
@param xrot Angle between the main axis of the ellipse and the x axis (in radians)
|
||||
"""
|
||||
self.center = center
|
||||
self.radii = radii
|
||||
self.xrot = xrot
|
||||
|
||||
def point(self, t):
|
||||
return NVector(
|
||||
self.center[0]
|
||||
+ self.radii[0] * math.cos(self.xrot) * math.cos(t)
|
||||
- self.radii[1] * math.sin(self.xrot) * math.sin(t),
|
||||
|
||||
self.center[1]
|
||||
+ self.radii[0] * math.sin(self.xrot) * math.cos(t)
|
||||
+ self.radii[1] * math.cos(self.xrot) * math.sin(t)
|
||||
)
|
||||
|
||||
def derivative(self, t):
|
||||
return NVector(
|
||||
- self.radii[0] * math.cos(self.xrot) * math.sin(t)
|
||||
- self.radii[1] * math.sin(self.xrot) * math.cos(t),
|
||||
|
||||
- self.radii[0] * math.sin(self.xrot) * math.sin(t)
|
||||
+ self.radii[1] * math.cos(self.xrot) * math.cos(t)
|
||||
)
|
||||
|
||||
def to_bezier(self, anglestart, angle_delta):
|
||||
points = []
|
||||
angle1 = anglestart
|
||||
angle_left = abs(angle_delta)
|
||||
step = math.pi / 2
|
||||
sign = -1 if anglestart+angle_delta < angle1 else 1
|
||||
|
||||
# We need to fix the first handle
|
||||
firststep = min(angle_left, step) * sign
|
||||
alpha = self._alpha(firststep)
|
||||
q1 = self.derivative(angle1) * alpha
|
||||
points.append(BezierPoint(self.point(angle1), NVector(0, 0), q1))
|
||||
|
||||
# Then we iterate until the angle has been completed
|
||||
tolerance = step / 2
|
||||
while angle_left > tolerance:
|
||||
lstep = min(angle_left, step)
|
||||
step_sign = lstep * sign
|
||||
angle2 = angle1 + step_sign
|
||||
angle_left -= abs(lstep)
|
||||
|
||||
alpha = self._alpha(step_sign)
|
||||
p2 = self.point(angle2)
|
||||
q2 = self.derivative(angle2) * alpha
|
||||
|
||||
points.append(BezierPoint(p2, -q2, q2))
|
||||
angle1 = angle2
|
||||
return points
|
||||
|
||||
def _alpha(self, step):
|
||||
return math.sin(step) * (math.sqrt(4+3*math.tan(step/2)**2) - 1) / 3
|
||||
|
||||
@classmethod
|
||||
def from_svg_arc(cls, start, rx, ry, xrot, large, sweep, dest):
|
||||
rx = abs(rx)
|
||||
ry = abs(ry)
|
||||
|
||||
x1 = start[0]
|
||||
y1 = start[1]
|
||||
x2 = dest[0]
|
||||
y2 = dest[1]
|
||||
phi = math.pi * xrot / 180
|
||||
|
||||
x1p, y1p = _matrix_mul(phi, (start-dest)/2, -1)
|
||||
|
||||
cr = x1p ** 2 / rx**2 + y1p**2 / ry**2
|
||||
if cr > 1:
|
||||
s = math.sqrt(cr)
|
||||
rx *= s
|
||||
ry *= s
|
||||
|
||||
dq = rx**2 * y1p**2 + ry**2 * x1p**2
|
||||
pq = (rx**2 * ry**2 - dq) / dq
|
||||
cpm = math.sqrt(max(0, pq))
|
||||
if large == sweep:
|
||||
cpm = -cpm
|
||||
cp = NVector(cpm * rx * y1p / ry, -cpm * ry * x1p / rx)
|
||||
c = _matrix_mul(phi, cp) + NVector((x1+x2)/2, (y1+y2)/2)
|
||||
theta1 = _angle(NVector(1, 0), NVector((x1p - cp[0]) / rx, (y1p - cp[1]) / ry))
|
||||
deltatheta = _angle(
|
||||
NVector((x1p - cp[0]) / rx, (y1p - cp[1]) / ry),
|
||||
NVector((-x1p - cp[0]) / rx, (-y1p - cp[1]) / ry)
|
||||
) % (2*math.pi)
|
||||
|
||||
if not sweep and deltatheta > 0:
|
||||
deltatheta -= 2*math.pi
|
||||
elif sweep and deltatheta < 0:
|
||||
deltatheta += 2*math.pi
|
||||
|
||||
return cls(c, NVector(rx, ry), phi), theta1, deltatheta
|
||||
|
||||
|
||||
def _matrix_mul(phi, p, sin_mul=1):
|
||||
c = math.cos(phi)
|
||||
s = math.sin(phi) * sin_mul
|
||||
|
||||
xr = c * p.x - s * p.y
|
||||
yr = s * p.x + c * p.y
|
||||
return NVector(xr, yr)
|
||||
|
||||
|
||||
def _angle(u, v):
|
||||
arg = math.acos(max(-1, min(1, u.dot(v) / (u.length * v.length))))
|
||||
if u[0] * v[1] - u[1] * v[0] < 0:
|
||||
return -arg
|
||||
return arg
|
||||
@@ -0,0 +1,13 @@
|
||||
from contextlib import contextmanager
|
||||
|
||||
|
||||
@contextmanager
|
||||
def open_file(file_or_name, mode="w"):
|
||||
if isinstance(file_or_name, str):
|
||||
obj = open(file_or_name, mode)
|
||||
try:
|
||||
yield obj
|
||||
finally:
|
||||
obj.close()
|
||||
else:
|
||||
yield file_or_name
|
||||
@@ -0,0 +1,831 @@
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import fontTools.pens.basePen
|
||||
import fontTools.ttLib
|
||||
import fontTools.t1Lib
|
||||
from fontTools.pens.boundsPen import ControlBoundsPen
|
||||
import enum
|
||||
import math
|
||||
from xml.etree import ElementTree
|
||||
from ..nvector import NVector
|
||||
from ..objects.bezier import Bezier, BezierPoint
|
||||
from ..objects.shapes import Path, Group, Fill, Stroke
|
||||
from ..objects.text import TextJustify
|
||||
from ..objects.base import LottieProp, CustomObject
|
||||
from ..objects.layers import ShapeLayer
|
||||
|
||||
|
||||
class BezierPen(fontTools.pens.basePen.BasePen):
|
||||
def __init__(self, glyphSet, offset=NVector(0, 0)):
|
||||
super().__init__(glyphSet)
|
||||
self.beziers = []
|
||||
self.current = Bezier()
|
||||
self.offset = offset
|
||||
|
||||
def _point(self, pt):
|
||||
return self.offset + NVector(pt[0], -pt[1])
|
||||
|
||||
def _moveTo(self, pt):
|
||||
self._endPath()
|
||||
|
||||
def _endPath(self):
|
||||
if len(self.current.points):
|
||||
self.beziers.append(self.current)
|
||||
self.current = Bezier()
|
||||
|
||||
def _closePath(self):
|
||||
self.current.close()
|
||||
self._endPath()
|
||||
|
||||
def _lineTo(self, pt):
|
||||
if len(self.current.points) == 0:
|
||||
self.current.points.append(self._point(self._getCurrentPoint()))
|
||||
|
||||
self.current.points.append(self._point(pt))
|
||||
|
||||
def _curveToOne(self, pt1, pt2, pt3):
|
||||
if len(self.current.points) == 0:
|
||||
cp = self._point(self._getCurrentPoint())
|
||||
self.current.points.append(
|
||||
BezierPoint(
|
||||
cp,
|
||||
None,
|
||||
self._point(pt1) - cp
|
||||
)
|
||||
|
||||
)
|
||||
else:
|
||||
self.current.points[-1].out_tangent = self._point(pt1) - self.current.points[-1].vertex
|
||||
|
||||
dest = self._point(pt3)
|
||||
self.current.points.append(
|
||||
BezierPoint(
|
||||
dest,
|
||||
self._point(pt2) - dest,
|
||||
None,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class SystemFont:
|
||||
def __init__(self, family):
|
||||
self.family = family
|
||||
self.files = {}
|
||||
self.styles = set()
|
||||
self._renderers = {}
|
||||
|
||||
def add_file(self, styles, file):
|
||||
self.styles |= set(styles)
|
||||
key = self._key(styles)
|
||||
self.files.setdefault(key, file)
|
||||
|
||||
def filename(self, styles):
|
||||
return self.files[self._key(styles)]
|
||||
|
||||
def _key(self, styles):
|
||||
if isinstance(styles, str):
|
||||
return (styles,)
|
||||
return tuple(sorted(styles))
|
||||
|
||||
def __getitem__(self, styles):
|
||||
key = self._key(styles)
|
||||
if key in self._renderers:
|
||||
return self._renderers[key]
|
||||
fr = RawFontRenderer(self.files[key])
|
||||
self._renderers[key] = fr
|
||||
return fr
|
||||
|
||||
def __repr__(self):
|
||||
return "<SystemFont %s>" % self.family
|
||||
|
||||
|
||||
class FontQuery:
|
||||
"""!
|
||||
@see https://www.freedesktop.org/software/fontconfig/fontconfig-user.html#AEN21
|
||||
https://manpages.ubuntu.com/manpages/cosmic/man1/fc-pattern.1.html
|
||||
"""
|
||||
def __init__(self, str=""):
|
||||
self._query = {}
|
||||
if isinstance(str, FontQuery):
|
||||
self._query = str._query.copy()
|
||||
elif str:
|
||||
chunks = str.split(":")
|
||||
family = chunks.pop(0)
|
||||
self._query = dict(
|
||||
chunk.split("=")
|
||||
for chunk in chunks
|
||||
if chunk
|
||||
)
|
||||
self.family(family)
|
||||
|
||||
def family(self, name):
|
||||
self._query["family"] = name
|
||||
return self
|
||||
|
||||
def weight(self, weight):
|
||||
self._query["weight"] = weight
|
||||
return self
|
||||
|
||||
def css_weight(self, weight):
|
||||
"""!
|
||||
Weight from CSS weight value.
|
||||
|
||||
Weight is different between CSS and fontconfig
|
||||
This creates some interpolations to ensure known values are translated properly
|
||||
@see https://www.freedesktop.org/software/fontconfig/fontconfig-user.html#AEN178
|
||||
https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#Common_weight_name_mapping
|
||||
"""
|
||||
if weight < 200:
|
||||
v = max(0, weight - 100) / 100 * 40
|
||||
elif weight < 500:
|
||||
v = -weight**3 / 200000 + weight**2 * 11/2000 - weight * 17/10 + 200
|
||||
elif weight < 700:
|
||||
v = -weight**2 * 3/1000 + weight * 41/10 - 1200
|
||||
else:
|
||||
v = (weight - 700) / 200 * 10 + 200
|
||||
return self.weight(int(round(v)))
|
||||
|
||||
def style(self, *styles):
|
||||
self._query["style"] = " ".join(styles)
|
||||
return self
|
||||
|
||||
def charset(self, *hex_ranges):
|
||||
self._query["charset"] = " ".join(hex_ranges)
|
||||
return self
|
||||
|
||||
def char(self, char):
|
||||
return self.charset("%x" % ord(char))
|
||||
|
||||
def custom(self, property, value):
|
||||
self._query[property] = value
|
||||
return self
|
||||
|
||||
def clone(self):
|
||||
return FontQuery(self)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._query.get(key, "")
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self._query
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self._query.get(key, default)
|
||||
|
||||
def __str__(self):
|
||||
return self._query.get("family", "") + ":" + ":".join(
|
||||
"%s=%s" % (p, v)
|
||||
for p, v in self._query.items()
|
||||
if p != "family"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "<FontQuery %r>" % str(self)
|
||||
|
||||
def weight_to_css(self):
|
||||
x = int(self["weight"])
|
||||
if x < 40:
|
||||
v = x / 40 * 100 + 100
|
||||
elif x < 100:
|
||||
v = x**3/300 - x**2 * 11/15 + x*167/3 - 3200/3
|
||||
elif x < 200:
|
||||
v = (2050 - 10 * math.sqrt(5) * math.sqrt(1205 - 6 * x)) / 3
|
||||
else:
|
||||
v = (x - 200) * 200 / 10 + 700
|
||||
return int(round(v))
|
||||
|
||||
|
||||
class _SystemFontList:
|
||||
def __init__(self):
|
||||
self.fonts = None
|
||||
|
||||
def _lazy_load(self):
|
||||
if self.fonts is None:
|
||||
self.load()
|
||||
|
||||
def load(self):
|
||||
self.fonts = {}
|
||||
self.load_fc_list()
|
||||
|
||||
def cmd(self, *a):
|
||||
p = subprocess.Popen(a, stdout=subprocess.PIPE)
|
||||
out, err = p.communicate()
|
||||
out = out.decode("utf-8").strip()
|
||||
return out, p.returncode
|
||||
|
||||
def load_fc_list(self):
|
||||
out, returncode = self.cmd("fc-list", r'--format=%{file}\t%{family[0]}\t%{style[0]}\n')
|
||||
if returncode == 0:
|
||||
for line in out.splitlines():
|
||||
file, family, styles = line.split("\t")
|
||||
self._get(family).add_file(styles.split(" "), file)
|
||||
|
||||
def best(self, query):
|
||||
"""!
|
||||
Returns the renderer best matching the name
|
||||
"""
|
||||
out, returncode = self.cmd("fc-match", r"--format=%{family}\t%{style}", str(query))
|
||||
if returncode == 0:
|
||||
return self._font_from_match(out)
|
||||
|
||||
def _font_from_match(self, out):
|
||||
fam, style = out.split("\t")
|
||||
fam = fam.split(",")[0]
|
||||
style = style.split(",")[0].split()
|
||||
return self[fam][style]
|
||||
|
||||
def all(self, query):
|
||||
"""!
|
||||
Yields all the renderers matching a query
|
||||
"""
|
||||
out, returncode = self.cmd("fc-match", "-s", r"--format=%{family}\t%{style}\n", str(query))
|
||||
if returncode == 0:
|
||||
for line in out.splitlines():
|
||||
try:
|
||||
yield self._font_from_match(line)
|
||||
except (fontTools.ttLib.TTLibError, fontTools.t1Lib.T1Error):
|
||||
pass
|
||||
|
||||
def default(self):
|
||||
"""!
|
||||
Returns the default fornt renderer
|
||||
"""
|
||||
return self.best()
|
||||
|
||||
def _get(self, family):
|
||||
self._lazy_load()
|
||||
if family in self.fonts:
|
||||
return self.fonts[family]
|
||||
font = SystemFont(family)
|
||||
self.fonts[family] = font
|
||||
return font
|
||||
|
||||
def __getitem__(self, key):
|
||||
self._lazy_load()
|
||||
return self.fonts[key]
|
||||
|
||||
def __iter__(self):
|
||||
self._lazy_load()
|
||||
return iter(self.fonts.values())
|
||||
|
||||
def keys(self):
|
||||
self._lazy_load()
|
||||
return self.fonts.keys()
|
||||
|
||||
def __contains__(self, item):
|
||||
self._lazy_load()
|
||||
return item in self.fonts
|
||||
|
||||
|
||||
## Dictionary of system fonts
|
||||
fonts = _SystemFontList()
|
||||
|
||||
|
||||
def collect_kerning_pairs(font):
|
||||
if "GPOS" not in font:
|
||||
return {}
|
||||
|
||||
gpos_table = font["GPOS"].table
|
||||
|
||||
unique_kern_lookups = set()
|
||||
for item in gpos_table.FeatureList.FeatureRecord:
|
||||
if item.FeatureTag == "kern":
|
||||
feature = item.Feature
|
||||
unique_kern_lookups |= set(feature.LookupListIndex)
|
||||
|
||||
kerning_pairs = {}
|
||||
for kern_lookup_index in sorted(unique_kern_lookups):
|
||||
lookup = gpos_table.LookupList.Lookup[kern_lookup_index]
|
||||
if lookup.LookupType in {2, 9}:
|
||||
for pairPos in lookup.SubTable:
|
||||
if pairPos.LookupType == 9: # extension table
|
||||
if pairPos.ExtensionLookupType == 8: # contextual
|
||||
continue
|
||||
elif pairPos.ExtensionLookupType == 2:
|
||||
pairPos = pairPos.ExtSubTable
|
||||
|
||||
if pairPos.Format != 1:
|
||||
continue
|
||||
|
||||
firstGlyphsList = pairPos.Coverage.glyphs
|
||||
for ps_index, _ in enumerate(pairPos.PairSet):
|
||||
for pairValueRecordItem in pairPos.PairSet[ps_index].PairValueRecord:
|
||||
secondGlyph = pairValueRecordItem.SecondGlyph
|
||||
valueFormat = pairPos.ValueFormat1
|
||||
|
||||
if valueFormat == 5: # RTL kerning
|
||||
kernValue = "<%d 0 %d 0>" % (
|
||||
pairValueRecordItem.Value1.XPlacement,
|
||||
pairValueRecordItem.Value1.XAdvance)
|
||||
elif valueFormat == 0: # RTL pair with value <0 0 0 0>
|
||||
kernValue = "<0 0 0 0>"
|
||||
elif valueFormat == 4: # LTR kerning
|
||||
kernValue = pairValueRecordItem.Value1.XAdvance
|
||||
else:
|
||||
print(
|
||||
"\tValueFormat1 = %d" % valueFormat,
|
||||
file=sys.stdout)
|
||||
continue # skip the rest
|
||||
|
||||
kerning_pairs[(firstGlyphsList[ps_index], secondGlyph)] = kernValue
|
||||
return kerning_pairs
|
||||
|
||||
|
||||
class GlyphMetrics:
|
||||
def __init__(self, glyph, lsb, aw, xmin, xmax):
|
||||
self.glyph = glyph
|
||||
self.lsb = lsb
|
||||
self.advance = aw
|
||||
self.xmin = xmin
|
||||
self.xmax = xmax
|
||||
self.width = xmax - xmin
|
||||
self.advance = xmax
|
||||
|
||||
def draw(self, pen):
|
||||
return self.glyph.draw(pen)
|
||||
|
||||
|
||||
class Font:
|
||||
def __init__(self, wrapped):
|
||||
self.wrapped = wrapped
|
||||
if isinstance(self.wrapped, fontTools.ttLib.TTFont):
|
||||
self.cmap = self.wrapped.getBestCmap() or {}
|
||||
else:
|
||||
self.cmap = {}
|
||||
|
||||
self.glyphset = self.wrapped.getGlyphSet()
|
||||
|
||||
@classmethod
|
||||
def open(cls, filename):
|
||||
try:
|
||||
f = fontTools.ttLib.TTFont(filename)
|
||||
except fontTools.ttLib.TTLibError:
|
||||
f = fontTools.t1Lib.T1Font(filename)
|
||||
f.parse()
|
||||
|
||||
return cls(f)
|
||||
|
||||
def getGlyphSet(self):
|
||||
return self.wrapped.getGlyphSet()
|
||||
|
||||
def getBestCmap(self):
|
||||
return {}
|
||||
|
||||
def glyph_name(self, codepoint):
|
||||
if isinstance(codepoint, str):
|
||||
if len(codepoint) != 1:
|
||||
return ""
|
||||
codepoint = ord(codepoint)
|
||||
|
||||
if codepoint in self.cmap:
|
||||
return self.cmap[codepoint]
|
||||
|
||||
return self.calculated_glyph_name(codepoint)
|
||||
|
||||
@staticmethod
|
||||
def calculated_glyph_name(codepoint):
|
||||
from fontTools import agl # Adobe Glyph List
|
||||
if codepoint in agl.UV2AGL:
|
||||
return agl.UV2AGL[codepoint]
|
||||
elif codepoint <= 0xFFFF:
|
||||
return "uni%04X" % codepoint
|
||||
else:
|
||||
return "u%X" % codepoint
|
||||
|
||||
def scale(self):
|
||||
if isinstance(self.wrapped, fontTools.ttLib.TTFont):
|
||||
return 1 / self.wrapped["head"].unitsPerEm
|
||||
elif isinstance(self.wrapped, fontTools.t1Lib.T1Font):
|
||||
return self.wrapped["FontMatrix"][0]
|
||||
|
||||
def yMax(self):
|
||||
if isinstance(self.wrapped, fontTools.ttLib.TTFont):
|
||||
return self.wrapped["head"].yMax
|
||||
elif isinstance(self.wrapped, fontTools.t1Lib.T1Font):
|
||||
return self.wrapped["FontBBox"][3]
|
||||
|
||||
def glyph(self, glyph_name):
|
||||
if isinstance(self.wrapped, fontTools.ttLib.TTFont):
|
||||
glyph = self.glyphset[glyph_name]
|
||||
|
||||
xmin = getattr(glyph._glyph, "xMin", glyph.lsb)
|
||||
xmax = getattr(glyph._glyph, "xMax", glyph.width)
|
||||
return GlyphMetrics(glyph, glyph.lsb, glyph.width, xmin, xmax)
|
||||
elif isinstance(self.wrapped, fontTools.t1Lib.T1Font):
|
||||
glyph = self.glyphset[glyph_name]
|
||||
bounds_pen = ControlBoundsPen(self.glyphset)
|
||||
bounds = bounds_pen.bounds
|
||||
glyph.draw(bounds_pen)
|
||||
if not hasattr(glyph, "width"):
|
||||
advance = bounds[2]
|
||||
else:
|
||||
advance = glyph.width
|
||||
return GlyphMetrics(glyph, bounds[0], advance, bounds[0], bounds[2])
|
||||
|
||||
def __contains__(self, key):
|
||||
if isinstance(self.wrapped, fontTools.t1Lib.T1Font):
|
||||
return key in self.wrapped.font
|
||||
return key in self.wrapped
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.wrapped[key]
|
||||
|
||||
|
||||
class FontRenderer:
|
||||
tab_width = 4
|
||||
|
||||
@property
|
||||
def font(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_query(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def kerning(self, c1, c2):
|
||||
return 0
|
||||
|
||||
def text_to_chars(self, text):
|
||||
return text
|
||||
|
||||
def _on_missing(self, char, size, pos, group):
|
||||
"""!
|
||||
- Character as string
|
||||
- Font size
|
||||
- [in, out] Character position
|
||||
- Group shape
|
||||
"""
|
||||
|
||||
def glyph_name(self, ch):
|
||||
return self.font.glyph_name(ch)
|
||||
|
||||
def scale(self, size):
|
||||
return size * self.font.scale()
|
||||
|
||||
def line_height(self, size):
|
||||
return self.font.yMax() * self.scale(size)
|
||||
|
||||
def ex(self, size):
|
||||
return self.font.glyph("x").advance * self.scale(size)
|
||||
|
||||
def glyph_beziers(self, glyph, offset=NVector(0, 0)):
|
||||
pen = BezierPen(self.font.glyphset, offset)
|
||||
glyph.draw(pen)
|
||||
return pen.beziers
|
||||
|
||||
def glyph_shapes(self, glyph, offset=NVector(0, 0)):
|
||||
beziers = self.glyph_beziers(glyph, offset)
|
||||
return [
|
||||
Path(bez)
|
||||
for bez in beziers
|
||||
]
|
||||
|
||||
def _on_character(self, ch, size, pos, scale, line, use_kerning, chars, i):
|
||||
chname = self.glyph_name(ch)
|
||||
|
||||
if chname in self.font.glyphset:
|
||||
glyphdata = self.font.glyph(chname)
|
||||
#pos.x += glyphdata.lsb * scale
|
||||
glyph_shapes = self.glyph_shapes(glyphdata, pos / scale)
|
||||
|
||||
if glyph_shapes:
|
||||
if len(glyph_shapes) > 1:
|
||||
glyph_shape_group = line.add_shape(Group())
|
||||
glyph_shape = glyph_shape_group
|
||||
else:
|
||||
glyph_shape_group = line
|
||||
glyph_shape = glyph_shapes[0]
|
||||
|
||||
for sh in glyph_shapes:
|
||||
sh.shape.value.scale(scale)
|
||||
glyph_shape_group.add_shape(sh)
|
||||
|
||||
glyph_shape.name = ch
|
||||
|
||||
kerning = 0
|
||||
if use_kerning and i < len(chars) - 1:
|
||||
nextcname = chars[i+1]
|
||||
kerning = self.kerning(chname, nextcname)
|
||||
|
||||
pos.x += (glyphdata.advance + kerning) * scale
|
||||
return True
|
||||
return False
|
||||
|
||||
def render(self, text, size, pos=None, use_kerning=True):
|
||||
"""!
|
||||
Renders some text
|
||||
|
||||
@param text String to render
|
||||
@param size Font size (in pizels)
|
||||
@param[in,out] pos Text position
|
||||
@param use_kerning Whether to honour kerning info from the font file
|
||||
|
||||
@returns a Group shape, augmented with some extra attributes:
|
||||
- line_height Line height
|
||||
- next_x X position of the next character
|
||||
"""
|
||||
scale = self.scale(size)
|
||||
line_height = self.line_height(size)
|
||||
group = Group()
|
||||
group.name = text
|
||||
if pos is None:
|
||||
pos = NVector(0, 0)
|
||||
start_x = pos.x
|
||||
line = Group()
|
||||
group.add_shape(line)
|
||||
#group.transform.scale.value = NVector(100, 100) * scale
|
||||
|
||||
chars = self.text_to_chars(text)
|
||||
for i, ch in enumerate(chars):
|
||||
if ch == "\n":
|
||||
line.next_x = pos.x
|
||||
pos.x = start_x
|
||||
pos.y += line_height
|
||||
line = Group()
|
||||
group.add_shape(line)
|
||||
continue
|
||||
elif ch == "\t":
|
||||
chname = self.glyph_name(ch)
|
||||
if chname in self.font.glyphset:
|
||||
width = self.font.glyph(chname).advance
|
||||
else:
|
||||
width = self.ex(size)
|
||||
pos.x += width * scale * self.tab_width
|
||||
continue
|
||||
|
||||
self._on_character(ch, size, pos, scale, line, use_kerning, chars, i)
|
||||
|
||||
group.line_height = line_height
|
||||
group.next_x = line.next_x = pos.x
|
||||
return group
|
||||
|
||||
|
||||
class RawFontRenderer(FontRenderer):
|
||||
def __init__(self, filename):
|
||||
self.filename = filename
|
||||
self._font = Font.open(filename)
|
||||
self._kerning = None
|
||||
|
||||
@property
|
||||
def font(self):
|
||||
return self._font
|
||||
|
||||
def kerning(self, c1, c2):
|
||||
if self._kerning is None:
|
||||
self._kerning = collect_kerning_pairs(self.font)
|
||||
return self._kerning.get((c1, c2), 0)
|
||||
|
||||
def __repr__(self):
|
||||
return "<FontRenderer %r>" % self.filename
|
||||
|
||||
def get_query(self):
|
||||
return self.filename
|
||||
|
||||
|
||||
class FallbackFontRenderer(FontRenderer):
|
||||
def __init__(self, query, max_attempts=10):
|
||||
self.query = FontQuery(query)
|
||||
self._best = None
|
||||
self._bq = None
|
||||
self._fallback = {}
|
||||
self.max_attempts = max_attempts
|
||||
|
||||
@property
|
||||
def font(self):
|
||||
return self.best.font
|
||||
|
||||
def get_query(self):
|
||||
return self.query
|
||||
|
||||
def ex(self, size):
|
||||
best = self.best
|
||||
if "x" not in self.font.glyphset:
|
||||
best = fonts.best(self.query.clone().char("x"))
|
||||
return best.ex(size)
|
||||
|
||||
@property
|
||||
def best(self):
|
||||
cq = str(self.query)
|
||||
if self._best is None or self._bq != cq:
|
||||
self._best = fonts.best(self.query)
|
||||
self._bq = cq
|
||||
return self._best
|
||||
|
||||
def fallback_renderer(self, char):
|
||||
if char in self._fallback:
|
||||
return self._fallback[char]
|
||||
|
||||
if len(char) != 1:
|
||||
return None
|
||||
|
||||
codepoint = ord(char)
|
||||
name = Font.calculated_glyph_name(codepoint)
|
||||
for i, font in enumerate(fonts.all(self.query.clone().char(char))):
|
||||
# For some reason fontconfig sometimes returns a font that doesn't
|
||||
# actually contain the glyph
|
||||
if name in font.font.glyphset or codepoint in font.cmap:
|
||||
self._fallback[char] = font
|
||||
return font
|
||||
|
||||
if i > self.max_attempts:
|
||||
self._fallback[char] = None
|
||||
return None
|
||||
|
||||
def _on_character(self, char, size, pos, scale, group, use_kerning, chars, i):
|
||||
if self.best._on_character(char, size, pos, scale, group, use_kerning, chars, i):
|
||||
return True
|
||||
|
||||
font = self.fallback_renderer(char)
|
||||
if not font:
|
||||
return False
|
||||
|
||||
child = font.render(char, size, pos)
|
||||
if len(child.shapes) == 2:
|
||||
group.add_shape(child.shapes[0])
|
||||
else:
|
||||
group.add_shape(child)
|
||||
|
||||
def __repr__(self):
|
||||
return "<FallbackFontRenderer %s>" % self.query
|
||||
|
||||
|
||||
class EmojiRenderer(FontRenderer):
|
||||
_split = None
|
||||
|
||||
def __init__(self, wrapped, emoji_dir):
|
||||
if not os.path.isdir(emoji_dir):
|
||||
raise Exception("Not a valid directory: %s" % emoji_dir)
|
||||
self.wrapped = wrapped
|
||||
self.emoji_dir = emoji_dir
|
||||
self._svgs = {}
|
||||
|
||||
@property
|
||||
def font(self):
|
||||
return self.wrapped.font
|
||||
|
||||
def _get_svg(self, char):
|
||||
from ..parsers.svg import parse_svg_file
|
||||
|
||||
if char in self._svgs:
|
||||
return self._svgs[char]
|
||||
|
||||
basename = "-".join("%x" % ord(cp) for cp in char) + ".svg"
|
||||
filename = os.path.join(self.emoji_dir, basename)
|
||||
if not os.path.isfile(filename):
|
||||
self._svgs[char] = None
|
||||
return None
|
||||
|
||||
svga = parse_svg_file(filename)
|
||||
svgshape = Group()
|
||||
svgshape.name = basename
|
||||
for layer in svga.layers:
|
||||
if isinstance(layer, ShapeLayer):
|
||||
for shape in layer.shapes:
|
||||
svgshape.add_shape(shape)
|
||||
|
||||
self._svgs[char] = svgshape
|
||||
svgshape._bbox = svgshape.bounding_box()
|
||||
return svgshape
|
||||
|
||||
def _on_character(self, char, size, pos, scale, group, use_kerning, chars, i):
|
||||
svgshape = self._get_svg(char)
|
||||
if svgshape:
|
||||
target_height = self.line_height(size)
|
||||
scale = target_height / svgshape._bbox.height
|
||||
shape_group = Group()
|
||||
shape_group = svgshape.clone()
|
||||
shape_group.transform.scale.value *= scale
|
||||
offset = NVector(
|
||||
-svgshape._bbox.x1 + svgshape._bbox.width * 0.075,
|
||||
-svgshape._bbox.y2 + svgshape._bbox.height * 0.1
|
||||
)
|
||||
shape_group.transform.position.value = pos + offset * scale
|
||||
group.add_shape(shape_group)
|
||||
pos.x += svgshape._bbox.width * scale
|
||||
return True
|
||||
return self.wrapped._on_character(char, size, pos, scale, group, use_kerning, chars, i)
|
||||
|
||||
def get_query(self):
|
||||
return self.wrapped.get_query()
|
||||
|
||||
@staticmethod
|
||||
def _get_splitter():
|
||||
if EmojiRenderer._split is None:
|
||||
try:
|
||||
import grapheme
|
||||
EmojiRenderer._split = grapheme.graphemes
|
||||
except ImportError:
|
||||
sys.stderr.write("Install `grapheme` for better Emoji support\n")
|
||||
EmojiRenderer._split = lambda x: x
|
||||
return EmojiRenderer._split
|
||||
|
||||
@staticmethod
|
||||
def emoji_split(string):
|
||||
return EmojiRenderer._get_splitter()(string)
|
||||
|
||||
def text_to_chars(self, string):
|
||||
return list(self.emoji_split(string))
|
||||
|
||||
|
||||
class FontStyle:
|
||||
def __init__(self, query, size, justify=TextJustify.Left, position=None, use_kerning=True, emoji_svg=None):
|
||||
self.emoji_svg = emoji_svg
|
||||
self._set_query(query)
|
||||
self.size = size
|
||||
self.justify = justify
|
||||
self.position = position.clone() if position else NVector(0, 0)
|
||||
self.use_kerning = use_kerning
|
||||
|
||||
def _set_query(self, query):
|
||||
if isinstance(query, str) and os.path.isfile(query):
|
||||
self._renderer = RawFontRenderer(query)
|
||||
else:
|
||||
self._renderer = FallbackFontRenderer(query)
|
||||
|
||||
if self.emoji_svg:
|
||||
self._renderer = EmojiRenderer(self._renderer, self.emoji_svg)
|
||||
|
||||
@property
|
||||
def query(self):
|
||||
return self._renderer.get_query()
|
||||
|
||||
@query.setter
|
||||
def query(self, value):
|
||||
if str(value) != str(self.query):
|
||||
self._set_query(value)
|
||||
|
||||
@property
|
||||
def renderer(self):
|
||||
return self._renderer
|
||||
|
||||
def render(self, text, pos=NVector(0, 0)):
|
||||
group = self._renderer.render(text, self.size, self.position+pos, self.use_kerning)
|
||||
for subg in group.shapes[:-1]:
|
||||
width = subg.next_x - self.position.x - pos.x
|
||||
if self.justify == TextJustify.Center:
|
||||
subg.transform.position.value.x -= width / 2
|
||||
elif self.justify == TextJustify.Right:
|
||||
subg.transform.position.value.x -= width
|
||||
return group
|
||||
|
||||
def clone(self):
|
||||
return FontStyle(str(self._renderer.query), self.size, self.justify, NVector(*self.position), self.use_kerning)
|
||||
|
||||
@property
|
||||
def ex(self):
|
||||
return self._renderer.ex(self.size)
|
||||
|
||||
@property
|
||||
def line_height(self):
|
||||
return self._renderer.line_height(self.size)
|
||||
|
||||
|
||||
def _propfac(a):
|
||||
return property(lambda s: s._get(a), lambda s, v: s._set(a, v))
|
||||
|
||||
|
||||
class FontShape(CustomObject):
|
||||
_props = [
|
||||
LottieProp("query_string", "_query", str),
|
||||
LottieProp("size", "_size", float),
|
||||
LottieProp("justify", "_justify", TextJustify),
|
||||
LottieProp("text", "_text", str),
|
||||
LottieProp("position", "_position", NVector),
|
||||
]
|
||||
wrapped_lottie = Group
|
||||
|
||||
def __init__(self, text="", query="", size=64, justify=TextJustify.Left):
|
||||
CustomObject.__init__(self)
|
||||
if isinstance(query, FontStyle):
|
||||
self.style = query
|
||||
else:
|
||||
self.style = FontStyle(query, size, justify)
|
||||
self.text = text
|
||||
self.hidden = None
|
||||
|
||||
def _get(self, a):
|
||||
return getattr(self.style, a)
|
||||
|
||||
def _set(self, a, v):
|
||||
return setattr(self.style, a, v)
|
||||
|
||||
query = _propfac("query")
|
||||
size = _propfac("size")
|
||||
justify = _propfac("justify")
|
||||
position = _propfac("position")
|
||||
|
||||
@property
|
||||
def query_string(self):
|
||||
return str(self.query)
|
||||
|
||||
@query_string.setter
|
||||
def query_string(self, v):
|
||||
self.query = v
|
||||
|
||||
def _build_wrapped(self):
|
||||
g = self.style.render(self.text)
|
||||
self.line_height = g.line_height
|
||||
return g
|
||||
|
||||
def bounding_box(self, time=0):
|
||||
return self.wrapped.bounding_box(time)
|
||||
@@ -0,0 +1,97 @@
|
||||
from..nvector import NVector
|
||||
|
||||
|
||||
# FABRIK
|
||||
class Chain:
|
||||
def __init__(self, tail, fixed_tail=True, tolerance=0.5, max_iter=8):
|
||||
self.joints = [tail.clone()]
|
||||
self.fixed_tail = fixed_tail
|
||||
self.lengths = []
|
||||
self.total_length = 0
|
||||
self.tolerance = tolerance
|
||||
self.max_iter = max_iter
|
||||
|
||||
def add_joint(self, point):
|
||||
length = (point - self.joints[-1]).length
|
||||
self.lengths.append(length)
|
||||
self.total_length += length
|
||||
self.joints.append(point.clone())
|
||||
|
||||
def add_joints(self, head, n):
|
||||
delta = head - self.joints[-1]
|
||||
self.total_length += delta.length
|
||||
segment = delta / n
|
||||
seglen = segment.length
|
||||
for i in range(n):
|
||||
self.lengths.append(seglen)
|
||||
self.joints.append(self.joints[-1] + segment)
|
||||
|
||||
def backward(self, target):
|
||||
"""!
|
||||
target -> -> start
|
||||
"""
|
||||
self.joints[-1] = target
|
||||
for i in range(len(self.joints)-2, -1, -1):
|
||||
r = self.joints[i+1] - self.joints[i]
|
||||
l = self.lengths[i] / r.length
|
||||
self.joints[i] = self.joints[i+1].lerp(self.joints[i], l)
|
||||
|
||||
def forward(self, target):
|
||||
"""!
|
||||
start -> -> tail
|
||||
"""
|
||||
self.joints[0] = target
|
||||
for i in range(0, len(self.joints)-1):
|
||||
r = self.joints[i+1] - self.joints[i]
|
||||
l = self.lengths[i] / r.length
|
||||
self.joints[i+1] = self.joints[i].lerp(self.joints[i+1], l)
|
||||
|
||||
def reach(self, target):
|
||||
if not self.fixed_tail:
|
||||
self.backward(target)
|
||||
return
|
||||
|
||||
distance = (target - self.joints[0]).length
|
||||
if distance >= self.total_length:
|
||||
for i in range(len(self.joints)-1):
|
||||
r = target - self.joints[i]
|
||||
l = self.lengths[i] / r.length
|
||||
self.joints[i+1] = self.joints[i].lerp(target, l)
|
||||
return
|
||||
|
||||
base = self.joints[0]
|
||||
|
||||
distance = (target - self.joints[-1]).length
|
||||
n_it = 0
|
||||
while distance > self.tolerance and n_it < self.max_iter:
|
||||
self.backward(target)
|
||||
self.forward(base)
|
||||
distance = (target - self.joints[-1]).length
|
||||
n_it += 1
|
||||
|
||||
|
||||
class Octopus:
|
||||
def __init__(self, master):
|
||||
self.chains = {"master": master}
|
||||
self.master = master
|
||||
|
||||
@property
|
||||
def base(self):
|
||||
return self.master.joints[-1]
|
||||
|
||||
def add_chain(self, name):
|
||||
ch = Chain(self.base)
|
||||
self.chains[name] = ch
|
||||
return ch
|
||||
|
||||
def reach(self, target_map):
|
||||
centroid = NVector(0, 0)
|
||||
for chain, target in target_map.items():
|
||||
self.chains[chain].backward(target)
|
||||
centroid += self.chains[chain].joints[0]
|
||||
centroid /= len(target_map)
|
||||
|
||||
self.master.reach(centroid)
|
||||
|
||||
for chain in target_map.keys():
|
||||
self.chains[chain].forward(self.base)
|
||||
@@ -0,0 +1,38 @@
|
||||
from io import StringIO
|
||||
from difflib import SequenceMatcher
|
||||
from ..exporters import prettyprint
|
||||
|
||||
|
||||
def difflines_str(a, b, widtha=None, widthb=None):
|
||||
lines_a = a.splitlines()
|
||||
lines_b = b.splitlines()
|
||||
ia = 0
|
||||
ib = 0
|
||||
|
||||
if widtha is None:
|
||||
widtha = max(map(len, lines_a))
|
||||
if widthb is None:
|
||||
widthb = max(map(len, lines_b))
|
||||
|
||||
for ja, jb, size in SequenceMatcher(None, lines_a, lines_b, False).get_matching_blocks():
|
||||
sideprinter(lines_a[ia:ja], lines_b[ib:jb], widtha, widthb, "\x1b[31m>", "=", "<\x1b[m")
|
||||
ia = ja+size
|
||||
ib = jb+size
|
||||
sideprinter(lines_a[ja:ia], lines_b[jb:ib], widtha, widthb, "\x1b[m ", "|", " \x1b[m")
|
||||
|
||||
|
||||
def sideprinter(left, right, widtha=40, widthb=40, prefix="", infix=" | ", suffix=""):
|
||||
if len(left) > len(right):
|
||||
right += [""] * (len(left) - len(right))
|
||||
else:
|
||||
left += [""] * (len(right) - len(left))
|
||||
for l, r in zip(left, right):
|
||||
print("".join([prefix, l[:widtha].ljust(widtha), infix, r[:widthb].ljust(widthb), suffix]))
|
||||
|
||||
|
||||
def difflines(a, b, widtha=None, widthb=None):
|
||||
ioa = StringIO()
|
||||
prettyprint(a, ioa)
|
||||
iob = StringIO()
|
||||
prettyprint(b, iob)
|
||||
difflines_str(ioa.getvalue(), iob.getvalue(), widtha, widthb)
|
||||
@@ -0,0 +1,229 @@
|
||||
from .. import objects
|
||||
|
||||
|
||||
class RestructuredLayer:
|
||||
def __init__(self, lottie):
|
||||
self.lottie = lottie
|
||||
self.children_pre = []
|
||||
self.children_post = []
|
||||
self.structured = False
|
||||
self.shapegroup = None
|
||||
self.matte_target = False
|
||||
self.matte_source = None
|
||||
self.matte_id = None
|
||||
|
||||
def add(self, child):
|
||||
c = self.children_pre if self.structured else self.children_post
|
||||
c.insert(0, child)
|
||||
|
||||
|
||||
class RestructuredShapeGroup:
|
||||
def __init__(self, lottie):
|
||||
self.lottie = lottie
|
||||
self.children = []
|
||||
self.fill = None
|
||||
self.stroke = None
|
||||
self.layer = False
|
||||
self.paths = None
|
||||
self.stroke_above = False
|
||||
|
||||
def empty(self):
|
||||
return not self.children
|
||||
|
||||
def finalize(self, thresh=6):
|
||||
for g in self.subgroups:
|
||||
if g.layer:
|
||||
self.layer = True
|
||||
for gg in self.subgroups:
|
||||
gg.layer = True
|
||||
return
|
||||
nchild = len(self.children)
|
||||
self.layer = nchild > thresh and self.lottie.name
|
||||
|
||||
@property
|
||||
def subgroups(self):
|
||||
for g in self.children:
|
||||
if isinstance(g, RestructuredShapeGroup):
|
||||
yield g
|
||||
|
||||
def add(self, child):
|
||||
self.children.insert(0, child)
|
||||
|
||||
|
||||
class RestructuredModifier:
|
||||
def __init__(self, lottie, child):
|
||||
self.child = child
|
||||
self.lottie = lottie
|
||||
|
||||
|
||||
class RestructuredPathMerger:
|
||||
def __init__(self):
|
||||
self.paths = []
|
||||
|
||||
def append(self, path):
|
||||
self.paths.append(path)
|
||||
|
||||
|
||||
class RestructuredAnimation:
|
||||
def __init__(self):
|
||||
self.layers = []
|
||||
self.precomp = {}
|
||||
|
||||
|
||||
class AbstractBuilder:
|
||||
merge_paths = False
|
||||
|
||||
def _on_animation(self, animation):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _on_shapegroup(self, shapegroup, out_parent):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _on_shape(self, shape, shapegroup, out_parent):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _on_merged_path(self, shape, shapegroup, out_parent):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _on_shape_modifier(self, shape, shapegroup, out_parent):
|
||||
raise NotImplementedError()
|
||||
|
||||
def process(self, animation: objects.Animation):
|
||||
out_parent = self._on_animation(animation)
|
||||
|
||||
restructured = self.restructure_animation(animation, self.merge_paths)
|
||||
for id, layers in restructured.precomp.items():
|
||||
self._on_precomp(id, out_parent, layers)
|
||||
|
||||
for asset in animation.assets or []:
|
||||
self._on_asset(asset)
|
||||
|
||||
for layer_builder in restructured.layers:
|
||||
self.process_layer(layer_builder, out_parent)
|
||||
|
||||
def _on_layer(self, layer_builder, out_parent):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _on_precomp(self, id, out_parent, layers):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _on_asset(self, asset):
|
||||
pass
|
||||
|
||||
def process_layer(self, layer_builder, out_parent):
|
||||
out_layer = self._on_layer(layer_builder, out_parent)
|
||||
|
||||
if out_layer is None:
|
||||
return
|
||||
|
||||
for c in layer_builder.children_pre:
|
||||
self.process_layer(c, out_layer)
|
||||
|
||||
shapegroup = getattr(layer_builder, "shapegroup", None)
|
||||
if shapegroup:
|
||||
self.shapegroup_process_children(shapegroup, out_layer)
|
||||
|
||||
for c in layer_builder.children_post:
|
||||
self.process_layer(c, out_layer)
|
||||
|
||||
self._on_layer_end(out_layer)
|
||||
|
||||
def _on_layer_end(self, out_layer):
|
||||
pass
|
||||
|
||||
def shapegroup_process_child(self, shape, shapegroup, out_parent):
|
||||
if isinstance(shape, RestructuredShapeGroup):
|
||||
return self._on_shapegroup(shape, out_parent)
|
||||
elif isinstance(shape, RestructuredPathMerger):
|
||||
return self._on_merged_path(shape, shapegroup, out_parent)
|
||||
elif isinstance(shape, RestructuredModifier):
|
||||
return self._on_shape_modifier(shape, shapegroup, out_parent)
|
||||
else:
|
||||
return self._on_shape(shape, shapegroup, out_parent)
|
||||
|
||||
def shapegroup_process_children(self, shapegroup, out_parent):
|
||||
for shape in shapegroup.children:
|
||||
self.shapegroup_process_child(shape, shapegroup, out_parent)
|
||||
|
||||
def restructure_animation(self, animation, merge_paths):
|
||||
restr = RestructuredAnimation()
|
||||
restr.layers = self.restructure_layer_list(animation.layers, merge_paths)
|
||||
if animation.assets:
|
||||
for asset in animation.assets:
|
||||
if isinstance(asset, objects.Precomp):
|
||||
restr.precomp[asset.id] = self.restructure_layer_list(asset.layers, merge_paths)
|
||||
return restr
|
||||
|
||||
def restructure_layer_list(self, layer_list, merge_paths):
|
||||
layers = {}
|
||||
flat_layers = []
|
||||
prev = None
|
||||
for layer in layer_list:
|
||||
laybuilder = RestructuredLayer(layer)
|
||||
flat_layers.append(laybuilder)
|
||||
|
||||
if layer.index is not None:
|
||||
layers[layer.index] = laybuilder
|
||||
|
||||
if isinstance(layer, objects.ShapeLayer):
|
||||
laybuilder.shapegroup = RestructuredShapeGroup(layer)
|
||||
laybuilder.layer = True
|
||||
for shape in layer.shapes:
|
||||
self.restructure_shapegroup(shape, laybuilder.shapegroup, merge_paths)
|
||||
laybuilder.shapegroup.finalize()
|
||||
|
||||
if layer.matte_mode not in {None, objects.MatteMode.Normal}:
|
||||
laybuilder.matte_source = prev
|
||||
if prev:
|
||||
prev.matte_target = laybuilder
|
||||
|
||||
prev = laybuilder
|
||||
|
||||
top_layers = []
|
||||
for layer in flat_layers:
|
||||
layer.structured = True
|
||||
if layer.lottie.parent_index is not None:
|
||||
layers[layer.lottie.parent_index].add(layer)
|
||||
else:
|
||||
top_layers.insert(0, layer)
|
||||
|
||||
return top_layers
|
||||
|
||||
def restructure_shapegroup(self, shape, shape_group, merge_paths):
|
||||
if isinstance(shape, (objects.Fill, objects.GradientFill)):
|
||||
if not shape_group.fill:
|
||||
shape_group.fill = shape
|
||||
elif isinstance(shape, objects.BaseStroke):
|
||||
if not shape_group.stroke or shape_group.stroke.width.get_value(0) < shape.width.get_value(0):
|
||||
shape_group.stroke = shape
|
||||
if not shape_group.fill:
|
||||
shape_group.stroke_above = True
|
||||
elif isinstance(shape, (objects.Path)):
|
||||
if merge_paths:
|
||||
if not shape_group.paths:
|
||||
shape_group.paths = RestructuredPathMerger()
|
||||
shape_group.add(shape_group.paths)
|
||||
shape_group.paths.append(shape)
|
||||
else:
|
||||
shape_group.add(shape)
|
||||
elif isinstance(shape, (objects.Group)):
|
||||
subgroup = RestructuredShapeGroup(shape)
|
||||
shape_group.add(subgroup)
|
||||
merge_paths = self.merge_paths and not any(isinstance(p, objects.Group) for p in shape.shapes)
|
||||
for subshape in shape.shapes:
|
||||
self.restructure_shapegroup(subshape, subgroup, merge_paths)
|
||||
subgroup.finalize()
|
||||
elif isinstance(shape, (objects.Modifier)):
|
||||
if shape_group.children:
|
||||
ch = shape_group.children.pop(0)
|
||||
shape_group.add(RestructuredModifier(shape, ch))
|
||||
elif isinstance(shape, (objects.ShapeElement)):
|
||||
shape_group.add(shape)
|
||||
elif isinstance(shape, (objects.base.CustomObject)):
|
||||
if self._custom_object_supported(shape):
|
||||
shape_group.add(shape)
|
||||
else:
|
||||
self.restructure_shapegroup(shape.wrapped, shape_group, self.merge_paths)
|
||||
|
||||
def _custom_object_supported(self, shape):
|
||||
return False
|
||||
@@ -0,0 +1,82 @@
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import inspect
|
||||
from ..exporters import exporters
|
||||
from .stripper import float_strip
|
||||
|
||||
|
||||
def _get_caller():
|
||||
return inspect.getmodule(inspect.currentframe().f_back.f_back)
|
||||
|
||||
|
||||
def _get_parser(caller, basename, path, formats, verbosity):
|
||||
if basename is None:
|
||||
basename = os.path.splitext(os.path.basename(caller.__file__))[0]
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
conflict_handler='resolve'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--name",
|
||||
"-n",
|
||||
default=basename,
|
||||
help="Output basename",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--path",
|
||||
default=path,
|
||||
help="Output path",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--formats", "-f",
|
||||
nargs="+",
|
||||
choices=list(sum((e.extensions for e in exporters), [])),
|
||||
default=formats,
|
||||
help="Formates to render",
|
||||
metavar="format"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--verbosity",
|
||||
type=int,
|
||||
default=int(verbosity)
|
||||
)
|
||||
from .. import __version__
|
||||
parser.add_argument(
|
||||
"--version", "-v",
|
||||
action="version",
|
||||
version="%(prog)s - python-lottie script " + __version__
|
||||
)
|
||||
exporters.set_options(parser)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def get_parser(basename=None, path="/tmp", formats=["html"], verbosity=1):
|
||||
caller = _get_caller()
|
||||
return _get_parser(caller, basename, path, formats, verbosity)
|
||||
|
||||
|
||||
def run(animation, ns):
|
||||
for fmt in ns.formats:
|
||||
if ns.path == "" and ns.name == "-":
|
||||
outfile = sys.stdout
|
||||
else:
|
||||
absname = os.path.abspath(os.path.join(ns.path, ns.name + "." + fmt))
|
||||
if ns.verbosity:
|
||||
sys.stderr.write("file://%s\n" % absname)
|
||||
outfile = absname
|
||||
exporter = exporters.get_from_extension(fmt)
|
||||
exporter.process(animation, outfile, **exporter.argparse_options(ns))
|
||||
|
||||
|
||||
def script_main(animation, basename=None, path="/tmp", formats=["html"], verbosity=1, strip=float_strip):
|
||||
"""
|
||||
Sets up a script to output an animation into various formats
|
||||
"""
|
||||
caller = _get_caller()
|
||||
if caller and caller.__name__ == "__main__":
|
||||
parser = _get_parser(caller, basename, path, formats, verbosity)
|
||||
strip(animation)
|
||||
run(animation, parser.parse_args())
|
||||
@@ -0,0 +1,52 @@
|
||||
from ..objects.base import LottieObject, ObjectVisitor
|
||||
from ..objects.bezier import Bezier
|
||||
from ..objects.helpers import Transform
|
||||
from ..nvector import NVector
|
||||
|
||||
|
||||
class Strip(ObjectVisitor):
|
||||
def __init__(self, float_round, remove_attributes={}):
|
||||
self.float_round = float_round
|
||||
self.remove_attributes = remove_attributes
|
||||
|
||||
def round(self, fl):
|
||||
return round(fl, self.float_round)
|
||||
|
||||
def nvector(self, value):
|
||||
value.components = list(map(self.round, value.components))
|
||||
return value
|
||||
|
||||
def visit_property(self, object, property, value):
|
||||
if isinstance(value, Bezier):
|
||||
for l in ["vertices", "in_tangents", "out_tangents"]:
|
||||
try:
|
||||
setattr(value, l, [self.nvector(NVector(p.x, p.y)) for p in getattr(value, l)])
|
||||
except:
|
||||
print("An exception occurred")
|
||||
elif property.lottie in self.remove_attributes:
|
||||
property.set(object, None)
|
||||
elif isinstance(value, float):
|
||||
property.set(object, round(value, 3))
|
||||
elif isinstance(value, NVector):
|
||||
self.nvector(value)
|
||||
|
||||
|
||||
class TransformStip(Strip):
|
||||
def visit(self, object):
|
||||
if isinstance(object, Transform):
|
||||
self.transform_unset(object, "anchor_point", NVector(0, 0))
|
||||
self.transform_unset(object, "position", NVector(0, 0))
|
||||
#self.transform_unset(object, "scale", NVector(100, 100))
|
||||
self.transform_unset(object, "rotation", 0)
|
||||
#self.transform_unset(object, "opacity", 100)
|
||||
self.transform_unset(object, "skew", 0)
|
||||
self.transform_unset(object, "skew_axis", 0)
|
||||
|
||||
def transform_unset(self, object, prop_name, value):
|
||||
prop = getattr(object, prop_name)
|
||||
if not prop.animated and prop.value == value:
|
||||
setattr(object, prop_name, None)
|
||||
|
||||
|
||||
heavy_strip = TransformStip(3, {"ind", "ix", "nm", "mn"})
|
||||
float_strip = Strip(3)
|
||||
@@ -0,0 +1,205 @@
|
||||
import math
|
||||
from ..nvector import NVector
|
||||
|
||||
|
||||
def _sign(x):
|
||||
if x < 0:
|
||||
return -1
|
||||
return 1
|
||||
|
||||
|
||||
class TransformMatrix:
|
||||
scalar = float
|
||||
|
||||
def __init__(self):
|
||||
""" Creates an Identity matrix """
|
||||
self.to_identity()
|
||||
|
||||
def clone(self):
|
||||
m = TransformMatrix()
|
||||
m._mat = list(self._mat)
|
||||
return m
|
||||
|
||||
return self
|
||||
|
||||
def __getitem__(self, key):
|
||||
row, col = key
|
||||
return self._mat[row*4+col]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
row, col = key
|
||||
self._mat[row*4+col] = self.scalar(value)
|
||||
|
||||
@property
|
||||
def a(self):
|
||||
return self[0, 0]
|
||||
|
||||
@a.setter
|
||||
def a(self, v):
|
||||
self[0, 0] = self.scalar(v)
|
||||
|
||||
@property
|
||||
def b(self):
|
||||
return self[0, 1]
|
||||
|
||||
@b.setter
|
||||
def b(self, v):
|
||||
self[0, 1] = self.scalar(v)
|
||||
|
||||
@property
|
||||
def c(self):
|
||||
return self[1, 0]
|
||||
|
||||
@c.setter
|
||||
def c(self, v):
|
||||
self[1, 0] = self.scalar(v)
|
||||
|
||||
@property
|
||||
def d(self):
|
||||
return self[1, 1]
|
||||
|
||||
@d.setter
|
||||
def d(self, v):
|
||||
self[1, 1] = self.scalar(v)
|
||||
|
||||
@property
|
||||
def tx(self):
|
||||
return self[3, 0]
|
||||
|
||||
@tx.setter
|
||||
def tx(self, v):
|
||||
self[3, 0] = self.scalar(v)
|
||||
|
||||
@property
|
||||
def ty(self):
|
||||
return self[3, 1]
|
||||
|
||||
@ty.setter
|
||||
def ty(self, v):
|
||||
self[3, 1] = self.scalar(v)
|
||||
|
||||
def __str__(self):
|
||||
return str(self._mat)
|
||||
|
||||
def scale(self, x, y=None):
|
||||
if y is None:
|
||||
y = x
|
||||
|
||||
m = TransformMatrix()
|
||||
m.a = x
|
||||
m.d = y
|
||||
self *= m
|
||||
return self
|
||||
|
||||
def translate(self, x, y=None):
|
||||
if y is None:
|
||||
x, y = x
|
||||
m = TransformMatrix()
|
||||
m.tx = x
|
||||
m.ty = y
|
||||
self *= m
|
||||
return self
|
||||
|
||||
def skew(self, x_rad, y_rad):
|
||||
m = TransformMatrix()
|
||||
m.c = math.tan(x_rad)
|
||||
m.b = math.tan(y_rad)
|
||||
self *= m
|
||||
return self
|
||||
|
||||
def skew_from_axis(self, skew, axis):
|
||||
self.rotate(axis)
|
||||
m = TransformMatrix()
|
||||
m.c = math.tan(skew)
|
||||
self *= m
|
||||
self.rotate(-axis)
|
||||
return self
|
||||
|
||||
def row(self, i):
|
||||
return NVector(self[i, 0], self[i, 1], self[i, 2], self[i, 3])
|
||||
|
||||
def column(self, i):
|
||||
return NVector(self[0, i], self[1, i], self[2, i], self[3, i])
|
||||
|
||||
def to_identity(self):
|
||||
self._mat = [
|
||||
1., 0., 0., 0.,
|
||||
0., 1., 0., 0.,
|
||||
0., 0., 1., 0.,
|
||||
0., 0., 0., 1.,
|
||||
]
|
||||
|
||||
def apply(self, vector):
|
||||
vector3 = NVector(vector.x, vector.y, 0, 1)
|
||||
return NVector(
|
||||
self.column(0).dot(vector3),
|
||||
self.column(1).dot(vector3),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def rotation(cls, radians):
|
||||
m = cls()
|
||||
m.a = math.cos(radians)
|
||||
m.b = -math.sin(radians)
|
||||
m.c = math.sin(radians)
|
||||
m.d = math.cos(radians)
|
||||
|
||||
return m
|
||||
|
||||
def __mul__(self, other):
|
||||
m = TransformMatrix()
|
||||
for row in range(4):
|
||||
for col in range(4):
|
||||
m[row, col] = self.row(row).dot(other.column(col))
|
||||
return m
|
||||
|
||||
def __imul__(self, other):
|
||||
m = self * other
|
||||
self._mat = m._mat
|
||||
return self
|
||||
|
||||
def rotate(self, radians):
|
||||
self *= TransformMatrix.rotation(radians)
|
||||
return self
|
||||
|
||||
def extract_transform(self):
|
||||
a = self.a
|
||||
b = self.b
|
||||
c = self.c
|
||||
d = self.d
|
||||
tx = self.tx
|
||||
ty = self.ty
|
||||
|
||||
dest_trans = {
|
||||
"translation": NVector(tx, ty),
|
||||
"angle": 0,
|
||||
"scale": NVector(1, 1),
|
||||
"skew_axis": 0,
|
||||
"skew_angle": 0,
|
||||
}
|
||||
|
||||
delta = a * d - b * c
|
||||
if a != 0 or b != 0:
|
||||
r = math.hypot(a, b)
|
||||
dest_trans["angle"] = - _sign(b) * math.acos(a/r)
|
||||
sx = r
|
||||
sy = delta / r
|
||||
dest_trans["skew_axis"] = 0
|
||||
else:
|
||||
r = math.hypot(c, d)
|
||||
dest_trans["angle"] = math.pi / 2 + _sign(d) * math.acos(c / r)
|
||||
sx = delta / r
|
||||
sy = r
|
||||
dest_trans["skew_axis"] = math.pi / 2
|
||||
|
||||
dest_trans["scale"] = NVector(sx, sy)
|
||||
|
||||
skew = math.atan2((a * c + b * d), r * r)
|
||||
dest_trans["skew_angle"] = skew
|
||||
|
||||
return dest_trans
|
||||
|
||||
def to_css_2d(self):
|
||||
return "matrix(%s, %s, %s, %s, %s, %s)" % (
|
||||
self.a, self.b, self.c, self.d, self.tx, self.ty
|
||||
)
|
||||
Reference in New Issue
Block a user