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