Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lifthrasiir/6c8c142292132e10b34307eb1cf53a93 to your computer and use it in GitHub Desktop.
Save lifthrasiir/6c8c142292132e10b34307eb1cf53a93 to your computer and use it in GitHub Desktop.
Material Design Icons experiment for IconVG
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