<div id="canvas-wrapper" aria-label="Bubble Gum"></div>

<div class="toggle p-flex-hzt-center" id="toggle-color">
	<button class="toggle__btn p-text" type="button" data-mode="sweet" data-active="true">sweet gum</button>
	<button class="toggle__btn p-text" type="button" data-mode="sour">sour gum</button>
</div>

<p class="credit p-text"><a href="https://www.ilithya.rocks/" target="blank" rel="external noopener" class="credit__link">by ilithya.rocks</a></p>

<script id="vertex" type="x-shader/x-vertex">
	varying vec2 vUv;

    void main() {
		vUv = uv;
		gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
</script>

<script id="fragment" type="x-shader/x-fragment">
precision highp float;
	
varying vec2 vUv;
	
uniform vec3 u_c1;
uniform vec3 u_c2;
uniform float u_time;

void main() {
    vec3 pX = vec3(vUv.x);
    vec3 pY = vec3(vUv.y);
	
	vec3 c1 = u_c1;
	vec3 c2 = u_c2;
	vec3 c3 = vec3(0.0, 1.0, 1.0); // aqua
	
    vec3 cmix1 = mix(c1, c2, pX + pY/2. + cos(u_time));
	vec3 cmix2 = mix(c2, c3, (pY - sin(u_time))*0.5);
	vec3 color = mix(cmix1, cmix2, pX * cos(u_time+2.));

    gl_FragColor = vec4(color, 1.0);
}
</script>
$c1: hotpink;
$c2: #fffffd; // white

* {
	user-select: none;
}

.p-text {
	color: $c2;
	font-family: Helvetica, Arial, sans-serif;
	font-size: 0.55rem;
	letter-spacing: 2px;
	line-height: 1.5;
	text-align: center;
	text-transform: lowercase;
	-webkit-tap-highlight-color: rgba(0,0,0,0);
}

.p-flex-hzt-center {
	display: flex;
	justify-content: center;
}

body {
	height: 100vh;
	background-color: $c1;
	margin: 0;
	padding: 0;
	overflow: hidden;
	position: relative;
}

.toggle {
	width: 100%;
	position: absolute;
	bottom: 36px;
	left: 0;
	z-index: 90;
	
	&__btn {
		$gap: 5px;
		$gap_anim: 2px;
		
		background-color: $c1;
		border: 1px solid $c2;
		cursor: pointer;
		outline: none;
		padding: $gap round($gap * 2.2) $gap round($gap * 2.5);
		transition: background-color 0.2s ease-in-out;
		
		&:last-child {
			border-left: 0;
		}
		
		&[data-active] {
			background-color: $c2;	
			color: $c1;
			font-weight: bold;
		}
	}
}

.credit {	
	width: 100%;
	position: absolute;
	bottom: 5px;
	left: 0;
	
	&__link {
		color: rgba($c2, 0.6);
		padding: 6px 15px 8px;
		text-decoration: none;
	}
}
View Compiled
/* 
 * BUBBLE GUM
 * Shader Exp XXXII.
 *
 * - Press buttons to change the gum's flavor: sweet vs. sour <3.
 *
 * Follow my shader experiments here:
 * https://github.com/ilithya/anydayshaders
 * https://twitter.com/hashtag/anydayshaders
 * https://www.instagram.com/explore/tags/anydayshaders/
 *
 * #061 - #100DaysOfCode
 * By ilithya | 2020
 * https://www.ilithya.rocks/
 * https://twitter.com/ilithya_rocks
 */

// GLOBAL
const nearDist = 1;
const farDist = 100;
const camera = new THREE.PerspectiveCamera(
	45,
	window.innerWidth / window.innerHeight,
	nearDist,
	farDist
);
const scene = new THREE.Scene();
const renderer = new THREE.WebGLRenderer();
const canvasWrapper = document.querySelector("#canvas-wrapper");
const clock = new THREE.Clock();
const zpos = farDist/4;

const init = () => {	
	camera.position.y = -0.5;	
	camera.position.z = zpos;	

	renderer.setClearColor('hotpink');
	renderer.setPixelRatio(window.devicePixelRatio);
	renderer.setSize(window.innerWidth, window.innerHeight);

	canvasWrapper.appendChild(renderer.domElement);
};
init();

// MATERIAL
const vertexShader = document.querySelector("#vertex").textContent;
const fragmentShader = document.querySelector("#fragment").textContent;
const uniSweet1 = { type: "v3", value: new THREE.Vector3(0.5, 0.0, 0.5) }; // purple
const uniSweet2 = { type: "v3", value: new THREE.Vector3(1.0, 0.41, 0.71) }; // hotpink
const uniSour1 = { type: "v3", value: new THREE.Vector3(0.90, 0.8, 0.30) }; // yellow
const uniSour2 = { type: "v3", value: new THREE.Vector3(1.0, 0.54, 0.40) }; // orange
const uniforms = {
	u_c1: uniSweet1,
	u_c2: uniSweet2,
	u_time: { type: "f", value: 1.0 },
};
const shaderMaterial = new THREE.ShaderMaterial({
	uniforms,
	fragmentShader,
	vertexShader,
});

// BACKGROUND
const bgWidth = window.innerWidth; // I wanted a big number here
const bgHeight = window.innerHeight; // Here too :)
/* FYI - the sizes of this plane are far too big in comparison with the actual width and height of our 3d camera */
const bgGeometry = new THREE.PlaneBufferGeometry(bgWidth, bgHeight);
const bgMesh = new THREE.Mesh(bgGeometry, shaderMaterial);
scene.add(bgMesh);

// BUBBLE GUM
const gumRadius = 5;
const gumGeometry = new THREE.SphereBufferGeometry(
	gumRadius,
	gumRadius * 6.4,
	gumRadius * 6.4
);
const gum = new THREE.Mesh(gumGeometry, shaderMaterial);
scene.add(gum);

// TOGGLE BUBBLE GUM'S FLAVOR
const toggle = {
	btnColor: document.querySelectorAll("#toggle-color button"),
	updateMaterial(mode) {		
		const defaultMode = 'sweet';
		
		shaderMaterial.uniforms.u_c1 = mode === defaultMode ? uniSweet1 : uniSour1;
		shaderMaterial.uniforms.u_c2 = mode === defaultMode ? uniSweet2 : uniSour2;
		shaderMaterial.uniformsNeedUpdate = true;
	},
	checkActiveBtnColor() {
		this.btnColor.forEach((el) => {
			el.addEventListener("click", (e) => {
				e.preventDefault();

				const target = e.currentTarget;

				this.btnColor.forEach((l) => delete l.dataset.active);
				target.dataset.active = true;

				this.updateMaterial(target.dataset.mode);
			});
		});
	}
};
toggle.checkActiveBtnColor();

// SCREEN RESIZE
const onWindowResize = () => {
	const w = window.innerWidth;
	const h = window.innerHeight;
	
	camera.aspect = w / h;
	camera.updateProjectionMatrix();

	renderer.setSize(w, h);	
};
window.addEventListener("resize", onWindowResize);

// CREATE ANIMATIONS
const createAnimShaders = () => uniforms.u_time.value = clock.getElapsedTime();

// RENDER BUBBLE GUM
const render = () => {	
	createAnimShaders();
	renderer.render(scene, camera);
	requestAnimationFrame(render);
};
render();

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://unpkg.com/three@0.117.1/build/three.min.js
  2. https://unpkg.com/three@0.117.1/examples/js/controls/OrbitControls.js