initial commit
This commit is contained in:
commit
102200541d
12 changed files with 1540 additions and 0 deletions
17
index.html
Normal file
17
index.html
Normal 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
13
public/css/style.css
Normal 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
852
public/vendor/bitecs/index.js
vendored
Normal 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
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
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
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
157
src/components/index.js
Normal 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
58
src/index.js
Normal 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
4
src/mixins/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
export const mixin = (target, mixin) => {
|
||||
Object.assign(target, mixin)
|
||||
}
|
||||
|
110
src/render/index.js
Normal file
110
src/render/index.js
Normal 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
298
src/render/webgl.js
Normal 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
15
src/types.js
Normal 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)
|
Loading…
Add table
Reference in a new issue