jaml-ui 0.14.4 → 0.17.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 (97) hide show
  1. package/DESIGN.md +9 -11
  2. package/dist/assets.d.ts +7 -0
  3. package/dist/assets.js +11 -0
  4. package/dist/components/AnalyzerExplorer.d.ts +4 -1
  5. package/dist/components/AnalyzerExplorer.js +14 -48
  6. package/dist/components/GameCard.js +8 -7
  7. package/dist/components/JamlAestheticSelector.d.ts +4 -0
  8. package/dist/components/JamlAestheticSelector.js +6 -19
  9. package/dist/components/JamlAnalyzerFullscreen.d.ts +7 -1
  10. package/dist/components/JamlAnalyzerFullscreen.js +18 -47
  11. package/dist/components/JamlIde.js +12 -24
  12. package/dist/components/JamlIdeVisual.js +3 -56
  13. package/dist/components/JamlMapPreview.d.ts +6 -1
  14. package/dist/components/JamlMapPreview.js +99 -21
  15. package/dist/components/JamlSeedInput.d.ts +5 -0
  16. package/dist/components/JamlSeedInput.js +11 -14
  17. package/dist/components/MotelyVersionBadge.d.ts +1 -3
  18. package/dist/components/MotelyVersionBadge.js +4 -16
  19. package/dist/components/jamlMap/JamlMapEditorDemo.d.ts +8 -0
  20. package/dist/components/jamlMap/JamlMapEditorDemo.js +170 -0
  21. package/dist/components/jamlMap/JokerPicker.d.ts +7 -0
  22. package/dist/components/jamlMap/JokerPicker.js +258 -0
  23. package/dist/components/jamlMap/MysterySlot.d.ts +32 -0
  24. package/dist/components/jamlMap/MysterySlot.js +109 -0
  25. package/dist/components/jamlMap/index.d.ts +3 -0
  26. package/dist/components/jamlMap/index.js +3 -0
  27. package/dist/core.d.ts +0 -2
  28. package/dist/core.js +0 -2
  29. package/dist/decode/motelyItemDecoder.d.ts +10 -23
  30. package/dist/decode/motelyItemDecoder.js +103 -248
  31. package/dist/decode/motelySprite.d.ts +19 -0
  32. package/dist/decode/motelySprite.js +84 -0
  33. package/dist/hooks/analyzerStreamRegistry.js +30 -82
  34. package/dist/hooks/useAnalyzer.d.ts +10 -3
  35. package/dist/hooks/useAnalyzer.js +11 -6
  36. package/dist/hooks/useIntersectionObserver.d.ts +14 -0
  37. package/dist/hooks/useIntersectionObserver.js +50 -0
  38. package/dist/index.d.ts +3 -8
  39. package/dist/index.js +2 -7
  40. package/dist/motely.d.ts +2 -1
  41. package/dist/motely.js +2 -1
  42. package/dist/motelyDisplay.d.ts +4 -623
  43. package/dist/motelyDisplay.js +26 -165
  44. package/dist/r3f/Card3D.d.ts +2 -2
  45. package/dist/r3f/Card3D.js +13 -46
  46. package/dist/r3f/JimboBillboard.d.ts +10 -0
  47. package/dist/r3f/JimboBillboard.js +29 -0
  48. package/dist/r3f/JimboText3D.d.ts +9 -0
  49. package/dist/r3f/JimboText3D.js +8 -0
  50. package/dist/r3f.d.ts +2 -0
  51. package/dist/r3f.js +2 -0
  52. package/dist/render/CanvasRenderer.js +7 -171
  53. package/dist/sprites/spriteData.d.ts +1 -0
  54. package/dist/sprites/spriteData.js +1 -0
  55. package/dist/sprites/spriteMapper.d.ts +78 -1
  56. package/dist/sprites/spriteMapper.js +52 -0
  57. package/dist/ui/JimboBadge.d.ts +13 -0
  58. package/dist/ui/JimboBadge.js +8 -0
  59. package/dist/ui/JimboFloating.d.ts +8 -0
  60. package/dist/ui/JimboFloating.js +17 -0
  61. package/dist/ui/JimboToggleList.d.ts +11 -0
  62. package/dist/ui/JimboToggleList.js +5 -0
  63. package/dist/ui/codeBlock.js +2 -3
  64. package/dist/ui/footer.d.ts +4 -0
  65. package/dist/ui/footer.js +6 -4
  66. package/dist/ui/hooks.d.ts +89 -0
  67. package/dist/ui/hooks.js +551 -0
  68. package/dist/ui/jimboBackground.js +2 -131
  69. package/dist/ui/jimboCopyRow.d.ts +4 -0
  70. package/dist/ui/jimboCopyRow.js +5 -22
  71. package/dist/ui/jimboFilterBar.d.ts +1 -4
  72. package/dist/ui/jimboFilterBar.js +2 -61
  73. package/dist/ui/jimboFlankNav.d.ts +1 -2
  74. package/dist/ui/jimboFlankNav.js +5 -30
  75. package/dist/ui/jimboTabs.d.ts +1 -5
  76. package/dist/ui/jimboTabs.js +6 -41
  77. package/dist/ui/jimboText.d.ts +1 -1
  78. package/dist/ui/jimboText.js +15 -32
  79. package/dist/ui/jimboTooltip.d.ts +1 -12
  80. package/dist/ui/jimboTooltip.js +6 -82
  81. package/dist/ui/panel.d.ts +3 -1
  82. package/dist/ui/panel.js +11 -47
  83. package/dist/ui/showcase.d.ts +4 -0
  84. package/dist/ui/showcase.js +9 -36
  85. package/dist/ui/sprites.d.ts +14 -0
  86. package/dist/ui/sprites.js +54 -13
  87. package/dist/ui.d.ts +4 -0
  88. package/dist/ui.js +5 -0
  89. package/package.json +130 -122
  90. package/dist/components/JamlSpeedometer.d.ts +0 -11
  91. package/dist/components/JamlSpeedometer.js +0 -54
  92. package/dist/decode/packedBalatroItem.d.ts +0 -13
  93. package/dist/decode/packedBalatroItem.js +0 -26
  94. package/dist/hooks/loadMotelyWasm.d.ts +0 -7
  95. package/dist/hooks/loadMotelyWasm.js +0 -16
  96. package/dist/utils/itemUtils.d.ts +0 -11
  97. package/dist/utils/itemUtils.js +0 -71
@@ -1,189 +1,50 @@
1
1
  import { Motely } from "motely-wasm";
2
- import { getItemDisplayName } from "./utils/itemUtils.js";
3
- const BOSS_ENTRIES = [
4
- { key: "AmberAcorn", label: "Amber Acorn" },
5
- { key: "CeruleanBell", label: "Cerulean Bell" },
6
- { key: "CrimsonHeart", label: "Crimson Heart" },
7
- { key: "VerdantLeaf", label: "Verdant Leaf" },
8
- { key: "VioletVessel", label: "Violet Vessel" },
9
- { key: "TheArm", label: "The Arm" },
10
- { key: "TheClub", label: "The Club" },
11
- { key: "TheEye", label: "The Eye" },
12
- { key: "TheFish", label: "The Fish" },
13
- { key: "TheFlint", label: "The Flint" },
14
- { key: "TheGoad", label: "The Goad" },
15
- { key: "TheHead", label: "The Head" },
16
- { key: "TheHook", label: "The Hook" },
17
- { key: "TheHouse", label: "The House" },
18
- { key: "TheManacle", label: "The Manacle" },
19
- { key: "TheMark", label: "The Mark" },
20
- { key: "TheMouth", label: "The Mouth" },
21
- { key: "TheNeedle", label: "The Needle" },
22
- { key: "TheOx", label: "The Ox" },
23
- { key: "ThePillar", label: "The Pillar" },
24
- { key: "ThePlant", label: "The Plant" },
25
- { key: "ThePsychic", label: "The Psychic" },
26
- { key: "TheSerpent", label: "The Serpent" },
27
- { key: "TheTooth", label: "The Tooth" },
28
- { key: "TheWall", label: "The Wall" },
29
- { key: "TheWater", label: "The Water" },
30
- { key: "TheWheel", label: "The Wheel" },
31
- { key: "TheWindow", label: "The Window" },
32
- ];
33
- const VOUCHER_ENTRIES = [
34
- { key: "Overstock", label: "Overstock" },
35
- { key: "OverstockPlus", label: "Overstock Plus" },
36
- { key: "ClearanceSale", label: "Clearance Sale" },
37
- { key: "Liquidation", label: "Liquidation" },
38
- { key: "Hone", label: "Hone" },
39
- { key: "GlowUp", label: "Glow Up" },
40
- { key: "RerollSurplus", label: "Reroll Surplus" },
41
- { key: "RerollGlut", label: "Reroll Glut" },
42
- { key: "CrystalBall", label: "Crystal Ball" },
43
- { key: "OmenGlobe", label: "Omen Globe" },
44
- { key: "Telescope", label: "Telescope" },
45
- { key: "Observatory", label: "Observatory" },
46
- { key: "Grabber", label: "Grabber" },
47
- { key: "NachoTong", label: "Nacho Tong" },
48
- { key: "Wasteful", label: "Wasteful" },
49
- { key: "Recyclomancy", label: "Recyclomancy" },
50
- { key: "TarotMerchant", label: "Tarot Merchant" },
51
- { key: "TarotTycoon", label: "Tarot Tycoon" },
52
- { key: "PlanetMerchant", label: "Planet Merchant" },
53
- { key: "PlanetTycoon", label: "Planet Tycoon" },
54
- { key: "SeedMoney", label: "Seed Money" },
55
- { key: "MoneyTree", label: "Money Tree" },
56
- { key: "Blank", label: "Blank" },
57
- { key: "Antimatter", label: "Antimatter" },
58
- { key: "MagicTrick", label: "Magic Trick" },
59
- { key: "Illusion", label: "Illusion" },
60
- { key: "Hieroglyph", label: "Hieroglyph" },
61
- { key: "Petroglyph", label: "Petroglyph" },
62
- { key: "DirectorsCut", label: "Director's Cut" },
63
- { key: "Retcon", label: "Retcon" },
64
- { key: "PaintBrush", label: "Paint Brush" },
65
- { key: "Palette", label: "Palette" },
66
- ];
67
- const TAG_ENTRIES = [
68
- { key: "UncommonTag", label: "Uncommon Tag" },
69
- { key: "RareTag", label: "Rare Tag" },
70
- { key: "NegativeTag", label: "Negative Tag" },
71
- { key: "FoilTag", label: "Foil Tag" },
72
- { key: "HolographicTag", label: "Holographic Tag" },
73
- { key: "PolychromeTag", label: "Polychrome Tag" },
74
- { key: "InvestmentTag", label: "Investment Tag" },
75
- { key: "VoucherTag", label: "Voucher Tag" },
76
- { key: "BossTag", label: "Boss Tag" },
77
- { key: "StandardTag", label: "Standard Tag" },
78
- { key: "CharmTag", label: "Charm Tag" },
79
- { key: "MeteorTag", label: "Meteor Tag" },
80
- { key: "BuffoonTag", label: "Buffoon Tag" },
81
- { key: "HandyTag", label: "Handy Tag" },
82
- { key: "GarbageTag", label: "Garbage Tag" },
83
- { key: "EtherealTag", label: "Ethereal Tag" },
84
- { key: "CouponTag", label: "Coupon Tag" },
85
- { key: "DoubleTag", label: "Double Tag" },
86
- { key: "JuggleTag", label: "Juggle Tag" },
87
- { key: "D6Tag", label: "D6 Tag" },
88
- { key: "TopupTag", label: "Top-up Tag" },
89
- { key: "SpeedTag", label: "Speed Tag" },
90
- { key: "OrbitalTag", label: "Orbital Tag" },
91
- { key: "EconomyTag", label: "Economy Tag" },
92
- ];
93
- const BOOSTER_PACK_ENTRIES = [
94
- { key: "Arcana", label: "Arcana Pack" },
95
- { key: "JumboArcana", label: "Jumbo Arcana Pack" },
96
- { key: "MegaArcana", label: "Mega Arcana Pack" },
97
- { key: "Celestial", label: "Celestial Pack" },
98
- { key: "JumboCelestial", label: "Jumbo Celestial Pack" },
99
- { key: "MegaCelestial", label: "Mega Celestial Pack" },
100
- { key: "Standard", label: "Standard Pack" },
101
- { key: "JumboStandard", label: "Jumbo Standard Pack" },
102
- { key: "MegaStandard", label: "Mega Standard Pack" },
103
- { key: "Buffoon", label: "Buffoon Pack" },
104
- { key: "JumboBuffoon", label: "Jumbo Buffoon Pack" },
105
- { key: "MegaBuffoon", label: "Mega Buffoon Pack" },
106
- { key: "Spectral", label: "Spectral Pack" },
107
- { key: "JumboSpectral", label: "Jumbo Spectral Pack" },
108
- { key: "MegaSpectral", label: "Mega Spectral Pack" },
109
- ];
110
- const BOSS_VALUE_MASK = 0xff;
111
- const ITEM_VALUE_MASK = 0xffff;
112
- export const MOTELY_DISPLAY_SCHEMA = {
113
- bosses: BOSS_ENTRIES,
114
- vouchers: VOUCHER_ENTRIES,
115
- tags: TAG_ENTRIES,
116
- boosterPacks: BOOSTER_PACK_ENTRIES,
117
- };
118
- function createLabelLookup(entries) {
119
- return {
120
- keyToLabel: new Map(entries.map((entry) => [entry.key, entry.label])),
121
- labelToKey: new Map(entries.map((entry) => [entry.label, entry.key])),
122
- };
123
- }
124
- const bossLookup = createLabelLookup(BOSS_ENTRIES);
125
- const voucherLookup = createLabelLookup(VOUCHER_ENTRIES);
126
- const tagLookup = createLabelLookup(TAG_ENTRIES);
127
- const boosterPackLookup = createLabelLookup(BOOSTER_PACK_ENTRIES);
2
+ /**
3
+ * Display-name utilities — thin wrappers over motely-wasm runtime enums.
4
+ * No hand-maintained lookup tables. The enum IS the source of truth.
5
+ */
128
6
  function spaceSplit(value) {
129
7
  return value.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2");
130
8
  }
131
- function displayNameFromKey(lookup, key, fallback) {
132
- return lookup.keyToLabel.get(key) ?? fallback;
133
- }
134
- function keyFromDisplayName(lookup, label) {
135
- return lookup.labelToKey.get(label) ?? null;
136
- }
137
9
  function runtimeEnumKey(enumObject, value) {
138
10
  if (!enumObject || typeof enumObject !== "object")
139
11
  return null;
140
12
  const key = enumObject[String(value)];
141
13
  return typeof key === "string" && key.length > 0 ? key : null;
142
14
  }
143
- export function motelyBossDisplayNameFromKey(key) {
144
- return displayNameFromKey(bossLookup, key, spaceSplit(key));
145
- }
146
- export function motelyVoucherDisplayNameFromKey(key) {
147
- return displayNameFromKey(voucherLookup, key, spaceSplit(key));
148
- }
149
- export function motelyTagDisplayNameFromKey(key) {
150
- return displayNameFromKey(tagLookup, key, spaceSplit(key));
151
- }
152
- export function motelyBoosterPackDisplayNameFromKey(key) {
153
- return displayNameFromKey(boosterPackLookup, key, `${spaceSplit(key)} Pack`);
154
- }
155
- export function motelyItemDisplayNameFromKey(key) {
156
- return getItemDisplayName(key);
157
- }
15
+ // ─── Public API (same signatures as before, zero hand-rolled tables) ────────
158
16
  export function motelyBossDisplayName(value) {
159
- const key = runtimeEnumKey(Motely.MotelyBossBlind, value & BOSS_VALUE_MASK);
160
- return key === null ? `boss#${value}` : motelyBossDisplayNameFromKey(key);
17
+ const key = runtimeEnumKey(Motely.MotelyBossBlind, value & 0xff);
18
+ return key === null ? `boss#${value}` : spaceSplit(key);
19
+ }
20
+ export function motelyBossDisplayNameFromKey(key) {
21
+ return spaceSplit(key);
161
22
  }
162
23
  export function motelyVoucherDisplayName(value) {
163
24
  const key = runtimeEnumKey(Motely.MotelyVoucher, value);
164
- return key === null ? `voucher#${value}` : motelyVoucherDisplayNameFromKey(key);
25
+ return key === null ? `voucher#${value}` : spaceSplit(key);
26
+ }
27
+ export function motelyVoucherDisplayNameFromKey(key) {
28
+ return spaceSplit(key);
165
29
  }
166
30
  export function motelyTagDisplayName(value) {
167
31
  const key = runtimeEnumKey(Motely.MotelyTag, value);
168
- return key === null ? `tag#${value}` : motelyTagDisplayNameFromKey(key);
32
+ return key === null ? `tag#${value}` : spaceSplit(key);
33
+ }
34
+ export function motelyTagDisplayNameFromKey(key) {
35
+ return spaceSplit(key);
169
36
  }
170
37
  export function motelyBoosterPackDisplayName(value) {
171
38
  const key = runtimeEnumKey(Motely.MotelyBoosterPack, value);
172
- return key === null ? `pack#${value}` : motelyBoosterPackDisplayNameFromKey(key);
39
+ return key === null ? `pack#${value}` : spaceSplit(key);
173
40
  }
174
- export function motelyItemDisplayNameFromValue(value) {
175
- const key = runtimeEnumKey(Motely.MotelyItemType, value & ITEM_VALUE_MASK);
176
- return key === null ? `item#${value}` : motelyItemDisplayNameFromKey(key);
177
- }
178
- export function motelyBossKeyFromDisplayName(label) {
179
- return keyFromDisplayName(bossLookup, label);
180
- }
181
- export function motelyVoucherKeyFromDisplayName(label) {
182
- return keyFromDisplayName(voucherLookup, label);
41
+ export function motelyBoosterPackDisplayNameFromKey(key) {
42
+ return `${spaceSplit(key)} Pack`;
183
43
  }
184
- export function motelyTagKeyFromDisplayName(label) {
185
- return keyFromDisplayName(tagLookup, label);
44
+ export function motelyItemDisplayNameFromKey(key) {
45
+ return spaceSplit(key);
186
46
  }
187
- export function motelyBoosterPackKeyFromDisplayName(label) {
188
- return keyFromDisplayName(boosterPackLookup, label);
47
+ export function motelyItemDisplayNameFromValue(value) {
48
+ const key = runtimeEnumKey(Motely.MotelyItemType, value & 0xffff);
49
+ return key === null ? `item#${value}` : spaceSplit(key);
189
50
  }
@@ -1,4 +1,4 @@
1
- import type { SpriteData } from '../sprites/spriteMapper.js';
1
+ import type { MotelySpriteData } from '../decode/motelySprite.js';
2
2
  export declare const CARD_DIMENSIONS: {
3
3
  readonly WIDTH: 0.7;
4
4
  readonly HEIGHT: 0.95;
@@ -13,7 +13,7 @@ export declare const CARD_MAGNET: {
13
13
  readonly LERP_OUT: 10;
14
14
  };
15
15
  export interface Card3DProps {
16
- sprite: SpriteData;
16
+ sprite: MotelySpriteData;
17
17
  position?: [number, number, number];
18
18
  rotation?: [number, number, number];
19
19
  selected?: boolean;
@@ -1,10 +1,9 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useRef, useMemo, useState, useEffect, memo } from 'react';
4
- import { useFrame } from '@react-three/fiber';
3
+ import { useRef, useMemo, useState, memo } from 'react';
4
+ import { useFrame, useLoader } from '@react-three/fiber';
5
5
  import { useSpring, animated } from '@react-spring/three';
6
6
  import * as THREE from 'three';
7
- import { SPRITE_SHEETS } from '../sprites/spriteData.js';
8
7
  export const CARD_DIMENSIONS = { WIDTH: 0.7, HEIGHT: 0.95, DEPTH: 0.02 };
9
8
  export const CARD_MAGNET = {
10
9
  MAX_TILT_X: 0.36,
@@ -14,50 +13,18 @@ export const CARD_MAGNET = {
14
13
  LERP_IN: 18,
15
14
  LERP_OUT: 10,
16
15
  };
17
- const SHEET_KEY_MAP = {
18
- Jokers: 'jokers',
19
- Tarots: 'tarots',
20
- Vouchers: 'vouchers',
21
- Boosters: 'boosters',
22
- Enhancers: 'enhancers',
23
- Editions: 'editions',
24
- BlindChips: 'blinds',
25
- tags: 'tags',
26
- };
27
- const _textureCache = new Map();
28
16
  function useSpriteTexture(sprite) {
29
- const [texture, setTexture] = useState(null);
30
- const serial = useRef(0);
31
- useEffect(() => {
32
- const id = ++serial.current;
33
- const sheet = SPRITE_SHEETS[SHEET_KEY_MAP[sprite.type]];
34
- const url = sheet.src;
35
- const cols = sheet.columns;
36
- const rows = sheet.rows;
37
- const { x, y } = sprite.pos;
38
- const applySlice = (base) => {
39
- const t = base.clone();
40
- t.repeat.set(1 / cols, 1 / rows);
41
- t.offset.set(x / cols, (rows - y - 1) / rows);
42
- t.needsUpdate = true;
43
- return t;
44
- };
45
- if (_textureCache.has(url)) {
46
- setTexture(applySlice(_textureCache.get(url)));
47
- return;
48
- }
49
- const loader = new THREE.TextureLoader();
50
- loader.load(url, (loaded) => {
51
- if (id !== serial.current)
52
- return;
53
- loaded.colorSpace = THREE.SRGBColorSpace;
54
- loaded.magFilter = THREE.NearestFilter;
55
- loaded.minFilter = THREE.NearestFilter;
56
- _textureCache.set(url, loaded);
57
- setTexture(applySlice(loaded));
58
- }, undefined, (err) => console.error('[Card3D] texture load failed:', url, err));
59
- }, [sprite.type, sprite.pos.x, sprite.pos.y]);
60
- return texture;
17
+ const texture = useLoader(THREE.TextureLoader, sprite.atlasPath);
18
+ return useMemo(() => {
19
+ const t = texture.clone();
20
+ t.colorSpace = THREE.SRGBColorSpace;
21
+ t.magFilter = THREE.NearestFilter;
22
+ t.minFilter = THREE.NearestFilter;
23
+ t.repeat.set(1 / sprite.gridCols, 1 / sprite.gridRows);
24
+ t.offset.set(sprite.gridCol / sprite.gridCols, 1 - ((sprite.gridRow + 1) / sprite.gridRows));
25
+ t.needsUpdate = true;
26
+ return t;
27
+ }, [texture, sprite.gridCol, sprite.gridRow, sprite.gridCols, sprite.gridRows]);
61
28
  }
62
29
  export const Card3D = memo(function Card3D({ sprite, position = [0, 0, 0], rotation = [0, 0, 0], selected = false, highlighted = false, onClick, onPointerEnter, onPointerLeave, }) {
63
30
  const tiltRef = useRef(null);
@@ -0,0 +1,10 @@
1
+ import type { MotelySpriteData } from '../decode/motelySprite.js';
2
+ export interface JimboBillboardProps {
3
+ sprite: MotelySpriteData | null;
4
+ label?: string;
5
+ width?: number;
6
+ height?: number;
7
+ yLockOnly?: boolean;
8
+ position?: [number, number, number];
9
+ }
10
+ export declare function JimboBillboard({ sprite, label, width, height, yLockOnly, position }: JimboBillboardProps): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useMemo } from 'react';
3
+ import { Billboard } from '@react-three/drei';
4
+ import { useLoader } from '@react-three/fiber';
5
+ import * as THREE from 'three';
6
+ export function JimboBillboard({ sprite, label, width = 3.4, height = 4.5, yLockOnly = false, position = [0, 0, 0] }) {
7
+ if (!sprite)
8
+ return null;
9
+ // Memoize texture to avoid per-render allocation
10
+ const texture = useLoader(THREE.TextureLoader, sprite.atlasPath);
11
+ const clonedTexture = useMemo(() => {
12
+ const tex = texture.clone();
13
+ tex.magFilter = THREE.NearestFilter;
14
+ tex.minFilter = THREE.NearestFilter;
15
+ // Set up sprite cropping
16
+ tex.repeat.set(1 / sprite.gridCols, 1 / sprite.gridRows);
17
+ tex.offset.set(sprite.gridCol / sprite.gridCols, 1 - ((sprite.gridRow + 1) / sprite.gridRows));
18
+ tex.needsUpdate = true;
19
+ return tex;
20
+ }, [texture, sprite.gridCol, sprite.gridRow, sprite.gridCols, sprite.gridRows]);
21
+ const material = useMemo(() => {
22
+ return new THREE.MeshBasicMaterial({
23
+ map: clonedTexture,
24
+ transparent: true,
25
+ alphaTest: 0.5
26
+ });
27
+ }, [clonedTexture]);
28
+ return (_jsx(Billboard, { lockY: yLockOnly, lockX: false, lockZ: false, position: position, children: _jsx("mesh", { material: material, children: _jsx("planeGeometry", { args: [width, height] }) }) }));
29
+ }
@@ -0,0 +1,9 @@
1
+ export interface JimboText3DProps {
2
+ children: string;
3
+ color?: string;
4
+ outlineColor?: string;
5
+ outlineWidth?: number;
6
+ position?: [number, number, number];
7
+ fontSize?: number;
8
+ }
9
+ export declare function JimboText3D({ children, color, outlineColor, outlineWidth, position, fontSize }: JimboText3DProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Text } from '@react-three/drei';
3
+ import { resolveJamlAssetUrl } from '../assets.js';
4
+ import { JimboColorOption } from '../ui/tokens.js';
5
+ export function JimboText3D({ children, color = JimboColorOption.WHITE, outlineColor = JimboColorOption.BLACK, outlineWidth = 0.05, position = [0, 0, 0], fontSize = 1 }) {
6
+ // We use the m6x11plus font from assets if possible, or fallback
7
+ return (_jsx(Text, { position: position, fontSize: fontSize, color: color, outlineColor: outlineColor, outlineWidth: outlineWidth, font: resolveJamlAssetUrl('font'), anchorX: "center", anchorY: "middle", children: children }));
8
+ }
package/dist/r3f.d.ts CHANGED
@@ -1 +1,3 @@
1
1
  export * from './r3f/Card3D.js';
2
+ export * from './r3f/JimboBillboard.js';
3
+ export * from './r3f/JimboText3D.js';
package/dist/r3f.js CHANGED
@@ -1 +1,3 @@
1
1
  export * from './r3f/Card3D.js';
2
+ export * from './r3f/JimboBillboard.js';
3
+ export * from './r3f/JimboText3D.js';
@@ -1,175 +1,11 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { useEffect, useRef, useState } from "react";
4
- import { SPRITE_SHEETS } from "../sprites/spriteData.js";
5
- function loadImage(url) {
6
- return new Promise((resolve) => {
7
- const image = new window.Image();
8
- image.addEventListener("load", () => {
9
- resolve(image);
10
- });
11
- image.addEventListener("error", () => {
12
- resolve(null);
13
- });
14
- image.src = url;
15
- });
16
- }
17
- function renderImage(canvas, context, image, layer, timestamp) {
18
- if (!image || !layer || !layer?.pos)
19
- return 0;
20
- const cardWidth = image.width / layer.columns;
21
- const cardHeight = image.height / layer.rows;
22
- const canvasStyle = canvas.style;
23
- if (layer.order === 0) {
24
- canvas.width = cardWidth;
25
- canvas.height = cardHeight;
26
- canvasStyle.width = `${cardWidth}px`;
27
- canvasStyle.height = `${cardHeight}px`;
28
- }
29
- canvasStyle.imageRendering = "pixelated";
30
- context.imageSmoothingEnabled = true;
31
- context.save();
32
- if (layer.animated && timestamp) {
33
- const elapsed = timestamp;
34
- const yOffset = Math.sin(elapsed / 1000) * 3;
35
- const xOffset = Math.sin(elapsed / 1500) * 1.5;
36
- context.globalAlpha = 0.65 + (Math.sin(elapsed / 2000) + 1) * 0.075;
37
- context.translate(xOffset, yOffset);
38
- }
39
- context.drawImage(image, layer.pos.x * cardWidth, layer.pos.y * cardHeight, cardWidth, cardHeight, 0, 0, canvas.width, canvas.height);
40
- context.restore();
41
- return cardWidth / cardHeight;
42
- }
3
+ import { useJamlCardRenderer } from "../ui/hooks.js";
43
4
  export function JamlCardRenderer({ layers, invert = false, className = "", hoverTilt = false }) {
44
- const canvasRef = useRef(null);
45
- const imageCacheRef = useRef(new Map());
46
- const [ratio, setRatio] = useState(3 / 4);
47
- const [, forceUpdate] = useState(0);
48
- const animationFrameRef = useRef(null);
49
- const [elapsed, setElapsed] = useState(0);
50
- const [isHovered, setIsHovered] = useState(false);
51
- const [transform, setTransform] = useState("none");
52
- const hasAnimatedLayer = layers?.some((layer) => layer.animated);
53
- useEffect(() => {
54
- let cancelled = false;
55
- const imageCache = imageCacheRef.current;
56
- const preload = async () => {
57
- const urls = Array.from(new Set(Object.values(SPRITE_SHEETS).map((sheet) => sheet.src)));
58
- const images = await Promise.all(urls.map((url) => loadImage(url)));
59
- if (cancelled)
60
- return;
61
- images.forEach((image, index) => {
62
- if (image) {
63
- imageCache.set(urls[index], image);
64
- }
65
- });
66
- forceUpdate((prev) => prev + 1);
67
- };
68
- preload().catch((err) => {
69
- console.error("[JamlCardRenderer]", err);
70
- });
71
- return () => {
72
- cancelled = true;
73
- imageCache.clear();
74
- };
75
- }, []);
76
- useEffect(() => {
77
- if (!hasAnimatedLayer)
78
- return;
79
- let startTime;
80
- const animate = (timestamp) => {
81
- if (!startTime)
82
- startTime = timestamp;
83
- const now = timestamp - startTime;
84
- if (!animationFrameRef.current || timestamp - 100 > animationFrameRef.current) {
85
- animationFrameRef.current = timestamp;
86
- setElapsed(now);
87
- }
88
- animationFrameRef.current = requestAnimationFrame(animate);
89
- };
90
- animationFrameRef.current = requestAnimationFrame(animate);
91
- return () => {
92
- if (animationFrameRef.current) {
93
- cancelAnimationFrame(animationFrameRef.current);
94
- }
95
- };
96
- }, [hasAnimatedLayer]);
97
- useEffect(() => {
98
- if (!canvasRef.current || !layers || layers.length === 0)
99
- return;
100
- const canvas = canvasRef.current;
101
- const context = canvas.getContext("2d");
102
- if (!context)
103
- return;
104
- let cancelled = false;
105
- context.clearRect(0, 0, canvas.width, canvas.height);
106
- [...layers]
107
- .sort((a, b) => a.order - b.order)
108
- .forEach((layer) => {
109
- if (imageCacheRef.current.has(layer.source)) {
110
- const image = imageCacheRef.current.get(layer.source);
111
- if (!image)
112
- return;
113
- const imageRatio = renderImage(canvas, context, image, layer, hasAnimatedLayer ? elapsed : undefined);
114
- if (layer.order === 0) {
115
- setRatio(imageRatio);
116
- }
117
- return;
118
- }
119
- loadImage(layer.source).then((img) => {
120
- if (cancelled || !img)
121
- return;
122
- const imageRatio = renderImage(canvas, context, img, layer, hasAnimatedLayer ? elapsed : undefined);
123
- imageCacheRef.current.set(layer.source, img);
124
- if (layer.order === 0) {
125
- setRatio(imageRatio);
126
- }
127
- forceUpdate((prev) => prev + 1);
128
- });
129
- });
130
- if (invert) {
131
- canvas.style.filter = "invert(0.94)";
132
- }
133
- else {
134
- canvas.style.filter = "none";
135
- }
136
- return () => { cancelled = true; };
137
- }, [layers, elapsed, invert, hasAnimatedLayer]);
138
- const handlePointerEnter = (event) => {
139
- if (!hoverTilt || event.pointerType === "touch")
140
- return;
141
- setIsHovered(true);
142
- };
143
- const handlePointerLeave = () => {
144
- if (!hoverTilt)
145
- return;
146
- setIsHovered(false);
147
- setTransform("none");
148
- };
149
- const handlePointerMove = (event) => {
150
- if (!hoverTilt || event.pointerType === "touch")
151
- return;
152
- const rect = event.currentTarget.getBoundingClientRect();
153
- const x = event.clientX - rect.left;
154
- const y = event.clientY - rect.top;
155
- const rotateY = (x / rect.width) * 12 - 6;
156
- const rotateX = (y / rect.height) * -16 + 8;
157
- setTransform(`perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) translateZ(10px)`);
158
- };
159
- const containerStyle = {
160
- aspectRatio: String(ratio),
161
- width: "100%",
162
- display: "flex",
163
- transition: hoverTilt && !isHovered ? "transform 0.4s ease" : undefined,
164
- transform: hoverTilt ? (isHovered ? transform : "none") : undefined,
165
- transformStyle: hoverTilt ? "preserve-3d" : undefined,
166
- transformOrigin: hoverTilt ? "center center" : undefined,
167
- };
168
- const canvasStyle = {
169
- borderRadius: "6px",
170
- boxShadow: hoverTilt && isHovered ? "0 2px 12px rgba(0,0,0,0.3)" : "0 2px 8px rgba(0,0,0,0.2)",
171
- imageRendering: "pixelated",
172
- transition: hoverTilt && !isHovered ? "box-shadow 0.4s ease-out" : undefined,
173
- };
174
- return (_jsx("div", { className: className, style: containerStyle, onPointerEnter: hoverTilt ? handlePointerEnter : undefined, onPointerLeave: hoverTilt ? handlePointerLeave : undefined, onPointerMove: hoverTilt ? handlePointerMove : undefined, children: _jsx("canvas", { ref: canvasRef, style: canvasStyle }) }));
5
+ const { canvasRef, containerStyle, canvasStyle, handlers } = useJamlCardRenderer({
6
+ layers,
7
+ invert,
8
+ hoverTilt
9
+ });
10
+ return (_jsx("div", { className: className, style: containerStyle, ...handlers, children: _jsx("canvas", { ref: canvasRef, style: canvasStyle }) }));
175
11
  }
@@ -32,6 +32,7 @@ export declare const SPRITE_SHEETS: {
32
32
  readonly vouchers: SpriteSheetInfo;
33
33
  readonly tags: SpriteSheetInfo;
34
34
  readonly boosters: SpriteSheetInfo;
35
+ readonly stakes: SpriteSheetInfo;
35
36
  };
36
37
  export declare const JOKERS: SpriteEntry[];
37
38
  export declare const JOKER_FACES: SpriteEntry[];
@@ -27,6 +27,7 @@ export const SPRITE_SHEETS = {
27
27
  vouchers: defineSpriteSheet("vouchers", 9, 4),
28
28
  tags: defineSpriteSheet("tags", 6, 5),
29
29
  boosters: defineSpriteSheet("boosters", 4, 9),
30
+ stakes: defineSpriteSheet("stakes", 5, 2),
30
31
  };
31
32
  export const JOKERS = [
32
33
  { name: "Joker", pos: { x: 0, y: 0 } }, { name: "Greedy Joker", pos: { x: 6, y: 1 } }, { name: "Lusty Joker", pos: { x: 7, y: 1 } }, { name: "Wrathful Joker", pos: { x: 8, y: 1 } }, { name: "Gluttonous Joker", pos: { x: 9, y: 1 } }, { name: "Jolly Joker", pos: { x: 2, y: 0 } }, { name: "Zany Joker", pos: { x: 3, y: 0 } }, { name: "Mad Joker", pos: { x: 4, y: 0 } }, { name: "Crazy Joker", pos: { x: 5, y: 0 } }, { name: "Droll Joker", pos: { x: 6, y: 0 } }, { name: "Sly Joker", pos: { x: 0, y: 14 } }, { name: "Wily Joker", pos: { x: 1, y: 14 } }, { name: "Clever Joker", pos: { x: 2, y: 14 } }, { name: "Devious Joker", pos: { x: 3, y: 14 } }, { name: "Crafty Joker", pos: { x: 4, y: 14 } }, { name: "Half Joker", pos: { x: 7, y: 0 } }, { name: "Joker Stencil", pos: { x: 2, y: 5 } }, { name: "Four Fingers", pos: { x: 6, y: 6 } }, { name: "Mime", pos: { x: 4, y: 1 } }, { name: "Credit Card", pos: { x: 5, y: 1 } }, { name: "Ceremonial Dagger", pos: { x: 5, y: 5 } }, { name: "Banner", pos: { x: 1, y: 2 } }, { name: "Mystic Summit", pos: { x: 2, y: 2 } }, { name: "Marble Joker", pos: { x: 3, y: 2 } }, { name: "Loyalty Card", pos: { x: 4, y: 2 } }, { name: "8 Ball", pos: { x: 0, y: 5 } }, { name: "Misprint", pos: { x: 6, y: 2 } }, { name: "Dusk", pos: { x: 4, y: 7 } }, { name: "Raised Fist", pos: { x: 8, y: 2 } }, { name: "Chaos the Clown", pos: { x: 1, y: 0 } }, { name: "Fibonacci", pos: { x: 1, y: 5 } }, { name: "Steel Joker", pos: { x: 7, y: 2 } }, { name: "Scary Face", pos: { x: 2, y: 3 } }, { name: "Abstract Joker", pos: { x: 3, y: 3 } }, { name: "Delayed Gratification", pos: { x: 4, y: 3 } }, { name: "Hack", pos: { x: 5, y: 2 } }, { name: "Pareidolia", pos: { x: 6, y: 3 } }, { name: "Gros Michel", pos: { x: 7, y: 6 } }, { name: "Even Steven", pos: { x: 8, y: 3 } }, { name: "Odd Todd", pos: { x: 9, y: 3 } }, { name: "Scholar", pos: { x: 3, y: 6 } }, { name: "Business Card", pos: { x: 1, y: 4 } }, { name: "Supernova", pos: { x: 2, y: 4 } }, { name: "Ride the Bus", pos: { x: 1, y: 6 } }, { name: "Space Joker", pos: { x: 3, y: 5 } }, { name: "Egg", pos: { x: 0, y: 10 } }, { name: "Burglar", pos: { x: 1, y: 10 } }, { name: "Blackboard", pos: { x: 2, y: 10 } }, { name: "Runner", pos: { x: 3, y: 10 } }, { name: "Ice Cream", pos: { x: 4, y: 10 } }, { name: "DNA", pos: { x: 5, y: 10 } }, { name: "Splash", pos: { x: 6, y: 10 } }, { name: "Blue Joker", pos: { x: 7, y: 10 } }, { name: "Sixth Sense", pos: { x: 8, y: 10 } }, { name: "Constellation", pos: { x: 9, y: 10 } }, { name: "Hiker", pos: { x: 0, y: 11 } }, { name: "Faceless Joker", pos: { x: 1, y: 11 } }, { name: "Green Joker", pos: { x: 2, y: 11 } }, { name: "Superposition", pos: { x: 3, y: 11 } }, { name: "To Do List", pos: { x: 4, y: 11 } }, { name: "Cavendish", pos: { x: 5, y: 11 } }, { name: "Card Sharp", pos: { x: 6, y: 11 } }, { name: "Red Card", pos: { x: 7, y: 11 } }, { name: "Madness", pos: { x: 8, y: 11 } }, { name: "Square Joker", pos: { x: 9, y: 11 } }, { name: "Seance", pos: { x: 0, y: 12 } }, { name: "Riff-raff", pos: { x: 1, y: 12 } }, { name: "Vampire", pos: { x: 2, y: 12 } }, { name: "Shortcut", pos: { x: 3, y: 12 } }, { name: "Hologram", pos: { x: 4, y: 12 } }, { name: "Vagabond", pos: { x: 5, y: 12 } }, { name: "Baron", pos: { x: 6, y: 12 } }, { name: "Cloud 9", pos: { x: 7, y: 12 } }, { name: "Rocket", pos: { x: 8, y: 12 } }, { name: "Obelisk", pos: { x: 9, y: 12 } }, { name: "Midas Mask", pos: { x: 0, y: 13 } }, { name: "Luchador", pos: { x: 1, y: 13 } }, { name: "Photograph", pos: { x: 2, y: 13 } }, { name: "Gift Card", pos: { x: 3, y: 13 } }, { name: "Turtle Bean", pos: { x: 4, y: 13 } }, { name: "Erosion", pos: { x: 5, y: 13 } }, { name: "Reserved Parking", pos: { x: 6, y: 13 } }, { name: "Mail In Rebate", pos: { x: 7, y: 13 } }, { name: "To the Moon", pos: { x: 8, y: 13 } }, { name: "Hallucination", pos: { x: 9, y: 13 } }, { name: "Fortune Teller", pos: { x: 7, y: 5 } }, { name: "Juggler", pos: { x: 0, y: 1 } }, { name: "Drunkard", pos: { x: 1, y: 1 } }, { name: "Stone Joker", pos: { x: 9, y: 0 } }, { name: "Golden Joker", pos: { x: 9, y: 2 } }, { name: "Lucky Cat", pos: { x: 5, y: 14 } }, { name: "Baseball Card", pos: { x: 6, y: 14 } }, { name: "Bull", pos: { x: 7, y: 14 } }, { name: "Diet Cola", pos: { x: 8, y: 14 } }, { name: "Trading Card", pos: { x: 9, y: 14 } }, { name: "Flash Card", pos: { x: 0, y: 15 } }, { name: "Popcorn", pos: { x: 1, y: 15 } }, { name: "Spare Trousers", pos: { x: 4, y: 15 } }, { name: "Ancient Joker", pos: { x: 7, y: 15 } }, { name: "Ramen", pos: { x: 2, y: 15 } }, { name: "Walkie Talkie", pos: { x: 8, y: 15 } }, { name: "Seltzer", pos: { x: 3, y: 15 } }, { name: "Castle", pos: { x: 9, y: 15 } }, { name: "Smiley Face", pos: { x: 6, y: 15 } }, { name: "Campfire", pos: { x: 5, y: 15 } }, { name: "Golden Ticket", pos: { x: 5, y: 3 } }, { name: "Mr. Bones", pos: { x: 3, y: 4 } }, { name: "Acrobat", pos: { x: 2, y: 1 } }, { name: "Sock and Buskin", pos: { x: 3, y: 1 } }, { name: "Swashbuckler", pos: { x: 9, y: 5 } }, { name: "Troubadour", pos: { x: 0, y: 2 } }, { name: "Certificate", pos: { x: 8, y: 8 } }, { name: "Smeared Joker", pos: { x: 4, y: 6 } }, { name: "Throwback", pos: { x: 5, y: 7 } }, { name: "Hanging Chad", pos: { x: 9, y: 6 } }, { name: "Rough Gem", pos: { x: 9, y: 7 } }, { name: "Bloodstone", pos: { x: 0, y: 8 } }, { name: "Arrowhead", pos: { x: 1, y: 8 } }, { name: "Onyx Agate", pos: { x: 2, y: 8 } }, { name: "Glass Joker", pos: { x: 1, y: 3 } }, { name: "Showman", pos: { x: 6, y: 5 } }, { name: "Flower Pot", pos: { x: 0, y: 6 } }, { name: "Blueprint", pos: { x: 0, y: 3 } }, { name: "Wee Joker", pos: { x: 0, y: 4 } }, { name: "Merry Andy", pos: { x: 8, y: 0 } }, { name: "Oops! All 6s", pos: { x: 5, y: 6 } }, { name: "The Idol", pos: { x: 6, y: 7 } }, { name: "Seeing Double", pos: { x: 4, y: 4 } }, { name: "Matador", pos: { x: 4, y: 5 } }, { name: "Hit the Road", pos: { x: 8, y: 5 } }, { name: "The Duo", pos: { x: 5, y: 4 } }, { name: "The Trio", pos: { x: 6, y: 4 } }, { name: "The Family", pos: { x: 7, y: 4 } }, { name: "The Order", pos: { x: 8, y: 4 } }, { name: "The Tribe", pos: { x: 9, y: 4 } }, { name: "Stuntman", pos: { x: 8, y: 6 } }, { name: "Invisible Joker", pos: { x: 1, y: 7 } }, { name: "Brainstorm", pos: { x: 7, y: 7 } }, { name: "Satellite", pos: { x: 8, y: 7 } }, { name: "Shoot the Moon", pos: { x: 2, y: 6 } }, { name: "Drivers License", pos: { x: 0, y: 7 } }, { name: "Cartomancer", pos: { x: 7, y: 3 } }, { name: "Astronomer", pos: { x: 2, y: 7 } }, { name: "Burnt Joker", pos: { x: 3, y: 7 } }, { name: "Bootstraps", pos: { x: 9, y: 8 } }, { name: "Canio", pos: { x: 3, y: 8 } }, { name: "Triboulet", pos: { x: 4, y: 8 } }, { name: "Yorick", pos: { x: 5, y: 8 } }, { name: "Chicot", pos: { x: 6, y: 8 } }, { name: "Perkeo", pos: { x: 7, y: 8 } },