├── logo.jpg ├── .gitignore ├── .travis.yml ├── .eslintrc.json ├── src ├── list.js └── renderer.js ├── LICENSE ├── example ├── index.html ├── stats.min.js ├── src │ └── app.js └── bundle.js ├── package.json ├── dist ├── renderer.js ├── renderer.m.js └── renderer.umd.js └── README.md /logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kutuluk/js13k-2d/HEAD/logo.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vscode/ 3 | dev/ 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | script: 5 | - npm run build 6 | - npm test 7 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "plugins": ["import"], 4 | "env": { 5 | "browser": true, 6 | "es6": true 7 | }, 8 | "rules": { 9 | "no-plusplus": "off", 10 | "no-param-reassign": "off", 11 | "no-bitwise": "off", 12 | "no-unused-expressions": "off" 13 | }, 14 | "globals": { 15 | "Stats": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/list.js: -------------------------------------------------------------------------------- 1 | class Node { 2 | constructor(list, cargo, next) { 3 | this.l = list; 4 | this.c = cargo; 5 | this.n = next; 6 | this.p = null; 7 | } 8 | 9 | r() { 10 | if (this.p) { 11 | this.p.n = this.n; 12 | } else { 13 | this.l.h = this.n; 14 | } 15 | this.n && (this.n.p = this.p); 16 | } 17 | } 18 | 19 | export default class List { 20 | constructor() { 21 | this.h = null; 22 | } 23 | 24 | add(cargo) { 25 | const node = new Node(this, cargo, this.h); 26 | this.h && (this.h.p = node); 27 | this.h = node; 28 | return node; 29 | } 30 | 31 | i(fn) { 32 | let node = this.h; 33 | while (node) { 34 | fn(node.c); 35 | node = node.n; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Evgeniy Pavlov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | js13k-2d example 7 | 44 | 45 | 46 | 47 | 48 |
49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js13k-2d", 3 | "description": "A 2kb webgl 2d sprite renderer, designed for Js13kGames", 4 | "keywords": [ 5 | "webgl", 6 | "2d", 7 | "renderer", 8 | "js13k" 9 | ], 10 | "repository": "github:kutuluk/js13k-2d", 11 | "author": "Evgeniy Pavlov ", 12 | "license": "MIT", 13 | "version": "0.8.0", 14 | "source": "src/renderer.js", 15 | "main": "dist/renderer.js", 16 | "module": "dist/renderer.m.js", 17 | "files": [ 18 | "src", 19 | "dist" 20 | ], 21 | "scripts": { 22 | "clean": "rimraf dist && mkdirp dist", 23 | "microbundle": "microbundle --compress true --sourcemap false --name Renderer", 24 | "example": "microbundle -i ./example/src/app.js -o ./example/bundle.js -f cjs --compress false --sourcemap false", 25 | "build": "npm-run-all --silent clean microbundle example", 26 | "lint": "eslint src", 27 | "release": "npm install && npm-run-all --silent lint build && npm publish" 28 | }, 29 | "devDependencies": { 30 | "eslint": "^5.3.0", 31 | "eslint-config-airbnb-base": "^13.1.0", 32 | "eslint-plugin-import": "^2.8.0", 33 | "microbundle": "^0.6.0", 34 | "mkdirp": "^0.5.1", 35 | "npm-run-all": "^4.1.2", 36 | "rimraf": "^2.6.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /example/stats.min.js: -------------------------------------------------------------------------------- 1 | // stats.js - http://github.com/mrdoob/stats.js 2 | (function(f,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"===typeof define&&define.amd?define(e):f.Stats=e()})(this,function(){var f=function(){function e(a){c.appendChild(a.dom);return a}function u(a){for(var d=0;dg+1E3&&(r.update(1E3*a/(c-g),100),g=c,a=0,t)){var d=performance.memory;t.update(d.usedJSHeapSize/ 4 | 1048576,d.jsHeapSizeLimit/1048576)}return c},update:function(){k=this.end()},domElement:c,setMode:u}};f.Panel=function(e,f,l){var c=Infinity,k=0,g=Math.round,a=g(window.devicePixelRatio||1),r=80*a,h=48*a,t=3*a,v=2*a,d=3*a,m=15*a,n=74*a,p=30*a,q=document.createElement("canvas");q.width=r;q.height=h;q.style.cssText="width:80px;height:48px";var b=q.getContext("2d");b.font="bold "+9*a+"px Helvetica,Arial,sans-serif";b.textBaseline="top";b.fillStyle=l;b.fillRect(0,0,r,h);b.fillStyle=f;b.fillText(e,t,v); 5 | b.fillRect(d,m,n,p);b.fillStyle=l;b.globalAlpha=.9;b.fillRect(d,m,n,p);return{dom:q,update:function(h,w){c=Math.min(c,h);k=Math.max(k,h);b.fillStyle=l;b.globalAlpha=1;b.fillRect(0,0,r,m);b.fillStyle=f;b.fillText(g(h)+" "+e+" ("+g(c)+"-"+g(k)+")",t,v);b.drawImage(q,d+a,m,n-a,p,d,m,n-a,p);b.fillRect(d+n-a,m,a,p);b.fillStyle=l;b.globalAlpha=.9;b.fillRect(d+n-a,m,a,g((1-h/w)*p))}}};return f}); 6 | -------------------------------------------------------------------------------- /dist/renderer.js: -------------------------------------------------------------------------------- 1 | var t=function(t,n,i){this.l=t,this.c=n,this.n=i,this.p=null};t.prototype.r=function(){this.p?this.p.n=this.n:this.l.h=this.n,this.n&&(this.n.p=this.p)};var n=function(){this.h=null};n.prototype.add=function(n){var i=new t(this,n,this.h);return this.h&&(this.h.p=i),this.h=i,i},n.prototype.i=function(t){for(var n=this.h;n;)t(n.c),n=n.n};var i={p:{t:0}},e=function(t){this.z=t,this.o=new n,this.t=new n};e.prototype.add=function(t){t.remove(),t.l=this,t.n=(1!==t.a||0===t.frame.p.a?this.t:this.o).add(t)};var r=function(t,n){var a=new e(0),o=[a],s=new ArrayBuffer(3407820),u=new Float32Array(s),c=new Uint32Array(s),h=r.Point,f=Object.assign({antialias:!1,alpha:!1},n),l=f.alpha?1:770,p=f.scale||1;delete f.scale;var v=t.getContext("webgl",f),d=v.getExtension("ANGLE_instanced_arrays"),g=function(t,n){var i=v.createShader(n);return v.shaderSource(i,t),v.compileShader(i),i},m=v.createProgram();v.attachShader(m,g("attribute vec2 g;\nattribute vec2 a;\nattribute vec2 t;\nattribute float r;\nattribute vec2 s;\nattribute vec4 u;\nattribute vec4 c;\nattribute float z;\nuniform mat4 m;\nvarying vec2 v;\nvarying vec4 i;\nvoid main(){\nv=u.xy+g*u.zw;\ni=c.abgr;\nvec2 p=(g-a)*s;\nfloat q=cos(r);\nfloat w=sin(r);\np=vec2(p.x*q-p.y*w,p.x*w+p.y*q);\np+=a+t;\ngl_Position=m*vec4(p,z,1);}",35633)),v.attachShader(m,g("precision mediump float;\nuniform sampler2D x;\nuniform float j;\nvarying vec2 v;\nvarying vec4 i;\nvoid main(){\nvec4 c=texture2D(x,v);\ngl_FragColor=c*i;\nif(j>0.0){\nif(c.a>>0,u[e]=t.l.z,E++}},C={gl:v,camera:{at:h(),to:h(),angle:0},background:function(t,n,i,e){v.clearColor(t,n,i,0===e?0:e||1)},layer:function(t){var n=o.find(function(n){return n.z===t});return n||(n=new e(t),o.push(n),o.sort(function(t,n){return n.z-t.z})),n},add:function(t){a.add(t)},texture:function(t,n,i,e){var r=t.width,a=t.height,o=v.createTexture();return v.bindTexture(3553,o),v.texParameteri(3553,10240,9728|+i),v.texParameteri(3553,10241,9728|+i|+e<<8|+e<<1),v.texImage2D(3553,0,6408,6408,5121,t),e&&v.generateMipmap(3553),{size:h(r,a),anchor:h(),uvs:[0,0,1,1],p:{a:0===n?0:n||1,t:o},frame:function(t,n,i){return{size:n,anchor:i||this.anchor,uvs:[t.x/r,t.y/a,n.x/r,n.y/a],p:this.p}}}},resize:F,render:function(){F();var t=C.camera,n=t.at,e=t.to,r=t.angle,a=n.x-b*e.x,s=n.y-w*e.y,u=Math.cos(r),c=Math.sin(r),h=2/b,f=-2/w,l=[u*h,c*f,0,0,-c*h,u*f,0,0,0,0,-1e-5,0,(n.x*(1-u)+n.y*c)*h-2*a/b-1,(n.y*(1-u)-n.x*c)*f+2*s/w+1,0,1];v.useProgram(m),v.enable(3042),v.enable(2929),v.uniformMatrix4fv(j,!1,l),v.viewport(0,0,b,w),v.clear(16640),z=i,P=!0,o.forEach(function(t){return t.o.i(_)}),L(),P=!1;for(var p=o.length-1;p>=0;p--)o[p].t.i(_);L()}};return F(),C};r.Point=function(){function t(t,n){if(!(this instanceof r.Point))return new r.Point(t,n);this.set(t,n)}return t.prototype.set=function(t,n){return this.x=t||0,this.y=n||(0!==n?this.x:0),this},t}(),r.Sprite=function(){function t(n,i){if(!(this instanceof t))return new t(n,i);Object.assign(this,{frame:n,visible:!0,position:r.Point(),rotation:0,scale:r.Point(1),tint:16777215,a:1,l:null,n:null},i)}var n={alpha:{configurable:!0}};return n.alpha.get=function(){return this.a},n.alpha.set=function(t){var n=t<1&&1===this.a||1===t&&this.a<1;this.a=t,n&&this.frame.p.a>0&&this.l&&this.l.add(this)},t.prototype.remove=function(){this.n&&this.n.r(),this.l=null,this.n=null},Object.defineProperties(t.prototype,n),t}(),module.exports=r; 2 | -------------------------------------------------------------------------------- /dist/renderer.m.js: -------------------------------------------------------------------------------- 1 | var t=function(t,n,i){this.l=t,this.c=n,this.n=i,this.p=null};t.prototype.r=function(){this.p?this.p.n=this.n:this.l.h=this.n,this.n&&(this.n.p=this.p)};var n=function(){this.h=null};n.prototype.add=function(n){var i=new t(this,n,this.h);return this.h&&(this.h.p=i),this.h=i,i},n.prototype.i=function(t){for(var n=this.h;n;)t(n.c),n=n.n};var i={p:{t:0}},e=function(t){this.z=t,this.o=new n,this.t=new n};e.prototype.add=function(t){t.remove(),t.l=this,t.n=(1!==t.a||0===t.frame.p.a?this.t:this.o).add(t)};var r=function(t,n){var a=new e(0),o=[a],s=new ArrayBuffer(3407820),u=new Float32Array(s),c=new Uint32Array(s),h=r.Point,f=Object.assign({antialias:!1,alpha:!1},n),l=f.alpha?1:770,p=f.scale||1;delete f.scale;var v=t.getContext("webgl",f),d=v.getExtension("ANGLE_instanced_arrays"),g=function(t,n){var i=v.createShader(n);return v.shaderSource(i,t),v.compileShader(i),i},m=v.createProgram();v.attachShader(m,g("attribute vec2 g;\nattribute vec2 a;\nattribute vec2 t;\nattribute float r;\nattribute vec2 s;\nattribute vec4 u;\nattribute vec4 c;\nattribute float z;\nuniform mat4 m;\nvarying vec2 v;\nvarying vec4 i;\nvoid main(){\nv=u.xy+g*u.zw;\ni=c.abgr;\nvec2 p=(g-a)*s;\nfloat q=cos(r);\nfloat w=sin(r);\np=vec2(p.x*q-p.y*w,p.x*w+p.y*q);\np+=a+t;\ngl_Position=m*vec4(p,z,1);}",35633)),v.attachShader(m,g("precision mediump float;\nuniform sampler2D x;\nuniform float j;\nvarying vec2 v;\nvarying vec4 i;\nvoid main(){\nvec4 c=texture2D(x,v);\ngl_FragColor=c*i;\nif(j>0.0){\nif(c.a>>0,u[e]=t.l.z,E++}},C={gl:v,camera:{at:h(),to:h(),angle:0},background:function(t,n,i,e){v.clearColor(t,n,i,0===e?0:e||1)},layer:function(t){var n=o.find(function(n){return n.z===t});return n||(n=new e(t),o.push(n),o.sort(function(t,n){return n.z-t.z})),n},add:function(t){a.add(t)},texture:function(t,n,i,e){var r=t.width,a=t.height,o=v.createTexture();return v.bindTexture(3553,o),v.texParameteri(3553,10240,9728|+i),v.texParameteri(3553,10241,9728|+i|+e<<8|+e<<1),v.texImage2D(3553,0,6408,6408,5121,t),e&&v.generateMipmap(3553),{size:h(r,a),anchor:h(),uvs:[0,0,1,1],p:{a:0===n?0:n||1,t:o},frame:function(t,n,i){return{size:n,anchor:i||this.anchor,uvs:[t.x/r,t.y/a,n.x/r,n.y/a],p:this.p}}}},resize:F,render:function(){F();var t=C.camera,n=t.at,e=t.to,r=t.angle,a=n.x-b*e.x,s=n.y-w*e.y,u=Math.cos(r),c=Math.sin(r),h=2/b,f=-2/w,l=[u*h,c*f,0,0,-c*h,u*f,0,0,0,0,-1e-5,0,(n.x*(1-u)+n.y*c)*h-2*a/b-1,(n.y*(1-u)-n.x*c)*f+2*s/w+1,0,1];v.useProgram(m),v.enable(3042),v.enable(2929),v.uniformMatrix4fv(j,!1,l),v.viewport(0,0,b,w),v.clear(16640),z=i,P=!0,o.forEach(function(t){return t.o.i(_)}),L(),P=!1;for(var p=o.length-1;p>=0;p--)o[p].t.i(_);L()}};return F(),C};r.Point=function(){function t(t,n){if(!(this instanceof r.Point))return new r.Point(t,n);this.set(t,n)}return t.prototype.set=function(t,n){return this.x=t||0,this.y=n||(0!==n?this.x:0),this},t}(),r.Sprite=function(){function t(n,i){if(!(this instanceof t))return new t(n,i);Object.assign(this,{frame:n,visible:!0,position:r.Point(),rotation:0,scale:r.Point(1),tint:16777215,a:1,l:null,n:null},i)}var n={alpha:{configurable:!0}};return n.alpha.get=function(){return this.a},n.alpha.set=function(t){var n=t<1&&1===this.a||1===t&&this.a<1;this.a=t,n&&this.frame.p.a>0&&this.l&&this.l.add(this)},t.prototype.remove=function(){this.n&&this.n.r(),this.l=null,this.n=null},Object.defineProperties(t.prototype,n),t}();export default r; 2 | -------------------------------------------------------------------------------- /dist/renderer.umd.js: -------------------------------------------------------------------------------- 1 | !function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):t.Renderer=n()}(this,function(){var t=function(t,n,e){this.l=t,this.c=n,this.n=e,this.p=null};t.prototype.r=function(){this.p?this.p.n=this.n:this.l.h=this.n,this.n&&(this.n.p=this.p)};var n=function(){this.h=null};n.prototype.add=function(n){var e=new t(this,n,this.h);return this.h&&(this.h.p=e),this.h=e,e},n.prototype.i=function(t){for(var n=this.h;n;)t(n.c),n=n.n};var e={p:{t:0}},i=function(t){this.z=t,this.o=new n,this.t=new n};i.prototype.add=function(t){t.remove(),t.l=this,t.n=(1!==t.a||0===t.frame.p.a?this.t:this.o).add(t)};var r=function(t,n){var a=new i(0),o=[a],u=new ArrayBuffer(3407820),s=new Float32Array(u),c=new Uint32Array(u),h=r.Point,f=Object.assign({antialias:!1,alpha:!1},n),l=f.alpha?1:770,p=f.scale||1;delete f.scale;var v=t.getContext("webgl",f),d=v.getExtension("ANGLE_instanced_arrays"),g=function(t,n){var e=v.createShader(n);return v.shaderSource(e,t),v.compileShader(e),e},m=v.createProgram();v.attachShader(m,g("attribute vec2 g;\nattribute vec2 a;\nattribute vec2 t;\nattribute float r;\nattribute vec2 s;\nattribute vec4 u;\nattribute vec4 c;\nattribute float z;\nuniform mat4 m;\nvarying vec2 v;\nvarying vec4 i;\nvoid main(){\nv=u.xy+g*u.zw;\ni=c.abgr;\nvec2 p=(g-a)*s;\nfloat q=cos(r);\nfloat w=sin(r);\np=vec2(p.x*q-p.y*w,p.x*w+p.y*q);\np+=a+t;\ngl_Position=m*vec4(p,z,1);}",35633)),v.attachShader(m,g("precision mediump float;\nuniform sampler2D x;\nuniform float j;\nvarying vec2 v;\nvarying vec4 i;\nvoid main(){\nvec4 c=texture2D(x,v);\ngl_FragColor=c*i;\nif(j>0.0){\nif(c.a>>0,s[i]=t.l.z,E++}},C={gl:v,camera:{at:h(),to:h(),angle:0},background:function(t,n,e,i){v.clearColor(t,n,e,0===i?0:i||1)},layer:function(t){var n=o.find(function(n){return n.z===t});return n||(n=new i(t),o.push(n),o.sort(function(t,n){return n.z-t.z})),n},add:function(t){a.add(t)},texture:function(t,n,e,i){var r=t.width,a=t.height,o=v.createTexture();return v.bindTexture(3553,o),v.texParameteri(3553,10240,9728|+e),v.texParameteri(3553,10241,9728|+e|+i<<8|+i<<1),v.texImage2D(3553,0,6408,6408,5121,t),i&&v.generateMipmap(3553),{size:h(r,a),anchor:h(),uvs:[0,0,1,1],p:{a:0===n?0:n||1,t:o},frame:function(t,n,e){return{size:n,anchor:e||this.anchor,uvs:[t.x/r,t.y/a,n.x/r,n.y/a],p:this.p}}}},resize:F,render:function(){F();var t=C.camera,n=t.at,i=t.to,r=t.angle,a=n.x-b*i.x,u=n.y-w*i.y,s=Math.cos(r),c=Math.sin(r),h=2/b,f=-2/w,l=[s*h,c*f,0,0,-c*h,s*f,0,0,0,0,-1e-5,0,(n.x*(1-s)+n.y*c)*h-2*a/b-1,(n.y*(1-s)-n.x*c)*f+2*u/w+1,0,1];v.useProgram(m),v.enable(3042),v.enable(2929),v.uniformMatrix4fv(j,!1,l),v.viewport(0,0,b,w),v.clear(16640),z=e,P=!0,o.forEach(function(t){return t.o.i(_)}),L(),P=!1;for(var p=o.length-1;p>=0;p--)o[p].t.i(_);L()}};return F(),C};return r.Point=function(){function t(t,n){if(!(this instanceof r.Point))return new r.Point(t,n);this.set(t,n)}return t.prototype.set=function(t,n){return this.x=t||0,this.y=n||(0!==n?this.x:0),this},t}(),r.Sprite=function(){function t(n,e){if(!(this instanceof t))return new t(n,e);Object.assign(this,{frame:n,visible:!0,position:r.Point(),rotation:0,scale:r.Point(1),tint:16777215,a:1,l:null,n:null},e)}var n={alpha:{configurable:!0}};return n.alpha.get=function(){return this.a},n.alpha.set=function(t){var n=t<1&&1===this.a||1===t&&this.a<1;this.a=t,n&&this.frame.p.a>0&&this.l&&this.l.add(this)},t.prototype.remove=function(){this.n&&this.n.r(),this.l=null,this.n=null},Object.defineProperties(t.prototype,n),t}(),r}); 2 | -------------------------------------------------------------------------------- /example/src/app.js: -------------------------------------------------------------------------------- 1 | import Renderer from '../../dist/renderer.m'; 2 | 3 | const { Point, Sprite } = Renderer; 4 | 5 | const stats = new Stats(); 6 | document.body.appendChild(stats.dom); 7 | 8 | const view = document.getElementById('view'); 9 | // const scene = Renderer(view, { alpha: true }); 10 | const scene = Renderer(view); 11 | const { gl } = scene; 12 | console.log(gl); 13 | 14 | scene.background(1, 1, 1, 0); 15 | 16 | scene.camera.at.set(400, 300); 17 | scene.camera.to.set(0.5); 18 | 19 | const atlasImg = () => { 20 | const canvas = document.createElement('canvas'); 21 | const size = 32; 22 | const half = size / 2; 23 | canvas.width = 128; 24 | canvas.height = 32; 25 | const ctx = canvas.getContext('2d'); 26 | 27 | let offset = 0; 28 | 29 | ctx.lineWidth = size / 16; 30 | ctx.fillStyle = '#cccccc'; 31 | ctx.strokeStyle = '#000000'; 32 | ctx.beginPath(); 33 | 34 | ctx.moveTo(offset + half, half); 35 | for (let angle = 0; angle < Math.PI * 2; angle += (Math.PI * 2) / 5) { 36 | ctx.lineTo(offset + half - Math.sin(angle) * half * 0.9, half - Math.cos(angle) * half * 0.9); 37 | } 38 | 39 | ctx.closePath(); 40 | ctx.fill(); 41 | ctx.stroke(); 42 | 43 | offset += size; 44 | 45 | ctx.beginPath(); 46 | 47 | ctx.moveTo(offset + 3, 3); 48 | ctx.lineTo(offset + size - 3, 3); 49 | ctx.lineTo(offset + size - 3, size - 3); 50 | ctx.lineTo(offset + 3, size - 3); 51 | 52 | ctx.closePath(); 53 | ctx.fill(); 54 | ctx.stroke(); 55 | 56 | offset += size; 57 | 58 | ctx.beginPath(); 59 | 60 | ctx.moveTo(offset + 3, 3); 61 | ctx.lineTo(offset + 29, 3); 62 | ctx.lineTo(offset + 29, 8); 63 | ctx.lineTo(offset + 8, 8); 64 | ctx.lineTo(offset + 8, 14); 65 | ctx.lineTo(offset + 20, 14); 66 | ctx.lineTo(offset + 20, 18); 67 | ctx.lineTo(offset + 8, 18); 68 | ctx.lineTo(offset + 8, 29); 69 | ctx.lineTo(offset + 3, 29); 70 | 71 | ctx.closePath(); 72 | ctx.fill(); 73 | ctx.stroke(); 74 | 75 | return canvas; 76 | }; 77 | 78 | const logoMask = () => { 79 | const canvas = document.createElement('canvas'); 80 | canvas.width = 800; 81 | canvas.height = 600; 82 | const ctx = canvas.getContext('2d'); 83 | 84 | ctx.fillStyle = '#ffffff'; 85 | ctx.beginPath(); 86 | 87 | ctx.moveTo(400, 300); 88 | for (let angle = 0; angle < Math.PI * 2; angle += (Math.PI * 2) / 5) { 89 | ctx.lineTo(400 - Math.sin(angle) * 250, 300 - Math.cos(angle) * 250); 90 | } 91 | 92 | ctx.closePath(); 93 | ctx.fill(); 94 | 95 | const { data } = ctx.getImageData(0, 0, 800, 600); 96 | 97 | return (x, y) => data[(y * 800 + x) * 4] > 0; 98 | }; 99 | 100 | const atlas = scene.texture(atlasImg(), 0.5); 101 | atlas.anchor = Point(0.5); 102 | 103 | const bFrame = atlas.frame(Point(), Point(32)); 104 | const qFrame = atlas.frame(Point(32, 0), Point(32)); 105 | const fFrame = atlas.frame(Point(64, 0), Point(32)); 106 | 107 | const frames = [atlas, bFrame, qFrame, fFrame]; 108 | 109 | let len = 0; 110 | 111 | const sprs = []; 112 | 113 | const mask = logoMask(); 114 | 115 | let cl = 0; 116 | 117 | const addSprite = (a) => { 118 | if (len % 250 === 0) { 119 | cl++; 120 | } 121 | 122 | const layer = scene.layer(cl); 123 | 124 | len += a; 125 | for (let i = 0; i < a; i++) { 126 | const sprite = Sprite(frames[i % 4]); 127 | 128 | let x = 0; 129 | let y = 0; 130 | 131 | while (!mask(x, y)) { 132 | x = ~~(800 * Math.random()); 133 | y = ~~(600 * Math.random()); 134 | } 135 | 136 | sprite.position.set(x, y); 137 | sprite.tint = Math.random() * 0xffffff; 138 | sprite.rotation = Math.random() * Math.PI * 2; 139 | // sprite.scale.set(0.5); 140 | // sprite.dr = (0.5 - Math.random()) * 0.1; 141 | // sprite.trans = !Math.round(Math.random()); 142 | // sprite.alpha = !Math.round(Math.random()) ? 1 : 0.8; 143 | sprs.push(sprite); 144 | layer.add(sprite); 145 | } 146 | }; 147 | 148 | const sprites = document.getElementById('info'); 149 | 150 | const dbgRenderInfo = gl.getExtension('WEBGL_debug_renderer_info'); 151 | const info = gl.getParameter(dbgRenderInfo ? dbgRenderInfo.UNMASKED_RENDERER_WEBGL : gl.VENDOR); 152 | 153 | let add = false; 154 | 155 | view.onmousedown = () => { 156 | add = true; 157 | }; 158 | view.ontouchstart = () => { 159 | add = true; 160 | }; 161 | 162 | view.onmouseup = () => { 163 | add = false; 164 | }; 165 | view.ontouchend = () => { 166 | add = false; 167 | }; 168 | 169 | const loop = () => { 170 | stats.begin(); 171 | 172 | if (len < 3000 || add) addSprite(25); 173 | 174 | sprites.innerHTML = `Renderer: ${info}
Sprites: ${len} (click to add)`; 175 | 176 | /* 177 | sprs.forEach((sprite) => { 178 | sprite.dr && (sprite.rotation += sprite.dr); 179 | if (sprite.trans && sprite.alpha > 0.8) { 180 | sprite.alpha -= 0.001; 181 | } 182 | }); 183 | */ 184 | 185 | scene.camera.angle += 0.005; 186 | 187 | scene.render(); 188 | stats.end(); 189 | 190 | requestAnimationFrame(loop); 191 | }; 192 | 193 | loop(); 194 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > ![logo](https://raw.githubusercontent.com/kutuluk/js13k-2d/master/logo.jpg 'logo') 2 | > 3 | > A 2kb webgl 2d sprite renderer, designed for [Js13kGames](http://js13kgames.com). 4 | 5 | [![NPM version](https://img.shields.io/npm/v/js13k-2d.svg?style=flat-square)](https://www.npmjs.com/package/js13k-2d) 6 | 7 | - **Tiny:** weighs about 2 kilobyte gzipped 8 | - **Extremely fast:** tens of thousands sprites onscreen at 60 fps 9 | 10 | ## Demo 11 | 12 | [Live examples](https://kutuluk.github.io/js13k-2d) 13 | 14 | ## Install 15 | 16 | ```sh 17 | $ npm install js13k-2d 18 | ``` 19 | 20 | Then with a module bundler like [rollup](http://rollupjs.org/) or [webpack](https://webpack.js.org/), use as you would anything else: 21 | 22 | ```javascript 23 | // using ES6 modules 24 | import Renderer from 'js13k-2d'; 25 | 26 | // using CommonJS modules 27 | var Renderer = require('js13k-2d'); 28 | ``` 29 | 30 | The [UMD](https://github.com/umdjs/umd) build is also available on [unpkg](https://unpkg.com): 31 | 32 | ```html 33 | 34 | ``` 35 | 36 | You can find the library on `window.Renderer`. 37 | 38 | ## Usage 39 | 40 | ```javascript 41 | // Import the library 42 | import Renderer from 'js13k-2d'; 43 | 44 | // Extract classes 45 | const { Point, Sprite } = Renderer; 46 | 47 | // Get canvas element, where the scene will be rendered to. 48 | const view = document.getElementById('view'); 49 | 50 | // Create a scene 51 | const scene = Renderer(view); 52 | 53 | // Set background color 54 | scene.background(1, 1, 1); 55 | 56 | // Create a texture 57 | const atlas = scene.texture(image); 58 | 59 | // Create a frame 60 | const frame = atlas.frame(Point(), Point(32)); 61 | 62 | // Create a sprite 63 | const sprite = Sprite(frame); 64 | 65 | // Add a sprite to the scene 66 | scene.add(sprite); 67 | 68 | const loop = () => { 69 | // Get the actual canvas size 70 | scene.resize(); 71 | const { width, height } = view; // or scene.gl.canvas 72 | 73 | // Change sprite position 74 | sprite.position.set(Math.random() * width, Math.random() * height); 75 | 76 | // Render a scene 77 | scene.render(); 78 | 79 | requestAnimationFrame(loop); 80 | }; 81 | 82 | loop(); 83 | ``` 84 | 85 | > For a better understanding of how to use the library, read along or see example folder and have a look at the [live examples](https://kutuluk.github.io/js13k-2d). 86 | 87 | ## API (in progress) 88 | 89 | **This library is under development and should be considered as an unstable. There are no guarantees regarding API stability until the release 1.0.** 90 | 91 | ## Renderer.Point 92 | 93 | The Point object represents a location in a two-dimensional coordinate system, where x represents the horizontal axis and y represents the vertical axis. The class provides the most minimal functionality. 94 | 95 | ### `new Renderer.Point(x, y)` 96 | 97 | Creates a point with a `x` and `y` position. If `y` is omitted, both `x` and `y` will be set to `x` (0 by default). The presence of the keyword `new` is optional, so it is recommended to omit it for size reduction. 98 | 99 | ##### Properties 100 | 101 | #### `x: number` 102 | 103 | Position of the point on the x axis. 104 | 105 | #### `y: number` 106 | 107 | Position of the point on the y axis. 108 | 109 | ##### Methods 110 | 111 | ### `set(x, y): this` 112 | 113 | Sets the point to a new `x` and `y` position. If `y` is omitted, both `x` and `y` will be set to `x` (0 by default). 114 | 115 | ##### Tips 116 | 117 | For a smaller size reduction, you can use this class as the base for your vector class: 118 | 119 | ```javascript 120 | class Vector extends Renderer.Point { 121 | clone() { 122 | return new Vector(this.x, this.y); 123 | } 124 | 125 | copy(vec) { 126 | this.x = vec.x; 127 | this.y = vec.y; 128 | return this; 129 | } 130 | 131 | add(vec) { 132 | this.x += vec.x; 133 | this.y += vec.y; 134 | return this; 135 | } 136 | 137 | cross(vec) { 138 | return this.x * vec.y - this.y * vec.x; 139 | } 140 | 141 | dot(vec) { 142 | return this.x * vec.x + this.y * vec.y; 143 | } 144 | 145 | // etc... 146 | } 147 | ``` 148 | 149 | And even override Renderer.Point. **Note**: you need to do this before calling Renderer. 150 | 151 | ```javascript 152 | Renderer.Point = Vector; 153 | 154 | // then 155 | const view = document.getElementById('view'); 156 | const scene = Renderer(view); 157 | 158 | console.log(new Renderer.Point() instanceof Vector); // true 159 | console.log(scene.camera.at instanceof Vector); // true 160 | console.log(Renderer.Sprite(frame).position instanceof Vector); // true 161 | // etc 162 | ``` 163 | 164 | ## Renderer(canvas, options) 165 | 166 | Returns an Renderer instance. 167 | 168 | ##### Parameters 169 | 170 | - `canvas` - The element where the scene will be rendered to. The provided element has to be `` otherwise it won't work. 171 | - `options`: 172 | 173 | - `scale : number` - The resolution multiplier by which the scene is rendered relative to the canvas' resolution. Use `window.devicePixelRatio` for the highest possible quality, `1` for the best performance. Default `1`. 174 | - `alpha : boolean` - Default `false`. 175 | - `antialias : boolean` - Default `false`. 176 | 177 | See [WebGL context attributes](https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement/getContext). Note that the default values ​​for `alpha` and `antialias` are overridden to `false`. 178 | 179 | ##### Properties 180 | 181 | #### `gl: WebGLRenderingContext` 182 | 183 | [MDN](https://developer.mozilla.org/docs/Web/API/WebGLRenderingContext) 184 | 185 | #### `camera` 186 | 187 | The object that defines the camera. Has the following properties: 188 | 189 | - `at: Renderer.Point` - The position that the camera is looking at. 190 | - `to: Renderer.Point` - The position on the screen in which the point the camera is looking at is displayed. Values ​​from 0 to 1. 191 | - `angle: number` - The angle in radians on which the camera is rotated. 192 | 193 | ##### Methods 194 | 195 | #### `background(r, g, b, a)` 196 | 197 | Sets the background color. Values ​​from 0 to 1. 198 | 199 | #### `texture(source, alphaTest, smooth, mipmap)` 200 | 201 | Creates a texture object that stores the information that represents an image. 202 | 203 | ##### Parameters 204 | 205 | - `source` - the image or the canvas 206 | - `alphaTest: number` - the value of the alpha component of the texture pixel below which the pixel is considered completely transparent and is not displayed. The pixels with the alpha component equal to or greater than the alphaTest are displayed opaque. When the alphaTest value is 0, the texture is displayed in blend mode. 207 | - `smooth: boolean` - smooth texture 208 | - `mipmap: boolean` - generate mipmap for texture 209 | 210 | Together, `smooth` and `mipmap` are provided with 4 modes of [texParameter](https://developer.mozilla.org/docs/Web/API/WebGLRenderingContext/texParameter): `NEAREST`, `LINEAR`, `NEAREST_MIPMAP_LINEAR` and `LINEAR_MIPMAP_LINEAR`. **Note:** if `mipmap` is set to true, then the width and height of the `source` must be a power of 2. 211 | 212 | The Texture object cannot be added to the display list directly. Instead use it as the texture for a Sprite and Frame. You can directly create a texture from an image and then reuse it multiple times like this: 213 | 214 | ```javascript 215 | const { Point, Sprite } = Renderer; 216 | 217 | const scene = Renderer(view); 218 | 219 | const texture = scene.texture(image); 220 | 221 | const sprite1 = Sprite(texture); 222 | sprite1.position.set(100, 100); 223 | 224 | const sprite2 = Sprite(texture, { 225 | position: Point(100, 200), 226 | }); 227 | 228 | scene.add(sprite1); 229 | scene.add(sprite2); 230 | 231 | scene.render(); 232 | ``` 233 | 234 | ##### Texture properties 235 | 236 | #### `texture.anchor: Renderer.Point` 237 | 238 | The anchor sets the origin point of the texture. The default is `{0,0}` this means the texture's origin is the top left. Setting the anchor to `{0.5,0.5}` means the texture's origin is centered. Setting the anchor to `{1,1}` would mean the texture's origin point will be the bottom right corner. It's also the pivot point of the sprite that it rotates around. 239 | 240 | #### `texture.size: Renderer.Point` 241 | 242 | Determines the size of the texture in pixels with sprite.scale equal to (1, 1). When creating a texture, its size is set equal to the size of the source. 243 | 244 | ##### Texture methods 245 | 246 | #### `texture.frame(origin, size, anchor)` 247 | 248 | Creates a Frame object that stores the information that represents part of an image. 249 | 250 | ##### Parameters 251 | 252 | - `origin: Renderer.Point` - the coordinates of the upper left edge of the frame 253 | - `size: Renderer.Point` - the size of the frame 254 | - `anchor: Renderer.Point` - the anchor of the frame. If anchor not present, the anchor of texture will be used. 255 | 256 | The Frame object cannot be added to the display list directly. Instead use it as the texture for a Sprite. The Frame has the same set of properties as the texture. 257 | 258 | Example: 259 | 260 | ```javascript 261 | const { Point, Sprite } = Renderer; 262 | 263 | const scene = Renderer(view); 264 | 265 | const atlas = scene.texture(image); 266 | 267 | const frame1 = atlas.frame(Point(), Point(32)); 268 | const frame2 = atlas.frame(Point(32, 0), Point(32)); 269 | 270 | const sprite1 = Sprite(frame1); 271 | sprite1.position.set(100, 100); 272 | 273 | const sprite2 = Sprite(frame2, { 274 | position: Point(100, 200), 275 | }); 276 | 277 | scene.add(sprite1); 278 | scene.add(sprite2); 279 | 280 | scene.render(); 281 | ``` 282 | 283 | #### `layer(z)` 284 | 285 | Gets or creates a layer from the given `z`. 286 | 287 | #### `add(sprite)` 288 | 289 | Adds a sprite to the layer with `z` equal to 0. 290 | 291 | #### `resize()` 292 | 293 | Makes inner canvas size equal to the displayed size of the canvas. Returns `true` if the change was made and `false` otherwise. This method is called inside render automatically. Directly it needs to be called only if it is necessary to get the actual canvas size before render or to determine the fact of changing the size of the canvas. 294 | 295 | #### `render()` 296 | 297 | Displays all the sprites with the visible field set to true. 298 | 299 | ## Renderer.Layer 300 | 301 | ##### Properties 302 | 303 | #### `z: number` 304 | 305 | Z-index of the layer. The larger, the higher the layer is. 306 | 307 | ##### Methods 308 | 309 | #### `add(sprite)` 310 | 311 | Adds the sprite to the layer. If the sprite is already present in another layer, it will be removed from it. 312 | 313 | ## Renderer.Sprite 314 | 315 | The Sprite object is the textured objects that are rendered to the screen. 316 | 317 | ### `new Renderer.Sprite(frame, props)` 318 | 319 | Creates a sprite from the frame with the specified properties. The presence of the keyword `new` is optional, so it is recommended to omit it for size reduction. 320 | 321 | ##### Properties 322 | 323 | #### `frame: frame or texture` 324 | 325 | #### `alpha: number` 326 | 327 | The opacity of the sprite. 328 | 329 | #### `position: Renderer.Point` 330 | 331 | The coordinates of the sprite. 332 | 333 | #### `rotation: number` 334 | 335 | The rotation of the sprite in radians. 336 | 337 | #### `scale: Renderer.Point` 338 | 339 | The scale factor of the sprite. 340 | 341 | #### `tint: number` 342 | 343 | The tint applied to the sprite. This is a hex value. A value of `0xffffff` will remove any tint effect. 344 | 345 | #### `visible: boolean` 346 | 347 | The visibility of the sprite. 348 | 349 | ##### Methods 350 | 351 | ### `remove()` 352 | 353 | Removes the sprite from the scene. 354 | -------------------------------------------------------------------------------- /example/bundle.js: -------------------------------------------------------------------------------- 1 | var t = function (t, n, i) { 2 | this.l = t, this.c = n, this.n = i, this.p = null; 3 | }; 4 | t.prototype.r = function () { 5 | this.p ? (this.p.n = this.n) : (this.l.h = this.n), this.n && (this.n.p = this.p); 6 | }; 7 | var n = function () { 8 | this.h = null; 9 | }; 10 | n.prototype.add = function (n) { 11 | var i = new t(this, n, this.h); 12 | return this.h && (this.h.p = i), this.h = i, i; 13 | }, n.prototype.i = function (t) { 14 | for (var n = this.h;n; ) 15 | { t(n.c), n = n.n; } 16 | }; 17 | var i = { 18 | p: { 19 | t: 0 20 | } 21 | }, e = function (t) { 22 | this.z = t, this.o = new n(), this.t = new n(); 23 | }; 24 | e.prototype.add = function (t) { 25 | t.remove(), t.l = this, t.n = (1 !== t.a || 0 === t.frame.p.a ? this.t : this.o).add(t); 26 | }; 27 | var r = function (t, n) { 28 | var a = new e(0), o = [a], s = new ArrayBuffer(3407820), u = new Float32Array(s), c = new Uint32Array(s), h = r.Point, f = Object.assign({ 29 | antialias: !1, 30 | alpha: !1 31 | }, n), l = f.alpha ? 1 : 770, p = f.scale || 1; 32 | delete f.scale; 33 | var v = t.getContext("webgl", f), d = v.getExtension("ANGLE_instanced_arrays"), g = function (t, n) { 34 | var i = v.createShader(n); 35 | return v.shaderSource(i, t), v.compileShader(i), i; 36 | }, m = v.createProgram(); 37 | v.attachShader(m, g("attribute vec2 g;\nattribute vec2 a;\nattribute vec2 t;\nattribute float r;\nattribute vec2 s;\nattribute vec4 u;\nattribute vec4 c;\nattribute float z;\nuniform mat4 m;\nvarying vec2 v;\nvarying vec4 i;\nvoid main(){\nv=u.xy+g*u.zw;\ni=c.abgr;\nvec2 p=(g-a)*s;\nfloat q=cos(r);\nfloat w=sin(r);\np=vec2(p.x*q-p.y*w,p.x*w+p.y*q);\np+=a+t;\ngl_Position=m*vec4(p,z,1);}", 35633)), v.attachShader(m, g("precision mediump float;\nuniform sampler2D x;\nuniform float j;\nvarying vec2 v;\nvarying vec4 i;\nvoid main(){\nvec4 c=texture2D(x,v);\ngl_FragColor=c*i;\nif(j>0.0){\nif(c.a>> 0, u[e] = t.l.z, E++; 61 | } 62 | }, C = { 63 | gl: v, 64 | camera: { 65 | at: h(), 66 | to: h(), 67 | angle: 0 68 | }, 69 | background: function (t, n, i, e) { 70 | v.clearColor(t, n, i, 0 === e ? 0 : e || 1); 71 | }, 72 | layer: function (t) { 73 | var n = o.find(function (n) { 74 | return n.z === t; 75 | }); 76 | return n || (n = new e(t), o.push(n), o.sort(function (t, n) { 77 | return n.z - t.z; 78 | })), n; 79 | }, 80 | add: function (t) { 81 | a.add(t); 82 | }, 83 | texture: function (t, n, i, e) { 84 | var r = t.width, a = t.height, o = v.createTexture(); 85 | return v.bindTexture(3553, o), v.texParameteri(3553, 10240, 9728 | +i), v.texParameteri(3553, 10241, 9728 | +i | +e << 8 | +e << 1), v.texImage2D(3553, 0, 6408, 6408, 5121, t), e && v.generateMipmap(3553), { 86 | size: h(r, a), 87 | anchor: h(), 88 | uvs: [0,0,1,1], 89 | p: { 90 | a: 0 === n ? 0 : n || 1, 91 | t: o 92 | }, 93 | frame: function (t, n, i) { 94 | return { 95 | size: n, 96 | anchor: i || this.anchor, 97 | uvs: [t.x / r,t.y / a,n.x / r,n.y / a], 98 | p: this.p 99 | }; 100 | } 101 | }; 102 | }, 103 | resize: F, 104 | render: function () { 105 | F(); 106 | var t = C.camera, n = t.at, e = t.to, r = t.angle, a = n.x - b * e.x, s = n.y - w * e.y, u = Math.cos(r), c = Math.sin(r), h = 2 / b, f = -2 / w, l = [u * h, 107 | c * f,0,0,-c * h,u * f,0,0,0,0,-1e-5,0,(n.x * (1 - u) + n.y * c) * h - 2 * a / b - 1, 108 | (n.y * (1 - u) - n.x * c) * f + 2 * s / w + 1,0,1]; 109 | v.useProgram(m), v.enable(3042), v.enable(2929), v.uniformMatrix4fv(j, !1, l), v.viewport(0, 0, b, w), v.clear(16640), z = i, P = !0, o.forEach(function (t) { 110 | return t.o.i(_); 111 | }), L(), P = !1; 112 | for (var p = o.length - 1;p >= 0; p--) 113 | { o[p].t.i(_); } 114 | L(); 115 | } 116 | }; 117 | return F(), C; 118 | }; 119 | r.Point = (function () { 120 | function t(t, n) { 121 | if (!(this instanceof r.Point)) 122 | { return new r.Point(t, n); } 123 | this.set(t, n); 124 | } 125 | 126 | return t.prototype.set = function (t, n) { 127 | return this.x = t || 0, this.y = n || (0 !== n ? this.x : 0), this; 128 | }, t; 129 | })(), r.Sprite = (function () { 130 | function t(n, i) { 131 | if (!(this instanceof t)) 132 | { return new t(n, i); } 133 | Object.assign(this, { 134 | frame: n, 135 | visible: !0, 136 | position: r.Point(), 137 | rotation: 0, 138 | scale: r.Point(1), 139 | tint: 16777215, 140 | a: 1, 141 | l: null, 142 | n: null 143 | }, i); 144 | } 145 | 146 | var n = { 147 | alpha: { 148 | configurable: !0 149 | } 150 | }; 151 | return n.alpha.get = function () { 152 | return this.a; 153 | }, n.alpha.set = function (t) { 154 | var n = t < 1 && 1 === this.a || 1 === t && this.a < 1; 155 | this.a = t, n && this.frame.p.a > 0 && this.l && this.l.add(this); 156 | }, t.prototype.remove = function () { 157 | this.n && this.n.r(), this.l = null, this.n = null; 158 | }, Object.defineProperties(t.prototype, n), t; 159 | })(); 160 | 161 | var Point = r.Point; 162 | var Sprite = r.Sprite; 163 | var stats = new Stats(); 164 | document.body.appendChild(stats.dom); 165 | var view = document.getElementById('view'); 166 | var scene = r(view); 167 | var gl = scene.gl; 168 | console.log(gl); 169 | scene.background(1, 1, 1, 0); 170 | scene.camera.at.set(400, 300); 171 | scene.camera.to.set(0.5); 172 | var atlasImg = function () { 173 | var canvas = document.createElement('canvas'); 174 | var size = 32; 175 | var half = size / 2; 176 | canvas.width = 128; 177 | canvas.height = 32; 178 | var ctx = canvas.getContext('2d'); 179 | var offset = 0; 180 | ctx.lineWidth = size / 16; 181 | ctx.fillStyle = '#cccccc'; 182 | ctx.strokeStyle = '#000000'; 183 | ctx.beginPath(); 184 | ctx.moveTo(offset + half, half); 185 | for (var angle = 0;angle < Math.PI * 2; angle += Math.PI * 2 / 5) { 186 | ctx.lineTo(offset + half - Math.sin(angle) * half * 0.9, half - Math.cos(angle) * half * 0.9); 187 | } 188 | ctx.closePath(); 189 | ctx.fill(); 190 | ctx.stroke(); 191 | offset += size; 192 | ctx.beginPath(); 193 | ctx.moveTo(offset + 3, 3); 194 | ctx.lineTo(offset + size - 3, 3); 195 | ctx.lineTo(offset + size - 3, size - 3); 196 | ctx.lineTo(offset + 3, size - 3); 197 | ctx.closePath(); 198 | ctx.fill(); 199 | ctx.stroke(); 200 | offset += size; 201 | ctx.beginPath(); 202 | ctx.moveTo(offset + 3, 3); 203 | ctx.lineTo(offset + 29, 3); 204 | ctx.lineTo(offset + 29, 8); 205 | ctx.lineTo(offset + 8, 8); 206 | ctx.lineTo(offset + 8, 14); 207 | ctx.lineTo(offset + 20, 14); 208 | ctx.lineTo(offset + 20, 18); 209 | ctx.lineTo(offset + 8, 18); 210 | ctx.lineTo(offset + 8, 29); 211 | ctx.lineTo(offset + 3, 29); 212 | ctx.closePath(); 213 | ctx.fill(); 214 | ctx.stroke(); 215 | return canvas; 216 | }; 217 | var logoMask = function () { 218 | var canvas = document.createElement('canvas'); 219 | canvas.width = 800; 220 | canvas.height = 600; 221 | var ctx = canvas.getContext('2d'); 222 | ctx.fillStyle = '#ffffff'; 223 | ctx.beginPath(); 224 | ctx.moveTo(400, 300); 225 | for (var angle = 0;angle < Math.PI * 2; angle += Math.PI * 2 / 5) { 226 | ctx.lineTo(400 - Math.sin(angle) * 250, 300 - Math.cos(angle) * 250); 227 | } 228 | ctx.closePath(); 229 | ctx.fill(); 230 | var ref = ctx.getImageData(0, 0, 800, 600); 231 | var data = ref.data; 232 | return function (x, y) { return data[(y * 800 + x) * 4] > 0; }; 233 | }; 234 | var atlas = scene.texture(atlasImg(), 0.5); 235 | atlas.anchor = Point(0.5); 236 | var bFrame = atlas.frame(Point(), Point(32)); 237 | var qFrame = atlas.frame(Point(32, 0), Point(32)); 238 | var fFrame = atlas.frame(Point(64, 0), Point(32)); 239 | var frames = [atlas,bFrame,qFrame,fFrame]; 240 | var len = 0; 241 | var mask = logoMask(); 242 | var cl = 0; 243 | var addSprite = function (a) { 244 | if (len % 250 === 0) { 245 | cl++; 246 | } 247 | var layer = scene.layer(cl); 248 | len += a; 249 | for (var i = 0;i < a; i++) { 250 | var sprite = Sprite(frames[i % 4]); 251 | var x = 0; 252 | var y = 0; 253 | while (!mask(x, y)) { 254 | x = ~(~(800 * Math.random())); 255 | y = ~(~(600 * Math.random())); 256 | } 257 | sprite.position.set(x, y); 258 | sprite.tint = Math.random() * 0xffffff; 259 | sprite.rotation = Math.random() * Math.PI * 2; 260 | layer.add(sprite); 261 | } 262 | }; 263 | var sprites = document.getElementById('info'); 264 | var dbgRenderInfo = gl.getExtension('WEBGL_debug_renderer_info'); 265 | var info = gl.getParameter(dbgRenderInfo ? dbgRenderInfo.UNMASKED_RENDERER_WEBGL : gl.VENDOR); 266 | var add = false; 267 | view.onmousedown = (function () { 268 | add = true; 269 | }); 270 | view.ontouchstart = (function () { 271 | add = true; 272 | }); 273 | view.onmouseup = (function () { 274 | add = false; 275 | }); 276 | view.ontouchend = (function () { 277 | add = false; 278 | }); 279 | var loop = function () { 280 | stats.begin(); 281 | if (len < 3000 || add) 282 | { addSprite(25); } 283 | sprites.innerHTML = "Renderer: " + info + "
Sprites: " + len + " (click to add)"; 284 | scene.camera.angle += 0.005; 285 | scene.render(); 286 | stats.end(); 287 | requestAnimationFrame(loop); 288 | }; 289 | loop(); 290 | -------------------------------------------------------------------------------- /src/renderer.js: -------------------------------------------------------------------------------- 1 | import List from './list'; 2 | 3 | // const DEVELOPMENT = process.env.NODE_ENV === 'development'; 4 | 5 | const GL_VERTEX_SHADER = 35633; 6 | const GL_FRAGMENT_SHADER = 35632; 7 | const GL_ARRAY_BUFFER = 34962; 8 | const GL_ELEMENT_ARRAY_BUFFER = 34963; 9 | const GL_STATIC_DRAW = 35044; 10 | const GL_DYNAMI_CDRAW = 35048; 11 | const GL_RGBA = 6408; 12 | const GL_UNSIGNED_BYTE = 5121; 13 | const GL_FLOAT = 5126; 14 | const GL_TRIANGLES = 4; 15 | const GL_DEPTH_TEST = 2929; 16 | const GL_LESS = 513; 17 | const GL_LEQUAL = 515; 18 | const GL_BLEND = 3042; 19 | const GL_ZERO = 0; 20 | const GL_ONE = 1; 21 | const GL_SRC_ALPHA = 770; 22 | const GL_ONE_MINUS_SRC_ALPHA = 771; 23 | const GL_COLOR_BUFFER_BIT = 16384; 24 | const GL_DEPTH_BUFFER_BIT = 256; 25 | const GL_TEXTURE_2D = 3553; 26 | const GL_NEAREST = 9728; 27 | const GL_TEXTURE_MAG_FILTER = 10240; 28 | const GL_TEXTURE_MIN_FILTER = 10241; 29 | 30 | const vertexShader = `attribute vec2 g; 31 | attribute vec2 a; 32 | attribute vec2 t; 33 | attribute float r; 34 | attribute vec2 s; 35 | attribute vec4 u; 36 | attribute vec4 c; 37 | attribute float z; 38 | uniform mat4 m; 39 | varying vec2 v; 40 | varying vec4 i; 41 | void main(){ 42 | v=u.xy+g*u.zw; 43 | i=c.abgr; 44 | vec2 p=(g-a)*s; 45 | float q=cos(r); 46 | float w=sin(r); 47 | p=vec2(p.x*q-p.y*w,p.x*w+p.y*q); 48 | p+=a+t; 49 | gl_Position=m*vec4(p,z,1);}`; 50 | 51 | const fragmentShader = `precision mediump float; 52 | uniform sampler2D x; 53 | uniform float j; 54 | varying vec2 v; 55 | varying vec4 i; 56 | void main(){ 57 | vec4 c=texture2D(x,v); 58 | gl_FragColor=c*i; 59 | if(j>0.0){ 60 | if(c.a { 90 | const zeroLayer = new Layer(0); 91 | const layers = [zeroLayer]; 92 | 93 | const floatSize = 2 + 2 + 1 + 2 + 4 + 1 + 1; 94 | const byteSize = floatSize * 4; 95 | const arrayBuffer = new ArrayBuffer(maxBatch * byteSize); 96 | const floatView = new Float32Array(arrayBuffer); 97 | const uintView = new Uint32Array(arrayBuffer); 98 | 99 | const { Point } = Renderer; 100 | 101 | const opts = Object.assign({ antialias: false, alpha: false }, options); 102 | const blend = opts.alpha ? GL_ONE : GL_SRC_ALPHA; 103 | const scale = opts.scale || 1; 104 | delete opts.scale; 105 | 106 | const gl = canvas.getContext('webgl', opts); 107 | 108 | /* 109 | if (DEVELOPMENT) { 110 | if (!gl) { 111 | throw new Error('WebGL not found'); 112 | } 113 | } 114 | */ 115 | 116 | const ext = gl.getExtension('ANGLE_instanced_arrays'); 117 | 118 | /* 119 | if (DEVELOPMENT) { 120 | if (!ext) { 121 | throw new Error('Requared ANGLE_instanced_arrays extension not found'); 122 | } 123 | } 124 | */ 125 | 126 | const compileShader = (source, type) => { 127 | const shader = gl.createShader(type); 128 | gl.shaderSource(shader, source); 129 | gl.compileShader(shader); 130 | 131 | /* 132 | if (DEVELOPMENT) { 133 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 134 | const error = gl.getShaderInfoLog(shader); 135 | gl.deleteShader(shader); 136 | throw new Error(error); 137 | } 138 | } 139 | */ 140 | 141 | return shader; 142 | }; 143 | 144 | const program = gl.createProgram(); 145 | gl.attachShader(program, compileShader(vertexShader, GL_VERTEX_SHADER)); 146 | gl.attachShader(program, compileShader(fragmentShader, GL_FRAGMENT_SHADER)); 147 | gl.linkProgram(program); 148 | 149 | /* 150 | if (DEVELOPMENT) { 151 | if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { 152 | const error = gl.getProgramInfoLog(program); 153 | gl.deleteProgram(program); 154 | throw new Error(error); 155 | } 156 | } 157 | */ 158 | 159 | const createBuffer = (type, src, usage) => { 160 | gl.bindBuffer(type, gl.createBuffer()); 161 | gl.bufferData(type, src, usage || GL_STATIC_DRAW); 162 | }; 163 | 164 | const bindAttrib = (name, size, stride, divisor, offset, type, norm) => { 165 | const location = gl.getAttribLocation(program, name); 166 | gl.enableVertexAttribArray(location); 167 | gl.vertexAttribPointer(location, size, type || GL_FLOAT, !!norm, stride || 0, offset || 0); 168 | divisor && ext.vertexAttribDivisorANGLE(location, divisor); 169 | }; 170 | 171 | // indicesBuffer 172 | createBuffer(GL_ELEMENT_ARRAY_BUFFER, new Uint8Array([0, 1, 2, 2, 1, 3])); 173 | 174 | // vertexBuffer 175 | createBuffer(GL_ARRAY_BUFFER, new Float32Array([0, 0, 0, 1, 1, 0, 1, 1])); 176 | 177 | // vertexLocation 178 | bindAttrib('g', 2); 179 | 180 | // dynamicBuffer 181 | createBuffer(GL_ARRAY_BUFFER, arrayBuffer, GL_DYNAMI_CDRAW); 182 | 183 | // anchorLocation 184 | bindAttrib('a', 2, byteSize, 1); 185 | // scaleLocation 186 | bindAttrib('s', 2, byteSize, 1, 8); 187 | // rotationLocation 188 | bindAttrib('r', 1, byteSize, 1, 16); 189 | // translationLocation 190 | bindAttrib('t', 2, byteSize, 1, 20); 191 | // uvsLocation 192 | bindAttrib('u', 4, byteSize, 1, 28); 193 | // colorLocation 194 | bindAttrib('c', 4, byteSize, 1, 44, GL_UNSIGNED_BYTE, true); 195 | // zLocation 196 | bindAttrib('z', 1, byteSize, 1, 48); 197 | 198 | const getUniformLocation = name => gl.getUniformLocation(program, name); 199 | const matrixLocation = getUniformLocation('m'); 200 | const textureLocation = getUniformLocation('x'); 201 | const alphaTestLocation = getUniformLocation('j'); 202 | 203 | let width; 204 | let height; 205 | 206 | let count = 0; 207 | let currentFrame; 208 | let alphaTestMode; 209 | 210 | const resize = () => { 211 | width = canvas.clientWidth * scale | 0; 212 | height = canvas.clientHeight * scale | 0; 213 | 214 | const change = canvas.width !== width || canvas.height !== height; 215 | 216 | canvas.width = width; 217 | canvas.height = height; 218 | 219 | return change; 220 | }; 221 | 222 | const flush = () => { 223 | if (!count) return; 224 | 225 | /* 226 | if (alphaTestMode) { 227 | gl.disable(GL_BLEND); 228 | } else { 229 | gl.enable(GL_BLEND); 230 | gl.blendFunc(blend, GL_ONE_MINUS_SRC_ALPHA); 231 | } 232 | */ 233 | 234 | gl.blendFunc(alphaTestMode ? GL_ONE : blend, alphaTestMode ? GL_ZERO : GL_ONE_MINUS_SRC_ALPHA); 235 | gl.depthFunc(alphaTestMode ? GL_LESS : GL_LEQUAL); 236 | 237 | gl.bindTexture(GL_TEXTURE_2D, currentFrame.p.t); 238 | gl.uniform1i(textureLocation, currentFrame.p.t); 239 | gl.uniform1f(alphaTestLocation, alphaTestMode ? currentFrame.p.a : 0); 240 | 241 | gl.bufferSubData(GL_ARRAY_BUFFER, 0, floatView.subarray(0, count * floatSize)); 242 | ext.drawElementsInstancedANGLE(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, 0, count); 243 | count = 0; 244 | }; 245 | 246 | const draw = (sprite) => { 247 | if (!sprite.visible) return; 248 | 249 | if (count === maxBatch) flush(); 250 | 251 | const { frame } = sprite; 252 | const { uvs } = frame; 253 | 254 | if (currentFrame.p.t !== frame.p.t) { 255 | currentFrame.p.t && flush(); 256 | currentFrame = frame; 257 | } 258 | 259 | let i = count * floatSize; 260 | 261 | floatView[i++] = frame.anchor.x; 262 | floatView[i++] = frame.anchor.y; 263 | 264 | floatView[i++] = sprite.scale.x * frame.size.x; 265 | floatView[i++] = sprite.scale.y * frame.size.y; 266 | 267 | floatView[i++] = sprite.rotation; 268 | 269 | floatView[i++] = sprite.position.x; 270 | floatView[i++] = sprite.position.y; 271 | 272 | /* eslint-disable prefer-destructuring */ 273 | floatView[i++] = uvs[0]; 274 | floatView[i++] = uvs[1]; 275 | floatView[i++] = uvs[2]; 276 | floatView[i++] = uvs[3]; 277 | /* eslint-enable prefer-destructuring */ 278 | 279 | uintView[i++] = (((sprite.tint & 0xffffff) << 8) | ((sprite.a * 255) & 255)) >>> 0; 280 | floatView[i] = sprite.l.z; 281 | 282 | count++; 283 | }; 284 | 285 | const renderer = { 286 | gl, 287 | 288 | camera: { 289 | at: Point(), 290 | to: Point(), // 0 -> 1 291 | angle: 0, 292 | }, 293 | 294 | background(r, g, b, a) { 295 | gl.clearColor(r, g, b, a === 0 ? 0 : (a || 1)); 296 | }, 297 | 298 | layer(z) { 299 | let l = layers.find(layer => layer.z === z); 300 | 301 | if (!l) { 302 | l = new Layer(z); 303 | layers.push(l); 304 | layers.sort((a, b) => b.z - a.z); 305 | } 306 | 307 | return l; 308 | }, 309 | 310 | add(sprite) { 311 | zeroLayer.add(sprite); 312 | }, 313 | 314 | texture(source, alphaTest, smooth, mipmap) { 315 | const srcWidth = source.width; 316 | const srcHeight = source.height; 317 | const t = gl.createTexture(); 318 | 319 | gl.bindTexture(GL_TEXTURE_2D, t); 320 | // NEAREST || LINEAR 321 | gl.texParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST | +smooth); 322 | // NEAREST || LINEAR || NEAREST_MIPMAP_LINEAR || LINEAR_MIPMAP_LINEAR 323 | gl.texParameteri( 324 | GL_TEXTURE_2D, 325 | GL_TEXTURE_MIN_FILTER, 326 | GL_NEAREST | +smooth | (+mipmap << 8) | (+mipmap << 1), 327 | ); 328 | gl.texImage2D(GL_TEXTURE_2D, 0, GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE, source); 329 | mipmap && gl.generateMipmap(GL_TEXTURE_2D); 330 | 331 | return { 332 | size: Point(srcWidth, srcHeight), 333 | anchor: Point(), 334 | uvs: [0, 0, 1, 1], 335 | p: { 336 | a: alphaTest === 0 ? 0 : (alphaTest || 1), 337 | t, 338 | }, 339 | frame(origin, size, anchor) { 340 | return { 341 | size, 342 | anchor: anchor || this.anchor, 343 | uvs: [ 344 | origin.x / srcWidth, 345 | origin.y / srcHeight, 346 | size.x / srcWidth, 347 | size.y / srcHeight, 348 | ], 349 | p: this.p, 350 | }; 351 | }, 352 | }; 353 | }, 354 | 355 | resize, 356 | 357 | render() { 358 | resize(); 359 | 360 | const { at, to, angle } = renderer.camera; 361 | 362 | const x = at.x - width * to.x; 363 | const y = at.y - height * to.y; 364 | 365 | const c = Math.cos(angle); 366 | const s = Math.sin(angle); 367 | 368 | const w = 2 / width; 369 | const h = -2 / height; 370 | 371 | /* 372 | 373 | | 1 | 0| 0| 0| 374 | | 0 | 1| 0| 0| 375 | | 0 | 0| 1| 0| 376 | | at.x| at.y| 0| 1| 377 | 378 | x 379 | 380 | | c| s| 0| 0| 381 | | -s| c| 0| 0| 382 | | 0| 0| 1| 0| 383 | | 0| 0| 0| 1| 384 | 385 | x 386 | 387 | | 1| 0| 0| 0| 388 | | 0| 1| 0| 0| 389 | | 0| 0| 1| 0| 390 | | -at.x| -at.y| 0| 1| 391 | 392 | x 393 | 394 | | 2/width| 0| 0| 0| 395 | | 0| -2/height| 0| 0| 396 | | 0| 0| -1/depth| 0| 397 | | -2x/width-1| 2y/height+1| 0| 1| 398 | 399 | */ 400 | 401 | // prettier-ignore 402 | const projection = [ 403 | c * w, s * h, 0, 0, 404 | -s * w, c * h, 0, 0, 405 | 0, 0, -1 / depth, 0, 406 | 407 | (at.x * (1 - c) + at.y * s) * w - 2 * x / width - 1, 408 | (at.y * (1 - c) - at.x * s) * h + 2 * y / height + 1, 409 | 0, 1, 410 | ]; 411 | 412 | gl.useProgram(program); 413 | gl.enable(GL_BLEND); 414 | gl.enable(GL_DEPTH_TEST); 415 | 416 | gl.uniformMatrix4fv(matrixLocation, false, projection); 417 | gl.viewport(0, 0, width, height); 418 | gl.clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 419 | 420 | currentFrame = nullFrame; 421 | 422 | alphaTestMode = true; 423 | layers.forEach(layer => layer.o.i(draw)); 424 | flush(); 425 | 426 | alphaTestMode = false; 427 | for (let l = layers.length - 1; l >= 0; l--) { 428 | layers[l].t.i(draw); 429 | } 430 | flush(); 431 | }, 432 | }; 433 | 434 | resize(); 435 | 436 | return renderer; 437 | }; 438 | 439 | Renderer.Point = class Point { 440 | constructor(x, y) { 441 | if (!(this instanceof Renderer.Point)) { 442 | return new Renderer.Point(x, y); 443 | } 444 | this.set(x, y); 445 | } 446 | 447 | set(x, y) { 448 | this.x = x || 0; 449 | this.y = y || (y !== 0 ? this.x : 0); 450 | return this; 451 | } 452 | }; 453 | 454 | Renderer.Sprite = class Sprite { 455 | constructor(frame, props) { 456 | /* 457 | if (DEVELOPMENT) { 458 | if (!frame) { 459 | throw new Error('A frame parameter is required'); 460 | } 461 | } 462 | */ 463 | 464 | if (!(this instanceof Sprite)) { 465 | return new Sprite(frame, props); 466 | } 467 | 468 | Object.assign( 469 | this, 470 | { 471 | frame, 472 | visible: true, 473 | position: Renderer.Point(), 474 | rotation: 0, 475 | scale: Renderer.Point(1), 476 | tint: 0xffffff, 477 | a: 1, 478 | l: null, 479 | n: null, 480 | }, 481 | props, 482 | ); 483 | } 484 | 485 | get alpha() { 486 | return this.a; 487 | } 488 | 489 | set alpha(value) { 490 | /* 491 | if (DEVELOPMENT) { 492 | if (!value || value < 0 || value > 1) { 493 | throw new Error('An alpha of a sprite should be in the range from 0 to 1.'); 494 | } 495 | } 496 | */ 497 | 498 | const change = (value < 1 && this.a === 1) || (value === 1 && this.a < 1); 499 | this.a = value; 500 | change && this.frame.p.a > 0 && this.l && this.l.add(this); 501 | } 502 | 503 | remove() { 504 | this.n && this.n.r(); 505 | this.l = null; 506 | this.n = null; 507 | } 508 | }; 509 | 510 | export default Renderer; 511 | --------------------------------------------------------------------------------