Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <!--
Lots of this code comes from this excellent article and examples:
http://jamie-wong.com/2016/07/15/ray-marching-signed-distance-functions/

Distance functions from Inigo Quilez (https://iquilezles.org/)
-->
<script type="x-frag-shader" id="frag-shader">
	////////////////
	//   CONFIG   //
	////////////////

	// Can be 0, 1, or 2 (more bounces may cause additional lag)
	#define REFLECT_BOUNCES 1
	
	const float CAMERA_SPIN = 0.05; // default = 0.25; radians/s
	// Outer sphere radius = 1.0; the following values are relative to that.
	const float REPETITION_GAP = 0.1; // default = 0.1; gap between instances
	const float INNER_SPHERE_RADIUS = 0.33; // default = 0.33
	const float STAR_RADIUS = 0.6; // default = 0.6; distance from origin to end of capsules that form star
	const float STAR_RADIUS_2 = 0.16; // default = 0.16; radius of the capsules themselves
	const float GEM_RADIUS = 0.25; // default = 0.25
	// Gem material is applied slightly beyond gem radius, to ensure gem tips are covered.
	// This also allow spreading the gem material out further, which is fun. Try setting to 0.4 :)
	const float GEM_MAT_RADIUS = 0.26; // default = 0.26
	const float GEM_SPIN = 2.0; // default = 2.0; radians/s
	// How much the glancing angle of reflection rays influences reflection strength.
	// 1.0 = realistic reflection falloff; 0.0 = perfect mirror surfaces
	const float FRESNEL_FACTOR = 1.0; // default = 1.0
	
	////////////////
	// END CONFIG //
	////////////////
	
	// Generic math helpers
	float map(float value, float min1, float max1, float min2, float max2) {
		return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
	}
	
	mat4 rotateY(float theta) {
		float c = cos(theta);
		float s = sin(theta);

		return mat4(
			vec4(c, 0, s, 0),
			vec4(0, 1, 0, 0),
			vec4(-s, 0, c, 0),
			vec4(0, 0, 0, 1)
		);
	}

	
	// Raymarching and SDFs
	
	const int MAX_MARCHING_STEPS = 255;
	const float MIN_DIST = 0.0;
	const float MAX_DIST = 100.0;
	const float EPSILON = 0.0001;
	const float REFL_OFFSET = 0.001;
	
	/**
	 * Constructive solid geometry operations on SDF-calculated distances.
	 */
	float intersectOp(float distA, float distB) {
			return max(distA, distB);
	}

	float unionOp(float distA, float distB) {
			return min(distA, distB);
	}

	float differenceOp(float distA, float distB) {
			return max(distA, -distB);
	}


	/**
	 * Signed distance function for a sphere centered at the origin with radius "r";
	 */
	float sdSphere(vec3 p, float r) {
		return length(p) - r;
	}

	/**
	 * Signed distance function for a cube centered at the origin
	 * with width = height = length = 2.0
	 */
	float sdCube(vec3 p) {
		// If d.x < 0, then -1 < p.x < 1, and same logic applies to p.y, p.z
		// So if all components of d are negative, then p is inside the unit cube
		vec3 d = abs(p) - vec3(1.0, 1.0, 1.0);

		// Assuming p is inside the cube, how far is it from the surface?
		// Result will be negative or zero.
		float insideDistance = min(max(d.x, max(d.y, d.z)), 0.0);

		// Assuming p is outside the cube, how far is it from the surface?
		// Result will be positive or zero.
		float outsideDistance = length(max(d, 0.0));

		return insideDistance + outsideDistance;
	}
	
	float sdCapsule( vec3 p, vec3 a, vec3 b, float r ) {
		vec3 pa = p-a, ba = b-a;
		float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 );
		return length( pa - ba*h ) - r;
	}
	
	float sdOctahedron( vec3 p, float s) {
		p = abs(p);
		float m = p.x+p.y+p.z-s;
		vec3 q;
				 if( 3.0*p.x < m ) q = p.xyz;
		else if( 3.0*p.y < m ) q = p.yzx;
		else if( 3.0*p.z < m ) q = p.zxy;
		else return m*0.57735027;

		float k = clamp(0.5*(q.z-q.y+s),0.0,s); 
		return length(vec3(q.x,q.y-s+k,q.z-k)); 
	}
	
	float domainFull = 2.0 + REPETITION_GAP;
	float domainHalf = domainFull * 0.5;
	
	vec3 repeatDomain(vec3 p) {
		return vec3(
			mod(p.x+domainHalf, domainFull) - domainHalf,
			p.y,
			mod(p.z+domainHalf, domainFull) - domainHalf
		);
	}


	/**
	 * Signed distance function describing the scene.
	 * 
	 * Absolute value of the return value indicates the distance to the surface.
	 * Sign indicates whether the point is inside or outside the surface,
	 * negative indicating inside.
	 */
	float sceneSDF(vec3 p) {
		vec3 samplePoint = repeatDomain(p);
		float sphereOuter = sdSphere(samplePoint, 1.0);
		
		// Only proceed to process interior if we're actually inside
		if (sphereOuter > EPSILON) return sphereOuter;
		
		float sphereInner = sdSphere(samplePoint, INNER_SPHERE_RADIUS);
		
		float capsuleX = sdCapsule(samplePoint, vec3(-STAR_RADIUS, 0.0, 0.0), vec3(STAR_RADIUS, 0.0, 0.0), STAR_RADIUS_2);
		float capsuleY = sdCapsule(samplePoint, vec3(0.0, -STAR_RADIUS, 0.0), vec3(0.0, STAR_RADIUS, 0.0), STAR_RADIUS_2);
		float capsuleZ = sdCapsule(samplePoint, vec3(0.0, 0.0, -STAR_RADIUS), vec3(0.0, 0.0, STAR_RADIUS), STAR_RADIUS_2);
		float star = unionOp(unionOp(capsuleX, capsuleY), capsuleZ);
		float centerCutout = unionOp(star, sphereInner);
		
		float sphereHollow = differenceOp(sphereOuter, centerCutout);
				
		// Allow moving cube with mouse
		float invCubeY = map(iMouse.y, 0.0, 1.0, -0.4, 2.4);
		float cube = sdCube(samplePoint + vec3(0.0, invCubeY, 0.0));
		
		float clippedModel = intersectOp(cube, sphereHollow);
		
		vec3 gemPoint = (rotateY(iTime * -GEM_SPIN) * vec4(samplePoint, 1.0)).xyz;
		float gem = sdOctahedron(gemPoint, GEM_RADIUS);
		
		return unionOp(clippedModel, gem);
	}

	/**
	 * Return the shortest distance from the eyepoint to the scene surface along
	 * the marching direction. If no part of the surface is found between start and end,
	 * return end.
	 * 
	 * eye: the eye point, acting as the origin of the ray
	 * marchingDirection: the normalized direction to march in
	 * start: the starting distance away from the eye
	 * end: the max distance away from the ey to march before giving up
	 */
	float shortestDistanceToSurface(vec3 eye, vec3 marchingDirection, float start, float end) {
		float depth = start;
		for (int i = 0; i < MAX_MARCHING_STEPS; i++) {
			float dist = sceneSDF(eye + depth * marchingDirection);
			if (dist < EPSILON) {
				return depth;
			}
			depth += dist;
			if (depth >= end) {
				return end;
			}
		}
		return end;
	}


	/**
	 * Return the normalized direction to march in from the eye point for a single pixel.
	 * 
	 * fieldOfView: vertical field of view in degrees
	 * size: resolution of the output image
	 * fragCoord: the x,y coordinate of the pixel in the output image
	 */
	vec3 rayDirection(float fieldOfView, vec2 size, vec2 fragCoord) {
		vec2 xy = fragCoord - size / 2.0;
		float z = size.y / tan(radians(fieldOfView) / 2.0);
		return normalize(vec3(xy, -z));
	}

	/**
	 * Using the gradient of the SDF, estimate the normal on the surface at point p.
	 */
	vec3 estimateNormal(vec3 p) {
		return normalize(vec3(
			sceneSDF(vec3(p.x + EPSILON, p.y, p.z)) - sceneSDF(vec3(p.x - EPSILON, p.y, p.z)),
			sceneSDF(vec3(p.x, p.y + EPSILON, p.z)) - sceneSDF(vec3(p.x, p.y - EPSILON, p.z)),
			sceneSDF(vec3(p.x, p.y, p.z  + EPSILON)) - sceneSDF(vec3(p.x, p.y, p.z - EPSILON))
		));
	}

	/**
	 * Lighting contribution of a single point light source via Phong illumination.
	 * 
	 * The vec3 returned is the RGB color of the light's contribution.
	 *
	 * k_a: Ambient color
	 * k_d: Diffuse color
	 * k_s: Specular color
	 * alpha: Shininess coefficient
	 * p: position of point being lit
	 * n: normal of point being lit
	 * eye: the position of the camera
	 * lightPos: the position of the light
	 * lightIntensity: color/intensity of the light
	 *
	 * See https://en.wikipedia.org/wiki/Phong_reflection_model#Description
	 */
	vec3 phongContribForLight(vec3 k_d, vec3 k_s, float alpha, vec3 p, vec3 n, vec3 eye, vec3 lightPos, vec3 lightIntensity) {
		vec3 N = n;
		vec3 L = normalize(lightPos - p);
		vec3 V = normalize(eye - p);
		vec3 R = normalize(reflect(-L, N));

		float dotLN = dot(L, N);
		float dotRV = dot(R, V);

		if (dotLN < 0.0) {
			// Light not visible from this point on the surface
			return vec3(0.0, 0.0, 0.0);
		}
		
		// Clamp added to prevent harsh lighting transitions at high reflection angles
		dotLN = clamp(dotLN, 0.0, 1.0);

		if (dotRV < 0.0) {
			// Light reflection in opposite direction as viewer, apply only diffuse component
			return lightIntensity * (k_d * dotLN);
		}
		return lightIntensity * (k_d * dotLN + k_s * pow(dotRV, alpha));
	}

	/**
	 * Lighting via Phong illumination.
	 * 
	 * The vec3 returned is the RGB color of that point after lighting is applied.
	 * k_a: Ambient color
	 * k_d: Diffuse color
	 * k_s: Specular color
	 * alpha: Shininess coefficient
	 * p: position of point being lit
	 * n: normal of point being lit
	 * eye: the position of the camera
	 *
	 * See https://en.wikipedia.org/wiki/Phong_reflection_model#Description
	 */
	vec3 phongIllumination(vec3 k_a, vec3 k_d, vec3 k_s, float alpha, vec3 p, vec3 n, vec3 eye) {
		const vec3 ambientLight = 0.5 * vec3(1.0, 1.0, 1.0);
		vec3 color = ambientLight * k_a;

		vec3 light1Pos = vec3(4.0, 2.0, 4.0);
		vec3 light1Intensity = vec3(0.45, 0.45, 0.45);
		color += phongContribForLight(k_d, k_s, alpha, p, n, eye, light1Pos, light1Intensity);

		vec3 light2Pos = vec3(-2.0, 3.0, 2.0);
		vec3 light2Intensity = vec3(0.45, 0.45, 0.45);
		color += phongContribForLight(k_d, k_s, alpha, p, n, eye, light2Pos, light2Intensity);
		
		return color;
	}

	/**
	 * Return a transform matrix that will transform a ray from view space
	 * to world coordinates, given the eye point, the camera target, and an up vector.
	 *
	 * This assumes that the center of the camera is aligned with the negative z axis in
	 * view space when calculating the ray marching direction. See rayDirection.
	 */
	mat4 viewMatrix(vec3 eye, vec3 center, vec3 up) {
		// Based on gluLookAt man page
		vec3 f = normalize(center - eye);
		vec3 s = normalize(cross(f, up));
		vec3 u = cross(s, f);
		return mat4(
			vec4(s, 0.0),
			vec4(u, 0.0),
			vec4(-f, 0.0),
			vec4(0.0, 0.0, 0.0, 1)
		);
	}
	
	
	//////////////////////
	// 3D Simplex Noise //
	//////////////////////
	
	// Solution from https://www.shadertoy.com/view/XsX3zB
	
	/* discontinuous pseudorandom uniformly distributed in [-0.5, +0.5]^3 */
	vec3 random3(vec3 c) {
		float j = 4096.0*sin(dot(c,vec3(17.0, 59.4, 15.0)));
		vec3 r;
		r.z = fract(512.0*j);
		j *= .125;
		r.x = fract(512.0*j);
		j *= .125;
		r.y = fract(512.0*j);
		return r-0.5;
	}

	/* skew constants for 3d simplex functions */
	const float F3 =  0.3333333;
	const float G3 =  0.1666667;

	/* 3d simplex noise */
	float simplex3d(vec3 p) {
		 /* 1. find current tetrahedron T and it's four vertices */
		 /* s, s+i1, s+i2, s+1.0 - absolute skewed (integer) coordinates of T vertices */
		 /* x, x1, x2, x3 - unskewed coordinates of p relative to each of T vertices*/

		 /* calculate s and x */
		 vec3 s = floor(p + dot(p, vec3(F3)));
		 vec3 x = p - s + dot(s, vec3(G3));

		 /* calculate i1 and i2 */
		 vec3 e = step(vec3(0.0), x - x.yzx);
		 vec3 i1 = e*(1.0 - e.zxy);
		 vec3 i2 = 1.0 - e.zxy*(1.0 - e);

		 /* x1, x2, x3 */
		 vec3 x1 = x - i1 + G3;
		 vec3 x2 = x - i2 + 2.0*G3;
		 vec3 x3 = x - 1.0 + 3.0*G3;

		 /* 2. find four surflets and store them in d */
		 vec4 w, d;

		 /* calculate surflet weights */
		 w.x = dot(x, x);
		 w.y = dot(x1, x1);
		 w.z = dot(x2, x2);
		 w.w = dot(x3, x3);

		 /* w fades from 0.6 at the center of the surflet to 0.0 at the margin */
		 w = max(0.6 - w, 0.0);

		 /* calculate surflet components */
		 d.x = dot(random3(s), x);
		 d.y = dot(random3(s + i1), x1);
		 d.z = dot(random3(s + i2), x2);
		 d.w = dot(random3(s + 1.0), x3);

		 /* multiply d by w^4 */
		 w *= w;
		 w *= w;
		 d *= w;

		 /* 3. return the sum of the four surflets */
		 return dot(d, vec4(52.0));
	}
	
	//////////////////////////
	// End 3D Simplex Noise //
	//////////////////////////

	vec3 readColorRamp(float p) {
		float texSize = 64.0;
		float rampX = (p * (texSize-2.0) + 0.5) / texSize;
		return texture2D(iColorRamp, vec2(rampX, 0.5)).rgb;
	}
	
	// "p" ranges from 0.0 to 1.0
	// Returns a vec4 representing a color along the gradient, where alpha is an emission value.
	// Used for coloring the inside of the sphere and gem.
	vec4 colorGradient(float p) {		
		// color sphere around gem
		if (p > GEM_MAT_RADIUS) {
			float rampPos = clamp(map(p, INNER_SPHERE_RADIUS, 1.0, 0.0, 1.0), 0.0, 1.0);
			return vec4(readColorRamp(rampPos), 0.0);
		}
		
		// color gem
		float brightness = pow(sin(p*100.0 - iTime*9.0) * 0.5 + 0.5, 8.0);
		vec3 color = readColorRamp(map(p, 0.1, GEM_MAT_RADIUS, 0.0, 1.0)) * 0.25;
		vec3 electricColor = mix(vec3(0.0, 0.35, 1.0), vec3(1.0, 1.0, 1.0), max(0.0, map(brightness, 0.85, 1.0, 0.0, 1.0)));
		return vec4(mix(color, electricColor, brightness), brightness);
	}
	
	// Calculates material color + Phong illumination
	// Alpha represents emission value
	vec4 colorAtPoint(vec3 p, vec3 normal, vec3 eye) {
		float distFromCenter = length(p);
		float noise = simplex3d(p * 10.0);
		
		vec4 materialColor = colorGradient(distFromCenter + noise*0.025);
		// Diffuse material color
		vec3 K_d = materialColor.rgb;
		// Ambient color should match diffuse, just darker and less saturated.
		vec3 K_a = K_d * 0.16 + 0.08;
		// Specular reflection color
		vec3 K_s = vec3(1.0, 1.0, 1.0);
		
		float shininess = 40.0;
		
		vec3 shadedColor = phongIllumination(K_a, K_d, K_s, shininess, p, normal, eye);
		// blend to raw diffuse color as emissive value increases
		return vec4(mix(shadedColor, K_d, materialColor.a), materialColor.a);
	}

	void mainImage( out vec4 fragColor, in vec2 fragCoord )
	{
		vec3 viewDir = rayDirection(45.0, iResolution.xy, fragCoord);
		float eyeAngle = iTime * CAMERA_SPIN;
		vec3 eye = vec3(
			sin(eyeAngle)*16.0,
			5.0,
			cos(eyeAngle)*16.0
		);
	
		mat4 viewToWorld = viewMatrix(eye, vec3(0.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));
		
		// represents `viewDir` in world space
		vec3 worldDir = (viewToWorld * vec4(viewDir, 0.0)).xyz;

		float dist = shortestDistanceToSurface(eye, worldDir, MIN_DIST, MAX_DIST);

		if (dist > MAX_DIST - EPSILON) {
			// Didn't hit anything
			fragColor = vec4(0.0, 0.0, 0.0, 0.0);
			return;
		}

		// The closest point on the surface to the eyepoint along the view ray
		vec3 p = repeatDomain(eye + dist * worldDir);
		vec3 normal = estimateNormal(p);
		
		// Reflection bounce #1
#if REFLECT_BOUNCES>0
		vec3 reflDir = normalize(reflect(worldDir, normal));
		float reflDist = shortestDistanceToSurface(p + reflDir*REFL_OFFSET, reflDir, MIN_DIST, MAX_DIST);
		vec3 reflColor = vec3(0.0, 0.0, 0.0);
		float fresnel = 0.0; // 0.0 means no reflection, as we don't want to reflect the black environment
		// Only account for reflection if we hit a surface
		if (reflDist < MAX_DIST - EPSILON) {
			vec3 reflP = repeatDomain(p + reflDist * reflDir);
			vec3 reflNormal = estimateNormal(reflP);
			// 0.0 when normal faces camera, 1.0 when normal is perpendicular to camera (unless FRESNEL_FACTOR overrides)
			fresnel = 1.0 - dot(worldDir, -normal)*FRESNEL_FACTOR;
			// make gem more mirror-like, by mapping fresnel from 0.0-1.0 to 0.5-1.0
			if (length(p) < GEM_MAT_RADIUS) {
				fresnel = fresnel * 0.5 + 0.5;
			}
			
			// Reflection bounce #2
	#if REFLECT_BOUNCES>1
			vec3 reflDir2 = normalize(reflect(reflDir, reflNormal));
			float reflDist2 = shortestDistanceToSurface(reflP + reflDir2*REFL_OFFSET, reflDir2, MIN_DIST, MAX_DIST);
			vec3 reflColor2 = vec3(0.0, 0.0, 0.0);
			float fresnel2 = 0.0;
			if (reflDist2 < MAX_DIST - EPSILON) {
				vec3 reflP2 = repeatDomain(reflP + reflDist2 * reflDir2);
				vec3 reflNormal2 = estimateNormal(reflP2);
				fresnel2 = 1.0 - dot(reflDir, -reflNormal)*FRESNEL_FACTOR;
				if (length(reflP) < GEM_MAT_RADIUS) {
					fresnel2 = fresnel2 * 0.5 + 0.5;
				}
				// Get color at second reflection point, as seen from first reflection point
				reflColor2 = colorAtPoint(reflP2, reflNormal2, reflP).rgb;
			}
			
			// Get color at first reflection point, as seen from first raymarched contact point
			vec4 color = colorAtPoint(reflP, reflNormal, p);
			// Blend first and second reflection point colors
			vec3 colorWithRefl = mix(color.rgb, reflColor2, fresnel2);
			// Account for light emission
			colorWithRefl += color.rgb * color.a;
			
			reflColor = colorWithRefl;
	#else
			// no second reflection bounce
			reflColor = colorAtPoint(reflP, reflNormal, p).rgb;
	#endif
		}
		
		// Get primary point color as seen from eye
		vec4 color = colorAtPoint(p, normal, eye);
		// Blend point color with first reflection color
		// (which has already been mixed with secondary reflection color)
		vec3 colorWithRefl = mix(color.rgb, reflColor, fresnel);
		
		// Account for light emission
		// As emission increases, add source color back in to override reflection color
		colorWithRefl += color.rgb * color.a;
		
		fragColor = vec4(colorWithRefl, 1.0);
#else
	// no reflection
	vec3 color = colorAtPoint(p, normal, eye).rgb;
	fragColor = vec4(color, 1.0);
#endif
	}
</script>
              
            
!

CSS

              
                canvas {
	touch-action: none;
}
              
            
!

JS

              
                console.clear();

// This is a Shadertoy-like environment I created with Regl.
// The core fragment shader code is declared in the HTML tab, while all the boilerplate is here.
// Some differences from Shadertoy:
// - iMouse values range from 0.0 to 1.0, with (0, 0) being the top-left of canvas.

// On high-dpi screens, render halfway between a dpr of 1.0 and the full high-res dpr.
const pixelRatio = window.devicePixelRatio > 1 ? ((window.devicePixelRatio - 1) / 2 + 1) : 1;

const regl = createREGL({ pixelRatio });

// Read fragment shader text from HTML tab, and make substitutions from URL params.
function readFragmentShader() {
	let shader = document.getElementById('frag-shader').textContent;
	const params = new URLSearchParams(location.search);
	// reflection bounces replacement is special since it's a #define value
	const REFLECT_BOUNCES = params.get('REFLECT_BOUNCES');
	if (REFLECT_BOUNCES) {
		shader = shader.replace(/#define REFLECT_BOUNCES \d/, `#define REFLECT_BOUNCES ${REFLECT_BOUNCES}`);
	}
	// replace constants
	[
		'CAMERA_SPIN',
		'INNER_SPHERE_RADIUS',
		'STAR_RADIUS',
		'STAR_RADIUS_2',
		'GEM_RADIUS',
		'GEM_MAT_RADIUS',
		'GEM_SPIN',
		'FRESNEL_FACTOR'
	].forEach(name => {
		const paramValue = params.get(name);
		if (paramValue) {
			shader = shader.replace(new RegExp(`${name} = .+?;`), `${name} = ${paramValue};`);
		}
	});
	
	return shader;
}

const drawColors = regl({
	depth: { enable: false },
	blend: {
		enable: true,
		func: {
			src: 'src alpha',
			dst: 'one'
		}
	},
	vert: `
		precision highp float;

		attribute vec2 a_position;

		void main() {
			gl_Position = vec4(a_position, 0, 1);
		}
	`,
	frag: `
		precision highp float;

		uniform float iTime;
		uniform vec3 iResolution;
		uniform vec4 iMouse;
		uniform sampler2D iColorRamp;

		${readFragmentShader()}
		
		void main() {
			mainImage(gl_FragColor, gl_FragCoord.xy);
		}
	`,
	attributes: {
		// two triangles that cover screen
		a_position: regl.buffer([
			// tri 1
			[-1, 1],
			[1, 1],
			[-1, -1],
			// tri 2
			[1, -1],
			[-1, -1],
			[1, 1]
		])
	},
	uniforms: {
		iTime: regl.context('time'),
		iResolution: (context) => [context.framebufferWidth, context.framebufferHeight, 1],
		iMouse: regl.prop('iMouse'),
		iColorRamp: regl.prop('iColorRamp')
	},
	count: 6
});


// Create color ramp texture

const gradient = (function GradientSamplerFactory() {
	// Based on: https://codepen.io/MillerTime/pen/NXxxma
	// The instance to be returned.
	const sampler = {};

	const colors = [
	{ "r": 255, "g": 32,  "b": 64  },
	{ "r": 242, "g": 121, "b": 2   },
	{ "r": 242, "g": 164, "b": 0   },
	{ "r": 200, "g": 212, "b": 0   },
	{ "r": 44,  "g": 228, "b": 46  },
	{ "r": 0,   "g": 180, "b": 141 },
	{ "r": 24,  "g": 100,  "b": 255 },
	{ "r": 255,  "g": 255,  "b": 255 }
];

	const colorCount = colors.length;
	const colorSpans = colorCount - 1;
	const spanSize = 1 / colorSpans;

	// Helper to interpolate between two numbers
	function interpolate(a, b, p) {
		return (b - a) * p + a;
	};

	sampler.sample = function sample(position) {
		// Normalize position to 0..1 scale (inclusive of 0, exlusive of 1).
		position -= position | 0;
		if (position < 0) position = 1 - position * -1;

		const startIndex = position * colorSpans | 0;
		const startColor = colors[startIndex];
		const endColor = colors[startIndex + 1];
		// Compute relative position between two chosen color stops.
		const innerPosition = (position - (startIndex / colorSpans)) / spanSize;

		const r = interpolate(startColor.r, endColor.r, innerPosition) | 0;
		const g = interpolate(startColor.g, endColor.g, innerPosition) | 0;
		const b = interpolate(startColor.b, endColor.b, innerPosition) | 0;

		return { r, g, b };
	};

	return sampler;
})();

const texSize = 64; // Number of pixels in color ramp, also used in shader
const channels = 3;
const texData = new Uint8Array(texSize * channels);
for (let i=0; i<texSize; i++) {
	const offset = i * channels;
	const color = gradient.sample(i / (texSize - 1));
	texData[offset] = color.r;
	texData[offset+1] = color.g;
	texData[offset+2] = color.b;
}

const colorRampTexture = regl.texture({
	width: texSize,
	height: 1,
	format: 'rgb',
	data: texData,
	mag: 'linear',
	min: 'linear',
	wrapS: 'clamp',
	wrapT: 'clamp'
});


const clearState = {
	color: [0, 0, 0, 1]
};

let play = true;
// window.onclick = () => play = !play;

let mouseX = 0;
let mouseY = 0;
['pointerdown', 'pointermove'].forEach(eventName => {
	document.querySelector('canvas').addEventListener(eventName, event => {
		mouseX = event.pageX / window.innerWidth;
		mouseY = event.pageY / window.innerHeight;
	});
});

regl.frame((context) => {
	if (play) {
		regl.clear(clearState);
		drawColors({
			// Use vec4 for iMouse, so we can add button down states later if desirable
			// (without breaking old code)
			iMouse: [mouseX, mouseY, 0.0, 0.0],
			iColorRamp: colorRampTexture
		});
	}
});

              
            
!
999px

Console