Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ivanpopelyshev/6128a6b31f1a7f51d536b369a3812ecf to your computer and use it in GitHub Desktop.
Save ivanpopelyshev/6128a6b31f1a7f51d536b369a3812ecf to your computer and use it in GitHub Desktop.
WIP Pixi v5 Gradient
export const enum GradientInterpolationMethod {
RGB = 0,
LinearRGB = 1
}
function toSRGB(x: number) {
if (x < 0.00313080) {
return x * 12.92;
}
return Math.pow(x, 1.0 / 2.4) * 1.055 - 0.055;
}
function toLinear(x: number) {
if (x <= 0.04045) {
return x / 12.92;
}
return Math.pow((x + 0.055) / 1.055, 2.4);
}
export class ColorStop {
constructor(public offset: number, public color: number, public alpha: number = 1.0) {
this.setColor(color);
}
setColor(color: number) {
this.color = color;
this.r = ((color >> 16) & 0xff) / 255.0;
this.g = ((color >> 8) & 0xff) / 255.0;
this.b = (color & 0xff) / 255.0;
this.cssColor = '';
}
r: number;
g: number;
b: number;
cssColor: string;
sR: number;
sG: number;
sB: number;
}
export class Gradient implements core.ITextureResource {
constructor(public width = 256, public wrapMode = core.WRAP_MODES.CLAMP, public interpolationMethod = GradientInterpolationMethod.RGB) {
this.baseTexture = new core.BaseTexture(this);
this.texture = new core.Texture(this.baseTexture);
}
texture: core.Texture;
baseTexture: core.BaseTexture;
tempCanvas: HTMLCanvasElement = null;
tempContext: CanvasRenderingContext2D = null;
stops: Array<ColorStop> = [];
addColorStop(offset: number, color: number, alpha: number) {
const stop = new ColorStop(offset, color, alpha);
const stops = this.stops;
let ind = stops.length;
while (ind > 0 && stop.offset < stops[ind - 1].offset) {
ind--;
}
stops.push(null);
for (let t = stops.length - 1; t > ind; t--) {
stops[t] = stops[t - 1];
}
stops[ind] = stop;
this.invalidate();
}
addColorStopRatio(ratio: number, color: number, alpha: number) {
this.addColorStop(ratio / (this.width - 1), color, alpha);
}
_values: Float32Array = null;
_byteValues: Uint8Array = null;
_hasLinear = false;
invalidate() {
this.baseTexture._updateID++;
this._hasLinear = false;
}
calculateStopsLinear() {
this._hasLinear = true;
for (let i = 0; i < this.stops.length; i++) {
const stop = this.stops[i];
stop.sR = toLinear(stop.r);
stop.sG = toLinear(stop.g);
stop.sB = toLinear(stop.b);
}
}
calculateLinearRGB() {
if (!this._hasLinear) {
this.calculateStopsLinear();
}
const w = this.width;
const w4 = this.width * 4;
//lets sort
if (!this._values || this._values.length !== w4) {
this._values = new Float32Array(w4);
this._byteValues = new Uint8Array(w4);
}
if (this.baseTexture.width !== w) {
this.baseTexture.resize(w, 1);
}
const stops = this.stops;
const values = this._values;
const len = stops.length;
const pma = this.baseTexture.premultiplyAlpha ? 1 : 0;
let ind = 0;
let cur = stops[0];
let next = stops[0];
let i = 0;
let i4 = 0;
if (len > 1) {
for (; i < w; i++, i4 += 4) {
let part = i / w;
while (cur.offset >= part) {
ind++;
if (ind >= len) {
break;
}
cur = next;
next = stops[ind];
}
const T = (part - cur.offset) / (next.offset - cur.offset);
const U = 1.0 - T;
const alpha = T * next.alpha + U * cur.alpha;
const alpha2 = 1.0 - pma * (1.0 - alpha);
//
values[i4] = toSRGB(T * next.sR + U * cur.sR) * alpha2;
values[i4 + 1] = toSRGB(T * next.sG + U * cur.sG) * alpha2;
values[i4 + 2] = toSRGB(T * next.sB + U * cur.sB) * alpha2;
values[i4 + 3] = alpha;
}
}
const A = cur.alpha;
const R = cur.r * (1.0 - pma * (1.0 - A));
const G = cur.g * (1.0 - pma * (1.0 - A));
const B = cur.b * (1.0 - pma * (1.0 - A));
while (i4 < w4) {
values[i4] = R;
values[i4 + 1] = G;
values[i4 + 2] = B;
values[i4 + 3] = A;
}
const bytes = this._byteValues;
for (i = 0; i < w4; i++) {
bytes[i] = Math.round(values[i] * 255.0);
}
}
calculateCanvas() {
const w = this.width;
if (!this.tempCanvas) {
this.tempCanvas = document.createElement('canvas');
this.tempCanvas.height = 1;
this.tempContext = this.tempCanvas.getContext('2d');
}
if (this.baseTexture.width !== w) {
this.baseTexture.resize(w, 1);
}
const canvas = this.tempCanvas;
const context = this.tempContext;
if (canvas.width !== this.width) {
canvas.width = this.width;
canvas.height = 1;
}
//TODO: make a test if first and last pixel color correspond to first and last stop color
const grad = context.createLinearGradient(0, 0, this.width, 0);
const stops = this.stops;
for (let i = 0; i < stops.length; i++) {
const cur = stops[i];
if (cur.cssColor.length === 0) {
const R = Math.round(cur.r * 255.0);
const G = Math.round(cur.g * 255.0);
const B = Math.round(cur.b * 255.0);
const A = Math.round(cur.alpha * 1000.0) / 1000.0;
cur.cssColor = `rgba(${R},${G},${B},${A})`;
}
grad.addColorStop(cur.offset, cur.cssColor);
}
context.fillStyle = grad;
context.fillRect(0, 0, this.width, 1);
}
onTextureDestroy(baseTexture: gobi.core.BaseTexture): boolean {
return false;
}
onTextureNew(baseTexture: gobi.core.BaseTexture): void {
baseTexture.resize(this.width, 1);
baseTexture.mipmap = false;
baseTexture.wrapMode = this.wrapMode;
}
onTextureTag(baseTexture: gobi.core.BaseTexture): void {
}
onTextureUpload(renderer: gobi.pixi.WebGLRenderer, baseTexture: gobi.core.BaseTexture, glTexture: gobi.glCore.GLTexture): boolean {
// do it if we make float texture
// glTexture.width = this.width;
// glTexture.height = 1;
if (this.interpolationMethod === GradientInterpolationMethod.LinearRGB) {
this.calculateLinearRGB();
glTexture.uploadData(this._byteValues, this.width, 1);
} else {
this.calculateCanvas();
glTexture.upload(this.tempCanvas);
}
return true;
}
destroy() {
this.texture.destroy(true);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment