diff --git a/.gitignore b/.gitignore index 4b396c8..f5a2157 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,14 @@ resources/ public/ .hugo_build.lock + +# Favicon +android-chrome-192x192.png +android-chrome-512x512.png +apple-touch-icon.png +favicon-16x16.png +favicon-32x32.png +favicon-dark.svg +favicon.ico +favicon.svg +site.webmanifest diff --git a/.gitmodules b/.gitmodules index dd62212..9f23218 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "themes/hextra"] path = themes/hextra url = https://github.com/imfing/hextra.git +[submodule "favicon/artwork"] + path = favicon/artwork + url = https://git.nicolabelluti.me/little-emulator/artwork diff --git a/favicon/artwork b/favicon/artwork new file mode 160000 index 0000000..8ad897f --- /dev/null +++ b/favicon/artwork @@ -0,0 +1 @@ +Subproject commit 8ad897fd75dd0201f2ad9d27a512ddae9564cc67 diff --git a/favicon/generate_favicons.sh b/favicon/generate_favicons.sh new file mode 100644 index 0000000..a8e20d6 --- /dev/null +++ b/favicon/generate_favicons.sh @@ -0,0 +1,169 @@ +#!/usr/bin/env bash + +# Make the script stop if something goes wrong: +# -e: any command fails +# -u: unset variable is used +# -o pipefail: a pipeline fails if any command fails +set -euo pipefail + +# Show a clear error before exiting +trap 'echo -e "\033[1;31mError at line $LINENO\033[0m" >&2; exit 1' ERR + +# =================================== CONFIG =================================== + +# Paths +SRC_SVG="$(dirname "$BASH_SOURCE[0]")/artwork/logo.svg" +OUTPUT_DIR="$(dirname "$BASH_SOURCE[0]")/../static" + +# Cropping +CROP_SIZE=800 # final square viewBox size (e.g., 800x800) + +# Branding / Manifest (for site.webmanifest) +APP_NAME="Little Emulator" +THEME_COLOR="#000000" +BACKGROUND_COLOR="#000000" + +# Dark variant color swap (case-insensitive) +COLOR_A="#36373b" +COLOR_B="#fafcfc" + +# Raster outputs +# size file_name background +PNG_LIST=( + "192 android-chrome-192x192.png none" + "512 android-chrome-512x512.png none" + "180 apple-touch-icon.png white" + "16 favicon-16x16.png none" + "32 favicon-32x32.png none" +) + +# ICO sizes +ICO_SIZES=(16 32 48) + +# ============================= Pre-flight checks ============================== + +[[ -f "${SRC_SVG}" ]] || { echo "\033[1;31mError: Source SVG not found: ${SRC_SVG}\033[0m" >&2; exit 1; } +command -v magick >/dev/null || { echo "\033[1;31mError: ImageMagick (magick) not found\033[0m" >&2; exit 1; } +command -v xmlstarlet >/dev/null || { echo "\033[1;31mError: xmlstarlet required for SVG cropping\033[0m" >&2; exit 1; } + +# ================================= Functions ================================== + +############################################## +# crop_svg_viewbox +# Rewrites the SVG viewBox to a centered square +############################################## +crop_svg_viewbox() { + local INPUT_FILE="$1" + local OUTPUT_FILE="$2" + local TARGET_SIZE="$3" + + local VIEWBOX MIN_X MIN_Y VIEWBOX_WIDTH VIEWBOX_HEIGHT + local NEW_MIN_X NEW_MIN_Y NEW_SIZE + + # Normalize: if no viewBox, derive it from width/height (viewBox="0 0 w h") + VIEWBOX="$(xmlstarlet sel -t -v "string(/*[local-name()='svg']/@viewBox)" "$INPUT_FILE" || :)" + if [[ $VIEWBOX =~ [^[:space:]] ]]; then + read -r MIN_X MIN_Y VIEWBOX_WIDTH VIEWBOX_HEIGHT <<<"$(tr ',' ' ' <<<"$VIEWBOX")" + else + VIEWBOX_WIDTH="$(xmlstarlet sel -t -v "string(/*[local-name()='svg']/@width)" "$INPUT_FILE")" + VIEWBOX_HEIGHT="$(xmlstarlet sel -t -v "string(/*[local-name()='svg']/@height)" "$INPUT_FILE")" + VIEWBOX_WIDTH="${VIEWBOX_WIDTH//[!0-9.+-]/}" + VIEWBOX_HEIGHT="${VIEWBOX_HEIGHT//[!0-9.+-]/}" + MIN_X=0 + MIN_Y=0 + VIEWBOX="$MIN_X $MIN_Y $VIEWBOX_WIDTH $VIEWBOX_HEIGHT" + fi + + # Compute centered square + read -r NEW_MIN_X NEW_MIN_Y NEW_SIZE < <(awk " + BEGIN { + SIDE = ($VIEWBOX_WIDTH < $TARGET_SIZE ? $VIEWBOX_WIDTH : $TARGET_SIZE) + if ($VIEWBOX_HEIGHT < SIDE) SIDE = $VIEWBOX_HEIGHT + CENTER_X = $MIN_X + ($VIEWBOX_WIDTH - SIDE) / 2.0 + CENTER_Y = $MIN_Y + ($VIEWBOX_HEIGHT - SIDE) / 2.0 + print CENTER_X, CENTER_Y, SIDE + } + ") + + # Apply normalized centered square viewBox, remove width/height, enforce centered rendering + xmlstarlet ed \ + --update "/*[local-name()='svg']/@viewBox" -v "$NEW_MIN_X $NEW_MIN_Y $NEW_SIZE $NEW_SIZE" \ + --insert "/*[local-name()='svg'][not(@viewBox)]" \ + --type attr --name "viewBox" --value "$NEW_MIN_X $NEW_MIN_Y $NEW_SIZE $NEW_SIZE" \ + \ + --delete "/*[local-name()='svg']/@width" \ + --delete "/*[local-name()='svg']/@height" \ + --update "/*[local-name()='svg']/@preserveAspectRatio" -v "xMidYMid meet" \ + "$INPUT_FILE" > "$OUTPUT_FILE" +} + +############################################## +# render_png +############################################## +render_png() { + local SIZE="$1" INPUT="$2" OUTPUT="$3" BACKGROUND_COLOR="${4:-none}" + + magick -density 384 "${INPUT}" \ + -background "${BACKGROUND_COLOR}" -alpha remove -alpha off \ + -resize "${SIZE}x${SIZE}" -gravity center -extent "${SIZE}x${SIZE}" \ + -strip "${OUTPUT}" +} + +# ==================================== Main ==================================== + +mkdir -p "${OUTPUT_DIR}" + +echo "▶️ Cropping SVG to centered ${CROP_SIZE}×${CROP_SIZE} viewBox…" +crop_svg_viewbox "${SRC_SVG}" "${OUTPUT_DIR}/favicon.svg" "${CROP_SIZE}" +echo "✅ Cropped vector saved: favicon.svg" + +echo "▶️ Creating dark SVG variant by swapping ${COLOR_A} and ${COLOR_B}..." +sed -E \ + -e "s/${COLOR_A//\#/\\#}/__TMP_COLOR__/Ig" \ + -e "s/${COLOR_B//\#/\\#}/${COLOR_A//\#/\\#}/Ig" \ + -e "s/__TMP_COLOR__/${COLOR_B//\#/\\#}/Ig" \ + "${OUTPUT_DIR}/favicon.svg" > "${OUTPUT_DIR}/favicon-dark.svg" +echo "✅ Dark variant saved: favicon-dark.svg" + +echo "▶️ Rendering PNG assets..." +for ITEM in "${PNG_LIST[@]}"; do + read -r SIZE NAME BG <<<"$ITEM" + render_png "$SIZE" "${OUTPUT_DIR}/favicon.svg" "${OUTPUT_DIR}/${NAME}" "${BG}" + echo " • ${NAME} (${SIZE}px)" +done +echo "✅ PNGs done." + +echo "▶️ Building multi-size ICO..." +TMP_PNG_DIR="$(mktemp -d)" +trap 'rm -rf "${TMP_PNG_DIR}"' EXIT +ICO_INPUTS=() +for SIZE in "${ICO_SIZES[@]}"; do + render_png "$SIZE" "${OUTPUT_DIR}/favicon.svg" "${TMP_PNG_DIR}/${SIZE}.png" "none" + ICO_INPUTS+=("${TMP_PNG_DIR}/${SIZE}.png") +done +magick "${ICO_INPUTS[@]}" -colors 256 "${OUTPUT_DIR}/favicon.ico" +echo "✅ ICO saved: favicon.ico" + +echo "▶️ Writing site.webmanifest..." +ICON_ITEMS=() +for ITEM in "${PNG_LIST[@]}"; do + read -r SIZE NAME BG <<<"$ITEM" + [[ "$NAME" == android-* ]] || continue + + ITEM='{ "src": "/'"$NAME"'", "sizes": "'"$SIZE"'x'"$SIZE"'", "type": "image/png" }' + ICON_ITEMS+=("$ITEM") +done +ICON_JSON="$(printf '%s\n' "${ICON_ITEMS[@]}" | paste -sd, -)" +cat > "${OUTPUT_DIR}/site.webmanifest" << JSON +{ + "name": "${APP_NAME}", + "short_name": "${APP_NAME}", + "icons": [ ${ICON_JSON} ], + "theme_color": "${THEME_COLOR}", + "background_color": "${BACKGROUND_COLOR}", + "display": "standalone" +} +JSON +echo "✅ Manifest saved: site.webmanifest" + +echo "🎉 All assets written to: $(readlink -f "$OUTPUT_DIR")" diff --git a/flake.nix b/flake.nix index 1abc7ad..dc0c2a1 100644 --- a/flake.nix +++ b/flake.nix @@ -14,6 +14,10 @@ # Packages to install buildInputs = [ pkgs.hugo + + # Favicon generation + pkgs.imagemagick + pkgs.xmlstarlet ]; }; });