-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathquixotic.min.js
1 lines (1 loc) · 39.5 KB
/
quixotic.min.js
1
const EPSILON = 0.000001; const mat4 = { rotateZ: function(out, a, rad) { let s = Math.sin(rad); let c = Math.cos(rad); let a00 = a[0]; let a01 = a[1]; let a02 = a[2]; let a03 = a[3]; let a10 = a[4]; let a11 = a[5]; let a12 = a[6]; let a13 = a[7]; if (a !== out) { out[8] = a[8]; out[9] = a[9]; out[10] = a[10]; out[11] = a[11]; out[12] = a[12]; out[13] = a[13]; out[14] = a[14]; out[15] = a[15]; } out[0] = a00 * c + a10 * s; out[1] = a01 * c + a11 * s; out[2] = a02 * c + a12 * s; out[3] = a03 * c + a13 * s; out[4] = a10 * c - a00 * s; out[5] = a11 * c - a01 * s; out[6] = a12 * c - a02 * s; out[7] = a13 * c - a03 * s; return out; }, create: function() { let out = new Float32Array(16); out[0] = 1; out[5] = 1; out[10] = 1; out[15] = 1; return out; }, perspective: function(out, fovy, aspect, near, far) { let f = 1.0 / Math.tan(fovy / 2), nf; out[0] = f / aspect; out[1] = 0; out[2] = 0; out[3] = 0; out[4] = 0; out[5] = f; out[6] = 0; out[7] = 0; out[8] = 0; out[9] = 0; out[11] = -1; out[12] = 0; out[13] = 0; out[15] = 0; if (far !== null && far !== Infinity) { nf = 1 / (near - far); out[10] = (far + near) * nf; out[14] = (2 * far * near) * nf; } else { out[10] = -1; out[14] = -2 * near; } return out; }, translate: function(out, a, v) { let x = v[0], y = v[1], z = v[2]; if (a === out) { out[12] = a[0] * x + a[4] * y + a[8] * z + a[12]; out[13] = a[1] * x + a[5] * y + a[9] * z + a[13]; out[14] = a[2] * x + a[6] * y + a[10] * z + a[14]; out[15] = a[3] * x + a[7] * y + a[11] * z + a[15]; return out; } else { let a00, a01, a02, a03; let a10, a11, a12, a13; let a20, a21, a22, a23; a00 = a[0]; a01 = a[1]; a02 = a[2]; a03 = a[3]; a10 = a[4]; a11 = a[5]; a12 = a[6]; a13 = a[7]; a20 = a[8]; a21 = a[9]; a22 = a[10]; a23 = a[11]; out[0] = a00; out[1] = a01; out[2] = a02; out[3] = a03; out[4] = a10; out[5] = a11; out[6] = a12; out[7] = a13; out[8] = a20; out[9] = a21; out[10] = a22; out[11] = a23; out[12] = a00 * x + a10 * y + a20 * z + a[12]; out[13] = a01 * x + a11 * y + a21 * z + a[13]; out[14] = a02 * x + a12 * y + a22 * z + a[14]; out[15] = a03 * x + a13 * y + a23 * z + a[15]; return out; } }, scale: function(out, a, v) { let x = v[0], y = v[1], z = v[2]; out[0] = a[0] * x; out[1] = a[1] * x; out[2] = a[2] * x; out[3] = a[3] * x; out[4] = a[4] * y; out[5] = a[5] * y; out[6] = a[6] * y; out[7] = a[7] * y; out[8] = a[8] * z; out[9] = a[9] * z; out[10] = a[10] * z; out[11] = a[11] * z; out[12] = a[12]; out[13] = a[13]; out[14] = a[14]; out[15] = a[15]; return out; }, lookAt: function(out, eye, center, up) { let x0, x1, x2, y0, y1, y2, z0, z1, z2, len; let eyex = eye[0]; let eyey = eye[1]; let eyez = eye[2]; let upx = up[0]; let upy = up[1]; let upz = up[2]; let centerx = center[0]; let centery = center[1]; let centerz = center[2]; if (Math.abs(eyex - centerx) < EPSILON && Math.abs(eyey - centery) < EPSILON && Math.abs(eyez - centerz) < EPSILON) { return identity(out); } z0 = eyex - centerx; z1 = eyey - centery; z2 = eyez - centerz; len = 1 / Math.hypot(z0, z1, z2); z0 *= len; z1 *= len; z2 *= len; x0 = upy * z2 - upz * z1; x1 = upz * z0 - upx * z2; x2 = upx * z1 - upy * z0; len = Math.hypot(x0, x1, x2); if (!len) { x0 = 0; x1 = 0; x2 = 0; } else { len = 1 / len; x0 *= len; x1 *= len; x2 *= len; } y0 = z1 * x2 - z2 * x1; y1 = z2 * x0 - z0 * x2; y2 = z0 * x1 - z1 * x0; len = Math.hypot(y0, y1, y2); if (!len) { y0 = 0; y1 = 0; y2 = 0; } else { len = 1 / len; y0 *= len; y1 *= len; y2 *= len; } out[0] = x0; out[1] = y0; out[2] = z0; out[3] = 0; out[4] = x1; out[5] = y1; out[6] = z1; out[7] = 0; out[8] = x2; out[9] = y2; out[10] = z2; out[11] = 0; out[12] = -(x0 * eyex + x1 * eyey + x2 * eyez); out[13] = -(y0 * eyex + y1 * eyey + y2 * eyez); out[14] = -(z0 * eyex + z1 * eyey + z2 * eyez); out[15] = 1; return out; }, multiply: function(out, a, b) { let a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3]; let a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7]; let a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11]; let a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15]; let b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3]; out[0] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; out[1] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; out[2] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; out[3] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7]; out[4] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; out[5] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; out[6] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; out[7] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11]; out[8] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; out[9] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; out[10] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; out[11] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15]; out[12] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; out[13] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; out[14] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; out[15] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; return out; }, moveToVec3: function(out, v) { out[12] = v[0]; out[13] = v[1]; out[14] = v[2]; } }; const mat3 = { clone: function(a) { let out = new Float32Array(9); out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; out[4] = a[4]; out[5] = a[5]; out[6] = a[6]; out[7] = a[7]; out[8] = a[8]; return out; }, create: function() { let out = new Float32Array(9); out[0] = 1; out[4] = 1; out[8] = 1; return out; } }; const vec3 = { multiply: function(out, a, b) { out[0] = a[0] * b[0]; out[1] = a[1] * b[1]; return out; }, create: function() { return new Float32Array(3);; }, copy: function(out, a) { out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; return out; } }; const vec2 = { create: function() { return new Float32Array(2);; }, copy: function(out, a) { out[0] = a[0]; out[1] = a[1]; return out; }, fromValues: function(x, y) { let out = new Float32Array(2); out[0] = x; out[1] = y; return out; }, multiply: function(out, a, b) { out[0] = a[0] * b[0]; out[1] = a[1] * b[1]; return out; }, add: function(out, a, b) { out[0] = a[0] + b[0]; out[1] = a[1] + b[1]; return out; } }; const FRAGMENT_SHADER = ` precision highp float; varying highp vec2 vTextureCoord; varying lowp vec4 vColor; uniform sampler2D uSampler; uniform bool aUseText; void main(void) { if( aUseText ){ gl_FragColor = texture2D(uSampler, vTextureCoord); } else { gl_FragColor = vColor; } } `; const VERTEX_SHADER = ` attribute vec4 aVertexPosition; attribute vec4 aVertexColor; attribute vec2 aTextureCoord; uniform mat4 uModelViewMatrix; uniform mat4 uProjectionMatrix; uniform mat3 uTextMatrix; uniform float uPointSize; varying lowp vec4 vColor; varying highp vec2 vTextureCoord; void main(void) { gl_PointSize = uPointSize; gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition; vColor = aVertexColor; vTextureCoord = (vec3(aTextureCoord, 1)*uTextMatrix).xy; } `; const div = document.createElement("div"); function parseColor(color) { div.style.color = color; color = div.style.color.match(/(\d+){1,3}/g); if (color) { switch (color.length) { case 3: return [parseInt(color[0]) / 255, parseInt(color[1]) / 255, parseInt(color[2]) / 255, 1]; case 4: return [parseInt(color[0]) / 255, parseInt(color[1]) / 255, parseInt(color[2]) / 255, parseInt(color[3]) / 255]; } } return [0, 0, 0, 1]; } const json = {}; const text = {}; const urls = {}; delete localStorage.fetchedFiles; if (!localStorage.fetchedFiles) { localStorage.fetchedFiles = `{}`; } function checkFetch(file, response) { if (!response.ok) { if (response.status == 404) { console.error("Can't find " + file); } console.error(response.statusText); } return response; } async function fetchFile(src) { const fetchedFiles = JSON.parse(localStorage.fetchedFiles); const folders = src.split("/"); const file = folders[folders.length - 1]; let fileDefined = defineHolder(fetchedFiles, folders); if (fileDefined.next().value) { return fileDefined.next().value; } return fetch(src).then(r => checkFetch(file, r)).then(r => r.text()).then(text => { const definedFile = fileDefined.next(text).value; localStorage.fetchedFiles = JSON.stringify(fetchedFiles); return definedFile; }); } async function getFile(src) { let fileDefined = defineHolder(text, src.split("/")); if (fileDefined.next().value) { return fileDefined.next().value; } return fetchFile(src).then(fetchedFile => { const returnValue = fileDefined.next(fetchedFile).value; return returnValue; }); } function getJson(src) { let fileDefined = defineHolder(json, src.split("/")); if (fileDefined.next().value) { return fileDefined.next().value; } return fetchFile(src).then(text => { const returnValue = fileDefined.next(JSON.parse(text)).value; return returnValue; }); } function* defineHolder(object, folders) { const length = folders.length; let lastFolder = object; let defined = false; let define; for (let i = 0; i < length; i++) { const folder = folders[i]; if (i + 1 == length) { if (lastFolder[folder]) { defined = true; define = yield true; continue; } else { define = yield false; continue; } } if (!lastFolder[folder]) { lastFolder[folder] = { name: folder }; } lastFolder = lastFolder[folder]; } if (defined) { return lastFolder[folders[length - 1]]; } lastFolder[folders[length - 1]] = define; return define; } function asyncImage(url) { if (urls[url]) { return urls[url]; } const promise = new Promise(function(resolve, reject) { var image = new Image(); image.onload = () => resolve(image); image.onerror = () => reject(new Error("Error loading image")); image.src = url; }); urls[url] = promise; return promise; } function onResize(element, callback) { let elementHeight = element.height, elementWidth = element.width; setInterval(function() { if (element.height !== elementHeight || element.width !== elementWidth) { elementHeight = element.height; elementWidth = element.width; callback(); } else {} }, 16); } const engineMath = { randomBetween: function(min, max) { return min + Math.random() * (max - min); }, between: function(c, x1, x2) { return c >= x1 && c <= x2; }, lerp: function(value1, value2, amount) { amount = amount < 0 ? 0 : amount; amount = amount > 1 ? 1 : amount; return value1 + (value2 - value1) * amount; }, }; class ClearableWeakMap { constructor(init) { this._wm = new WeakMap(init); } clear() { this._wm = new WeakMap(); } delete(k) { return this._wm.delete(k); } get(k) { return this._wm.get(k); } has(k) { return this._wm.has(k); } set(k, v) { this._wm.set(k, v); return this; } } class WebglEntity { constructor() { this.matrix = mat4.create(); this.coords = vec3.create(); } translate(newCoords) { const { matrix, coords } = this; mat4.translate(matrix, matrix, newCoords); vec3.copy(coords, [matrix[12], matrix[13], matrix[14]]); } move(newCoords) { const { matrix, coords } = this; vec3.copy(coords, newCoords); mat4.moveToVec3(matrix, coords); } } class Camera extends WebglEntity { constructor(fieldOfView, aspect, zNear, zFar) { super(); if (fieldOfView == "from") { this.projection = aspect; this.matrx = zNear; this.coords = zFar; } else { this.projection = mat4.perspective(mat4.create(), fieldOfView, aspect, zNear, zFar); } } lookAt(lookAt) { const { matrix, projection, coords } = this; mat4.lookAt(matrix, coords, lookAt, [0, 1, 0]); mat4.multiply(matrix, projection, matrix); } follow(object, axes) { const newCoords = vec3.multiply([], object.coords, axes || [1, 1, 1]); newCoords[2] = this.coords[2]; super.move(newCoords); this.lookAt(object.coords); } static fromCamera(camera) { TODO.log("fix Camera.fromCamera(), need to clone instead of refrence"); const { projection, matrix, coords } = camera; return new Camera("from", projection, matrix, coords); } } class Rect extends WebglEntity { constructor() { super(); this.positionsBuffer = undefined; this.fragColorPos = undefined; this.strokeColorPos = undefined; this.strokePositionBuffer = undefined; this.vertexAttribInfo = undefined; this.vertextColorAttribInfo = undefined; this.vertexCount = undefined; this.textureInfo = undefined; this.multiTextures = false; this.strokeSize = 1; this.fillers = { fill: false, texture: false, stroke: false }; } setup(matrix, positionsBuffer, strokePositionBuffer, vertexAttribInfo, vertextColorAttribInfo, vertexCount) { this.matrix = matrix; this.positionsBuffer = positionsBuffer; this.strokePositionBuffer = strokePositionBuffer; this.vertexAttribInfo = vertexAttribInfo; this.vertextColorAttribInfo = vertextColorAttribInfo; this.vertexCount = vertexCount; return this; } scale(scale) { const matrix = this.matrix; mat4.scale(matrix, matrix, scale); return this; } attachImage(newTexture) { this.fillers.texture = true; if (this.multiTextures) { this.textureInfo.push(newTexture); return; } this.textureInfo = newTexture; this.fillers.TRIANGLE_STRIP = true; return this; } enableMultiTextures() { this.multiTextures = true; this.textureInfo = [this.textureInfo]; return this; } detachImage(i) { const { textureInfo, fillers, multiTextures } = this; if (multiTextures) { textureInfo.splice(i, 1); if (textureInfo.length <= 0) { fillers.texture = false; } return; } this.textureInfo = null; fillers.texture = false; return this; } scaleToImage(textInfo) { textInfo = textInfo ? textInfo : this.textureInfo; if (!textInfo) { return this; } const { width, height } = textInfo; if (width !== height) { if (width > height) { this.scale([width / height, 1, 1]); } else { this.scale([1, height / width, 1]); } } return this; } } class Texture { constructor() { this.matrix = mat3.create(); this.image = undefined; this.width = undefined; this.height = undefined; this.rotation = 0; this.y = 0; this.x = 0; let onload = function() {}; Object.defineProperty(this, "onload", { get() { return onload; }, set(value) { if (this.loaded) { value(); } else { onload = value; } } }); this.loaded = false; } setup(image, y, width, height, matrix, rotation) { this.image = image; this.y = y; this.width = width; this.height = height; this.rotation = 0; this.x = 0; if (matrix) { this.matrix = matrix; if (rotation) { this.rotation = rotation; } } this.loaded = true; } static from(texture) { const newTexture = new Texture(); const { image, y, width, height, matrix, rotation } = texture; newTexture.setup(image, y, width, height, mat3.clone(matrix), rotation); return newTexture; } scale(w, h) { const matrix = this.matrix; mat3.scale(matrix, matrix, [w, h]); } rotate(rad) { const matrix = this.matrix; this.rotation = (this.rotation + rad) % (Math.PI * 2); mat3.rotate(matrix, matrix, rad); } } class SpriteSheet { constructor(texture) { this.y = texture.y; this.matrix = texture.matrix; this.sprites = {}; } updateY(y) { this.y = y; const sprites = Object.values(this.sprites); for (let i = sprites.length; i--;) { sprites[i].y += y; } } update(timePassed) { const _y = this.y; const sprites = Object.values(this.sprites); for (let i = sprites.length; i--;) { const sprite = sprites[i]; const { animate, interval, coords } = sprite; if (animate) { sprite.time += timePassed; if (sprite.time > interval) { sprite.index++; sprite.time = 0; if (sprite.index == coords.length) { sprite.index = 0; } const currentCoords = coords[sprite.index]; sprite.x = currentCoords[0]; sprite.y = _y + currentCoords[1]; } } } } setSpriteAnimation(name, coords, width, height, fps) { const { matrix, sprites, y } = this; sprites[name] = { width, height, matrix, coords, x: coords[0][0], y: y + coords[0][1], sprite: true, animate: true, index: 0, time: 0, interval: 1000 / fps / 1000, }; } resetSpriteAnimation(sprite) { this.sprite[sprite].index = 0; } setSprite(name, x, _y, width, height) { const { sprites, matrix, y } = this; sprites[name] = { x, width, height, y: y + y, sprite: true, matrix: matrix }; } getSprite(name) { return this.sprites[name]; } } class Display { constructor(gl, programInfo, zAxis, texture) { this.gl = gl; this.programInfo = programInfo; this.canvas = gl.canvas; this.currentCamera = new Camera(45 * Math.PI / 180, gl.canvas.width / gl.canvas.height, 0.1, 100.0); this.currentCamera.translate([0, 0, zAxis]); this.currentCamera.lookAt([0, 0, 0]); this.zAxis = zAxis; this.drawZAxis = 0; this.last = {}; texture.textAttribInfo = { numComponents: 2, type: gl.FLOAT, normalize: false, stride: 0, offset: 0 }; this.texture = texture; this.spriteSheets = []; const context = texture.context; const canvas = texture.canvas; this.images = {}; onResize(texture.canvas, () => { const images = Object.values(this.images); for (let i = images.length; i--;) { const { image, y } = images[i]; context.drawImage(image, 0, y); } const internalFormat = gl.RGBA; gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, internalFormat, gl.UNSIGNED_BYTE, canvas); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); }); } newCamera() { return Camera.fromCamera(this.camera); } changeCamera(camera) { if (camera instanceof Camera) { this.currentCamera = camera; } else { throw new TypeError("Provided Camera isn't an instance Camera"); } } setDrawZAxis(drawZAxis) { this.drawZAxis = drawZAxis; } setSize(width, height) { const canvas = this.gl.canvas; canvas.width = width; canvas.height = height; } parseColor(color, config) { if (Array.isArray(color)) { if (config.type == "stroke") { return [...parseColor(color[1]), ...parseColor(color[0]), ...parseColor(color[2]), ...parseColor(color[3])]; } return color.map(e => parseColor(e)).flat(1); } else { const colors = []; const [r, g, b, a] = parseColor(color); for (let i = config.length - 1; i > -1; i--) { colors.push(r, g, b, a); } return colors; } } randomColor() { return "rgb(" + Math.random() * 255 + "," + Math.random() * 255 + "," + Math.random() * 255 + ")"; } clear(color) { const gl = this.gl; const [r, g, b, a] = parseColor(color, { type: "", length: 4 }); gl.clearColor(r, g, b, a); gl.clearDepth(1.0); gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LEQUAL); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); } stroke(color, width) { const drawZAxis = this.drawZAxis; const [x, y, z] = this.currentCamera.coords; this.drawZAxis = z - 1.21; const rect = this.rect(x, y, 1, 1); this.strokeRect(rect, color, width); this.drawBuffer(rect); this.drawZAxis = drawZAxis; } rect(x, y, w, h) { const { rect, stroke } = this.createRectPos(w, h); const square = new Rect(); square.setup(...this.getRectInfo(x, y, rect, stroke)); return square; } setRectSize(rect, area, h) { const { createStaticDrawBuffer, gl, createRectPos } = this; let w; if (typeof area == "object") { w = area[0]; h = area[1]; } else { w = area; } rect.positionsBuffer = createStaticDrawBuffer(gl, createRectPos(w, h).rect); } polygon(rect, x, y) { let stroke = rect; x = x ? x : 0; y = y ? y : 0; const polygon = new Rect(); polygon.setup(...this.getRectInfo(x, y, rect, stroke)); return polygon; } createSpriteSheet(src) { const texture = this.createTexture(src); const spriteSheet = new SpriteSheet(texture); texture.onload = () => spriteSheet.updateY(texture.y); this.spriteSheets.push(spriteSheet); return spriteSheet; } createTexture(src) { const { createAttribInfo, images, gl, texture: { context, canvas } } = this; if (images[src]) { return Texture.from(images[src]); } const imageInfo = new Texture(); asyncImage(src).then(image => { const { width, height } = image; const y = canvas.height; if (canvas.width < width) { canvas.width = width; } imageInfo.setup(image, y, width, height, 0); canvas.height += height; images[src] = imageInfo; imageInfo.onload(imageInfo); }).catch(console.error); return imageInfo; } fillRect(rect, color) { const { createStaticDrawBuffer, gl, parseColor } = this; rect.fillers.fill = true; if (color) { rect.fragColorPos = createStaticDrawBuffer(gl, parseColor(color, { type: "", length: rect.vertexCount })); } } strokeRect(rect, color, size) { rect.fillers.stroke = true; if (color) { rect.strokeSize = size; if (size) { const { gl, parseColor, createStaticDrawBuffer } = this; rect.strokeColorPos = createStaticDrawBuffer(gl, parseColor(color, { type: "stroke", length: rect.vertexCount })); } } else { rect.strokeSize = 1; const { gl, parseColor, createStaticDrawBuffer } = this; rect.strokeColorPos = createStaticDrawBuffer(gl, parseColor("#000", { type: "stroke", length: rect.vertexCount })); } } createRectPos(w, h) { const rect = [w / 2, h / 2, -w / 2, h / 2, w / 2, -h / 2, -w / 2, -h / 2]; const stroke = [-w / 2, h / 2, w / 2, h / 2, w / 2, -h / 2, -w / 2, -h / 2, ]; return { rect, stroke }; } getRectInfo(x, y, rect, stroke) { return this.createSquareBuffer(rect, stroke, [x, y, this.drawZAxis]); } createStaticDrawBuffer(gl, data) { const buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW); return buffer; } createSquareBuffer(positions, strokePosition, coords) { const { gl, createStaticDrawBuffer } = this; const positionsBuffer = createStaticDrawBuffer(gl, positions); const strokePositionBuffer = createStaticDrawBuffer(gl, strokePosition); const modelViewMatrix = mat4.create(); mat4.translate(modelViewMatrix, modelViewMatrix, coords); return [modelViewMatrix, positionsBuffer, strokePositionBuffer, this.createAttribInfo(2, gl.FLOAT, false, 0, 0), this.createAttribInfo(4, gl.FLOAT, false, 0, 0), positions.length / 2]; } createAttribInfo(numComponents, type, normalize, stride, offset) { return { numComponents, type, normalize, stride, offset }; } enableAttrib(buffer, attrib, gl, { numComponents, type, normalize, stride, offset }) { gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.vertexAttribPointer(attrib, numComponents, type, normalize, stride, offset); gl.enableVertexAttribArray(attrib); } drawTexture(texture, gl, canvas, enableAttrib, createStaticDrawBuffer, textAttribInfo, vertexCount, textureCoord, textMatrix, useText) { const _width = canvas.width; const _height = canvas.height; const { x, y, width, height, matrix } = texture; const realX = x / _width; const realWidth = realX + width / _width; const realHeight = y / _height; const realY = (y + height) / _height; const fragTextPos = createStaticDrawBuffer(gl, [realWidth, realHeight, realX, realHeight, realWidth, realY, realX, realY, ]); gl.uniformMatrix3fv(textMatrix, false, matrix); enableAttrib(fragTextPos, textureCoord, gl, textAttribInfo); gl.uniform1f(useText, true); gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertexCount); gl.uniform1f(useText, false); gl.disableVertexAttribArray(textureCoord); } drawBuffer(buffer) { const { gl, drawTexture, enableAttrib, createStaticDrawBuffer, currentCamera, texture: { context, canvas, textAttribInfo }, programInfo: { uniformLocations, program, attribLocations: { vertexPosition, vertexColor, textureCoord } } } = this; const cameraMatrix = currentCamera.matrix; const { positionsBuffer, fragColorPos, strokeColorPos, strokePositionBuffer, matrix, vertexAttribInfo, vertextColorAttribInfo, vertexCount, fragTextPos, fillers: { fill, stroke, texture }, strokeSize, textureInfo, multiTextures } = buffer; gl.uniformMatrix4fv(uniformLocations.projectionMatrix, false, cameraMatrix); gl.uniformMatrix4fv(uniformLocations.modelViewMatrix, false, matrix); if (stroke) { gl.lineWidth(strokeSize); enableAttrib(strokePositionBuffer, vertexPosition, gl, vertexAttribInfo); enableAttrib(strokeColorPos, vertexColor, gl, vertextColorAttribInfo); gl.drawArrays(gl.LINE_LOOP, 0, vertexCount); } if (fill) { enableAttrib(positionsBuffer, vertexPosition, gl, vertexAttribInfo); enableAttrib(fragColorPos, vertexColor, gl, vertextColorAttribInfo); gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertexCount); gl.disableVertexAttribArray(vertexColor); } if (texture) { const _width = canvas.width; const _height = canvas.height; if (multiTextures) { const length = textureInfo.length; for (let i = length; i--;) { let currentTexture = textureInfo[length - i - 1]; drawTexture(currentTexture, gl, canvas, enableAttrib, createStaticDrawBuffer, textAttribInfo, vertexCount, textureCoord, uniformLocations.textMatrix, uniformLocations.useText); } } else { drawTexture(textureInfo, gl, canvas, enableAttrib, createStaticDrawBuffer, textAttribInfo, vertexCount, textureCoord, uniformLocations.textMatrix, uniformLocations.useText); } } } static loadShader(gl, program, type, source) { const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); gl.attachShader(program, shader); } static async create(canvas, width, height, zAxis = 6) { canvas.width = width; canvas.height = height; const gl = canvas.getContext("webgl"); const shaderProgram = gl.createProgram(); Display.loadShader(gl, shaderProgram, gl.VERTEX_SHADER, VERTEX_SHADER); Display.loadShader(gl, shaderProgram, gl.FRAGMENT_SHADER, FRAGMENT_SHADER); gl.linkProgram(shaderProgram); const programInfo = { program: shaderProgram, attribLocations: { vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'), vertexColor: gl.getAttribLocation(shaderProgram, 'aVertexColor'), textureCoord: gl.getAttribLocation(shaderProgram, 'aTextureCoord'), }, uniformLocations: { projectionMatrix: gl.getUniformLocation(shaderProgram, 'uProjectionMatrix'), modelViewMatrix: gl.getUniformLocation(shaderProgram, 'uModelViewMatrix'), textMatrix: gl.getUniformLocation(shaderProgram, 'uTextMatrix'), sampler: gl.getUniformLocation(shaderProgram, 'uSampler'), useText: gl.getUniformLocation(shaderProgram, 'aUseText'), pointSize: gl.getUniformLocation(shaderProgram, 'uPointSize'), }, }; gl.useProgram(programInfo.program); gl.uniform1f(programInfo.uniformLocations.pointSize, 1.0); gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); const textureBuffer = gl.createTexture(); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, textureBuffer); gl.uniform1i(programInfo.uniformLocations.uSampler, 0); const textureCanvas = document.createElement("canvas"); textureCanvas.width = 0; textureCanvas.height = 0; let texture = { canvas: textureCanvas, buffer: textureBuffer, context: textureCanvas.getContext("2d"), }; return new Display(gl, programInfo, zAxis, texture); } } class Engine { constructor(time_step, update, render, allowedSkippedFrames) { this.accumulated_time = 0; this.animation_frame_request = undefined, this.time = undefined, this.time_step = time_step, this.updated = false; this.update = update; this.render = render; this.allowedSkippedFrames = allowedSkippedFrames; this.run = this.run.bind(this); this.end = false; } run(time_stamp) { const { accumulated_time, time, time_step, updated, update, render, allowedSkippedFrames, end } = this; this.accumulated_time += time_stamp - time; this.time = time_stamp; if (accumulated_time > time_stamp * allowedSkippedFrames) { this.accumulated_time = time_stamp; } while (this.accumulated_time >= time_step) { this.accumulated_time -= time_step; update(time_stamp); this.updated = true; } if (updated) { this.updated = false; render(time_stamp); } if (end) { return; } this.animation_frame_request = requestAnimationFrame(this.run); } start() { this.accumulated_time = this.time_step; this.time = performance.now(); this.animation_frame_request = requestAnimationFrame(this.run); } stop() { this.end = true; cancelAnimationFrame(this.animation_frame_request); } } class Controller { constructor() { this.keysPressed = {}; this.functionalKeys = {}; this.functionalKeysUp = {}; this.mouseButtons = []; this.mouseDown = { x: 0, y: 0, down: false }; this.keyDownUp = this.keyDownUp.bind(this); } keyDownUp(e) { const key = e.key, { functionalKeysUp, keysPressed } = this; e.preventDefault(); if (e.type == "keyup") { if (functionalKeysUp[key]) { functionalKeysUp[key](); } keysPressed[key] = false; } else { keysPressed[key] = true; } } mouseDownUpMove(x, y, e) { if (e.type == "mousemove") { this.mouseDown.x = x; this.mouseDown.y = y; return } this.mouseDown = { x: x, y: y, down: e.type == "mousedown" }; return this; } attachMouseButton(x, y, w, h, behavior) { this.mouseButtons.push({ x: x, y: y, w: w, h: h, call: behavior }); } detachMouseButton(i) { console.log("fix maybe?"); this.mouseButtons.splice(i); } attachKeyDown(key, call) { this.functionalKeys[key] = call; return this; } attachKeyUp(key, call) { this.functionalKeysUp[key] = call; return this; } detachKeyUp(key) { delete this.functionalKeysUp[key]; } detachKeyDown(key) { delete this.functionalKeys[key]; } update() { const { mouseDown, mouseButtons, keysPressed, functionalKeys } = this; const functionalKeysKeys = Object.keys(functionalKeys); for (let i = functionalKeysKeys.length; i--;) { let key = functionalKeysKeys[i]; if (keysPressed[key]) { functionalKeys[key](); } } if (mouseDown.down) { for (let i = mouseButtons.length; i--;) { let { x, y } = mouseDown; let button = mouseButtons[i]; if (engineMath.between(x, button.x, button.w) && engineMath.between(y, button.y, button.h)) { button.call(x, y); } } } } } class Entity extends Rect { constructor() { super(); this.velocity = vec2.create(); this.area = undefined; this.mass = 2; this.updateFillers = {}; this.delete = false; this.checkedCollide = new ClearableWeakMap(); } setup(w, h, ...args) { this.area = vec2.fromValues(w, h); super.setup(...args); return this; } remove() { this.delete = true; return this; } fill(...args) { this.updateFillers.fill = args; } stroke(...args) { this.updateFillers.stroke = args; } attachImage(image) { super.attachImage(image); if (!this.fillers.fill) { this.fill("#00000000"); } } update(deltaTime, speed) { const { checkedCollide, area, coords, velocity, velocity: [x, y] } = this; const mass = area[0] * area[1] * this.mass; const angle = Math.atan2(y, x); const length = Math.hypot(x, y); coords[0] += speed * (Math.cos(angle) * length / mass); coords[1] += speed * (Math.sin(angle) * length / mass); vec2.multiply(velocity, velocity, [deltaTime, deltaTime]); super.move(coords); checkedCollide.clear(); return this; } scale(w, h, z) { if (typeof w == "object") { h = w[1]; z = w[2]; w = w[0]; } if (!z) z = 1; super.scale([w, h, z]); const area = this.area; area[0] *= w; area[1] *= h; return this; } move(x, y) { super.move([x, y, this.coords[2]]); return this; } rotate(rad) { const matrix = this.matrix; mat4.rotateZ(matrix, matrix, rad); return this; } moveTowrd(object, acceleration) { const coords = this.coords; const newCoords = object.coords; const [x, y] = coords; const [nx, ny] = newCoords; const angle = Math.atan2(ny - y, nx - x); this.accelerate(Math.cos(angle) * acceleration, Math.sin(angle) * acceleration); return this; } setSize(w, h) { if (typeof w == "object") { h = w[1]; w = w[0]; } const area = this.area; const [_w, _h] = area; super.scale([w / _w, h / _h, 1]); area[0] = w; area[1] = h; return this; } accelerate(x, y) { const { velocity } = this; vec2.add(velocity, velocity, [x, y]); return this; } } class CollisionHandler { constructor(world) { this.world = world; } checkCollide(object) { const { world, colliding } = this; if (!world.objectsCollisionInfo[object.name]) { return; } const info = world.objectsCollisionInfo[object.name]; const allObjectsObject = world.objects; const allObjects = Object.keys(info).map(key => { if (typeof allObjectsObject[key] == "undefined") { throw new Error("Trying to get collision info of an undefined(" + key + ")object."); } return allObjectsObject[key]; }); for (let i = allObjects.length; i--;) { const currentObjectsList = allObjects[i]; if (!currentObjectsList) { continue; } if (!Array.isArray(currentObjectsList)) { this.checkCollideObjects(object, currentObjectsList, info[currentObjectsList.name]); continue; } for (let j = currentObjectsList.length; j--;) { const currentObject = currentObjectsList[j]; const name = currentObject.name; if (currentObject.delete) { allObjectsObject[name].splice(j, 1); continue; } if (currentObject == object) { continue; } this.checkCollideObjects(object, currentObject, info[name]); } } } checkCollideObjects(object1, object2, info) { const angle = this.colliding(object1, object2); if (object1.checkedCollide.has(object2) || object2.checkedCollide.has(object1)) { return; } object1.checkedCollide.set(object2, true); if (angle) { if (info.call) { object1[info.call](object2); } if (info.elastic) { const velocity = object1.velocity; const [vx1, vy1] = object1.velocity; const _velocity = object2.velocity; const [vx2, vy2] = _velocity; const PI = Math.PI; if ((angle >= 0 && angle < PI / 4) || (angle > 7 / 4 * PI && angle < PI * 2)) { if (vx1 > 0) { velocity[0] = -vx1; } if (vx2 < 0) { _velocity[0] = -vx2; } } else if (angle >= PI / 4 && angle < 3 / 4 * PI) { if (vy1 > 0) { velocity[1] = -vy1; } if (vy2 < 0) { _velocity[1] = -vy2; } } else if (angle >= 3 / 4 * PI && angle < 5 / 4 * PI) { if (vx1 < 0) { velocity[0] = -vx1; } if (vx2 > 0) { _velocity[0] = -vx2; } } else { if (vy1 < 0) { velocity[1] = -vy1; } if (vy2 > 0) { _velocity[1] = -vy2; } } } } } colliding(o1, o2) { const [x1, y1] = o1.coords; const [w1, h1] = o1.area; const halfWidth1 = w1 / 2; const halfHeight1 = h1 / 2; const left1 = x1 - halfWidth1; const right1 = x1 + halfWidth1; const top1 = y1 - halfHeight1; const bottom1 = y1 + halfHeight1; const [x2, y2] = o2.coords; const [w2, h2] = o2.area; const halfWidth2 = w2 / 2; const halfHeight2 = h2 / 2; const left2 = x2 - halfWidth2; const right2 = x2 + halfWidth2; const top2 = y2 - halfHeight2; const bottom2 = y2 + halfHeight2; if (left1 <= right2 && right1 >= left2 && top1 <= bottom2 && bottom1 >= top2) { let dx = x2 - x1; let dy = y2 - y1; let angle = Math.atan2(dy, dx); if (angle < 0) { angle += Math.PI * 2; } return angle; } } } export default class Quixotic { constructor(display, controller) { this.display = display; this.controller = controller; this.engine = undefined; this.render = undefined; this.update = undefined; this.frameRate = undefined; this.time = 0; this.onSpeedChange = () => {}; let _speed = 1; Object.defineProperty(this, "speed", { get() { return _speed; }, set(value) { _speed = value; this.onSpeedChange(value); }, }); this.speed = 1; this.world = { objects: {}, objectsCollisionInfo: {}, objectsArray: [], classesInfo: {} }; this.timePassed = 0; } newClass(Class, info) { const world = this.world; const { collision, name, objects, array, args } = info; if (collision) { world.objectsCollisionInfo[name] = collision; } world.objects[name] = {}; if (array) { world.objects[name] = []; } const classInfo = { name, }; if (args) classInfo.args = args; world.classesInfo[Class.name] = classInfo; } createEntity(Class, ...args) { const display = this.display; const { rect, stroke } = display.createRectPos(5, 5); Class = Class ? Class : Entity; const className = Class.name; if (className !== "Entity" && !Entity.prototype.isPrototypeOf(Class.prototype)) { throw new TypeError("Expected extended class of Entity. Instead got: " + className); } let instance; const { objectsArray, classesInfo, objects } = this.world; const classInfo = classesInfo[className]; if (classInfo) { if (classInfo.args) { instance = new Class(...[...classInfo.args, ...args]); } else { instance = new Class(...args); } const name = classInfo.name; if (Array.isArray(objects[name])) { objects[name].push(instance); instance.name = name; } else { console.log("Didn't save object in world.objects object, object wouldn't detect collision"); } } else { instance = new Class(...args); } instance.setup(5, 5, ...display.getRectInfo(0, 0, rect, stroke, display.randomColor())); objectsArray.push(instance); return instance; } buildWorld({ objects, classes, tileMap }) { const world = this.world; if (Array.isArray(objects)) { for (let i = objects.length - 1; i > -1; i--) { const object = objects[i]; const { name, array, amount, position, collision, args, area } = object; let createClass; if (!object.class) { createClass = Entity; } else { const className = object.class; createClass = classes[className]; world.classesInfo[className] = { name, args }; } const _args = args ? args : []; let pos; if (collision) { world.objectsCollisionInfo[name] = collision; } if (position) { let p = amount; switch (position.type) { case "random": const { rangeX, rangeY } = position; pos = function() { return [engineMath.randomBetween(...rangeX), engineMath.randomBetween(...rangeY)]; }; break; case "set": if (array) { const positions = position.positions; pos = function() { p--; return positions[p]; }; } else { pos = function() { return position.position; }; } } } if (array) { let _array = []; for (let j = amount; j--;) { const instance = this.createEntity(createClass, ..._args); instance.name = name; if (position) { instance.move(...pos()); } if (area) { instance.setSize(area); } _array.push(instance); } world.objects[name] = _array; world.objectsArray.push(..._array); } else { const instance = this.createEntity(createClass, ..._args); instance.name = name; if (position) { instance.move(...pos()); } if (area) { instance.setSize(area); } world.objects[name] = instance; world.objectsArray.push(instance); } } } if (tileMap) { const { columns, tiles, values, tileSize, startCoords: [_x, _y] } = tileMap; const rows = tiles.length / columns; let i = tiles.length - 1; for (let y = rows - 1; y > -1; y--) { for (let x = columns - 1; x > -1; x--) { const value = tiles[i]; if(!value) continue; if(Array.isArray(value)){ for(let j = value.length; j--;){ const instance = this.createEntity(values[value[value.length - j - 1]]); instance.move(x * tileSize + _x, -y * tileSize + _y); instance.setSize(tileSize, tileSize); i--; } continue; } const instance = this.createEntity(values[value]); instance.move(x * tileSize + _x, -y * tileSize + _y); instance.setSize(tileSize, tileSize); i--; } } } return; } setup(game) { const { style: { backgroundColor, backgroundImage, stroke }, world, engine: { frameRate, update, render }, setup } = game; this.buildWorld(world); this.collisionHandler = new CollisionHandler(this.world); const { collisionHandler, display, controller, entitySystem, world: { objectsArray, objects } } = this; if (backgroundImage) { display.gl.canvas.style.background = `url(${backgroundImage})`; if (repeatX || repeatY) { console.log("not read yet"); } } this.frameRate = frameRate; let lastUpdated = 0; this.update = (time) => { controller.update(); let deltaTime = time - lastUpdated; lastUpdated = time; const speed = this.speed; this.timePassed += deltaTime * speed; for (let i = objectsArray.length; i--;) { const object = objectsArray[i]; if (object.delete) { objectsArray.splice(i, 1); } collisionHandler.checkCollide(object); object.update(deltaTime / 1000, speed); } update(deltaTime / 1000, this); }; let lastRendered = 0; this.render = (timeStamp) => { const deltaTime = timeStamp - lastRendered; lastRendered = timeStamp; if (backgroundColor) display.clear(backgroundColor); const length = objectsArray.length; for (let i = objectsArray.length; i--;) { const object = objectsArray[length - i - 1]; const updateFillers = Object.entries(object.updateFillers); const fillersLength = updateFillers.length; if (fillersLength) { for (let i = fillersLength; i--;) { const [func, args] = updateFillers[fillersLength - i - 1]; display[func + "Rect"](object, ...args); } object.updateFillers = {}; } display.drawBuffer(object); } const speed = this.speed; const spriteSheets = display.spriteSheets; for (let i = spriteSheets.length; i--;) { spriteSheets[i].update(deltaTime / 1000 * speed); } render(display, this); if (stroke) { display.stroke(stroke.color, stroke.width); } }; setup(this, display, controller, this.world); this.engine = new Engine(this.frameRate, this.update, this.render, 3); this.engine.start(); return game; } end() { this.engine.stop(); } static async create({ display: { canvas, width, height, zAxis }, }) { const minLength = innerWidth > innerHeight ? innerHeight : innerWidth; if (!width) { width = minLength; } if (!height) { height = minLength; } const display = await Display.create(canvas, width, height, zAxis); const controller = new Controller(); function handleMouse(e, canvas) { controller.mouseDownUpMove(e.pageX - canvas.offsetLeft, e.pageY - canvas.offsetTop, e); } window.addEventListener("keydown", controller.keyDownUp); window.addEventListener("keyup", controller.keyDownUp); canvas.addEventListener("mousedown", (e) => handleMouse(e, canvas)); canvas.addEventListener("mouseup", (e) => handleMouse(e, canvas)); canvas.addEventListener("mousemove", (e) => handleMouse(e, canvas)); return new Quixotic(display, controller); } } export { Entity, engineMath, Rect, Texture, Camera, getJson, SpriteSheet, getFile };