@jjlmoya/utils-tabletop 1.26.0 → 1.28.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 +4 -0
- package/src/entries.ts +4 -0
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +1 -1
- package/src/tool/mus-scoreboard/bibliography.astro +16 -0
- package/src/tool/mus-scoreboard/bibliography.ts +12 -0
- package/src/tool/mus-scoreboard/component.astro +169 -0
- package/src/tool/mus-scoreboard/controller.ts +178 -0
- package/src/tool/mus-scoreboard/dom-views.ts +87 -0
- package/src/tool/mus-scoreboard/entry.ts +27 -0
- package/src/tool/mus-scoreboard/evaluator.ts +27 -0
- package/src/tool/mus-scoreboard/i18n/de.ts +187 -0
- package/src/tool/mus-scoreboard/i18n/en.ts +187 -0
- package/src/tool/mus-scoreboard/i18n/es.ts +187 -0
- package/src/tool/mus-scoreboard/i18n/fr.ts +187 -0
- package/src/tool/mus-scoreboard/i18n/id.ts +187 -0
- package/src/tool/mus-scoreboard/i18n/it.ts +187 -0
- package/src/tool/mus-scoreboard/i18n/ja.ts +187 -0
- package/src/tool/mus-scoreboard/i18n/ko.ts +187 -0
- package/src/tool/mus-scoreboard/i18n/nl.ts +187 -0
- package/src/tool/mus-scoreboard/i18n/pl.ts +187 -0
- package/src/tool/mus-scoreboard/i18n/pt.ts +187 -0
- package/src/tool/mus-scoreboard/i18n/ru.ts +187 -0
- package/src/tool/mus-scoreboard/i18n/sv.ts +187 -0
- package/src/tool/mus-scoreboard/i18n/tr.ts +187 -0
- package/src/tool/mus-scoreboard/i18n/zh.ts +187 -0
- package/src/tool/mus-scoreboard/index.ts +11 -0
- package/src/tool/mus-scoreboard/logic.test.ts +82 -0
- package/src/tool/mus-scoreboard/logic.ts +122 -0
- package/src/tool/mus-scoreboard/online-mus-scoreboard.css +740 -0
- package/src/tool/mus-scoreboard/seo.astro +16 -0
- package/src/tool/mus-scoreboard/storage.ts +29 -0
- package/src/tool/mus-scoreboard/ui.ts +63 -0
- package/src/tool/token-stamp-studio/bibliography.astro +16 -0
- package/src/tool/token-stamp-studio/bibliography.ts +12 -0
- package/src/tool/token-stamp-studio/component.astro +91 -0
- package/src/tool/token-stamp-studio/controller.ts +179 -0
- package/src/tool/token-stamp-studio/dom-views.ts +266 -0
- package/src/tool/token-stamp-studio/entry.ts +27 -0
- package/src/tool/token-stamp-studio/evaluator.ts +6 -0
- package/src/tool/token-stamp-studio/i18n/de.ts +132 -0
- package/src/tool/token-stamp-studio/i18n/en.ts +132 -0
- package/src/tool/token-stamp-studio/i18n/es.ts +132 -0
- package/src/tool/token-stamp-studio/i18n/fr.ts +132 -0
- package/src/tool/token-stamp-studio/i18n/id.ts +132 -0
- package/src/tool/token-stamp-studio/i18n/it.ts +132 -0
- package/src/tool/token-stamp-studio/i18n/ja.ts +132 -0
- package/src/tool/token-stamp-studio/i18n/ko.ts +132 -0
- package/src/tool/token-stamp-studio/i18n/nl.ts +132 -0
- package/src/tool/token-stamp-studio/i18n/pl.ts +132 -0
- package/src/tool/token-stamp-studio/i18n/pt.ts +132 -0
- package/src/tool/token-stamp-studio/i18n/ru.ts +132 -0
- package/src/tool/token-stamp-studio/i18n/sv.ts +132 -0
- package/src/tool/token-stamp-studio/i18n/tr.ts +132 -0
- package/src/tool/token-stamp-studio/i18n/zh.ts +132 -0
- package/src/tool/token-stamp-studio/index.ts +11 -0
- package/src/tool/token-stamp-studio/logic.test.ts +36 -0
- package/src/tool/token-stamp-studio/logic.ts +127 -0
- package/src/tool/token-stamp-studio/seo.astro +16 -0
- package/src/tool/token-stamp-studio/storage.ts +69 -0
- package/src/tool/token-stamp-studio/store.ts +139 -0
- package/src/tool/token-stamp-studio/token-stamp-studio.css +646 -0
- package/src/tool/token-stamp-studio/types.ts +65 -0
- package/src/tool/token-stamp-studio/ui.ts +50 -0
- package/src/tool/token-stamp-studio/workspace-media.ts +36 -0
- package/src/tool/token-stamp-studio/workspace.ts +83 -0
- package/src/tools.ts +4 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface TokenStampUI {
|
|
2
|
+
[key: string]: string;
|
|
3
|
+
frameLegend: string;
|
|
4
|
+
frameHint: string;
|
|
5
|
+
randomFrame: string;
|
|
6
|
+
surfaceLegend: string;
|
|
7
|
+
backgroundLabel: string;
|
|
8
|
+
borderLabel: string;
|
|
9
|
+
textLabel: string;
|
|
10
|
+
overlayLabel: string;
|
|
11
|
+
randomColors: string;
|
|
12
|
+
borderWidthLabel: string;
|
|
13
|
+
opacityLabel: string;
|
|
14
|
+
overlayOpacityLabel: string;
|
|
15
|
+
stageLabel: string;
|
|
16
|
+
chooseImage: string;
|
|
17
|
+
clearImage: string;
|
|
18
|
+
positionHint: string;
|
|
19
|
+
markerName: string;
|
|
20
|
+
markerNamePlaceholder: string;
|
|
21
|
+
textLegend: string;
|
|
22
|
+
textHint: string;
|
|
23
|
+
textPlaceholder: string;
|
|
24
|
+
addText: string;
|
|
25
|
+
removeText: string;
|
|
26
|
+
textSizeLabel: string;
|
|
27
|
+
alignmentLabel: string;
|
|
28
|
+
alignLeft: string;
|
|
29
|
+
alignCenter: string;
|
|
30
|
+
alignRight: string;
|
|
31
|
+
imageLegend: string;
|
|
32
|
+
imageZoomLabel: string;
|
|
33
|
+
scaleLabel: string;
|
|
34
|
+
savedMarkers: string;
|
|
35
|
+
noSavedMarkers: string;
|
|
36
|
+
reuseMarker: string;
|
|
37
|
+
deleteMarker: string;
|
|
38
|
+
newMarker: string;
|
|
39
|
+
download: string;
|
|
40
|
+
copy: string;
|
|
41
|
+
downloadBatch: string;
|
|
42
|
+
batchLabel: string;
|
|
43
|
+
batchHint: string;
|
|
44
|
+
chooseBatch: string;
|
|
45
|
+
batchReady: string;
|
|
46
|
+
noImage: string;
|
|
47
|
+
noText: string;
|
|
48
|
+
tokenDetails: string;
|
|
49
|
+
exportHint: string;
|
|
50
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function readFile(file: File): Promise<string> {
|
|
2
|
+
return new Promise((resolve, reject) => {
|
|
3
|
+
const reader = new FileReader();
|
|
4
|
+
reader.addEventListener('load', () => resolve(String(reader.result)));
|
|
5
|
+
reader.addEventListener('error', () => reject(reader.error));
|
|
6
|
+
reader.readAsDataURL(file);
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function loadImage(source: string): Promise<HTMLImageElement> {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const image = new Image();
|
|
13
|
+
image.addEventListener('load', () => resolve(image));
|
|
14
|
+
image.addEventListener('error', () => reject(new Error('Image could not be loaded')));
|
|
15
|
+
image.src = source;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function downloadCanvas(canvas: HTMLCanvasElement, name: string): void {
|
|
20
|
+
const link = document.createElement('a');
|
|
21
|
+
link.download = `${name || 'tabletop-token'}.png`;
|
|
22
|
+
link.href = canvas.toDataURL('image/png');
|
|
23
|
+
link.click();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function copyCanvas(canvas: HTMLCanvasElement): Promise<boolean> {
|
|
27
|
+
if (!navigator.clipboard?.write || typeof ClipboardItem === 'undefined') return false;
|
|
28
|
+
const blob = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, 'image/png'));
|
|
29
|
+
if (!blob) return false;
|
|
30
|
+
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function cleanName(name: string): string {
|
|
35
|
+
return name.replace(/\.[^/.]+$/, '').replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '').toLowerCase() || 'tabletop-token';
|
|
36
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { drawTokenCanvas } from './dom-views';
|
|
2
|
+
import { loadImage, readFile, cleanName, copyCanvas, downloadCanvas } from './workspace-media';
|
|
3
|
+
import type { StudioElements } from './controller';
|
|
4
|
+
import type { SavedMarker, TokenStampState } from './types';
|
|
5
|
+
import type { TokenStampUI } from './ui';
|
|
6
|
+
|
|
7
|
+
export interface WorkspaceContext {
|
|
8
|
+
getState: () => TokenStampState;
|
|
9
|
+
setState: (state: TokenStampState) => void;
|
|
10
|
+
ui: TokenStampUI;
|
|
11
|
+
getMarkers: () => SavedMarker[];
|
|
12
|
+
getImage: () => HTMLImageElement | null;
|
|
13
|
+
selectMarker: (marker: SavedMarker) => void;
|
|
14
|
+
addMarker: () => void;
|
|
15
|
+
removeMarker: (id: string) => void;
|
|
16
|
+
setImage: (image: HTMLImageElement | null) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getElement<T extends Element>(root: HTMLElement, selector: string): T {
|
|
20
|
+
const element = root.querySelector<T>(selector);
|
|
21
|
+
if (!element) throw new Error(`Token stamp studio element missing: ${selector}`);
|
|
22
|
+
return element;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function attachLibraryEvents(elements: StudioElements, context: WorkspaceContext): void {
|
|
26
|
+
elements.library.addEventListener('click', (event) => {
|
|
27
|
+
const target = event.target as HTMLElement;
|
|
28
|
+
const deleteId = target.closest<HTMLElement>('[data-delete-marker]')?.dataset.deleteMarker;
|
|
29
|
+
if (deleteId) { context.removeMarker(deleteId); return; }
|
|
30
|
+
if (target.closest('[data-new-marker]')) { context.addMarker(); return; }
|
|
31
|
+
const markerId = target.closest<HTMLElement>('[data-marker-id]')?.dataset.markerId;
|
|
32
|
+
const marker = context.getMarkers().find((item) => item.id === markerId);
|
|
33
|
+
if (!marker) return;
|
|
34
|
+
context.selectMarker(marker);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function attachWorkspaceEvents(elements: StudioElements, context: WorkspaceContext): void {
|
|
39
|
+
getElement<HTMLButtonElement>(elements.root, '[data-clear-image]').addEventListener('click', () => context.setState({ ...context.getState(), imageSrc: null, imageName: '', imageX: 0, imageY: 0 }));
|
|
40
|
+
const exportCanvas = (): HTMLCanvasElement => {
|
|
41
|
+
const canvas = document.createElement('canvas');
|
|
42
|
+
drawTokenCanvas(canvas, context.getState(), context.getImage(), false);
|
|
43
|
+
return canvas;
|
|
44
|
+
};
|
|
45
|
+
elements.download.addEventListener('click', () => downloadCanvas(exportCanvas(), cleanName(context.getState().imageName)));
|
|
46
|
+
elements.copy.addEventListener('click', () => void copyCanvas(exportCanvas()).catch(() => false));
|
|
47
|
+
elements.markerName.addEventListener('input', (event) => context.setState({ ...context.getState(), markerName: (event.target as HTMLInputElement).value }));
|
|
48
|
+
attachLibraryEvents(elements, context);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function attachFileEvents(elements: StudioElements, context: WorkspaceContext): void {
|
|
52
|
+
const load = async (file: File) => context.setImage(await loadDroppedFile(file, context));
|
|
53
|
+
elements.imageInput.addEventListener('change', () => { const file = elements.imageInput.files?.[0]; if (file) void load(file); });
|
|
54
|
+
elements.root.addEventListener('dragover', (event) => event.preventDefault());
|
|
55
|
+
elements.root.addEventListener('drop', (event) => { event.preventDefault(); const file = event.dataTransfer?.files[0]; if (file?.type.startsWith('image/')) void load(file); });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function loadDroppedFile(file: File, context: WorkspaceContext): Promise<HTMLImageElement> {
|
|
59
|
+
const source = await readFile(file);
|
|
60
|
+
const image = await loadImage(source);
|
|
61
|
+
context.setState({ ...context.getState(), imageSrc: source, imageName: file.name, imageX: 0, imageY: 0, imageZoom: 1 });
|
|
62
|
+
return image;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function exportBatch(files: File[], state: TokenStampState): Promise<void> {
|
|
66
|
+
for (const file of files) {
|
|
67
|
+
const source = await readFile(file);
|
|
68
|
+
const image = await loadImage(source);
|
|
69
|
+
const canvas = document.createElement('canvas');
|
|
70
|
+
drawTokenCanvas(canvas, { ...state, imageSrc: source, imageName: file.name }, image, false);
|
|
71
|
+
downloadCanvas(canvas, cleanName(file.name));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function attachBatchEvents(elements: StudioElements, context: WorkspaceContext): void {
|
|
76
|
+
let files: File[] = [];
|
|
77
|
+
elements.batchInput.addEventListener('change', () => {
|
|
78
|
+
files = Array.from(elements.batchInput.files ?? []);
|
|
79
|
+
elements.batchStatus.textContent = files.length ? context.ui.batchReady.replace('{count}', String(files.length)) : context.ui.batchHint;
|
|
80
|
+
elements.batchDownload.disabled = files.length === 0;
|
|
81
|
+
});
|
|
82
|
+
elements.batchDownload.addEventListener('click', () => void exportBatch(files, context.getState()));
|
|
83
|
+
}
|
package/src/tools.ts
CHANGED
|
@@ -13,6 +13,8 @@ import { HIDDEN_ROLE_DEALER_TOOL } from './tool/hidden-role-dealer';
|
|
|
13
13
|
import { SCATTER_DIRECTION_SELECTOR_TOOL } from './tool/scatter-direction-selector';
|
|
14
14
|
import { DUNGEON_MAP_GENERATOR_TOOL } from './tool/dungeon-map-generator';
|
|
15
15
|
import { ENCOUNTER_DIFFICULTY_CALCULATOR_TOOL } from './tool/encounter-difficulty-calculator';
|
|
16
|
+
import { TOKEN_STAMP_STUDIO_TOOL } from './tool/token-stamp-studio';
|
|
17
|
+
import { MUS_SCOREBOARD_TOOL } from './tool/mus-scoreboard';
|
|
16
18
|
|
|
17
19
|
export const ALL_TOOLS: ToolDefinition[] = [
|
|
18
20
|
DICE_ROLLER_SIMULATOR_TOOL,
|
|
@@ -28,5 +30,7 @@ export const ALL_TOOLS: ToolDefinition[] = [
|
|
|
28
30
|
SCATTER_DIRECTION_SELECTOR_TOOL,
|
|
29
31
|
DUNGEON_MAP_GENERATOR_TOOL,
|
|
30
32
|
ENCOUNTER_DIFFICULTY_CALCULATOR_TOOL,
|
|
33
|
+
TOKEN_STAMP_STUDIO_TOOL,
|
|
34
|
+
MUS_SCOREBOARD_TOOL,
|
|
31
35
|
];
|
|
32
36
|
|