feat: анимации с реанимации
This commit is contained in:
@@ -0,0 +1,660 @@
|
||||
"""Собирает lottie-морфы иконок композера прямо из шрифта Material Symbols.
|
||||
|
||||
Контуры глифов берутся из MaterialSymbolsOutlined.ttf (инстанс по умолчанию —
|
||||
FILL 0, GRAD 0, opsz 24, wght 400, то есть ровно то, что рисует Icon в приложении),
|
||||
разбиваются на равное число безье-сегментов и попарно сопоставляются, чтобы
|
||||
lottie мог интерполировать один глиф в другой.
|
||||
|
||||
python3 tool/make_morph_icons.py
|
||||
|
||||
Пересобирать нужно после обновления material_symbols_icons.
|
||||
"""
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import struct
|
||||
|
||||
class Font:
|
||||
def __init__(self, path):
|
||||
self.data = open(path, 'rb').read()
|
||||
self.tables = {}
|
||||
num_tables = struct.unpack('>H', self.data[4:6])[0]
|
||||
for i in range(num_tables):
|
||||
off = 12 + i * 16
|
||||
tag = self.data[off:off + 4].decode('latin1')
|
||||
t_off, t_len = struct.unpack('>II', self.data[off + 8:off + 16])
|
||||
self.tables[tag] = (t_off, t_len)
|
||||
|
||||
head_off = self.tables['head'][0]
|
||||
self.units_per_em = struct.unpack(
|
||||
'>H', self.data[head_off + 18:head_off + 20])[0]
|
||||
self.index_to_loc = struct.unpack(
|
||||
'>h', self.data[head_off + 50:head_off + 52])[0]
|
||||
maxp_off = self.tables['maxp'][0]
|
||||
self.num_glyphs = struct.unpack(
|
||||
'>H', self.data[maxp_off + 4:maxp_off + 6])[0]
|
||||
self._read_loca()
|
||||
self._read_cmap()
|
||||
|
||||
def _read_loca(self):
|
||||
off, _ = self.tables['loca']
|
||||
n = self.num_glyphs + 1
|
||||
if self.index_to_loc == 0:
|
||||
raw = struct.unpack('>%dH' % n, self.data[off:off + 2 * n])
|
||||
self.loca = [v * 2 for v in raw]
|
||||
else:
|
||||
self.loca = list(struct.unpack('>%dI' % n, self.data[off:off + 4 * n]))
|
||||
|
||||
def _read_cmap(self):
|
||||
off, _ = self.tables['cmap']
|
||||
n = struct.unpack('>H', self.data[off + 2:off + 4])[0]
|
||||
best = None
|
||||
for i in range(n):
|
||||
rec = off + 4 + i * 8
|
||||
pid, eid, sub = struct.unpack('>HHI', self.data[rec:rec + 8])
|
||||
fmt = struct.unpack('>H', self.data[off + sub:off + sub + 2])[0]
|
||||
if fmt in (4, 12):
|
||||
if best is None or fmt == 12:
|
||||
best = (fmt, off + sub)
|
||||
fmt, sub = best
|
||||
self.cmap = {}
|
||||
if fmt == 4:
|
||||
seg_x2 = struct.unpack('>H', self.data[sub + 6:sub + 8])[0]
|
||||
seg = seg_x2 // 2
|
||||
base = sub + 14
|
||||
ends = struct.unpack('>%dH' % seg, self.data[base:base + seg_x2])
|
||||
base += seg_x2 + 2
|
||||
starts = struct.unpack('>%dH' % seg, self.data[base:base + seg_x2])
|
||||
base += seg_x2
|
||||
deltas = struct.unpack('>%dh' % seg, self.data[base:base + seg_x2])
|
||||
range_off_pos = base + seg_x2
|
||||
offsets = struct.unpack(
|
||||
'>%dH' % seg, self.data[range_off_pos:range_off_pos + seg_x2])
|
||||
for i in range(seg):
|
||||
for c in range(starts[i], min(ends[i], 0xFFFF) + 1):
|
||||
if offsets[i] == 0:
|
||||
gid = (c + deltas[i]) & 0xFFFF
|
||||
else:
|
||||
p = range_off_pos + i * 2 + offsets[i] + (c - starts[i]) * 2
|
||||
gid = struct.unpack('>H', self.data[p:p + 2])[0]
|
||||
if gid:
|
||||
gid = (gid + deltas[i]) & 0xFFFF
|
||||
if gid:
|
||||
self.cmap[c] = gid
|
||||
else:
|
||||
n_groups = struct.unpack('>I', self.data[sub + 12:sub + 16])[0]
|
||||
for i in range(n_groups):
|
||||
p = sub + 16 + i * 12
|
||||
s, e, g = struct.unpack('>III', self.data[p:p + 12])
|
||||
for c in range(s, e + 1):
|
||||
self.cmap[c] = g + (c - s)
|
||||
|
||||
def contours(self, codepoint):
|
||||
gid = self.cmap[codepoint]
|
||||
return self._glyph_contours(gid)
|
||||
|
||||
def _glyph_contours(self, gid, depth=0):
|
||||
goff, _ = self.tables['glyf']
|
||||
start, end = self.loca[gid], self.loca[gid + 1]
|
||||
if start == end:
|
||||
return []
|
||||
d = self.data[goff + start:goff + end]
|
||||
n_contours = struct.unpack('>h', d[0:2])[0]
|
||||
if n_contours < 0:
|
||||
return self._composite(d, depth)
|
||||
|
||||
end_pts = struct.unpack('>%dH' % n_contours, d[10:10 + 2 * n_contours])
|
||||
n_points = end_pts[-1] + 1
|
||||
p = 10 + 2 * n_contours
|
||||
instr_len = struct.unpack('>H', d[p:p + 2])[0]
|
||||
p += 2 + instr_len
|
||||
|
||||
flags = []
|
||||
while len(flags) < n_points:
|
||||
f = d[p]
|
||||
p += 1
|
||||
flags.append(f)
|
||||
if f & 8:
|
||||
rep = d[p]
|
||||
p += 1
|
||||
flags.extend([f] * rep)
|
||||
flags = flags[:n_points]
|
||||
|
||||
xs, x = [], 0
|
||||
for f in flags:
|
||||
if f & 2:
|
||||
dx = d[p]
|
||||
p += 1
|
||||
x += dx if f & 16 else -dx
|
||||
elif not f & 16:
|
||||
dx = struct.unpack('>h', d[p:p + 2])[0]
|
||||
p += 2
|
||||
x += dx
|
||||
xs.append(x)
|
||||
|
||||
ys, y = [], 0
|
||||
for f in flags:
|
||||
if f & 4:
|
||||
dy = d[p]
|
||||
p += 1
|
||||
y += dy if f & 32 else -dy
|
||||
elif not f & 32:
|
||||
dy = struct.unpack('>h', d[p:p + 2])[0]
|
||||
p += 2
|
||||
y += dy
|
||||
ys.append(y)
|
||||
|
||||
out, first = [], 0
|
||||
for e in end_pts:
|
||||
pts = [(xs[i], ys[i], bool(flags[i] & 1)) for i in range(first, e + 1)]
|
||||
if pts:
|
||||
out.append(pts)
|
||||
first = e + 1
|
||||
return out
|
||||
|
||||
def _composite(self, d, depth):
|
||||
if depth > 4:
|
||||
return []
|
||||
out = []
|
||||
p = 10
|
||||
while True:
|
||||
flags, glyph_index = struct.unpack('>HH', d[p:p + 4])
|
||||
p += 4
|
||||
if flags & 1:
|
||||
a1, a2 = struct.unpack('>hh', d[p:p + 4])
|
||||
p += 4
|
||||
else:
|
||||
a1, a2 = struct.unpack('>bb', d[p:p + 2])
|
||||
p += 2
|
||||
sx = sy = 1.0
|
||||
s01 = s10 = 0.0
|
||||
if flags & 8:
|
||||
sx = sy = _f2dot14(d, p)
|
||||
p += 2
|
||||
elif flags & 0x40:
|
||||
sx = _f2dot14(d, p)
|
||||
sy = _f2dot14(d, p + 2)
|
||||
p += 4
|
||||
elif flags & 0x80:
|
||||
sx = _f2dot14(d, p)
|
||||
s01 = _f2dot14(d, p + 2)
|
||||
s10 = _f2dot14(d, p + 4)
|
||||
sy = _f2dot14(d, p + 6)
|
||||
p += 8
|
||||
dx, dy = (a1, a2) if flags & 2 else (0, 0)
|
||||
for contour in self._glyph_contours(glyph_index, depth + 1):
|
||||
out.append([
|
||||
(x * sx + y * s10 + dx, x * s01 + y * sy + dy, on)
|
||||
for x, y, on in contour
|
||||
])
|
||||
if not flags & 0x20:
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
def _f2dot14(d, p):
|
||||
return struct.unpack('>h', d[p:p + 2])[0] / 16384.0
|
||||
|
||||
|
||||
def to_cubic(contour):
|
||||
"""TrueType quadratic contour -> list of cubic segments [(p0,c1,c2,p1), ...]."""
|
||||
pts = []
|
||||
for x, y, on in contour:
|
||||
pts.append((float(x), float(y), on))
|
||||
|
||||
if not pts[0][2]:
|
||||
if pts[-1][2]:
|
||||
pts = [pts[-1]] + pts[:-1]
|
||||
else:
|
||||
mx = (pts[0][0] + pts[-1][0]) / 2
|
||||
my = (pts[0][1] + pts[-1][1]) / 2
|
||||
pts = [(mx, my, True)] + pts
|
||||
|
||||
expanded = []
|
||||
for i, (x, y, on) in enumerate(pts):
|
||||
nx, ny, non = pts[(i + 1) % len(pts)]
|
||||
expanded.append((x, y, on))
|
||||
if not on and not non:
|
||||
expanded.append(((x + nx) / 2, (y + ny) / 2, True))
|
||||
|
||||
segments = []
|
||||
i = 0
|
||||
n = len(expanded)
|
||||
while i < n:
|
||||
x0, y0, on0 = expanded[i]
|
||||
assert on0
|
||||
x1, y1, on1 = expanded[(i + 1) % n]
|
||||
if on1:
|
||||
segments.append(((x0, y0), (x0, y0), (x1, y1), (x1, y1)))
|
||||
i += 1
|
||||
else:
|
||||
x2, y2, _ = expanded[(i + 2) % n]
|
||||
c1 = (x0 + 2 / 3 * (x1 - x0), y0 + 2 / 3 * (y1 - y0))
|
||||
c2 = (x2 + 2 / 3 * (x1 - x2), y2 + 2 / 3 * (y1 - y2))
|
||||
segments.append(((x0, y0), c1, c2, (x2, y2)))
|
||||
i += 2
|
||||
return segments
|
||||
|
||||
|
||||
|
||||
def _find_font():
|
||||
root = os.path.expanduser('~/.pub-cache/hosted/pub.dev')
|
||||
candidates = sorted(
|
||||
name for name in os.listdir(root)
|
||||
if name.startswith('material_symbols_icons-')
|
||||
)
|
||||
if not candidates:
|
||||
raise SystemExit('material_symbols_icons не найден в pub-cache')
|
||||
return os.path.join(root, candidates[-1], 'lib', 'fonts',
|
||||
'MaterialSymbolsOutlined.ttf')
|
||||
|
||||
FONT = os.environ.get('MATERIAL_SYMBOLS_TTF') or _find_font()
|
||||
|
||||
|
||||
UPM = 960.0
|
||||
CANVAS = 600.0
|
||||
MIN_AREA = 500.0
|
||||
|
||||
_font = Font(FONT)
|
||||
|
||||
|
||||
def _bezier(seg, t):
|
||||
(x0, y0), (x1, y1), (x2, y2), (x3, y3) = seg
|
||||
mt = 1 - t
|
||||
x = mt ** 3 * x0 + 3 * mt * mt * t * x1 + 3 * mt * t * t * x2 + t ** 3 * x3
|
||||
y = mt ** 3 * y0 + 3 * mt * mt * t * y1 + 3 * mt * t * t * y2 + t ** 3 * y3
|
||||
return x, y
|
||||
|
||||
|
||||
def _split_cubic(seg, t):
|
||||
p0, c1, c2, p3 = seg
|
||||
|
||||
def mid(a, b, k):
|
||||
return (a[0] + (b[0] - a[0]) * k, a[1] + (b[1] - a[1]) * k)
|
||||
|
||||
a = mid(p0, c1, t)
|
||||
b = mid(c1, c2, t)
|
||||
c = mid(c2, p3, t)
|
||||
d = mid(a, b, t)
|
||||
e = mid(b, c, t)
|
||||
f = mid(d, e, t)
|
||||
return (p0, a, d, f), (f, e, c, p3)
|
||||
|
||||
|
||||
def _seg_metrics(seg, steps=64):
|
||||
pts = [_bezier(seg, i / steps) for i in range(steps + 1)]
|
||||
acc = [0.0]
|
||||
total = 0.0
|
||||
for i in range(steps):
|
||||
total += math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1])
|
||||
acc.append(total)
|
||||
return acc, total, steps
|
||||
|
||||
|
||||
def _t_at_length(metrics, target):
|
||||
acc, total, steps = metrics
|
||||
if total <= 0:
|
||||
return 0.0
|
||||
for i in range(steps):
|
||||
if acc[i + 1] >= target:
|
||||
span = acc[i + 1] - acc[i]
|
||||
k = 0.0 if span <= 0 else (target - acc[i]) / span
|
||||
return (i + k) / steps
|
||||
return 1.0
|
||||
|
||||
|
||||
def _canvas_segments(contour):
|
||||
out = []
|
||||
for seg in to_cubic(contour):
|
||||
out.append(tuple(
|
||||
(x / UPM * CANVAS, (1 - y / UPM) * CANVAS) for x, y in seg
|
||||
))
|
||||
return out
|
||||
|
||||
|
||||
def _exact_path(contour, count):
|
||||
"""Subdivide the original beziers: geometry stays bit-for-bit the glyph."""
|
||||
segments = _canvas_segments(contour)
|
||||
metrics = [_seg_metrics(s) for s in segments]
|
||||
lengths = [m[1] for m in metrics]
|
||||
total = sum(lengths)
|
||||
if total <= 0:
|
||||
return []
|
||||
|
||||
quota = [max(1, int(round(count * length / total))) for length in lengths]
|
||||
while sum(quota) > count and max(quota) > 1:
|
||||
idx = max(range(len(quota)), key=lambda i: (quota[i], lengths[i]))
|
||||
quota[idx] -= 1
|
||||
while sum(quota) < count:
|
||||
idx = max(range(len(quota)), key=lambda i: lengths[i] / quota[i])
|
||||
quota[idx] += 1
|
||||
|
||||
pieces = []
|
||||
for seg, metric, parts in zip(segments, metrics, quota):
|
||||
rest = seg
|
||||
consumed = 0.0
|
||||
length = metric[1]
|
||||
for k in range(parts - 1):
|
||||
t_abs = _t_at_length(metric, length * (k + 1) / parts)
|
||||
span = 1.0 - consumed
|
||||
t_local = 0.0 if span <= 0 else (t_abs - consumed) / span
|
||||
t_local = min(max(t_local, 1e-4), 1 - 1e-4)
|
||||
head, rest = _split_cubic(rest, t_local)
|
||||
pieces.append(head)
|
||||
consumed = t_abs
|
||||
pieces.append(rest)
|
||||
|
||||
path = []
|
||||
n = len(pieces)
|
||||
for i, (p0, c1, _, _) in enumerate(pieces):
|
||||
prev_c2 = pieces[(i - 1) % n][2]
|
||||
path.append((
|
||||
p0,
|
||||
(prev_c2[0] - p0[0], prev_c2[1] - p0[1]),
|
||||
(c1[0] - p0[0], c1[1] - p0[1]),
|
||||
))
|
||||
return path
|
||||
|
||||
|
||||
def _area(path):
|
||||
area = 0.0
|
||||
n = len(path)
|
||||
for i in range(n):
|
||||
x0, y0 = path[i][0]
|
||||
x1, y1 = path[(i + 1) % n][0]
|
||||
area += x0 * y1 - x1 * y0
|
||||
return area / 2
|
||||
|
||||
|
||||
def glyph_paths(codepoint, count):
|
||||
out = []
|
||||
for contour in _font.contours(codepoint):
|
||||
path = _exact_path(contour, count)
|
||||
if not path:
|
||||
continue
|
||||
area = _area(path)
|
||||
if abs(area) < MIN_AREA:
|
||||
continue
|
||||
out.append((area, path))
|
||||
return out
|
||||
|
||||
|
||||
def _centroid(path):
|
||||
return (sum(p[0][0] for p in path) / len(path),
|
||||
sum(p[0][1] for p in path) / len(path))
|
||||
|
||||
|
||||
def _collapsed(path):
|
||||
cx, cy = _centroid(path)
|
||||
return [((cx, cy), (0.0, 0.0), (0.0, 0.0))] * len(path)
|
||||
|
||||
|
||||
def _rotate(path, shift):
|
||||
return path[shift:] + path[:shift]
|
||||
|
||||
|
||||
def _align(src, dst):
|
||||
n = len(src)
|
||||
best, best_cost = 0, None
|
||||
for shift in range(n):
|
||||
cost = 0.0
|
||||
for i in range(n):
|
||||
x0, y0 = src[i][0]
|
||||
x1, y1 = dst[(i + shift) % n][0]
|
||||
cost += (x0 - x1) ** 2 + (y0 - y1) ** 2
|
||||
if best_cost is None or cost < best_cost:
|
||||
best, best_cost = shift, cost
|
||||
return _rotate(dst, best)
|
||||
|
||||
|
||||
def outer_sign(shapes):
|
||||
"""The biggest contour is always an outline: its winding defines 'outer'."""
|
||||
biggest = max(shapes, key=lambda s: abs(s[0]))
|
||||
return 1.0 if biggest[0] > 0 else -1.0
|
||||
|
||||
|
||||
def pair_glyphs(from_cp, to_cp, count):
|
||||
"""[(path_from, path_to), ...] with matching vertex counts and winding."""
|
||||
src = glyph_paths(from_cp, count)
|
||||
dst = glyph_paths(to_cp, count)
|
||||
src_sign = outer_sign(src)
|
||||
dst_sign = outer_sign(dst)
|
||||
|
||||
pairs = []
|
||||
for outer in (True, False):
|
||||
a = sorted([s for s in src if (s[0] * src_sign > 0) == outer],
|
||||
key=lambda s: -abs(s[0]))
|
||||
b = sorted([s for s in dst if (s[0] * dst_sign > 0) == outer],
|
||||
key=lambda s: -abs(s[0]))
|
||||
for i in range(max(len(a), len(b))):
|
||||
if i < len(a) and i < len(b):
|
||||
pairs.append((a[i][1], _align(a[i][1], b[i][1])))
|
||||
elif i < len(a):
|
||||
pairs.append((a[i][1], _collapsed(a[i][1])))
|
||||
else:
|
||||
pairs.append((_collapsed(b[i][1]), b[i][1]))
|
||||
return pairs
|
||||
|
||||
|
||||
|
||||
MIC = 0xE31D
|
||||
CAM = 0xE04B
|
||||
SEND = 0xE163
|
||||
|
||||
POINTS = 56
|
||||
FPS = 60
|
||||
DUR = 24
|
||||
OUT_DIR = os.path.join(os.path.dirname(os.path.dirname(
|
||||
os.path.abspath(__file__))), 'assets', 'lottie')
|
||||
|
||||
EASE_OUT = {'x': 0.2, 'y': 0}
|
||||
EASE_IN = {'x': 0.0, 'y': 1.0}
|
||||
EASE_OUT_V = {'x': [0.2], 'y': [0]}
|
||||
EASE_IN_V = {'x': [0.0], 'y': [1.0]}
|
||||
EASE_SOFT_OUT_V = {'x': [0.33], 'y': [0]}
|
||||
EASE_SOFT_IN_V = {'x': [0.25], 'y': [1.0]}
|
||||
|
||||
|
||||
def r2(value):
|
||||
return round(value, 2)
|
||||
|
||||
|
||||
def path_value(path):
|
||||
return {
|
||||
'i': [[r2(p[1][0]), r2(p[1][1])] for p in path],
|
||||
'o': [[r2(p[2][0]), r2(p[2][1])] for p in path],
|
||||
'v': [[r2(p[0][0]), r2(p[0][1])] for p in path],
|
||||
'c': True,
|
||||
}
|
||||
|
||||
|
||||
COLLAPSE_END = 10
|
||||
GROW_START = 12
|
||||
|
||||
|
||||
def _is_point(path):
|
||||
first = path[0][0]
|
||||
return all(abs(p[0][0] - first[0]) < 0.01 and abs(p[0][1] - first[1]) < 0.01
|
||||
for p in path)
|
||||
|
||||
|
||||
def shape_item(index, path_from, path_to):
|
||||
start, end = 0, DUR
|
||||
if _is_point(path_to):
|
||||
end = COLLAPSE_END
|
||||
elif _is_point(path_from):
|
||||
start = GROW_START
|
||||
return {
|
||||
'ind': index,
|
||||
'ty': 'sh',
|
||||
'ix': index + 1,
|
||||
'ks': {
|
||||
'a': 1,
|
||||
'k': [
|
||||
{'i': EASE_IN, 'o': EASE_OUT, 't': start,
|
||||
's': [path_value(path_from)]},
|
||||
{'t': end, 's': [path_value(path_to)]},
|
||||
],
|
||||
'ix': 2,
|
||||
},
|
||||
'nm': 'Path %d' % (index + 1),
|
||||
'mn': 'ADBE Vector Shape - Group',
|
||||
'hd': False,
|
||||
}
|
||||
|
||||
|
||||
def keyframes(stops, vector):
|
||||
out = []
|
||||
for i, (frame, value) in enumerate(stops):
|
||||
entry = {'t': frame, 's': value if isinstance(value, list) else [value]}
|
||||
if i < len(stops) - 1:
|
||||
if vector:
|
||||
entry['i'] = EASE_IN_V if i == 0 else EASE_SOFT_IN_V
|
||||
entry['o'] = EASE_OUT_V if i == 0 else EASE_SOFT_OUT_V
|
||||
else:
|
||||
entry['i'] = EASE_IN
|
||||
entry['o'] = EASE_OUT
|
||||
out.append(entry)
|
||||
return out
|
||||
|
||||
|
||||
def transform(rotation=None, scale=None, offset_x=None):
|
||||
half = CANVAS / 2
|
||||
ks = {
|
||||
'o': {'a': 0, 'k': 100, 'ix': 11},
|
||||
'r': {'a': 0, 'k': 0, 'ix': 10},
|
||||
'p': {'a': 0, 'k': [half, half, 0], 'ix': 2},
|
||||
'a': {'a': 0, 'k': [half, half, 0], 'ix': 1},
|
||||
's': {'a': 0, 'k': [100, 100, 100], 'ix': 6},
|
||||
}
|
||||
if rotation:
|
||||
ks['r'] = {'a': 1, 'k': keyframes(rotation, vector=False), 'ix': 10}
|
||||
if scale:
|
||||
stops = [(f, [v, v, 100]) for f, v in scale]
|
||||
ks['s'] = {'a': 1, 'k': keyframes(stops, vector=True), 'ix': 6}
|
||||
if offset_x:
|
||||
stops = [(f, [half + dx, half, 0]) for f, dx in offset_x]
|
||||
ks['p'] = {'a': 1, 'k': keyframes(stops, vector=True), 'ix': 2}
|
||||
return ks
|
||||
|
||||
|
||||
def build(name, from_cp, to_cp, rotation=None, scale=None, offset_x=None):
|
||||
pairs = pair_glyphs(from_cp, to_cp, POINTS)
|
||||
items = [shape_item(i, a, b) for i, (a, b) in enumerate(pairs)]
|
||||
items.append({
|
||||
'ty': 'fl',
|
||||
'c': {'a': 0, 'k': [1, 1, 1, 1], 'ix': 4},
|
||||
'o': {'a': 0, 'k': 100, 'ix': 5},
|
||||
'r': 1,
|
||||
'bm': 0,
|
||||
'nm': 'Fill',
|
||||
'mn': 'ADBE Vector Graphic - Fill',
|
||||
'hd': False,
|
||||
})
|
||||
items.append({
|
||||
'ty': 'tr',
|
||||
'p': {'a': 0, 'k': [0, 0], 'ix': 2},
|
||||
'a': {'a': 0, 'k': [0, 0], 'ix': 1},
|
||||
's': {'a': 0, 'k': [100, 100], 'ix': 3},
|
||||
'r': {'a': 0, 'k': 0, 'ix': 6},
|
||||
'o': {'a': 0, 'k': 100, 'ix': 7},
|
||||
'sk': {'a': 0, 'k': 0, 'ix': 4},
|
||||
'sa': {'a': 0, 'k': 0, 'ix': 5},
|
||||
'nm': 'Transform',
|
||||
})
|
||||
|
||||
return {
|
||||
'v': '5.12.1',
|
||||
'fr': FPS,
|
||||
'ip': 0,
|
||||
'op': DUR,
|
||||
'w': int(CANVAS),
|
||||
'h': int(CANVAS),
|
||||
'nm': name,
|
||||
'ddd': 0,
|
||||
'assets': [],
|
||||
'layers': [{
|
||||
'ddd': 0,
|
||||
'ind': 1,
|
||||
'ty': 4,
|
||||
'nm': name,
|
||||
'sr': 1,
|
||||
'ks': transform(rotation, scale, offset_x),
|
||||
'ao': 0,
|
||||
'shapes': [{
|
||||
'ty': 'gr',
|
||||
'it': items,
|
||||
'nm': 'Group 1',
|
||||
'np': len(items),
|
||||
'cix': 2,
|
||||
'bm': 0,
|
||||
'ix': 1,
|
||||
'mn': 'ADBE Vector Group',
|
||||
'hd': False,
|
||||
}],
|
||||
'ip': 0,
|
||||
'op': DUR,
|
||||
'st': 0,
|
||||
'bm': 0,
|
||||
}],
|
||||
'markers': [],
|
||||
}
|
||||
|
||||
|
||||
SPECS = [
|
||||
dict(
|
||||
name='ic_mic_to_videocam',
|
||||
from_cp=MIC, to_cp=CAM,
|
||||
rotation=[(0, 0), (10, -14), (DUR, 0)],
|
||||
scale=[(0, 100), (10, 88), (DUR, 100)],
|
||||
),
|
||||
dict(
|
||||
name='ic_videocam_to_mic',
|
||||
from_cp=CAM, to_cp=MIC,
|
||||
rotation=[(0, 0), (11, 14), (DUR, 0)],
|
||||
scale=[(0, 100), (11, 111), (DUR, 100)],
|
||||
),
|
||||
dict(
|
||||
name='ic_mic_to_send',
|
||||
from_cp=MIC, to_cp=SEND,
|
||||
scale=[(0, 100), (9, 90), (DUR, 100)],
|
||||
offset_x=[(0, 0), (9, -34), (19, 12), (DUR, 0)],
|
||||
),
|
||||
dict(
|
||||
name='ic_videocam_to_send',
|
||||
from_cp=CAM, to_cp=SEND,
|
||||
rotation=[(0, 0), (9, 10), (DUR, 0)],
|
||||
scale=[(0, 100), (9, 92), (DUR, 100)],
|
||||
offset_x=[(0, 0), (9, -26), (19, 10), (DUR, 0)],
|
||||
),
|
||||
dict(
|
||||
name='ic_send_to_mic',
|
||||
from_cp=SEND, to_cp=MIC,
|
||||
rotation=[(0, 0), (10, 9), (DUR, 0)],
|
||||
scale=[(0, 100), (10, 91), (DUR, 100)],
|
||||
offset_x=[(0, 0), (10, 30), (19, -10), (DUR, 0)],
|
||||
),
|
||||
dict(
|
||||
name='ic_send_to_videocam',
|
||||
from_cp=SEND, to_cp=CAM,
|
||||
rotation=[(0, 0), (10, -11), (DUR, 0)],
|
||||
scale=[(0, 100), (10, 90), (DUR, 100)],
|
||||
offset_x=[(0, 0), (10, 24), (19, -8), (DUR, 0)],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
for spec in SPECS:
|
||||
data = build(**spec)
|
||||
path = os.path.join(OUT_DIR, spec['name'] + '.json')
|
||||
with open(path, 'w') as fh:
|
||||
json.dump(data, fh, separators=(',', ':'))
|
||||
print(f"{spec['name']:24s} {os.path.getsize(path) // 1024:3d} KB "
|
||||
f"paths={len(data['layers'][0]['shapes'][0]['it']) - 2}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user