diff --git a/art/avior_round.py b/art/avior_round.py new file mode 100644 index 0000000..b122919 --- /dev/null +++ b/art/avior_round.py @@ -0,0 +1,211 @@ +"""The Avior Studio mark and the steps that led to it. + +The mark is `mark`: a white ibis and its reflection in still water, cradled by one brush sweep that starts +thin and ends round, the bird's outline lightly smoothed. The other animations are the exploration around +it (the untouched round-two cradle, heavier smoothing, and one-dial steps), kept so the choice can be +revisited. White silhouette on black; the site puts it on a black tile.""" +import math +import re +from spritesmith import P_ell, P_limb, capsule + +KEY = 'avior_round' +NAME = 'Avior Studio — rounded cradle' +SIZE = (128, 128) +STEPS = { + 'mark': dict(w0=2.0, w1=7.0, eye=1.7, centre=True), # the chosen mark: Smooth, the skinnier sweep, a dot eye + 'smooth': dict(), + 'k2': dict(k=2), + 'k4': dict(k=4), + 'fat': dict(fat=.6), + 'legs': dict(legw=5.2), + 'thin_start': dict(w0=3.6), + 'heavy': dict(w1=12.5), + 'light': dict(w1=9.5), + 'eased': dict(ease=True), + 'bigger': dict(scale=1.07), + 'smaller': dict(scale=.94), + 'no_mirror': dict(refl=0), + 'mirror': dict(refl=.42), + 'ripples': dict(ripw=(2.0, 1.6, 1.2)), + 'no_ripples': dict(ripw=None), + 'gap_left': dict(start=142, end=392), + 'gap_right': dict(start=158, end=408), + 'low': dict(cy=61), + 'thin1': dict(w0=2.2, w1=8.5), + 'thin2': dict(w0=2.0, w1=7.0), + 'thin3': dict(w0=1.8, w1=5.5), + 'thin4': dict(w0=1.6, w1=4.0), + 'even': dict(w0=4.0, w1=4.0), +} +ANIMS = {k: 1 for k in ['base', 'caps', 'smoother', 'smoothest', 'plump', 'built'] + list(STEPS)} +PALETTE = {'ink': '#ffffff', 'paper': '#000000'} +VARIANTS = {'cream': {'ink': '#f4f3eb'}, 'bone': {'ink': '#e6dcc4'}} + +BIRD = ('M34 64 C39 54 45 43 58 42 C65 41 70 45 73 42 C76 39 70 31 73 24 C76 17 84 18 87 23 C89 27 85 31 82 31 ' + 'C80 39 85 46 76 52 C71 56 68 66 55 68 C46 71 39 69 31 73 Z') +BEAK = 'M84 25 C98 35 104 47 102 60 C100 48 92 37 82 31 Z' +LEG_A = 'M55 58 L54 88 L52 92 L57 92 L58 88 L59 58 Z' +LEG_B = 'M66 58 L66 86 L69 92 L74 92 L71 85 L71 58 Z' + + +def sample(path, step=1.0): + toks = re.findall(r'[MCLZ]|-?[\d.]+', path) + pts, cur, n = [], None, 0 + while n < len(toks): + c = toks[n]; n += 1 + if c == 'M': + cur = (float(toks[n]), float(toks[n + 1])); n += 2; pts.append(cur) + elif c == 'L': + p = (float(toks[n]), float(toks[n + 1])); n += 2 + k = max(1, int(math.dist(cur, p) / step)) + pts += [(cur[0] + (p[0] - cur[0]) * t / k, cur[1] + (p[1] - cur[1]) * t / k) for t in range(1, k + 1)] + cur = p + elif c == 'C': + c1, c2, p = [(float(toks[n + j]), float(toks[n + j + 1])) for j in (0, 2, 4)]; n += 6 + k = max(2, int((math.dist(cur, c1) + math.dist(c1, c2) + math.dist(c2, p)) / step)) + for j in range(1, k + 1): + t = j / k; u = 1 - t + pts.append((u ** 3 * cur[0] + 3 * u * u * t * c1[0] + 3 * u * t * t * c2[0] + t ** 3 * p[0], + u ** 3 * cur[1] + 3 * u * u * t * c1[1] + 3 * u * t * t * c2[1] + t ** 3 * p[1])) + cur = p + return pts + + +def smooth(pts, k, passes=2): + """Average each point with its k neighbours either side, closed, a few passes: rounds every corner.""" + n = len(pts) + for _ in range(passes): + pts = [(sum(pts[(i + j) % n][0] for j in range(-k, k + 1)) / (2 * k + 1), + sum(pts[(i + j) % n][1] for j in range(-k, k + 1)) / (2 * k + 1)) for i in range(n)] + return pts + + +def inflate(pts, d): + """Push each point outward along its normal by d (positive = fatter).""" + n = len(pts); out = [] + for i, p in enumerate(pts): + a, b = pts[i - 1], pts[(i + 1) % n] + tx, ty = b[0] - a[0], b[1] - a[1] + l = math.hypot(tx, ty) or 1 + out.append((p[0] + ty / l * d, p[1] - tx / l * d)) + return out + + +def poly(pts): + return 'M' + ' L'.join(f'{x:.2f} {y:.2f}' for x, y in pts) + 'Z' + + +def brush(cx, cy, r, start, end, w0, w1, steps=96, ease=False): + outer, inner = [], [] + for n in range(steps + 1): + t = n / steps + if ease: + t_w = t * t * (3 - 2 * t) + else: + t_w = t + a = math.radians(start + (end - start) * t) + w = w0 + (w1 - w0) * t_w + outer.append((cx + (r + w / 2) * math.cos(a), cy + (r + w / 2) * math.sin(a))) + inner.append((cx + (r - w / 2) * math.cos(a), cy + (r - w / 2) * math.sin(a))) + return poly(outer + inner[::-1]) + + +def sweep(s, w0=1.0, w1=11.0, caps=False, ease=False, start=150, end=400, cy=58): + cx, r = 64, 54 + s.flat(brush(cx, cy, r, start, end, w0, w1, ease=ease), '@ink') + if caps: + for a, w in ((start, w0), (end, w1)): + s.dot(cx + r * math.cos(math.radians(a)), cy + r * math.sin(math.radians(a)), w / 2, '@ink') + + +def bird_smooth(s, k=0, fat=0.0, legs='capsule', legw=4.2): + """The silhouette, smoothed by window k (0 = as drawn), inflated by `fat`, with rounded leg ends.""" + body = sample(BIRD) + if k: + body = smooth(body, k) + if fat: + body = inflate(body, fat) + beak = sample(BEAK) + if k: + beak = smooth(beak, max(1, k // 3), passes=1) + s.flat(poly(body), '@ink') + s.flat(poly(beak), '@ink') + if k: + s.dot(84, 28.5, 3.2 + k * .12, '@ink') # keep the beak rooted in the head as the head rounds off + if legs == 'capsule': + s.flat(capsule((55.5, 60), (53.5, 91), legw + fat), '@ink') + s.flat(capsule((68, 60), (71, 91), legw + fat), '@ink') + else: + s.flat(LEG_A, '@ink'); s.flat(LEG_B, '@ink') + + +def bird_built(s): + """The ibis rebuilt from round parts only: ellipses and tapered limbs merged into one silhouette.""" + body = P_ell(54, 60, 22, 13, rot=-14) + neck = P_limb([(70, 60), (74, 44), (77, 30)], 14, 10) + head = P_ell(80, 26, 9, 8) + beak = P_limb([(86, 28), (96, 38), (102, 50), (103, 60)], 5.5, 1.2) + tail = P_limb([(50, 62), (40, 67), (32, 73)], 10, 3) + for d in (body, neck, head, beak, tail): + s.flat(d, '@ink') + s.flat(capsule((55.5, 62), (53.5, 91), 4.4), '@ink') + s.flat(capsule((68, 62), (71, 91), 4.4), '@ink') + + +def reflection(s, draw, top=92, op=.3, cy=58): + cid = s._id() + s.defs.append(f'') + s.defs.append(f'') + s.raw(f'' + f'') + draw() + s.raw('') + + +def ripples(s, dy=-2, op=.5, ws=(1.4, 1.1, .9)): + for d, w in zip(['M40 96 Q62 99 84 95', 'M47 103 Q62 101 80 103', 'M52 110 Q62 112 74 110'], ws): + s.raw(f'') + + +def s_ink(): + from spritesmith import pal + return pal('ink') + + +def cradle(s, k=3, fat=0.0, legw=4.2, w0=2.5, w1=11.0, ease=False, scale=1.0, refl=.3, ripw=(1.4, 1.1, .9), start=150, end=400, cy=58, eye=0.0, centre=False): + if centre: + # The drawing's ink (sweep, bird and reflection) is centred on (64.9, 55.6) at full size; move it onto + # the canvas centre and scale it to leave an even margin, so tiles and icons sit true. + s.raw('') + sweep(s, w0=w0, w1=w1, caps=True, ease=ease, start=start, end=end, cy=cy) + def b(): + with s.g(sx=scale, sy=scale, about=(62, 92)): + bird_smooth(s, k, fat=fat, legw=legw) + if refl: + reflection(s, b, op=refl, cy=cy) + b() + if eye: + with s.g(sx=scale, sy=scale, about=(62, 92)): + s.dot(82.2, 25.2, eye, '@paper') # the eye, in the ground colour + if ripw: + ripples(s, ws=ripw) + if centre: + s.raw('') + + +def draw(s, anim, i): + if anim in STEPS: + cradle(s, **STEPS[anim]) + elif anim == 'base': + sweep(s); reflection(s, lambda: bird_smooth(s, 0, legs='flat')); bird_smooth(s, 0, legs='flat'); ripples(s) + elif anim == 'caps': + sweep(s, w0=2.5, caps=True); reflection(s, lambda: bird_smooth(s, 0)); bird_smooth(s, 0); ripples(s) + elif anim == 'smoother': + sweep(s, w0=2.5, caps=True); reflection(s, lambda: bird_smooth(s, 6)); bird_smooth(s, 6); ripples(s) + elif anim == 'smoothest': + sweep(s, w0=3, caps=True); reflection(s, lambda: bird_smooth(s, 10)); bird_smooth(s, 10); ripples(s) + elif anim == 'plump': + sweep(s, w0=3, caps=True); reflection(s, lambda: bird_smooth(s, 6, fat=1.2)); bird_smooth(s, 6, fat=1.2); ripples(s) + elif anim == 'built': + sweep(s, w0=3, caps=True); reflection(s, lambda: bird_built(s)); bird_built(s); ripples(s) diff --git a/public/art/identity/enso-cradle-tile.svg b/public/art/identity/enso-cradle-tile.svg new file mode 100644 index 0000000..8ed260c --- /dev/null +++ b/public/art/identity/enso-cradle-tile.svg @@ -0,0 +1 @@ +Avior Studio \ No newline at end of file diff --git a/public/art/identity/enso-cradle.png b/public/art/identity/enso-cradle.png new file mode 100644 index 0000000..4418277 Binary files /dev/null and b/public/art/identity/enso-cradle.png differ diff --git a/public/art/identity/enso-cradle.svg b/public/art/identity/enso-cradle.svg new file mode 100644 index 0000000..3555843 --- /dev/null +++ b/public/art/identity/enso-cradle.svg @@ -0,0 +1 @@ +Avior Studio — rounded cradle \ No newline at end of file diff --git a/public/favicon.png b/public/favicon.png index 6bdf9cc..9a445e9 100644 Binary files a/public/favicon.png and b/public/favicon.png differ diff --git a/public/logo-mark.png b/public/logo-mark.png index 9ee7a2a..3b84dd7 100644 Binary files a/public/logo-mark.png and b/public/logo-mark.png differ diff --git a/public/logo-mark.svg b/public/logo-mark.svg index f1510a6..8ed260c 100644 --- a/public/logo-mark.svg +++ b/public/logo-mark.svg @@ -1 +1 @@ -Avior Studio — ibis standing above its reflection \ No newline at end of file +Avior Studio \ No newline at end of file diff --git a/public/logo.png b/public/logo.png index a731322..459ad28 100644 Binary files a/public/logo.png and b/public/logo.png differ diff --git a/scripts/render-logos.py b/scripts/render-logos.py index e5a5db8..e74b199 100644 --- a/scripts/render-logos.py +++ b/scripts/render-logos.py @@ -1,47 +1,42 @@ -"""Render Avior identity candidates using a Spritesmith checkout and CairoSVG/Pillow. +"""Render the Avior Studio identity from its Spritesmith source. -Usage: python scripts/render-logos.py /path/to/spritesmith-be/spritesmith -Run with the Python environment containing Spritesmith's rendering dependencies. +Usage: python scripts/render-logos.py /path/to/spritesmith-checkout +Run with a Python environment that has CairoSVG and Pillow (Spritesmith's own .venv works). + +The mark is the `mark` animation of art/avior_round.py: a white ibis and its reflection cradled by a +brush sweep, outline smoothed, ends rounded. It is written transparent (for dark surfaces) and on a black +rounded tile (the site mark, favicon and share image). """ -from io import BytesIO -from pathlib import Path +import re import subprocess import sys +from pathlib import Path + import cairosvg -from PIL import Image, ImageDraw, ImageFont root = Path(__file__).resolve().parents[1] -out = root / 'public/art/identity' -out.mkdir(parents=True, exist_ok=True) -sheet = Image.new('RGB', (1440, 660), '#f4efe4') -draw = ImageDraw.Draw(sheet) -font = ImageFont.truetype('DejaVuSans.ttf', 24) -small = ImageFont.truetype('DejaVuSans.ttf', 16) -for index, (key, label, style) in enumerate([ - ('badge', '01 · Game badge', 'cozy'), - ('pond', '02 · Pond emblem', 'cozy'), - ('seal', '03 · Studio seal', 'flat'), -]): - svg = out / f'ibis-{key}.svg' - subprocess.run([sys.executable, str(Path(sys.argv[1]).resolve().parent / 'py/spritesmith.py'), - 'svg', str(root / 'art/avior_ibis.py'), '--anim', key, - '--style', style, '--cell', '256', '--out', str(svg)], check=True) - png = cairosvg.svg2png(url=str(svg), output_width=400, output_height=400) - (out / f'ibis-{key}.png').write_bytes(png) - image = Image.open(BytesIO(png)) - x = index * 480 + 40 - sheet.paste(image, (x, 65), image) - draw.text((x, 490), label, font=font, fill='#203c42') - draw.text((x, 530), 'AVIOR STUDIO', font=small, fill='#203c42') - tiny = image.resize((48, 48), Image.Resampling.LANCZOS) - sheet.paste(tiny, (x, 570), tiny) - draw.text((x + 68, 584), '48px mark', font=small, fill='#203c42') -sheet.save(out / 'logo-directions.png') - -# The selected studio identity is direction 02, the pond emblem. -selected = out / 'ibis-pond.svg' -(root / 'public/logo-mark.svg').write_bytes(selected.read_bytes()) -for destination, size in [('public/logo-mark.png', 256), ('public/logo.png', 1024), - ('public/favicon.png', 64), ('src/assets/aviorstudio-logo.png', 1024)]: - cairosvg.svg2png(url=str(selected), write_to=str(root / destination), - output_width=size, output_height=size) +spritesmith = Path(sys.argv[1]).resolve() / 'py/spritesmith.py' +identity = root / 'public/art/identity' +identity.mkdir(parents=True, exist_ok=True) + +mark = identity / 'enso-cradle.svg' +subprocess.run([sys.executable, str(spritesmith), 'svg', str(root / 'art/avior_round.py'), + '--anim', 'mark', '--style', 'cozy', '--cell', '256', '--out', str(mark)], check=True) + +# The tile: the same drawing over a black rounded square, in one SVG. +inner = re.sub(r'^]*>', '', mark.read_text(encoding='utf-8').strip())[:-len('')] +inner = re.sub(r'.*?', '', inner, count=1) +tile = ('Avior Studio' + '' + inner + '') +(root / 'public/logo-mark.svg').write_text(tile, encoding='utf-8') +(identity / 'enso-cradle-tile.svg').write_text(tile, encoding='utf-8') + +# The share image is a plain black square, like the studio's first mark. +square = ('Avior Studio' + '' + inner + '') + +for destination, source, size in [('public/logo-mark.png', tile, 256), ('public/favicon.png', tile, 64), + ('public/logo.png', square, 1024), ('src/assets/aviorstudio-logo.png', square, 1024), + ('public/art/identity/enso-cradle.png', mark.read_text(encoding='utf-8'), 512)]: + cairosvg.svg2png(bytestring=source.encode(), write_to=str(root / destination), output_width=size, output_height=size) + print('wrote', destination) diff --git a/src/assets/aviorstudio-logo.png b/src/assets/aviorstudio-logo.png index a731322..459ad28 100644 Binary files a/src/assets/aviorstudio-logo.png and b/src/assets/aviorstudio-logo.png differ diff --git a/src/pages/identity.astro b/src/pages/identity.astro index 782a600..ad5d0b1 100644 --- a/src/pages/identity.astro +++ b/src/pages/identity.astro @@ -1,100 +1,69 @@ --- import Full from '../layouts/Full.astro'; -const variants = [ - { - key: 'badge', - number: '01', - name: 'Game badge', - description: 'Warm sunlight, strong outlines and a shield shape. Closest to the playful art in our games.', - }, - { - key: 'pond', - number: '02', - name: 'Pond emblem', - description: 'An open golden sweep around the ibis and its pond. Closest to the original studio mark.', - }, - { - key: 'seal', - number: '03', - name: 'Studio seal', - description: 'A quieter circular mark with clean shapes, warm feathers and deep teal.', - }, +const earlier = [ + { key: 'badge', name: 'Game badge' }, + { key: 'pond', name: 'Pond emblem' }, + { key: 'seal', name: 'Studio seal' }, ]; ---
← Back to the studio -

AVIOR STUDIO / IDENTITY EXPLORATION

-

One ibis. Three directions.

+

AVIOR STUDIO / IDENTITY

+

An ibis, cradled.

- An ibis standing in water, above its reflection. Compare each direction as - a large mark, a studio wordmark and a small icon. + One brush sweep thickens under a white ibis standing in still water. The mark is a + flat silhouette drawn with Spritesmith, its outline lightly smoothed and every end rounded, so it + exports at any size with no raster source.

-
- {variants.map((variant) => ( -
-
- {`${variant.name}: -
-
-

{variant.number}

-

{variant.name}

- {variant.key === 'pond' &&

Selected studio identity

} -

{variant.description}

-
-
- - Avior Studio -
-
- - Avior Studio -
-
- Icon sizes - {[48, 32, 16].map((size) => ( -
- - {size}px -
- ))} -
- - Download SVG ↗ - -
+ +
+
+ A white ibis standing above its reflection, cradled by a brush sweep +
+
+

The mark

+

+ White on black. The sweep starts thin at the top left, passes under the water and ends in a + round cap at the right, so the bird reads as held rather than enclosed. +

+
+ + Avior Studio +
+
+ Tile sizes + {[48, 32, 16].map((size) => ( +
+ + {size}px +
+ ))} +
+

+ Mark, transparent SVG + Tile SVG + Share image PNG +

+
+
+ +

Earlier directions

+

Three coloured ibis directions from the first exploration, kept for reference.

+
+ {earlier.map((variant) => ( +
+ {`${variant.name}: +
{variant.name}
+
))}
@@ -127,127 +96,116 @@ const variants = [ line-height: 1.6; margin-bottom: 40px; } - .selected { color: #28594d; font-size: 12px; font-weight: 700; } - .directions { + .mark { display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 24px; - } - article { - background: #fffaf0; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); border: 1px solid #d6d8c9; border-radius: 16px; overflow: hidden; + background: #fffaf0; } - .hero-mark { - background: #f0ecdf; - padding: 24px; + .hero { + background: #000; + padding: 48px; } - .hero-mark img { + .hero img { display: block; width: 100%; height: auto; } .copy { - padding: 24px; - min-height: 195px; - } - .number { - font: 600 12px var(--mono); - color: #677b6d; - margin: 0 0 8px; + padding: 28px; + display: grid; + align-content: start; + gap: 16px; } h2 { - margin: 0 0 12px; + margin: 0; font-size: 25px; letter-spacing: -0.025em; } - .copy > p:last-child { + .copy > p { margin: 0; font-size: 15px; line-height: 1.6; } - .wordmark, - .dark { - padding: 20px; + .bar { display: flex; align-items: center; gap: 12px; + padding: 16px 20px; + border: 1px solid #d6d8c9; + border-radius: 10px; font-size: 20px; font-weight: 700; letter-spacing: -0.025em; } - .dark { - background: #152b30; - color: #fff4d7; - } .icons { - min-height: 106px; display: flex; gap: 18px; - align-items: center; - padding: 16px 20px; + align-items: flex-end; } .icons > span { font-size: 12px; margin-right: auto; + align-self: center; } .icons div { display: flex; flex-direction: column; align-items: center; - justify-content: flex-end; gap: 8px; } .icons small { font: 10px var(--mono); } - .download { - display: block; - padding: 16px 24px 24px; + .downloads { + display: flex; + flex-wrap: wrap; + gap: 8px 20px; + } + .downloads a { color: #28594d; font-size: 14px; } - @media (max-width: 950px) { - .directions { - gap: 12px; - } - .wordmark, - .dark { - font-size: 16px; - padding: 14px; - } - .icons { - gap: 10px; - padding: 16px 12px; - } - .copy { - padding: 18px; - min-height: 230px; - } + .earlier-title { + margin-top: 56px; + } + .earlier-intro { + margin: 8px 0 20px; + font-size: 15px; + } + .earlier { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 24px; + } + .earlier figure { + margin: 0; + padding: 20px; + background: #f0ecdf; + border-radius: 12px; + text-align: center; + } + .earlier img { + display: block; + width: 60%; + height: auto; + margin: 0 auto 12px; + } + .earlier figcaption { + font-size: 13px; } - @media (max-width: 700px) { + @media (max-width: 800px) { .identity { width: calc(100% - 28px); } - .directions { + .mark, + .earlier { grid-template-columns: 1fr; - gap: 28px; - } - .copy { - min-height: 0; - } - .hero-mark { - padding: 24px 48px; - } - .wordmark, - .dark { - font-size: 20px; - padding: 20px; } - .icons { - padding: 20px; - gap: 20px; + .hero { + padding: 32px 64px; } }