@actis/core 26.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,340 @@
1
+ //#region src/pass/Pass.ts
2
+ /**
3
+ * Represents a render pass in the WebGL pipeline.
4
+ * Responsible for managing framebuffer, texture, and draw calls for a single pass.
5
+ */
6
+ var Pass = class {
7
+ constructor(gl, shader, width, height, offscreen = true, textures = []) {
8
+ this.gl = gl;
9
+ this.shader = shader;
10
+ this.width = width;
11
+ this.height = height;
12
+ this.texture = this.createTexture();
13
+ this.framebuffer = this.createFramebuffer(this.texture);
14
+ this.next = null;
15
+ this.offscreen = offscreen;
16
+ this.textures = textures;
17
+ this.positionAttributeLocation = this.shader.getAttribLocation("a_position");
18
+ this.positionBuffer = this.createPositionBuffer();
19
+ }
20
+ use() {
21
+ this.shader.use();
22
+ if (this.offscreen) {
23
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.framebuffer);
24
+ this.gl.viewport(0, 0, this.width, this.height);
25
+ } else {
26
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
27
+ this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
28
+ }
29
+ }
30
+ resize(width, height) {
31
+ this.width = width;
32
+ this.height = height;
33
+ if (this.offscreen) {
34
+ this.texture = this.createTexture();
35
+ this.framebuffer = this.createFramebuffer(this.texture);
36
+ }
37
+ }
38
+ draw() {
39
+ const positions = new Float32Array([
40
+ -1,
41
+ -1,
42
+ 1,
43
+ -1,
44
+ -1,
45
+ 1,
46
+ -1,
47
+ 1,
48
+ 1,
49
+ -1,
50
+ 1,
51
+ 1
52
+ ]);
53
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
54
+ this.gl.bufferData(this.gl.ARRAY_BUFFER, positions, this.gl.STATIC_DRAW);
55
+ this.gl.enableVertexAttribArray(this.positionAttributeLocation);
56
+ this.gl.vertexAttribPointer(this.positionAttributeLocation, 2, this.gl.FLOAT, false, 0, 0);
57
+ this.textures.forEach((texture, index) => {
58
+ this.gl.activeTexture(this.gl.TEXTURE0 + index);
59
+ this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
60
+ this.shader.setUniform(`u_texture${index}`, "1i", index);
61
+ });
62
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, positions.length / 2);
63
+ }
64
+ createTexture() {
65
+ const texture = this.gl.createTexture();
66
+ this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
67
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
68
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
69
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
70
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
71
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.width, this.height, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, null);
72
+ return texture;
73
+ }
74
+ createFramebuffer(texture) {
75
+ const framebuffer = this.gl.createFramebuffer();
76
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer);
77
+ this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, this.gl.COLOR_ATTACHMENT0, this.gl.TEXTURE_2D, texture, 0);
78
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
79
+ return framebuffer;
80
+ }
81
+ createPositionBuffer() {
82
+ const buffer = this.gl.createBuffer();
83
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer);
84
+ return buffer;
85
+ }
86
+ };
87
+ //#endregion
88
+ //#region src/shader/Shader.ts
89
+ const ERROR_LOG_REGEX = /ERROR: 0:(\d+): (.*)(?=\n|$)/;
90
+ /**
91
+ * Compiles and manages a WebGL shader program.
92
+ * Responsible for compiling, linking, and providing access to uniforms and attributes.
93
+ */
94
+ var Shader = class {
95
+ constructor(gl, vertexSource, fragmentSource, onError, passName) {
96
+ this.gl = gl;
97
+ this.onError = onError;
98
+ this.passName = passName;
99
+ this.uniformLocations = /* @__PURE__ */ new Map();
100
+ const vertexShader = this.compileShader(vertexSource || this.defaultVertexShader(), gl.VERTEX_SHADER);
101
+ const fragmentShader = this.compileShader(fragmentSource, gl.FRAGMENT_SHADER);
102
+ this.program = this.linkProgram(vertexShader, fragmentShader);
103
+ }
104
+ compileShader(source, type) {
105
+ const shader = this.gl.createShader(type);
106
+ if (!shader) throw new Error("Failed to create shader");
107
+ this.gl.shaderSource(shader, source);
108
+ this.gl.compileShader(shader);
109
+ if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
110
+ const log = this.gl.getShaderInfoLog(shader);
111
+ this.gl.deleteShader(shader);
112
+ const coords = this.extractErrorCoords(log || "");
113
+ this.onError({
114
+ passName: this.passName,
115
+ coords
116
+ });
117
+ throw new Error(`Failed to compile shader: ${log}`);
118
+ }
119
+ return shader;
120
+ }
121
+ extractErrorCoords(log) {
122
+ const match = ERROR_LOG_REGEX.exec(log);
123
+ if (match) return {
124
+ line: Number.parseInt(match[1], 10),
125
+ message: match[2]
126
+ };
127
+ return {
128
+ line: 0,
129
+ message: ""
130
+ };
131
+ }
132
+ linkProgram(vertexShader, fragmentShader) {
133
+ const program = this.gl.createProgram();
134
+ if (!program) throw new Error("Failed to create program");
135
+ if (vertexShader) this.gl.attachShader(program, vertexShader);
136
+ this.gl.attachShader(program, fragmentShader);
137
+ this.gl.linkProgram(program);
138
+ if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
139
+ const log = this.gl.getProgramInfoLog(program);
140
+ this.gl.deleteProgram(program);
141
+ throw new Error(`Failed to link program: ${log}`);
142
+ }
143
+ return program;
144
+ }
145
+ use() {
146
+ this.gl.useProgram(this.program);
147
+ }
148
+ setUniform(name, type, ...values) {
149
+ let location = this.uniformLocations.get(name);
150
+ if (!location) {
151
+ location = this.gl.getUniformLocation(this.program, name);
152
+ this.uniformLocations.set(name, location);
153
+ }
154
+ switch (type) {
155
+ case "1f":
156
+ this.gl.uniform1f(location, values[0]);
157
+ break;
158
+ case "1i":
159
+ this.gl.uniform1i(location, values[0]);
160
+ break;
161
+ case "2fv":
162
+ this.gl.uniform2fv(location, values[0]);
163
+ break;
164
+ case "3fv":
165
+ this.gl.uniform3fv(location, values[0]);
166
+ break;
167
+ case "4fv":
168
+ this.gl.uniform4fv(location, values[0]);
169
+ break;
170
+ }
171
+ }
172
+ getAttribLocation(name) {
173
+ return this.gl.getAttribLocation(this.program, name);
174
+ }
175
+ defaultVertexShader() {
176
+ return `
177
+ attribute vec4 a_position;
178
+ void main() {
179
+ gl_Position = a_position;
180
+ }`;
181
+ }
182
+ };
183
+ //#endregion
184
+ //#region src/engine/WebGLRenderer.ts
185
+ /**
186
+ * Renderer class responsible for managing the WebGL context, render passes, and animation loop.
187
+ */
188
+ var WebGLRenderer = class {
189
+ constructor(canvas, onError) {
190
+ this.canvas = canvas;
191
+ this.animationRequestID = -1;
192
+ this.gl = this.initializeWebGLContext(canvas);
193
+ this.passes = null;
194
+ this.textureMap = /* @__PURE__ */ new Map();
195
+ this.now = /* @__PURE__ */ new Date();
196
+ this.onError = onError;
197
+ this.mouseX = 0;
198
+ this.mouseY = 0;
199
+ this.time = 0;
200
+ this.timeDelta = 0;
201
+ this.realToCSSPixels = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
202
+ this.paused = false;
203
+ this.playbackTime = 0;
204
+ this.lastTime = 0;
205
+ this.frameRate = 0;
206
+ this.currentFrame = 0;
207
+ this.currentTime = 0;
208
+ this.startTime = 0;
209
+ this.initMouseEvents();
210
+ if (typeof window !== "undefined") window.addEventListener("resize", this.resizeCanvasToDisplaySize.bind(this));
211
+ }
212
+ initializeWebGLContext(canvas) {
213
+ const opts = {
214
+ alpha: false,
215
+ depth: false,
216
+ stencil: false,
217
+ premultipliedAlpha: false,
218
+ antialias: false,
219
+ preserveDrawingBuffer: true,
220
+ powerPreference: "high-performance"
221
+ };
222
+ const gl = canvas.getContext("webgl2", opts) || canvas.getContext("experimental-webgl2", opts) || canvas.getContext("webgl", opts) || canvas.getContext("experimental-webgl", opts);
223
+ if (!gl) throw new Error("WebGL not supported");
224
+ return gl;
225
+ }
226
+ initMouseEvents() {
227
+ this.canvas.addEventListener("mousemove", this.setMousePosition.bind(this));
228
+ this.canvas.addEventListener("touchstart", this.preventDefault, { passive: false });
229
+ this.canvas.addEventListener("touchmove", this.handleTouchMove.bind(this), { passive: false });
230
+ }
231
+ setMousePosition(e) {
232
+ const mouse = {
233
+ x: e.clientX || e.pageX,
234
+ y: e.clientY || e.pageY
235
+ };
236
+ const rect = this.canvas.getBoundingClientRect();
237
+ if (mouse.x >= rect.left && mouse.x <= rect.right && mouse.y >= rect.top && mouse.y <= rect.bottom) {
238
+ this.mouseX = (mouse.x - rect.left) * this.realToCSSPixels;
239
+ this.mouseY = this.canvas.height - (mouse.y - rect.top) * this.realToCSSPixels;
240
+ }
241
+ }
242
+ handleTouchMove(e) {
243
+ e.preventDefault();
244
+ if (e.touches.length > 0) this.setMousePosition(e.touches[0]);
245
+ }
246
+ preventDefault(e) {
247
+ e.preventDefault();
248
+ }
249
+ addPass(pass) {
250
+ if (this.passes) {
251
+ let current = this.passes;
252
+ while (current.next) current = current.next;
253
+ current.next = pass;
254
+ } else this.passes = pass;
255
+ }
256
+ resizeCanvasToDisplaySize() {
257
+ const displayWidth = Math.floor(this.canvas.clientWidth * this.realToCSSPixels);
258
+ const displayHeight = Math.floor(this.canvas.clientHeight * this.realToCSSPixels);
259
+ if (this.canvas.width !== displayWidth || this.canvas.height !== displayHeight) {
260
+ this.canvas.width = displayWidth;
261
+ this.canvas.height = displayHeight;
262
+ let current = this.passes;
263
+ while (current) {
264
+ current.resize(displayWidth, displayHeight);
265
+ current = current.next;
266
+ }
267
+ }
268
+ }
269
+ updateUniforms(pass) {
270
+ pass.shader.setUniform("u_date", "4fv", [
271
+ this.now.getFullYear(),
272
+ this.now.getMonth() + 1,
273
+ this.now.getDate(),
274
+ this.now.getHours() * 3600 + this.now.getMinutes() * 60 + this.now.getSeconds() + this.now.getMilliseconds() / 1e3
275
+ ]);
276
+ pass.shader.setUniform("u_frame", "1i", this.currentFrame);
277
+ pass.shader.setUniform("u_time", "1f", this.currentTime);
278
+ pass.shader.setUniform("u_timeDelta", "1f", this.timeDelta);
279
+ pass.shader.setUniform("u_frameRate", "1f", this.frameRate);
280
+ pass.shader.setUniform("u_resolution", "2fv", [pass.width, pass.height]);
281
+ pass.shader.setUniform("u_mouse", "2fv", [this.mouseX, this.mouseY]);
282
+ }
283
+ updateTime(currentTime) {
284
+ if (this.paused) {
285
+ this.timeDelta = 0;
286
+ return;
287
+ }
288
+ const t = currentTime ?? (typeof performance !== "undefined" ? performance.now() : Date.now());
289
+ if (this.startTime === 0) this.startTime = t;
290
+ if (this.lastTime === 0) this.lastTime = t;
291
+ this.timeDelta = (t - this.startTime) / 1e3;
292
+ this.currentTime += this.timeDelta;
293
+ this.frameRate = 1 / this.timeDelta;
294
+ this.currentFrame++;
295
+ this.startTime = t;
296
+ }
297
+ render(currentTime) {
298
+ this.updateTime(currentTime);
299
+ this.resizeCanvasToDisplaySize();
300
+ let current = this.passes;
301
+ while (current) {
302
+ current.use();
303
+ this.updateUniforms(current);
304
+ current.draw();
305
+ current = current.next;
306
+ }
307
+ if (typeof requestAnimationFrame !== "undefined") this.animationRequestID = requestAnimationFrame(this.render.bind(this));
308
+ }
309
+ setup(config) {
310
+ const displayWidth = Math.floor(this.canvas.clientWidth * this.realToCSSPixels);
311
+ const displayHeight = Math.floor(this.canvas.clientHeight * this.realToCSSPixels);
312
+ config.passes.forEach((passConfig) => {
313
+ try {
314
+ const shader = new Shader(this.gl, passConfig.vertexShader, passConfig.fragmentShader, this.onError, passConfig.name);
315
+ const offscreen = passConfig.offscreen || passConfig.name !== "MainBuffer";
316
+ const pass = new Pass(this.gl, shader, displayWidth, displayHeight, offscreen, passConfig.textures.map((textureName) => this.textureMap.get(textureName)));
317
+ this.addPass(pass);
318
+ this.textureMap.set(passConfig.name, pass.texture);
319
+ } catch (error) {
320
+ console.error(`Error in pass ${passConfig.name}: ${error.message}`);
321
+ }
322
+ });
323
+ }
324
+ play() {
325
+ this.paused = false;
326
+ this.animationRequestID = requestAnimationFrame(this.render.bind(this));
327
+ }
328
+ pause() {
329
+ this.paused = true;
330
+ cancelAnimationFrame(this.animationRequestID);
331
+ }
332
+ reset() {
333
+ this.playbackTime = 0;
334
+ this.currentFrame = 0;
335
+ this.lastTime = 0;
336
+ this.frameRate = 0;
337
+ }
338
+ };
339
+ //#endregion
340
+ export { Pass, Shader, WebGLRenderer };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@actis/core",
3
+ "type": "module",
4
+ "version": "26.3.0",
5
+ "description": "A versatile WebGL renderer designed with multipass support in mind.",
6
+ "author": "Leandro Peres <leandroperes@protonmail.com>",
7
+ "license": "GPL-3.0",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/SOHNE/Actis.git",
11
+ "directory": "packages-engine/core"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/SOHNE/Actis/issues"
15
+ },
16
+ "keywords": [
17
+ "shader",
18
+ "glsl",
19
+ "multipass",
20
+ "webgl",
21
+ "canvas",
22
+ "actis"
23
+ ],
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.mjs",
28
+ "require": "./dist/index.cjs"
29
+ }
30
+ },
31
+ "main": "dist/index.cjs",
32
+ "module": "dist/index.mjs",
33
+ "types": "dist/index.d.ts",
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "devDependencies": {
38
+ "@types/webgl2": "^0.0.12",
39
+ "tsdown": "^0.21.7",
40
+ "tslib": "^2.8.1",
41
+ "typescript": "5.9.3"
42
+ },
43
+ "scripts": {
44
+ "build": "tsdown",
45
+ "dev": "tsdown --watch",
46
+ "test": "echo \"Error: no test specified\" && exit 1"
47
+ }
48
+ }