Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 32 additions & 24 deletions brand/bytefolk/README.md
Original file line number Diff line number Diff line change
@@ -1,40 +1,41 @@
# ByteFolk brand mark
# ByteFolk organization avatar

The ByteFolk organization mark is the minimal one-color **horned B** approved in
[issue #18](https://github.com/bytefolk/.github/issues/18). It combines a capital `B`,
two modular horns, and an extended lower bowl that hints at the digital-employee mascot without
turning the logo into an illustration.
The current organization avatar uses the **Open Herd icon without the ByteFolk wordmark**,
as specified in [issue #18, R2](https://github.com/bytefolk/.github/issues/18).
Its only graphic source is `g#open-herd-mark` in
[`bytefolk-concept-c-open-herd.svg`](bytefolk-concept-c-open-herd.svg).
The mark-only SVG preserves that group's four paths, five rectangles, and colors exactly;
it only removes the horizontal lockup's translation and wordmark and fits the icon canvas.
R2 supersedes the earlier R1 one-color horned B avatar direction.

## Assets

| File | Use |
| --- | --- |
| `symbol.svg` | Primary mark on white or very light surfaces |
| `symbol-reversed.svg` | White mark on black or dark surfaces |
| `lockup.svg` | Horizontal symbol and `ByteFolk` wordmark |
| `avatar-1024.png` | Exact opaque upload artifact for the GitHub organization profile |
| `bytefolk-concept-c-open-herd.svg` | Unchanged original Open Herd horizontal lockup and source group |
| `bytefolk-concept-c-open-herd-mark.svg` | Exact icon-only extraction; the current avatar export source |
| `avatar-1024.png` | Opaque 1024×1024 Open Herd upload artifact for the GitHub organization profile |
| `symbol.svg` | Historical R1 black horned B; not an avatar source |
| `symbol-reversed.svg` | Historical R1 white horned B; not an avatar source |
| `lockup.svg` | Historical R1 horned B lockup; not an avatar source |

The full-body mascot is a separate expressive brand character. It must not replace the symbol in
organization avatars, favicons, repository ownership marks, or compact product navigation.
Historical R1 SVGs remain unchanged for reference. Their presence or earlier approval must not
be used to restore the black B organization avatar. The full-body mascot is also a separate asset.

## Usage

- Primary ink: `#141414`.
- Reversed ink: `#FFFFFF`.
- Keep clear space of at least one horn-stem width around the symbol.
- Keep every visible avatar pixel (any grayscale value below 250) inside the 496 px-radius circle
- Preserve the source palette: blue `#1677ff`, purple `#722ed1`, ink `#141414`, and white `#ffffff`.
- Use the icon without visible text for organization avatars and compact navigation.
- Keep every visible avatar pixel (any color channel below 250) inside the 496 px-radius circle
centered at `(511.5, 511.5)` on the 1024 px canvas. This leaves 16 px inside GitHub's 512 px
circular crop.
- Use the standalone symbol at 16 px or larger.
- Use the horizontal lockup at 120 px wide or larger.
- Use the standalone icon at 16 px or larger. Reserve the full lockup for wider brand surfaces.
- Upload `avatar-1024.png` without recropping or recoloring it. The image already includes padding
and a validated circular safe area for GitHub's presentation.
- On dark surfaces, use `symbol-reversed.svg`; do not place the primary black mark directly on a
dark background.
- Preserve the opaque white avatar background in both GitHub light and dark themes.

Do not add gradients, shadows, outlines, background frames, eyes, nostrils, mane tiles, extra
colors, text inside the mark, or product-specific decoration. Do not stretch, rotate, crop, or
rearrange the horns and B geometry.
Do not redraw, recolor, stretch, rotate, crop, or rearrange the source geometry. Do not add
gradients, shadows, outlines, text, or decoration. Preserve the existing white details and seams.

## Reproduce the GitHub avatar

Expand All @@ -43,7 +44,14 @@ ImageMagick is required for deterministic raster export:
```bash
brand/bytefolk/render-avatar.sh
brand/bytefolk/render-avatar.sh --check
ruby scripts/validate-brand-assets.rb
```

The first command rewrites `avatar-1024.png` from `symbol.svg`. The second renders to a temporary
file and verifies byte-for-byte equality with the tracked artifact.
The first command renders the mark-only SVG at density 768, scales proportionally to fit
900×900, centers it on an opaque white 1024×1024 canvas, and writes `avatar-1024.png`.
The second renders to a temporary file and verifies byte-for-byte equality with the tracked
artifact. The validator checks the approved source, icon geometry and palette, and avatar pixels.

The tracked export was produced with ImageMagick 7.1.2-22. Other renderer versions may produce
different antialiasing or PNG bytes; do not replace the approved artifact without verifying the
source and visual result. Upload the tracked PNG, then verify GitHub's served pixels separately.
Binary file modified brand/bytefolk/avatar-1024.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions brand/bytefolk/bytefolk-concept-c-open-herd-mark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions brand/bytefolk/bytefolk-concept-c-open-herd.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 7 additions & 4 deletions brand/bytefolk/render-avatar.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
set -euo pipefail

asset_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source_svg="$asset_dir/symbol.svg"
source_svg="$asset_dir/bytefolk-concept-c-open-herd-mark.svg"
tracked_png="$asset_dir/avatar-1024.png"
mode="${1:-write}"

Expand All @@ -15,9 +15,12 @@ render() {
local destination="$1"
magick \
-background '#ffffff' \
-density 768 \
"$source_svg" \
-filter box \
-resize '1024x1024!' \
-resize 900x900 \
-gravity center \
-background '#ffffff' \
-extent 1024x1024 \
-alpha remove \
-alpha off \
-colorspace sRGB \
Expand All @@ -40,7 +43,7 @@ case "$mode" in
echo "avatar-1024.png is stale; run brand/bytefolk/render-avatar.sh" >&2
exit 1
fi
echo "ByteFolk avatar matches symbol.svg"
echo "ByteFolk avatar matches the Open Herd mark-only source"
;;
*)
echo "Usage: brand/bytefolk/render-avatar.sh [write|--check]" >&2
Expand Down
60 changes: 49 additions & 11 deletions scripts/validate-brand-assets.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,26 @@
require "pathname"
require "rexml/document"
require "zlib"
require "digest"

ROOT = Pathname.new(File.expand_path("..", __dir__)).freeze
ASSET_DIR = ROOT.join("brand", "bytefolk").freeze
SVG_NAMES = %w[symbol.svg symbol-reversed.svg lockup.svg].freeze
LEGACY_SVG_NAMES = %w[symbol.svg symbol-reversed.svg lockup.svg].freeze
SOURCE_NAME = "bytefolk-concept-c-open-herd.svg"
MARK_NAME = "bytefolk-concept-c-open-herd-mark.svg"
SVG_NAMES = (LEGACY_SVG_NAMES + [SOURCE_NAME, MARK_NAME]).freeze
# Issue #18 R2: preserve the owner's original source and approved rendered upload.
SOURCE_SHA256 = "160c6f108e25d5f6d2a06ae68bd3cc11715f4580c0a0d9d02060d016a4526e49"
AVATAR_SHA256 = "aefe9f3494bff30ff93809f929b04f709f5f36eedbfabdb5f5764efe78578461"
PALETTE = { "--bf-blue" => "#1677ff", "--bf-purple" => "#722ed1", "--bf-ink" => "#141414", "--bf-paper" => "#ffffff" }.freeze
PROHIBITED_ELEMENTS = %w[filter foreignObject image linearGradient radialGradient script].freeze
CIRCULAR_SAFETY_MARGIN = 16.0
INK_THRESHOLD = 250

errors = []
circular_metrics = nil
geometry_signatures = {}
documents = {}

SVG_NAMES.each do |name|
path = ASSET_DIR.join(name)
Expand All @@ -34,6 +43,7 @@
errors << "#{path.relative_path_from(ROOT)}: root element must be svg"
next
end
documents[name] = document
errors << "#{path.relative_path_from(ROOT)}: viewBox must be present" if root.attributes["viewBox"].to_s.empty?
%w[width height].each do |attribute|
errors << "#{path.relative_path_from(ROOT)}: root #{attribute} is prohibited" if root.attributes[attribute]
Expand All @@ -51,6 +61,8 @@
end
end

next unless LEGACY_SVG_NAMES.include?(name)

symbol_group = REXML::XPath.first(document, "//*[starts-with(@id, 'bytefolk-symbol')]")
if symbol_group
child_geometry = symbol_group.elements.map do |element|
Expand All @@ -63,15 +75,43 @@
end
end

if geometry_signatures.length == SVG_NAMES.length && geometry_signatures.values.uniq.length != 1
if geometry_signatures.length == LEGACY_SVG_NAMES.length && geometry_signatures.values.uniq.length != 1
errors << "ByteFolk primary, reversed, and lockup symbol geometry must remain identical"
end

if documents[SOURCE_NAME] && documents[MARK_NAME]
unless Digest::SHA256.file(ASSET_DIR.join(SOURCE_NAME)).hexdigest == SOURCE_SHA256
errors << "#{SOURCE_NAME}: original Open Herd source must remain unchanged"
end
source_group = REXML::XPath.first(documents[SOURCE_NAME], "//*[@id='open-herd-mark']")
mark = documents[MARK_NAME].root
unless mark.attributes["viewBox"] == "0 0 142 116" &&
(mark.attributes.to_a.map(&:name) - %w[xmlns viewBox role aria-label]).empty?
errors << "#{MARK_NAME}: preserve the icon canvas and do not add root styling or transforms"
end
normalize = lambda do |element|
attributes = element.attributes.map do |key, value|
value = PALETTE.fetch(value[4...-1], value) if value.start_with?("var(")
value = "#ffffff" if value == "#fff"
[key, value]
end.sort
[element.name, attributes, element.elements.size]
end
source_geometry = source_group&.elements&.map { |element| normalize.call(element) }
icon_geometry = mark.elements.reject { |element| %w[title desc].include?(element.name) }.map { |element| normalize.call(element) }
unless source_geometry && icon_geometry == source_geometry
errors << "#{MARK_NAME}: icon geometry and palette must match the original group exactly, without text"
end
end

png_path = ASSET_DIR.join("avatar-1024.png")
if !png_path.file?
errors << "#{png_path.relative_path_from(ROOT)}: missing"
else
bytes = png_path.binread
unless Digest::SHA256.hexdigest(bytes) == AVATAR_SHA256
errors << "#{png_path.relative_path_from(ROOT)}: does not match the approved R2 Open Herd upload"
end
signature = "\x89PNG\r\n\x1A\n".b
if bytes.byteslice(0, 8) != signature
errors << "#{png_path.relative_path_from(ROOT)}: invalid PNG signature"
Expand Down Expand Up @@ -107,7 +147,7 @@

if raw.bytesize == expected_size
prior = Array.new(stride, 0)
pixels = []
colors = {}
cursor = 0
min_x = width
min_y = height
Expand Down Expand Up @@ -147,11 +187,8 @@
end

row.each_slice(3).with_index do |(red, green, blue), x|
unless red == green && green == blue
errors << "#{png_path.relative_path_from(ROOT)}: avatar must remain grayscale"
break
end
next if red >= INK_THRESHOLD
colors[[red, green, blue]] = true
next if [red, green, blue].min >= INK_THRESHOLD

min_x = [min_x, x].min
min_y = [min_y, y].min
Expand All @@ -161,12 +198,13 @@
max_ink_radius = [max_ink_radius, ink_radius].max
outside_circle_ink += 1 if ink_radius > safe_radius
end
pixels.concat(row)
prior = row
end

darkest = pixels.min
errors << "#{png_path.relative_path_from(ROOT)}: primary ink is not #141414" unless darkest == 20
PALETTE.each_value do |hex|
rgb = hex.delete_prefix("#").scan(/../).map { |channel| channel.to_i(16) }
errors << "#{png_path.relative_path_from(ROOT)}: missing source color #{hex}" unless colors[rgb]
end
margins = [min_x, min_y, width - 1 - max_x, height - 1 - max_y]
if max_x.negative? || margins.any? { |margin| margin < 100 }
errors << "#{png_path.relative_path_from(ROOT)}: insufficient rectangular padding #{margins.inspect}"
Expand Down