feat: distinguish fish by visual family
This commit is contained in:
@@ -1,9 +1,16 @@
|
|||||||
---
|
---
|
||||||
const { name, size = 92 } = Astro.props as { name: string; size?: number };
|
import { fishVisualFamily, type FishVisualFamily } from "../lib/fish-visuals";
|
||||||
const variant = [...name].reduce((sum, char) => sum + (char.codePointAt(0) ?? 0), 0) % 3;
|
const { name, size = 92, family: familyOverride } = Astro.props as { name: string; size?: number; family?: FishVisualFamily };
|
||||||
|
const family = familyOverride ?? fishVisualFamily(name);
|
||||||
---
|
---
|
||||||
<svg class="fish-silhouette" width={size} height={Math.round(size * .48)} viewBox="0 0 120 58" aria-hidden="true">
|
<svg class="fish-silhouette" data-family={family} width={size} height={Math.round(size * .48)} viewBox="0 0 120 58" aria-hidden="true">
|
||||||
{variant === 0 && <><path d="M22 29C38 7 78 8 101 29 78 50 38 51 22 29Z"/><path d="m25 29-22-18v36Z"/><path d="M55 14 69 2l8 15M57 44l15 11 8-16"/><circle cx="91" cy="25" r="2.4" class="fish-eye"/></>}
|
{family === "pike" && <><path d="M19 30C35 16 70 15 94 24l22 1-7 9-15 1C70 45 35 44 19 30Z"/><path d="M22 30 2 15v30Z"/><path d="m49 18 12-14 15 12M49 42l13 12 15-13"/><circle cx="105" cy="28" r="2.2" class="fish-eye"/></>}
|
||||||
{variant === 1 && <><path d="M19 30C39 14 80 13 104 29 80 45 39 47 19 30Z"/><path d="M22 30 3 17v28Z"/><path d="m48 17 11-14 13 13M47 44l12 11 13-12"/><path d="M101 29h15"/><circle cx="94" cy="25" r="2.4" class="fish-eye"/></>}
|
{family === "salmonid" && <><path d="M20 30C39 12 79 12 103 28 81 46 40 47 20 30Z"/><path d="M23 30 3 13v34Z"/><path d="m50 16 11-13 15 12M51 43l11 12 14-12"/><path d="m80 17 7-7 5 9"/><circle cx="94" cy="25" r="2.2" class="fish-eye"/></>}
|
||||||
{variant === 2 && <><path d="M20 30C34 3 79 4 103 29 79 54 34 55 20 30Z"/><path d="M23 30 2 8v44Z"/><path d="m52 10 8-9 14 10M52 49l9 8 14-10"/><circle cx="92" cy="24" r="2.4" class="fish-eye"/></>}
|
{family === "cyprinid" && <><path d="M22 29C38 5 77 5 101 29 77 53 38 53 22 29Z"/><path d="M25 29 3 9v40Z"/><path d="M52 10 68 1l12 13M53 48l16 9 12-14"/><circle cx="91" cy="24" r="2.2" class="fish-eye"/></>}
|
||||||
|
{family === "perch" && <><path d="M20 31C39 13 79 14 103 29 80 47 39 49 20 31Z"/><path d="M23 31 3 16v31Z"/><path d="m42 18 7-15 7 13 8-13 8 14 8-10 5 13M52 45l13 11 15-13"/><circle cx="94" cy="25" r="2.2" class="fish-eye"/></>}
|
||||||
|
{family === "catfish" && <><path d="M17 31C37 17 78 17 104 29 81 45 38 47 17 31Z"/><path d="M21 31 2 18v28Z"/><path d="M47 20 65 7l13 13M48 43l17 11 14-13"/><path class="fish-detail" d="M99 31c9 1 13 5 18 10M99 33c8 4 9 9 12 15"/><circle cx="94" cy="27" r="2.2" class="fish-eye"/></>}
|
||||||
|
{family === "eel" && <><path d="M5 35c20-29 42 14 69-10 17-15 33-7 42 1-20-4-24 17-43 17C43 43 30 22 5 45Z"/><path d="m105 24 12-9-2 13"/><circle cx="106" cy="26" r="1.8" class="fish-eye"/></>}
|
||||||
|
{family === "flatfish" && <><path d="M18 31C37 4 80 6 108 29 80 52 37 54 18 31Z"/><path d="M21 31 3 17v29Z"/><path d="m53 12 13-10 13 11M54 49l13 8 13-10"/><circle cx="91" cy="23" r="2.1" class="fish-eye"/><circle cx="97" cy="26" r="1.7" class="fish-eye"/></>}
|
||||||
|
{family === "marine" && <><path d="M18 30C40 14 78 15 101 28l16-5-6 10 5 8-16-5C76 45 39 45 18 30Z"/><path d="M22 30 2 12v36Z"/><path d="m54 17 13-13 12 13M54 43l13 11 12-12"/><circle cx="94" cy="26" r="2.1" class="fish-eye"/></>}
|
||||||
|
{family === "generic" && <><path d="M21 29C38 9 78 10 103 29 78 48 38 49 21 29Z"/><path d="m24 29-21-17v35Z"/><path d="M54 15 68 3l10 14M55 44l14 11 10-15"/><circle cx="93" cy="25" r="2.2" class="fish-eye"/></>}
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export type FishVisualFamily = "pike" | "salmonid" | "cyprinid" | "perch" | "catfish" | "eel" | "flatfish" | "marine" | "generic";
|
||||||
|
|
||||||
|
const FAMILY_TERMS: Array<[FishVisualFamily, RegExp]> = [
|
||||||
|
["pike", /щук|судак|pike|pickerel|zander|hecht/i],
|
||||||
|
["salmonid", /форел|лосос|сёмг|голец|таймен|хариус|trout|salmon|char|grayling|forelle|lachs|saibling/i],
|
||||||
|
["cyprinid", /карп|карась|лещ|плотв|линь|язь|жерех|голав|усач|амур|толстолоб|carp|bream|roach|tench|barbel|karpfen|brasse/i],
|
||||||
|
["perch", /окун|ёрш|perch|ruff|barsch/i],
|
||||||
|
["catfish", /сом|catfish|wels/i],
|
||||||
|
["eel", /угор|миног|eel|lamprey|aal/i],
|
||||||
|
["flatfish", /камбал|палтус|скат|flounder|halibut|ray|rochen/i],
|
||||||
|
["marine", /сельд|треск|скумбр|тунец|акул|сардин|herring|cod|mackerel|tuna|shark|hering|kabeljau/i],
|
||||||
|
];
|
||||||
|
|
||||||
|
export function fishVisualFamily(name: string): FishVisualFamily {
|
||||||
|
return FAMILY_TERMS.find(([, pattern]) => pattern.test(name))?.[0] ?? "generic";
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
.fish-silhouette{fill:currentColor;color:#315f63;pointer-events:none}.fish-silhouette .fish-eye{fill:var(--paper)}.spot-main{position:relative}.spot-main>.fish-silhouette{position:absolute;right:2px;top:38px;opacity:.075}.catalog-grid>a{position:relative;overflow:hidden}.catalog-grid>a>.fish-silhouette{position:absolute;right:34px;bottom:-3px;opacity:.07;transform:rotate(-4deg)}@media(max-width:720px){.spot-main>.fish-silhouette{width:68px;right:0;opacity:.055}}
|
.fish-silhouette{fill:currentColor;color:#315f63;pointer-events:none}.fish-silhouette .fish-eye{fill:var(--paper)}.fish-silhouette .fish-detail{fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round}.spot-main{position:relative}.spot-main>.fish-silhouette{position:absolute;right:2px;top:38px;opacity:.075}.catalog-grid>a{position:relative;overflow:hidden}.catalog-grid>a>.fish-silhouette{position:absolute;right:20px;bottom:12px;opacity:.12;transform:rotate(-4deg);transition:opacity var(--motion-base) var(--ease-out),transform var(--motion-base) var(--ease-out)}.catalog-grid>a:hover>.fish-silhouette{opacity:.2;transform:rotate(-2deg) translateX(-2px)}@media(max-width:720px){.spot-main>.fish-silhouette{width:68px;right:0;opacity:.055}}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
|||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
|
|
||||||
import { activityLevel, plural } from "../../src/lib/presentation.ts";
|
import { activityLevel, plural } from "../../src/lib/presentation.ts";
|
||||||
|
import { fishVisualFamily } from "../../src/lib/fish-visuals.ts";
|
||||||
|
|
||||||
test("activity levels share one complete 0-100 scale", () => {
|
test("activity levels share one complete 0-100 scale", () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
@@ -17,3 +18,10 @@ test("Russian plural forms cover common and exceptional endings", () => {
|
|||||||
["точек", "точка", "точки", "точек", "точек", "точка", "точки", "точек"],
|
["точек", "точка", "точки", "точек", "точек", "точка", "точки", "точек"],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("fish visual families follow body type instead of a name hash", () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
["Щука", "Озёрная форель", "Карп", "Окунь", "Сом", "Угорь", "Палтус", "Треска", "Неизвестная рыба"].map(fishVisualFamily),
|
||||||
|
["pike", "salmonid", "cyprinid", "perch", "catfish", "eel", "flatfish", "marine", "generic"],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@
|
|||||||
- [x] **B05 · Motion-система.** Смысловые микроанимации пульса, поплавка, загрузки и интерактивного отклика используют общие duration/easing-токены; декоративное движение empty-state удалено, reduced motion покрывает элементы и псевдоэлементы.
|
- [x] **B05 · Motion-система.** Смысловые микроанимации пульса, поплавка, загрузки и интерактивного отклика используют общие duration/easing-токены; декоративное движение empty-state удалено, reduced motion покрывает элементы и псевдоэлементы.
|
||||||
- [x] **B06 · Brand QA.** Desktop/mobile-проверка усилила каталоги atlas-hero, счётчиками, береговыми контурами и выразительными карточками. Favicon/PWA PNG перегенерированы из SVG-мастера без размытия; `any` и полнофоновые `maskable`-иконки разделены, manifest и Caddy cache обновлены, OG подтверждён как 1200×630.
|
- [x] **B06 · Brand QA.** Desktop/mobile-проверка усилила каталоги atlas-hero, счётчиками, береговыми контурами и выразительными карточками. Favicon/PWA PNG перегенерированы из SVG-мастера без размытия; `any` и полнофоновые `maskable`-иконки разделены, manifest и Caddy cache обновлены, OG подтверждён как 1200×630.
|
||||||
- [x] **B07 · Аудит официальных изображений.** Текущий records-parser получает названия рыб/водоёмов и `title` приманки, но не image URL; официальные gallery/userguide содержат тематические медиа без устойчивого полного соответствия каноническим сущностям. Изображения не хотлинкать и не считать разрешение на данные автоматическим разрешением на медиапубликацию.
|
- [x] **B07 · Аудит официальных изображений.** Текущий records-parser получает названия рыб/водоёмов и `title` приманки, но не image URL; официальные gallery/userguide содержат тематические медиа без устойчивого полного соответствия каноническим сущностям. Изображения не хотлинкать и не считать разрешение на данные автоматическим разрешением на медиапубликацию.
|
||||||
- [ ] **B08 · Семейства силуэтов рыб.** Заменить три случайных варианта по hash имени на осмысленные `visual_family`: щуковые, карповые, лососёвые, окуневые, сомовые, угреобразные, плоские и морские. Название остаётся главным идентификатором; семейство хранится отдельно и допускает ручную коррекцию.
|
- [x] **B08 · Семейства силуэтов рыб.** Три случайных hash-варианта заменены классификатором и отдельными формами `pike`, `salmonid`, `cyprinid`, `perch`, `catfish`, `eel`, `flatfish`, `marine`, плюс честный `generic`. Компонент допускает ручное переопределение family; название остаётся главным идентификатором.
|
||||||
- [ ] **B09 · Глифы снастей и приманок.** Ввести компактные SVG-глифы по типу (`spinner`, `wobbler`, `soft`, `boilie`, `worm`, `hook`, `rig`, `unknown`) и детерминированный цвет по normalized name. Не изображать конкретную модель, если известна только строка из рекорда.
|
- [ ] **B09 · Глифы снастей и приманок.** Ввести компактные SVG-глифы по типу (`spinner`, `wobbler`, `soft`, `boilie`, `worm`, `hook`, `rig`, `unknown`) и детерминированный цвет по normalized name. Не изображать конкретную модель, если известна только строка из рекорда.
|
||||||
- [ ] **B10 · Визуальные отпечатки водоёмов.** Расширить текущие три береговых контура до детерминированного набора по slug: форма берега, число волн, положение точки и индексный код. Явно обозначать их как абстрактный знак, не карту и не игровую географию.
|
- [ ] **B10 · Визуальные отпечатки водоёмов.** Расширить текущие три береговых контура до детерминированного набора по slug: форма берега, число волн, положение точки и индексный код. Явно обозначать их как абстрактный знак, не карту и не игровую географию.
|
||||||
- [ ] **B11 · Опциональный media pipeline.** После отдельного письменного разрешения на изображения добавить allowlist официальных asset URL, локальное сохранение без hotlink, content hash, MIME/dimensions, attribution/source URL, дату получения и fallback на SVG. Изменение/удаление оригинала должно отправлять media asset на повторную проверку, а не молча менять публичную карточку.
|
- [ ] **B11 · Опциональный media pipeline.** После отдельного письменного разрешения на изображения добавить allowlist официальных asset URL, локальное сохранение без hotlink, content hash, MIME/dimensions, attribution/source URL, дату получения и fallback на SVG. Изменение/удаление оригинала должно отправлять media asset на повторную проверку, а не молча менять публичную карточку.
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ CTA задаются смыслом через `data-action`: `primary` — ос
|
|||||||
|
|
||||||
Каталоги используют собственную редакционную композицию: круглый тёмный seal показывает тип и объём справочника, порядковые номера создают ощущение системного атласа. Водоём обозначается абстрактным береговым контуром и точкой наблюдения, рыба — силуэтом. Эти фоновые знаки всегда декоративны, имеют низкий контраст и не заменяют текстовое название.
|
Каталоги используют собственную редакционную композицию: круглый тёмный seal показывает тип и объём справочника, порядковые номера создают ощущение системного атласа. Водоём обозначается абстрактным береговым контуром и точкой наблюдения, рыба — силуэтом. Эти фоновые знаки всегда декоративны, имеют низкий контраст и не заменяют текстовое название.
|
||||||
|
|
||||||
|
Силуэт рыбы выбирается по узнаваемому типу тела, а не случайному hash имени: щуковые, лососёвые, карповые, окуневые, сомовые, угреобразные, плоские и морские. Классификатор поддерживает русские и основные названия источников, неизвестная сущность получает нейтральную форму. Явный `family` имеет приоритет над эвристикой и служит точкой ручной коррекции.
|
||||||
|
|
||||||
## Brand assets и QA
|
## Brand assets и QA
|
||||||
|
|
||||||
`favicon.svg` — мастер обычных favicon, Apple Touch и PWA `purpose: any`; PNG генерируются из него напрямую в целевом размере, а не масштабируются из малого растра. `icon-maskable.svg` — полнофоновый мастер для отдельных `purpose: maskable` 192/512 px, чтобы системная маска не открывала прозрачные углы. OG имеет ровно 1200×630 px и использует тот же тёмный водный фон, лаймовый поплавок и координатную сетку. Проверены desktop и 390 px каталоги, 32/180/192/512 px assets, светлая поверхность сайта и тёмные бренд-носители.
|
`favicon.svg` — мастер обычных favicon, Apple Touch и PWA `purpose: any`; PNG генерируются из него напрямую в целевом размере, а не масштабируются из малого растра. `icon-maskable.svg` — полнофоновый мастер для отдельных `purpose: maskable` 192/512 px, чтобы системная маска не открывала прозрачные углы. OG имеет ровно 1200×630 px и использует тот же тёмный водный фон, лаймовый поплавок и координатную сетку. Проверены desktop и 390 px каталоги, 32/180/192/512 px assets, светлая поверхность сайта и тёмные бренд-носители.
|
||||||
|
|||||||
Reference in New Issue
Block a user