@jjlmoya/utils-games-development 1.69.0 → 1.71.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 +2 -1
- package/src/entries.ts +4 -1
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +9 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/fixerEditor/auto-trim.ts +37 -0
- package/src/tool/fixerEditor/bibliography.astro +6 -0
- package/src/tool/fixerEditor/bibliography.ts +6 -0
- package/src/tool/fixerEditor/component.astro +114 -0
- package/src/tool/fixerEditor/controller.ts +275 -0
- package/src/tool/fixerEditor/dom-views.ts +200 -0
- package/src/tool/fixerEditor/entry.ts +28 -0
- package/src/tool/fixerEditor/evaluator.ts +17 -0
- package/src/tool/fixerEditor/events.ts +98 -0
- package/src/tool/fixerEditor/exporter.ts +42 -0
- package/src/tool/fixerEditor/fixer-editor.css +603 -0
- package/src/tool/fixerEditor/i18n/de.ts +82 -0
- package/src/tool/fixerEditor/i18n/en.ts +114 -0
- package/src/tool/fixerEditor/i18n/es.ts +82 -0
- package/src/tool/fixerEditor/i18n/fr.ts +82 -0
- package/src/tool/fixerEditor/i18n/id.ts +82 -0
- package/src/tool/fixerEditor/i18n/it.ts +82 -0
- package/src/tool/fixerEditor/i18n/ja.ts +81 -0
- package/src/tool/fixerEditor/i18n/ko.ts +81 -0
- package/src/tool/fixerEditor/i18n/localized.ts +391 -0
- package/src/tool/fixerEditor/i18n/nl.ts +82 -0
- package/src/tool/fixerEditor/i18n/pl.ts +82 -0
- package/src/tool/fixerEditor/i18n/pt.ts +82 -0
- package/src/tool/fixerEditor/i18n/ru.ts +82 -0
- package/src/tool/fixerEditor/i18n/sv.ts +82 -0
- package/src/tool/fixerEditor/i18n/tr.ts +82 -0
- package/src/tool/fixerEditor/i18n/zh.ts +80 -0
- package/src/tool/fixerEditor/index.ts +11 -0
- package/src/tool/fixerEditor/logic.test.ts +69 -0
- package/src/tool/fixerEditor/logic.ts +234 -0
- package/src/tool/fixerEditor/runtime.ts +88 -0
- package/src/tool/fixerEditor/seo.astro +15 -0
- package/src/tool/fixerEditor/state.ts +10 -0
- package/src/tool/fixerEditor/storage.ts +68 -0
- package/src/tool/fixerEditor/types.ts +70 -0
- package/src/tool/fixerEditor/ui.ts +71 -0
- package/src/tools.ts +2 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createFrameSpecs, detectGridFromImageData } from './logic';
|
|
2
|
+
import { saveDraftImage } from './storage';
|
|
3
|
+
import type { ControllerContext } from './controller';
|
|
4
|
+
|
|
5
|
+
export function loadImage(context: ControllerContext, file: File): void {
|
|
6
|
+
const image = new Image();
|
|
7
|
+
image.onload = () => rememberImage(context, image, file.name, file);
|
|
8
|
+
image.onerror = () => { context.refs.status.textContent = context.ui.statusInvalidImage; URL.revokeObjectURL(image.src); };
|
|
9
|
+
image.src = URL.createObjectURL(file);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function rememberImage(context: ControllerContext, image: HTMLImageElement, sourceName: string, file: File): void {
|
|
13
|
+
const reader = new FileReader();
|
|
14
|
+
reader.onload = () => {
|
|
15
|
+
const imageDataUrl = typeof reader.result === 'string' ? reader.result : undefined;
|
|
16
|
+
if (imageDataUrl) saveDraftImage(imageDataUrl);
|
|
17
|
+
applyImage(context, image, sourceName, imageDataUrl);
|
|
18
|
+
};
|
|
19
|
+
reader.onerror = () => applyImage(context, image, sourceName);
|
|
20
|
+
reader.readAsDataURL(file);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function restoreDraftImage(context: ControllerContext, sourceImage: string, sourceName: string): void {
|
|
24
|
+
const image = new Image();
|
|
25
|
+
image.onload = () => applyImage(context, image, sourceName, sourceImage);
|
|
26
|
+
image.onerror = () => { context.refs.status.textContent = context.ui.statusInvalidImage; };
|
|
27
|
+
image.src = sourceImage;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function applyImage(context: ControllerContext, image: HTMLImageElement, sourceName: string, imageDataUrl?: string): void {
|
|
31
|
+
const pending = context.state.pendingProject;
|
|
32
|
+
const matchesProject = !pending || hasMatchingSize(pending, image);
|
|
33
|
+
context.state.image = image;
|
|
34
|
+
if (imageDataUrl) context.state.imageDataUrl = imageDataUrl;
|
|
35
|
+
else delete context.state.imageDataUrl;
|
|
36
|
+
context.state.sourceName = sourceName;
|
|
37
|
+
context.state.grid = chooseGrid(pending, matchesProject, context.state.grid, image);
|
|
38
|
+
context.state.adjustments = chooseAdjustments(pending, matchesProject);
|
|
39
|
+
context.state.pendingProject = null;
|
|
40
|
+
context.state.frames = createFrameSpecs(image.naturalWidth, image.naturalHeight, context.state.grid);
|
|
41
|
+
context.state.selected = Math.min(context.state.selected, Math.max(0, context.state.frames.length - 1));
|
|
42
|
+
context.updateView(matchesProject ? context.ui.loaded : context.ui.statusImageMismatch);
|
|
43
|
+
URL.revokeObjectURL(image.src);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function chooseGrid(project: NonNullable<ControllerContext['state']['pendingProject']> | null, matches: boolean, current: ControllerContext['state']['grid'], image: HTMLImageElement): ControllerContext['state']['grid'] {
|
|
47
|
+
if (matches && project) return project.grid;
|
|
48
|
+
return detectImageGrid(image) ?? current;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function chooseAdjustments(project: NonNullable<ControllerContext['state']['pendingProject']> | null, matches: boolean): ControllerContext['state']['adjustments'] {
|
|
52
|
+
if (!matches) return {};
|
|
53
|
+
return project?.adjustments ?? {};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hasMatchingSize(project: NonNullable<ControllerContext['state']['pendingProject']>, image: HTMLImageElement): boolean {
|
|
57
|
+
return project.sourceWidth === image.naturalWidth && project.sourceHeight === image.naturalHeight;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function detectImageGrid(image: HTMLImageElement) {
|
|
61
|
+
const canvas = document.createElement('canvas');
|
|
62
|
+
canvas.width = image.naturalWidth;
|
|
63
|
+
canvas.height = image.naturalHeight;
|
|
64
|
+
const context = canvas.getContext('2d');
|
|
65
|
+
if (!context) return null;
|
|
66
|
+
context.drawImage(image, 0, 0);
|
|
67
|
+
return detectGridFromImageData(context.getImageData(0, 0, canvas.width, canvas.height).data, canvas.width, canvas.height);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function autoDetectGrid(context: ControllerContext): void {
|
|
71
|
+
const { state, ui } = context;
|
|
72
|
+
if (!state.image) return;
|
|
73
|
+
const grid = detectImageGrid(state.image);
|
|
74
|
+
if (!grid) return;
|
|
75
|
+
context.recordHistory();
|
|
76
|
+
state.grid = grid;
|
|
77
|
+
state.frames = createFrameSpecs(state.image.naturalWidth, state.image.naturalHeight, state.grid);
|
|
78
|
+
state.selected = Math.min(state.selected, Math.max(0, state.frames.length - 1));
|
|
79
|
+
context.updateView(ui.ready);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function animate(context: ControllerContext): void {
|
|
83
|
+
const { state } = context;
|
|
84
|
+
if (!state.playing || !state.image || state.frames.length === 0) return;
|
|
85
|
+
state.selected = (state.selected + 1) % state.frames.length;
|
|
86
|
+
context.updateView(context.ui.ready);
|
|
87
|
+
state.animationId = window.setTimeout(() => animate(context), 1000 / state.fps);
|
|
88
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { fixerEditor } 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 = fixerEditor.i18n[locale] ?? fixerEditor.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,10 @@
|
|
|
1
|
+
import { DEFAULT_GRID } from './logic';
|
|
2
|
+
import type { EditorState } from './controller';
|
|
3
|
+
import type { DraftFile } from './types';
|
|
4
|
+
|
|
5
|
+
export function createInitialState(draft: DraftFile | null): EditorState {
|
|
6
|
+
const saved = { sourceName: '', grid: DEFAULT_GRID, adjustments: {}, selected: 0, step: 1, zoom: 3, fps: 12, ...(draft || {}) };
|
|
7
|
+
const state: EditorState = { image: null, sourceName: saved.sourceName, grid: saved.grid, frames: [], adjustments: saved.adjustments, selected: saved.selected, step: saved.step, zoom: saved.zoom, fps: saved.fps, playing: false, animationId: null, history: [], future: [], pendingProject: draft };
|
|
8
|
+
if (saved.sourceImage) state.imageDataUrl = saved.sourceImage;
|
|
9
|
+
return state;
|
|
10
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { clamp, parseProjectFile } from './logic';
|
|
2
|
+
import type { DraftFile, DraftSession, ProjectFile } from './types';
|
|
3
|
+
|
|
4
|
+
const STORAGE_KEY = 'jjlmoya-fixer-editor-draft-v1';
|
|
5
|
+
const IMAGE_KEY = 'jjlmoya-fixer-editor-image-v1';
|
|
6
|
+
|
|
7
|
+
export function saveDraft(project: ProjectFile, session: DraftSession): boolean {
|
|
8
|
+
try {
|
|
9
|
+
const draft = { ...project, ...session };
|
|
10
|
+
delete draft.sourceImage;
|
|
11
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(draft));
|
|
12
|
+
return true;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function loadDraft(): DraftFile | null {
|
|
19
|
+
try {
|
|
20
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
21
|
+
if (!raw) return null;
|
|
22
|
+
const value = JSON.parse(raw) as unknown;
|
|
23
|
+
const project = parseProjectFile(value);
|
|
24
|
+
const sourceImage = loadDraftImage();
|
|
25
|
+
return project ? { ...project, ...readDraftSession(value), ...(sourceImage ? { sourceImage } : {}) } : null;
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function saveDraftImage(sourceImage: string): boolean {
|
|
32
|
+
try {
|
|
33
|
+
localStorage.setItem(IMAGE_KEY, sourceImage);
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function loadDraftImage(): string | null {
|
|
41
|
+
try {
|
|
42
|
+
const sourceImage = localStorage.getItem(IMAGE_KEY);
|
|
43
|
+
return sourceImage && sourceImage.startsWith('data:image/') ? sourceImage : null;
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readDraftSession(value: unknown): DraftSession {
|
|
50
|
+
const candidate = value && typeof value === 'object' ? value as Partial<DraftSession> : {};
|
|
51
|
+
const session: DraftSession = { selected: readInteger(candidate.selected, 0, 8192, 0), step: readInteger(candidate.step, 1, 4, 1), zoom: readInteger(candidate.zoom, 1, 8, 3), fps: readInteger(candidate.fps, 1, 30, 12) };
|
|
52
|
+
if (typeof candidate.sourceImage === 'string') session.sourceImage = candidate.sourceImage;
|
|
53
|
+
return session;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readInteger(value: unknown, min: number, max: number, fallback: number): number {
|
|
57
|
+
if (typeof value !== 'number' || !Number.isInteger(value)) return fallback;
|
|
58
|
+
return clamp(value, min, max);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function clearDraft(): void {
|
|
62
|
+
try {
|
|
63
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
64
|
+
localStorage.removeItem(IMAGE_KEY);
|
|
65
|
+
} catch {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export interface GridSettings {
|
|
2
|
+
columns: number;
|
|
3
|
+
rows: number;
|
|
4
|
+
cellWidth: number;
|
|
5
|
+
cellHeight: number;
|
|
6
|
+
marginX: number;
|
|
7
|
+
marginY: number;
|
|
8
|
+
gapX: number;
|
|
9
|
+
gapY: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CropRect {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface FrameAdjustment {
|
|
20
|
+
crop: CropRect;
|
|
21
|
+
offsetX: number;
|
|
22
|
+
offsetY: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface FrameSpec {
|
|
26
|
+
index: number;
|
|
27
|
+
row: number;
|
|
28
|
+
column: number;
|
|
29
|
+
sourceX: number;
|
|
30
|
+
sourceY: number;
|
|
31
|
+
width: number;
|
|
32
|
+
height: number;
|
|
33
|
+
adjustment: FrameAdjustment;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface FrameMetadata {
|
|
37
|
+
index: number;
|
|
38
|
+
x: number;
|
|
39
|
+
y: number;
|
|
40
|
+
width: number;
|
|
41
|
+
height: number;
|
|
42
|
+
crop: CropRect;
|
|
43
|
+
offset: { x: number; y: number };
|
|
44
|
+
modified: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ProjectFile {
|
|
48
|
+
version: 1;
|
|
49
|
+
sourceName: string;
|
|
50
|
+
sourceWidth: number;
|
|
51
|
+
sourceHeight: number;
|
|
52
|
+
grid: GridSettings;
|
|
53
|
+
adjustments: Record<string, FrameAdjustment>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface DraftSession {
|
|
57
|
+
selected: number;
|
|
58
|
+
step: number;
|
|
59
|
+
zoom: number;
|
|
60
|
+
fps: number;
|
|
61
|
+
sourceImage?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type DraftFile = ProjectFile & DraftSession;
|
|
65
|
+
|
|
66
|
+
export interface HistoryState {
|
|
67
|
+
grid: GridSettings;
|
|
68
|
+
adjustments: Record<string, FrameAdjustment>;
|
|
69
|
+
selected: number;
|
|
70
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export interface FixerEditorUI extends Record<string, string> {
|
|
2
|
+
uploadTitle: string;
|
|
3
|
+
uploadHint: string;
|
|
4
|
+
chooseImage: string;
|
|
5
|
+
sourceFile: string;
|
|
6
|
+
noImage: string;
|
|
7
|
+
gridSettings: string;
|
|
8
|
+
columns: string;
|
|
9
|
+
rows: string;
|
|
10
|
+
cellWidth: string;
|
|
11
|
+
cellHeight: string;
|
|
12
|
+
marginX: string;
|
|
13
|
+
marginY: string;
|
|
14
|
+
gapX: string;
|
|
15
|
+
gapY: string;
|
|
16
|
+
frameTools: string;
|
|
17
|
+
cropX: string;
|
|
18
|
+
cropY: string;
|
|
19
|
+
cropWidth: string;
|
|
20
|
+
cropHeight: string;
|
|
21
|
+
offsetX: string;
|
|
22
|
+
offsetY: string;
|
|
23
|
+
trimTransparent: string;
|
|
24
|
+
trimAll: string;
|
|
25
|
+
autoDetectGrid: string;
|
|
26
|
+
resetFrame: string;
|
|
27
|
+
nudgeStep: string;
|
|
28
|
+
stepOne: string;
|
|
29
|
+
stepTwo: string;
|
|
30
|
+
stepFour: string;
|
|
31
|
+
history: string;
|
|
32
|
+
undo: string;
|
|
33
|
+
redo: string;
|
|
34
|
+
resetAll: string;
|
|
35
|
+
saveProject: string;
|
|
36
|
+
loadProject: string;
|
|
37
|
+
exportSheet: string;
|
|
38
|
+
exportJson: string;
|
|
39
|
+
sheetView: string;
|
|
40
|
+
correctedView: string;
|
|
41
|
+
activeFrame: string;
|
|
42
|
+
frameStrip: string;
|
|
43
|
+
flipbook: string;
|
|
44
|
+
play: string;
|
|
45
|
+
pause: string;
|
|
46
|
+
fps: string;
|
|
47
|
+
zoom: string;
|
|
48
|
+
ready: string;
|
|
49
|
+
loaded: string;
|
|
50
|
+
modified: string;
|
|
51
|
+
unchanged: string;
|
|
52
|
+
frameCount: string;
|
|
53
|
+
correctionCount: string;
|
|
54
|
+
imageSize: string;
|
|
55
|
+
statusEmpty: string;
|
|
56
|
+
statusSaved: string;
|
|
57
|
+
statusLoaded: string;
|
|
58
|
+
statusExported: string;
|
|
59
|
+
statusInvalidImage: string;
|
|
60
|
+
statusInvalidProject: string;
|
|
61
|
+
statusProjectNeedsImage: string;
|
|
62
|
+
statusImageMismatch: string;
|
|
63
|
+
metadataNote: string;
|
|
64
|
+
sourceLabel: string;
|
|
65
|
+
resultLabel: string;
|
|
66
|
+
selectFrame: string;
|
|
67
|
+
moveUp: string;
|
|
68
|
+
moveDown: string;
|
|
69
|
+
moveLeft: string;
|
|
70
|
+
moveRight: string;
|
|
71
|
+
}
|
package/src/tools.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ToolDefinition } from './types';
|
|
|
3
3
|
import { STEAM_CAPSULE_GENERATOR_TOOL } from './tool/steamCapsuleGenerator';
|
|
4
4
|
import { ITCHIO_GAME_TESTER_TOOL } from './tool/itchioGameTester';
|
|
5
5
|
import { SPRITE_SHEET_PACKER_TOOL } from './tool/spriteSheetPacker';
|
|
6
|
+
import { FIXER_EDITOR_TOOL } from './tool/fixerEditor';
|
|
6
7
|
import { AUDIO_LOOP_POINT_FINDER_TOOL } from './tool/audioLoopPointFinder';
|
|
7
8
|
import { SAVE_FILE_EDITOR_TOOL } from './tool/saveFileEditor';
|
|
8
9
|
import { LOCALIZATION_SANITIZER_TOOL } from './tool/localizationSanitizer';
|
|
@@ -19,6 +20,7 @@ export const ALL_TOOLS: ToolDefinition[] = [
|
|
|
19
20
|
STEAM_CAPSULE_GENERATOR_TOOL,
|
|
20
21
|
ITCHIO_GAME_TESTER_TOOL,
|
|
21
22
|
SPRITE_SHEET_PACKER_TOOL,
|
|
23
|
+
FIXER_EDITOR_TOOL,
|
|
22
24
|
AUDIO_LOOP_POINT_FINDER_TOOL,
|
|
23
25
|
SAVE_FILE_EDITOR_TOOL,
|
|
24
26
|
LOCALIZATION_SANITIZER_TOOL,
|