/* * 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 < colors.length; ++k) { let tint = (typeof (SliderTrackOverride) != 'undefined') ? SliderTrackOverride : colors[k]; let bordertint = (typeof (SliderBorder) != 'undefined') ? SliderBorder : 0xffffff; let borderR = (bordertint >> 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 < curve0.length; ++i) if (i == 0 || Math.abs(curve0[i].x - curve0[i - 1].x) > 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 < divs; ++i) { vert.push(vert[4 * c] + radius * Math.cos(theta_1 + i * theta), vert[4 * c + 1] + radius * Math.sin(theta_1 + i * theta), t, 1.0); let newv = vert.length / 4 - 1; index.push(c, last, newv); last = newv; } index.push(c, last, p2); } // add half-circle for head & tail of curve addArc(0, 1, 2, curve[0].t); addArc(5 * curve.length - 5, 5 * curve.length - 6, 5 * curve.length - 7, curve[curve.length - 1].t); // add sectors for turning points of curve for (let i = 1; i < curve.length - 1; ++i) { let dx1 = curve[i].x - curve[i - 1].x; let dy1 = curve[i].y - curve[i - 1].y; let dx2 = curve[i + 1].x - curve[i].x; let dy2 = curve[i + 1].y - curve[i].y; let t = dx1 * dy2 - dx2 * dy1; // d1 x d2 if (t > 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 < DIVIDES; ++i) { let theta = 2 * Math.PI / DIVIDES * i; vert.push(radius * Math.cos(theta), radius * Math.sin(theta), 0.0, 1.0); index.push(0, i + 1, (i + 1) % DIVIDES + 1); } return new PIXI.Geometry().addAttribute('position', vert, 4).addIndex(index) } function SliderMesh(curve, radius, tintid) // constructor. { Container.call(this); this.curve = curve; this.geometry = curveGeometry(curve.curve, radius); this.alpha = 1.0; this.tintid = tintid; this.startt = 0.0; this.endt = 1.0; // blend mode, culling, depth testing, direction of rendering triangles, backface, etc. this.state = PIXI.State.for2d(); this.drawMode = PIXI.DRAW_MODES.TRIANGLES; // Inherited from DisplayMode, set defaults this.blendMode = PIXI.BLEND_MODES.NORMAL; this._roundPixels = PIXI.settings.ROUND_PIXELS; } if (Container) { SliderMesh.__proto__ = Container; } SliderMesh.prototype = Object.create(Container && Container.prototype); SliderMesh.prototype.constructor = SliderMesh; // This should be called directly on prototype before any draw // as we only need ONE texture & ONE shader SliderMesh.prototype.initialize = function (colors, radius, transform, SliderTrackOverride, SliderBorder) { this.ncolors = colors.length; this.uSampler2 = newTexture(colors, SliderTrackOverride, SliderBorder); this.circle = circleGeometry(radius); this.uniforms = { uSampler2: this.uSampler2, alpha: 1.0, dx: transform.dx, dy: transform.dy, ox: transform.ox, oy: transform.oy, texturepos: 0, }; this.shader = PIXI.Shader.from(vertexSrc, fragmentSrc, this.uniforms); } // this should be called directly on prototype when window resizes SliderMesh.prototype.resetTransform = function resetTransform(transform) { this.uniforms.dx = transform.dx; this.uniforms.dy = transform.dy; this.uniforms.ox = transform.ox; this.uniforms.oy = transform.oy; }; // Standard renderer draw. SliderMesh.prototype._render = function _render(renderer) { // not batchable. manual rendering this._renderDefault(renderer); }; // Standard non-batching way of rendering. SliderMesh.prototype._renderDefault = function _renderDefault(renderer) { var shader = this.shader; shader.alpha = this.worldAlpha; if (shader.update) { shader.update(); } renderer.batch.flush(); // upload color info to shared shader uniform this.uniforms.alpha = this.alpha; this.uniforms.texturepos = this.tintid / this.ncolors; this.uniforms.dt = 0; this.uniforms.ot = 0.5; let ox0 = this.uniforms.ox; let oy0 = this.uniforms.oy; const gl = renderer.gl; gl.clearDepth(1.0); // setting depth of clear gl.clear(gl.DEPTH_BUFFER_BIT); // this really clears the depth buffer // first render: to store min depth in depth buffer, but not actually drawing anything gl.colorMask(false, false, false, false); // translation is not supported renderer.state.set(this.state); // set state renderer.state.setDepthTest(true); // enable depth testing let glType; let indexLength; function bind(geometry) { renderer.shader.bind(shader); // bind shader & sync uniforms renderer.geometry.bind(geometry, shader); // bind the geometry let byteSize = geometry.indexBuffer.data.BYTES_PER_ELEMENT; // size of each index glType = byteSize === 2 ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT; // type of each index indexLength = geometry.indexBuffer.data.length; // number of indices } if (this.startt == 0.0 && this.endt == 1.0) { // display whole slider this.uniforms.dt = 0; this.uniforms.ot = 1; bind(this.geometry); gl.drawElements(this.drawMode, indexLength, glType, 0); } else if (this.endt == 1.0) { // snaking out if (this.startt != 1.0) { // we want portion: t > 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; });