mirror of
https://gitlab.com/skysthelimit.dev/selenite.git
synced 2025-06-16 10:32:08 -05:00
393 lines
16 KiB
JavaScript
393 lines
16 KiB
JavaScript
/*
|
|
* 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;
|
|
|
|
}); |