<script src="https://threejs.org/build/three.min.js"></script>
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tween.js/18.6.4/tween.umd.js"></script>
html, body {
  height: 100%;
  margin: 0;
  overflow: hidden;
}
canvas {
  width: 100%;
  height: 100%;
  display: block;
}

.info{
  position: absolute;
  top: 2%;
  display: inline-block;
  left: 0;
  right: 0;
  text-align: center;
  color: #ff8844;
  font-size: 2rem;
  text-shadow: 0 0 3px #ffcc88;
}
// This example uses THREE.CatmullRomCurve3 to create a path for a custom Keyframe Track position animation.
// AnimationClip creation function on line 136
// Uncomment line 173 to see the curve helper
console.clear();
// Global Variables
var canvas, scene, renderer, camera;
var controls, raycaster, mouse, txtLoader, clock, delta = 0;
// track mouse down coord
// so that in click handler,
// we can ignore flipping the card
// if the user was dragging the camera
var mouseDownCoord = {x:0,y:0}
var mouseClickCoord = {x:0,y:0}
var ground;
window.cards = [];
var cards = window.cards;
var player_one_matches = [];
var player_one_cards = [];

var flipped = [];
var reset_timer = null;
var ignore_clicks = false;

const colorDark = new THREE.Color( 0xb0b0b0 );
const colorLight = new THREE.Color( 0xffffff );
const animationDuration = 0.5; // seconds
const reset_delay = 1000;

init();

function init(){
  
  scene = new THREE.Scene();
  renderer = new THREE.WebGLRenderer({
    antialias: true
  });
  renderer.shadowMap.enabled = true;
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
  
  canvas = renderer.domElement;
  document.body.appendChild(canvas);
  
  camera = new THREE.PerspectiveCamera(60, 1, 0.1, 1000);
  camera.position.set( 0, 10, -1.5 );
  scene.add(camera);
  controls = new THREE.OrbitControls(camera, canvas);
  
  window.addEventListener( 'mousemove', onMouseMove, false );
  window.addEventListener( 'click', onMouseClick, false );
  window.addEventListener( 'mousedown', onMouseDown, false);
  window.addEventListener( 'touchstart', onTouchStart, false);
  window.addEventListener( 'touchend', onTouchEnd, false)
  raycaster = new THREE.Raycaster();
  mouse = new THREE.Vector2();
  txtLoader = new THREE.TextureLoader();
  clock = new THREE.Clock();
  
  // Lights
  initLights()

  // Card
  for(let i = 0; i<4; i++){
    for(let j = 0; j<4; j++){
      initCard(i,j);
    }  
  }
  
  
  // Ground
  initGround();

  render();
}

function render(){
  
  if( resize( renderer ) ) {
    camera.aspect = canvas.clientWidth / canvas.clientHeight;
    camera.updateProjectionMatrix();
  }
  
  delta = clock.getDelta();
  
  // for(let i = 0; i < cards.length; i ++){
  //   cards[i].mixer.update( delta );  
  // }
  TWEEN.update();
  
  
  renderer.render( scene, camera );
  requestAnimationFrame( render );
}

function initLights(){
  var ambientLight = new THREE.AmbientLight( 0xffffff, 0.4 )
  var dirLight = new THREE.DirectionalLight( 0xcceeff, 0.9 );
  dirLight.castShadow = true;
  dirLight.shadow.mapSize.width = 1024;
  dirLight.shadow.mapSize.height = 1024;
  dirLight.position.setScalar( 5 );
  scene.add( dirLight , ambientLight );
}

function initCard(irow,icol){
  
  // The Card
  var faceUpTexture = txtLoader.load('https://images-na.ssl-images-amazon.com/images/I/61YXNhfzlzL._SL1012_.jpg');
  var faceDownTexture = txtLoader.load('https://vignette3.wikia.nocookie.net/yugioh/images/9/94/Back-Anime-2.png/revision/latest?cb=20110624090942');
  // faceUpTexture.flipY = false;
  var darkMaterial = new THREE.MeshPhongMaterial({ color: 0x111111 });
  var faceUpMaterial = new THREE.MeshPhongMaterial({ 
    color: colorDark, 
    map: faceUpTexture,
    shininess: 40
  });
  var faceDownMaterial = new THREE.MeshPhongMaterial({ 
    color: colorDark,
    map: faceDownTexture,
    shininess: 40
  });
  var card = new THREE.Mesh( 
    new THREE.BoxBufferGeometry( 2 , 0.04 , 2 ),
    [
      darkMaterial, // left
      darkMaterial, // right
      faceDownMaterial, // facedown
      faceUpMaterial, // faceup
      darkMaterial, // 
      darkMaterial, // 
    ]
  );
  
  card.scale.x = 0.65;
  let offset = {x:3,y:3}
  card.position.set(
    (2*irow)-offset.x, 
    0, 
    (2*icol)-offset.y+(icol*0.5)
  )
  card.castShadow = true;
  card.receiveShadow = true;

  // Animation 
  card.faceUp = false;
  // card.mixer = new THREE.AnimationMixer( card );
  // var flipUpsideClip = createFlipUpsideClip(card,'faceup');
  // var flipDownsideClip = createFlipUpsideClip(card,'facedown');
  
  card.actions = {
    // flipUpside: card.mixer.clipAction( flipUpsideClip ),
    // flipDownside: card.mixer.clipAction( flipDownsideClip )
    
    flipUpside: getFlipTween(card,'faceup'),
    flipDownside: getFlipTween(card,'facedown')
  };
  // card.actions.flipUpside.loop = THREE.LoopOnce;
  // card.actions.flipDownside.loop = THREE.LoopOnce;
  // card.actions.flipUpside.clampWhenFinished = true;
  // card.actions.flipDownside.clampWhenFinished = true;
  
  cards.push(card);
  scene.add( card );
}

function getFlipTween(card, direction){
  const initPos = card.position;
  var zAxis = new THREE.Vector3( 0, 0, 1 );
  var qInitial = new THREE.Quaternion().setFromAxisAngle( zAxis, direction === 'facedown' ? Math.PI : 0 );
  var qFinal = new THREE.Quaternion().setFromAxisAngle( zAxis, direction === 'faceup' ? Math.PI : 0 );
  let pos = {
    x: initPos.x,
    y: initPos.y,
    z: initPos.z,
    
    rx: qInitial.x,
    ry: qInitial.y,
    rz: qInitial.z,
    rw: qInitial.w
  };
  function posUpdate(){
    card.position.set(pos.x,pos.y,pos.z)
    card.quaternion.set(pos.rx,pos.ry,pos.rz,pos.rw)
  }
  const flipTweenStart = new TWEEN.Tween(pos)
  .to({
    x: initPos.x, 
    y: initPos.y + 1, 
    z: initPos.z,
    
    rx: qFinal.x,
    ry: qFinal.y,
    rz: qFinal.z,
    rw: qFinal.w
  }, (animationDuration * 1000) / 2)
  .easing(TWEEN.Easing.Quadratic.Out) 
  // Use an easing function to make the animation smooth.
  .onUpdate(posUpdate)
  //.start() // Start the tween immediately.
  
  const flipTweenKF2 = new TWEEN.Tween(pos)
    .to({
      x: initPos.x,
      y: initPos.y,
      z: initPos.z,
      
      rx: qFinal.x,
      ry: qFinal.y,
      rz: qFinal.z,
      rw: qFinal.w
    }, (animationDuration * 1000) / 2)
  .easing(TWEEN.Easing.Quadratic.In)
  .onUpdate(posUpdate)
  
  return flipTweenStart.chain(flipTweenKF2)
}

function getHandUpdateTween(card,updateTo){
  // const initPos = {
  //   x: ,
  //   y: card.position.y,
  //   z: card.position.z
  // };
  // const initScale = {
  //   x: card.scale.x,
  //   y: card.scale.y,
  //   z: card.scale.z,
  // }
  
  let tweenProps = {
    pos_x: card.position.x,
    pos_y: card.position.y,
    pos_z: card.position.z,
    
    rot_x: card.rotation.x,
    rot_y: card.rotation.y,
    rot_z: card.rotation.z,
    rot_w: card.rotation.w,
    
    scale_x: card.scale.x,
    scale_y: card.scale.y,
    scale_z: card.scale.z,
    
  };//
  //
  function propsUpdate(){
    card.position.set(
      tweenProps.pos_x,
      tweenProps.pos_y,
      tweenProps.pos_z
    )
    card.rotation.x = tweenProps.rot_x;
    card.rotation.y = tweenProps.rot_y;
    card.rotation.z = tweenProps.rot_z;
    
    card.scale.set(
      tweenProps.scale_x,
      tweenProps.scale_y,
      tweenProps.scale_z,
    )
  }
  const tween = new TWEEN.Tween(tweenProps)
  .to(updateTo, 500)
  .easing(TWEEN.Easing.Quadratic.Out) 
  // Use an easing function to make the animation smooth.
  .onUpdate(propsUpdate)
  
  return tween;
}

function initGround(){
  
  ground = new THREE.Mesh(
    new THREE.PlaneBufferGeometry(20, 20), 
    new THREE.MeshStandardMaterial({
      //map: txtLoader.load( "https://threejs.org/examples/textures/hardwood2_diffuse.jpg" ), 
      metalness: 0, 
      roughness: 1,
      color: '#000000'
    })
  );
  ground.geometry.rotateX(-Math.PI * 0.5);
  ground.position.set(0, -0.001, 0);
  ground.receiveShadow = true;
  scene.add(ground);
}

// function createFlipUpsideClip( card, side ){ // 'faceup' or 'facedown'
//   // Create a keyframe track (i.e. a timed sequence of keyframes) for each animated property
//   // Note: the keyframe track type should correspond to the type of the property being animated
//   // Rotation
//   var zAxis = new THREE.Vector3( 0, 0, 1 );
  
//   if( side === 'faceup' ){
//     var qInitial = new THREE.Quaternion().setFromAxisAngle( zAxis, 0 );
//     var qFinal = new THREE.Quaternion().setFromAxisAngle( zAxis, Math.PI );
//   } else if( side === 'facedown' ){
//     var qInitial = new THREE.Quaternion().setFromAxisAngle( zAxis, Math.PI );
//     var qFinal = new THREE.Quaternion().setFromAxisAngle( zAxis, 0 );
//   }
  
//   var quaternionKF = new THREE.QuaternionKeyframeTrack( 
//     '.quaternion', 
//     [ 0, animationDuration ], 
//     [ 
//       qInitial.x, 
//       qInitial.y, 
//       qInitial.z, 
//       qInitial.w, 
//       qFinal.x, 
//       qFinal.y, 
//       qFinal.z, 
//       qFinal.w 
//     ]
//   );
  
//   function pointFromInitialOffset(x,y,z){
//     return new THREE.Vector3(
//       card.position.x + x,
//       card.position.y + y,
//       card.position.z + z
//     )
//   }

//   // Position
//   var pointsArr = [ 
//     pointFromInitialOffset( 0, 0, 0 ),
//     pointFromInitialOffset( 0, 0.8, 0 ), 
//     pointFromInitialOffset( 0, 1.5, 0 ), 
//     pointFromInitialOffset( 0, 1.2, 0 ), 
//     pointFromInitialOffset( 0, 0, 0 )
//   ];
//   if( side === 'facedown' ){
//     pointsArr.forEach( function( vec3 , i ){
//       //vec3.x = -vec3.x;
//     });
//   }
//   var CRC = new THREE.CatmullRomCurve3( pointsArr, false, 'catmullrom', 1 );
//   var CRCPoints = CRC.getPoints( 60 );

//   //var helper = pointsHelper( CRCPoints );
//   //scene.add( helper ); // Optional helper for position curve

//   var posArrFlat = [];
//   for( var i = 0; i < CRCPoints.length; i++ ){
//     posArrFlat.push( CRCPoints[i].x, CRCPoints[i].y, CRCPoints[i].z );
//   }

//   var timesArr = [];
//   var len = posArrFlat.length - 3;
//   for( var j = 0; j < posArrFlat.length/3; j++ ){
//     var x = ((animationDuration / len) * j * 3) + 0; // + delay
//     timesArr.push( x );
//   }

//   var positionKF = new THREE.VectorKeyframeTrack( 
//     '.position', 
//     timesArr, 
//     posArrFlat,
//     THREE.InterpolateSmooth
//   );
  
//   var flipUpsideClip = new THREE.AnimationClip( 
//     'Flip' , 
//     animationDuration , 
//     [ positionKF, quaternionKF ] 
//   );
  
//   return flipUpsideClip;
// }

function onMouseMove( evt ){
  for(let i = 0; i<cards.length; i++){
    let card = cards[i];
    if( raycast( card ) == true ){
      card.material[2].color.set( colorLight );
      card.material[3].color.set( colorLight );
    } else {
      card.material[2].color.set( colorDark );
      card.material[3].color.set( colorDark );
    }
  }
}

function onMouseDown (evt){
  mouseDownCoord = {
    x: evt.clientX,
    y: evt.clientY
  }
}

function onTouchStart (evt){
  const touches = evt.changedTouches;
//   for (let i = 0; i < touches.length; i++) {
//     console.log(`touchstart: ${i}.`,touches[i]);
//     //ongoingTouches.push(copyTouch(touches[i]));
    
//   }
  if(touches.length){
    //alert(touches[0].clientX);
    onMouseDown({
    
      clientX: touches[0].clientX,
      clientY: touches[0].clientY

    })  
  }
  
}
function onTouchEnd(evt){
  const touches = evt.changedTouches;
//   for (let i = 0; i < touches.length; i++) {
//     console.log(`touchend: ${i}.`,touches[i]);
//     //ongoingTouches.push(copyTouch(touches[i]));
    
//   }
  if(touches?.[0]){
    onMouseClick({
      clientX: touches[0].clientX,
      clientY: touches[0].clientY
    })
  }
}

function onMouseClick( evt ){
  if(ignore_clicks){
    return;
  }
  mouseClickCoord = {
      x: evt.clientX,
      y: evt.clientY
    }
  let drag_distance = Math.hypot(
      mouseClickCoord.x - mouseDownCoord.x,
      mouseClickCoord.y - mouseDownCoord.y
    )
  console.log({
    mouseDownCoord,
    mouseClickCoord,
    drag_distance
  })
  // ignore clicks if you dragged the mouse
  if(drag_distance > 10){
    return;
  }
  for(let i = 0; i<cards.length; i++){
    let _card = cards[i];
    
    if( raycast( _card ) == true ){
      if(
        // ignore if we already flipped this card over
        flipped.indexOf(i)>-1
        // or if it's in the player hand
        || player_one_cards.indexOf(i)>-1
       ){
        return;
      }
      if( _card.faceUp ){ // card faceup
        // so turn it facedown
        //_card.actions.flipUpside.stop();
        _card.actions.flipDownside.start();
        _card.faceUp = false;

      } else if( !_card.faceUp ) { // card facedown
        // so turn it faceup
        //_card.actions.flipDownside.stop();
        _card.actions.flipUpside.start();
        _card.faceUp = true;
        flipped.push(i);
      }
    }
  }
  if(flipped.length > 1){
    // we've flipped 2+ cards, set a timer, and then flip them back
    ignore_clicks = true;
    // temp: 50/50
    let match = true; //Math.random() >= 0.5; 
    // TODO: flippedCardsMatch()
    if(match){
      // move cards to players hand
      setTimeout(moveFlippedToPlayersHand,animationDuration*1000)
      //moveFlippedToPlayersHand();
    }else{
      // reset cards
      resetCards();
    }
  }
  
}

function lerp(v0,v1,t){
  return v0*(1-t)+v1*t
}

function addMatchToHand(i_card_a,i_card_b){
  camera.attach(cards[i_card_a])
  camera.attach(cards[i_card_b])
  
//   cards[i_card_a].position.set(0,-.5,-1)
//   cards[i_card_a].scale.set(.1,.1,.1)
//   cards[i_card_a].rotation.set(1,Math.PI,Math.PI,'XYZ')
  
//   cards[i_card_b].scale.set(.1,.1,.1)
//   cards[i_card_b].position.set(-.1,-.5,-1)
//   cards[i_card_b].rotation.set(1,Math.PI,Math.PI,'XYZ')
  let matches_count = player_one_matches.length;
  for(let a = 1; a<=player_one_cards.length; a++){
    let i_card = player_one_cards[a-1];
    let card = cards[i_card];
    let even = a % 2 == 0;
    let lerp_max = .07 * matches_count
    
    let updateTo = {
    }
    updateTo.pos_x = lerp(
      0, // 0 basis
      lerp_max, // lerp max width
      (1/matches_count)*(even?a+1:a+2)) // % of lerp
      -(even?.1:.105) // slight offset for "paired" card
      -(a*.01) // padding between cards
      -(lerp_max) // center
      +(.05)
    updateTo.pos_y = -0.5 + (0.001 * a);
    updateTo.pos_z = -1.0 + (0.001 * a);
    
    updateTo.rot_x = 0.5;//1;
    updateTo.rot_y = Math.PI;
    updateTo.rot_z = Math.PI;
    
    updateTo.scale_x = .09 * .65
    updateTo.scale_y = .09
    updateTo.scale_z = .09 //
    console.log(updateTo);
    getHandUpdateTween(card,updateTo).start()
  }
}

function moveFlippedToPlayersHand(){
  player_one_matches.push(flipped)
  player_one_cards.push(flipped[0],flipped[1])
  
  addMatchToHand(flipped[0],flipped[1])
  flipped = [];
  console.warn('moving flipped cards to players hand',player_one_matches,player_one_cards)
  ignore_clicks = false;
  
}

function resetCards(){
  reset_timer = setTimeout(()=>{
       for(let a = 0; a<flipped.length; a++){
         let fci = flipped[a];
         let fc = cards[fci];
         // fc.actions.flipUpside.stop();
         fc.actions.flipDownside.start();
         fc.faceUp = false;
       }
       flipped = [];
       ignore_clicks = false;
    },reset_delay);
}

function raycast( object ){
  // calculate mouse position in normalized device coordinates
	// (-1 to +1) for both components
	mouse.x = ( mouseClickCoord.x / window.innerWidth ) * 2 - 1;
	mouse.y = - ( mouseClickCoord.y / window.innerHeight ) * 2 + 1;
  
  // update the picking ray with the camera and mouse position
	raycaster.setFromCamera( mouse, camera );

	// calculate objects intersecting the picking ray
	var intersects = raycaster.intersectObject( object );
  if( intersects.length > 0 ){
    return true;
  } else {
    return false;
  }
}

function pointsHelper( pointsArray ){
  var geometry = new THREE.BufferGeometry().setFromPoints( pointsArray );
  var material = new THREE.LineBasicMaterial( { color : 0xff0000 } );
  var curveObject = new THREE.Line( geometry, material );
  return curveObject;
}

function resize(renderer) {
  const canvas = renderer.domElement;
  const width = canvas.clientWidth;
  const height = canvas.clientHeight;
  const needResize = canvas.width !== width || canvas.height !== height;
  if (needResize) {
    renderer.setSize(width, height, false);
  }
  return needResize;
}
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.