diff --git a/demo/index.html b/demo/index.html new file mode 100644 index 0000000..ee1a1bf --- /dev/null +++ b/demo/index.html @@ -0,0 +1,23 @@ + + + + + <repa-shader> demo + + + + + + + + + diff --git a/src/repa-shader.js b/src/repa-shader.js new file mode 100644 index 0000000..ab14b02 --- /dev/null +++ b/src/repa-shader.js @@ -0,0 +1,317 @@ +const createLogger = pfx => { + return { + info: (...args) => console.info(...pfx, ...args), + log: (...args) => console.log(...pfx, ...args), + warn: (...args) => console.warn(...pfx, ...args), + error: (...args) => console.error(...pfx, ...args), + }; +}; + +const CHUNKS = { + es300: '#version 300 es\n', + geeker: ` +#define FC gl_FragCoord +#define r resolution +#define m mouse +#define t time +#define f frame +precision highp float; +uniform vec2 resolution; +uniform vec2 mouse; +uniform float time; +uniform float frame; +out vec4 outColor; +`, // TODO sampler2D, MRT + geekestStart: ` +void main() { +`, + geekestEnd: ` +} +`, +}; + +const DEMO = ` +precision highp float; +uniform vec2 resolution; +uniform vec2 mouse; +uniform float time; +out vec4 outColor; +void main(){ + vec2 r=resolution, p=(gl_FragCoord.xy*2.-r)/min(r.x,r.y)-mouse; + for (int i=0;i<8;++i) { + p.xy=abs(p)/abs(dot(p,p))-vec2(.9+cos(time*.2)*.4); + } + outColor=vec4(p.xxy,1); +} +`; + +class RepaShader extends HTMLElement { + constructor(cfg = {}) { + super(); + this._cfg = cfg; + this.attachShadow({ mode: 'open' }); + this.logger = createLogger(["%c[repa-shader]", "background: #1d2021; color: #bada55"]); + } + + connectedCallback() { + this._createStyle(); + + if (!this._target) { + this._target = this._createTarget(); + } + + if (!this._gl) { + const glopts = this._cfg.glopts || {alpha: true, preserveDrawingBuffer: true}; + this._gl = this._target.getContext('webgl2', glopts); + if (!this._gl) { + this.logger.error("WebGL2 not supported"); + return; + } + } + + // TODO resize + // TODO postprogram + // TODO source + reset + + this._gl.bindBuffer(this._gl.ARRAY_BUFFER, this._gl.createBuffer()); + this._gl.bufferData(this._gl.ARRAY_BUFFER, new Float32Array([-1,1,0,-1,-1,0,1,1,0,1,-1,0]), this._gl.STATIC_DRAW); + this._gl.disable(this._gl.DEPTH_TEST); + this._gl.disable(this._gl.CULL_FACE); + this._gl.disable(this._gl.BLEND); + this._gl.clearColor(0,0,0,1); + + // TODO remove + this.demo(); + } + + render(source, time) { + if (!source) { + return; + } + this._fsSource = source; + this.reset(time); + } + + _resizeTarget() { + const {width, height} = this._target.getBoundingClientRect(); + this._target.width = width; + this._target.height = height; + // TODO buffers + this._gl.viewport(0, 0, width, height); + } + + _onMouseMove(e) { + const x = Math.min(Math.max(e.offsetX, 0), this._target.width); + const y = Math.min(Math.max(e.offsetY, 0), this._target.height); + this._mousePosition = [x / this._target.width, 1 - y / this._target.height]; + } + + reset(time) { + this._resizeTarget(); + + if (this.hasAttribute('mouse')) { + this._target.addEventListener('pointermove', this._onMouseMove.bind(this)); + } + + this.mode = this.getAttribute('mode') || this.mode; + + const program = this._gl.createProgram(); + const vs = this._createShader(program, this.VS, true); + if (!vs) { + return; + } + const fs = this._createShader(program, this.FS, false); + if (!fs) { + this._gl.deleteShader(vs); + return; + } + + this._gl.linkProgram(program); + this._gl.deleteShader(vs); + this._gl.deleteShader(fs); + + if (!this._gl.getProgramParameter(program, this._gl.LINK_STATUS)) { + const msg = this._gl.getProgramInfoLog(program); + this.logger.error("Program link error: ", msg); + // TODO error callback + program = null; + return; + } + + // TODO geek mode + const resolution = 'resolution'; + const mouse = 'mouse'; + const nowTime = 'time'; + const frame = 'frame'; + // TODO sound? backbuffer? mrt? + + if (this._program) { + this._gl.deleteProgram(this._program); + } + this.program = program; + this._gl.useProgram(this.program); + this._uniLocation = {}; + this._uniLocation.resolution = this._gl.getUniformLocation(this.program, resolution); + this._uniLocation.mouse = this._gl.getUniformLocation(this.program, mouse); + this._uniLocation.time = this._gl.getUniformLocation(this.program, nowTime); + this._uniLocation.frame = this._gl.getUniformLocation(this.program, frame); + + this._attLocation = this._gl.getAttribLocation(this.program, 'position'); + this._mousePosition= [0, 0]; + this._startTime = Date.now(); + this._frame = 0; + + this.draw(time); + } + + draw(time) { + if (this.running) { + requestAnimationFrame(this.draw.bind(this)); + } + + if (time) { + this._nowTime = time; + } else { + this._nowTime = (Date.now() - this._startTime) * 0.001; + } + + ++this._frame; + + this._gl.useProgram(this.program); + // TODO buffers + + this._gl.enableVertexAttribArray(this._attLocation); + this._gl.vertexAttribPointer(this._attLocation, 3, this._gl.FLOAT, false, 0, 0); + this._gl.clear(this._gl.COLOR_BUFFER_BIT); + this._gl.uniform2fv(this._uniLocation.resolution, [this._target.width, this._target.height]); + this._gl.uniform2fv(this._uniLocation.mouse, this._mousePosition); + this._gl.uniform1f(this._uniLocation.time, this._nowTime * .001); + this._gl.uniform1f(this._uniLocation.frame, this._frame); + + this._gl.drawArrays(this._gl.TRIANGLE_STRIP, 0, 4); + + this._gl.flush(); + // TODO draw callback + } + + run() { + this.removeAttribute('paused'); + this.draw(); + } + + pause() { + this.setAttribute('paused', ''); + } + + get running() { + return !this.hasAttribute('paused'); + } + + _createShader(program, source, isVertex) { + if (!this._gl) { + return null; + } + const type = isVertex ? this._gl.VERTEX_SHADER : this._gl.FRAGMENT_SHADER; + const shader = this._gl.createShader(type); + this._gl.shaderSource(shader, source); + this._gl.compileShader(shader); + + if (!this._gl.getShaderParameter(shader, this._gl.COMPILE_STATUS)) { + const msg = this._gl.getShaderInfoLog(shader); + this.logger.error("Shader compile error:", msg); // TODO format + error callback + return null; + } + // TODO success callback + this.logger.info(`Shader successfully compiled [${isVertex ? 'vertex' : 'fragment'}]`); + + this._gl.attachShader(program, shader); + const log = this._gl.getShaderInfoLog(shader); + if (log) { + this.logger.log(log); + } + return shader; + } + + _createTarget() { + const target = document.createElement('canvas'); + target.width = this._cfg.width || 300; // TODO + target.height = this._cfg.height || 300; // TODO + this.shadowRoot.appendChild(target); + + return target; + } + + _createStyle() { + let style = this.shadowRoot.querySelector('style') || document.createElement('style'); + style.textContent = ` + :host { + display: block; + } + `; + this.shadowRoot.appendChild(style); + } + + _appendStyle(content) { + const style = this.shadowRoot.querySelector('style'); + style.textContent += content; + } + + get target() { + return this._target; + } + + // TODO + get VS() { + return `#version 300 es +in vec3 position; +void main(){ + gl_Position=vec4(position, 1.); +} +`; + } + + get FS() { + // auto guessing mode + // - contains `precision` -> twigl classic300es + // - no `precision`, but has `main()` -> twigl geeker300es + // - no `precision`, no `main()` -> twigl geekest300es + // TODO: mrt + if (!this.mode) { + const hasPrecision = this._fsSource.includes('precision'); + const hasMain = this._fsSource.includes('main()'); + if (hasPrecision) { + this.mode = 'classic'; + } else if (hasMain) { + this.mode = 'geeker'; + } else { + this.mode = 'geekest'; + } + } + + let start = ''; + let end = ''; + switch (this.mode) { + case 'classic': + start = CHUNKS.es300; + break; + case 'geeker': + start = CHUNKS.es300 + CHUNKS.geeker; + break; + case 'geekest': + start = CHUNKS.es300 + CHUNKS.geeker + CHUNKS.geekestStart; + end = CHUNKS.geekestEnd; + break; + } + + return `${start}\n${this._fsSource}\n${end}`; + } + + demo() { + this.render(DEMO); + } + +} + +customElements.define("repa-shader", RepaShader); + +export default RepaShader;