<!--
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;
}
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.