<!--  
  See more interactive works of mine:  https://codepen.io/ScavengerFrontend
-->

<div id="scene-container"></div>

<!-- Simplex Noise -->
<script>
  // from https://github.com/hughsk/glsl-noise/blob/master/simplex/3d.glsl
function noise() {
  return `
    //
    // Description : Array and textureless GLSL 2D/3D/4D simplex
    //               noise functions.
    //      Author : Ian McEwan, Ashima Arts.
    //  Maintainer : ijm
    //     Lastmod : 20110822 (ijm)
    //     License : Copyright (C) 2011 Ashima Arts. All rights reserved.
    //               Distributed under the MIT License. See LICENSE file.
    //               https://github.com/ashima/webgl-noise
    //

    vec3 mod289(vec3 x) {
      return x - floor(x * (1.0 / 289.0)) * 289.0;
    }

    vec4 mod289(vec4 x) {
      return x - floor(x * (1.0 / 289.0)) * 289.0;
    }

    vec4 permute(vec4 x) {
         return mod289(((x*34.0)+1.0)*x);
    }

    vec4 taylorInvSqrt(vec4 r)
    {
      return 1.79284291400159 - 0.85373472095314 * r;
    }

    float noise(vec3 v)
      {
      const vec2  C = vec2(1.0/6.0, 1.0/3.0) ;
      const vec4  D = vec4(0.0, 0.5, 1.0, 2.0);

    // First corner
      vec3 i  = floor(v + dot(v, C.yyy) );
      vec3 x0 =   v - i + dot(i, C.xxx) ;

    // Other corners
      vec3 g = step(x0.yzx, x0.xyz);
      vec3 l = 1.0 - g;
      vec3 i1 = min( g.xyz, l.zxy );
      vec3 i2 = max( g.xyz, l.zxy );

      //   x0 = x0 - 0.0 + 0.0 * C.xxx;
      //   x1 = x0 - i1  + 1.0 * C.xxx;
      //   x2 = x0 - i2  + 2.0 * C.xxx;
      //   x3 = x0 - 1.0 + 3.0 * C.xxx;
      vec3 x1 = x0 - i1 + C.xxx;
      vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y
      vec3 x3 = x0 - D.yyy;      // -1.0+3.0*C.x = -0.5 = -D.y

    // Permutations
      i = mod289(i);
      vec4 p = permute( permute( permute(
                 i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
               + i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
               + i.x + vec4(0.0, i1.x, i2.x, 1.0 ));

    // Gradients: 7x7 points over a square, mapped onto an octahedron.
    // The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
      float n_ = 0.142857142857; // 1.0/7.0
      vec3  ns = n_ * D.wyz - D.xzx;

      vec4 j = p - 49.0 * floor(p * ns.z * ns.z);  //  mod(p,7*7)

      vec4 x_ = floor(j * ns.z);
      vec4 y_ = floor(j - 7.0 * x_ );    // mod(j,N)

      vec4 x = x_ *ns.x + ns.yyyy;
      vec4 y = y_ *ns.x + ns.yyyy;
      vec4 h = 1.0 - abs(x) - abs(y);

      vec4 b0 = vec4( x.xy, y.xy );
      vec4 b1 = vec4( x.zw, y.zw );

      //vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;
      //vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;
      vec4 s0 = floor(b0)*2.0 + 1.0;
      vec4 s1 = floor(b1)*2.0 + 1.0;
      vec4 sh = -step(h, vec4(0.0));

      vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;
      vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;

      vec3 p0 = vec3(a0.xy,h.x);
      vec3 p1 = vec3(a0.zw,h.y);
      vec3 p2 = vec3(a1.xy,h.z);
      vec3 p3 = vec3(a1.zw,h.w);

    //Normalise gradients
      vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
      p0 *= norm.x;
      p1 *= norm.y;
      p2 *= norm.z;
      p3 *= norm.w;

    // Mix final noise value
      vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
      m = m * m;
      return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
                                    dot(p2,x2), dot(p3,x3) ) );
      }

  `
}
</script>
* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding; 0;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
  background: rgb(214,182,182);
  background: linear-gradient(180deg, rgba(214,182,182,1) 0%, rgba(79,106,95,1) 50%, rgba(36,14,21,1) 100%);
}
/*
  
  This is the worst present!
  A non-cozy blanket.
  Now imagine wrapping yourself in it. 
  What a horrible feeling. Brrr.

  Big thanks to Marco Fugaro - read his article about calculating normals:
  https://discourse.threejs.org/t/calculating-vertex-normals-after-displacement-in-the-vertex-shader/16989
  
  Made by Anna Scavenger
  https://twitter.com/ouchpixels
  December 2023
  
  License: You can remix, adapt, and build upon this thing non-commercially.
  
*/

// TODO: update version!
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.118.3/build/three.module.js';
// import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.165.0/build/three.module.min.js';

let container, scene, camera, renderer;
let blanket;

// LANDSCAPE / PORTRAIT

let isMobile = /(Android|iPhone|iOS|iPod|iPad)/i.test(navigator.userAgent);
let windowRatio = window.innerWidth / window.innerHeight;
let isLandscape = (windowRatio > 1) ? true : false;

// MOUSE

let isMouseMove = false;
let mouseX = 0;

const clock = new THREE.Clock();

init();
render();

function init() {
  
  container = document.querySelector("#scene-container");

  scene = new THREE.Scene();

  initCamera();
  initLights();
  initRenderer();
  
  initBlanket();
  
  window.addEventListener('resize', onWindowResize, false);
  document.addEventListener('mousemove', onMouseMove);
  document.addEventListener('touchmove', onTouchMove);
  window.addEventListener('mouseout', onMouseLeave);
  
}

function initCamera() {
  
  camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 1000);
  camera.position.set(0, 2.0, 10.0);
  camera.position.z = (windowRatio > 2) ? ((5 / windowRatio) + 9) : (15 / windowRatio);
  
}

function initLights() {

  const dirLight = new THREE.DirectionalLight(0xffffff, 0.75);
  dirLight.position.set(-0.5, 10, -10);
  scene.add(dirLight);

  const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
  //ambientLight.position.set(0.5, 10, -5);
  scene.add(ambientLight);

}

function initRenderer() {
    
  renderer = new THREE.WebGLRenderer({alpha: true, antialias: true });
  // renderer.setPixelRatio(window.devicePixelRatio > 1.4 ? Math.min(window.devicePixelRatio, 1.25) : Math.min(window.devicePixelRatio, 1.25));
  renderer.setSize(window.innerWidth, window.innerHeight);
  //renderer.outputColorSpace = THREE.SRGBColorSpace;
  container.appendChild(renderer.domElement);
    
}

function initBlanket() {
  
  const SIZE = 6.5;
  const RESOLUTION = 75;

  const geometry = new THREE.PlaneBufferGeometry(SIZE, SIZE, RESOLUTION, RESOLUTION);
  geometry.rotateX(-0.5 * Math.PI);

  const tartanMaterial = new THREE.ShaderMaterial({
  
    lights: true,
    side: THREE.DoubleSide,
  
    extensions: {
      derivatives: true,
    },

    defines: {
      STANDARD: '',
      PHYSICAL: '',
    },

    uniforms: {
    
      ...THREE.ShaderLib.physical.uniforms,
      roughness: { value: 0.0 },
      diffuse: {value: new THREE.Color(0xffffff)},
      time: { value: 0.0 },
      amplitude: { value: 0.4 },
      frequency: { value: 0.4 },
      speed: { value: 0.3 },
      u_time: { value: 0.0 },
      u_resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }
    
    },

    vertexShader: monkeyPatch(THREE.ShaderChunk.meshphysical_vert, {
    
      header: `
        uniform float time;
        uniform float amplitude;
        uniform float speed;
        uniform float frequency;
      
        varying vec2 vUv;

        ${noise()}
      
        float displace(vec3 point) {
          return noise(vec3(point.x * frequency, point.z * frequency, time * speed)) * amplitude;
        }
      
        // http://lolengine.net/blog/2013/09/21/picking-orthogonal-vector-combing-coconuts
        vec3 orthogonal(vec3 v) {
          return normalize(abs(v.x) > abs(v.z) ? vec3(-v.y, v.x, 0.0)
        : vec3(0.0, -v.z, v.y));
        }
      `,
      // adapted from http://tonfilm.blogspot.com/2007/01/calculate-normals-in-shader.html
      main: `
        vec3 displacedPosition = position + normal * displace(position);

        float offset = ${SIZE / RESOLUTION};
        vec3 tangent = orthogonal(normal);
        vec3 bitangent = normalize(cross(normal, tangent));
        vec3 neighbour1 = position + tangent * offset;
        vec3 neighbour2 = position + bitangent * offset;
        vec3 displacedNeighbour1 = neighbour1 + normal * displace(neighbour1);
        vec3 displacedNeighbour2 = neighbour2 + normal * displace(neighbour2);

        // https://i.ya-webdesign.com/images/vector-normals-tangent-16.png
        vec3 displacedTangent = displacedNeighbour1 - displacedPosition;
        vec3 displacedBitangent = displacedNeighbour2 - displacedPosition;

        // https://upload.wikimedia.org/wikipedia/commons/d/d2/Right_hand_rule_cross_product.svg
        vec3 displacedNormal = normalize(cross(displacedTangent, displacedBitangent));
      `,

      '#include <defaultnormal_vertex>': THREE.ShaderChunk.defaultnormal_vertex.replace(
      // transformedNormal will be used in the lighting calculations
      'vec3 transformedNormal = objectNormal;',
      `vec3 transformedNormal = displacedNormal;`
      ),

      // transformed is the output position
      '#include <morphtarget_vertex>': `vUv = uv;`,
      '#include <displacementmap_vertex>': `
        transformed = displacedPosition;
      `,
    }),
  
    fragmentShader: monkeyPatch(THREE.ShaderChunk.meshphysical_frag, {
    
      header: `
      
        #define FREQUENCY 40
        #define TILT -60
        #define PATTERN 0.7
      
        varying vec2 vUv;
        uniform vec2 u_resolution;
      
        float coordinateGrid(vec2 r, float lineWidth, float offset, bool doubleLine) {

          float pixel = 0.0;
  
  for(float i = 0.0; i < 2.0; i += PATTERN) {
               
        float x = mod(i, PATTERN * 2.0);
        
        if (doubleLine) {
            
            if (x == 0.0) {
                pixel += 1.0 - step(lineWidth, abs(r.x - i - offset)); //first x line
                pixel += 1.0 - step(lineWidth, abs(r.y - i + offset)); //first y line
            } else {
                pixel += 1.0 - step(lineWidth, abs(r.x - i + offset)); //second x line
                pixel += 1.0 - step(lineWidth, abs(r.y - i - offset)); //second y line
            }
            
        } else {
            pixel += 1.0 - step(lineWidth, abs(r.x - i*2.0 - offset)); //first x line
            pixel += 1.0 - step(lineWidth, abs(r.y - i*2.0 + offset)); //first y line
        }
  }

  return pixel;
}
    `,
    main: ``,
    '#include <logdepthbuf_fragment>': `
    
    vec2 r = vUv;

    vec4 lightred = vec4(220.0/255.0, 23.0/255.0, 10.0/255.0, 1.0);
    vec4 darkRed = vec4(120.0/255.0, 12.0/255.0, 30.0/255.0, 1.0);
    vec4 yellow = vec4(190.0/255.0, 170.0/255.0, 59.0/255.0, 1.0);
    vec4 white = vec4(242.0/255.0, 242.0/255.0, 203.0/255.0, 0.1);
    vec4 blue = vec4(5.0/255.0, 10.0/255.0, 0.0/255.0, 0.8); 
    vec4 purp = vec4(0.0/255.0, 0.0/255.0, 0.0/255.0, 0.9); 

    vec4 pixel = lightred; // bg color
    
    pixel = mix(pixel, darkRed, coordinateGrid(r, 0.15, 0.0, true)); //paired line
    pixel = mix(pixel, white, coordinateGrid(r, 0.01, 0.005, true)); //paired line
    pixel = mix(pixel, white, coordinateGrid(r, 0.01, -0.35, false)); //paired line
    pixel = mix(pixel, purp, coordinateGrid(r, 0.01, -0.4, false)); //single line
    pixel = mix(pixel, purp, coordinateGrid(r, 0.01, -0.3, false)); //single line
    pixel = mix(pixel, blue, coordinateGrid(r, 0.02, 0.15, true)); //paired line
    pixel = mix(pixel, yellow, coordinateGrid(r, 0.01, 0.05, true)); //paired line

    float stripe = fract( dot(r, vec2(FREQUENCY,TILT)));
    pixel = mix(pixel, darkRed, stripe);
  
          diffuseColor = pixel;
    `,
    
  })
});

  blanket = new THREE.Mesh(geometry, tartanMaterial);
  blanket.position.set(0, 2.0, -0.5);
  blanket.rotation.set(Math.PI * 0.1, Math.PI * 0.25, 0);
  scene.add(blanket);

}

function render() {
  
  update();
  renderer.render(scene, camera);
  requestAnimationFrame(render);
  
}

function update() {
  
  let t = clock.getDelta();
  
  if (isMouseMove) {
    
    blanket.material.uniforms.time.value = 0.1 + 3.0 * mouseX;
    
  } else {
    
    blanket.material.uniforms.time.value += 3.0 * t;
  
  }

}

// * UTILS * 

function monkeyPatch(shader, { defines = '', header = '', main = '', ...replaces }) {
  
  let patchedShader = shader;

  const replaceAll = (str, find, rep) => str.split(find).join(rep);
  
  Object.keys(replaces).forEach((key) => {
    
    patchedShader = replaceAll(patchedShader, key, replaces[key])
    
  });

  patchedShader = patchedShader.replace(
    'void main() {',
    `
    ${header}
    void main() {
      ${main}
    `
  );

  return `
    ${defines}
    ${patchedShader}
  `

}

// * EVENTS *

function onWindowResize() {
  
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  
}

function onMouseMove(event) {
  
  mouseX = (event.clientX / window.innerWidth) * 2 - 1;
  isMouseMove = true;
  console.log(mouseX);
    
}

function onTouchMove(event) {
    
  let x = event.changedTouches[0].clientX;
  mouseX = (x / window.innerWidth) * 2 - 1;
  isMouseMove = true;
        
}

function onMouseLeave(event) {
  
  console.log('mouseleft');
  isMouseMove = false;
        
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.