/* * custom class, extends PIXI.Container * each instance is a Pixi-renderable slider * properties: alpha * * constructor params * curve: array of points, in osu pixels * radius: radius of hit circle, in osu! pixels * transform: {dx,ox,dy,oy} (x,y)->(x*dx+ox, y*dy+oy) [-1,1]x[-1,1] * tint: 24-bit integer color of inner slider body, RGB from highbits to lowbits */ define([], function() { Container = PIXI.Container; // vertex shader source const vertexSrc = ` precision mediump float; attribute vec4 position; varying float dist; uniform float dx,dy,dt,ox,oy,ot; void main() { dist = position[3]; gl_Position = vec4(position[0], position[1], position[3] + 2.0 * float(position[2]*dt>ot), 1.0); gl_Position.x = gl_Position.x * dx + ox; gl_Position.y = gl_Position.y * dy + oy; }`; // fragment shader source const fragmentSrc = ` precision mediump float; varying float dist; uniform sampler2D uSampler2; uniform float alpha; uniform float texturepos; void main() { gl_FragColor = alpha * texture2D(uSampler2, vec2(dist, texturepos)); }`; // create line texture for slider from tint color function newTexture(colors, SliderTrackOverride, SliderBorder) { const borderwidth = 0.128; const innerPortion = 1 - borderwidth; const edgeOpacity = 0.8; const centerOpacity = 0.3; const blurrate = 0.015; const width = 200; let buff = new Uint8Array(colors.length * width * 4); for (let k=0; k>16)/255; let borderG = ((bordertint>>8)&255)/255; let borderB = (bordertint&255)/255; let borderA = 1.0; let innerR = (tint>>16)/255; let innerG = ((tint>>8)&255)/255; let innerB = (tint&255)/255; let innerA = 1.0; for (let i = 0; i < width; i++) { let position = i / width; let R,G,B,A; if (position >= innerPortion) // draw border color { R = borderR; G = borderG; B = borderB; A = borderA; } else // draw inner color { R = innerR; G = innerG; B = innerB; // TODO: tune this to make opacity transition smoother at center A = innerA * ((edgeOpacity - centerOpacity) * position / innerPortion + centerOpacity); } // pre-multiply alpha R*=A; G*=A; B*=A; // blur at edge for "antialiasing" without supersampling if (1-position < blurrate) // outer edge { R *= (1-position) / blurrate; G *= (1-position) / blurrate; B *= (1-position) / blurrate; A *= (1-position) / blurrate; } if (innerPortion - position > 0 && innerPortion - position < blurrate) { let mu = (innerPortion - position) / blurrate; R = mu * R + (1-mu) * borderR * borderA; G = mu * G + (1-mu) * borderG * borderA; B = mu * B + (1-mu) * borderB * borderA; A = mu * innerA + (1-mu) * borderA; } buff[(k*width+i)*4] = R*255; buff[(k*width+i)*4+1] = G*255; buff[(k*width+i)*4+2] = B*255; buff[(k*width+i)*4+3] = A*255; } } return PIXI.Texture.fromBuffer(buff, width, colors.length); } const DIVIDES = 64; // approximate a circle with a polygon of DEVIDES sides // create mesh from control curve // given curve0: in osu pixels // given radius: in osu pixels // output mesh: in osu pixels function curveGeometry(curve0, radius) // returning PIXI.Geometry object { // osu relative coordinate -> osu pixels curve = new Array(); // filter out coinciding points for (let i=0; i 0.00001 || Math.abs(curve0[i].y - curve0[i-1].y) > 0.00001) curve.push(curve0[i]); let vert = new Array(); let index = new Array(); vert.push(curve[0].x, curve[0].y, curve[0].t, 0.0); // first point on curve // add rectangles around each segment of curve for (let i = 1; i < curve.length; ++i) { let x = curve[i].x; let y = curve[i].y; let t = curve[i].t; let lx = curve[i-1].x; let ly = curve[i-1].y; let lt = curve[i-1].t; let dx = x - lx; let dy = y - ly; let length = Math.hypot(dx, dy); let ox = radius * -dy / length; let oy = radius * dx / length; vert.push(lx+ox, ly+oy, lt, 1.0); vert.push(lx-ox, ly-oy, lt, 1.0); vert.push(x+ox, y+oy, t, 1.0); vert.push(x-ox, y-oy, t, 1.0); vert.push(x, y, t, 0.0); let n = 5*i+1; // indices for 4 triangles composing 2 rectangles index.push(n-6, n-5, n-1, n-5, n-1, n-3); index.push(n-6, n-4, n-1, n-4, n-1, n-2); } function addArc(c, p1, p2, t) // c as center, sector from c-p1 to c-p2 counterclockwise { let theta_1 = Math.atan2(vert[4*p1+1]-vert[4*c+1], vert[4*p1]-vert[4*c]) let theta_2 = Math.atan2(vert[4*p2+1]-vert[4*c+1], vert[4*p2]-vert[4*c]) if (theta_1 > theta_2) theta_2 += 2*Math.PI; let theta = theta_2 - theta_1; let divs = Math.ceil(DIVIDES * Math.abs(theta) / (2*Math.PI)); theta /= divs; let last = p1; for (let i=1; i 0) { // turning counterclockwise addArc(5*i, 5*i-1, 5*i+2); } else { // turning clockwise or straight back addArc(5*i, 5*i+1, 5*i-2); } } return new PIXI.Geometry().addAttribute('position', vert, 4).addIndex(index) } function circleGeometry(radius) { let vert = new Array(); let index = new Array(); vert.push(0.0, 0.0, 0.0, 0.0); // center for (let i=0; i this.startt this.uniforms.dt = -1; this.uniforms.ot = -this.startt; bind(this.geometry); gl.drawElements(this.drawMode, indexLength, glType, 0); } this.uniforms.dt = 0; this.uniforms.ot = 1; let p = this.curve.pointAt(this.startt); this.uniforms.ox += p.x * this.uniforms.dx; this.uniforms.oy += p.y * this.uniforms.dy; bind(this.circle); gl.drawElements(this.drawMode, indexLength, glType, 0); } else if (this.startt == 0.0) { // snaking in if (this.endt != 0.0) { // we want portion: t < this.endt this.uniforms.dt = 1; this.uniforms.ot = this.endt; bind(this.geometry); gl.drawElements(this.drawMode, indexLength, glType, 0); } this.uniforms.dt = 0; this.uniforms.ot = 1; let p = this.curve.pointAt(this.endt); this.uniforms.ox += p.x * this.uniforms.dx; this.uniforms.oy += p.y * this.uniforms.dy; bind(this.circle); gl.drawElements(this.drawMode, indexLength, glType, 0); } else { console.error("can't snake both end of slider"); } // second render: draw at previously calculated min depth gl.depthFunc(gl.EQUAL); gl.colorMask(true, true, true, true); if (this.startt == 0.0 && this.endt == 1.0) { // display whole slider gl.drawElements(this.drawMode, indexLength, glType, 0); } else if (this.endt == 1.0) { // snaking out if (this.startt != 1.0) { gl.drawElements(this.drawMode, indexLength, glType, 0); this.uniforms.ox = ox0; this.uniforms.oy = oy0; this.uniforms.dt = -1; this.uniforms.ot = -this.startt; bind(this.geometry); } gl.drawElements(this.drawMode, indexLength, glType, 0); } else if (this.startt == 0.0) { // snaking in if (this.endt != 0.0) { gl.drawElements(this.drawMode, indexLength, glType, 0); this.uniforms.ox = ox0; this.uniforms.oy = oy0; this.uniforms.dt = 1; this.uniforms.ot = this.endt; bind(this.geometry); } gl.drawElements(this.drawMode, indexLength, glType, 0); } // restore state // TODO: We don't know the previous state. THIS MIGHT CAUSE BUGS gl.depthFunc(gl.LESS); // restore to default depth func renderer.state.setDepthTest(false); // restore depth test to disabled // restore uniform this.uniforms.ox = ox0; this.uniforms.oy = oy0; }; SliderMesh.prototype.destroy = function destroy (options) { Container.prototype.destroy.call(this, options); this.geometry.dispose(); this.geometry = null; this.shader = null; this.state = null; }; return SliderMesh; });