<digital-art aria-hidden="true">
<script type="frag">
const int ITERS = 100;
const float PI = 3.141592654;
uniform float time;
uniform float meter;
uniform float mouseX;
uniform float mouseY;
uniform vec2 resolution;
varying vec2 vUv;
// Calculate cameras "orthonormal basis", i.e. its transform matrix components
vec3 getCameraRayDir(vec2 uv, vec3 camPos, vec3 camTarget) {
vec3 camForward = normalize(camTarget - camPos);
vec3 camRight = normalize(cross(vec3(0.0, 1.0, 0.0), camForward));
vec3 camUp = normalize(cross(camForward, camRight));
float fPersp = 2.0;
vec3 vDir = normalize(uv.x * camRight + uv.y * camUp + camForward * fPersp);
return vDir;
}
// distance function for a sphere
float sphere(vec3 p, float r)
{
return length(p) - r;
}
float deformation(vec3 pos) {
return -.07 * abs(sin(atan(pos.z, pos.x) * 8.));
}
float subtract(float a, float b)
{
return max(-a, b);
}
float pumpkin(vec3 pos, vec3 center) {
float eyeR = sin(time * 1.7) * .2;
float eyeL = cos(time * 1.7) * .2;
float d = deformation(pos - center);
vec3 spherePos = vec3(0) + center;
vec3 eyeLeftPos = vec3(-1., 1., -3. + eyeL) + center;
vec3 eyeRightPos = vec3(1., 1., -3. + eyeR) + center;
vec3 mouthPos = vec3(0.0, -1., -3.) + center;
vec3 mouth2Pos = vec3(0.0, -.6 - meter * .06 + sin(time * 2.) * .3, -3.) + center;
vec3 mouthScale = vec3(0.6, 1., 1.);
float t = sphere(pos - spherePos, 3.0) + d;
t = subtract(sphere(pos - spherePos, 2.9), t);
t = subtract(sphere(pos - eyeLeftPos, .7), t);
t = subtract(sphere(pos - eyeRightPos, .7) , t);
float mouth = sphere((pos - mouthPos) * mouthScale, .9) + d;
mouth = subtract(sphere((pos - mouth2Pos) * mouthScale, .89) + d, mouth);
t = subtract(mouth, t);
return t;
}
float scene(vec3 pos) {
float t = pumpkin(mod(pos, 10.), vec3(5.));
//float t = pumpkin(pos, vec3(0.));
t = min(t, pumpkin(pos, vec3(0.)));
return t;
}
// cast a ray along a direction and return
// the distance to the first thing it hits
// if nothing was hit, return -1
float castRay(in vec3 rayOrigin, in vec3 rayDir)
{
float maxd = 80.0;
float t = 0.1;
for (int i=0; i< ITERS; i++) {
float h = scene(rayOrigin + rayDir * t );
if (h<(0.001 * t) || t>maxd) break;
t += h;
}
if( t>maxd ) t=-1.0;
return t;
}
// calculate normal:
vec3 calcNormal(vec3 pos)
{
// Center sample
float c = scene(pos);
// Use offset samples to compute gradient / normal
vec2 eps_zero = vec2(0.001, 0.0);
return normalize(vec3( scene(pos + eps_zero.xyy), scene(pos + eps_zero.yxy), scene(pos + eps_zero.yyx) ) - c);
}
float S(float a, float b, float c) {
return sin(a * b + c);
}
vec3 background(vec2 p, float time) {
float s = S(.4, time, time);
float u = S(2.5 + 1. * s * 2., .5 * p.x, 1. + time);
float v = S(6.4 + 3. * s * s, .5 * p.y, 2. + time);
float w = S(2.3 + 2. * s, p.y + p.x, 3. + time );
float r = .3 + .05 * u + .1 * v + .05 * w;
float g = .1 + .05 * u + .025 * v + .05 * w;
float b = .0 + .01 * u + .01 * v + .005 * w;
return vec3(r, g, b);
}
// Visualize depth based on the distance
vec3 render(vec2 p, vec3 rayOrigin, vec3 rayDir)
{
float t = castRay(rayOrigin, rayDir);
if (t == -1.0) {
return background(p * (1.2), time * .3);
}
// shading based on the distance
// vec3 col = vec3(4.0 - t * 0.35) * vec3(.7, 0, 1.0);
// shading based on the normals
vec3 pos = rayOrigin + rayDir * t;
vec3 N = calcNormal(pos);
vec3 L = normalize(vec3(sin(time *.1), 2.0, -0.5));
// L is vector from surface point to light
// N is surface normal. N and L must be normalized!
float NoL = max(dot(N, L), 0.0);
vec3 LDirectional = vec3(1.0, 0.9, 0.8) * NoL;
vec3 LAmbient = vec3(0.2);
vec3 col = vec3(1., .3, .0);
vec3 diffuse = col * (LDirectional + LAmbient);
return diffuse;
}
// normalize coords and correct for aspect ratio
vec2 normalizeScreenCoords()
{
float aspectRatio = resolution.x / resolution.y;
vec2 result = 2.0 * (gl_FragCoord.xy / resolution - 0.5);
result.x *= aspectRatio;
return result;
}
void main() {
float rotation = sin(time * .3) * PI / 4.;
vec3 camPos = vec3(12. * sin(rotation), -mouseY * 4., -12.0 * cos(rotation));
vec3 camTarget = vec3(0);
float aspectRatio = resolution.x / resolution.y;
vec2 p = (vUv - .5) * vec2(aspectRatio, 1.) * 2.;
vec3 rayDir = getCameraRayDir(p, camPos, camTarget);
vec3 col = render(p, camPos, rayDir);
gl_FragColor = vec4(col, 1.);
}
</script>
<script type="vert">
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4( position, 1.0 );
}
</script>
</digital-art>
<button>Play Music</button>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
body {
background: #222;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
margin: 0;
height: 100vh;
overflow: hidden;
}
button {
position: absolute;
background: orange;
right: 16px;
top: 16px;
font-family: 'Press Start 2P', monospace;
font-size: 1em;
padding: 16px;
color: black;
border: none;
cursor: pointer;
}
button:focus {
outline: 2px solid black;
}
button:active {
outline: 2px solid white;
}
digital-art {
display: block;
width: 100vw;
height: 100vh;
pointer-events: none;
}
digital-art * {
display: none;
}
digital-art canvas {
display: block;
}
class Music {
constructor() {}
async init() {
await Tone.start();
const meter = new Tone.Meter();
const gain = new Tone.Gain(.2);
gain.connect(Tone.Destination);
const noiseGain = new Tone.Gain(.1);
noiseGain.connect(meter);
meter.connect(gain);
this.digitalArt = document.querySelector('digital-art');
const noiseSynth = new Tone.NoiseSynth().connect(noiseGain);
const synth = new Tone.FMSynth().connect(meter);
const synth2 = new Tone.AMSynth().connect(meter);
const seq = new Tone.Sequence((time, note) => {
synth.triggerAttackRelease(note, "1n", time);
}, ["C2", ["D#2", "D2"], "G#2", "G2"], 2);
const rythm = new Tone.Sequence((time, note) => {
noiseSynth.triggerAttackRelease("16n", time);
}, ['x', 'x', 'x', 'x'], .25);
const seq2 = new Tone.Sequence((time, note) => {
synth2.triggerAttackRelease(note, "8n", time);
}, ['C3', 'D#3', 'G3', 'C4', 'G4', 'C4', 'G3', 'D#3'], .25)
rythm.start(0);
seq.start(0); // 4
seq2.start(0); // 20
this.noiseSynth = noiseSynth;
this.synth = synth;
this.synth2 = synth2;
this.seq = seq;
this.seq2 = seq2;
this.rythm = rythm;
this.meter = meter;
this.meterTimer = -1;
}
get state() {
return Tone.Transport.state;
}
async start() {
if (!this.synth) {
await this.init();
}
Tone.Transport.start('+0', 0);
this.meterTimer = setInterval(() => {
if (this.digitalArt) {
this.digitalArt.setAttribute('meter', this.meter.getValue());
}
}, 10)
}
stop() {
Tone.Transport.stop();
clearInterval(this.meterTimer);
if (this.digitalArt) {
this.digitalArt.setAttribute('meter', '0');
}
this.meterTimer = -1;
}
}
class DigitalArt extends HTMLElement {
constructor() {
super();
this.renderer = null;
this.scene = null;
this.camera = null;
this.uniforms = {};
this.clock = null;
this.onResize = this.onResize.bind(this);
this.onMouseMove = this.onMouseMove.bind(this);
this.animate = this.animate.bind(this);
this.resources = [];
this.frag = this.querySelector('script[type=frag]').textContent.trim();
this.vert = this.querySelector('script[type=vert]').textContent.trim();
}
static register() {
customElements.define("digital-art", DigitalArt);
}
static get observedAttributes() {
return ['meter'];
}
get meter() {
return parseInt(this.getAttribute('meter'), 10);
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'meter') {
this.uniforms.meter.value = parseInt(newValue, 10)
}
}
connectedCallback() {
if (! this.renderer) {
this.setup();
}
}
disconnectedCallback() {
this.dispose();
}
setup() {
this.renderer = new THREE.WebGLRenderer();
this.renderer.setPixelRatio(1);
this.appendChild(this.renderer.domElement);
this.clock = new THREE.Clock();
this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
this.scene = new THREE.Scene();
this.uniforms = {
time: { value: 1.0 },
resolution: { value: new THREE.Vector2(427,842) },
meter: { value: 0. },
mouseX: { value: 0. },
mouseY: { value: 0. }
}
const geometry = new THREE.PlaneBufferGeometry(2, 2);
const material = new THREE.ShaderMaterial({
uniforms: this.uniforms,
vertexShader: this.vert,
fragmentShader: this.frag,
});
this.resources.push(geometry, material);
const mesh = new THREE.Mesh(geometry, material);
this.scene.add(mesh);
this.onResize();
window.addEventListener('resize', this.onResize, false);
this.frame = requestAnimationFrame(this.animate);
window.addEventListener('mousemove', this.onMouseMove, false);
}
onMouseMove(e) {
this.uniforms.mouseY.value = e.clientY / innerHeight - .5;
this.uniforms.mouseX.value = e.clientX / innerWidth - .5;
}
onResize() {
const { renderer, uniforms } = this;
const width = this.clientWidth;
const height = this.clientHeight;
renderer.setSize(width, height);
uniforms.resolution.value.x = width;
uniforms.resolution.value.y = height;
}
animate(timestamp) {
const { animate, uniforms, renderer, clock, scene, camera } = this;
this.frame = requestAnimationFrame(animate);
uniforms.time.value = clock.getElapsedTime();
renderer.render(scene, camera);
}
cleanupScene(sceneOrGroup) {
if (!sceneOrGroup) {
sceneOrGroup = this.scene;
}
for (let item of [...sceneOrGroup.children]) {
if (item.type === 'group') {
this.cleanupScene(item);
}
sceneOrGroup.remove(item);
}
}
dispose() {
cancelAnimationFrame(this.frame);
window.removeEventListener('resize', this.onResize, false);
window.removeEventListener('mousemove', this.onMouseMove, false);
this.cleanupScene();
const removedResources = this.resources.splice(0, this.resources.length);
for (let item of removedResources) {
if (typeof item.dispose === 'function') {
item.dispose();
}
}
this.removeChild(this.renderer.domElement);
this.renderer.dispose();
this.renderer = null;
this.scene = null;
this.camera = null;
this.uniforms = {};
this.clock = null;
}
}
DigitalArt.register();
const music = new Music();
const button = document.querySelector('button');
button.addEventListener('click', async () => {
if (music.state == "started") {
music.stop();
button.textContent = "Restart Music";
} else {
music.start();
button.textContent = "Stop Music"
}
})
This Pen doesn't use any external CSS resources.