diff --git a/brand/bytefolk/README.md b/brand/bytefolk/README.md index 439b86f..437b0e2 100644 --- a/brand/bytefolk/README.md +++ b/brand/bytefolk/README.md @@ -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 @@ -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. diff --git a/brand/bytefolk/avatar-1024.png b/brand/bytefolk/avatar-1024.png index 82e530b..a023ee1 100644 Binary files a/brand/bytefolk/avatar-1024.png and b/brand/bytefolk/avatar-1024.png differ diff --git a/brand/bytefolk/bytefolk-concept-c-open-herd-mark.svg b/brand/bytefolk/bytefolk-concept-c-open-herd-mark.svg new file mode 100644 index 0000000..4f95076 --- /dev/null +++ b/brand/bytefolk/bytefolk-concept-c-open-herd-mark.svg @@ -0,0 +1,13 @@ + + ByteFolk Open Herd organization mark + The mark-only organization icon. The ByteFolk wordmark is intentionally omitted. + + + + + + + + + + diff --git a/brand/bytefolk/bytefolk-concept-c-open-herd.svg b/brand/bytefolk/bytefolk-concept-c-open-herd.svg new file mode 100644 index 0000000..6e4e928 --- /dev/null +++ b/brand/bytefolk/bytefolk-concept-c-open-herd.svg @@ -0,0 +1,35 @@ + + ByteFolk Logo Concept C — Open Herd + Four independent contributors assemble into an abstract hybrid animal and human face beside an open technical wordmark. + + + + + + + + + + + + + + + + ByteFolk + diff --git a/brand/bytefolk/render-avatar.sh b/brand/bytefolk/render-avatar.sh index 511a90d..a35cd40 100755 --- a/brand/bytefolk/render-avatar.sh +++ b/brand/bytefolk/render-avatar.sh @@ -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}" @@ -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 \ @@ -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 diff --git a/scripts/validate-brand-assets.rb b/scripts/validate-brand-assets.rb index beda490..7ea98ac 100644 --- a/scripts/validate-brand-assets.rb +++ b/scripts/validate-brand-assets.rb @@ -3,10 +3,18 @@ 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 @@ -14,6 +22,7 @@ errors = [] circular_metrics = nil geometry_signatures = {} +documents = {} SVG_NAMES.each do |name| path = ASSET_DIR.join(name) @@ -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] @@ -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| @@ -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" @@ -107,7 +147,7 @@ if raw.bytesize == expected_size prior = Array.new(stride, 0) - pixels = [] + colors = {} cursor = 0 min_x = width min_y = height @@ -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 @@ -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}"