initial commit

This commit is contained in:
Rowan 2025-03-12 11:55:58 -05:00
commit 102200541d
12 changed files with 1540 additions and 0 deletions

17
index.html Normal file
View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>HTML 5 Boilerplate</title>
<link rel="stylesheet" href="public/css/style.css">
</head>
<body>
<main>
<canvas></canvas>
</main>
<script type="module" src="src/index.js"></script>
</body>
</html>

13
public/css/style.css Normal file
View file

@ -0,0 +1,13 @@
html, body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
main canvas {
aspect-ratio: 16 / 9;
width: min(100%, 100vh * 16 / 9);
height: 100%;
}

852
public/vendor/bitecs/index.js vendored Normal file
View file

@ -0,0 +1,852 @@
// src/core/utils/defineHiddenProperty.ts
var defineHiddenProperty = (obj, key, value) => Object.defineProperty(obj, key, {
value,
enumerable: false,
writable: true,
configurable: true
});
// src/core/EntityIndex.ts
var getId = (index, id) => id & index.entityMask;
var getVersion = (index, id) => id >>> index.versionShift & (1 << index.versionBits) - 1;
var incrementVersion = (index, id) => {
const currentVersion = getVersion(index, id);
const newVersion = currentVersion + 1 & (1 << index.versionBits) - 1;
return id & index.entityMask | newVersion << index.versionShift;
};
var withVersioning = (versionBits) => ({
versioning: true,
versionBits
});
var createEntityIndex = (options) => {
const config = options ? typeof options === "function" ? options() : options : { versioning: false, versionBits: 8 };
const versionBits = config.versionBits ?? 8;
const versioning = config.versioning ?? false;
const entityBits = 32 - versionBits;
const entityMask = (1 << entityBits) - 1;
const versionShift = entityBits;
const versionMask = (1 << versionBits) - 1 << versionShift;
return {
aliveCount: 0,
dense: [],
sparse: [],
maxId: 0,
versioning,
versionBits,
entityMask,
versionShift,
versionMask
};
};
var addEntityId = (index) => {
if (index.aliveCount < index.dense.length) {
const recycledId = index.dense[index.aliveCount];
const entityId = recycledId;
index.sparse[entityId] = index.aliveCount;
index.aliveCount++;
return recycledId;
}
const id = ++index.maxId;
index.dense.push(id);
index.sparse[id] = index.aliveCount;
index.aliveCount++;
return id;
};
var removeEntityId = (index, id) => {
const denseIndex = index.sparse[id];
if (denseIndex === void 0 || denseIndex >= index.aliveCount) {
return;
}
const lastIndex = index.aliveCount - 1;
const lastId = index.dense[lastIndex];
index.sparse[lastId] = denseIndex;
index.dense[denseIndex] = lastId;
index.sparse[id] = lastIndex;
index.dense[lastIndex] = id;
if (index.versioning) {
const newId = incrementVersion(index, id);
index.dense[lastIndex] = newId;
}
index.aliveCount--;
};
var isEntityIdAlive = (index, id) => {
const entityId = getId(index, id);
const denseIndex = index.sparse[entityId];
return denseIndex !== void 0 && denseIndex < index.aliveCount && index.dense[denseIndex] === id;
};
// src/core/World.ts
var $internal = Symbol.for("bitecs_internal");
var createBaseWorld = (context, entityIndex) => defineHiddenProperty(context || {}, $internal, {
entityIndex: entityIndex || createEntityIndex(),
entityMasks: [[]],
entityComponents: /* @__PURE__ */ new Map(),
bitflag: 1,
componentMap: /* @__PURE__ */ new Map(),
componentCount: 0,
queries: /* @__PURE__ */ new Set(),
queriesHashMap: /* @__PURE__ */ new Map(),
notQueries: /* @__PURE__ */ new Set(),
dirtyQueries: /* @__PURE__ */ new Set(),
entitiesWithRelations: /* @__PURE__ */ new Set()
});
function createWorld(...args) {
let entityIndex;
let context;
args.forEach((arg) => {
if (typeof arg === "object" && "add" in arg && "remove" in arg) {
entityIndex = arg;
} else if (typeof arg === "object") {
context = arg;
}
});
return createBaseWorld(context, entityIndex);
}
var resetWorld = (world) => {
const ctx = world[$internal];
ctx.entityIndex = createEntityIndex();
ctx.entityMasks = [[]];
ctx.entityComponents = /* @__PURE__ */ new Map();
ctx.bitflag = 1;
ctx.componentMap = /* @__PURE__ */ new Map();
ctx.componentCount = 0;
ctx.queries = /* @__PURE__ */ new Set();
ctx.queriesHashMap = /* @__PURE__ */ new Map();
ctx.notQueries = /* @__PURE__ */ new Set();
ctx.dirtyQueries = /* @__PURE__ */ new Set();
ctx.entitiesWithRelations = /* @__PURE__ */ new Set();
return world;
};
var deleteWorld = (world) => {
delete world[$internal];
};
var getWorldComponents = (world) => Object.keys(world[$internal].componentMap);
var getAllEntities = (world) => Array.from(world[$internal].entityComponents.keys());
// src/core/utils/SparseSet.ts
var createSparseSet = () => {
const dense = [];
const sparse = [];
const has = (val) => dense[sparse[val]] === val;
const add = (val) => {
if (has(val)) return;
sparse[val] = dense.push(val) - 1;
};
const remove = (val) => {
if (!has(val)) return;
const index = sparse[val];
const swapped = dense.pop();
if (swapped !== val) {
dense[index] = swapped;
sparse[swapped] = index;
}
};
const reset = () => {
dense.length = 0;
sparse.length = 0;
};
return {
add,
remove,
has,
sparse,
dense,
reset
};
};
var SharedArrayBufferOrArrayBuffer = typeof SharedArrayBuffer !== "undefined" ? SharedArrayBuffer : ArrayBuffer;
var createUint32SparseSet = (initialCapacity = 1e3) => {
const sparse = [];
let length = 0;
let dense = new Uint32Array(new SharedArrayBufferOrArrayBuffer(initialCapacity * 4));
const has = (val) => val < sparse.length && sparse[val] < length && dense[sparse[val]] === val;
const add = (val) => {
if (has(val)) return;
if (length >= dense.length) {
const newDense = new Uint32Array(new SharedArrayBufferOrArrayBuffer(dense.length * 2 * 4));
newDense.set(dense);
dense = newDense;
}
dense[length] = val;
sparse[val] = length;
length++;
};
const remove = (val) => {
if (!has(val)) return;
length--;
const index = sparse[val];
const swapped = dense[length];
dense[index] = swapped;
sparse[swapped] = index;
};
const reset = () => {
length = 0;
sparse.length = 0;
};
return {
add,
remove,
has,
sparse,
get dense() {
return new Uint32Array(dense.buffer, 0, length);
},
reset
};
};
// src/core/utils/Observer.ts
var createObservable = () => {
const observers = /* @__PURE__ */ new Set();
const subscribe = (observer) => {
observers.add(observer);
return () => {
observers.delete(observer);
};
};
const notify = (entity, ...args) => {
return Array.from(observers).reduce((acc, listener) => {
const result = listener(entity, ...args);
return result && typeof result === "object" ? { ...acc, ...result } : acc;
}, {});
};
return {
subscribe,
notify
};
};
// src/core/Query.ts
var $opType = Symbol.for("bitecs-opType");
var $opTerms = Symbol.for("bitecs-opTerms");
var Or = (...components) => ({
[$opType]: "Or",
[$opTerms]: components
});
var And = (...components) => ({
[$opType]: "And",
[$opTerms]: components
});
var Not = (...components) => ({
[$opType]: "Not",
[$opTerms]: components
});
var Any = Or;
var All = And;
var None = Not;
var onAdd = (...terms) => ({
[$opType]: "add",
[$opTerms]: terms
});
var onRemove = (...terms) => ({
[$opType]: "remove",
[$opTerms]: terms
});
var onSet = (component) => ({
[$opType]: "set",
[$opTerms]: [component]
});
var onGet = (component) => ({
[$opType]: "get",
[$opTerms]: [component]
});
function observe(world, hook, callback) {
const ctx = world[$internal];
const { [$opType]: type, [$opTerms]: components } = hook;
if (type === "add" || type === "remove") {
const hash = queryHash(world, components);
let queryData = ctx.queriesHashMap.get(hash);
if (!queryData) {
queryData = registerQuery(world, components);
}
const observableKey = type === "add" ? "addObservable" : "removeObservable";
return queryData[observableKey].subscribe(callback);
} else if (type === "set" || type === "get") {
if (components.length !== 1) {
throw new Error("Set and Get hooks can only observe a single component");
}
const component = components[0];
let componentData = ctx.componentMap.get(component);
if (!componentData) {
componentData = registerComponent(world, component);
}
const observableKey = type === "set" ? "setObservable" : "getObservable";
return componentData[observableKey].subscribe(callback);
}
throw new Error(`Invalid hook type: ${type}`);
}
var queryHash = (world, terms) => {
const ctx = world[$internal];
const getComponentId = (component) => {
if (!ctx.componentMap.has(component)) {
registerComponent(world, component);
}
return ctx.componentMap.get(component).id;
};
const termToString = (term) => {
if ($opType in term) {
const componentIds = term[$opTerms].map(getComponentId);
const sortedComponentIds = componentIds.sort((a, b) => a - b);
const sortedType = term[$opType].toLowerCase();
return `${sortedType}(${sortedComponentIds.join(",")})`;
} else {
return getComponentId(term).toString();
}
};
return terms.map(termToString).sort().join("-");
};
var registerQuery = (world, terms, options = {}) => {
const ctx = world[$internal];
const hash = queryHash(world, terms);
const components = [];
const notComponents = [];
const orComponents = [];
const processComponents = (comps, targetArray) => {
comps.forEach((comp) => {
if (!ctx.componentMap.has(comp)) registerComponent(world, comp);
targetArray.push(comp);
});
};
terms.forEach((term) => {
if ($opType in term) {
if (term[$opType] === "Not") {
processComponents(term[$opTerms], notComponents);
} else if (term[$opType] === "Or") {
processComponents(term[$opTerms], orComponents);
}
} else {
if (!ctx.componentMap.has(term)) registerComponent(world, term);
components.push(term);
}
});
const mapComponents = (c) => ctx.componentMap.get(c);
const allComponents = components.concat(notComponents.flat()).concat(orComponents.flat()).map(mapComponents);
const sparseSet = options.buffered ? createUint32SparseSet() : createSparseSet();
const toRemove = createSparseSet();
const generations = allComponents.map((c) => c.generationId).reduce((a, v) => {
if (a.includes(v)) return a;
a.push(v);
return a;
}, []);
const reduceBitflags = (a, c) => {
if (!a[c.generationId]) a[c.generationId] = 0;
a[c.generationId] |= c.bitflag;
return a;
};
const masks = components.map(mapComponents).reduce(reduceBitflags, {});
const notMasks = notComponents.map(mapComponents).reduce(reduceBitflags, {});
const orMasks = orComponents.map(mapComponents).reduce(reduceBitflags, {});
const hasMasks = allComponents.reduce(reduceBitflags, {});
const addObservable = createObservable();
const removeObservable = createObservable();
const query2 = Object.assign(sparseSet, {
components,
notComponents,
orComponents,
allComponents,
masks,
notMasks,
orMasks,
hasMasks,
generations,
toRemove,
addObservable,
removeObservable,
queues: {}
});
ctx.queries.add(query2);
ctx.queriesHashMap.set(hash, query2);
allComponents.forEach((c) => {
c.queries.add(query2);
});
if (notComponents.length) ctx.notQueries.add(query2);
const entityIndex = ctx.entityIndex;
for (let i = 0; i < entityIndex.aliveCount; i++) {
const eid = entityIndex.dense[i];
if (hasComponent(world, eid, Prefab)) continue;
const match = queryCheckEntity(world, query2, eid);
if (match) {
queryAddEntity(query2, eid);
}
}
return query2;
};
function innerQuery(world, terms, options = {}) {
const ctx = world[$internal];
const hash = queryHash(world, terms);
let queryData = ctx.queriesHashMap.get(hash);
if (!queryData) {
queryData = registerQuery(world, terms, options);
} else if (options.buffered && !("buffer" in queryData.dense)) {
queryData = registerQuery(world, terms, { buffered: true });
}
return queryData.dense;
}
function query(world, terms) {
commitRemovals(world);
return innerQuery(world, terms);
}
function queryCheckEntity(world, query2, eid) {
const ctx = world[$internal];
const { masks, notMasks, orMasks, generations } = query2;
for (let i = 0; i < generations.length; i++) {
const generationId = generations[i];
const qMask = masks[generationId];
const qNotMask = notMasks[generationId];
const qOrMask = orMasks[generationId];
const eMask = ctx.entityMasks[generationId][eid];
if (qNotMask && (eMask & qNotMask) !== 0) {
return false;
}
if (qMask && (eMask & qMask) !== qMask) {
return false;
}
if (qOrMask && (eMask & qOrMask) === 0) {
return false;
}
}
return true;
}
var queryAddEntity = (query2, eid) => {
query2.toRemove.remove(eid);
query2.addObservable.notify(eid);
query2.add(eid);
};
var queryCommitRemovals = (query2) => {
for (let i = 0; i < query2.toRemove.dense.length; i++) {
const eid = query2.toRemove.dense[i];
query2.remove(eid);
}
query2.toRemove.reset();
};
var commitRemovals = (world) => {
const ctx = world[$internal];
if (!ctx.dirtyQueries.size) return;
ctx.dirtyQueries.forEach(queryCommitRemovals);
ctx.dirtyQueries.clear();
};
var queryRemoveEntity = (world, query2, eid) => {
const ctx = world[$internal];
const has = query2.has(eid);
if (!has || query2.toRemove.has(eid)) return;
query2.toRemove.add(eid);
ctx.dirtyQueries.add(query2);
query2.removeObservable.notify(eid);
};
var removeQuery = (world, terms) => {
const ctx = world[$internal];
const hash = queryHash(world, terms);
const query2 = ctx.queriesHashMap.get(hash);
if (query2) {
ctx.queries.delete(query2);
ctx.queriesHashMap.delete(hash);
}
};
// src/core/Relation.ts
var $relation = Symbol.for("bitecs-relation");
var $pairTarget = Symbol.for("bitecs-pairTarget");
var $isPairComponent = Symbol.for("bitecs-isPairComponent");
var $relationData = Symbol.for("bitecs-relationData");
var createBaseRelation = () => {
const data = {
pairsMap: /* @__PURE__ */ new Map(),
initStore: void 0,
exclusiveRelation: false,
autoRemoveSubject: false,
onTargetRemoved: void 0
};
const relation = (target) => {
if (target === void 0) throw Error("Relation target is undefined");
const normalizedTarget = target === "*" ? Wildcard : target;
if (!data.pairsMap.has(normalizedTarget)) {
const component = data.initStore ? data.initStore(target) : {};
defineHiddenProperty(component, $relation, relation);
defineHiddenProperty(component, $pairTarget, normalizedTarget);
defineHiddenProperty(component, $isPairComponent, true);
data.pairsMap.set(normalizedTarget, component);
}
return data.pairsMap.get(normalizedTarget);
};
defineHiddenProperty(relation, $relationData, data);
return relation;
};
var withStore = (createStore) => (relation) => {
const ctx = relation[$relationData];
ctx.initStore = createStore;
return relation;
};
var makeExclusive = (relation) => {
const ctx = relation[$relationData];
ctx.exclusiveRelation = true;
return relation;
};
var withAutoRemoveSubject = (relation) => {
const ctx = relation[$relationData];
ctx.autoRemoveSubject = true;
return relation;
};
var withOnTargetRemoved = (onRemove2) => (relation) => {
const ctx = relation[$relationData];
ctx.onTargetRemoved = onRemove2;
return relation;
};
var Pair = (relation, target) => {
if (relation === void 0) throw Error("Relation is undefined");
return relation(target);
};
var getRelationTargets = (world, eid, relation) => {
const components = getEntityComponents(world, eid);
const targets = [];
for (const c of components) {
if (c[$relation] === relation && c[$pairTarget] !== Wildcard && !isRelation(c[$pairTarget])) {
targets.push(c[$pairTarget]);
}
}
return targets;
};
function createRelation(...args) {
if (args.length === 1 && typeof args[0] === "object") {
const { store, exclusive, autoRemoveSubject, onTargetRemoved } = args[0];
const modifiers = [
store && withStore(store),
exclusive && makeExclusive,
autoRemoveSubject && withAutoRemoveSubject,
onTargetRemoved && withOnTargetRemoved(onTargetRemoved)
].filter(Boolean);
return modifiers.reduce((acc, modifier) => modifier(acc), createBaseRelation());
} else {
const modifiers = args;
return modifiers.reduce((acc, modifier) => modifier(acc), createBaseRelation());
}
}
var $wildcard = Symbol.for("bitecs-wildcard");
function createWildcardRelation() {
const relation = createBaseRelation();
Object.defineProperty(relation, $wildcard, {
value: true,
enumerable: false,
writable: false,
configurable: false
});
return relation;
}
function getWildcard() {
const GLOBAL_WILDCARD = Symbol.for("bitecs-global-wildcard");
if (!globalThis[GLOBAL_WILDCARD]) {
globalThis[GLOBAL_WILDCARD] = createWildcardRelation();
}
return globalThis[GLOBAL_WILDCARD];
}
var Wildcard = getWildcard();
function createIsARelation() {
return createBaseRelation();
}
function getIsA() {
const GLOBAL_ISA = Symbol.for("bitecs-global-isa");
if (!globalThis[GLOBAL_ISA]) {
globalThis[GLOBAL_ISA] = createIsARelation();
}
return globalThis[GLOBAL_ISA];
}
var IsA = getIsA();
function isWildcard(relation) {
if (!relation) return false;
const symbols = Object.getOwnPropertySymbols(relation);
return symbols.includes($wildcard);
}
function isRelation(component) {
if (!component) return false;
const symbols = Object.getOwnPropertySymbols(component);
return symbols.includes($relationData);
}
// src/core/Component.ts
var registerComponent = (world, component) => {
if (!component) {
throw new Error(`bitECS - Cannot register null or undefined component`);
}
const ctx = world[$internal];
const queries = /* @__PURE__ */ new Set();
const data = {
id: ctx.componentCount++,
generationId: ctx.entityMasks.length - 1,
bitflag: ctx.bitflag,
ref: component,
queries,
setObservable: createObservable(),
getObservable: createObservable()
};
ctx.componentMap.set(component, data);
ctx.bitflag *= 2;
if (ctx.bitflag >= 2 ** 31) {
ctx.bitflag = 1;
ctx.entityMasks.push([]);
}
return data;
};
var registerComponents = (world, components) => {
components.forEach((component) => registerComponent(world, component));
};
var hasComponent = (world, eid, component) => {
const ctx = world[$internal];
const registeredComponent = ctx.componentMap.get(component);
if (!registeredComponent) return false;
const { generationId, bitflag } = registeredComponent;
const mask = ctx.entityMasks[generationId][eid];
return (mask & bitflag) === bitflag;
};
var getComponentData = (world, eid, component) => {
const ctx = world[$internal];
const componentData = ctx.componentMap.get(component);
if (!componentData) {
return void 0;
}
if (!hasComponent(world, eid, component)) {
return void 0;
}
return componentData.getObservable.notify(eid);
};
var set = (component, data) => ({
component,
data
});
var recursivelyInherit = (ctx, world, baseEid, inheritedEid, visited = /* @__PURE__ */ new Set()) => {
if (visited.has(inheritedEid)) return;
visited.add(inheritedEid);
addComponent(world, baseEid, IsA(inheritedEid));
for (const component of getEntityComponents(world, inheritedEid)) {
if (component === Prefab) continue;
if (!hasComponent(world, baseEid, component)) {
addComponent(world, baseEid, component);
const componentData = ctx.componentMap.get(component);
if (componentData?.setObservable) {
const data = getComponentData(world, inheritedEid, component);
componentData.setObservable.notify(baseEid, data);
}
}
}
for (const parentEid of getRelationTargets(world, inheritedEid, IsA)) {
recursivelyInherit(ctx, world, baseEid, parentEid, visited);
}
};
var addComponent = (world, eid, ...components) => {
if (!entityExists(world, eid)) {
throw new Error(`Cannot add component - entity ${eid} does not exist in the world.`);
}
const ctx = world[$internal];
components.forEach((componentOrSet) => {
const component = "component" in componentOrSet ? componentOrSet.component : componentOrSet;
const data = "data" in componentOrSet ? componentOrSet.data : void 0;
if (!ctx.componentMap.has(component)) registerComponent(world, component);
const componentData = ctx.componentMap.get(component);
if (data !== void 0) {
componentData.setObservable.notify(eid, data);
}
if (hasComponent(world, eid, component)) return;
const { generationId, bitflag, queries } = componentData;
ctx.entityMasks[generationId][eid] |= bitflag;
if (!hasComponent(world, eid, Prefab)) {
queries.forEach((queryData) => {
queryData.toRemove.remove(eid);
const match = queryCheckEntity(world, queryData, eid);
if (match) queryAddEntity(queryData, eid);
else queryRemoveEntity(world, queryData, eid);
});
}
ctx.entityComponents.get(eid).add(component);
if (component[$isPairComponent]) {
const relation = component[$relation];
const target = component[$pairTarget];
addComponent(world, eid, Pair(relation, Wildcard));
addComponent(world, eid, Pair(Wildcard, target));
if (typeof target === "number") {
addComponent(world, target, Pair(Wildcard, eid));
addComponent(world, target, Pair(Wildcard, relation));
ctx.entitiesWithRelations.add(target);
ctx.entitiesWithRelations.add(eid);
}
ctx.entitiesWithRelations.add(target);
const relationData = relation[$relationData];
if (relationData.exclusiveRelation === true && target !== Wildcard) {
const oldTarget = getRelationTargets(world, eid, relation)[0];
if (oldTarget !== void 0 && oldTarget !== null && oldTarget !== target) {
removeComponent(world, eid, relation(oldTarget));
}
}
if (relation === IsA) {
const inheritedTargets = getRelationTargets(world, eid, IsA);
for (const inherited of inheritedTargets) {
recursivelyInherit(ctx, world, eid, inherited);
}
}
}
});
};
var addComponents = addComponent;
var removeComponent = (world, eid, ...components) => {
const ctx = world[$internal];
if (!entityExists(world, eid)) {
throw new Error(`Cannot remove component - entity ${eid} does not exist in the world.`);
}
components.forEach((component) => {
if (!hasComponent(world, eid, component)) return;
const componentNode = ctx.componentMap.get(component);
const { generationId, bitflag, queries } = componentNode;
ctx.entityMasks[generationId][eid] &= ~bitflag;
queries.forEach((queryData) => {
queryData.toRemove.remove(eid);
const match = queryCheckEntity(world, queryData, eid);
if (match) queryAddEntity(queryData, eid);
else queryRemoveEntity(world, queryData, eid);
});
ctx.entityComponents.get(eid).delete(component);
if (component[$isPairComponent]) {
const target = component[$pairTarget];
removeComponent(world, eid, Pair(Wildcard, target));
const relation = component[$relation];
const otherTargets = getRelationTargets(world, eid, relation);
if (otherTargets.length === 0) {
removeComponent(world, eid, Pair(relation, Wildcard));
}
}
});
};
var removeComponents = removeComponent;
// src/core/Entity.ts
var Prefab = {};
var addPrefab = (world) => {
const eid = addEntity(world);
addComponent(world, eid, Prefab);
return eid;
};
var addEntity = (world) => {
const ctx = world[$internal];
const eid = addEntityId(ctx.entityIndex);
ctx.notQueries.forEach((q) => {
const match = queryCheckEntity(world, q, eid);
if (match) queryAddEntity(q, eid);
});
ctx.entityComponents.set(eid, /* @__PURE__ */ new Set());
return eid;
};
var removeEntity = (world, eid) => {
const ctx = world[$internal];
if (!isEntityIdAlive(ctx.entityIndex, eid)) return;
const removalQueue = [eid];
const processedEntities = /* @__PURE__ */ new Set();
while (removalQueue.length > 0) {
const currentEid = removalQueue.shift();
if (processedEntities.has(currentEid)) continue;
processedEntities.add(currentEid);
const componentRemovalQueue = [];
if (ctx.entitiesWithRelations.has(currentEid)) {
for (const subject of innerQuery(world, [Wildcard(currentEid)])) {
if (!entityExists(world, subject)) {
continue;
}
for (const component of ctx.entityComponents.get(subject)) {
if (!component[$isPairComponent]) {
continue;
}
const relation = component[$relation];
const relationData = relation[$relationData];
componentRemovalQueue.push(() => removeComponent(world, subject, Pair(Wildcard, currentEid)));
if (component[$pairTarget] === currentEid) {
componentRemovalQueue.push(() => removeComponent(world, subject, component));
if (relationData.autoRemoveSubject) {
removalQueue.push(subject);
}
if (relationData.onTargetRemoved) {
componentRemovalQueue.push(() => relationData.onTargetRemoved(world, subject, currentEid));
}
}
}
}
ctx.entitiesWithRelations.delete(currentEid);
}
for (const removeOperation of componentRemovalQueue) {
removeOperation();
}
for (const eid2 of removalQueue) {
removeEntity(world, eid2);
}
for (const query2 of ctx.queries) {
queryRemoveEntity(world, query2, currentEid);
}
removeEntityId(ctx.entityIndex, currentEid);
ctx.entityComponents.delete(currentEid);
for (let i = 0; i < ctx.entityMasks.length; i++) {
ctx.entityMasks[i][currentEid] = 0;
}
}
};
var getEntityComponents = (world, eid) => {
const ctx = world[$internal];
if (eid === void 0) throw new Error(`getEntityComponents: entity id is undefined.`);
if (!isEntityIdAlive(ctx.entityIndex, eid))
throw new Error(`getEntityComponents: entity ${eid} does not exist in the world.`);
return Array.from(ctx.entityComponents.get(eid));
};
var entityExists = (world, eid) => isEntityIdAlive(world[$internal].entityIndex, eid);
// src/core/utils/pipe.ts
var pipe = (...functions) => {
return (...args) => functions.reduce((result, fn) => [fn(...result)], args)[0];
};
export {
$internal,
All,
And,
Any,
IsA,
None,
Not,
Or,
Pair,
Prefab,
Wildcard,
addComponent,
addComponents,
addEntity,
addPrefab,
commitRemovals,
createEntityIndex,
createRelation,
createWorld,
deleteWorld,
entityExists,
getAllEntities,
getComponentData,
getEntityComponents,
getId,
getRelationTargets,
getVersion,
getWorldComponents,
hasComponent,
innerQuery,
isRelation,
isWildcard,
observe,
onAdd,
onGet,
onRemove,
onSet,
pipe,
query,
registerComponent,
registerComponents,
registerQuery,
removeComponent,
removeComponents,
removeEntity,
removeQuery,
resetWorld,
set,
withAutoRemoveSubject,
withOnTargetRemoved,
withStore,
withVersioning
};
//# sourceMappingURL=index.mjs.map

7
public/vendor/bitecs/index.js.map vendored Normal file

File diff suppressed because one or more lines are too long

2
public/vendor/bitecs/index.min.js vendored Normal file

File diff suppressed because one or more lines are too long

7
public/vendor/bitecs/index.min.js.map vendored Normal file

File diff suppressed because one or more lines are too long

157
src/components/index.js Normal file
View file

@ -0,0 +1,157 @@
const DefaultSize = 512
export const Rgba8 = {
r: new Uint8Array(DefaultSize),
g: new Uint8Array(DefaultSize),
b: new Uint8Array(DefaultSize),
a: new Uint8Array(DefaultSize),
}
export const Range = {
min: new Float32Array(DefaultSize),
max: new Float32Array(DefaultSize)
}
export const UVector2 = {
x: new Uint32Array(DefaultSize),
y: new Uint32Array(DefaultSize)
}
export const UVector3 = {
...UVector2,
z: new Uint32Array(DefaultSize)
}
export const Vector2 = {
x: new Float32Array(DefaultSize),
y: new Float32Array(DefaultSize),
}
export const Vector3 = {
...Vector2,
z: new Float32Array(DefaultSize)
}
export const Quaternion = {
...Vector3,
w: new Float32Array(DefaultSize)
}
export const Transform = {
translation: Vector3,
rotation: Quaternion,
scale: Vector3
}
export const Viewport = {
position: UVector2,
size: UVector2,
depth: Range
}
export const RenderTarget = []
class RenderGraph {
#nodes = new Map()
#edges = new Map()
add(name, render, dependencies = []) {
this.node.set(name, render)
this.edges.set(name, dependencies)
}
update(world) {
const executed = new Set()
const order = this.sort()
if(!order) {
return
}
for(const node of order) {
const render = this.nodes.get(node)
render(world)
executed.add(node)
}
}
sort() {
const visited = new Set()
const stack = []
const cycles = new Set()
const visit = node => {
if(cycles.has(node)) {
return false
}
if (visited.has(node)) {
return true
}
visited.add(node)
cycles.add(node)
for(const dep of this.dependentsFor(name)) {
if (!visit(dep)) {
return false
}
}
cycles.delete(name)
stack.push(node)
}
for(const node in this.nodes) {
if(!visited.has(node) && !visit(node)) {
return
}
}
return stack.reverse()
}
dependentsFor(name) {
return this.edges.values()
.filter(deps => deps.includes(name))
}
}
export const Camera = {
viewport: Viewport,
order: new Int32Array(DefaultSize),
clearColor: Rgba8,
renderTarget: RenderTarget
}
export const Renderable = {}
const Topology = Object.freeze({
Points: 0,
Lines: 1,
LineStrip: 2,
LineLoop: 3,
Triangles: 4,
TriangleStrip: 5,
TriangleFan: 6
})
export const VertexPositions = new Float32Array(1024)
export const Indices = new Uint16Array(1024)
export const Mesh = {
topology: new Uint8Array(DefaultSize),
positions: new Uint32Array(DefaultSize),
indices: new Uint32Array(DefaultSize),
vao: new Uint32Array(DefaultSize),
drawMode: new Uint32Array(DefaultSize),
vertexCount: new Uint32Array(DefaultSize),
indexCount: new Uint32Array(DefaultSize)
}
export const Sprite = {
data: [],
height: new Uint32Array(DefaultSize),
width: new Uint32Array(DefaultSize)
}

58
src/index.js Normal file
View file

@ -0,0 +1,58 @@
import * as Components from './components/index.js'
const { Transform, Camera, Renderable, Sprite } = Components
import { Renderer2D } from './render/index.js'
import { addEntity, addComponent, createWorld, set, observe, onSet } from '../public/vendor/bitecs/index.js'
const canvas = document.querySelector('main canvas')
const addComponents = (world, entity, components) => {
components.forEach(component => addComponent(world, entity, component))
}
const setData = (component, eid, data) => {
const entries = Object.entries(data)
if (Array.isArray(data) || ArrayBuffer.isView(data) || entries.length === 0) {
component[eid] = data
} else {
entries.forEach(([key, value]) => setData(component[key], eid, value))
}
}
if (canvas.getContext) {
const world = createWorld()
Object.values(Components).forEach(component => {
observe(world, onSet(component), (eid, params) => {
setData(component, eid, params)
})
})
const camera = addEntity(world)
addComponents(world, camera, [
Transform,
set(Camera, { viewport: { position: { x: 16, y: 16 } } })
])
const obj = addEntity(world)
const pixels = new Uint8ClampedArray(64 * 64 * 4)
for (let i = 0; i < pixels.length; i += 4) {
pixels[i + 0] = 255
pixels[i + 3] = 255
}
addComponents(world, obj, [
Renderable,
set(Transform, { translation: { x: 32, y: 32 } }),
set(Sprite, {
data: pixels,
width: 64,
height: 64
})
])
const renderer = Renderer2D(canvas)
renderer(world)
} else {
// canvas not supported
}

4
src/mixins/index.js Normal file
View file

@ -0,0 +1,4 @@
export const mixin = (target, mixin) => {
Object.assign(target, mixin)
}

110
src/render/index.js Normal file
View file

@ -0,0 +1,110 @@
import { query } from '/public/vendor/bitecs/index.js'
import { Transform, Camera, Renderable } from '/src/components/index.js'
const vertSource = `#version 300 es
in vec2 a_position;
in vec4 a_color;
uniform vec2 u_resolution;
uniform vec2 u_cameraPosition;
uniform float u_cameraZoom;
out vec4 v_color;
void main() {
vec2 position = (a_position - u_cameraPosition) * u_cameraZoom;
vec2 clipSpace = position / (u_resolution / 2.0) - 1.0;
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
v_color = a_color;
}`
const fragSource = `#version 300 es
precision mediump float;
in vec4 v_color;
out vec4 outColor;
void main() {
outColor = v_color;
}`
const createMesh = (world, gl, entity, positions, indices, drawMode) => {
addComponent(world, Mesh, entity)
addComponent(world, VertexPositions, entity)
VertexPositions.data.set(positions, entity * positions.length)
if (indices) {
addComponent(world, Indices, entity)
Indices.data.set(indices, entity * indices.length)
}
const vao = gl.createVertexArray()
gl.bindVertexArray(vao)
const positionBuffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer)
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW)
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(0)
if (indices) {
const indexBuffer = gl.createBuffer()
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW)
}
gl.bindVertexArray(null)
MeshHandle.positionBuffer[entity] = positionBuffer
MeshHandle.indexBuffer[entity] = indexBuffer
MeshHandle.vao[entity] = vao
MeshHandle.drawMode[entity] = drawMode
MeshHandle.vertexCount[entity] = positions.length / 3
MeshHandle.indexCount[entity] = indices ? indices.length : 0
}
export const Renderer2D = canvas => {
const gl = canvas.getContext('webgl2')
const program = Program.fromSource(gl, vertSource, fragSource)
const position = new Attribute(gl, program, 'a_position', new Buffer(), new BufferSchema(DataType.f32, { size: 2 }))
const color = new Attribute(gl, program, 'a_color', new Buffer(), new BufferSchema(DataType.f32, { size: 4 }))
const resolution = new Uniform(gl, program, 'u_resolution', 2, UniformType.Float)
const cameraPosition = new Uniform(gl, program, 'u_cameraPosition', 2, UniformType.Float)
// TODO: make this a property of camera
const viewport = new Viewport(gl)
viewport.resize()
// TODO: separate into window class
window.addEventListener('resize', () => {
canvas.width = window.innerWidth
canvas.height = window.innerHeight
resolution.set(canvas.width, canvas.height)
viewport.resize()
})
return world => {
viewport.clear()
const cameras = query(world, [Transform, Camera])
// FIXME: Supporting only one camera for now
const camera = cameras[0]
const vx = Camera.viewport.position.x[camera]
const vy = Camera.viewport.position.y[camera]
const target = Camera.renderTarget[camera] || ctx
cameraPosition.set(vx, vy)
const renderable = query(world, [Renderable, Transform])
for (let i = 0; i < renderable.length; i++) {
const eid = renderable[i]
const x = Transform.translation.x[eid] - vx
const y = Transform.translation.y[eid] - vy
//target.putImageData(new ImageData(Sprite.data[eid], Sprite.width[eid], Sprite.height[eid]), x, y)
}
}
}

298
src/render/webgl.js Normal file
View file

@ -0,0 +1,298 @@
class GLObject {
#gl
constructor(gl) {
this.#gl = gl
}
}
export class ShaderError extends Error {
constructor(message) {
super(message)
}
}
export class Shader extends GLObject {
#gl
#shader
#type
constructor(gl, type, source) {
super(gl)
const shader = gl.createShader(type)
gl.shaderSource(shader, source)
gl.compileShader(shader)
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
gl.deleteShader(shader)
throw new ShaderError(gl.getShaderInfoLog(shader))
}
this.#shader = shader
this.#type = type
}
static vertex(gl, source) {
return new Shader(gl, gl.VERTEX_SHADER, source)
}
static fragment(gl, source) {
return new Shader(gl, gl.FRAGMENT_SHADER, source)
}
attach(program) {
this.#gl.attachShader(program, this.#shader)
}
}
export class ProgramError extends Error {
constructor(message) {
super(message)
}
}
export class Program {
#program
constructor(gl, vertexShader, fragmentShader) {
super(gl)
const program = gl.createProgram()
vertexShader.attach(program)
fragmentShader.attach(program)
gl.linkProgram(program)
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
gl.deleteProgram(program)
throw new ProgramError(gl.getProgramInfoLog(program))
}
this.#program = program
}
static fromSource(gl, vertexSource, fragmentSource) {
return new Program(
gl,
Shader.vertex(gl, vertexSource),
Shader.fragment(gl, fragmentSource)
)
}
}
export const DataType = {
i8: 5120,
u8: 5121,
i16: 5122,
u16: 5123,
f16: 5131,
f32: 5126,
i32: 5124,
u32: 5125
}
export class Buffer extends GLObject {
#buffer
#target
constructor(gl, target = gl.ARRAY_BUFFER) {
super(gl)
const buffer = gl.createBuffer()
this.#buffer = buffer
this.#target = target
}
get size() {
return this.#gl.getBufferParameter(
this.#target,
this.#gl.BUFFER_SIZE
)
}
get usage() {
return this.#gl.getBufferParameter(
this.#target,
this.#gl.BUFFER_USAGE
)
}
bind() {
this.#gl.bindBuffer(this.#target, this.#buffer)
}
set(value, usage) {
if(!usage) {
console.warn('WARN: Buffer.set called without "usage" argument. Defaulting to DYNAMIC_DRAW')
usage = gl.DYNAMIC_DRAW
}
this.#gl.bufferData(
this.#target,
value,
usage
)
}
}
export class BufferSchema {
type
#size
normalize
stride
offset
constructor(type, { size = 4, normalize = false, stride = 0, offset = 0 } = {}) {
this.type = type
this.#size = size
this.normalize = normalize
this.stride = stride
this.offset = offset
}
get size() {
return this.#size
}
set size(value) {
if (value > 0 && value < 5) {
this.#size = value
} else {
throw new RangeError("BufferSchema size must be between 1 and 4")
}
}
bind(gl, location) {
gl.vertexAttribPointer(
location,
this.size,
this.type,
this.normalize,
this.stride,
this.offset
)
}
}
export class Attribute extends GLObject {
#name
#buffer
#program
#location
#schema
constructor(gl, program, name, buffer, schema) {
super(gl)
this.#name = name
this.#buffer = buffer
this.#program = program
this.#schema = schema
this.#location = gl.getAttribLocation(program, name)
}
get buffer() {
return this.#buffer
}
get location() {
return this.#location
}
bind() {
this.#gl.enableVertexAttribArray(this.#location)
this.#buffer.bind()
this.#schema.bind(this.#gl, this.#location)
}
}
export const UniformType = {
Int: 'i',
IntVec: 'iv',
UInt: 'ui',
UIntVec: 'uiv',
Float: 'f',
FloatVec: 'fv'
}
// TODO: clean this api up
export class Uniform extends GLObject {
#name
#program
#location
#length
#type
#fn
constructor(gl, program, name, length, type) {
super(gl)
this.#name = name
this.#program = program
this.#location = gl.getUniformLocation(program, name)
this.#length = length
this.#type = type
this.#setFn(gl, length, type)
}
#setFn(gl, length, type) {
this.#fn = gl[`uniform${length}${type}`].bind(gl)
}
get length() {
return this.#length
}
set length(value) {
this.#length = value
this.#setFn(this.#gl, value, this.#type)
}
get type() {
return this.#type
}
set type(value) {
this.#type = value
this.#setFn(this.#gl, this.#length, value)
}
// TODO: add data validation
set(...args) {
this.#fn(this.#location, ...args)
}
}
export class Viewport extends GLObject {
constructor(gl, clearColor = [0, 0, 0, 1]) {
super(gl)
this.clearColor = clearColor
}
get clearColor() {
return this.#gl.getParameter(this.#gl.COLOR_CLEAR_VALUE)
}
set clearColor(value) {
this.#gl.clearColor(...value)
}
get MaxViewportDimensions() {
return this.#gl.getParameter(this.#gl.MAX_VIEWPORT_DIMS)
}
get bounds() {
return this.#gl.getParameter(this.#gl.VIEWPORT)
}
resize(x, y, width, height) {
this.#gl.viewport(x, y, width, height)
}
clear() {
this.#gl.clear(this.#gl.COLOR_BUFFER_BIT)
}
}

15
src/types.js Normal file
View file

@ -0,0 +1,15 @@
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Typed_arrays#typed_array_views
export const i8 = size => createStore(Int8Array, size)
export const u8 = size => createStore(Uint8Array, size)
export const u8c = size => createStore(Uint8ClampedArray, size)
export const i16 = size => createStore(Int16Array, size)
export const u16 = size => createStore(Uint16Array, size)
export const i32 = size => createStore(Int32Array, size)
export const u32 = size => createStore(Uint32Array, size)
export const f16 = size => createStore(Float16Array, size)
export const f32 = size => createStore(Float32Array, size)
export const f64 = size => createStore(Float64Array, size)
export const i64 = size => createStore(BigInt64Array, size)
export const u64 = size => createStore(BigUint64Array, size)
const createStore = (type, size = 64) => new type(size)