这款HTML5 3D魔方小游戏是一款令人着迷的拼图游戏,充满了挑战和乐趣。通过创新性的HTML、Canvas和JavaScript技术,这款游戏提供了一个独特的游戏体验,玩家可以在其中探索3D魔方的世界。
游戏特点:
3D魔方体验:游戏中的魔方呈现为令人印象深刻的3D图形,使玩家感受到真实的拼图乐趣。
多种首选项:玩家可以根据自己的喜好选择翻转类型、混淆程度、相机角度和颜色方案,以获得个性化的游戏体验。
挑战计时:游戏具有计时功能,玩家可以尽量快地解决魔方并竞争获得最佳时间。
统计数据:玩家可以随时查看总解决次数、最佳时间、最差时间以及5次、12次和25次的平均时间,以了解自己的进步。
PWA支持:游戏支持渐进式Web应用(PWA),可以将游戏添加到主屏幕,以便在离线状态下玩。
如何玩:
双击开始游戏。
使用鼠标或触摸屏来旋转3D魔方,以还原其原始状态。
尽量快地还原魔方,以争取获得最佳时间和提高解决次数统计。
点击按钮以查看游戏统计数据、更改首选项或返回到上一个界面。
这款HTML5 3D魔方小游戏将挑战您的空间感知和解决问题的能力,同时提供了美丽的3D图形,以及许多自定义选项和统计数据来保持您的兴趣。立即尝试,看看您是否能在最短时间内还原魔方,成为魔方之王!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>HTML5 3D酷炫拼魔方游戏特效 </title>
<meta name="viewport" content="width=device-width,height=device-height,user-scalable=no,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0">
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="ui">
<div class="ui__background"></div>
<div class="ui__game"></div>
<div class="ui__texts">
<h1 class="text text--title">
<span>THE</span>
<span>CUBE</span>
</h1>
<div class="text text--note">
双击即可开始
</div>
<div class="text text--timer">
0:00
</div>
<div class="text text--complete">
<span>Complete!</span>
</div>
<div class="text text--best-time">
<icon trophy></icon>
<span>Best Time!</span>
</div>
</div>
<div class="ui__prefs">
<range name="flip" title="Flip Type" list="Swift ,Smooth,Bounce"></range>
<range name="scramble" title="Scramble Length" list="20,25,30"></range>
<range name="fov" title="Camera Angle" list="Ortographic,Perspective"></range>
<range name="theme" title="Color Scheme" list="Cube,Erno,Dust,Camo,Rain"></range>
</div>
<div class="ui__stats">
<div class="stats" name="total-solves">
<i>Total solves:</i><b>-</b>
</div>
<div class="stats" name="best-time">
<i>Best time:</i><b>-</b>
</div>
<div class="stats" name="worst-time">
<i>Worst time:</i><b>-</b>
</div>
<div class="stats" name="average-5">
<i>Average of 5:</i><b>-</b>
</div>
<div class="stats" name="average-12">
<i>Average of 12:</i><b>-</b>
</div>
<div class="stats" name="average-25">
<i>Average of 25:</i><b>-</b>
</div>
</div>
<div class="ui__buttons">
<button class="btn btn--bl btn--stats">
<icon trophy></icon>
</button>
<button class="btn btn--bl btn--prefs">
<icon settings></icon>
</button>
<button class="btn btn--bl btn--back">
<icon back></icon>
</button>
<button class="btn btn--br btn--pwa"></button>
</div>
</div>
<script src='js/three.min.js'></script>
<script src="js/index.js"></script>
</body>
</html>
const animationEngine = ( () => {
let uniqueID = 0;
class AnimationEngine {
constructor() {
this.ids = [];
this.animations = {};
this.update = this.update.bind( this );
this.raf = 0;
this.time = 0;
}
update() {
const now = performance.now();
const delta = now - this.time;
this.time = now;
let i = this.ids.length;
this.raf = i ? requestAnimationFrame( this.update ) : 0;
while ( i-- )
this.animations[ this.ids[ i ] ] && this.animations[ this.ids[ i ] ].update( delta );
}
add( animation ) {
animation.id = uniqueID ++;
this.ids.push( animation.id );
this.animations[ animation.id ] = animation;
if ( this.raf !== 0 ) return;
this.time = performance.now();
this.raf = requestAnimationFrame( this.update );
}
remove( animation ) {
const index = this.ids.indexOf( animation.id );
if ( index < 0 ) return;
this.ids.splice( index, 1 );
delete this.animations[ animation.id ];
animation = null;
}
}
return new AnimationEngine();
} )();
class Animation {
constructor( start ) {
if ( start === true ) this.start();
}
start() {
animationEngine.add( this );
}
stop() {
animationEngine.remove( this );
}
update( delta ) {}
}
class World extends Animation {
constructor( game ) {
super( true );
this.game = game;
this.container = this.game.dom.game;
this.scene = new THREE.Scene();
this.renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true } );
this.renderer.setPixelRatio( window.devicePixelRatio );
this.container.appendChild( this.renderer.domElement );
this.camera = new THREE.PerspectiveCamera( 2, 1, 0.1, 10000 );
this.stage = { width: 2, height: 3 };
this.fov = 10;
this.createLights();
this.onResize = [];
this.resize();
window.addEventListener( 'resize', () => this.resize(), false );
}
update() {
this.renderer.render( this.scene, this.camera );
}
resize() {
this.width = this.container.offsetWidth;
this.height = this.container.offsetHeight;
this.renderer.setSize( this.width, this.height );
this.camera.fov = this.fov;
this.camera.aspect = this.width / this.height;
const aspect = this.stage.width / this.stage.height;
const fovRad = this.fov * THREE.Math.DEG2RAD;
let distance = ( aspect < this.camera.aspect )
? ( this.stage.height / 2 ) / Math.tan( fovRad / 2 )
: ( this.stage.width / this.camera.aspect ) / ( 2 * Math.tan( fovRad / 2 ) );
distance *= 0.5;
this.camera.position.set( distance, distance, distance);
this.camera.lookAt( this.scene.position );
this.camera.updateProjectionMatrix();
const docFontSize = ( aspect < this.camera.aspect )
? ( this.height / 100 ) * aspect
: this.width / 100;
document.documentElement.style.fontSize = docFontSize + 'px';
if ( this.onResize ) this.onResize.forEach( cb => cb() );
}
createLights() {
this.lights = {
holder: new THREE.Object3D,
ambient: new THREE.AmbientLight( 0xffffff, 0.69 ),
front: new THREE.DirectionalLight( 0xffffff, 0.36 ),
back: new THREE.DirectionalLight( 0xffffff, 0.19 ),
};
this.lights.front.position.set( 1.5, 5, 3 );
this.lights.back.position.set( -1.5, -5, -3 );
this.lights.holder.add( this.lights.ambient );
this.lights.holder.add( this.lights.front );
this.lights.holder.add( this.lights.back );
this.scene.add( this.lights.holder );
}
enableShadows() {
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
this.lights.front.castShadow = true;
this.lights.front.shadow.mapSize.width = 512;
this.lights.front.shadow.mapSize.height = 512;
var d = 1.5;
this.lights.front.shadow.camera.left = -d;
this.lights.front.shadow.camera.right = d;
this.lights.front.shadow.camera.top = d;
this.lights.front.shadow.camera.bottom = -d;
this.lights.front.shadow.camera.near = 1;
this.lights.front.shadow.camera.far = 9;
this.game.cube.holder.traverse( node => {
if ( node instanceof THREE.Mesh ) {
node.castShadow = true;
node.receiveShadow = true;
}
} );
}
}
function RoundedBoxGeometry( size, radius, radiusSegments ) {
THREE.BufferGeometry.call( this );
this.type = 'RoundedBoxGeometry';
radiusSegments = ! isNaN( radiusSegments ) ? Math.max( 1, Math.floor( radiusSegments ) ) : 1;
var width, height, depth;
width = height = depth = size;
radius = size * radius;
radius = Math.min( radius, Math.min( width, Math.min( height, Math.min( depth ) ) ) / 2 );
var edgeHalfWidth = width / 2 - radius;
var edgeHalfHeight = height / 2 - radius;
var edgeHalfDepth = depth / 2 - radius;
this.parameters = {
width: width,
height: height,
depth: depth,
radius: radius,
radiusSegments: radiusSegments
};
var rs1 = radiusSegments + 1;
var totalVertexCount = ( rs1 * radiusSegments + 1 ) << 3;
var positions = new THREE.BufferAttribute( new Float32Array( totalVertexCount * 3 ), 3 );
var normals = new THREE.BufferAttribute( new Float32Array( totalVertexCount * 3 ), 3 );
var
cornerVerts = [],
cornerNormals = [],
normal = new THREE.Vector3(),
vertex = new THREE.Vector3(),
vertexPool = [],
normalPool = [],
indices = []
;
var
lastVertex = rs1 * radiusSegments,
cornerVertNumber = rs1 * radiusSegments + 1
;
doVertices();
doFaces();
doCorners();
doHeightEdges();
doWidthEdges();
doDepthEdges();
function doVertices() {
var cornerLayout = [
new THREE.Vector3( 1, 1, 1 ),
new THREE.Vector3( 1, 1, - 1 ),
new THREE.Vector3( - 1, 1, - 1 ),
new THREE.Vector3( - 1, 1, 1 ),
new THREE.Vector3( 1, - 1, 1 ),
new THREE.Vector3( 1, - 1, - 1 ),
new THREE.Vector3( - 1, - 1, - 1 ),
new THREE.Vector3( - 1, - 1, 1 )
];
for ( var j = 0; j < 8; j ++ ) {
cornerVerts.push( [] );
cornerNormals.push( [] );
}
var PIhalf = Math.PI / 2;
var cornerOffset = new THREE.Vector3( edgeHalfWidth, edgeHalfHeight, edgeHalfDepth );
for ( var y = 0; y <= radiusSegments; y ++ ) {
var v = y / radiusSegments;
var va = v * PIhalf;
var cosVa = Math.cos( va );
var sinVa = Math.sin( va );
if ( y == radiusSegments ) {
vertex.set( 0, 1, 0 );
var vert = vertex.clone().multiplyScalar( radius ).add( cornerOffset );
cornerVerts[ 0 ].push( vert );
vertexPool.push( vert );
var norm = vertex.clone();
cornerNormals[ 0 ].push( norm );
normalPool.push( norm );
continue;
}
for ( var x = 0; x <= radiusSegments; x ++ ) {
var u = x / radiusSegments;
var ha = u * PIhalf;
vertex.x = cosVa * Math.cos( ha );
vertex.y = sinVa;
vertex.z = cosVa * Math.sin( ha );
var vert = vertex.clone().multiplyScalar( radius ).add( cornerOffset );
cornerVerts[ 0 ].push( vert );
vertexPool.push( vert );
var norm = vertex.clone().normalize();
cornerNormals[ 0 ].push( norm );
normalPool.push( norm );
}
}
for ( var i = 1; i < 8; i ++ ) {
for ( var j = 0; j < cornerVerts[ 0 ].length; j ++ ) {
var vert = cornerVerts[ 0 ][ j ].clone().multiply( cornerLayout[ i ] );
cornerVerts[ i ].push( vert );
vertexPool.push( vert );
var norm = cornerNormals[ 0 ][ j ].clone().multiply( cornerLayout[ i ] );
cornerNormals[ i ].push( norm );
normalPool.push( norm );
}
}
}
function doCorners() {
var flips = [
true,
false,
true,
false,
false,
true,
false,
true
];
var lastRowOffset = rs1 * ( radiusSegments - 1 );
for ( var i = 0; i < 8; i ++ ) {
var cornerOffset = cornerVertNumber * i;
for ( var v = 0; v < radiusSegments - 1; v ++ ) {
var r1 = v * rs1;
var r2 = ( v + 1 ) * rs1;
for ( var u = 0; u < radiusSegments; u ++ ) {
var u1 = u + 1;
var a = cornerOffset + r1 + u;
var b = cornerOffset + r1 + u1;
var c = cornerOffset + r2 + u;
var d = cornerOffset + r2 + u1;
if ( ! flips[ i ] ) {
indices.push( a );
indices.push( b );
indices.push( c );
indices.push( b );
indices.push( d );
indices.push( c );
} else {
indices.push( a );
indices.push( c );
indices.push( b );
indices.push( b );
indices.push( c );
indices.push( d );
}
}
}
for ( var u = 0; u < radiusSegments; u ++ ) {
var a = cornerOffset + lastRowOffset + u;
var b = cornerOffset + lastRowOffset + u + 1;
var c = cornerOffset + lastVertex;
if ( ! flips[ i ] ) {
indices.push( a );
indices.push( b );
indices.push( c );
} else {
indices.push( a );
indices.push( c );
indices.push( b );
}
}
}
}
function doFaces() {
var a = lastVertex;
var b = lastVertex + cornerVertNumber;
var c = lastVertex + cornerVertNumber * 2;
var d = lastVertex + cornerVertNumber * 3;
indices.push( a );
indices.push( b );
indices.push( c );
indices.push( a );
indices.push( c );
indices.push( d );
a = lastVertex + cornerVertNumber * 4;
b = lastVertex + cornerVertNumber * 5;
c = lastVertex + cornerVertNumber * 6;
d = lastVertex + cornerVertNumber * 7;
indices.push( a );
indices.push( c );
indices.push( b );
indices.push( a );
indices.push( d );
indices.push( c );
a = 0;
b = cornerVertNumber;
c = cornerVertNumber * 4;
d = cornerVertNumber * 5;
indices.push( a );
indices.push( c );
indices.push( b );
indices.push( b );
indices.push( c );
indices.push( d );
a = cornerVertNumber * 2;
b = cornerVertNumber * 3;
c = cornerVertNumber * 6;
d = cornerVertNumber * 7;
indices.push( a );
indices.push( c );
indices.push( b );
indices.push( b );
indices.push( c );
indices.push( d );
a = radiusSegments;
b = radiusSegments + cornerVertNumber * 3;
c = radiusSegments + cornerVertNumber * 4;
d = radiusSegments + cornerVertNumber * 7;
indices.push( a );
indices.push( b );
indices.push( c );
indices.push( b );
indices.push( d );
indices.push( c );
a = radiusSegments + cornerVertNumber;
b = radiusSegments + cornerVertNumber * 2;
c = radiusSegments + cornerVertNumber * 5;
d = radiusSegments + cornerVertNumber * 6;
indices.push( a );
indices.push( c );
indices.push( b );
indices.push( b );
indices.push( c );
indices.push( d );
}
function doHeightEdges() {
for ( var i = 0; i < 4; i ++ ) {
var cOffset = i * cornerVertNumber;
var cRowOffset = 4 * cornerVertNumber + cOffset;
var needsFlip = i & 1 === 1;
for ( var u = 0; u < radiusSegments; u ++ ) {
var u1 = u + 1;
var a = cOffset + u;
var b = cOffset + u1;
var c = cRowOffset + u;
var d = cRowOffset + u1;
if ( ! needsFlip ) {
indices.push( a );
indices.push( b );
indices.push( c );
indices.push( b );
indices.push( d );
indices.push( c );
} else {
indices.push( a );
indices.push( c );
indices.push( b );
indices.push( b );
indices.push( c );
indices.push( d );
}
}
}
}
function doDepthEdges() {
var cStarts = [ 0, 2, 4, 6 ];
var cEnds = [ 1, 3, 5, 7 ];
for ( var i = 0; i < 4; i ++ ) {
var cStart = cornerVertNumber * cStarts[ i ];
var cEnd = cornerVertNumber * cEnds[ i ];
var needsFlip = 1 >= i;
for ( var u = 0; u < radiusSegments; u ++ ) {
var urs1 = u * rs1;
var u1rs1 = ( u + 1 ) * rs1;
var a = cStart + urs1;
var b = cStart + u1rs1;
var c = cEnd + urs1;
var d = cEnd + u1rs1;
if ( needsFlip ) {
indices.push( a );
indices.push( c );
indices.push( b );
indices.push( b );
indices.push( c );
indices.push( d );
} else {
indices.push( a );
indices.push( b );
indices.push( c );
indices.push( b );
indices.push( d );
indices.push( c );
}
}
}
}
function doWidthEdges() {
var end = radiusSegments - 1;
var cStarts = [ 0, 1, 4, 5 ];
var cEnds = [ 3, 2, 7, 6 ];
var needsFlip = [ 0, 1, 1, 0 ];
for ( var i = 0; i < 4; i ++ ) {
var cStart = cStarts[ i ] * cornerVertNumber;
var cEnd = cEnds[ i ] * cornerVertNumber;
for ( var u = 0; u <= end; u ++ ) {
var a = cStart + radiusSegments + u * rs1;
var b = cStart + ( u != end ? radiusSegments + ( u + 1 ) * rs1 : cornerVertNumber - 1 );
var c = cEnd + radiusSegments + u * rs1;
var d = cEnd + ( u != end ? radiusSegments + ( u + 1 ) * rs1 : cornerVertNumber - 1 );
if ( ! needsFlip[ i ] ) {
indices.push( a );
indices.push( b );
indices.push( c );
indices.push( b );
indices.push( d );
indices.push( c );
} else {
indices.push( a );
indices.push( c );
indices.push( b );
indices.push( b );
indices.push( c );
indices.push( d );
}
}
}
}
var index = 0;
for ( var i = 0; i < vertexPool.length; i ++ ) {
positions.setXYZ(
index,
vertexPool[ i ].x,
vertexPool[ i ].y,
vertexPool[ i ].z
);
normals.setXYZ(
index,
normalPool[ i ].x,
normalPool[ i ].y,
normalPool[ i ].z
);
index ++;
}
this.setIndex( new THREE.BufferAttribute( new Uint16Array( indices ), 1 ) );
this.addAttribute( 'position', positions );
this.addAttribute( 'normal', normals );
}
RoundedBoxGeometry.prototype = Object.create( THREE.BufferGeometry.prototype );
RoundedBoxGeometry.constructor = RoundedBoxGeometry;
function RoundedPlaneGeometry( size, radius, depth ) {
var x, y, width, height;
x = y = - size / 2;
width = height = size;
radius = size * radius;
const shape = new THREE.Shape();
shape.moveTo( x, y + radius );
shape.lineTo( x, y + height - radius );
shape.quadraticCurveTo( x, y + height, x + radius, y + height );
shape.lineTo( x + width - radius, y + height );
shape.quadraticCurveTo( x + width, y + height, x + width, y + height - radius );
shape.lineTo( x + width, y + radius );
shape.quadraticCurveTo( x + width, y, x + width - radius, y );
shape.lineTo( x + radius, y );
shape.quadraticCurveTo( x, y, x, y + radius );
const geometry = new THREE.ExtrudeBufferGeometry(
shape,
{ depth: depth, bevelEnabled: false, curveSegments: 3 }
);
return geometry;
}
class Cube {
constructor( game ) {
this.game = game;
this.geometry = {
pieceSize: 1 / 3,
pieceCornerRadius: 0.12,
edgeCornerRoundness: 0.15,
edgeScale: 0.82,
edgeDepth: 0.01,
};
this.holder = new THREE.Object3D();
this.object = new THREE.Object3D();
this.animator = new THREE.Object3D();
this.holder.add( this.animator );
this.animator.add( this.object );
this.cubes = [];
this.generatePositions();
this.generateModel();
this.pieces.forEach( piece => {
this.cubes.push( piece.userData.cube );
this.object.add( piece );
} );
this.holder.traverse( node => {
if ( node.frustumCulled ) node.frustumCulled = false;
} );
this.game.world.scene.add( this.holder );
}
reset() {
this.game.controls.edges.rotation.set( 0, 0, 0 );
this.holder.rotation.set( 0, 0, 0 );
this.object.rotation.set( 0, 0, 0 );
this.animator.rotation.set( 0, 0, 0 );
this.pieces.forEach( piece => {
piece.position.copy( piece.userData.start.position );
piece.rotation.copy( piece.userData.start.rotation );
} );
}
generatePositions() {
let x, y, z;
this.positions = [];
for ( x = 0; x < 3; x ++ ) {
for ( y = 0; y < 3; y ++ ) {
for ( z = 0; z < 3; z ++ ) {
let position = new THREE.Vector3( x - 1, y - 1, z - 1 );
let edges = [];
if ( x == 0 ) edges.push(0);
if ( x == 2 ) edges.push(1);
if ( y == 0 ) edges.push(2);
if ( y == 2 ) edges.push(3);
if ( z == 0 ) edges.push(4);
if ( z == 2 ) edges.push(5);
position.edges = edges;
this.positions.push( position );
}
}
}
}
generateModel() {
this.pieces = [];
this.edges = [];
const pieceSize = 1 / 3;
const mainMaterial = new THREE.MeshLambertMaterial();
const pieceMesh = new THREE.Mesh(
new RoundedBoxGeometry( pieceSize, this.geometry.pieceCornerRadius, 3 ),
mainMaterial.clone()
);
const edgeGeometry = RoundedPlaneGeometry(
pieceSize,
this.geometry.edgeCornerRoundness,
this.geometry.edgeDepth );
this.positions.forEach( ( position, index ) => {
const piece = new THREE.Object3D();
const pieceCube = pieceMesh.clone();
const pieceEdges = [];
piece.position.copy( position.clone().divideScalar( 3 ) );
piece.add( pieceCube );
piece.name = index;
piece.edgesName = '';
position.edges.forEach( position => {
const edge = new THREE.Mesh( edgeGeometry, mainMaterial.clone() );
const name = [ 'L', 'R', 'D', 'U', 'B', 'F' ][ position ];
const distance = pieceSize / 2;
edge.position.set(
distance * [ - 1, 1, 0, 0, 0, 0 ][ position ],
distance * [ 0, 0, - 1, 1, 0, 0 ][ position ],
distance * [ 0, 0, 0, 0, - 1, 1 ][ position ]
);
edge.rotation.set(
Math.PI / 2 * [ 0, 0, 1, - 1, 0, 0 ][ position ],
Math.PI / 2 * [ - 1, 1, 0, 0, 2, 0 ][ position ],
0
);
edge.scale.set(
this.geometry.edgeScale,
this.geometry.edgeScale,
this.geometry.edgeScale
);
edge.name = name;
piece.add( edge );
pieceEdges.push( name );
this.edges.push( edge );
} );
piece.userData.edges = pieceEdges;
piece.userData.cube = pieceCube;
piece.userData.start = {
position: piece.position.clone(),
rotation: piece.rotation.clone(),
};
this.pieces.push( piece );
} );
}
}
const Easing = {
Power: {
In: power => {
power = Math.round( power || 1 );
return t => Math.pow( t, power );
},
Out: power => {
power = Math.round( power || 1 );
return t => 1 - Math.abs( Math.pow( t - 1, power ) );
},
InOut: power => {
power = Math.round( power || 1 );
return t => ( t < 0.5 )
? Math.pow( t * 2, power ) / 2
: ( 1 - Math.abs( Math.pow( ( t * 2 - 1 ) - 1, power ) ) ) / 2 + 0.5;
},
},
Sine: {
In: () => t => 1 + Math.sin( Math.PI / 2 * t - Math.PI / 2 ),
Out: () => t => Math.sin( Math.PI / 2 * t ),
InOut: () => t => ( 1 + Math.sin( Math.PI * t - Math.PI / 2 ) ) / 2,
},
Back: {
Out: s => {
s = s || 1.70158;
return t => { return ( t -= 1 ) * t * ( ( s + 1 ) * t + s ) + 1; };
},
In: s => {
s = s || 1.70158;
return t => { return t * t * ( ( s + 1 ) * t - s ); };
}
},
Elastic: {
Out: ( amplitude, period ) => {
let PI2 = Math.PI * 2;
let p1 = ( amplitude >= 1 ) ? amplitude : 1;
let p2 = ( period || 0.3 ) / ( amplitude < 1 ? amplitude : 1 );
let p3 = p2 / PI2 * ( Math.asin( 1 / p1 ) || 0 );
p2 = PI2 / p2;
return t => { return p1 * Math.pow( 2, -10 * t ) * Math.sin( ( t - p3 ) * p2 ) + 1 }
},
},
};
class Tween extends Animation {
constructor( options ) {
super( false );
this.duration = options.duration || 500;
this.easing = options.easing || ( t => t );
this.onUpdate = options.onUpdate || ( () => {} );
this.onComplete = options.onComplete || ( () => {} );
this.delay = options.delay || false;
this.yoyo = options.yoyo ? false : null;
this.progress = 0;
this.value = 0;
this.delta = 0;
this.getFromTo( options );
if ( this.delay ) setTimeout( () => super.start(), this.delay );
else super.start();
this.onUpdate( this );
}
update( delta ) {
const old = this.value * 1;
const direction = ( this.yoyo === true ) ? - 1 : 1;
this.progress += ( delta / this.duration ) * direction;
this.value = this.easing( this.progress );
this.delta = this.value - old;
if ( this.values !== null ) this.updateFromTo();
if ( this.yoyo !== null ) this.updateYoyo();
else if ( this.progress <= 1 ) this.onUpdate( this );
else {
this.progress = 1;
this.value = 1;
this.onUpdate( this );
this.onComplete( this );
super.stop();
}
}
updateYoyo() {
if ( this.progress > 1 || this.progress < 0 ) {
this.value = this.progress = ( this.progress > 1 ) ? 1 : 0;
this.yoyo = ! this.yoyo;
}
this.onUpdate( this );
}
updateFromTo() {
this.values.forEach( key => {
this.target[ key ] = this.from[ key ] + ( this.to[ key ] - this.from[ key ] ) * this.value;
} );
}
getFromTo( options ) {
if ( ! options.target || ! options.to ) {
this.values = null;
return;
}
this.target = options.target || null;
this.from = options.from || {};
this.to = options.to || null;
this.values = [];
if ( Object.keys( this.from ).length < 1 )
Object.keys( this.to ).forEach( key => { this.from[ key ] = this.target[ key ]; } );
Object.keys( this.to ).forEach( key => { this.values.push( key ); } );
}
}
window.addEventListener( 'touchmove', () => {} );
document.addEventListener( 'touchmove', event => { event.preventDefault(); }, { passive: false } );
class Draggable {
constructor( element, options ) {
this.position = {
current: new THREE.Vector2(),
start: new THREE.Vector2(),
delta: new THREE.Vector2(),
old: new THREE.Vector2(),
drag: new THREE.Vector2(),
};
this.options = Object.assign( {
calcDelta: false,
}, options || {} );
this.element = element;
this.touch = null;
this.drag = {
start: ( event ) => {
if ( event.type == 'mousedown' && event.which != 1 ) return;
if ( event.type == 'touchstart' && event.touches.length > 1 ) return;
this.getPositionCurrent( event );
if ( this.options.calcDelta ) {
this.position.start = this.position.current.clone();
this.position.delta.set( 0, 0 );
this.position.drag.set( 0, 0 );
}
this.touch = ( event.type == 'touchstart' );
this.onDragStart( this.position );
window.addEventListener( ( this.touch ) ? 'touchmove' : 'mousemove', this.drag.move, false );
window.addEventListener( ( this.touch ) ? 'touchend' : 'mouseup', this.drag.end, false );
},
move: ( event ) => {
if ( this.options.calcDelta ) {
this.position.old = this.position.current.clone();
}
this.getPositionCurrent( event );
if ( this.options.calcDelta ) {
this.position.delta = this.position.current.clone().sub( this.position.old );
this.position.drag = this.position.current.clone().sub( this.position.start );
}
this.onDragMove( this.position );
},
end: ( event ) => {
this.getPositionCurrent( event );
this.onDragEnd( this.position );
window.removeEventListener( ( this.touch ) ? 'touchmove' : 'mousemove', this.drag.move, false );
window.removeEventListener( ( this.touch ) ? 'touchend' : 'mouseup', this.drag.end, false );
},
};
this.onDragStart = () => {};
this.onDragMove = () => {};
this.onDragEnd = () => {};
this.enable();
return this;
}
enable() {
this.element.addEventListener( 'touchstart', this.drag.start, false );
this.element.addEventListener( 'mousedown', this.drag.start, false );
return this;
}
disable() {
this.element.removeEventListener( 'touchstart', this.drag.start, false );
this.element.removeEventListener( 'mousedown', this.drag.start, false );
return this;
}
getPositionCurrent( event ) {
const dragEvent = event.touches
? ( event.touches[ 0 ] || event.changedTouches[ 0 ] )
: event;
this.position.current.set( dragEvent.pageX, dragEvent.pageY );
}
convertPosition( position ) {
position.x = ( position.x / this.element.offsetWidth ) * 2 - 1;
position.y = - ( ( position.y / this.element.offsetHeight ) * 2 - 1 );
return position;
}
}
const STILL = 0;
const PREPARING = 1;
const ROTATING = 2;
const ANIMATING = 3;
class Controls {
constructor( game ) {
this.game = game;
this.flipConfig = 0;
this.flipEasings = [ Easing.Power.Out( 3 ), Easing.Sine.Out(), Easing.Back.Out( 2 ) ];
this.flipSpeeds = [ 125, 200, 350 ];
this.raycaster = new THREE.Raycaster();
const helperMaterial = new THREE.MeshBasicMaterial( { depthWrite: false, transparent: true, opacity: 0, color: 0x0033ff } );
this.group = new THREE.Object3D();
this.game.cube.object.add( this.group );
this.helper = new THREE.Mesh(
new THREE.PlaneBufferGeometry( 20, 20 ),
helperMaterial.clone(),
);
this.helper.rotation.set( 0, Math.PI / 4, 0 );
this.game.world.scene.add( this.helper );
this.edges = new THREE.Mesh(
new THREE.BoxBufferGeometry( 0.95, 0.95, 0.95 ),
helperMaterial.clone(),
);
this.game.world.scene.add( this.edges );
this.onSolved = () => {};
this.onMove = () => {};
this.momentum = [];
this.scramble = null;
this.state = STILL;
this.initDraggable();
}
enable() {
this.draggable.enable();
}
disable() {
this.draggable.disable();
}
initDraggable() {
this.draggable = new Draggable( this.game.dom.game );
this.draggable.onDragStart = position => {
if ( this.scramble !== null ) return;
if ( this.state === PREPARING || this.state === ROTATING ) return;
this.gettingDrag = this.state === ANIMATING;
const edgeIntersect = this.getIntersect( position.current, this.edges, false );
if ( edgeIntersect !== false ) {
this.dragNormal = edgeIntersect.face.normal.round();
this.flipType = 'layer';
this.attach( this.helper, this.edges );
this.helper.rotation.set( 0, 0, 0 );
this.helper.position.set( 0, 0, 0 );
this.helper.lookAt( this.dragNormal );
this.helper.translateZ( 0.5 );
this.helper.updateMatrixWorld();
this.detach( this.helper, this.edges );
} else {
this.dragNormal = new THREE.Vector3( 0, 0, 1 );
this.flipType = 'cube';
this.helper.position.set( 0, 0, 0 );
this.helper.rotation.set( 0, Math.PI / 4, 0 );
this.helper.updateMatrixWorld();
}
const planeIntersect = this.getIntersect( position.current, this.helper, false ).point;
if ( planeIntersect === false ) return;
this.dragCurrent = this.helper.worldToLocal( planeIntersect );
this.dragTotal = new THREE.Vector3();
this.state = ( this.state === STILL ) ? PREPARING : this.state;
};
this.draggable.onDragMove = position => {
if ( this.scramble !== null ) return;
if ( this.state === STILL || ( this.state === ANIMATING && this.gettingDrag === false ) ) return;
const planeIntersect = this.getIntersect( position.current, this.helper, false );
if ( planeIntersect === false ) return;
const point = this.helper.worldToLocal( planeIntersect.point.clone() );
this.dragDelta = point.clone().sub( this.dragCurrent ).setZ( 0 );
this.dragTotal.add( this.dragDelta );
this.dragCurrent = point;
this.addMomentumPoint( this.dragDelta );
if ( this.state === PREPARING && this.dragTotal.length() > 0.05 ) {
this.dragDirection = this.getMainAxis( this.dragTotal );
if ( this.flipType === 'layer' ) {
const direction = new THREE.Vector3();
direction[ this.dragDirection ] = 1;
const worldDirection = this.helper.localToWorld( direction ).sub( this.helper.position );
const objectDirection = this.edges.worldToLocal( worldDirection ).round();
this.flipAxis = objectDirection.cross( this.dragNormal ).negate();
this.dragIntersect = this.getIntersect( position.current, this.game.cube.cubes, true );
this.selectLayer( this.getLayer( false ) );
} else {
const axis = ( this.dragDirection != 'x' )
? ( ( this.dragDirection == 'y' && position.current.x > this.game.world.width / 2 ) ? 'z' : 'x' )
: 'y';
this.flipAxis = new THREE.Vector3();
this.flipAxis[ axis ] = 1 * ( ( axis == 'x' ) ? - 1 : 1 );
}
this.flipAngle = 0;
this.state = ROTATING;
} else if ( this.state === ROTATING ) {
const rotation = this.dragDelta[ this.dragDirection ];
if ( this.flipType === 'layer' ) {
this.group.rotateOnAxis( this.flipAxis, rotation );
this.flipAngle += rotation;
} else {
this.edges.rotateOnWorldAxis( this.flipAxis, rotation );
this.game.cube.object.rotation.copy( this.edges.rotation );
this.flipAngle += rotation;
}
}
};
this.draggable.onDragEnd = position => {
if ( this.scramble !== null ) return;
if ( this.state !== ROTATING ) {
this.gettingDrag = false;
this.state = STILL;
return;
}
this.state = ANIMATING;
const momentum = this.getMomentum()[ this.dragDirection ];
const flip = ( Math.abs( momentum ) > 0.05 && Math.abs( this.flipAngle ) < Math.PI / 2 );
const angle = flip
? this.roundAngle( this.flipAngle + Math.sign( this.flipAngle ) * ( Math.PI / 4 ) )
: this.roundAngle( this.flipAngle );
const delta = angle - this.flipAngle;
if ( this.flipType === 'layer' ) {
this.rotateLayer( delta, false, layer => {
this.state = this.gettingDrag ? PREPARING : STILL;
this.gettingDrag = false;
this.checkIsSolved();
} );
} else {
this.rotateCube( delta, () => {
this.state = this.gettingDrag ? PREPARING : STILL;
this.gettingDrag = false;
} );
}
};
}
rotateLayer( rotation, scramble, callback ) {
const config = scramble ? 0 : this.flipConfig;
const easing = this.flipEasings[ config ];
const duration = this.flipSpeeds[ config ];
const bounce = ( config == 2 ) ? this.bounceCube() : ( () => {} );
this.rotationTween = new Tween( {
easing: easing,
duration: duration,
onUpdate: tween => {
let deltaAngle = tween.delta * rotation;
this.group.rotateOnAxis( this.flipAxis, deltaAngle );
bounce( tween.value, deltaAngle, rotation );
},
onComplete: () => {
if ( ! scramble ) this.onMove();
const layer = this.flipLayer.slice( 0 );
this.game.cube.object.rotation.setFromVector3( this.snapRotation( this.game.cube.object.rotation.toVector3() ) );
this.group.rotation.setFromVector3( this.snapRotation( this.group.rotation.toVector3() ) );
this.deselectLayer( this.flipLayer );
callback( layer );
},
} );
}
bounceCube() {
let fixDelta = true;
return ( progress, delta, rotation ) => {
if ( progress >= 1 ) {
if ( fixDelta ) {
delta = ( progress - 1 ) * rotation;
fixDelta = false;
}
this.game.cube.object.rotateOnAxis( this.flipAxis, delta );
}
}
}
rotateCube( rotation, callback ) {
const config = this.flipConfig;
const easing = [ Easing.Power.Out( 4 ), Easing.Sine.Out(), Easing.Back.Out( 2 ) ][ config ];
const duration = [ 100, 150, 350 ][ config ];
this.rotationTween = new Tween( {
easing: easing,
duration: duration,
onUpdate: tween => {
this.edges.rotateOnWorldAxis( this.flipAxis, tween.delta * rotation );
this.game.cube.object.rotation.copy( this.edges.rotation );
},
onComplete: () => {
this.edges.rotation.setFromVector3( this.snapRotation( this.edges.rotation.toVector3() ) );
this.game.cube.object.rotation.copy( this.edges.rotation );
callback();
},
} );
}
selectLayer( layer ) {
this.group.rotation.set( 0, 0, 0 );
this.movePieces( layer, this.game.cube.object, this.group );
this.flipLayer = layer;
}
deselectLayer( layer ) {
this.movePieces( layer, this.group, this.game.cube.object );
this.flipLayer = null;
}
movePieces( layer, from, to ) {
from.updateMatrixWorld();
to.updateMatrixWorld();
layer.forEach( index => {
const piece = this.game.cube.pieces[ index ];
piece.applyMatrix( from.matrixWorld );
from.remove( piece );
piece.applyMatrix( new THREE.Matrix4().getInverse( to.matrixWorld ) );
to.add( piece );
} );
}
getLayer( position ) {
const layer = [];
let axis;
if ( position === false ) {
axis = this.getMainAxis( this.flipAxis );
position = this.getPiecePosition( this.dragIntersect.object );
} else {
axis = this.getMainAxis( position );
}
this.game.cube.pieces.forEach( piece => {
const piecePosition = this.getPiecePosition( piece );
if ( piecePosition[ axis ] == position[ axis ] ) layer.push( piece.name );
} );
return layer;
}
getPiecePosition( piece ) {
let position = new THREE.Vector3()
.setFromMatrixPosition( piece.matrixWorld )
.multiplyScalar( 3 );
return this.game.cube.object.worldToLocal( position.sub( this.game.cube.animator.position ) ).round();
}
scrambleCube() {
if ( this.scramble == null ) {
this.scramble = this.game.scrambler;
this.scramble.callback = ( typeof callback !== 'function' ) ? () => {} : callback;
}
const converted = this.scramble.converted;
const move = converted[ 0 ];
const layer = this.getLayer( move.position );
this.flipAxis = new THREE.Vector3();
this.flipAxis[ move.axis ] = 1;
this.selectLayer( layer );
this.rotateLayer( move.angle, true, () => {
converted.shift();
if ( converted.length > 0 ) {
this.scrambleCube();
} else {
this.scramble = null;
}
} );
}
getIntersect( position, object, multiple ) {
this.raycaster.setFromCamera(
this.draggable.convertPosition( position.clone() ),
this.game.world.camera
);
const intersect = ( multiple )
? this.raycaster.intersectObjects( object )
: this.raycaster.intersectObject( object );
return ( intersect.length > 0 ) ? intersect[ 0 ] : false;
}
getMainAxis( vector ) {
return Object.keys( vector ).reduce(
( a, b ) => Math.abs( vector[ a ] ) > Math.abs( vector[ b ] ) ? a : b
);
}
detach( child, parent ) {
child.applyMatrix( parent.matrixWorld );
parent.remove( child );
this.game.world.scene.add( child );
}
attach( child, parent ) {
child.applyMatrix( new THREE.Matrix4().getInverse( parent.matrixWorld ) );
this.game.world.scene.remove( child );
parent.add( child );
}
addMomentumPoint( delta ) {
const time = Date.now();
this.momentum = this.momentum.filter( moment => time - moment.time < 500 );
if ( delta !== false ) this.momentum.push( { delta, time } );
}
getMomentum() {
const points = this.momentum.length;
const momentum = new THREE.Vector2();
this.addMomentumPoint( false );
this.momentum.forEach( ( point, index ) => {
momentum.add( point.delta.multiplyScalar( index / points ) );
} );
return momentum;
}
roundAngle( angle ) {
const round = Math.PI / 2;
return Math.sign( angle ) * Math.round( Math.abs( angle) / round ) * round;
}
snapRotation( angle ) {
return angle.set(
this.roundAngle( angle.x ),
this.roundAngle( angle.y ),
this.roundAngle( angle.z )
);
}
checkIsSolved() {
const start = performance.now();
let solved = true;
const sides = { 'x-': [], 'x+': [], 'y-': [], 'y+': [], 'z-': [], 'z+': [] };
this.game.cube.edges.forEach( edge => {
const position = edge.parent
.localToWorld( edge.position.clone() )
.sub( this.game.cube.object.position );
const mainAxis = this.getMainAxis( position );
const mainSign = position.multiplyScalar( 2 ).round()[ mainAxis ] < 1 ? '-' : '+';
sides[ mainAxis + mainSign ].push( edge.name );
} );
Object.keys( sides ).forEach( side => {
if ( ! sides[ side ].every( value => value === sides[ side ][ 0 ] ) ) solved = false;
} );
if ( solved ) this.onSolved();
}
}
class Scrambler {
constructor( game ) {
this.game = game;
this.scrambleLength = 20;
this.moves = [];
this.conveted = [];
this.pring = '';
}
scramble( scramble ) {
let count = 0;
this.moves = ( typeof scramble !== 'undefined' ) ? scramble.split( ' ' ) : [];
if ( this.moves.length < 1 ) {
const faces = 'UDLRFB';
const modifiers = [ "", "'", "2" ];
const total = ( typeof scramble === 'undefined' ) ? this.scrambleLength : scramble;
while ( count < total ) {
const move = faces[ Math.floor( Math.random() * 6 ) ] + modifiers[ Math.floor( Math.random() * 3 ) ];
if ( count > 0 && move.charAt( 0 ) == this.moves[ count - 1 ].charAt( 0 ) ) continue;
if ( count > 1 && move.charAt( 0 ) == this.moves[ count - 2 ].charAt( 0 ) ) continue;
this.moves.push( move );
count ++;
}
}
this.callback = () => {};
this.convert();
this.print = this.moves.join( ' ' );
return this;
}
convert( moves ) {
this.converted = [];
this.moves.forEach( move => {
const face = move.charAt( 0 );
const modifier = move.charAt( 1 );
const axis = { D: 'y', U: 'y', L: 'x', R: 'x', F: 'z', B: 'z' }[ face ];
const row = { D: -1, U: 1, L: -1, R: 1, F: 1, B: -1 }[ face ];
const position = new THREE.Vector3();
position[ { D: 'y', U: 'y', L: 'x', R: 'x', F: 'z', B: 'z' }[ face ] ] = row;
const angle = ( Math.PI / 2 ) * - row * ( ( modifier == "'" ) ? - 1 : 1 );
const convertedMove = { position, axis, angle, name: move };
this.converted.push( convertedMove );
if ( modifier == "2" ) this.converted.push( convertedMove );
} );
}
}
class Transition {
constructor( game ) {
this.game = game;
this.tweens = {};
this.durations = {};
this.data = {
cubeY: -0.2,
cameraZoom: 0.85,
};
this.activeTransitions = 0;
}
init() {
this.game.controls.disable();
this.game.cube.object.position.y = this.data.cubeY;
this.game.controls.edges.position.y = this.data.cubeY;
this.game.cube.animator.position.y = 4;
this.game.cube.animator.rotation.x = - Math.PI / 3;
this.game.world.camera.zoom = this.data.cameraZoom;
this.game.world.camera.updateProjectionMatrix();
this.tweens.buttons = {};
this.tweens.timer = [];
this.tweens.title = [];
this.tweens.best = [];
this.tweens.complete = [];
this.tweens.range = [];
this.tweens.stats = [];
}
buttons( show, hide ) {
const buttonTween = ( button, show ) => {
return new Tween( {
target: button.style,
duration: 300,
easing: show ? Easing.Power.Out( 2 ) : Easing.Power.In( 3 ),
from: { opacity: show ? 0 : 1 },
to: { opacity: show ? 1 : 0 },
onUpdate: tween => {
const translate = show ? 1 - tween.value : tween.value;
button.style.transform = `translate3d(0, ${translate * 1.5}em, 0)`;
},
onComplete: () => button.style.pointerEvents = show ? 'all' : 'none'
} );
};
hide.forEach( button =>
this.tweens.buttons[ button ] = buttonTween( this.game.dom.buttons[ button ], false )
);
setTimeout( () => show.forEach( button => {
this.tweens.buttons[ button ] = buttonTween( this.game.dom.buttons[ button ], true );
} ), hide ? 500 : 0 );
}
cube( show ) {
this.activeTransitions++;
try { this.tweens.cube.stop(); } catch(e) {}
const currentY = this.game.cube.animator.position.y;
const currentRotation = this.game.cube.animator.rotation.x;
this.tweens.cube = new Tween( {
duration: show ? 3000 : 1250,
easing: show ? Easing.Elastic.Out( 0.8, 0.6 ) : Easing.Back.In( 1 ),
onUpdate: tween => {
this.game.cube.animator.position.y = show
? ( 1 - tween.value ) * 4
: currentY + tween.value * 4;
this.game.cube.animator.rotation.x = show
? ( 1 - tween.value ) * Math.PI / 3
: currentRotation + tween.value * - Math.PI / 3;
}
} );
this.durations.cube = show ? 1500 : 1500;
setTimeout( () => this.activeTransitions--, this.durations.cube );
}
float() {
try { this.tweens.float.stop(); } catch(e) {}
this.tweens.float = new Tween( {
duration: 1500,
easing: Easing.Sine.InOut(),
yoyo: true,
onUpdate: tween => {
this.game.cube.holder.position.y = (- 0.02 + tween.value * 0.04);
this.game.cube.holder.rotation.x = 0.005 - tween.value * 0.01;
this.game.cube.holder.rotation.z = - this.game.cube.holder.rotation.x;
this.game.cube.holder.rotation.y = this.game.cube.holder.rotation.x;
},
} );
}
zoom( play, time ) {
this.activeTransitions++;
const zoom = ( play ) ? 1 : this.data.cameraZoom;
const duration = ( time > 0 ) ? Math.max( time, 1500 ) : 1500;
const rotations = ( time > 0 ) ? Math.round( duration / 1500 ) : 1;
const easing = Easing.Power.InOut( ( time > 0 ) ? 2 : 3 );
this.tweens.zoom = new Tween( {
target: this.game.world.camera,
duration: duration,
easing: easing,
to: { zoom: zoom },
onUpdate: () => { this.game.world.camera.updateProjectionMatrix(); },
} );
this.tweens.rotate = new Tween( {
target: this.game.cube.animator.rotation,
duration: duration,
easing: easing,
to: { y: - Math.PI * 2 * rotations },
onComplete: () => { this.game.cube.animator.rotation.y = 0; },
} );
this.durations.zoom = duration;
setTimeout( () => this.activeTransitions--, this.durations.zoom );
}
elevate( complete ) {
this.activeTransitions++;
const cubeY =
this.tweens.elevate = new Tween( {
target: this.game.cube.object.position,
duration: complete ? 1500 : 0,
easing: Easing.Power.InOut( 3 ),
to: { y: complete ? -0.05 : this.data.cubeY }
} );
this.durations.elevate = 1500;
setTimeout( () => this.activeTransitions--, this.durations.elevate );
}
complete( show, best ) {
this.activeTransitions++;
const text = best ? this.game.dom.texts.best : this.game.dom.texts.complete;
if ( text.querySelector( 'span i' ) === null )
text.querySelectorAll( 'span' ).forEach( span => this.splitLetters( span ) );
const letters = text.querySelectorAll( '.icon, i' );
this.flipLetters( best ? 'best' : 'complete', letters, show );
text.style.opacity = 1;
const duration = this.durations[ best ? 'best' : 'complete' ];
if ( ! show ) setTimeout( () => this.game.dom.texts.timer.style.transform = '', duration );
setTimeout( () => this.activeTransitions--, duration );
}
stats( show ) {
if ( show ) this.game.scores.calcStats();
this.activeTransitions++;
this.tweens.stats.forEach( tween => { tween.stop(); tween = null; } );
let tweenId = -1;
const stats = this.game.dom.stats.querySelectorAll( '.stats' );
const easing = show ? Easing.Power.Out( 2 ) : Easing.Power.In( 3 );
stats.forEach( ( stat, index ) => {
const delay = index * ( show ? 80 : 60 );
this.tweens.stats[ tweenId++ ] = new Tween( {
delay: delay,
duration: 400,
easing: easing,
onUpdate: tween => {
const translate = show ? ( 1 - tween.value ) * 2 : tween.value;
const opacity = show ? tween.value : ( 1 - tween.value );
stat.style.transform = `translate3d(0, ${translate}em, 0)`;
stat.style.opacity = opacity;
}
} );
} );
this.durations.stats = 0;
setTimeout( () => this.activeTransitions--, this.durations.stats );
}
preferences( show ) {
this.activeTransitions++;
this.tweens.range.forEach( tween => { tween.stop(); tween = null; } );
let tweenId = -1;
let listMax = 0;
const ranges = this.game.dom.prefs.querySelectorAll( '.range' );
const easing = show ? Easing.Power.Out(2) : Easing.Power.In(3);
ranges.forEach( ( range, rangeIndex ) => {
const label = range.querySelector( '.range__label' );
const track = range.querySelector( '.range__track-line' );
const handle = range.querySelector( '.range__handle' );
const list = range.querySelectorAll( '.range__list div' );
const delay = rangeIndex * ( show ? 120 : 100 );
label.style.opacity = show ? 0 : 1;
track.style.opacity = show ? 0 : 1;
handle.style.opacity = show ? 0 : 1;
handle.style.pointerEvents = show ? 'all' : 'none';
this.tweens.range[ tweenId++ ] = new Tween( {
delay: show ? delay : delay,
duration: 400,
easing: easing,
onUpdate: tween => {
const translate = show ? ( 1 - tween.value ) : tween.value;
const opacity = show ? tween.value : ( 1 - tween.value );
label.style.transform = `translate3d(0, ${translate}em, 0)`;
label.style.opacity = opacity;
}
} );
this.tweens.range[ tweenId++ ] = new Tween( {
delay: show ? delay + 100 : delay,
duration: 400,
easing: easing,
onUpdate: tween => {
const translate = show ? ( 1 - tween.value ) : tween.value;
const scale = show ? tween.value : ( 1 - tween.value );
const opacity = scale;
track.style.transform = `translate3d(0, ${translate}em, 0) scale3d(${scale}, 1, 1)`;
track.style.opacity = opacity;
}
} );
this.tweens.range[ tweenId++ ] = new Tween( {
delay: show ? delay + 100 : delay,
duration: 400,
easing: easing,
onUpdate: tween => {
const translate = show ? ( 1 - tween.value ) : tween.value;
const opacity = 1 - translate;
const scale = 0.5 + opacity * 0.5;
handle.style.transform = `translate3d(0, ${translate}em, 0) scale3d(${scale}, ${scale}, ${scale})`;
handle.style.opacity = opacity;
}
} );
list.forEach( ( listItem, labelIndex ) => {
listItem.style.opacity = show ? 0 : 1;
this.tweens.range[ tweenId++ ] = new Tween( {
delay: show ? delay + 200 + labelIndex * 50 : delay,
duration: 400,
easing: easing,
onUpdate: tween => {
const translate = show ? ( 1 - tween.value ) : tween.value;
const opacity = show ? tween.value : ( 1 - tween.value );
listItem.style.transform = `translate3d(0, ${translate}em, 0)`;
listItem.style.opacity = opacity;
}
} );
} );
listMax = list.length > listMax ? list.length - 1 : listMax;
range.style.opacity = 1;
} );
this.durations.preferences = show
? ( ( ranges.length - 1 ) * 100 ) + 200 + listMax * 50 + 400
: ( ( ranges.length - 1 ) * 100 ) + 400;
setTimeout( () => this.activeTransitions--, this.durations.preferences );
}
title( show ) {
this.activeTransitions++;
const title = this.game.dom.texts.title;
if ( title.querySelector( 'span i' ) === null )
title.querySelectorAll( 'span' ).forEach( span => this.splitLetters( span ) );
const letters = title.querySelectorAll( 'i' );
this.flipLetters( 'title', letters, show );
title.style.opacity = 1;
const note = this.game.dom.texts.note;
this.tweens.title[ letters.length ] = new Tween( {
target: note.style,
easing: Easing.Sine.InOut(),
duration: show ? 800 : 400,
yoyo: show ? true : null,
from: { opacity: show ? 0 : ( parseFloat( getComputedStyle( note ).opacity ) ) },
to: { opacity: show ? 1 : 0 },
} );
setTimeout( () => this.activeTransitions--, this.durations.title );
}
timer( show ) {
this.activeTransitions++;
const timer = this.game.dom.texts.timer;
timer.style.opacity = 0;
this.game.timer.convert();
this.game.timer.setText();
this.splitLetters( timer );
const letters = timer.querySelectorAll( 'i' );
this.flipLetters( 'timer', letters, show );
timer.style.opacity = 1;
setTimeout( () => this.activeTransitions--, this.durations.timer );
}
splitLetters( element ) {
const text = element.innerHTML;
element.innerHTML = '';
text.split( '' ).forEach( letter => {
const i = document.createElement( 'i' );
i.innerHTML = letter;
element.appendChild( i );
} );
}
flipLetters( type, letters, show ) {
try { this.tweens[ type ].forEach( tween => tween.stop() ); } catch(e) {}
letters.forEach( ( letter, index ) => {
letter.style.opacity = show ? 0 : 1;
this.tweens[ type ][ index ] = new Tween( {
easing: Easing.Sine.Out(),
duration: show ? 800 : 400,
delay: index * 50,
onUpdate: tween => {
const rotation = show ? ( 1 - tween.value ) * -80 : tween.value * 80;
letter.style.transform = `rotate3d(0, 1, 0, ${rotation}deg)`;
letter.style.opacity = show ? tween.value : ( 1 - tween.value );
},
} );
} );
this.durations[ type ] = ( letters.length - 1 ) * 50 + ( show ? 800 : 400 );
}
}
class Timer extends Animation {
constructor( game ) {
super( false );
this.game = game;
this.reset();
}
start( continueGame ) {
this.startTime = continueGame ? ( Date.now() - this.deltaTime ) : Date.now();
this.deltaTime = 0;
this.converted = this.convert();
super.start();
}
reset() {
this.startTime = 0;
this.currentTime = 0;
this.deltaTime = 0;
this.converted = '0:00';
}
stop() {
this.currentTime = Date.now();
this.deltaTime = this.currentTime - this.startTime;
this.convert();
super.stop();
return { time: this.converted, millis: this.deltaTime };
}
update() {
const old = this.converted;
this.currentTime = Date.now();
this.deltaTime = this.currentTime - this.startTime;
this.convert();
if ( this.converted != old ) {
localStorage.setItem( 'theCube_time', this.deltaTime );
this.setText();
}
}
convert() {
const seconds = parseInt( ( this.deltaTime / 1000 ) % 60 );
const minutes = parseInt( ( this.deltaTime / ( 1000 * 60 ) ) );
this.converted = minutes + ':' + ( seconds < 10 ? '0' : '' ) + seconds;
}
setText() {
this.game.dom.texts.timer.innerHTML = this.converted;
}
}
const RangeHTML = [
'<div class="range">',
'<div class="range__label"></div>',
'<div class="range__track">',
'<div class="range__track-line"></div>',
'<div class="range__handle"><div></div></div>',
'</div>',
'<div class="range__list"></div>',
'</div>',
].join( '\n' );
document.querySelectorAll( 'range' ).forEach( el => {
const temp = document.createElement( 'div' );
temp.innerHTML = RangeHTML;
const range = temp.querySelector( '.range' );
const rangeLabel = range.querySelector( '.range__label' );
const rangeList = range.querySelector( '.range__list' );
range.setAttribute( 'name', el.getAttribute( 'name' ) );
rangeLabel.innerHTML = el.getAttribute( 'title' );
el.getAttribute( 'list' ).split( ',' ).forEach( listItemText => {
const listItem = document.createElement( 'div' );
listItem.innerHTML = listItemText;
rangeList.appendChild( listItem );
} );
el.parentNode.replaceChild( range, el );
} );
class Range {
constructor( name, options ) {
options = Object.assign( {
range: [ 0, 1 ],
value: 0,
step: 0,
onUpdate: () => {},
onComplete: () => {},
}, options || {} );
this.element = document.querySelector( '.range[name="' + name + '"]' );
this.track = this.element.querySelector( '.range__track' );
this.handle = this.element.querySelector( '.range__handle' );
this.value = options.value;
this.min = options.range[0];
this.max = options.range[1];
this.step = options.step;
this.onUpdate = options.onUpdate;
this.onComplete = options.onComplete;
this.value = this.round( this.limitValue( this.value ) );
this.setHandlePosition();
this.initDraggable();
}
initDraggable() {
let current;
this.draggable = new Draggable( this.handle, { calcDelta: true } );
this.draggable.onDragStart = position => {
current = this.positionFromValue( this.value );
this.handle.style.left = current + 'px';
};
this.draggable.onDragMove = position => {
current = this.limitPosition( current + position.delta.x );
this.value = this.round( this.valueFromPosition( current ) );
this.setHandlePosition();
this.onUpdate( this.value );
};
this.draggable.onDragEnd = position => {
this.onComplete( this.value );
};
}
round( value ) {
if ( this.step < 1 ) return value;
return Math.round( ( value - this.min ) / this.step ) * this.step + this.min;
}
limitValue( value ) {
const max = Math.max( this.max, this.min );
const min = Math.min( this.max, this.min );
return Math.min( Math.max( value, min ), max );
}
limitPosition( position ) {
return Math.min( Math.max( position, 0 ), this.track.offsetWidth );
}
percentsFromValue( value ) {
return ( value - this.min ) / ( this.max - this.min );
}
valueFromPosition( position ) {
return this.min + ( this.max - this.min ) * ( position / this.track.offsetWidth );
}
positionFromValue( value ) {
return this.percentsFromValue( value ) * this.track.offsetWidth;
}
setHandlePosition() {
this.handle.style.left = this.percentsFromValue( this.value ) * 100 + '%';
}
}
class Preferences {
constructor( game ) {
this.game = game;
}
init() {
this.ranges = {
flip: new Range( 'flip', {
value: this.game.controls.flipConfig,
range: [ 0, 2 ],
step: 1,
onUpdate: value => {
this.game.controls.flipConfig = value;
},
} ),
scramble: new Range( 'scramble', {
value: this.game.scrambler.scrambleLength,
range: [ 20, 30 ],
step: 5,
onUpdate: value => {
this.game.scrambler.scrambleLength = value;
},
} ),
fov: new Range( 'fov', {
value: this.game.world.fov,
range: [ 2, 45 ],
onUpdate: value => {
this.game.world.fov = value;
this.game.world.resize();
},
} ),
theme: new Range( 'theme', {
value: { cube: 0, erno: 1, dust: 2, camo: 3, rain: 4 }[ this.game.themes.theme ],
range: [ 0, 4 ],
step: 1,
onUpdate: value => {
const theme = [ 'cube', 'erno', 'dust', 'camo', 'rain' ][ value ];
this.game.themes.setTheme( theme );
},
} ),
};
}
}
class Confetti {
constructor( game ) {
this.game = game;
this.started = 0;
this.options = {
speed: { min: 0.0011, max: 0.0022 },
revolution: { min: 0.01, max: 0.05 },
size: { min: 0.1, max: 0.15 },
colors: [ 0x41aac8, 0x82ca38, 0xffef48, 0xef3923, 0xff8c0a ],
};
this.geometry = new THREE.PlaneGeometry( 1, 1 );
this.material = new THREE.MeshLambertMaterial( { side: THREE.DoubleSide } );
this.holders = [
new ConfettiStage( this.game, this, 1, 20 ),
new ConfettiStage( this.game, this, -1, 30 ),
];
}
start() {
if ( this.started > 0 ) return;
this.holders.forEach( holder => {
this.game.world.scene.add( holder.holder );
holder.start();
this.started ++;
} );
}
stop() {
if ( this.started == 0 ) return;
this.holders.forEach( holder => {
holder.stop( () => {
this.game.world.scene.remove( holder.holder );
this.started --;
} );
} );
}
updateColors( colors ) {
this.holders.forEach( holder => {
holder.options.colors.forEach( ( color, index ) => {
holder.options.colors[ index ] = colors[ [ 'D', 'F', 'R', 'B', 'L' ][ index ] ];
} );
} );
}
}
class ConfettiStage extends Animation {
constructor( game, parent, distance, count ) {
super( false );
this.game = game;
this.parent = parent;
this.distanceFromCube = distance;
this.count = count;
this.particles = [];
this.holder = new THREE.Object3D();
this.holder.rotation.copy( this.game.world.camera.rotation );
this.object = new THREE.Object3D();
this.holder.add( this.object );
this.resizeViewport = this.resizeViewport.bind( this );
this.game.world.onResize.push( this.resizeViewport );
this.resizeViewport();
this.geometry = this.parent.geometry;
this.material = this.parent.material;
this.options = this.parent.options;
let i = this.count;
while ( i-- ) this.particles.push( new Particle( this ) );
}
start() {
this.time = performance.now();
this.playing = true;
let i = this.count;
while ( i-- ) this.particles[ i ].reset();
super.start();
}
stop( callback ) {
this.playing = false;
this.completed = 0;
this.callback = callback;
}
reset() {
super.stop();
this.callback();
}
update() {
const now = performance.now();
const delta = now - this.time;
this.time = now;
let i = this.count;
while ( i-- )
if ( ! this.particles[ i ].completed ) this.particles[ i ].update( delta );
if ( ! this.playing && this.completed == this.count ) this.reset();
}
resizeViewport() {
const fovRad = this.game.world.camera.fov * THREE.Math.DEG2RAD;
this.height = 2 * Math.tan( fovRad / 2 ) * ( this.game.world.camera.position.length() - this.distanceFromCube );
this.width = this.height * this.game.world.camera.aspect;
const scale = 1 / this.game.transition.data.cameraZoom;
this.width *= scale;
this.height *= scale;
this.object.position.z = this.distanceFromCube;
this.object.position.y = this.height / 2;
}
}
class Particle {
constructor( confetti ) {
this.confetti = confetti;
this.options = this.confetti.options;
this.velocity = new THREE.Vector3();
this.force = new THREE.Vector3();
this.mesh = new THREE.Mesh( this.confetti.geometry, this.confetti.material.clone() );
this.confetti.object.add( this.mesh );
this.size = THREE.Math.randFloat( this.options.size.min, this.options.size.max );
this.mesh.scale.set( this.size, this.size, this.size );
return this;
}
reset( randomHeight = true ) {
this.completed = false;
this.color = new THREE.Color( this.options.colors[ Math.floor( Math.random() * this.options.colors.length ) ] );
this.mesh.material.color.set( this.color );
this.speed = THREE.Math.randFloat( this.options.speed.min, this.options.speed.max ) * - 1;
this.mesh.position.x = THREE.Math.randFloat( - this.confetti.width / 2, this.confetti.width / 2 );
this.mesh.position.y = ( randomHeight )
? THREE.Math.randFloat( this.size, this.confetti.height + this.size )
: this.size;
this.revolutionSpeed = THREE.Math.randFloat( this.options.revolution.min, this.options.revolution.max );
this.revolutionAxis = [ 'x', 'y', 'z' ][ Math.floor( Math.random() * 3 ) ];
this.mesh.rotation.set( Math.random() * Math.PI / 3, Math.random() * Math.PI / 3, Math.random() * Math.PI / 3 );
}
stop() {
this.completed = true;
this.confetti.completed ++;
}
update( delta ) {
this.mesh.position.y += this.speed * delta;
this.mesh.rotation[ this.revolutionAxis ] += this.revolutionSpeed;
if ( this.mesh.position.y < - this.confetti.height - this.size )
( this.confetti.playing ) ? this.reset( false ) : this.stop();
}
}
class Scores {
constructor( game ) {
this.game = game;
this.scores = [];
this.solves = 0;
this.best = 0;
this.worst = 0;
}
addScore( time ) {
this.scores.push( time );
this.solves++;
if ( this.scores.lenght > 100 ) this.scores.shift();
let bestTime = false;
if ( time < this.best || this.best === 0 ) {
this.best = time;
bestTime = true;
}
if ( time > this.worst ) this.worst = time;
return bestTime;
}
calcStats() {
this.setStat( 'total-solves', this.solves );
this.setStat( 'best-time', this.convertTime( this.best ) );
this.setStat( 'worst-time', this.convertTime( this.worst ) );
this.setStat( 'average-5', this.getAverage( 5 ) );
this.setStat( 'average-12', this.getAverage( 12 ) );
this.setStat( 'average-25', this.getAverage( 25 ) );
}
setStat( name, value ) {
if ( value === 0 ) return;
this.game.dom.stats.querySelector( `.stats[name="${name}"] b` ).innerHTML = value;
}
getAverage( count ) {
if ( this.scores.length < count ) return 0;
return this.convertTime( this.scores.slice(-count).reduce( ( a, b ) => a + b, 0 ) / count );
}
convertTime( time ) {
if ( time <= 0 ) return 0;
const seconds = parseInt( ( time / 1000 ) % 60 );
const minutes = parseInt( ( time / ( 1000 * 60 ) ) );
return minutes + ':' + ( seconds < 10 ? '0' : '' ) + seconds;
}
}
class Storage {
constructor( game ) {
this.game = game;
}
init() {
this.loadGame();
this.loadPreferences();
}
loadGame() {
this.game.saved = false;
}
loadPreferences() {
this.game.controls.flipConfig = 0;
this.game.scrambler.scrambleLength = 20;
this.game.world.fov = 10;
this.game.world.resize();
this.game.themes.setTheme( 'cube' );
return false;
}
}
class Themes {
constructor( game ) {
this.game = game;
this.theme = null;
this.colors = {
cube: {
U: 0xfff7ff,
D: 0xffef48,
F: 0xef3923,
R: 0x41aac8,
B: 0xff8c0a,
L: 0x82ca38,
P: 0x08101a,
G: 0xd1d5db,
},
erno: {
U: 0xffffff,
D: 0xffd500,
F: 0xc41e3a,
R: 0x0051ba,
B: 0xff5800,
L: 0x009e60,
P: 0x111111,
G: 0x8abdff,
},
dust: {
U: 0xfff6eb,
D: 0xe7c48d,
F: 0x8f253e,
R: 0x607e69,
B: 0xbe6f62,
L: 0x849f5d,
P: 0x111111,
G: 0xE7C48D,
},
camo: {
U: 0xfff6eb,
D: 0xbfb672,
F: 0x805831,
R: 0x718456,
B: 0x37241c,
L: 0x37431d,
P: 0x111111,
G: 0xBFB672,
},
rain: {
U: 0xfafaff,
D: 0xedb92d,
F: 0xce2135,
R: 0x449a89,
B: 0xec582f,
L: 0xa3a947,
P: 0x111111,
G: 0x87b9ac,
},
};
}
setTheme( theme ) {
if ( theme === this.theme ) return;
this.theme = theme;
const colors = this.colors[ this.theme ];
this.game.cube.pieces.forEach( piece => {
piece.userData.cube.material.color.setHex( colors.P );
} );
this.game.cube.edges.forEach( edge => {
edge.material.color.setHex( colors[ edge.name ] );
} );
this.game.dom.rangeHandles.forEach( handle => {
handle.style.background = '#' + colors.R.toString(16).padStart(6, '0');
} );
this.game.confetti.updateColors( colors );
this.game.dom.back.style.background = '#' + colors.G.toString(16).padStart(6, '0');
this.game.dom.buttons.pwa.style.color = '#' + colors.R.toString(16).padStart(6, '0');
}
}
class IconsConverter {
constructor( options ) {
options = Object.assign( {
tagName: 'icon',
className: 'icon',
styles: false,
icons: {},
observe: false,
convert: false,
}, options || {} );
this.tagName = options.tagName;
this.className = options.className;
this.icons = options.icons;
this.svgTag = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
this.svgTag.setAttribute( 'class', this.className );
if ( options.styles ) this.addStyles();
if ( options.convert ) this.convertAllIcons();
if ( options.observe ) {
const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
this.observer = new MutationObserver( mutations => { this.convertAllIcons(); } );
this.observer.observe( document.documentElement, { childList: true, subtree: true } );
}
return this;
}
convertAllIcons() {
document.querySelectorAll( this.tagName ).forEach( icon => { this.convertIcon( icon ); } );
}
convertIcon( icon ) {
const svgData = this.icons[ icon.attributes[0].localName ];
if ( typeof svgData === 'undefined' ) return;
const svg = this.svgTag.cloneNode( true );
const viewBox = svgData.viewbox.split( ' ' );
svg.setAttributeNS( null, 'viewBox', svgData.viewbox );
svg.style.width = viewBox[2] / viewBox[3] + 'em';
svg.style.height = '1em';
svg.innerHTML = svgData.content;
icon.parentNode.replaceChild( svg, icon );
}
addStyles() {
const style = document.createElement( 'style' );
style.innerHTML = `.${this.className} { display: inline-block; font-size: inherit; overflow: visible; vertical-align: -0.125em; preserveAspectRatio: none; }`;
document.head.appendChild( style );
}
}
const Icons = new IconsConverter( {
icons: {
settings: {
viewbox: '0 0 512 512',
content: '<path fill="currentColor" d="M444.788 291.1l42.616 24.599c4.867 2.809 7.126 8.618 5.459 13.985-11.07 35.642-29.97 67.842-54.689 94.586a12.016 12.016 0 0 1-14.832 2.254l-42.584-24.595a191.577 191.577 0 0 1-60.759 35.13v49.182a12.01 12.01 0 0 1-9.377 11.718c-34.956 7.85-72.499 8.256-109.219.007-5.49-1.233-9.403-6.096-9.403-11.723v-49.184a191.555 191.555 0 0 1-60.759-35.13l-42.584 24.595a12.016 12.016 0 0 1-14.832-2.254c-24.718-26.744-43.619-58.944-54.689-94.586-1.667-5.366.592-11.175 5.459-13.985L67.212 291.1a193.48 193.48 0 0 1 0-70.199l-42.616-24.599c-4.867-2.809-7.126-8.618-5.459-13.985 11.07-35.642 29.97-67.842 54.689-94.586a12.016 12.016 0 0 1 14.832-2.254l42.584 24.595a191.577 191.577 0 0 1 60.759-35.13V25.759a12.01 12.01 0 0 1 9.377-11.718c34.956-7.85 72.499-8.256 109.219-.007 5.49 1.233 9.403 6.096 9.403 11.723v49.184a191.555 191.555 0 0 1 60.759 35.13l42.584-24.595a12.016 12.016 0 0 1 14.832 2.254c24.718 26.744 43.619 58.944 54.689 94.586 1.667 5.366-.592 11.175-5.459 13.985L444.788 220.9a193.485 193.485 0 0 1 0 70.2zM336 256c0-44.112-35.888-80-80-80s-80 35.888-80 80 35.888 80 80 80 80-35.888 80-80z" class=""></path>',
},
back: {
viewbox: '0 0 512 512',
content: '<path transform="translate(512, 0) scale(-1,1)" fill="currentColor" d="M503.691 189.836L327.687 37.851C312.281 24.546 288 35.347 288 56.015v80.053C127.371 137.907 0 170.1 0 322.326c0 61.441 39.581 122.309 83.333 154.132 13.653 9.931 33.111-2.533 28.077-18.631C66.066 312.814 132.917 274.316 288 272.085V360c0 20.7 24.3 31.453 39.687 18.164l176.004-152c11.071-9.562 11.086-26.753 0-36.328z" class=""></path>',
},
trophy: {
viewbox: '0 0 576 512',
content: '<path fill="currentColor" d="M552 64H448V24c0-13.3-10.7-24-24-24H152c-13.3 0-24 10.7-24 24v40H24C10.7 64 0 74.7 0 88v56c0 66.5 77.9 131.7 171.9 142.4C203.3 338.5 240 360 240 360v72h-48c-35.3 0-64 20.7-64 56v12c0 6.6 5.4 12 12 12h296c6.6 0 12-5.4 12-12v-12c0-35.3-28.7-56-64-56h-48v-72s36.7-21.5 68.1-73.6C498.4 275.6 576 210.3 576 144V88c0-13.3-10.7-24-24-24zM64 144v-16h64.2c1 32.6 5.8 61.2 12.8 86.2-47.5-16.4-77-49.9-77-70.2zm448 0c0 20.2-29.4 53.8-77 70.2 7-25 11.8-53.6 12.8-86.2H512v16zm-127.3 4.7l-39.6 38.6 9.4 54.6c1.7 9.8-8.7 17.2-17.4 12.6l-49-25.8-49 25.8c-8.8 4.6-19.1-2.9-17.4-12.6l9.4-54.6-39.6-38.6c-7.1-6.9-3.2-19 6.7-20.5l54.8-8 24.5-49.6c4.4-8.9 17.1-8.9 21.5 0l24.5 49.6 54.8 8c9.6 1.5 13.5 13.6 6.4 20.5z" class=""></path>',
},
share: {
viewbox: '0 0 36 50',
content: '<path fill="currentColor" d="M19,4.414L19,32C19,32.552 18.552,33 18,33C17.448,33 17,32.552 17,32L17,4.414L10.707,10.707C10.317,11.098 9.683,11.098 9.293,10.707C8.902,10.317 8.902,9.683 9.293,9.293L18,0.586L26.707,9.293C27.098,9.683 27.098,10.317 26.707,10.707C26.317,11.098 25.683,11.098 25.293,10.707L19,4.414ZM34,18L26,18C25.448,18 25,17.552 25,17C25,16.448 25.448,16 26,16L36,16L36,50L0,50L0,16L10,16C10.552,16 11,16.448 11,17C11,17.552 10.552,18 10,18L2,18L2,48L34,48L34,18Z" />',
},
pwa: {
viewbox: '0 0 740 280',
content: '<path d="M544.62 229.7L565.998 175.641H627.722L598.43 93.6366L635.066 0.988922L740 279.601H662.615L644.683 229.7H544.62V229.7Z" fill="#3d3d3d"/><path d="M478.6 279.601L590.935 0.989288H516.461L439.618 181.035L384.974 0.989655H327.73L269.058 181.035L227.681 98.9917L190.236 214.352L228.254 279.601H301.545L354.565 118.139L405.116 279.601H478.6V279.601Z" fill="currentColor"/><path d="M70.6927 183.958H116.565C130.46 183.958 142.834 182.407 153.685 179.305L165.548 142.757L198.704 40.6105C196.177 36.6063 193.293 32.8203 190.051 29.2531C173.028 10.4101 148.121 0.988861 115.33 0.988861H0V279.601H70.6927V183.958V183.958ZM131.411 65.0863C138.061 71.7785 141.385 80.7339 141.385 91.9534C141.385 103.259 138.461 112.225 132.614 118.853C126.203 126.217 114.399 129.898 97.2023 129.898H70.6927V55.0474H97.3972C113.424 55.0474 124.762 58.3937 131.411 65.0863V65.0863Z" fill="#3d3d3d"/>',
}
},
convert: true,
} );
const MENU = 0;
const PLAYING = 1;
const COMPLETE = 2;
const STATS = 3;
const PREFS = 4;
const SHOW = true;
const HIDE = false;
class Game {
constructor() {
const qs = document.querySelector.bind( document );
const qsa = document.querySelectorAll.bind( document );
this.dom = {
ui: qs( '.ui' ),
game: qs( '.ui__game' ),
back: qs( '.ui__background' ),
texts: qs( '.ui__texts' ),
prefs: qs( '.ui__prefs' ),
stats: qs( '.ui__stats' ),
texts: {
title: qs( '.text--title' ),
note: qs( '.text--note' ),
timer: qs( '.text--timer' ),
stats: qs( '.text--timer' ),
complete: qs( '.text--complete' ),
best: qs( '.text--best-time' ),
},
buttons: {
prefs: qs( '.btn--prefs' ),
back: qs( '.btn--back' ),
stats: qs( '.btn--stats' ),
pwa: qs( '.btn--pwa' ),
},
rangeHandles: qsa( '.range__handle div' ),
};
this.world = new World( this );
this.cube = new Cube( this );
this.controls = new Controls( this );
this.scrambler = new Scrambler( this );
this.transition = new Transition( this );
this.timer = new Timer( this );
this.preferences = new Preferences( this );
this.scores = new Scores( this );
this.storage = new Storage( this );
this.confetti = new Confetti( this );
this.themes = new Themes( this );
this.initActions();
this.state = MENU;
this.saved = false;
this.newGame = false;
this.storage.init();
this.preferences.init();
this.transition.init();
this.scores.calcStats();
setTimeout( () => {
this.transition.float();
this.transition.cube( SHOW );
setTimeout( () => this.transition.title( SHOW ), 700 );
setTimeout( () => this.transition.buttons( [ 'prefs', 'pwa' ], [] ), 1000 );
}, 500 );
}
initActions() {
let tappedTwice = false;
this.dom.game.onclick = event => {
if ( this.transition.activeTransitions > 0 ) return;
if ( this.state === PLAYING ) return;
if ( this.state === MENU ) {
if ( ! tappedTwice ) {
tappedTwice = true;
setTimeout( () => tappedTwice = false, 300 );
return false;
}
if ( ! this.saved ) {
this.scrambler.scramble();
this.controls.scrambleCube();
this.newGame = true;
}
const duration = this.saved ? 0 : this.scrambler.converted.length * this.controls.flipSpeeds[0];
this.state = PLAYING;
this.saved = true;
this.transition.buttons( [], [ 'pwa', 'prefs' ] );
this.transition.zoom( PLAYING, duration );
this.transition.title( HIDE );
setTimeout( () => {
this.transition.timer( SHOW );
this.transition.buttons( [ 'back' ], [] );
}, this.transition.durations.zoom - 1000 );
setTimeout( () => {
this.controls.enable();
if ( ! this.newGame ) this.timer.start( true );
}, this.transition.durations.zoom );
} else if ( this.state === COMPLETE ) {
this.state = STATS;
this.saved = false;
this.transition.timer( HIDE );
this.transition.complete( HIDE, this.bestTime );
this.transition.cube( HIDE );
this.timer.reset();
setTimeout( () => {
this.cube.reset();
this.confetti.stop();
this.transition.stats( SHOW );
this.transition.elevate( 0 );
}, 1000 );
return false;
} else if ( this.state === STATS ) {
this.state = MENU;
this.transition.buttons( [ 'pwa', 'prefs' ], [] );
this.transition.stats( HIDE );
setTimeout( () => this.transition.cube( SHOW ), 500 );
setTimeout( () => this.transition.title( SHOW ), 1200 );
}
};
this.controls.onMove = () => {
if ( this.newGame ) {
this.timer.start( true );
this.newGame = false;
}
};
this.dom.buttons.back.onclick = event => {
if ( this.transition.activeTransitions > 0 ) return;
if ( this.state === PREFS ) {
this.state = MENU;
this.transition.buttons( [ 'pwa', 'prefs' ], [ 'back' ] );
this.transition.preferences( HIDE );
setTimeout( () => this.transition.cube( SHOW ), 500 );
setTimeout( () => this.transition.title( SHOW ), 1200 );
} else if ( this.state === PLAYING ) {
this.state = MENU;
this.transition.buttons( [ 'pwa', 'prefs' ], [ 'back' ] );
this.transition.zoom( MENU, 0 );
this.controls.disable();
if ( ! this.newGame ) this.timer.stop();
this.transition.timer( HIDE );
setTimeout( () => this.transition.title( SHOW ), this.transition.durations.zoom - 1000 );
this.playing = false;
this.controls.disable();
}
};
this.dom.buttons.prefs.onclick = event => {
if ( this.transition.activeTransitions > 0 ) return;
this.state = PREFS;
this.transition.buttons( [ 'back' ], [ 'pwa', 'prefs' ] );
this.transition.title( HIDE );
this.transition.cube( HIDE );
setTimeout( () => this.transition.preferences( SHOW ), 1000 );
};
this.dom.buttons.stats.onclick = event => {
if ( this.transition.activeTransitions > 0 ) return;
this.state = STATS;
this.transition.buttons( [], [ 'pwa', 'prefs' ] );
this.transition.title( HIDE );
this.transition.cube( HIDE );
setTimeout( () => this.transition.stats( SHOW ), 1000 );
};
this.controls.onSolved = () => {
this.transition.buttons( [], [ 'back' ] );
this.state = COMPLETE;
this.saved = false;
this.controls.disable();
this.timer.stop();
this.bestTime = this.scores.addScore( this.timer.deltaTime );
this.transition.zoom( MENU, 0 );
this.transition.elevate( SHOW );
setTimeout( () => {
this.transition.complete( SHOW, this.bestTime );
this.confetti.start();
}, 1000 );
};
}
}
window.game = new Game();
Three.js官网: Three.js – JavaScript 3D Library 或 https://github.com/mrdoob/three.js
three.js官网中文文档three.js docs
Threejs中文网Three.js中文网
*, *:before, *:after {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
box-sizing: border-box;
cursor: inherit;
margin: 0;
padding: 0;
outline: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
font-style: inherit;
text-transform: uppercase;
}
*:focus {
outline: none;
}
html {
-webkit-tap-highlight-color: transparent;
-webkit-text-size-adjust: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
overflow: hidden;
height: 100%;
}
body {
font-family: "BungeeFont", sans-serif;
font-weight: normal;
font-style: normal;
line-height: 1;
cursor: default;
overflow: hidden;
height: 100%;
font-size: 5rem;
}
.icon {
display: inline-block;
font-size: inherit;
overflow: visible;
vertical-align: -0.125em;
preserveAspectRatio: none;
}
.range {
position: relative;
width: 14em;
z-index: 1;
opacity: 0;
}
.range:not(:last-child) {
margin-bottom: 2em;
}
.range__label {
position: relative;
font-size: 0.9em;
line-height: 0.75em;
padding-bottom: 0.5em;
z-index: 2;
}
.range__track {
position: relative;
height: 1em;
margin-left: 0.5em;
margin-right: 0.5em;
z-index: 3;
}
.range__track-line {
position: absolute;
background: rgba(0, 0, 0, 0.2);
height: 2px;
top: 50%;
margin-top: -1px;
left: -0.5em;
right: -0.5em;
transform-origin: left center;
}
.range__handle {
position: absolute;
width: 0;
height: 0;
top: 50%;
left: 0;
cursor: pointer;
z-index: 1;
}
.range__handle div {
transition: background 500ms ease;
position: absolute;
left: 0;
top: 0;
width: 0.9em;
height: 0.9em;
border-radius: 0.2em;
margin-left: -0.45em;
margin-top: -0.45em;
background: #41aac8;
border-bottom: 2px solid rgba(0, 0, 0, 0.2);
}
.range.is-active .range__handle div {
transform: scale(1.25);
}
.range__handle:after {
content: "";
position: absolute;
left: 0;
top: 0;
width: 3em;
height: 3em;
margin-left: -1.5em;
margin-top: -1.5em;
}
.range__list {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
position: relative;
padding-top: 0.5em;
font-size: 0.55em;
color: rgba(0, 0, 0, 0.5);
z-index: 1;
}
.stats {
position: relative;
width: 14em;
z-index: 1;
display: flex;
justify-content: space-between;
opacity: 0;
}
.stats:not(:last-child) {
margin-bottom: 1.5em;
}
.stats i {
display: inline-block;
color: rgba(0, 0, 0, 0.5);
font-size: 0.9em;
}
.stats b {
display: inline-block;
font-size: 0.9em;
}
.text {
position: absolute;
left: 0;
right: 0;
text-align: center;
line-height: 0.75;
perspective: 100rem;
opacity: 0;
}
.text i {
display: inline-block;
opacity: 0;
white-space: pre-wrap;
}
.text--title {
bottom: 75%;
font-size: 4.4em;
height: 1.2em;
}
.text--title span {
display: block;
}
.text--title span:first-child {
font-size: 0.5em;
margin-bottom: 0.2em;
}
.text--note {
top: 87%;
font-size: 1em;
}
.text--timer {
bottom: 78%;
font-size: 3.5em;
line-height: 1;
}
.text--complete, .text--best-time {
font-size: 1.5em;
top: 83%;
line-height: 1em;
}
.btn {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: transparent;
border-radius: 0;
border-width: 0;
position: absolute;
pointer-events: none;
font-size: 1.2em;
color: rgba(0, 0, 0, 0.25);
opacity: 0;
}
.btn:after {
position: absolute;
content: "";
width: 3em;
height: 3em;
left: 50%;
top: 50%;
margin-left: -1.5em;
margin-top: -1.5em;
border-radius: 100%;
}
.btn--bl {
bottom: 0.8em;
left: 0.8em;
}
.btn--br {
bottom: 0.8em;
right: 0.8em;
}
.btn--bc {
bottom: 0.8em;
left: calc(50% - 0.5em);
}
.btn--pwa {
transition: color 500ms ease;
color: inherit;
height: 1em;
}
.btn--pwa svg {
font-size: 0.6em;
margin: 0.35em 0;
}
.btn svg {
display: block;
}
.ui {
pointer-events: none;
color: #070d15;
}
.ui, .ui__background, .ui__game, .ui__texts, .ui__prefs, .ui__stats, .ui__buttons {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
.ui__background {
z-index: 1;
transition: background 500ms ease;
background: #d1d5db;
}
.ui__background:after {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
content: "";
background-image: linear-gradient(to bottom, white 50%, rgba(255, 255, 255, 0) 100%);
}
.ui__game {
pointer-events: all;
z-index: 2;
}
.ui__game canvas {
display: block;
width: 100%;
height: 100%;
}
.ui__texts {
z-index: 3;
}
.ui__prefs, .ui__stats {
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: center;
overflow: hidden;
z-index: 4;
}
.ui__buttons {
z-index: 5;
}
.ui__notification {
transition: transform 500ms ease, opacity 500ms ease;
font-family: sans-serif;
position: absolute;
left: 50%;
bottom: 0.6em;
padding: 0.6em;
margin-left: -9.4em;
width: 18.8em;
z-index: 6;
background: rgba(17, 17, 17, 0.9);
border-radius: 0.8em;
display: flex;
align-items: center;
flex-flow: row nowrap;
color: #fff;
user-select: none;
opacity: 0;
pointer-events: none;
transform: translateY(100%);
}
.ui__notification.is-active {
opacity: 1;
pointer-events: all;
transform: none;
}
.ui__notification * {
text-transform: none;
}
.ui__notification-icon {
background-size: 100% 100%;
left: 0.6em;
top: 0.6em;
width: 2.8em;
height: 2.8em;
border-radius: 0.5em;
background: #fff;
margin-right: 0.6em;
display: block;
}
.ui__notification-text {
font-size: 0.75em;
line-height: 1.4em;
}
.ui__notification-text .icon {
color: #4f82fd;
font-size: 1.1em;
}
.ui__notification-text b {
font-weight: 700;
}
.btn--stats {
visibility: hidden;
}