Created
June 16, 2021 02:50
-
-
Save lifthrasiir/6c8c142292132e10b34307eb1cf53a93 to your computer and use it in GitHub Desktop.
Material Design Icons experiment for IconVG
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os, os.path, sys, re, math, collections | |
mults = [1, 2, 4, 8, 10, 16, 100] | |
freqs = {} | |
for absmult in mults: | |
for relmult in mults: | |
freqs[absmult, relmult] = collections.Counter() | |
ops = collections.Counter() | |
lineops = collections.Counter() | |
csize = collections.Counter() | |
log = lambda *args, **kwargs: print(*args, **kwargs, file=sys.stderr) | |
def arc_to_cubes(x1, y1, rx, ry, rot, larc, sweep, x2, y2): | |
if rx <= 0 or ry <= 0: return [('L', x2, y2)] | |
phi = math.pi * 2 * rot | |
halfdx = (x1 - x2) / 2; halfdy = (y1 - y2) / 2 | |
cosphi = math.cos(phi); sinphi = math.sin(phi) | |
x1p = cosphi * halfdx + sinphi * halfdy | |
y1p = -sinphi * halfdx + cosphi * halfdy | |
rxsq = rx * rx; rysq = ry * ry | |
x1psq = x1p * x1p; y1psq = y1p * y1p | |
radiick = x1psq / rxsq + y1psq / rysq | |
if radiick > 1: | |
s = math.sqrt(radiick) | |
rx *= s; ry *= s; rxsq = rx * rx; rysq = ry * ry | |
denom = rxsq * y1psq + rysq * x1psq | |
step2 = 0.0 | |
a = rxsq * rysq / denom - 1.0 | |
if a > 0: step2 = math.sqrt(a) | |
if larc == sweep: step2 = -step2 | |
cxp = step2 * rx * y1p / ry | |
cyp = -step2 * ry * x1p / rx | |
cx = cosphi * cxp - sinphi * cyp + (x1 + x2) / 2 | |
cy = sinphi * cxp + cosphi * cyp + (y1 + y2) / 2 | |
ax = (x1p - cxp) / rx; ay = (y1p - cyp) / ry | |
bx = (-x1p - cxp) / rx; by = (-y1p - cyp) / ry | |
theta10 = angle(1.0, 0.0, ax, ay) | |
dtheta = angle(ax, ay, bx, by) | |
if sweep: | |
if dtheta < 0: dtheta += 2 * math.pi | |
else: | |
if dtheta > 0: dtheta -= 2 * math.pi | |
n = int(math.ceil(abs(dtheta) / (math.pi / 2 + 0.001))) | |
cubes = [] | |
for i in range(n): | |
theta1 = theta10 + dtheta * i / n | |
theta2 = theta10 + dtheta * (i+1) / n | |
halfdtheta = (theta2 - theta1) / 2 | |
q = math.sin(halfdtheta / 2) | |
t = 8 * q * q / (3 * math.sin(halfdtheta)) | |
cos1 = math.cos(theta1); sin1 = math.sin(theta1) | |
cos2 = math.cos(theta2); sin2 = math.sin(theta2) | |
ix1 = rx * (cos1 - t * sin1); iy1 = ry * (sin1 + t * cos1) | |
ix2 = rx * (cos2 + t * sin2); iy2 = ry * (sin2 - t * cos2) | |
ix3 = rx * cos2; iy3 = ry * sin2 | |
cubes.append(( | |
'C', | |
cx + cosphi * ix1 - sinphi * iy1, cy + sinphi * ix1 + cosphi * iy1, | |
cx + cosphi * ix2 - sinphi * iy2, cy + sinphi * ix2 + cosphi * iy2, | |
cx + cosphi * ix3 - sinphi * iy3, cy + sinphi * ix3 + cosphi * iy3, | |
)) | |
return cubes | |
def angle(ux, uy, vx, vy): | |
uu = math.hypot(ux, uy) | |
vv = math.hypot(vx, vy) | |
cos = (ux * vx + uy * vy) / (uu * vv) | |
ret = 0.0 | |
if cos <= -1: ret = math.pi | |
elif cos >= 1: ret = 0.0 | |
else: ret = math.acos(cos) | |
if ux * vy < uy * vx: ret = -ret | |
return ret | |
print('<!doctype html><style>span { white-space: nowrap } .p { margin-left: -24px; position: relative; left: 24px; fill: white; mix-blend-mode: exclusion; }</style>') | |
for k in os.listdir(sys.argv[1]): | |
if not k.endswith('.svg'): continue | |
with open(os.path.join(sys.argv[1], k), 'r', encoding='utf-8') as f: | |
svg = f.read() | |
m = re.match(r'^<\?xml version="1\.0" encoding="UTF-8"\?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1\.1//EN" "http://www\.w3\.org/Graphics/SVG/1\.1/DTD/svg11\.dtd"><svg xmlns="http://www\.w3\.org/2000/svg" xmlns:xlink="http://www\.w3\.org/1999/xlink" version="1\.1" id="([a-z0-9-]+)" width="24" height="24" viewBox="0 0 24 24"><path d="(.*)" /></svg>$', svg) | |
if not m: log(k, svg); break | |
name = m.group(1) | |
path = re.findall(r'-?(?:\d+(?:\.\d+)?|\.\d+)|[^, ]', m.group(2)) | |
strange = [x for x in path if not re.match(r'^(?:[MAVHLZCSQTvclmzhsa]|-?\.\d{1,3}|-?\d{1,2}(?:\.\d{1,5})?)$', x)] | |
assert not strange, (m.group(2), strange) | |
width = height = 24 | |
shape = [] | |
i = 0 | |
op = '' | |
x = y = None | |
def num(): global i; i += 1; return float(path[i-1]) | |
def smooth(op): | |
last = shape[-1] | |
if last[0] == op: | |
return 2 * last[-2] - last[-4], 2 * last[-1] - last[-3] | |
else: | |
return last[-2:] | |
try: | |
while i < len(path): | |
if path[i] == 'Z' or path[i] == 'z': | |
shape.append(('Z',)) | |
i += 1 | |
continue | |
if path[i] in 'MAVHLZCSQTvclmzhsa': op = path[i]; i += 1 | |
if op == 'M': x = num(); y = num(); shape.append(('M', x, y)); op = 'L' # implicit | |
elif op == 'm': x += num(); y += num(); shape.append(('M', x, y)); op = 'l' # implicit | |
elif op == 'L': x = num(); y = num(); shape.append(('L', x, y)) | |
elif op == 'l': x += num(); y += num(); shape.append(('L', x, y)) | |
elif op == 'H': x = num(); shape.append(('L', x, y)) | |
elif op == 'h': x += num(); shape.append(('L', x, y)) | |
elif op == 'V': y = num(); shape.append(('L', x, y)) | |
elif op == 'v': y += num(); shape.append(('L', x, y)) | |
elif op == 'C': | |
x1 = num(); y1 = num(); x2 = num(); y2 = num(); x = num(); y = num() | |
shape.append(('C', x1, y1, x2, y2, x, y)) | |
elif op == 'c': | |
x1 = num() + x; y1 = num() + y; x2 = num() + x; y2 = num() + y; x += num(); y += num() | |
shape.append(('C', x1, y1, x2, y2, x, y)) | |
elif op == 'S': | |
x1, y1 = smooth('C'); x2 = num(); y2 = num(); x = num(); y = num() | |
shape.append(('C', x1, y1, x2, y2, x, y)) | |
elif op == 's': | |
x1, y1 = smooth('C'); x2 = num() + x; y2 = num() + y; x += num(); y += num() | |
shape.append(('C', x1, y1, x2, y2, x, y)) | |
elif op == 'Q': | |
x1 = num(); y1 = num(); x = num(); y = num() | |
shape.append(('Q', x1, y1, x, y)) | |
elif op == 'q': | |
x1 = num() + x; y1 = num() + y; x += num(); y += num() | |
shape.append(('Q', x1, y1, x, y)) | |
elif op == 'T': | |
x1, y1 = smooth('Q'); x = num(); y = num() | |
shape.append(('Q', x1, y1, x, y)) | |
elif op == 't': | |
x1, y1 = smooth('Q'); x += num(); y += num() | |
shape.append(('Q', x1, y1, x, y)) | |
elif op == 'A': | |
x0 = x; y0 = y; rx = num(); ry = num(); rot = num(); larc = int(path[i]); sweep = int(path[i+1]); i += 2; x = num(); y = num() | |
shape.extend(arc_to_cubes(x0, y0, rx, ry, rot, larc, sweep, x, y)) | |
elif op == 'a': | |
x0 = x; y0 = y; rx = num() + x; ry = num() + y; rot = num(); larc = int(path[i]); sweep = int(path[i+1]); i += 2; x += num(); y += num() | |
shape.extend(arc_to_cubes(x0, y0, rx, ry, rot, larc, sweep, x, y)) | |
else: | |
assert False, op | |
except: | |
log(path[:i], path[i:]) | |
raise | |
if path[-1][0] != 'Z': path.append(('Z',)) # implicit | |
print('<span><svg class=p width=24 height=24><path d="' + ' '.join(' '.join(map(str, x)) for x in shape) + '"></svg>' + svg[136:] + '</span>') | |
# analysis | |
coords = [] | |
lastop = '' | |
run = 0 | |
linerun = 0 | |
linedir = '' | |
for x in shape: | |
op = x[0] | |
if lastop == op: | |
run += 1 | |
else: | |
if lastop: ops[lastop, (run-1).bit_length()] += 1 | |
lastop = op | |
run = 1 | |
islinerun = False | |
if op == 'L': | |
if coords: | |
if x[1] == coords[-1][0]: | |
if x[2] != coords[-1][1] and linedir != 'V': | |
linedir = 'V' | |
linerun += 1 | |
islinerun = True | |
elif x[2] == coords[-1][1]: | |
if linedir != 'H': | |
linedir = 'H' | |
linerun += 1 | |
islinerun = True | |
if not islinerun: | |
if linedir: lineops[(linerun-1).bit_length()] += 1 | |
linedir = '' | |
linerun = 0 | |
if op == 'M': | |
coords.append(x[1:3] + (1,)) | |
elif op == 'L': | |
coords.append(x[1:3] + (1,)) | |
elif op == 'C': | |
coords.append(x[1:3] + (1,)) | |
coords.append(x[3:5] + (1,)) | |
coords.append(x[5:7] + (1,)) | |
elif op == 'Q': | |
coords.append(x[1:3] + (1,)) | |
coords.append(x[3:5] + (1,)) | |
elif op != 'Z': | |
assert False, x | |
ops[lastop, (run-1).bit_length()] += 1 | |
if linedir: lineops[(linerun-1).bit_length()] += 1 | |
def count(freqs, v, lastv, absmult, relmult, threshold=0.01): | |
dv = (v - lastv) * relmult | |
v *= absmult | |
v = int(round(v)).bit_length() if abs(round(v) - v) < threshold else -1 | |
dv = 0 if abs(round(dv)) < threshold else 1 + int(round(dv)).bit_length() if abs(round(dv) - dv) < threshold else -1 | |
freqs[v, dv] += 1 | |
for i, (x, y, refdist) in enumerate(coords): | |
assert -5 <= x <= width * 2, (name, x) | |
assert -5 <= y <= height * 2, (name, y) | |
lastx, lasty = (0, 0) if i < refdist else coords[i - refdist][:2] | |
for (absmult, relmult), f in freqs.items(): | |
count(f, x, lastx, absmult, relmult) | |
count(f, y, lasty, absmult, relmult) | |
def concrete_size(v, lastv, threshold=0.01, *, compzero, opprange): | |
dv = v - lastv | |
if compzero and abs(round(dv)) < threshold: | |
return lastv, 0 | |
if abs(round(dv) - dv) < threshold: | |
dv = int(round(dv)) | |
if -16 <= v <= 15: | |
return lastv + dv, 1 | |
if abs(round(v) - v) < threshold: | |
v = int(round(v)) | |
if (-48 <= v <= 47) if opprange and int(lastv) == lastv and -15 <= lastv <= 16 else (-32 <= v <= 31): | |
return v, 1 | |
return v, 2 | |
for compzero in (False, True): | |
for opprange in (False, True): | |
lastx = 0 | |
lasty = 0 | |
sz = 0 | |
for i, (x, y, _) in enumerate(coords): | |
nocompzero = i == 0 | |
lastx, xsz = concrete_size(x, lastx, compzero=compzero and not nocompzero, opprange=opprange) | |
lasty, ysz = concrete_size(y, lasty, compzero=compzero and not nocompzero and xsz > 0, opprange=opprange) | |
sz += xsz + ysz | |
csize[compzero, opprange] += sz | |
oprange = 'MLCQZ' | |
irange = range(0, max([i for op, i in ops.keys()] + list(lineops.keys())) + 1) | |
log('OPS: ' + ''.join('%8d' % i for i in irange)) | |
log(' ' + ''.join('--------' for i in irange)) | |
for op in oprange: | |
log('%5s' % op + ' | ' + ''.join('%8d' % ops[op,i] for i in irange) + ' | %8d' % sum(ops[op,i] for i in irange)) | |
log(' ' + ''.join('--------' for i in irange) + '-+---------') | |
log(' line | ' + ''.join('%8d' % lineops[i] for i in irange) + ' | %8d' % sum(lineops.values())) | |
log(' ' + ''.join('--------' for i in irange) + '-+---------') | |
log(' ' + ''.join('%8d' % sum(ops[op,i] for op in oprange) for i in irange) + ' | %8d' % sum(ops.values())) | |
log() | |
def logfreqs(freqs, label): | |
irange = list(range(0, max(i for i, j in freqs.keys()) + 1)) + [-1] | |
jrange = [0] + list(range(2, max(j for i, j in freqs.keys()) + 1)) + [-1] | |
log('%-11s' % label + ''.join('%8s' % ('no rel' if j < 0 else 'rel=%d' % j) for j in jrange)) | |
log(' ' + ''.join('--------' for j in jrange)) | |
for i in irange: | |
log('%8s | ' % ('no abs' if i < 0 else 'abs=%d' % i) + ''.join('%8d' % freqs[i,j] for j in jrange) + ' | %8d' % sum(freqs[i,j] for j in jrange)) | |
log(' ' + ''.join('--------' for j in jrange) + '-+---------') | |
log(' ' + ''.join('%8d' % sum(freqs[i,j] for i in irange) for j in jrange) + ' | %8d' % sum(freqs.values())) | |
log() | |
#for (absmult, relmult), f in freqs.items(): | |
# logfreqs(f, 'MULT=%dX/%dX' % (absmult, relmult)) | |
# logcumulfreqs(f, 'MULT=%dX/%dX' % (absmult, relmult)) | |
encodings = {} | |
for (absmult, relmult), f in freqs.items(): | |
irange = range(9) | |
jrange = [0] + list(range(2, 9)) | |
for i in irange: | |
for j in jrange: | |
if 0 <= i <= 7 and 0 <= j <= 7: | |
misses = sum(v for (ii, jj), v in f.items() if (ii > i or ii < 0) and (jj > j or jj < 0)) | |
kind = 1 if absmult == relmult == 1 else 2 | |
encodings.setdefault(max(i, j) + 1, []).append((misses, kind, | |
f'1 bit flag, {i} bits {absmult}x absolute or {j} bits {relmult}x relative')) | |
if 0 <= j <= i <= 8: | |
misses = sum(v for (ii, jj), v in f.items() if (ii > i or ii <= j) and (jj > j or jj < 0)) | |
kind = 4 if absmult == relmult == 1 else 8 | |
encodings.setdefault(i, []).append((misses, kind, | |
f'{i} bits {absmult}x absolute' if j == 0 else | |
f'{i} bits {relmult}x relative' if j == i else | |
f'{i} bits {absmult}x absolute, {j} bits reinterpreted as {relmult}x relative')) | |
for bits, encs in sorted(encodings.items()): | |
encs.sort() | |
limit = encs[0][0] * 1.1 | |
kindseen = 0 | |
log('%d BITS:' % bits) | |
for misses, kind, encoding in encs: | |
if misses > limit: | |
if kindseen == 15: break | |
if kindseen & kind: continue | |
kindseen |= kind | |
log(str(misses) + ('* ' if misses > limit else ' '), encoding) | |
log() | |
log(f'{csize=}') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment