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

              
                <!-- "Squid Game - Giant Doll" (https://skfb.ly/o7OMD) by Rzyas is licensed under Creative Commons Attribution (http://creativecommons.org/licenses/by/4.0/). -->
<!-- Doll audio from https://www.youtube.com/watch?v=Nk8V30AIuP4 -->
<!-- Shotgun Audio from https://soundbible.com/1919-Shotgun-Blast.html -->
<!-- Sigh audio from https://soundbible.com/773-Sigh.html -->

<div class="container">
  <div class="game">
    <canvas class="webcam" width=640 height=480></canvas>
    <div class="timer"><span class="time">01:00</span></div>
    <div class="distance"><span class="total">000</span> /<span>100</span></div>
    <div class="movement"><span class="total">00%</span></div>
  </div>
</div>

<div class="howto ui is-visible">
  <svg viewBox="0 0 191.99 60.82">
    <path d="M28.3,10.23A22.3,22.3,0,1,1,6,32.53a22.29,22.29,0,0,1,22.3-22.3m0-6a28.3,28.3,0,1,0,28.29,28.3A28.33,28.33,0,0,0,28.3,4.23Z" fill="#fff"/>
    <path d="M186,10.57V54.49H142.06V10.57H186m6-6H136.06V60.49H192V4.57Z" fill="#fff"/>
    <path d="M93.18,12l23.7,41.05H69.48l23.7-41m0-12L88,9,64.28,50.05l-5.19,9h68.19l-5.2-9L98.38,9l-5.2-9Z" fill="#fff"/>
  </svg>
  <h1>Red Light, Green Light</h1>
  <p>
    You have 60 seconds to reach the end of the field.
  </p>
  <p>
    When the doll is not watching, move your head to move forward and make the top left counter reach 100m.
  </p>
  <p>
    But watch out to not move when the doll is watching you. If the movement sensor reaches 100% on the bottom right, you are eliminated.
  </p>
  <p class="note">This demo requires access to your webcam.<br>It currently doesn't work on iOs Safari.</p>
  <button class="cta start"></button>
</div>
<div class="dead ui">
  <svg viewBox="0 0 191.99 60.82">
    <path d="M28.3,10.23A22.3,22.3,0,1,1,6,32.53a22.29,22.29,0,0,1,22.3-22.3m0-6a28.3,28.3,0,1,0,28.29,28.3A28.33,28.33,0,0,0,28.3,4.23Z" fill="#fff"/>
    <path d="M186,10.57V54.49H142.06V10.57H186m6-6H136.06V60.49H192V4.57Z" fill="#fff"/>
    <path d="M93.18,12l23.7,41.05H69.48l23.7-41m0-12L88,9,64.28,50.05l-5.19,9h68.19l-5.2-9L98.38,9l-5.2-9Z" fill="#fff"/>
  </svg>
  <h1>You died!</h1>
  <p>
    Oops, it looks like you were moving too much or your reached the timeout...
  </p>
  <button class="cta replay replay1 is-ready">Replay</button>
</div>
<div class="win ui">
  <svg viewBox="0 0 191.99 60.82">
    <path d="M28.3,10.23A22.3,22.3,0,1,1,6,32.53a22.29,22.29,0,0,1,22.3-22.3m0-6a28.3,28.3,0,1,0,28.29,28.3A28.33,28.33,0,0,0,28.3,4.23Z" fill="#fff"/>
    <path d="M186,10.57V54.49H142.06V10.57H186m6-6H136.06V60.49H192V4.57Z" fill="#fff"/>
    <path d="M93.18,12l23.7,41.05H69.48l23.7-41m0-12L88,9,64.28,50.05l-5.19,9h68.19l-5.2-9L98.38,9l-5.2-9Z" fill="#fff"/>
  </svg>
  <h1>You won!</h1>
  <p>
    Well done, you survived... for now.
  </p>
  <button class="cta replay replay2 is-ready">Replay</button>
</div>

              
            
!

CSS

              
                @font-face {
 font-family: "Digital";
 src: url("https://assets.codepen.io/127738/digital-7+%28mono%29.ttf") format("truetype");
}
.timer {
  font-family: Digital, monospace;
  font-size: min(max(14px, 7vw), 46px);
  color: #d7213c;
  border: 5px outset #8b8b8b;
  background: black;
  text-align: center;
  padding: 0 20px;
  position: absolute;
  bottom: 1.5vw;
  left: 1.5vw;
}
.distance {
  font-family: Digital, monospace;
  font-size: min(max(14px, 7vw), 46px);
  color: #d7213c;
  border: 5px outset #8b8b8b;
  background: black;
  text-align: center;
  padding: 0 20px;
  position: absolute;
  top: 1.5vw;
  left: 1.5vw;
}
.movement {
  font-family: Digital, monospace;
  font-size: min(max(14px, 7vw), 46px);
  color: #d7213c;
  border: 5px outset #8b8b8b;
  background: black;
  text-align: center;
  padding: 0 20px;
  position: absolute;
  bottom: 1.5vw;
  right: 1.5vw;
}
.container {
  height: 100vh;
  display: grid;
  place-items: center;
  background: linear-gradient(#58abc2 50%, #d7327a 50%);
  filter: brightness(0.7);
  transition: 0.4s ease-out;
  &.is-playing {
    filter: brightness(1);
  }
}
.info {
  font-size: 30px;
  position: fixed;
  top: 0;
  left: 0;
  padding: 10px;
  background: white;
  z-index:19;
}
body {
  margin: 0;
}

.game {
  background: url(https://assets.codepen.io/127738/Squid_Game_Doll_bg.jpg);
  background-size: cover;
  background-position: center;
  position: relative;
  box-shadow: 0 0 10px rgba(0,0,0,0.4);
  font-size: 0;
  border-radius: 10px;
}
.doll {}
.webcam {
  position: absolute;
  top: 1.5vw;
  right: 1.5vw;
  width: 25%;
}

.ui {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  max-width: 620px;
  width: 90%;
  background: black;
  color: white;
  z-index: 50;
  text-align: center;
  font-family: 'Quicksand', sans-serif;
  font-weight: 400;
  border-radius: 20px;
  box-shadow: 0 0 20px 2px #00000059;
  padding: 20px 60px;
  
  max-height: 90vh;
  overflow: auto;
  box-sizing: border-box;
  
  opacity: 0;
  pointer-events: none;
  transform: translate(-50%, -50%) scale(0.8);
  transition: 0.3s ease-out;
  &.is-visible {
    opacity: 1;
    pointer-events: auto;
    transform: translate(-50%, -50%);
  }
  svg {
    position: absolute;
    top: 10%;
    left: 10%;
    opacity: 0.15;
    width: 80%;
    height: 80%;
    pointer-events: none;
  }
  h1 {
    font-size: 600;
    font-size: 26px;
  }
  .note {
    font-size: 0.8em;
  }
  p {
    font-size: 18px;
    margin-bottom: 0;
    & + p {
      margin-top: 10px;
    }
  }
}
.cta {
  pointer-events: none;
  background: white;
  border-radius: 0;
  border: none;
  font: inherit;
  font-size: 24px;
  font-weight: 600;
  margin-top: 30px;
  cursor: pointer;
  padding: 10px 20px;
  &::before {
    content: 'Loading...';
  }
  &.is-ready {
    pointer-events: unset;
    &::before {
      content: 'Start';
    }
  }
}

.win,
.dead {
  .cta::before {
    content: none;
  }
}
              
            
!

JS

              
                /* Face recognition from : https://nenadmarkus.com/p/picojs-intro/demo/ */

console.clear();
let distance = 0;
let isWatching = true;
let pos = {
  x: -1,
  y: -1
};
let prev_pos = {
  x: 0,
  y: 0
};
let distanceSinceWatching = 0;
const elDistance = document.querySelector('.distance .total');
const elStart = document.querySelector('.start');
const elHowTo = document.querySelector('.howto');
const elGame = document.querySelector('.game');
const elContainer = document.querySelector('.container');
const elTime = document.querySelector('.timer .time');
const elMovement = document.querySelector('.movement');
const elReplay1 = document.querySelector('.replay1');
const elReplay2 = document.querySelector('.replay2');
const elDead = document.querySelector('.dead');
const elWin = document.querySelector('.win');
const audioDoll = new Audio('https://assets.codepen.io/127738/squid-game-sound.mp3');
const audioDollDuration = 5.433469;
const shotGun = new Audio('https://assets.codepen.io/127738/shotgun.mp3');
shotGun.volume = 0.2;
const sigh = new Audio('https://assets.codepen.io/127738/sigh.mp3');

const MAX_TIME = 60;
const FINISH_DISTANCE = 100;
const IN_GAME_MAX_DISTANCE = 4000;
const MAX_MOVEMENT = 180;

let playing = false;

var mycamvas;

function init () {
  
  var initialized = false;
  if (initialized) return;
  /*
    (1) prepare the pico.js face detector
  */
  var update_memory = pico.instantiate_detection_memory(5); // we will use the detecions of the last 5 frames
  var facefinder_classify_region = function (r, c, s, pixels, ldim) {
    return -1.0;
  };
  var cascadeurl = "https://raw.githubusercontent.com/nenadmarkus/pico/c2e81f9d23cc11d1a612fd21e4f9de0921a5d0d9/rnt/cascades/facefinder";
  fetch(cascadeurl).then(function (response) {
    response.arrayBuffer().then(function (buffer) {
      var bytes = new Int8Array(buffer);
      facefinder_classify_region = pico.unpack_cascade(bytes);
      console.log("* cascade loaded");
    });
  });
  /*
    (2) get the drawing context on the canvas and define a function to transform an RGBA image to grayscale
  */
  var ctx = document.querySelector(".webcam").getContext("2d");
  ctx.lineWidth = 4;
  ctx.strokeStyle = "#d7327a";
  function rgba_to_grayscale(rgba, nrows, ncols) {
    var gray = new Uint8Array(nrows * ncols);
    for (var r = 0; r < nrows; ++r)
      for (var c = 0; c < ncols; ++c)
        // gray = 0.2*red + 0.7*green + 0.1*blue
        gray[r * ncols + c] =
          (2 * rgba[r * 4 * ncols + 4 * c + 0] +
            7 * rgba[r * 4 * ncols + 4 * c + 1] +
            1 * rgba[r * 4 * ncols + 4 * c + 2]) /
          10;
    return gray;
  }
  /*
    (3) this function is called each time a video frame becomes available
  */
  var processfn = function (video, dt) {
    // render the video frame to the canvas element and extract RGBA pixel data
    ctx.drawImage(video, 0, 0);
    var rgba = ctx.getImageData(0, 0, 640, 480).data;
    // prepare input to `run_cascade`
    image = {
      pixels: rgba_to_grayscale(rgba, 480, 640),
      nrows: 480,
      ncols: 640,
      ldim: 640
    };
    params = {
      shiftfactor: 0.1, // move the detection window by 10% of its size
      minsize: 100, // minimum size of a face
      maxsize: 1000, // maximum size of a face
      scalefactor: 1.1 // for multiscale processing: resize the detection window by 10% when moving to the higher scale
    };
    // run the cascade over the frame and cluster the obtained detections
    // dets is an array that contains (r, c, s, q) quadruplets
    // (representing row, column, scale and detection score)
    dets = pico.run_cascade(image, facefinder_classify_region, params);
    dets = update_memory(dets);
    dets = pico.cluster_detections(dets, 0.2); // set IoU threshold to 0.2
    // draw detections
    if (dets.length) {
      const det = dets[0];
      // check the detection score
      // if it's above the threshold, draw it
      // (the constant 50.0 is empirical: other cascades might require a different one)
      if (det[3] > 50.0) {
        prev_pos.x = pos.x;
        prev_pos.y = pos.y;
        pos.x = det[1];
        pos.y = det[0];
        let _dist = Math.hypot(pos.x - prev_pos.x, pos.y - prev_pos.y);
        // If not watching, increase the distance from the face position
        if (prev_pos.x === -1) {
          _dist = 0;
        }
        if (!isWatching) {
          distance += _dist;
          /* If user reached the end */
          if (distance > IN_GAME_MAX_DISTANCE) {
            reachedEnd();
          }
        } else {
          distanceSinceWatching += _dist;
          if (distanceSinceWatching > MAX_MOVEMENT) {
            dead();
          }
        }
        let formattedDistance = '' + Math.round((distance / IN_GAME_MAX_DISTANCE) * FINISH_DISTANCE);
        if (formattedDistance.length === 1) {
          formattedDistance = '00' + formattedDistance;
        } else  if (formattedDistance.length == 2) {
          formattedDistance = '0' + formattedDistance;
        }
        elDistance.innerHTML = formattedDistance;
        let movingDistance = Math.floor(distanceSinceWatching / MAX_MOVEMENT * 100);
        movingDistance = '' + Math.min(100, movingDistance);
        if (movingDistance.length === 1) {
          movingDistance = '0' + movingDistance;
        }
        elMovement.innerHTML = `${movingDistance}%`;
        
        
        ctx.beginPath();
        ctx.arc(det[1], det[0], det[2] / 2, 0, 2 * Math.PI, false);
        ctx.stroke();
      }
    }
    updateTimer(MAX_TIME - (Date.now() - mycamvas.startTime) / 1000);
    
    /* Check if reached timeout */
    if ((Date.now() - mycamvas.startTime) / 1000 > MAX_TIME) {
      timeOut();
    }
    
  };
  /*
    (4) instantiate camera handling (see https://github.com/cbrandolino/camvas)
  */
  mycamvas = new camvas(ctx, processfn, () => {
    playing = true;
    updateWatching();
  });
  /*
    (5) it seems that everything went well
  */
  initialized = true;
}

elStart.addEventListener('click', () => {
  init();

  elContainer.classList.add('is-playing');
  elHowTo.classList.remove('is-visible');
});

function reachedEnd () {
  watchingTween.kill();
  sigh.currentTime = 0;
  sigh.play();
  audioDoll.pause();
  mycamvas.stop();
  playing = false;
  elWin.classList.add('is-visible');
  elContainer.classList.remove('is-playing');
}

function timeOut () {
  watchingTween.kill();
  audioDoll.pause();
  shotGun.currentTime = 0;
  shotGun.play();
  mycamvas.stop();
  playing = false;
  elDead.classList.add('is-visible');
  elContainer.classList.remove('is-playing');
}

function dead () {
  watchingTween.kill();
  audioDoll.pause();
  shotGun.currentTime = 0;
  shotGun.play();
  mycamvas.stop();
  playing = false;
  elDead.classList.add('is-visible');
  elContainer.classList.remove('is-playing');
}

let watchingTween = null;
function updateWatching () {
  if (!playing)  return;
  
  isWatching = !isWatching;
  
  let duration = Math.random() * 3.5 + 2.5;
  if (isWatching) {
    duration  = Math.random() * 2 + 2;
  }
  if (!isWatching) {
    audioDoll.currentTime = 0;
    audioDoll.playbackRate = (audioDollDuration - 0.5) / duration;
    audioDoll.play();
  }
  gsap.to(head.rotation, {
    y: isWatching ? 0 : -Math.PI, 
    duration: 0.4
  });
  watchingTween = gsap.to({}, {
    duration: duration,
    onComplete: updateWatching
  });
  
  if (!isWatching) {
    distanceSinceWatching = 0;
  }
}

function updateTimer (timeLeft) {
  let min = '' + Math.floor(timeLeft / 60);
  if (min.length === 1) {
    min = '0' + min;
  }
  let sec = '' + Math.floor(timeLeft % 60);
  if (sec.length === 1) {
    sec = '0' + sec;
  }
  if (timeLeft < 0) {
    min = '00';
    sec = '00';
  }
  elTime.innerHTML = `${min}:${sec}`;
}

/* THREEJS PART */
const scene = new THREE.Scene();
let sceneWidth = 0;
if (window.innerWidth / window.innerHeight > 1.9) {
  sceneWidth = window.innerWidth * 0.6;
} else {
  sceneWidth = window.innerWidth * 0.95;
}
let sceneHeight = sceneWidth / 2;
const camera = new THREE.PerspectiveCamera(75, sceneWidth / sceneHeight, 0.1, 1000);

const renderer = new THREE.WebGLRenderer({
  alpha: true,
  antialias: true
});
renderer.setSize(sceneWidth, sceneHeight);
elGame.appendChild(renderer.domElement);

camera.position.y = 2.8;
camera.position.z = 11;
// camera.lookAt(new THREE.Vector3(0, -2, 0));

const light = new THREE.HemisphereLight(0xffffbb, 0x080820, 1);
scene.add(light);
const light2 = new THREE.AmbientLight(0x404040, 1.2);
scene.add(light2);
const spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(100, 1000, 100);
scene.add( spotLight );

let head = new THREE.Group();
scene.add(head);
const loader = new THREE.GLTFLoader();
loader.load(
	'https://assets.codepen.io/127738/Squid_game_doll.gltf',
	function (gltf) {
		scene.add(gltf.scene);
    head.add(gltf.scene.children[0].children[0].children[0].children[1]);
    head.add(gltf.scene.children[0].children[0].children[0].children[1]);
    head.add(gltf.scene.children[0].children[0].children[0].children[2]);
    head.children[0].position.y = -8;
    head.children[1].position.y = -8;
    head.children[2].position.y = -8;
    head.children[0].position.z = 1;
    head.children[1].position.z = 1;
    head.children[2].position.z = 1;
    head.children[0].scale.setScalar(1);
    head.children[1].scale.setScalar(1);
    head.children[2].scale.setScalar(1);
    head.position.y = 8;
    head.position.z = -1;
    
    elStart.classList.add('is-ready');
	},
	function (xhr) {console.log((xhr.loaded / xhr.total * 100) + '% loaded');},
	function (error) {console.log( 'An error happened', error);}
);

renderer.setAnimationLoop(renderWebGL);

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


/* On Resize */
function onWindowResize(){
  if (window.innerWidth / window.innerHeight > 1.9) {
    sceneWidth = window.innerWidth * 0.6;
  } else {
    sceneWidth = window.innerWidth * 0.95;
  }
  sceneHeight = sceneWidth / 2;
  camera.aspect = sceneWidth / sceneHeight;
  camera.updateProjectionMatrix();

  renderer.setSize(sceneWidth, sceneHeight);
}
window.addEventListener('resize', onWindowResize, false);
onWindowResize();

function replay () {
  elContainer.classList.add('is-playing');
  elDead.classList.remove('is-visible');
  elWin.classList.remove('is-visible');
  distance = 0;
  isWatching = true;
  distanceSinceWatching = 0;
  playing = true;
  updateWatching();
  mycamvas.startTime = Date.now();
  mycamvas.play();
}
elReplay1.addEventListener('click', () => {
  replay();
});
elReplay2.addEventListener('click', () => {
  replay();
});
              
            
!
999px

Console