@jjlmoya/utils-games-development 1.61.0 → 1.63.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.
- package/package.json +1 -1
- package/src/category/index.ts +3 -1
- package/src/entries.ts +5 -1
- package/src/index.ts +2 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/gameUIAccessibilityTester/bibliography.astro +6 -0
- package/src/tool/gameUIAccessibilityTester/bibliography.ts +10 -0
- package/src/tool/gameUIAccessibilityTester/component.astro +210 -0
- package/src/tool/gameUIAccessibilityTester/controller-actions.ts +92 -0
- package/src/tool/gameUIAccessibilityTester/controller.ts +237 -0
- package/src/tool/gameUIAccessibilityTester/dom-views.ts +193 -0
- package/src/tool/gameUIAccessibilityTester/entry.ts +28 -0
- package/src/tool/gameUIAccessibilityTester/evaluator.ts +43 -0
- package/src/tool/gameUIAccessibilityTester/game-ui-accessibility-stress-tester.css +982 -0
- package/src/tool/gameUIAccessibilityTester/i18n/de.ts +203 -0
- package/src/tool/gameUIAccessibilityTester/i18n/en.ts +199 -0
- package/src/tool/gameUIAccessibilityTester/i18n/es.ts +203 -0
- package/src/tool/gameUIAccessibilityTester/i18n/fr.ts +203 -0
- package/src/tool/gameUIAccessibilityTester/i18n/id.ts +203 -0
- package/src/tool/gameUIAccessibilityTester/i18n/it.ts +203 -0
- package/src/tool/gameUIAccessibilityTester/i18n/ja.ts +203 -0
- package/src/tool/gameUIAccessibilityTester/i18n/ko.ts +203 -0
- package/src/tool/gameUIAccessibilityTester/i18n/nl.ts +203 -0
- package/src/tool/gameUIAccessibilityTester/i18n/pl.ts +203 -0
- package/src/tool/gameUIAccessibilityTester/i18n/pt.ts +203 -0
- package/src/tool/gameUIAccessibilityTester/i18n/ru.ts +203 -0
- package/src/tool/gameUIAccessibilityTester/i18n/sv.ts +203 -0
- package/src/tool/gameUIAccessibilityTester/i18n/tr.ts +203 -0
- package/src/tool/gameUIAccessibilityTester/i18n/zh.ts +203 -0
- package/src/tool/gameUIAccessibilityTester/index.ts +11 -0
- package/src/tool/gameUIAccessibilityTester/logic.test.ts +57 -0
- package/src/tool/gameUIAccessibilityTester/logic.ts +157 -0
- package/src/tool/gameUIAccessibilityTester/report.ts +59 -0
- package/src/tool/gameUIAccessibilityTester/seo.astro +15 -0
- package/src/tool/gameUIAccessibilityTester/storage.ts +34 -0
- package/src/tool/gameUIAccessibilityTester/ui.ts +80 -0
- package/src/tool/hitboxHurtboxAnimator/bibliography.astro +12 -0
- package/src/tool/hitboxHurtboxAnimator/bibliography.ts +10 -0
- package/src/tool/hitboxHurtboxAnimator/canvas-interactions.ts +59 -0
- package/src/tool/hitboxHurtboxAnimator/canvas-metrics.ts +8 -0
- package/src/tool/hitboxHurtboxAnimator/component.astro +148 -0
- package/src/tool/hitboxHurtboxAnimator/controller.ts +247 -0
- package/src/tool/hitboxHurtboxAnimator/dom-views.ts +194 -0
- package/src/tool/hitboxHurtboxAnimator/editor-state.ts +19 -0
- package/src/tool/hitboxHurtboxAnimator/entry.ts +28 -0
- package/src/tool/hitboxHurtboxAnimator/evaluator.ts +15 -0
- package/src/tool/hitboxHurtboxAnimator/file-io.ts +82 -0
- package/src/tool/hitboxHurtboxAnimator/hitbox-hurtbox-animator.css +614 -0
- package/src/tool/hitboxHurtboxAnimator/i18n/de.ts +195 -0
- package/src/tool/hitboxHurtboxAnimator/i18n/en.ts +191 -0
- package/src/tool/hitboxHurtboxAnimator/i18n/es.ts +195 -0
- package/src/tool/hitboxHurtboxAnimator/i18n/fr.ts +195 -0
- package/src/tool/hitboxHurtboxAnimator/i18n/id.ts +195 -0
- package/src/tool/hitboxHurtboxAnimator/i18n/it.ts +195 -0
- package/src/tool/hitboxHurtboxAnimator/i18n/ja.ts +195 -0
- package/src/tool/hitboxHurtboxAnimator/i18n/ko.ts +195 -0
- package/src/tool/hitboxHurtboxAnimator/i18n/nl.ts +195 -0
- package/src/tool/hitboxHurtboxAnimator/i18n/pl.ts +195 -0
- package/src/tool/hitboxHurtboxAnimator/i18n/pt.ts +195 -0
- package/src/tool/hitboxHurtboxAnimator/i18n/ru.ts +195 -0
- package/src/tool/hitboxHurtboxAnimator/i18n/sv.ts +195 -0
- package/src/tool/hitboxHurtboxAnimator/i18n/tr.ts +195 -0
- package/src/tool/hitboxHurtboxAnimator/i18n/zh.ts +195 -0
- package/src/tool/hitboxHurtboxAnimator/index.ts +11 -0
- package/src/tool/hitboxHurtboxAnimator/logic.test.ts +97 -0
- package/src/tool/hitboxHurtboxAnimator/logic.ts +173 -0
- package/src/tool/hitboxHurtboxAnimator/project-actions.ts +128 -0
- package/src/tool/hitboxHurtboxAnimator/seo.astro +13 -0
- package/src/tool/hitboxHurtboxAnimator/storage.ts +27 -0
- package/src/tool/hitboxHurtboxAnimator/ui.ts +70 -0
- package/src/tools.ts +4 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { simulateColor, type RGBColor, type SimulationMode } from './logic';
|
|
2
|
+
|
|
3
|
+
export interface RenderOptions {
|
|
4
|
+
source: HTMLCanvasElement;
|
|
5
|
+
destination: HTMLCanvasElement;
|
|
6
|
+
mode: SimulationMode;
|
|
7
|
+
blur: number;
|
|
8
|
+
downscale: number;
|
|
9
|
+
heatmap: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SheetLabels {
|
|
13
|
+
title: string;
|
|
14
|
+
original: string;
|
|
15
|
+
simulated: string;
|
|
16
|
+
settings: string;
|
|
17
|
+
localOnly: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const MAX_IMAGE_EDGE = 1600;
|
|
21
|
+
|
|
22
|
+
function context(canvas: HTMLCanvasElement): CanvasRenderingContext2D {
|
|
23
|
+
const value = canvas.getContext('2d', { willReadFrequently: true });
|
|
24
|
+
if (!value) throw new Error('Canvas 2D context unavailable');
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createCanvas(width: number, height: number): HTMLCanvasElement {
|
|
29
|
+
const canvas = document.createElement('canvas');
|
|
30
|
+
canvas.width = width;
|
|
31
|
+
canvas.height = height;
|
|
32
|
+
return canvas;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function fittedSize(width: number, height: number): { width: number; height: number } {
|
|
36
|
+
const scale = Math.min(1, MAX_IMAGE_EDGE / Math.max(width, height));
|
|
37
|
+
return {
|
|
38
|
+
width: Math.max(1, Math.round(width * scale)),
|
|
39
|
+
height: Math.max(1, Math.round(height * scale)),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function loadImageFile(file: File): Promise<HTMLCanvasElement> {
|
|
44
|
+
const bitmap = await createImageBitmap(file);
|
|
45
|
+
const size = fittedSize(bitmap.width, bitmap.height);
|
|
46
|
+
const canvas = createCanvas(size.width, size.height);
|
|
47
|
+
const canvasContext = context(canvas);
|
|
48
|
+
canvasContext.imageSmoothingEnabled = true;
|
|
49
|
+
canvasContext.imageSmoothingQuality = 'high';
|
|
50
|
+
canvasContext.drawImage(bitmap, 0, 0, size.width, size.height);
|
|
51
|
+
bitmap.close();
|
|
52
|
+
return canvas;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function stressedSource(source: HTMLCanvasElement, scale: number, blur: number): HTMLCanvasElement {
|
|
56
|
+
const smallWidth = Math.max(1, Math.round(source.width * scale));
|
|
57
|
+
const smallHeight = Math.max(1, Math.round(source.height * scale));
|
|
58
|
+
const small = createCanvas(smallWidth, smallHeight);
|
|
59
|
+
const smallContext = context(small);
|
|
60
|
+
smallContext.imageSmoothingEnabled = true;
|
|
61
|
+
smallContext.drawImage(source, 0, 0, smallWidth, smallHeight);
|
|
62
|
+
const output = createCanvas(source.width, source.height);
|
|
63
|
+
const outputContext = context(output);
|
|
64
|
+
outputContext.imageSmoothingEnabled = scale === 1;
|
|
65
|
+
outputContext.filter = `blur(${blur}px)`;
|
|
66
|
+
outputContext.drawImage(small, 0, 0, source.width, source.height);
|
|
67
|
+
return output;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function transformPixels(image: ImageData, mode: SimulationMode): ImageData {
|
|
71
|
+
const output = new ImageData(new Uint8ClampedArray(image.data), image.width, image.height);
|
|
72
|
+
for (let index = 0; index < output.data.length; index += 4) {
|
|
73
|
+
const color = simulateColor({
|
|
74
|
+
r: output.data[index] ?? 0,
|
|
75
|
+
g: output.data[index + 1] ?? 0,
|
|
76
|
+
b: output.data[index + 2] ?? 0,
|
|
77
|
+
}, mode);
|
|
78
|
+
output.data[index] = color.r;
|
|
79
|
+
output.data[index + 1] = color.g;
|
|
80
|
+
output.data[index + 2] = color.b;
|
|
81
|
+
}
|
|
82
|
+
return output;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function pixelDistance(data: Uint8ClampedArray, first: number, second: number): number {
|
|
86
|
+
const red = (data[first] ?? 0) - (data[second] ?? 0);
|
|
87
|
+
const green = (data[first + 1] ?? 0) - (data[second + 1] ?? 0);
|
|
88
|
+
const blue = (data[first + 2] ?? 0) - (data[second + 2] ?? 0);
|
|
89
|
+
return Math.sqrt(red ** 2 + green ** 2 + blue ** 2);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function collapsedSignal(original: ImageData, simulated: ImageData, index: number, neighbor: number): boolean {
|
|
93
|
+
const before = pixelDistance(original.data, index, neighbor);
|
|
94
|
+
const after = pixelDistance(simulated.data, index, neighbor);
|
|
95
|
+
return before >= 32 && after / before < 0.48;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function heatmapPixels(original: ImageData, simulated: ImageData): ImageData {
|
|
99
|
+
const output = new ImageData(new Uint8ClampedArray(simulated.data), simulated.width, simulated.height);
|
|
100
|
+
const row = simulated.width * 4;
|
|
101
|
+
for (let index = 0; index < output.data.length - row - 4; index += 4) {
|
|
102
|
+
const collapsed = collapsedSignal(original, simulated, index, index + 4)
|
|
103
|
+
|| collapsedSignal(original, simulated, index, index + row);
|
|
104
|
+
if (collapsed) {
|
|
105
|
+
output.data[index] = 255;
|
|
106
|
+
output.data[index + 1] = Math.round((output.data[index + 1] ?? 0) * 0.18);
|
|
107
|
+
output.data[index + 2] = 72;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return output;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function renderSimulation(options: RenderOptions): void {
|
|
114
|
+
const stressed = stressedSource(options.source, options.downscale, options.blur);
|
|
115
|
+
const sourceContext = context(stressed);
|
|
116
|
+
const original = sourceContext.getImageData(0, 0, stressed.width, stressed.height);
|
|
117
|
+
const simulated = transformPixels(original, options.mode);
|
|
118
|
+
const rendered = options.heatmap ? heatmapPixels(original, simulated) : simulated;
|
|
119
|
+
options.destination.width = stressed.width;
|
|
120
|
+
options.destination.height = stressed.height;
|
|
121
|
+
context(options.destination).putImageData(rendered, 0, 0);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function copyOriginal(source: HTMLCanvasElement, destination: HTMLCanvasElement): void {
|
|
125
|
+
destination.width = source.width;
|
|
126
|
+
destination.height = source.height;
|
|
127
|
+
context(destination).drawImage(source, 0, 0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function sampleCanvas(canvas: HTMLCanvasElement, clientX: number, clientY: number): {
|
|
131
|
+
color: RGBColor;
|
|
132
|
+
left: number;
|
|
133
|
+
top: number;
|
|
134
|
+
} {
|
|
135
|
+
const bounds = canvas.getBoundingClientRect();
|
|
136
|
+
const left = Math.min(1, Math.max(0, (clientX - bounds.left) / bounds.width));
|
|
137
|
+
const top = Math.min(1, Math.max(0, (clientY - bounds.top) / bounds.height));
|
|
138
|
+
return { color: sampleAtFraction(canvas, left, top), left, top };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function sampleAtFraction(canvas: HTMLCanvasElement, left: number, top: number): RGBColor {
|
|
142
|
+
const x = Math.min(canvas.width - 1, Math.floor(left * canvas.width));
|
|
143
|
+
const y = Math.min(canvas.height - 1, Math.floor(top * canvas.height));
|
|
144
|
+
const data = context(canvas).getImageData(x, y, 1, 1).data;
|
|
145
|
+
return { r: data[0] ?? 0, g: data[1] ?? 0, b: data[2] ?? 0 };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function sheetDimensions(source: HTMLCanvasElement): { width: number; height: number; imageHeight: number } {
|
|
149
|
+
const width = Math.max(960, source.width * 2 + 72);
|
|
150
|
+
const imageHeight = Math.round((source.height / source.width) * ((width - 72) / 2));
|
|
151
|
+
return { width, height: imageHeight + 190, imageHeight };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function createComparisonSheet(
|
|
155
|
+
original: HTMLCanvasElement,
|
|
156
|
+
simulated: HTMLCanvasElement,
|
|
157
|
+
labels: SheetLabels,
|
|
158
|
+
): HTMLCanvasElement {
|
|
159
|
+
const size = sheetDimensions(original);
|
|
160
|
+
const sheet = createCanvas(size.width, size.height);
|
|
161
|
+
const sheetContext = context(sheet);
|
|
162
|
+
const imageWidth = (size.width - 72) / 2;
|
|
163
|
+
sheetContext.fillStyle = '#071018';
|
|
164
|
+
sheetContext.fillRect(0, 0, size.width, size.height);
|
|
165
|
+
sheetContext.fillStyle = '#e8fbff';
|
|
166
|
+
sheetContext.font = '700 28px sans-serif';
|
|
167
|
+
sheetContext.fillText(labels.title, 24, 42);
|
|
168
|
+
sheetContext.font = '600 18px sans-serif';
|
|
169
|
+
sheetContext.fillText(labels.original, 24, 80);
|
|
170
|
+
sheetContext.fillText(labels.simulated, imageWidth + 48, 80);
|
|
171
|
+
sheetContext.drawImage(original, 24, 98, imageWidth, size.imageHeight);
|
|
172
|
+
sheetContext.drawImage(simulated, imageWidth + 48, 98, imageWidth, size.imageHeight);
|
|
173
|
+
sheetContext.font = '500 16px sans-serif';
|
|
174
|
+
sheetContext.fillText(labels.settings, 24, size.height - 48);
|
|
175
|
+
sheetContext.fillStyle = '#90aab4';
|
|
176
|
+
sheetContext.fillText(labels.localOnly, 24, size.height - 20);
|
|
177
|
+
return sheet;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function triggerDownload(blob: Blob, filename: string): void {
|
|
181
|
+
const url = URL.createObjectURL(blob);
|
|
182
|
+
const anchor = document.createElement('a');
|
|
183
|
+
anchor.href = url;
|
|
184
|
+
anchor.download = filename;
|
|
185
|
+
anchor.click();
|
|
186
|
+
URL.revokeObjectURL(url);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function downloadCanvas(canvas: HTMLCanvasElement, filename: string): void {
|
|
190
|
+
canvas.toBlob((blob) => {
|
|
191
|
+
if (blob) triggerDownload(blob, filename);
|
|
192
|
+
}, 'image/png');
|
|
193
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { GamesToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
import type { GameUiAccessibilityTesterUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export type { GameUiAccessibilityTesterUI };
|
|
5
|
+
|
|
6
|
+
export type GameUiAccessibilityTesterLocaleContent = ToolLocaleContent<GameUiAccessibilityTesterUI>;
|
|
7
|
+
|
|
8
|
+
export const gameUiAccessibilityTester: GamesToolEntry<GameUiAccessibilityTesterUI> = {
|
|
9
|
+
id: 'game-ui-accessibility-stress-tester',
|
|
10
|
+
icons: { bg: 'mdi:accessibility', fg: 'mdi:eye-check-outline' },
|
|
11
|
+
i18n: {
|
|
12
|
+
de: () => import('./i18n/de').then((module) => module.content),
|
|
13
|
+
en: () => import('./i18n/en').then((module) => module.content),
|
|
14
|
+
es: () => import('./i18n/es').then((module) => module.content),
|
|
15
|
+
fr: () => import('./i18n/fr').then((module) => module.content),
|
|
16
|
+
id: () => import('./i18n/id').then((module) => module.content),
|
|
17
|
+
it: () => import('./i18n/it').then((module) => module.content),
|
|
18
|
+
ja: () => import('./i18n/ja').then((module) => module.content),
|
|
19
|
+
ko: () => import('./i18n/ko').then((module) => module.content),
|
|
20
|
+
nl: () => import('./i18n/nl').then((module) => module.content),
|
|
21
|
+
pl: () => import('./i18n/pl').then((module) => module.content),
|
|
22
|
+
pt: () => import('./i18n/pt').then((module) => module.content),
|
|
23
|
+
ru: () => import('./i18n/ru').then((module) => module.content),
|
|
24
|
+
sv: () => import('./i18n/sv').then((module) => module.content),
|
|
25
|
+
tr: () => import('./i18n/tr').then((module) => module.content),
|
|
26
|
+
zh: () => import('./i18n/zh').then((module) => module.content),
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
contrastRatio,
|
|
3
|
+
separationRetention,
|
|
4
|
+
simulateColor,
|
|
5
|
+
type RGBColor,
|
|
6
|
+
type SimulationMode,
|
|
7
|
+
} from './logic';
|
|
8
|
+
|
|
9
|
+
export type DiagnosticStatus = 'strong' | 'watch' | 'review';
|
|
10
|
+
|
|
11
|
+
export interface PairDiagnostic {
|
|
12
|
+
originalContrast: number;
|
|
13
|
+
simulatedContrast: number;
|
|
14
|
+
retained: number;
|
|
15
|
+
status: DiagnosticStatus;
|
|
16
|
+
simulatedFirst: RGBColor;
|
|
17
|
+
simulatedSecond: RGBColor;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function diagnosticStatus(contrast: number, retained: number): DiagnosticStatus {
|
|
21
|
+
if (contrast < 3 || retained < 45) return 'review';
|
|
22
|
+
if (contrast < 4.5 || retained < 70) return 'watch';
|
|
23
|
+
return 'strong';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function evaluatePair(
|
|
27
|
+
first: RGBColor,
|
|
28
|
+
second: RGBColor,
|
|
29
|
+
mode: SimulationMode,
|
|
30
|
+
): PairDiagnostic {
|
|
31
|
+
const simulatedFirst = simulateColor(first, mode);
|
|
32
|
+
const simulatedSecond = simulateColor(second, mode);
|
|
33
|
+
const simulatedContrast = contrastRatio(simulatedFirst, simulatedSecond);
|
|
34
|
+
const retained = separationRetention(first, second, simulatedFirst, simulatedSecond);
|
|
35
|
+
return {
|
|
36
|
+
originalContrast: contrastRatio(first, second),
|
|
37
|
+
simulatedContrast,
|
|
38
|
+
retained,
|
|
39
|
+
status: diagnosticStatus(simulatedContrast, retained),
|
|
40
|
+
simulatedFirst,
|
|
41
|
+
simulatedSecond,
|
|
42
|
+
};
|
|
43
|
+
}
|