@aethervtt/shard-sprite 0.0.1
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/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/atlas.d.ts +112 -0
- package/dist/atlas.js +511 -0
- package/dist/clip.d.ts +73 -0
- package/dist/clip.js +278 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +186 -0
- package/dist/lighting-shaders.d.ts +5 -0
- package/dist/lighting-shaders.js +267 -0
- package/dist/lighting.d.ts +44 -0
- package/dist/lighting.js +149 -0
- package/dist/lights2d.d.ts +203 -0
- package/dist/lights2d.js +1093 -0
- package/dist/occluders.d.ts +64 -0
- package/dist/occluders.js +241 -0
- package/dist/outline.d.ts +24 -0
- package/dist/outline.js +183 -0
- package/dist/plugin.d.ts +6 -0
- package/dist/plugin.js +67 -0
- package/dist/preview.d.ts +9 -0
- package/dist/preview.js +71 -0
- package/dist/render.d.ts +202 -0
- package/dist/render.js +1216 -0
- package/dist/shaders.d.ts +2 -0
- package/dist/shaders.js +263 -0
- package/dist/sprite.d.ts +26 -0
- package/dist/sprite.js +67 -0
- package/dist/tilemap.d.ts +94 -0
- package/dist/tilemap.js +267 -0
- package/package.json +39 -0
package/dist/tilemap.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { AssetStore, defineAssetType, defineDataAsset } from "@aethervtt/shard-assets";
|
|
2
|
+
import {
|
|
3
|
+
defineComponent,
|
|
4
|
+
defineResource,
|
|
5
|
+
defineSchema,
|
|
6
|
+
ShardError,
|
|
7
|
+
t
|
|
8
|
+
} from "@aethervtt/shard-core";
|
|
9
|
+
import { Visibility } from "@aethervtt/shard-render";
|
|
10
|
+
import { Transform } from "@aethervtt/shard-transform";
|
|
11
|
+
const TileFlags = { FlipX: 1, FlipY: 2, Rotate90: 4 };
|
|
12
|
+
const TilemapDataSchema = defineSchema(
|
|
13
|
+
"sprite/TilemapData",
|
|
14
|
+
{
|
|
15
|
+
layers: t.list(
|
|
16
|
+
t.struct({
|
|
17
|
+
name: t.string({ description: 'Layer name, e.g. "ground".' }),
|
|
18
|
+
width: t.u32({ min: 1, description: "Tiles across." }),
|
|
19
|
+
height: t.u32({ min: 1, description: "Tiles down." }),
|
|
20
|
+
tiles: t.string({
|
|
21
|
+
description: "Base64 of width \xD7 height little-endian u16s, row by row from the top: atlas region + 1 (0 is empty)."
|
|
22
|
+
}),
|
|
23
|
+
flags: t.string({
|
|
24
|
+
description: "Base64 of width \xD7 height bytes: 1 flip x, 2 flip y, 4 rotate 90\xB0. Empty: none."
|
|
25
|
+
}),
|
|
26
|
+
occludes: t.bool({
|
|
27
|
+
description: "Filled cells block 2D light (shadowed PointLight2d and SpotLight2d)."
|
|
28
|
+
})
|
|
29
|
+
}),
|
|
30
|
+
{ description: "Layers, drawn in order." }
|
|
31
|
+
),
|
|
32
|
+
animations: t.list(
|
|
33
|
+
t.struct({
|
|
34
|
+
tile: t.u16({ min: 1, description: "The tile that animates, everywhere it appears." }),
|
|
35
|
+
frames: t.list(t.u16, { description: "Tiles it cycles through." }),
|
|
36
|
+
frameTime: t.f32({
|
|
37
|
+
default: 0.2,
|
|
38
|
+
min: 1e-3,
|
|
39
|
+
unit: "s",
|
|
40
|
+
description: "Seconds per frame."
|
|
41
|
+
})
|
|
42
|
+
}),
|
|
43
|
+
{ description: "Animated tiles: water, torches, conveyor belts." }
|
|
44
|
+
)
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
description: "Tile layers for a Tilemap: indices into its atlas, per-tile flags, animated tiles."
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
const MAX_EDITS = 4096;
|
|
51
|
+
class TileLayer {
|
|
52
|
+
name;
|
|
53
|
+
width;
|
|
54
|
+
height;
|
|
55
|
+
tiles;
|
|
56
|
+
flags;
|
|
57
|
+
/** Tile indices (y · width + x) edited since `editBase`: renderers upload their chunks. */
|
|
58
|
+
edits = [];
|
|
59
|
+
/** Bumps when the edit log overflows: renderers re-upload everything. */
|
|
60
|
+
editBase = 0;
|
|
61
|
+
/** Filled cells block 2D light. */
|
|
62
|
+
occludes = false;
|
|
63
|
+
constructor(name, width, height, tiles, flags) {
|
|
64
|
+
this.name = name;
|
|
65
|
+
this.width = width;
|
|
66
|
+
this.height = height;
|
|
67
|
+
this.tiles = tiles ?? new Uint16Array(width * height);
|
|
68
|
+
this.flags = flags ?? new Uint8Array(width * height);
|
|
69
|
+
}
|
|
70
|
+
get(x, y) {
|
|
71
|
+
if (x < 0 || y < 0 || x >= this.width || y >= this.height) return 0;
|
|
72
|
+
return this.tiles[y * this.width + x];
|
|
73
|
+
}
|
|
74
|
+
set(x, y, tile, flags = 0) {
|
|
75
|
+
if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
|
|
76
|
+
throw new ShardError(
|
|
77
|
+
"sprite/tile-out-of-range",
|
|
78
|
+
`Tile (${x}, ${y}) is outside the ${this.width}\xD7${this.height} layer "${this.name}"`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const i = y * this.width + x;
|
|
82
|
+
if (this.tiles[i] === tile && this.flags[i] === flags) return;
|
|
83
|
+
this.tiles[i] = tile;
|
|
84
|
+
this.flags[i] = flags;
|
|
85
|
+
if (this.edits.length >= MAX_EDITS) {
|
|
86
|
+
this.edits.length = 0;
|
|
87
|
+
this.editBase++;
|
|
88
|
+
} else {
|
|
89
|
+
this.edits.push(i);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
class TilemapData {
|
|
94
|
+
layers;
|
|
95
|
+
animations;
|
|
96
|
+
/** Bumps when the layers are replaced (hot reload). */
|
|
97
|
+
version = 0;
|
|
98
|
+
constructor(layers = [], animations = []) {
|
|
99
|
+
this.layers = layers;
|
|
100
|
+
this.animations = animations;
|
|
101
|
+
}
|
|
102
|
+
/** An empty map of `layers` layers, width × height tiles each. */
|
|
103
|
+
static create(width, height, layers = ["main"]) {
|
|
104
|
+
return new TilemapData(layers.map((name) => new TileLayer(name, width, height)));
|
|
105
|
+
}
|
|
106
|
+
static fromJson(json) {
|
|
107
|
+
const v = TilemapDataSchema.deserialize(json);
|
|
108
|
+
const layers = v.layers.map((l, i) => {
|
|
109
|
+
const n = l.width * l.height;
|
|
110
|
+
const tileBytes = fromBase64(l.tiles);
|
|
111
|
+
if (tileBytes.length !== n * 2) {
|
|
112
|
+
throw new ShardError(
|
|
113
|
+
"sprite/invalid-tilemap",
|
|
114
|
+
`Layer "${l.name}" has ${tileBytes.length / 2} tiles; ${l.width}\xD7${l.height} needs ${n}`,
|
|
115
|
+
{ path: `/layers/${i}/tiles` }
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const tiles = new Uint16Array(n);
|
|
119
|
+
const view = new DataView(tileBytes.buffer, tileBytes.byteOffset, tileBytes.byteLength);
|
|
120
|
+
for (let k = 0; k < n; k++) tiles[k] = view.getUint16(k * 2, true);
|
|
121
|
+
let flags;
|
|
122
|
+
if (l.flags) {
|
|
123
|
+
flags = fromBase64(l.flags);
|
|
124
|
+
if (flags.length !== n) {
|
|
125
|
+
throw new ShardError(
|
|
126
|
+
"sprite/invalid-tilemap",
|
|
127
|
+
`Layer "${l.name}" has ${flags.length} flag bytes; ${l.width}\xD7${l.height} needs ${n}`,
|
|
128
|
+
{ path: `/layers/${i}/flags` }
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const layer = new TileLayer(l.name, l.width, l.height, tiles, flags);
|
|
133
|
+
layer.occludes = l.occludes;
|
|
134
|
+
return layer;
|
|
135
|
+
});
|
|
136
|
+
return new TilemapData(layers, v.animations);
|
|
137
|
+
}
|
|
138
|
+
toJson() {
|
|
139
|
+
return {
|
|
140
|
+
layers: this.layers.map((l) => {
|
|
141
|
+
const bytes = new Uint8Array(l.tiles.length * 2);
|
|
142
|
+
const view = new DataView(bytes.buffer);
|
|
143
|
+
for (let k = 0; k < l.tiles.length; k++) view.setUint16(k * 2, l.tiles[k], true);
|
|
144
|
+
return {
|
|
145
|
+
name: l.name,
|
|
146
|
+
width: l.width,
|
|
147
|
+
height: l.height,
|
|
148
|
+
tiles: toBase64(bytes),
|
|
149
|
+
flags: l.flags.some((f) => f !== 0) ? toBase64(l.flags) : "",
|
|
150
|
+
...l.occludes ? { occludes: true } : {}
|
|
151
|
+
};
|
|
152
|
+
}),
|
|
153
|
+
animations: this.animations
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** The largest tile index any layer uses (for validating against an atlas). */
|
|
157
|
+
maxTile() {
|
|
158
|
+
let max = 0;
|
|
159
|
+
for (const l of this.layers)
|
|
160
|
+
for (let k = 0; k < l.tiles.length; k++) max = Math.max(max, l.tiles[k]);
|
|
161
|
+
return max;
|
|
162
|
+
}
|
|
163
|
+
copyFrom(other) {
|
|
164
|
+
this.layers = other.layers;
|
|
165
|
+
this.animations = other.animations;
|
|
166
|
+
this.version++;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function fromBase64(s) {
|
|
170
|
+
const bin = atob(s);
|
|
171
|
+
const out = new Uint8Array(bin.length);
|
|
172
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
function toBase64(bytes) {
|
|
176
|
+
let s = "";
|
|
177
|
+
for (let i = 0; i < bytes.length; i += 32768) {
|
|
178
|
+
s += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
179
|
+
}
|
|
180
|
+
return btoa(s);
|
|
181
|
+
}
|
|
182
|
+
class TilemapDataStore extends AssetStore {
|
|
183
|
+
constructor() {
|
|
184
|
+
super("TilemapData");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const TilemapDatas = defineResource("sprite/TilemapDatas", {
|
|
188
|
+
description: "Loaded tilemap data by guid.",
|
|
189
|
+
init: () => new TilemapDataStore()
|
|
190
|
+
});
|
|
191
|
+
const TilemapDataAssetType = defineAssetType("TilemapData", {
|
|
192
|
+
store: TilemapDatas,
|
|
193
|
+
load: (artifact) => TilemapData.fromJson(artifact.json),
|
|
194
|
+
update: (existing, next) => existing.copyFrom(next)
|
|
195
|
+
});
|
|
196
|
+
const TilemapDataImporter = defineDataAsset("TilemapData", TilemapDataSchema, {
|
|
197
|
+
extension: "tilemap"
|
|
198
|
+
});
|
|
199
|
+
const Tilemap = defineComponent(
|
|
200
|
+
"sprite/Tilemap",
|
|
201
|
+
{
|
|
202
|
+
atlas: t.handle("TextureAtlas", { description: "Tile N draws region N \u2212 1." }),
|
|
203
|
+
data: t.handle("TilemapData", { description: "The tile layers." }),
|
|
204
|
+
tileSize: t.vec2({
|
|
205
|
+
default: [1, 1],
|
|
206
|
+
min: 0,
|
|
207
|
+
unit: "m",
|
|
208
|
+
description: "World size of one tile."
|
|
209
|
+
}),
|
|
210
|
+
chunkSize: t.u16({
|
|
211
|
+
default: 32,
|
|
212
|
+
min: 4,
|
|
213
|
+
max: 256,
|
|
214
|
+
description: "Tiles per chunk side: chunks are the unit of culling and re-upload."
|
|
215
|
+
}),
|
|
216
|
+
layer: t.i16({
|
|
217
|
+
description: "Draw-order band, like Sprite.layer (tilemaps draw first in a band)."
|
|
218
|
+
}),
|
|
219
|
+
lit: t.bool({
|
|
220
|
+
default: true,
|
|
221
|
+
description: "Lit by 2D lights under a Lighting2d camera."
|
|
222
|
+
})
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
description: "Tile layers drawn over the entity's XY plane: tile (x, y) covers [x, x+1] \xD7 [\u2212y\u22121, \u2212y] tile sizes, rows going down from the top.",
|
|
226
|
+
requires: [Transform, Visibility]
|
|
227
|
+
}
|
|
228
|
+
);
|
|
229
|
+
function layerOf(world, tilemap, layer) {
|
|
230
|
+
const value = world.get(tilemap, Tilemap);
|
|
231
|
+
if (!value) {
|
|
232
|
+
throw new ShardError("sprite/not-a-tilemap", `Entity ${tilemap} has no Tilemap`);
|
|
233
|
+
}
|
|
234
|
+
const data = world.resource(TilemapDatas).get(value.data);
|
|
235
|
+
if (!data) {
|
|
236
|
+
throw new ShardError("sprite/tilemap-not-loaded", `Tilemap ${tilemap} has no loaded data`, {
|
|
237
|
+
hint: "Wait for its TilemapData asset, or create one with TilemapData.create."
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
const l = typeof layer === "number" ? data.layers[layer] : data.layers.find((x) => x.name === layer);
|
|
241
|
+
if (!l) {
|
|
242
|
+
throw new ShardError(
|
|
243
|
+
"sprite/no-layer",
|
|
244
|
+
`Tilemap ${tilemap} has no layer ${JSON.stringify(layer)}`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return l;
|
|
248
|
+
}
|
|
249
|
+
function setTile(world, tilemap, x, y, tile, options = {}) {
|
|
250
|
+
layerOf(world, tilemap, options.layer ?? 0).set(x, y, tile, options.flags ?? 0);
|
|
251
|
+
}
|
|
252
|
+
function tileAt(world, tilemap, x, y, layer = 0) {
|
|
253
|
+
return layerOf(world, tilemap, layer).get(x, y);
|
|
254
|
+
}
|
|
255
|
+
export {
|
|
256
|
+
TileFlags,
|
|
257
|
+
TileLayer,
|
|
258
|
+
Tilemap,
|
|
259
|
+
TilemapData,
|
|
260
|
+
TilemapDataAssetType,
|
|
261
|
+
TilemapDataImporter,
|
|
262
|
+
TilemapDataSchema,
|
|
263
|
+
TilemapDataStore,
|
|
264
|
+
TilemapDatas,
|
|
265
|
+
setTile,
|
|
266
|
+
tileAt
|
|
267
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aethervtt/shard-sprite",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"LICENSE",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@aethervtt/shard-assets": "0.0.1",
|
|
20
|
+
"@aethervtt/shard-core": "0.0.1",
|
|
21
|
+
"@aethervtt/shard-gpu": "0.0.1",
|
|
22
|
+
"@aethervtt/shard-render": "0.0.1",
|
|
23
|
+
"@aethervtt/shard-runtime": "0.0.1",
|
|
24
|
+
"@aethervtt/shard-texture": "0.0.1",
|
|
25
|
+
"@aethervtt/shard-transform": "0.0.1"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/aethervtt/shard.git",
|
|
30
|
+
"directory": "packages/sprite"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public",
|
|
34
|
+
"provenance": true
|
|
35
|
+
},
|
|
36
|
+
"shard": {
|
|
37
|
+
"buildSha": "d6c5c6aef4675575c878b02cf2c8f738b4190229"
|
|
38
|
+
}
|
|
39
|
+
}
|