@jjlmoya/utils-games-development 1.68.0 → 1.69.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +5 -1
  4. package/src/tests/locale_completeness.test.ts +2 -2
  5. package/src/tests/tool_validation.test.ts +2 -2
  6. package/src/tool/gamePixelPerUnitPlanner/bibliography.astro +6 -0
  7. package/src/tool/gamePixelPerUnitPlanner/bibliography.ts +20 -0
  8. package/src/tool/gamePixelPerUnitPlanner/component.astro +56 -0
  9. package/src/tool/gamePixelPerUnitPlanner/controller.ts +127 -0
  10. package/src/tool/gamePixelPerUnitPlanner/dom-views.ts +47 -0
  11. package/src/tool/gamePixelPerUnitPlanner/entry.ts +27 -0
  12. package/src/tool/gamePixelPerUnitPlanner/evaluator.ts +14 -0
  13. package/src/tool/gamePixelPerUnitPlanner/game-pixel-per-unit-planner.css +652 -0
  14. package/src/tool/gamePixelPerUnitPlanner/i18n/de.ts +51 -0
  15. package/src/tool/gamePixelPerUnitPlanner/i18n/en.ts +171 -0
  16. package/src/tool/gamePixelPerUnitPlanner/i18n/es.ts +53 -0
  17. package/src/tool/gamePixelPerUnitPlanner/i18n/fr.ts +33 -0
  18. package/src/tool/gamePixelPerUnitPlanner/i18n/id.ts +33 -0
  19. package/src/tool/gamePixelPerUnitPlanner/i18n/it.ts +33 -0
  20. package/src/tool/gamePixelPerUnitPlanner/i18n/ja.ts +33 -0
  21. package/src/tool/gamePixelPerUnitPlanner/i18n/ko.ts +33 -0
  22. package/src/tool/gamePixelPerUnitPlanner/i18n/nl.ts +33 -0
  23. package/src/tool/gamePixelPerUnitPlanner/i18n/pl.ts +33 -0
  24. package/src/tool/gamePixelPerUnitPlanner/i18n/pt.ts +33 -0
  25. package/src/tool/gamePixelPerUnitPlanner/i18n/ru.ts +33 -0
  26. package/src/tool/gamePixelPerUnitPlanner/i18n/sv.ts +33 -0
  27. package/src/tool/gamePixelPerUnitPlanner/i18n/tr.ts +33 -0
  28. package/src/tool/gamePixelPerUnitPlanner/i18n/zh.ts +33 -0
  29. package/src/tool/gamePixelPerUnitPlanner/index.ts +11 -0
  30. package/src/tool/gamePixelPerUnitPlanner/logic.test.ts +34 -0
  31. package/src/tool/gamePixelPerUnitPlanner/logic.ts +129 -0
  32. package/src/tool/gamePixelPerUnitPlanner/seo.astro +15 -0
  33. package/src/tool/gamePixelPerUnitPlanner/storage.ts +23 -0
  34. package/src/tool/gamePixelPerUnitPlanner/ui.ts +65 -0
  35. package/src/tools.ts +2 -0
@@ -0,0 +1,33 @@
1
+ import type { FAQPage, HowTo, SoftwareApplication, WithContext } from 'schema-dts';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+ import type { GamePixelPerUnitPlannerUI } from '../ui';
4
+ import { bibliographyEntries } from '../bibliography';
5
+
6
+ const faq = [
7
+ { question: 'Co oznacza liczba pikseli na jednostkę w grze?', answer: 'Piksele na jednostkę, czyli PPU, mówią, ile pikseli tekstury reprezentuje jedną jednostkę świata. Spójna gęstość pomaga utrzymać przewidywalną skalę spriteów, kafelków i kamery.' },
8
+ { question: 'Dlaczego skale całkowite są ważne?', answer: 'Skala całkowita daje każdemu pikselowi źródłowemu tę samą całkowitą liczbę pikseli ekranu. Ułamki mogą tworzyć nierówne krawędzie lub rozmycie.' },
9
+ { question: 'Czym jest pixel bleeding?', answer: 'To niepożądany kolor pobrany z sąsiedniego teksela albo innego obszaru atlasu. Filtrowanie, krawędzie, ruch subpikselowy i brak marginesu mogą go powodować.' },
10
+ { question: 'Jak używać zalecanej skali?', answer: 'Potraktuj ją jako kandydata, który mieści się w rozdzielczości i jest blisko celu. Potem sprawdź w silniku filtr nearest, ustawienie kamery i margines atlasu.' },
11
+ { question: 'Czy planista wybiera właściwe PPU dla każdego silnika?', answer: 'Nie. To przejrzysta pomoc arytmetyczna. Kamera, import, mipmapy, zaokrąglenia i pixel snapping różnią się między silnikami i wymagają testu.' },
12
+ ];
13
+ const howTo = [
14
+ { name: 'Wybierz ekran docelowy', text: 'Podaj szerokość i wysokość widoku gry lub rozdzielczości referencyjnej w pikselach.' },
15
+ { name: 'Wczytaj sprite', text: 'Wybierz obraz, aby wykryć jego natywne wymiary. Możesz też użyć dołączonego spritea Boba.' },
16
+ { name: 'Wybierz skalę', text: 'Przesuń suwak albo wybierz preset. Całkowite mnożniki są najostrzejszymi kandydatami.' },
17
+ { name: 'Odczytaj podgląd', text: 'Sprawdź footprint spritea, poziome i pionowe PPU, widoczny świat oraz ostrzeżenie o bleeding.' },
18
+ { name: 'Przetestuj w silniku', text: 'Zweryfikuj wybór z filtrem nearest, wyrównaną kamerą, marginesem atlasu i prawdziwymi rozdzielczościami.' },
19
+ ];
20
+ const softwareApplication: WithContext<SoftwareApplication> = { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: 'Planista pikseli na jednostkę dla gier', applicationCategory: 'DeveloperApplication', operatingSystem: 'Any' };
21
+ const faqPage: WithContext<FAQPage> = { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) };
22
+ const howToSchema: WithContext<HowTo> = { '@context': 'https://schema.org', '@type': 'HowTo', name: 'Sprawdzanie skali spritea pixel art', step: howTo.map((step) => ({ '@type': 'HowToStep', name: step.name, text: step.text })) };
23
+
24
+ export const content: ToolLocaleContent<GamePixelPerUnitPlannerUI> = {
25
+ slug: 'planista-pikseli-na-jednostke-sprite-gra', title: 'Planista pikseli na jednostkę dla spriteów gier', description: 'Wczytaj sprite albo użyj przykładu Boba, aby zobaczyć footprint przy skalach całkowitych, porównać PPU i znaleźć ryzyko pixel bleeding.',
26
+ ui: {
27
+ inputsTitle: 'Wczytaj i sprawdź sprite', uploadTitle: 'Twój obraz źródłowy', uploadHint: 'Wybierz PNG, GIF, WebP lub JPEG. Natywne wymiary sterują każdym podglądem.', chooseSpriteLabel: 'Wybierz sprite', noSpriteLabel: 'Nie wczytano spritea', defaultSpriteLabel: 'Przykładowy sprite Boba', loadedSpriteLabel: 'Wczytano', clearSpriteLabel: 'Usuń sprite', displayWidthLabel: 'Szerokość ekranu px', displayHeightLabel: 'Wysokość ekranu px', spriteWidthLabel: 'Szerokość spritea px', spriteHeightLabel: 'Wysokość spritea px', worldWidthLabel: 'Szerokość spritea w jednostkach', worldHeightLabel: 'Wysokość spritea w jednostkach', targetScaleLabel: 'Docelowa skala ekranu', targetScaleHint: 'Piksele ekranu na każdy piksel tekstury.', resolutionPresetsLabel: 'Rozdzielczości referencyjne', preset320: '320 x 180', preset384: '384 x 216', preset640: '640 x 360', scalePresetsLabel: 'Szybkie skale', scale1: '1x', scale2: '2x', scale3: '3x', scale4: '4x', scale6: '6x', resetLabel: 'Przywróć wartości', fieldTitle: 'Zobacz różne rozmiary', fieldCaption: 'Wczytany obraz jest renderowany ze skalowaniem nearest, aby ocenić rzeczywisty footprint przy każdym całkowitym mnożniku.', previewPlaceholder: 'Wczytaj sprite, aby rozpocząć test wizualny', previewScaleLabel: 'Skala podglądu', sourceImageAlt: 'Podgląd wczytanego spritea', viewportLabel: 'Ekran', spriteLabel: 'Wyrenderowany sprite', crispTitle: 'Ostre skale', crispCaption: 'Całkowite mnożniki zachowują równy rozmiar pikseli. Szare kroki przekraczają podany ekran.', fitLabel: 'Mieści się na ekranie:', yesLabel: 'tak', noLabel: 'nie', recommendedLabel: 'najbliższe dopasowanie', summaryTitle: 'Podsumowanie planu', ppuXLabel: 'PPU poziome', ppuYLabel: 'PPU pionowe', viewportWorldLabel: 'Widoczny świat', fitScaleLabel: 'Największa pasująca skala', bleedingRiskLabel: 'Ryzyko bleeding', lowRisk: 'Niskie', mediumRisk: 'Średnie', highRisk: 'Wysokie', riskLowMessage: 'Osie są wyrównane, a całkowita skala mieści się na ekranie. Nadal sprawdź filtrowanie i margines atlasu.', riskMediumMessage: 'Cel pasuje niedokładnie. Sprawdź różnicę osi i wyróżnione skale.', riskHighMessage: 'Skala może tworzyć nierówne próbkowanie. Wybierz całkowity mnożnik i sprawdź wymiary spritea.', alignmentLabel: 'Uwaga o próbkowaniu', tableTitle: 'Dostępny rejestr skal', tableScale: 'Skala', tableWidth: 'Wyrenderowana szerokość', tableHeight: 'Wyrenderowana wysokość', tableFits: 'Mieści się na ekranie', modelNote: 'PPU to wyrenderowane piksele spritea podzielone przez jego rozmiar w jednostkach na każdej osi. Ryzyko bleeding jest heurystyką, a nie inspekcją tekstury ani gwarancją silnika.', privacyDisclosure: 'Plik jest przetwarzany w tej przeglądarce. Spritey, pliki projektu i telemetria nie są wysyłane.', statusReady: 'Podgląd zaktualizowany', unitPixels: 'px', unitUnits: 'jednostki',
28
+ },
29
+ seo: [
30
+ { type: 'title', level: 2, text: 'Zamień rozmiar spritea w decyzję o skali' }, { type: 'paragraph', html: 'Sprite ma rozmiar bitmapy oraz rozmiar zajmowany w świecie gry. PPU łączy te miary. Podgląd pokazuje skutek wizualny zamiast pozostawiać decyzję abstrakcyjnej liczbie.' }, { type: 'paragraph', html: 'Wczytaj prawdziwy obraz, aby użyć jego natywnych wymiarów. Narzędzie mnoży obie osie przez wybraną skalę i oblicza widoczny fragment świata.' }, { type: 'title', level: 2, text: 'Co obserwować w podglądzie' }, { type: 'list', items: ['Porównaj footprint z ekranem referencyjnym.', 'Najpierw sprawdź skale całkowite.', 'Użyj obu wartości PPU, aby znaleźć niezamierzone rozciągnięcie.', 'Potraktuj ostrzeżenie bleeding jako sygnał do testu w silniku.'] }, { type: 'title', level: 2, text: 'Dlaczego skale całkowite są czystsze' }, { type: 'paragraph', html: 'Przy 3x każdy piksel źródłowy zajmuje trzy piksele ekranu. Przy 2,5x renderer musi rozdzielać różne szerokości. Nearest nie miesza kolorów, ale nie naprawia kamery ustawionej między pikselami.' }, { type: 'table', headers: ['Sygnał', 'Odczyt', 'Następna decyzja'], rows: [['Równe PPU', 'Obie osie mają tę samą gęstość.', 'Porównaj z kafelkami i siatką projektu.'], ['Skala ułamkowa', 'Footprint nie używa liczby całkowitej.', 'Sprawdź najbliższą skalę całkowitą.'], ['Nie mieści się', 'Sprite przekracza ekran.', 'Zmniejsz skalę albo zwiększ rozdzielczość.']] }, { type: 'title', level: 2, text: 'Odróżnij bleeding od rozmiaru' }, { type: 'paragraph', html: 'Pixel bleeding często pochodzi z sąsiednich tekseli atlasu, filtrowania przy krawędziach lub niewyrównanych współrzędnych kamery. Przy rozmyciu sprawdź filtr, a przy szwach także margines i granice.' }, { type: 'title', level: 2, text: 'Naucz się na spricie Boba' }, { type: 'paragraph', html: 'Przykład Boba z różową kokardą widać od razu. Zmieniaj rozdzielczość i skalę, aby zobaczyć, kiedy postać przestaje się mieścić albo piksele tracą równość.' }, { type: 'title', level: 2, text: 'Co narzędzie sprawdza, a czego nie' }, { type: 'paragraph', html: 'Narzędzie porównuje wymiary i footprinty w kontrolowanym widoku. Nie otwiera projektu silnika, nie bada atlasu, nie mierzy urządzenia i nie gwarantuje ruchu pixel perfect.' }, { type: 'title', level: 2, text: 'Krótki sposób wyboru skali' }, { type: 'paragraph', html: 'Wczytaj sprite, wybierz rozdzielczość, sprawdź 1x do 4x i wybierz największą skalę całkowitą z wolnym miejscem. Powtórz dla rozdzielczości obsługiwanych przez grę.' }, { type: 'tip', title: 'Ostatni test wykonaj w silniku', html: 'Użyj podglądu, aby zawęzić wybór. Potem włącz filtr nearest, sprawdź margines atlasu i ustawienie kamery oraz przetestuj kilka rozdzielczości.' },
31
+ ],
32
+ faqTitle: 'Pytania o skalowanie spriteów', faq, bibliographyTitle: 'Źródła o pixel arcie', bibliography: bibliographyEntries, howTo, schemas: [softwareApplication, faqPage, howToSchema],
33
+ };
@@ -0,0 +1,33 @@
1
+ import type { FAQPage, HowTo, SoftwareApplication, WithContext } from 'schema-dts';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+ import type { GamePixelPerUnitPlannerUI } from '../ui';
4
+ import { bibliographyEntries } from '../bibliography';
5
+
6
+ const faq = [
7
+ { question: 'O que significa pixels por unidade em um jogo?', answer: 'Pixels por unidade, ou PPU, indica quantos pixels da textura representam uma unidade do mundo. Uma densidade coerente mantém sprites, tiles e câmera numa escala previsível.' },
8
+ { question: 'Por que escalas inteiras são importantes?', answer: 'Uma escala inteira dá a cada pixel de origem o mesmo número inteiro de pixels na tela. Frações podem criar bordas irregulares ou suavização.' },
9
+ { question: 'O que é pixel bleeding?', answer: 'É uma cor indesejada de um texel vizinho ou de outra área do atlas. Filtro, bordas, movimento subpixel e pouca margem podem causar esse problema.' },
10
+ { question: 'Como uso a escala recomendada?', answer: 'Use-a como candidata que cabe na resolução e fica próxima do alvo. Depois confira no motor o filtro nearest, o alinhamento da câmera e a margem do atlas.' },
11
+ { question: 'O planejador escolhe o PPU correto para todo motor?', answer: 'Não. É uma ajuda matemática transparente. Câmera, importação, mipmaps, arredondamento e pixel snapping variam e exigem um teste real.' },
12
+ ];
13
+ const howTo = [
14
+ { name: 'Escolher a tela de destino', text: 'Informe a largura e a altura da tela do jogo ou da resolução de referência em pixels.' },
15
+ { name: 'Carregar o sprite', text: 'Escolha uma imagem para detectar suas dimensões nativas. Você também pode usar o sprite Bob incluído.' },
16
+ { name: 'Escolher uma escala', text: 'Mova o controle ou escolha um preset. Multiplicadores inteiros são os candidatos mais nítidos.' },
17
+ { name: 'Ler a preview', text: 'Confira o footprint do sprite, o PPU horizontal e vertical, o mundo visível e o aviso de bleeding.' },
18
+ { name: 'Testar no motor', text: 'Valide a escolha com filtro nearest, câmera alinhada, margem de atlas e resoluções reais.' },
19
+ ];
20
+ const softwareApplication: WithContext<SoftwareApplication> = { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: 'Planejador de pixels por unidade para jogos', applicationCategory: 'DeveloperApplication', operatingSystem: 'Any' };
21
+ const faqPage: WithContext<FAQPage> = { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) };
22
+ const howToSchema: WithContext<HowTo> = { '@context': 'https://schema.org', '@type': 'HowTo', name: 'Testar a escala de um sprite pixel art', step: howTo.map((step) => ({ '@type': 'HowToStep', name: step.name, text: step.text })) };
23
+
24
+ export const content: ToolLocaleContent<GamePixelPerUnitPlannerUI> = {
25
+ slug: 'planejador-pixels-por-unidade-sprite-jogos', title: 'Planejador de pixels por unidade para sprites de jogos', description: 'Envie um sprite ou use o exemplo do Bob para ver o footprint em várias escalas inteiras, comparar o PPU e encontrar risco de pixel bleeding.',
26
+ ui: {
27
+ inputsTitle: 'Carregue e teste seu sprite', uploadTitle: 'Sua imagem de origem', uploadHint: 'Escolha PNG, GIF, WebP ou JPEG. As dimensões nativas alimentam cada preview.', chooseSpriteLabel: 'Escolher sprite', noSpriteLabel: 'Nenhum sprite carregado', defaultSpriteLabel: 'Sprite Bob de exemplo', loadedSpriteLabel: 'Carregado', clearSpriteLabel: 'Remover sprite', displayWidthLabel: 'Largura da tela px', displayHeightLabel: 'Altura da tela px', spriteWidthLabel: 'Largura do sprite px', spriteHeightLabel: 'Altura do sprite px', worldWidthLabel: 'Largura do sprite em unidades', worldHeightLabel: 'Altura do sprite em unidades', targetScaleLabel: 'Escala alvo na tela', targetScaleHint: 'Pixels na tela para cada pixel da textura.', resolutionPresetsLabel: 'Resoluções de referência', preset320: '320 x 180', preset384: '384 x 216', preset640: '640 x 360', scalePresetsLabel: 'Escalas rápidas', scale1: '1x', scale2: '2x', scale3: '3x', scale4: '4x', scale6: '6x', resetLabel: 'Redefinir valores', fieldTitle: 'Veja em vários tamanhos', fieldCaption: 'A imagem carregada usa escala nearest para avaliar seu footprint real em cada multiplicador inteiro.', previewPlaceholder: 'Carregue um sprite para começar o teste visual', previewScaleLabel: 'Escala da preview', sourceImageAlt: 'Preview do sprite carregado', viewportLabel: 'Tela', spriteLabel: 'Sprite renderizado', crispTitle: 'Escalas nítidas', crispCaption: 'Multiplicadores inteiros mantêm os pixels uniformes. Etapas cinzentas ultrapassam a tela declarada.', fitLabel: 'Cabe na tela:', yesLabel: 'sim', noLabel: 'não', recommendedLabel: 'ajuste mais próximo', summaryTitle: 'Resumo do plano', ppuXLabel: 'PPU horizontal', ppuYLabel: 'PPU vertical', viewportWorldLabel: 'Mundo visível', fitScaleLabel: 'Maior escala que cabe', bleedingRiskLabel: 'Risco de bleeding', lowRisk: 'Baixo', mediumRisk: 'Médio', highRisk: 'Alto', riskLowMessage: 'Os eixos estão alinhados e a escala alvo é inteira e cabe na tela. Confira ainda o filtro e a margem do atlas.', riskMediumMessage: 'O alvo se ajusta de forma imperfeita. Examine a diferença entre os eixos e as escalas destacadas.', riskHighMessage: 'A escala pode criar amostragem irregular. Prefira um multiplicador inteiro e confira as dimensões.', alignmentLabel: 'Nota de amostragem', tableTitle: 'Registro acessível de escalas', tableScale: 'Escala', tableWidth: 'Largura renderizada', tableHeight: 'Altura renderizada', tableFits: 'Cabe na tela', modelNote: 'O PPU é calculado como pixels renderizados do sprite divididos pelo seu tamanho em unidades em cada eixo. O risco de bleeding é uma heurística, não uma inspeção da textura nem uma garantia do motor.', privacyDisclosure: 'O arquivo é processado neste navegador. Sprites, arquivos do projeto e telemetria não são enviados.', statusReady: 'Preview atualizada', unitPixels: 'px', unitUnits: 'unidades',
28
+ },
29
+ seo: [
30
+ { type: 'title', level: 2, text: 'Transforme o tamanho do sprite em uma decisão de escala' }, { type: 'paragraph', html: 'Um sprite tem o tamanho do bitmap e o tamanho que ocupa no mundo do jogo. O PPU conecta essas medidas. A preview mostra a consequência visual em vez de deixar uma cifra abstrata decidir.' }, { type: 'paragraph', html: 'Carregue uma imagem real para usar suas dimensões nativas. A ferramenta multiplica cada eixo pela escala escolhida e calcula a parte do mundo visível na resolução informada.' }, { type: 'title', level: 2, text: 'O que observar na preview' }, { type: 'list', items: ['Compare o footprint com a tela de referência.', 'Teste primeiro escalas inteiras para manter pixels uniformes.', 'Use os dois PPUs para encontrar deformações não intencionais.', 'Trate o aviso de bleeding como sinal para testar o motor.'] }, { type: 'title', level: 2, text: 'Por que escalas inteiras costumam ser mais limpas' }, { type: 'paragraph', html: 'Em 3x, cada pixel de origem ocupa três pixels da tela. Em 2,5x, o renderizador precisa distribuir larguras diferentes. Nearest evita misturar cores, mas não corrige uma câmera entre posições de pixel.' }, { type: 'table', headers: ['Sinal', 'Leitura', 'Próxima decisão'], rows: [['PPU igual', 'Os dois eixos têm a mesma densidade.', 'Compare com tiles e a grade do projeto.'], ['Escala fracionária', 'O footprint não usa um inteiro.', 'Teste o inteiro mais próximo.'], ['Não cabe', 'O sprite ultrapassa a tela.', 'Reduza a escala ou aumente a resolução.']] }, { type: 'title', level: 2, text: 'Separe bleeding de tamanho' }, { type: 'paragraph', html: 'Pixel bleeding costuma vir de texels vizinhos no atlas, filtro nas bordas ou coordenadas de câmera desalinhadas. Para desfoque, confira o filtro; para emendas, confira também margem e limites.' }, { type: 'title', level: 2, text: 'Aprenda com o sprite Bob' }, { type: 'paragraph', html: 'O exemplo do Bob com o laço rosa aparece logo ao abrir. Mude resolução e escala para ver quando o personagem deixa de caber ou os pixels perdem uniformidade.' }, { type: 'title', level: 2, text: 'O que a ferramenta valida e o que não valida' }, { type: 'paragraph', html: 'A ferramenta compara dimensões e footprints em uma vista controlada. Não abre um projeto, não inspeciona um atlas, não mede um dispositivo e não garante movimento pixel perfect.' }, { type: 'title', level: 2, text: 'Um fluxo curto para escolher a escala' }, { type: 'paragraph', html: 'Carregue o sprite, escolha a resolução, teste de 1x a 4x e use a maior escala inteira que deixe espaço. Repita com as resoluções suportadas pelo jogo.' }, { type: 'tip', title: 'A última verificação acontece no motor', html: 'Use a preview para restringir a escolha. Depois ative filtro nearest, confira a margem do atlas e o alinhamento da câmera e teste várias resoluções.' },
31
+ ],
32
+ faqTitle: 'Perguntas sobre escala de sprites', faq, bibliographyTitle: 'Referências de pixel art', bibliography: bibliographyEntries, howTo, schemas: [softwareApplication, faqPage, howToSchema],
33
+ };
@@ -0,0 +1,33 @@
1
+ import type { FAQPage, HowTo, SoftwareApplication, WithContext } from 'schema-dts';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+ import type { GamePixelPerUnitPlannerUI } from '../ui';
4
+ import { bibliographyEntries } from '../bibliography';
5
+
6
+ const faq = [
7
+ { question: 'Что означает число пикселей на единицу в игре?', answer: 'Пиксели на единицу, или PPU, показывают, сколько пикселей текстуры представляют одну единицу мира. Единая плотность помогает согласовать спрайты, тайлы и камеру.' },
8
+ { question: 'Почему важен целочисленный масштаб?', answer: 'При целочисленном масштабе каждый исходный пиксель получает одинаковое целое число экранных пикселей. Дробный масштаб может дать неровные края или размытие.' },
9
+ { question: 'Что такое pixel bleeding?', answer: 'Это нежелательный цвет соседнего текселя или другой области атласа. Фильтрация, границы, движение между пикселями и малая прокладка могут его вызвать.' },
10
+ { question: 'Как использовать рекомендуемый масштаб?', answer: 'Выберите его как вариант, который помещается в разрешение и близок к цели. Затем проверьте в движке фильтрацию nearest, положение камеры и отступ атласа.' },
11
+ { question: 'Выбирает ли планировщик правильный PPU для любого движка?', answer: 'Нет. Это прозрачный арифметический помощник. Камера, импорт, mipmap, округление и привязка к пикселям зависят от движка и требуют проверки.' },
12
+ ];
13
+ const howTo = [
14
+ { name: 'Выберите целевой экран', text: 'Укажите ширину и высоту игрового окна или опорного разрешения в пикселях.' },
15
+ { name: 'Загрузите спрайт', text: 'Выберите изображение, чтобы определить его исходные размеры. Можно также использовать пример Bob.' },
16
+ { name: 'Выберите масштаб', text: 'Передвиньте ползунок или выберите пресет. Целые множители обычно дают самый четкий результат.' },
17
+ { name: 'Прочитайте превью', text: 'Проверьте размер спрайта, горизонтальный и вертикальный PPU, видимый мир и предупреждение о bleeding.' },
18
+ { name: 'Проверьте в движке', text: 'Подтвердите выбор с фильтрацией nearest, выровненной камерой, отступом атласа и реальными разрешениями.' },
19
+ ];
20
+ const softwareApplication: WithContext<SoftwareApplication> = { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: 'Планировщик пикселей на единицу для игр', applicationCategory: 'DeveloperApplication', operatingSystem: 'Any' };
21
+ const faqPage: WithContext<FAQPage> = { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) };
22
+ const howToSchema: WithContext<HowTo> = { '@context': 'https://schema.org', '@type': 'HowTo', name: 'Проверка масштаба пиксельного спрайта', step: howTo.map((step) => ({ '@type': 'HowToStep', name: step.name, text: step.text })) };
23
+
24
+ export const content: ToolLocaleContent<GamePixelPerUnitPlannerUI> = {
25
+ slug: 'planirovshchik-pikseley-na-edinitsu-sprayt-igry', title: 'Планировщик пикселей на единицу для игровых спрайтов', description: 'Загрузите спрайт или используйте пример Bob, чтобы увидеть его размер при целых масштабах, сравнить PPU и найти риск pixel bleeding.',
26
+ ui: {
27
+ inputsTitle: 'Загрузите и проверьте спрайт', uploadTitle: 'Исходное изображение', uploadHint: 'Выберите PNG, GIF, WebP или JPEG. Исходные размеры используются во всех превью.', chooseSpriteLabel: 'Выбрать спрайт', noSpriteLabel: 'Спрайт еще не загружен', defaultSpriteLabel: 'Пример спрайта Bob', loadedSpriteLabel: 'Загружен', clearSpriteLabel: 'Удалить спрайт', displayWidthLabel: 'Ширина экрана px', displayHeightLabel: 'Высота экрана px', spriteWidthLabel: 'Ширина спрайта px', spriteHeightLabel: 'Высота спрайта px', worldWidthLabel: 'Ширина спрайта в единицах', worldHeightLabel: 'Высота спрайта в единицах', targetScaleLabel: 'Целевой масштаб экрана', targetScaleHint: 'Экранные пиксели на один пиксель текстуры.', resolutionPresetsLabel: 'Опорные разрешения', preset320: '320 x 180', preset384: '384 x 216', preset640: '640 x 360', scalePresetsLabel: 'Быстрые масштабы', scale1: '1x', scale2: '2x', scale3: '3x', scale4: '4x', scale6: '6x', resetLabel: 'Сбросить значения', fieldTitle: 'Смотрите в разных размерах', fieldCaption: 'Загруженное изображение отображается с масштабированием nearest, чтобы оценить его размер при каждом целом множителе.', previewPlaceholder: 'Загрузите спрайт, чтобы начать визуальную проверку', previewScaleLabel: 'Масштаб превью', sourceImageAlt: 'Превью загруженного спрайта', viewportLabel: 'Экран', spriteLabel: 'Отображаемый спрайт', crispTitle: 'Четкие масштабы', crispCaption: 'Целые множители сохраняют одинаковый размер пикселей. Серые шаги выходят за пределы экрана.', fitLabel: 'Помещается на экране:', yesLabel: 'да', noLabel: 'нет', recommendedLabel: 'ближайший вариант', summaryTitle: 'Итог плана', ppuXLabel: 'PPU по горизонтали', ppuYLabel: 'PPU по вертикали', viewportWorldLabel: 'Видимый мир', fitScaleLabel: 'Максимальный подходящий масштаб', bleedingRiskLabel: 'Риск bleeding', lowRisk: 'Низкий', mediumRisk: 'Средний', highRisk: 'Высокий', riskLowMessage: 'Оси выровнены, целый масштаб помещается на экране. Все же проверьте фильтрацию и отступ атласа.', riskMediumMessage: 'Цель помещается не идеально. Изучите разницу осей и выделенные масштабы.', riskHighMessage: 'Этот масштаб может дать неравномерную выборку. Выберите целый множитель и проверьте размеры.', alignmentLabel: 'Примечание о выборке', tableTitle: 'Доступный список масштабов', tableScale: 'Масштаб', tableWidth: 'Ширина после рендера', tableHeight: 'Высота после рендера', tableFits: 'Помещается на экране', modelNote: 'PPU рассчитывается как число отображаемых пикселей спрайта, деленное на его размер в единицах по каждой оси. Риск bleeding является эвристикой, а не проверкой текстуры или гарантией движка.', privacyDisclosure: 'Файл обрабатывается в этом браузере. Спрайты, файлы проекта и телеметрия не отправляются.', statusReady: 'Превью обновлено', unitPixels: 'px', unitUnits: 'единицы',
28
+ },
29
+ seo: [
30
+ { type: 'title', level: 2, text: 'Превратите размер спрайта в решение о масштабе' }, { type: 'paragraph', html: 'У спрайта есть размер bitmap и размер в игровом мире. PPU связывает эти измерения. Превью показывает визуальный результат, а не только абстрактное число.' }, { type: 'paragraph', html: 'Загрузите настоящее изображение, чтобы использовать его исходные размеры. Инструмент умножает обе оси на выбранный масштаб и вычисляет видимую часть мира.' }, { type: 'title', level: 2, text: 'Что смотреть в превью' }, { type: 'list', items: ['Сравните размер спрайта с опорным экраном.', 'Сначала проверьте целые масштабы.', 'Используйте оба значения PPU для поиска растяжения.', 'Считайте предупреждение bleeding сигналом для теста движка.'] }, { type: 'title', level: 2, text: 'Почему целые масштабы выглядят чище' }, { type: 'paragraph', html: 'При 3x каждый исходный пиксель занимает три экранных пикселя. При 2.5x рендереру приходится распределять разные ширины. Nearest не смешивает цвета, но не исправляет камеру между пикселями.' }, { type: 'table', headers: ['Сигнал', 'Значение', 'Следующее решение'], rows: [['Одинаковый PPU', 'Плотность обеих осей совпадает.', 'Сравните с тайлами и сеткой проекта.'], ['Дробный масштаб', 'Размер не использует целый множитель.', 'Проверьте ближайший целый масштаб.'], ['Не помещается', 'Спрайт выходит за экран.', 'Уменьшите масштаб или увеличьте разрешение.']] }, { type: 'title', level: 2, text: 'Отделяйте bleeding от размера' }, { type: 'paragraph', html: 'Pixel bleeding часто появляется из-за соседних текселей атласа, фильтра на границе или невыравненных координат камеры. При размытости проверьте фильтр, при швах также отступ и границы.' }, { type: 'title', level: 2, text: 'Изучайте процесс на спрайте Bob' }, { type: 'paragraph', html: 'Пример Bob с розовым бантом виден сразу. Меняйте разрешение и масштаб, чтобы увидеть, когда персонаж перестает помещаться или пиксели теряют равномерность.' }, { type: 'title', level: 2, text: 'Что проверяет инструмент' }, { type: 'paragraph', html: 'Инструмент сравнивает размеры и footprint в контролируемом виде. Он не открывает проект движка, не проверяет атлас, не измеряет устройство и не гарантирует pixel perfect движение.' }, { type: 'title', level: 2, text: 'Короткий путь к выбору масштаба' }, { type: 'paragraph', html: 'Загрузите спрайт, выберите разрешение, проверьте 1x-4x и выберите самый большой целый масштаб с запасом. Повторите проверку на реальных разрешениях игры.' }, { type: 'tip', title: 'Последняя проверка проходит в движке', html: 'Используйте превью для сужения выбора. Затем включите nearest, проверьте отступ атласа и камеру и протестируйте несколько разрешений.' },
31
+ ],
32
+ faqTitle: 'Вопросы о масштабе спрайтов', faq, bibliographyTitle: 'Источники о пиксельной графике', bibliography: bibliographyEntries, howTo, schemas: [softwareApplication, faqPage, howToSchema],
33
+ };
@@ -0,0 +1,33 @@
1
+ import type { FAQPage, HowTo, SoftwareApplication, WithContext } from 'schema-dts';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+ import type { GamePixelPerUnitPlannerUI } from '../ui';
4
+ import { bibliographyEntries } from '../bibliography';
5
+
6
+ const faq = [
7
+ { question: 'Vad betyder pixlar per enhet i ett spel?', answer: 'Pixlar per enhet, eller PPU, visar hur många texturpixlar som motsvarar en världsenhet. En jämn täthet håller sprites, tiles och kamera på en förutsägbar skala.' },
8
+ { question: 'Varför är heltalskalor viktiga?', answer: 'En heltalsskala ger varje källpixel samma hela antal skärmpixlar. Bråkdelar kan skapa ojämna kanter eller oskärpa.' },
9
+ { question: 'Vad är pixel bleeding?', answer: 'Det är oönskad färg från en närliggande texel eller atlasdel. Filtrering, kanter, subpixelrörelse och för liten marginal kan orsaka det.' },
10
+ { question: 'Hur använder jag den rekommenderade skalan?', answer: 'Använd den som kandidat som ryms i upplösningen och ligger nära ditt mål. Kontrollera sedan nearest-filtrering, kamera och atlasmarginal i motorn.' },
11
+ { question: 'Väljer planeraren rätt PPU för varje motor?', answer: 'Nej. Det är ett transparent räkneverktyg. Kamera, import, mipmaps, avrundning och pixel snapping varierar och kräver ett verkligt test.' },
12
+ ];
13
+ const howTo = [
14
+ { name: 'Välj målskärmen', text: 'Ange bredd och höjd för spelvyn eller referensupplösningen i pixlar.' },
15
+ { name: 'Ladda en sprite', text: 'Välj en bild så att dess ursprungliga mått upptäcks. Du kan också använda Bobs exempel.' },
16
+ { name: 'Välj skala', text: 'Flytta reglaget eller välj ett preset. Heltalsmultiplikatorer är de skarpaste kandidaterna.' },
17
+ { name: 'Läs förhandsvisningen', text: 'Kontrollera sprite-footprint, horisontell och vertikal PPU, synlig värld och bleeding-varningen.' },
18
+ { name: 'Testa i motorn', text: 'Bekräfta valet med nearest-filtrering, justerad kamera, atlasmarginal och verkliga upplösningar.' },
19
+ ];
20
+ const softwareApplication: WithContext<SoftwareApplication> = { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: 'Planerare för pixlar per enhet i spel', applicationCategory: 'DeveloperApplication', operatingSystem: 'Any' };
21
+ const faqPage: WithContext<FAQPage> = { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) };
22
+ const howToSchema: WithContext<HowTo> = { '@context': 'https://schema.org', '@type': 'HowTo', name: 'Testa skalning av en pixel art-sprite', step: howTo.map((step) => ({ '@type': 'HowToStep', name: step.name, text: step.text })) };
23
+
24
+ export const content: ToolLocaleContent<GamePixelPerUnitPlannerUI> = {
25
+ slug: 'planerare-pixlar-per-enhet-sprite-spel', title: 'Planerare för pixlar per enhet för spelsprites', description: 'Ladda upp en sprite eller använd Bobs exempel för att se footprint vid heltalsskalor, jämföra PPU och hitta risk för pixel bleeding.',
26
+ ui: {
27
+ inputsTitle: 'Ladda och testa din sprite', uploadTitle: 'Din källbild', uploadHint: 'Välj PNG, GIF, WebP eller JPEG. De ursprungliga måtten används i varje preview.', chooseSpriteLabel: 'Välj sprite', noSpriteLabel: 'Ingen sprite laddad', defaultSpriteLabel: 'Bobs exempelsprite', loadedSpriteLabel: 'Laddad', clearSpriteLabel: 'Ta bort sprite', displayWidthLabel: 'Skärmbredd px', displayHeightLabel: 'Skärmhöjd px', spriteWidthLabel: 'Spritebredd px', spriteHeightLabel: 'Spritehöjd px', worldWidthLabel: 'Spritebredd i enheter', worldHeightLabel: 'Spritehöjd i enheter', targetScaleLabel: 'Målskala på skärmen', targetScaleHint: 'Skärmpixlar för varje pixel i texturen.', resolutionPresetsLabel: 'Referensupplösningar', preset320: '320 x 180', preset384: '384 x 216', preset640: '640 x 360', scalePresetsLabel: 'Snabba skalor', scale1: '1x', scale2: '2x', scale3: '3x', scale4: '4x', scale6: '6x', resetLabel: 'Återställ värden', fieldTitle: 'Se den i flera storlekar', fieldCaption: 'Den laddade bilden renderas med nearest-skalning så att du kan bedöma footprint vid varje heltalsmultiplikator.', previewPlaceholder: 'Ladda en sprite för att börja det visuella testet', previewScaleLabel: 'Preview-skala', sourceImageAlt: 'Förhandsvisning av laddad sprite', viewportLabel: 'Skärm', spriteLabel: 'Renderad sprite', crispTitle: 'Skarpa skalor', crispCaption: 'Heltalsmultiplikatorer håller pixlarna jämna. Grå steg överskrider den angivna skärmen.', fitLabel: 'Ryms på skärmen:', yesLabel: 'ja', noLabel: 'nej', recommendedLabel: 'närmast passande', summaryTitle: 'Plansammanfattning', ppuXLabel: 'Horisontell PPU', ppuYLabel: 'Vertikal PPU', viewportWorldLabel: 'Synlig värld', fitScaleLabel: 'Största passande skala', bleedingRiskLabel: 'Bleeding-risk', lowRisk: 'Låg', mediumRisk: 'Medel', highRisk: 'Hög', riskLowMessage: 'Axlarna är justerade och målskalan är ett heltal som ryms. Kontrollera ändå filtrering och atlasmarginal.', riskMediumMessage: 'Målet passar inte perfekt. Granska skillnaden mellan axlarna och de markerade skalorna.', riskHighMessage: 'Skalan kan ge ojämn sampling. Välj en heltalsmultiplikator och kontrollera sprite-måtten.', alignmentLabel: 'Samplingsnotis', tableTitle: 'Tillgängligt skalregister', tableScale: 'Skala', tableWidth: 'Renderad bredd', tableHeight: 'Renderad höjd', tableFits: 'Ryms på skärmen', modelNote: 'PPU beräknas som renderade spritepixlar delat med spritens storlek i enheter på varje axel. Bleeding-risk är en heuristik, inte en texturkontroll eller motorgaranti.', privacyDisclosure: 'Filen behandlas i denna webbläsare. Sprites, projektfiler och telemetri skickas inte.', statusReady: 'Preview uppdaterad', unitPixels: 'px', unitUnits: 'enheter',
28
+ },
29
+ seo: [
30
+ { type: 'title', level: 2, text: 'Gör spritens storlek till ett skalbeslut' }, { type: 'paragraph', html: 'En sprite har bitmapens storlek och storleken den upptar i spelvärlden. PPU kopplar ihop måtten. Previewn visar det visuella resultatet i stället för att låta en abstrakt siffra avgöra.' }, { type: 'paragraph', html: 'Ladda en riktig bild för att använda dess ursprungliga mått. Verktyget multiplicerar sedan varje axel med vald skala och räknar ut den synliga världen.' }, { type: 'title', level: 2, text: 'Vad du ska se i previewn' }, { type: 'list', items: ['Jämför footprint med referensskärmen.', 'Testa heltalsskalor först för jämna pixlar.', 'Använd båda PPU-värdena för att hitta oavsiktlig uttänjning.', 'Se bleeding-varningen som en signal för ett motortest.'] }, { type: 'title', level: 2, text: 'Varför heltalsskalor ofta blir renare' }, { type: 'paragraph', html: 'Vid 3x upptar varje källpixel tre skärmpixlar. Vid 2,5x måste renderaren fördela olika bredder. Nearest blandar inte färger, men korrigerar inte en kamera mellan pixelpositioner.' }, { type: 'table', headers: ['Signal', 'Tolkning', 'Nästa beslut'], rows: [['Samma PPU', 'Båda axlarna har samma täthet.', 'Jämför med tiles och projektets rutnät.'], ['Bråkig skala', 'Footprint använder inte ett heltal.', 'Testa närmaste heltalsskala.'], ['Ryms inte', 'Spriten överskrider skärmen.', 'Minska skalan eller öka upplösningen.']] }, { type: 'title', level: 2, text: 'Skilj bleeding från storlek' }, { type: 'paragraph', html: 'Pixel bleeding kommer ofta från granntesel i ett atlas, filtrering vid kanter eller kamera som inte är justerad. Kontrollera filtret vid oskärpa och även marginaler och gränser vid sömmar.' }, { type: 'title', level: 2, text: 'Lär dig flödet med Bob-spriten' }, { type: 'paragraph', html: 'Bob-exemplet med den rosa rosetten syns direkt. Ändra upplösning och skala för att se när figuren inte längre ryms eller pixlarna blir ojämna.' }, { type: 'title', level: 2, text: 'Vad verktyget kontrollerar och inte kontrollerar' }, { type: 'paragraph', html: 'Verktyget jämför mått och footprints i en kontrollerad vy. Det öppnar inget motorprojekt, granskar inget atlas, mäter ingen enhet och garanterar inte pixel perfect-rörelse.' }, { type: 'title', level: 2, text: 'En kort väg till rätt skala' }, { type: 'paragraph', html: 'Ladda spriten, välj upplösning, prova 1x till 4x och välj största heltalsskala med luft kvar. Upprepa sedan för spelets verkliga upplösningar.' }, { type: 'tip', title: 'Det sista testet görs i motorn', html: 'Använd previewn för att begränsa valet. Aktivera sedan nearest, kontrollera atlasmarginal och kamera och testa flera upplösningar.' },
31
+ ],
32
+ faqTitle: 'Frågor om spriteskalning', faq, bibliographyTitle: 'Referenser om pixel art', bibliography: bibliographyEntries, howTo, schemas: [softwareApplication, faqPage, howToSchema],
33
+ };
@@ -0,0 +1,33 @@
1
+ import type { FAQPage, HowTo, SoftwareApplication, WithContext } from 'schema-dts';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+ import type { GamePixelPerUnitPlannerUI } from '../ui';
4
+ import { bibliographyEntries } from '../bibliography';
5
+
6
+ const faq = [
7
+ { question: 'Bir oyunda birim başına piksel ne demektir?', answer: 'Birim başına piksel veya PPU, bir dünya birimini kaç doku pikselinin temsil ettiğini gösterir. Tutarlı yoğunluk sprite, tile ve kameranın öngörülebilir ölçekte kalmasını sağlar.' },
8
+ { question: 'Tam sayı ölçekleri neden önemlidir?', answer: 'Tam sayı ölçeği her kaynak pikseline ekranda aynı sayıda tam piksel verir. Kesirli ölçekler düzensiz kenarlara veya bulanıklığa yol açabilir.' },
9
+ { question: 'Pixel bleeding nedir?', answer: 'Komşu bir texel veya atlas bölgesinden gelen istenmeyen renktir. Filtreleme, kenarlar, alt piksel hareketi ve yetersiz boşluk buna neden olabilir.' },
10
+ { question: 'Önerilen ölçeği nasıl kullanırım?', answer: 'Çözünürlüğe sığan ve hedefe yakın olan aday olarak kullanın. Ardından motorda nearest filtresini, kamera hizasını ve atlas boşluğunu kontrol edin.' },
11
+ { question: 'Planlayıcı her motor için doğru PPUyu seçer mi?', answer: 'Hayır. Bu şeffaf bir hesaplama yardımcısıdır. Kamera, içe aktarma, mipmap, yuvarlama ve pixel snapping motora göre değişir ve gerçek test ister.' },
12
+ ];
13
+ const howTo = [
14
+ { name: 'Hedef ekranı seçin', text: 'Oyun görünümünün veya referans çözünürlüğünün genişlik ve yüksekliğini piksel olarak girin.' },
15
+ { name: 'Sprite yükleyin', text: 'Yerel boyutları algılamak için bir görsel seçin. Dahili Bob spriteını da kullanabilirsiniz.' },
16
+ { name: 'Ölçek seçin', text: 'Kaydırıcıyı hareket ettirin veya hazır ayar seçin. Tam sayı çarpanları en net adaylardır.' },
17
+ { name: 'Önizlemeyi okuyun', text: 'Sprite footprintini, yatay ve dikey PPUyu, görünür dünyayı ve bleeding uyarısını kontrol edin.' },
18
+ { name: 'Motorda test edin', text: 'Seçimi nearest filtre, hizalı kamera, atlas boşluğu ve gerçek oyun çözünürlükleriyle doğrulayın.' },
19
+ ];
20
+ const softwareApplication: WithContext<SoftwareApplication> = { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: 'Oyunlar için birim başına piksel planlayıcı', applicationCategory: 'DeveloperApplication', operatingSystem: 'Any' };
21
+ const faqPage: WithContext<FAQPage> = { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) };
22
+ const howToSchema: WithContext<HowTo> = { '@context': 'https://schema.org', '@type': 'HowTo', name: 'Bir pixel art spriteının ölçeğini test etme', step: howTo.map((step) => ({ '@type': 'HowToStep', name: step.name, text: step.text })) };
23
+
24
+ export const content: ToolLocaleContent<GamePixelPerUnitPlannerUI> = {
25
+ slug: 'oyun-sprite-birim-basina-piksel-planlayici', title: 'Oyun spriteı için birim başına piksel planlayıcı', description: 'Bir sprite yükleyin veya Bob örneğini kullanın; tam sayı ölçeklerinde footprinti görün, PPUyu karşılaştırın ve pixel bleeding riskini bulun.',
26
+ ui: {
27
+ inputsTitle: 'Sprite yükleyin ve test edin', uploadTitle: 'Kaynak görseliniz', uploadHint: 'PNG, GIF, WebP veya JPEG seçin. Yerel boyutlar tüm önizlemeleri besler.', chooseSpriteLabel: 'Sprite seç', noSpriteLabel: 'Henüz sprite yüklenmedi', defaultSpriteLabel: 'Bob örnek spriteı', loadedSpriteLabel: 'Yüklendi', clearSpriteLabel: 'Spriteı kaldır', displayWidthLabel: 'Ekran genişliği px', displayHeightLabel: 'Ekran yüksekliği px', spriteWidthLabel: 'Sprite genişliği px', spriteHeightLabel: 'Sprite yüksekliği px', worldWidthLabel: 'Sprite genişliği birim', worldHeightLabel: 'Sprite yüksekliği birim', targetScaleLabel: 'Hedef ekran ölçeği', targetScaleHint: 'Doku pikseli başına ekran pikseli.', resolutionPresetsLabel: 'Referans çözünürlükleri', preset320: '320 x 180', preset384: '384 x 216', preset640: '640 x 360', scalePresetsLabel: 'Hızlı ölçekler', scale1: '1x', scale2: '2x', scale3: '3x', scale4: '4x', scale6: '6x', resetLabel: 'Değerleri sıfırla', fieldTitle: 'Farklı boyutlarda görün', fieldCaption: 'Yüklenen görsel, her tam sayı çarpanındaki gerçek footprinti değerlendirmek için nearest ölçeklemeyle çizilir.', previewPlaceholder: 'Görsel teste başlamak için sprite yükleyin', previewScaleLabel: 'Önizleme ölçeği', sourceImageAlt: 'Yüklenen sprite önizlemesi', viewportLabel: 'Ekran', spriteLabel: 'Çizilen sprite', crispTitle: 'Keskin ölçekler', crispCaption: 'Tam sayı çarpanları pikselleri eşit tutar. Gri adımlar belirtilen ekranı aşar.', fitLabel: 'Ekrana sığar:', yesLabel: 'evet', noLabel: 'hayır', recommendedLabel: 'en yakın uyum', summaryTitle: 'Plan özeti', ppuXLabel: 'Yatay PPU', ppuYLabel: 'Dikey PPU', viewportWorldLabel: 'Görünen dünya', fitScaleLabel: 'Sığan en büyük ölçek', bleedingRiskLabel: 'Bleeding riski', lowRisk: 'Düşük', mediumRisk: 'Orta', highRisk: 'Yüksek', riskLowMessage: 'Eksenler hizalı ve hedef ölçek tam sayı olarak ekrana sığıyor. Yine de filtreleme ile atlas boşluğunu kontrol edin.', riskMediumMessage: 'Hedef kusursuz sığmıyor. Eksen farkını ve vurgulanan ölçekleri inceleyin.', riskHighMessage: 'Bu ölçek düzensiz örnekleme yaratabilir. Tam sayı çarpanı seçin ve boyutları kontrol edin.', alignmentLabel: 'Örnekleme notu', tableTitle: 'Erişilebilir ölçek kaydı', tableScale: 'Ölçek', tableWidth: 'Çizilen genişlik', tableHeight: 'Çizilen yükseklik', tableFits: 'Ekrana sığar', modelNote: 'PPU, her eksende çizilen sprite piksellerinin birim cinsinden sprite boyutuna bölünmesiyle hesaplanır. Bleeding riski sezgiseldir; doku incelemesi veya motor garantisi değildir.', privacyDisclosure: 'Dosya bu tarayıcıda işlenir. Sprite, proje dosyası ve telemetri gönderilmez.', statusReady: 'Önizleme güncellendi', unitPixels: 'px', unitUnits: 'birim',
28
+ },
29
+ seo: [
30
+ { type: 'title', level: 2, text: 'Sprite boyutunu ölçek kararına dönüştürün' }, { type: 'paragraph', html: 'Bir spriteın bitmap boyutu ve oyun dünyasında kapladığı alan vardır. PPU bu iki ölçüyü bağlar. Önizleme, soyut bir sayının yerine görsel sonucu gösterir.' }, { type: 'paragraph', html: 'Gerçek bir görsel yükleyerek yerel boyutlarını kullanın. Araç her ekseni seçilen ölçekle çarpar ve belirtilen çözünürlükte görünen dünya alanını hesaplar.' }, { type: 'title', level: 2, text: 'Önizlemede neye bakmalı' }, { type: 'list', items: ['Footprinti referans ekranıyla karşılaştırın.', 'Eşit pikseller için önce tam sayı ölçeklerini deneyin.', 'İstenmeyen esnemeyi bulmak için iki PPU değerini kullanın.', 'Bleeding uyarısını motor testi için bir işaret sayın.'] }, { type: 'title', level: 2, text: 'Tam sayı ölçekleri neden daha temizdir' }, { type: 'paragraph', html: '3x ölçekte her kaynak pikseli üç ekran pikseli kaplar. 2,5x ölçekte çizici farklı genişlikler dağıtmak zorunda kalır. Nearest renkleri karıştırmaz ama piksel konumları arasındaki kamerayı düzeltemez.' }, { type: 'table', headers: ['Sinyal', 'Anlamı', 'Sonraki karar'], rows: [['Eşit PPU', 'İki eksen aynı yoğunluğa sahip.', 'Tile ve proje ızgarasıyla karşılaştırın.'], ['Kesirli ölçek', 'Footprint tam sayı kullanmıyor.', 'En yakın tam sayı ölçeğini deneyin.'], ['Sığmıyor', 'Sprite ekranı aşıyor.', 'Ölçeği küçültün veya çözünürlüğü artırın.']] }, { type: 'title', level: 2, text: 'Bleeding ile boyutu ayırın' }, { type: 'paragraph', html: 'Pixel bleeding çoğunlukla atlas komşu texellerinden, kenar filtresinden veya hizasız kamera koordinatlarından gelir. Bulanıklıkta filtreyi, dikişlerde ise boşluk ve sınırları da kontrol edin.' }, { type: 'title', level: 2, text: 'Bob spriteıyla akışı öğrenin' }, { type: 'paragraph', html: 'Pembe fiyonklu Bob örneği başlangıçta görünür. Çözünürlük ve ölçeği değiştirerek karakterin ne zaman sığmadığını veya piksellerin eşitliğini kaybettiğini görün.' }, { type: 'title', level: 2, text: 'Araç neyi doğrular ve neyi doğrulamaz' }, { type: 'paragraph', html: 'Araç boyutları ve footprintleri kontrollü bir görünümde karşılaştırır. Motor projesi açmaz, atlas incelemez, cihaz ölçmez ve pixel perfect hareketi garanti etmez.' }, { type: 'title', level: 2, text: 'Ölçek seçmek için kısa akış' }, { type: 'paragraph', html: 'Spriteı yükleyin, çözünürlüğü seçin, 1x ile 4x arasını deneyin ve boşluk bırakan en büyük tam sayı ölçeğini seçin. Gerçek oyun çözünürlüklerinde tekrarlayın.' }, { type: 'tip', title: 'Son kontrol motorda yapılır', html: 'Önizlemeyi seçimi daraltmak için kullanın. Ardından nearest filtreyi, atlas boşluğunu ve kamera hizasını kontrol edin; birkaç çözünürlükte test yapın.' },
31
+ ],
32
+ faqTitle: 'Sprite ölçekleme soruları', faq, bibliographyTitle: 'Pixel art kaynakları', bibliography: bibliographyEntries, howTo, schemas: [softwareApplication, faqPage, howToSchema],
33
+ };
@@ -0,0 +1,33 @@
1
+ import type { FAQPage, HowTo, SoftwareApplication, WithContext } from 'schema-dts';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+ import type { GamePixelPerUnitPlannerUI } from '../ui';
4
+ import { bibliographyEntries } from '../bibliography';
5
+
6
+ const faq = [
7
+ { question: '游戏中的每单位像素是什么意思?', answer: '每单位像素,也就是 PPU,表示一个世界单位由多少纹理像素表示。统一的密度可以让精灵、图块和摄像机保持可预测的比例。' },
8
+ { question: '为什么整数缩放很重要?', answer: '整数缩放会让每个源像素对应相同数量的屏幕像素。小数缩放可能造成边缘不均匀或画面模糊。' },
9
+ { question: '什么是像素溢色?', answer: '这是相邻纹素或图集其他区域的颜色意外出现。滤镜、边缘、子像素移动和不足的留白都可能造成它。' },
10
+ { question: '如何使用推荐缩放?', answer: '把它作为接近目标并且能放入分辨率的候选值。然后在引擎中检查 nearest 滤镜、摄像机位置和图集留白。' },
11
+ { question: '这个规划器能为所有引擎选择正确的 PPU 吗?', answer: '不能。这是透明的计算辅助工具。摄像机、导入、 mipmap、取整和像素吸附都会因引擎不同而变化,需要实际测试。' },
12
+ ];
13
+ const howTo = [
14
+ { name: '选择目标画面', text: '输入游戏视图或参考分辨率的宽度和高度,单位为像素。' },
15
+ { name: '加载精灵', text: '选择图片以自动读取原始尺寸,也可以直接使用内置的 Bob 示例。' },
16
+ { name: '选择缩放', text: '移动滑块或选择预设。整数倍率通常是最清晰的候选。' },
17
+ { name: '阅读预览', text: '查看精灵占用范围、水平和垂直 PPU、可见世界以及溢色提示。' },
18
+ { name: '在引擎中测试', text: '使用 nearest 滤镜、对齐的摄像机、图集留白和真实分辨率验证选择。' },
19
+ ];
20
+ const softwareApplication: WithContext<SoftwareApplication> = { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: '游戏每单位像素规划器', applicationCategory: 'DeveloperApplication', operatingSystem: 'Any' };
21
+ const faqPage: WithContext<FAQPage> = { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) };
22
+ const howToSchema: WithContext<HowTo> = { '@context': 'https://schema.org', '@type': 'HowTo', name: '测试像素艺术精灵的缩放', step: howTo.map((step) => ({ '@type': 'HowToStep', name: step.name, text: step.text })) };
23
+
24
+ export const content: ToolLocaleContent<GamePixelPerUnitPlannerUI> = {
25
+ slug: 'game-pixel-per-unit-planner', title: '游戏精灵每单位像素规划器', description: '上传精灵或使用 Bob 示例,查看不同整数倍率下的占用范围,比较 PPU 并发现像素溢色风险。',
26
+ ui: {
27
+ inputsTitle: '加载并测试精灵', uploadTitle: '源图像', uploadHint: '选择 PNG、GIF、WebP 或 JPEG。原始尺寸会用于每个预览。', chooseSpriteLabel: '选择精灵', noSpriteLabel: '尚未加载精灵', defaultSpriteLabel: 'Bob 示例精灵', loadedSpriteLabel: '已加载', clearSpriteLabel: '移除精灵', displayWidthLabel: '屏幕宽度 px', displayHeightLabel: '屏幕高度 px', spriteWidthLabel: '精灵宽度 px', spriteHeightLabel: '精灵高度 px', worldWidthLabel: '精灵世界宽度', worldHeightLabel: '精灵世界高度', targetScaleLabel: '屏幕目标倍率', targetScaleHint: '纹理每个像素对应的屏幕像素数。', resolutionPresetsLabel: '参考分辨率', preset320: '320 x 180', preset384: '384 x 216', preset640: '640 x 360', scalePresetsLabel: '快速倍率', scale1: '1x', scale2: '2x', scale3: '3x', scale4: '4x', scale6: '6x', resetLabel: '重置数值', fieldTitle: '查看不同尺寸', fieldCaption: '加载的图像使用 nearest 缩放渲染,方便判断每个整数倍率下的实际占用范围。', previewPlaceholder: '加载精灵以开始视觉测试', previewScaleLabel: '预览倍率', sourceImageAlt: '已加载精灵预览', viewportLabel: '屏幕', spriteLabel: '渲染精灵', crispTitle: '清晰倍率', crispCaption: '整数倍率保持像素大小一致。灰色步骤会超出声明的屏幕。', fitLabel: '适合屏幕:', yesLabel: '是', noLabel: '否', recommendedLabel: '最接近', summaryTitle: '规划摘要', ppuXLabel: '水平 PPU', ppuYLabel: '垂直 PPU', viewportWorldLabel: '可见世界', fitScaleLabel: '可容纳的最大倍率', bleedingRiskLabel: '溢色风险', lowRisk: '低', mediumRisk: '中', highRisk: '高', riskLowMessage: '两个轴已对齐,整数目标倍率可以放入屏幕。仍然需要检查滤镜和图集留白。', riskMediumMessage: '目标并未完美适配。请检查轴向差异和突出显示的倍率。', riskHighMessage: '该倍率可能产生不均匀采样。请优先选择整数倍率并检查精灵尺寸。', alignmentLabel: '采样说明', tableTitle: '可访问的倍率记录', tableScale: '倍率', tableWidth: '渲染宽度', tableHeight: '渲染高度', tableFits: '适合屏幕', modelNote: 'PPU 等于每个轴上的渲染精灵像素数除以精灵的世界单位尺寸。溢色风险是启发式提示,不是纹理检查或引擎保证。', privacyDisclosure: '文件在此浏览器中处理。不会上传精灵、项目文件或遥测数据。', statusReady: '预览已更新', unitPixels: 'px', unitUnits: '单位',
28
+ },
29
+ seo: [
30
+ { type: 'title', level: 2, text: '把精灵尺寸转化为缩放决策' }, { type: 'paragraph', html: '精灵有位图尺寸,也有它在游戏世界中占据的尺寸。PPU把这两个测量连接起来。预览可以直接展示视觉结果,而不是只给出一个抽象数字。' }, { type: 'paragraph', html: '加载真实图像后,工具会使用它的原始尺寸。选择倍率后,工具计算两个轴的占用范围以及指定分辨率中可见的世界。' }, { type: 'title', level: 2, text: '在预览中观察什么' }, { type: 'list', items: ['将占用范围与参考屏幕比较。', '先测试整数倍率以保持像素均匀。', '用两个 PPU 值寻找意外拉伸。', '把溢色提示当作引擎测试信号。'] }, { type: 'title', level: 2, text: '整数倍率为什么更干净' }, { type: 'paragraph', html: '在 3 倍时,一个源像素占据三个屏幕像素。在 2.5 倍时,渲染器必须分配不同宽度。Nearest 可以避免颜色混合,但不能修复位于像素之间的摄像机。' }, { type: 'table', headers: ['信号', '含义', '下一步'], rows: [['PPU 相同', '两个轴具有相同密度。', '与图块和项目网格比较。'], ['小数倍率', '占用范围没有使用整数。', '测试最近的整数倍率。'], ['无法容纳', '精灵超过屏幕。', '降低倍率或提高分辨率。']] }, { type: 'title', level: 2, text: '区分溢色和尺寸问题' }, { type: 'paragraph', html: '像素溢色通常来自图集中的相邻纹素、边缘滤镜或没有对齐的摄像机坐标。画面模糊时检查滤镜,出现接缝时还要检查留白和边界。' }, { type: 'title', level: 2, text: '用 Bob 示例了解流程' }, { type: 'paragraph', html: '带粉色蝴蝶结的 Bob 示例会在开始时显示。改变分辨率和倍率,就能看到角色何时无法容纳,以及像素何时失去均匀性。' }, { type: 'title', level: 2, text: '工具能检查什么' }, { type: 'paragraph', html: '工具会在受控视图中比较尺寸和占用范围。它不会打开引擎项目、检查图集、测量设备,也不会保证 pixel perfect 移动。' }, { type: 'title', level: 2, text: '选择倍率的简短流程' }, { type: 'paragraph', html: '加载精灵,选择分辨率,测试 1 倍到 4 倍,并选择仍有空间的最大整数倍率。然后在游戏实际支持的分辨率下重复检查。' }, { type: 'tip', title: '最后一步要在引擎中确认', html: '使用预览缩小候选范围,然后启用 nearest 滤镜,检查图集留白和摄像机对齐,并测试多个分辨率。' },
31
+ ],
32
+ faqTitle: '精灵缩放问题', faq, bibliographyTitle: '像素艺术参考资料', bibliography: bibliographyEntries, howTo, schemas: [softwareApplication, faqPage, howToSchema],
33
+ };
@@ -0,0 +1,11 @@
1
+ import type { ToolDefinition } from '../../types';
2
+ import { gamePixelPerUnitPlanner } from './entry';
3
+
4
+ export * from './entry';
5
+
6
+ export const GAME_PIXEL_PER_UNIT_PLANNER_TOOL: ToolDefinition = {
7
+ entry: gamePixelPerUnitPlanner,
8
+ Component: () => import('./component.astro'),
9
+ SEOComponent: () => import('./seo.astro'),
10
+ BibliographyComponent: () => import('./bibliography.astro'),
11
+ };
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { calculatePixelPlan, normalizePixelConfig } from './logic';
3
+
4
+ describe('game pixel per unit planner logic', () => {
5
+ it('calculates the reference Bob sprite plan', () => {
6
+ const result = calculatePixelPlan({ spriteWidth: 64, spriteHeight: 64, targetScale: 2, worldWidth: 1, worldHeight: 1, displayWidth: 320, displayHeight: 180 });
7
+ expect(result.ppuX).toBe(128);
8
+ expect(result.ppuY).toBe(128);
9
+ expect(result.viewportWorldWidth).toBe(2.5);
10
+ expect(result.viewportWorldHeight).toBe(1.40625);
11
+ });
12
+
13
+ it('finds integer scales that fit and recommends the closest one', () => {
14
+ const result = calculatePixelPlan({ spriteWidth: 16, spriteHeight: 16, targetScale: 4, displayWidth: 320, displayHeight: 180 });
15
+ expect(result.steps.some((step) => step.scale === 4 && step.fitsViewport)).toBe(true);
16
+ expect(result.recommendedScale).toBe(4);
17
+ expect(result.fitScale).toBe(11.25);
18
+ });
19
+
20
+ it('marks fractional and mismatched plans as risky', () => {
21
+ const result = calculatePixelPlan({ spriteWidth: 17, spriteHeight: 16, targetScale: 2.5, worldWidth: 1, worldHeight: 1 });
22
+ expect(result.scaleIsInteger).toBe(false);
23
+ expect(result.axisMismatch).toBeGreaterThan(0);
24
+ expect(result.bleedRisk).toBe('high');
25
+ });
26
+
27
+ it('normalizes invalid values at documented boundaries', () => {
28
+ const config = normalizePixelConfig({ displayWidth: Number.NaN, spriteWidth: -4, worldHeight: 0, targetScale: 99 });
29
+ expect(config.displayWidth).toBe(320);
30
+ expect(config.spriteWidth).toBe(1);
31
+ expect(config.worldHeight).toBe(0.01);
32
+ expect(config.targetScale).toBe(16);
33
+ });
34
+ });
@@ -0,0 +1,129 @@
1
+ export interface PixelPerUnitConfig {
2
+ displayWidth: number;
3
+ displayHeight: number;
4
+ spriteWidth: number;
5
+ spriteHeight: number;
6
+ worldWidth: number;
7
+ worldHeight: number;
8
+ targetScale: number;
9
+ }
10
+
11
+ export interface CrispScaleStep {
12
+ scale: number;
13
+ renderedWidth: number;
14
+ renderedHeight: number;
15
+ fitsViewport: boolean;
16
+ deltaFromTarget: number;
17
+ }
18
+
19
+ export type BleedRisk = 'low' | 'medium' | 'high';
20
+
21
+ export interface PixelPerUnitResult {
22
+ config: PixelPerUnitConfig;
23
+ ppuX: number;
24
+ ppuY: number;
25
+ viewportWorldWidth: number;
26
+ viewportWorldHeight: number;
27
+ renderedSpriteWidth: number;
28
+ renderedSpriteHeight: number;
29
+ fitScale: number;
30
+ recommendedScale: number;
31
+ scaleIsInteger: boolean;
32
+ axisMismatch: number;
33
+ bleedRisk: BleedRisk;
34
+ steps: CrispScaleStep[];
35
+ }
36
+
37
+ export const DEFAULT_PIXEL_CONFIG: PixelPerUnitConfig = {
38
+ displayWidth: 320,
39
+ displayHeight: 180,
40
+ spriteWidth: 64,
41
+ spriteHeight: 64,
42
+ worldWidth: 1,
43
+ worldHeight: 1,
44
+ targetScale: 4,
45
+ };
46
+
47
+ function clamp(value: number, minimum: number, maximum: number): number {
48
+ return Math.min(maximum, Math.max(minimum, value));
49
+ }
50
+
51
+ function finiteOr(value: number | undefined, fallback: number): number {
52
+ return Number.isFinite(value) ? value as number : fallback;
53
+ }
54
+
55
+ function normalizeInteger(value: number | undefined, fallback: number, minimum: number, maximum: number): number {
56
+ return Math.round(clamp(finiteOr(value, fallback), minimum, maximum));
57
+ }
58
+
59
+ function normalizePositive(value: number | undefined, fallback: number, minimum: number, maximum: number): number {
60
+ return clamp(finiteOr(value, fallback), minimum, maximum);
61
+ }
62
+
63
+ function normalizeScale(value: number | undefined): number {
64
+ const raw = normalizePositive(value, DEFAULT_PIXEL_CONFIG.targetScale, 0.5, 16);
65
+ return Math.round(raw * 4) / 4;
66
+ }
67
+
68
+ export function normalizePixelConfig(input: Partial<PixelPerUnitConfig> = {}): PixelPerUnitConfig {
69
+ return {
70
+ displayWidth: normalizeInteger(input.displayWidth, DEFAULT_PIXEL_CONFIG.displayWidth, 160, 7680),
71
+ displayHeight: normalizeInteger(input.displayHeight, DEFAULT_PIXEL_CONFIG.displayHeight, 90, 4320),
72
+ spriteWidth: normalizeInteger(input.spriteWidth, DEFAULT_PIXEL_CONFIG.spriteWidth, 1, 4096),
73
+ spriteHeight: normalizeInteger(input.spriteHeight, DEFAULT_PIXEL_CONFIG.spriteHeight, 1, 4096),
74
+ worldWidth: normalizePositive(input.worldWidth, DEFAULT_PIXEL_CONFIG.worldWidth, 0.01, 1000),
75
+ worldHeight: normalizePositive(input.worldHeight, DEFAULT_PIXEL_CONFIG.worldHeight, 0.01, 1000),
76
+ targetScale: normalizeScale(input.targetScale),
77
+ };
78
+ }
79
+
80
+ function makeScaleSteps(config: PixelPerUnitConfig): CrispScaleStep[] {
81
+ const start = Math.max(1, Math.floor(config.targetScale) - 2);
82
+ const end = Math.min(16, Math.ceil(config.targetScale) + 2);
83
+ return Array.from({ length: end - start + 1 }, (_, index) => {
84
+ const scale = start + index;
85
+ return {
86
+ scale,
87
+ renderedWidth: config.spriteWidth * scale,
88
+ renderedHeight: config.spriteHeight * scale,
89
+ fitsViewport: config.spriteWidth * scale <= config.displayWidth && config.spriteHeight * scale <= config.displayHeight,
90
+ deltaFromTarget: Math.abs(scale - config.targetScale),
91
+ };
92
+ });
93
+ }
94
+
95
+ function findRecommendedScale(config: PixelPerUnitConfig, steps: CrispScaleStep[]): number {
96
+ const fitting = steps.filter((step) => step.fitsViewport);
97
+ const candidates = fitting.length > 0 ? fitting : steps;
98
+ return candidates.reduce((best, step) => step.deltaFromTarget < best.deltaFromTarget ? step : best).scale;
99
+ }
100
+
101
+ function calculateRisk(config: PixelPerUnitConfig, axisMismatch: number, fitScale: number): BleedRisk {
102
+ if (!Number.isInteger(config.targetScale) || axisMismatch > 0.08) return 'high';
103
+ if (axisMismatch > 0.01 || config.targetScale > fitScale) return 'medium';
104
+ return 'low';
105
+ }
106
+
107
+ export function calculatePixelPlan(input: Partial<PixelPerUnitConfig> = {}): PixelPerUnitResult {
108
+ const config = normalizePixelConfig(input);
109
+ const ppuX = config.spriteWidth * config.targetScale / config.worldWidth;
110
+ const ppuY = config.spriteHeight * config.targetScale / config.worldHeight;
111
+ const axisMismatch = Math.abs(ppuX - ppuY) / Math.max(ppuX, ppuY);
112
+ const fitScale = Math.min(config.displayWidth / config.spriteWidth, config.displayHeight / config.spriteHeight);
113
+ const steps = makeScaleSteps(config);
114
+ return {
115
+ config,
116
+ ppuX,
117
+ ppuY,
118
+ viewportWorldWidth: config.displayWidth / ppuX,
119
+ viewportWorldHeight: config.displayHeight / ppuY,
120
+ renderedSpriteWidth: config.spriteWidth * config.targetScale,
121
+ renderedSpriteHeight: config.spriteHeight * config.targetScale,
122
+ fitScale,
123
+ recommendedScale: findRecommendedScale(config, steps),
124
+ scaleIsInteger: Number.isInteger(config.targetScale),
125
+ axisMismatch,
126
+ bleedRisk: calculateRisk(config, axisMismatch, fitScale),
127
+ steps,
128
+ };
129
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { gamePixelPerUnitPlanner } from './entry';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props;
11
+ const loader = gamePixelPerUnitPlanner.i18n[locale] ?? gamePixelPerUnitPlanner.i18n.en;
12
+ const content = loader ? await loader() : null;
13
+ ---
14
+
15
+ {content && content.seo.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,23 @@
1
+ import type { PixelPerUnitConfig } from './logic';
2
+
3
+ const STORAGE_KEY = 'jjlmoya-game-pixel-per-unit-planner';
4
+
5
+ function isRecord(value: unknown): value is Record<string, unknown> {
6
+ return typeof value === 'object' && value !== null;
7
+ }
8
+
9
+ export function loadPixelConfig(): Partial<PixelPerUnitConfig> {
10
+ try {
11
+ const raw = window.localStorage.getItem(STORAGE_KEY);
12
+ const parsed: unknown = raw ? JSON.parse(raw) : {};
13
+ return isRecord(parsed) ? parsed as Partial<PixelPerUnitConfig> : {};
14
+ } catch {
15
+ return {};
16
+ }
17
+ }
18
+
19
+ export function savePixelConfig(config: PixelPerUnitConfig): void {
20
+ try {
21
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
22
+ } catch {}
23
+ }
@@ -0,0 +1,65 @@
1
+ export interface GamePixelPerUnitPlannerUI extends Record<string, string> {
2
+ inputsTitle: string;
3
+ uploadTitle: string;
4
+ uploadHint: string;
5
+ chooseSpriteLabel: string;
6
+ noSpriteLabel: string;
7
+ defaultSpriteLabel: string;
8
+ loadedSpriteLabel: string;
9
+ clearSpriteLabel: string;
10
+ displayWidthLabel: string;
11
+ displayHeightLabel: string;
12
+ spriteWidthLabel: string;
13
+ spriteHeightLabel: string;
14
+ worldWidthLabel: string;
15
+ worldHeightLabel: string;
16
+ targetScaleLabel: string;
17
+ targetScaleHint: string;
18
+ resolutionPresetsLabel: string;
19
+ preset320: string;
20
+ preset384: string;
21
+ preset640: string;
22
+ scalePresetsLabel: string;
23
+ scale1: string;
24
+ scale2: string;
25
+ scale3: string;
26
+ scale4: string;
27
+ scale6: string;
28
+ resetLabel: string;
29
+ fieldTitle: string;
30
+ fieldCaption: string;
31
+ previewPlaceholder: string;
32
+ previewScaleLabel: string;
33
+ sourceImageAlt: string;
34
+ viewportLabel: string;
35
+ spriteLabel: string;
36
+ crispTitle: string;
37
+ crispCaption: string;
38
+ fitLabel: string;
39
+ yesLabel: string;
40
+ noLabel: string;
41
+ recommendedLabel: string;
42
+ summaryTitle: string;
43
+ ppuXLabel: string;
44
+ ppuYLabel: string;
45
+ viewportWorldLabel: string;
46
+ fitScaleLabel: string;
47
+ bleedingRiskLabel: string;
48
+ lowRisk: string;
49
+ mediumRisk: string;
50
+ highRisk: string;
51
+ riskLowMessage: string;
52
+ riskMediumMessage: string;
53
+ riskHighMessage: string;
54
+ alignmentLabel: string;
55
+ tableTitle: string;
56
+ tableScale: string;
57
+ tableWidth: string;
58
+ tableHeight: string;
59
+ tableFits: string;
60
+ modelNote: string;
61
+ privacyDisclosure: string;
62
+ statusReady: string;
63
+ unitPixels: string;
64
+ unitUnits: string;
65
+ }
package/src/tools.ts CHANGED
@@ -13,6 +13,7 @@ import { GAME_UI_ACCESSIBILITY_TESTER_TOOL } from './tool/gameUIAccessibilityTes
13
13
  import { HITBOX_HURTBOX_ANIMATOR_TOOL } from './tool/hitboxHurtboxAnimator';
14
14
  import { DAMAGE_FORMULA_LAB_TOOL } from './tool/damageFormulaLab';
15
15
  import { GAME_DELTA_TIME_FIXED_TIMESTEP_LAB_TOOL } from './tool/gameDeltaTimeFixedTimestepLab';
16
+ import { GAME_PIXEL_PER_UNIT_PLANNER_TOOL } from './tool/gamePixelPerUnitPlanner';
16
17
 
17
18
  export const ALL_TOOLS: ToolDefinition[] = [
18
19
  STEAM_CAPSULE_GENERATOR_TOOL,
@@ -28,4 +29,5 @@ export const ALL_TOOLS: ToolDefinition[] = [
28
29
  HITBOX_HURTBOX_ANIMATOR_TOOL,
29
30
  DAMAGE_FORMULA_LAB_TOOL,
30
31
  GAME_DELTA_TIME_FIXED_TIMESTEP_LAB_TOOL,
32
+ GAME_PIXEL_PER_UNIT_PLANNER_TOOL,
31
33
  ];