- ECMAScript5 compatible JavaScript
- WebGL enabled canvas
Created
February 1, 2012 04:13
-
-
Save bellbind/1715047 to your computer and use it in GitHub Desktop.
[html5][threejs][webgl][javascript]3D view samegame
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var ThreeView = function (width, height) { | |
// for filling (width, height) area, use camera.z = width * 2 | |
var angle = 2 * (Math.atan(1/4 * 1.2) / Math.PI * 180); | |
// angle may be around 30deg | |
var aspect = width / height; | |
var near = width; | |
var far = width * 3; | |
var scoreOpts = {fontSize: width / 16, color: "rgba(255, 255, 0, 0.3)", | |
fontFamily: "monospaced"}; | |
return Object.create(ThreeView.prototype, { | |
scene: {value: new THREE.Scene()}, | |
camera: {value: new THREE.PerspectiveCamera(angle, aspect, near, far)}, | |
picker: {value: new Picker(width, height)}, | |
tweener: {value: new Tweener()}, | |
textGameClear: {value: textMesh( | |
"conguratulation", {fontSize: 0|width / 8, color: "white"})}, | |
textGameOver: {value: textMesh( | |
"game over", {fontSize: 0|width / 8, color: "red"})}, | |
scoreOpts: {value: scoreOpts}, | |
textScore: {value: textMesh("score: 00000000", scoreOpts)}, | |
newGameButton: {value: {}}, | |
width: {value: width, writable: true}, | |
height: {value: height, writable: true}, | |
gems: {value: [], writable: true}, | |
radius: {value: width / 16, writable: true}, | |
leastSelected: {value: 2, writable: true}, | |
colors: {value: [0xff0000, 0x00ff00, 0x0000ff], writable: true}, | |
board: {value: null, writable: true}, | |
selected: {value: null, writable: true}, | |
log: {value: [], writable: true}, | |
}).init(); | |
}; | |
ThreeView.prototype.constructor = ThreeView; | |
ThreeView.prototype.newGame = function () { | |
this.board = this.newBoard(); | |
this.selected = this.board.noSelect(); | |
this.log = []; | |
this.newPanel(); | |
this.updatePanel(); | |
return this; | |
}; | |
ThreeView.prototype.newBoard = function () { | |
var width = 0|(this.width / (this.radius)); | |
var height = 0|(this.height / (this.radius)); | |
return SameGame.newRandomBoard({ | |
colors: this.colors.length, selectedMin: this.leastSelected, | |
width: width, height: height}); | |
}; | |
ThreeView.prototype.newPanel = function () { | |
this.scene.remove(this.textCleared); | |
this.gems.forEach(function (gem) { | |
this.scene.remove(gem); | |
}, this); | |
this.gems = []; | |
var radius = this.radius; | |
this.board.forEach(function (cell) { | |
var color = this.colors[cell.gem.color]; | |
var pos = this.scenePosition(cell); | |
var geom = new THREE.SphereGeometry( | |
this.radius, 16, 16); | |
var mat = new THREE.MeshPhongMaterial({ | |
color: color, | |
opacity: 0.5, | |
transparent: true, | |
}); | |
var mesh = new THREE.Mesh(geom, mat); | |
mesh.position.x = pos.x; | |
mesh.position.y = pos.y; | |
this.gems.push(mesh); | |
this.scene.add(mesh); | |
mesh.gem = cell.gem; | |
}, this); | |
}; | |
ThreeView.prototype.scenePosition = function (cell) { | |
return { | |
x: 2 * this.radius * (cell.x - this.board.width / 2) + this.radius, | |
y: 2 * this.radius * (cell.y - this.board.height / 2) + this.radius}; | |
}; | |
ThreeView.prototype.init = function () { | |
this.camera.position.z = this.width * 2; | |
this.scene.add(this.camera); | |
this.textGameClear.position.z = this.width / 2; | |
this.textGameClear.visible = false; | |
this.scene.add(this.textGameClear); | |
this.textGameOver.position.z = this.width / 2; | |
this.textGameOver.visible = false; | |
this.scene.add(this.textGameOver); | |
this.textScore.position.z = this.width / 2; | |
this.textScore.position.x = this.width / 3; | |
this.textScore.position.y = -2 * this.height / 3; | |
this.scene.add(this.textScore); | |
var light = new THREE.PointLight(0xffffff); | |
light.position.set(this.width, this.width, this.width); | |
this.scene.add(light); | |
this.prepareNewGameButton(); | |
this.newGame(); | |
return this; | |
}; | |
ThreeView.prototype.prepareNewGameButton = function () { | |
var unit = this.width / 16; | |
var buttonGeom = new THREE.CylinderGeometry( | |
unit * 1.6, unit * 1.8, unit / 10, 16); | |
var buttonMat = new THREE.MeshPhongMaterial({ | |
color: 0x333333, | |
opacity: 0.75, | |
}); | |
var buttonMesh = new THREE.Mesh(buttonGeom, buttonMat); | |
buttonMesh.position.z = this.width / 2; | |
buttonMesh.position.y = this.height / 2; | |
buttonMesh.position.x = this.width / 2; | |
buttonMesh.rotation.x = Math.PI/2; | |
var path = new THREE.Path(); | |
path.moveTo(unit, 0); | |
path.lineTo(-unit, unit); | |
path.lineTo(-unit, -unit); | |
path.lineTo(unit, 0); | |
var shapes = path.toShapes(); | |
var markGeom = new THREE.ExtrudeGeometry(shapes, { | |
amount: 1, bevelThickness: unit / 20}); | |
var markMat = new THREE.MeshPhongMaterial({ | |
color: 0xff0000, | |
opacity: 1, | |
}); | |
var markMesh = new THREE.Mesh(markGeom, markMat); | |
markMesh.position.y = unit / 10; | |
markMesh.rotation.x = -Math.PI /2; | |
buttonMesh.add(markMesh); | |
buttonMesh.visible = false; | |
markMesh.visible = false; | |
this.newGameButton.button = buttonMesh; | |
this.newGameButton.mark = markMesh; | |
this.newGameButton.mesh = [buttonMesh, markMesh]; | |
this.scene.add(buttonMesh); | |
}; | |
ThreeView.prototype.selectedBy = function (ev) { | |
var picked = this.picker.picking(ev, this.camera, this.gems); | |
if (picked.length > 0) { | |
var mesh = picked[0]; | |
var pos = this.board.getPos(mesh.gem); | |
if (pos) return this.board.select(pos.x, pos.y); | |
} | |
return this.board.noSelect(); | |
}; | |
ThreeView.prototype.updatePanel = function () { | |
var now = new Date(); | |
var newGems = []; | |
this.gems.forEach(function (mesh) { | |
var cell = this.board.getPos(mesh.gem); | |
if (cell === null) { | |
this.scene.remove(mesh); | |
return; | |
} | |
newGems.push(mesh); | |
var pos = this.scenePosition(cell); | |
if (!mesh.tween || mesh.tween.finished) { | |
mesh.tween = new Tween( | |
300, | |
{x: mesh.position.x, y: mesh.position.y}, | |
{x: mesh.position.x, y: mesh.position.y}, | |
mesh.position); | |
this.tweener.add(mesh.tween); | |
} | |
mesh.tween.begin.x = mesh.position.x; | |
mesh.tween.begin.y = mesh.position.y; | |
mesh.tween.end.x = pos.x; | |
mesh.tween.end.y = pos.y; | |
mesh.tween.start(now); | |
mesh.material.wireframe = this.selected.has(cell.x, cell.y); | |
}, this); | |
this.gems = newGems; | |
this.updateScore(); | |
var isGameClear = this.board.isGameClear(); | |
var isGameOver = this.board.isGameOver(); | |
this.textGameClear.visible = isGameClear | |
this.textGameOver.visible = !isGameClear && isGameOver; | |
this.newGameButton.mesh.forEach(function (m) { | |
m.visible = isGameOver; | |
}); | |
}; | |
ThreeView.prototype.updateScore = function () { | |
var score = this.score().toString(); | |
var text = "score: "; | |
for (var i = score.length; i < 8; i++) { | |
text += "0"; | |
} | |
text += score; | |
updateTextMesh(this.textScore, text, this.scoreOpts); | |
}; | |
ThreeView.prototype.score = function (ev) { | |
return this.log.reduce(function (sum, selected) { | |
return sum + selected.count() * selected.count(); | |
}, 0) * 100 * (this.board.isGameClear() ? 10 : 1); | |
}; | |
ThreeView.prototype.onHover = function (ev) { | |
this.selected = this.selectedBy(ev); | |
this.updatePanel(); | |
}; | |
ThreeView.prototype.onOut = function (ev) { | |
this.selected = this.board.noSelect(); | |
this.updatePanel(); | |
}; | |
ThreeView.prototype.onSelect = function (ev) { | |
this.selected = this.selectedBy(ev); | |
if (this.selected.count() === 0) return; | |
this.board = this.board.remove(this.selected); | |
this.log.push(this.selected); | |
this.selected = this.board.noSelect(); | |
this.board = this.board.shrink(); | |
this.updatePanel(); | |
}; | |
ThreeView.prototype.onPush = function (ev) { | |
var picked = this.picker.picking(ev, this.camera, this.newGameButton.mesh); | |
if (picked.length > 0 && this.board.isGameOver()) { | |
this.newGame(); | |
} | |
}; | |
ThreeView.prototype.update = function () { | |
this.tweener.update(); | |
}; | |
window.addEventListener("load", function () { | |
var view = new ThreeView(800, 450); | |
var renderer = new THREE.WebGLRenderer( | |
{antialias: true, clearColor: 0x333333, clearAlpha: 1}); | |
renderer.setSize(view.width, view.height); | |
document.body.appendChild(renderer.domElement); | |
renderer.domElement.addEventListener( | |
"mousemove", view.onHover.bind(view), false); | |
renderer.domElement.addEventListener( | |
"mouseup", view.onSelect.bind(view), false); | |
renderer.domElement.addEventListener( | |
"mouseup", view.onPush.bind(view), false); | |
renderer.domElement.addEventListener( | |
"mouseout", view.onOut.bind(view), false); | |
//var control = new THREE.TrackballControls( | |
// view.camera, renderer.domElement); | |
var render = function () { | |
//control.update(); | |
view.update(); | |
renderer.render(view.scene, view.camera); | |
requestAnimationFrame(render); | |
}; | |
render(); | |
}, false); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// additional library for Three.js | |
// missing Function.prototype.bind for current WebKit | |
Function.prototype.bind = (Function.prototype.bind || function bind(thisp) { | |
// it is not compatible to ES5 bind when use bind with "new" operator | |
var func = this; | |
var bounds = Array.prototype.slice.call(arguments, 1); | |
var curry = function () { | |
var args = bounds.concat(Array.prototype.slice.call(arguments)); | |
var callAsNew = this instanceof curry; | |
return func.apply(callAsNew ? this : (thisp || this), args); | |
}; | |
var pad = function () {}; | |
pad.prototype = func.prototype; | |
curry.prototype = new pad(); | |
return curry; | |
}); | |
// screen text | |
var textMesh = function (text, opts) { | |
var fontSize = opts.fontSize || 100; | |
var color = opts.color || "white"; | |
var fontFamily = opts.fontFamily || "sans-serif"; | |
var canvas = document.createElement("canvas"); | |
var c2d = canvas.getContext("2d"); | |
var font = fontSize + "pt " + fontFamily; | |
c2d.font = font; | |
canvas.width = c2d.measureText(text).width; | |
canvas.height = 0 | fontSize * 2; | |
c2d.font = font; | |
c2d.textAlign = "center"; | |
c2d.textBaseline = "middle"; | |
c2d.fillStyle = color; | |
c2d.fillText(text, canvas.width / 2, canvas.height / 2); | |
var texture = new THREE.Texture(canvas); | |
texture.needsUpdate = true; | |
var mat = new THREE.MeshBasicMaterial({map: texture, transparent: true}); | |
var geom = new THREE.PlaneGeometry(canvas.width, canvas.height); | |
var mesh = new THREE.Mesh(geom, mat); | |
return mesh; | |
}; | |
var updateTextMesh = function (mesh, text, opts) { | |
var texture = mesh.material.map; | |
var canvas = texture.image; | |
var fontSize = opts.fontSize || 100; | |
var color = opts.color || "white"; | |
var fontFamily = opts.fontFamily || "sans-serif"; | |
var c2d = canvas.getContext("2d"); | |
var font = fontSize + "pt " + fontFamily; | |
c2d.clearRect(0, 0, canvas.width, canvas.height); | |
c2d.font = font; | |
c2d.textAlign = "center"; | |
c2d.textBaseline = "middle"; | |
c2d.fillStyle = color; | |
c2d.fillText(text, canvas.width / 2, canvas.height / 2); | |
texture.needsUpdate = true; | |
}; | |
// picking utility | |
var Picker = function (screenWidth, screenHeight) { | |
return Object.create(Picker.prototype, { | |
width: {value: screenWidth}, | |
height: {value: screenHeight}, | |
projector: {value: new THREE.Projector()}, | |
}); | |
}; | |
Picker.prototype.constructor = Picker; | |
Picker.prototype.picking = function (mouseEvent, camera, meshes) { | |
var rect = mouseEvent.target.getBoundingClientRect(); | |
var targetX = mouseEvent.clientX - rect.left; | |
var targetY = mouseEvent.clientY - rect.top; | |
// convert (targetX, targetY) to 3D (x, y) | |
// 0(left) <= targetX <= width(right), 0(top) <= targetY <= height(bottom) | |
// -1.0(left) <= x <= 1.0(right), -1.0(bottom) <= y <= 1.0(top) | |
var x = 2 * targetX / this.width - 1; | |
var y = -(2 * targetY / this.height - 1); | |
//[note] of projector.pickingRay(vec3, camera) | |
// vec3.xy is just 2D point of the front of rendering area: -1 to 1 cube | |
// vec3.z is reserved to modify in the method | |
var ray = this.projector.pickingRay(new THREE.Vector3(x, y), camera); | |
return ray.intersectObjects(meshes).map(function (intersector) { | |
return intersector.object; | |
}); | |
}; | |
// simple tween animation | |
var Tween = function (msec, begin, end, to, timing) { | |
return Object.create(Tween.prototype, { | |
begin: {value: begin}, | |
end: {value: end}, | |
span: {value: msec}, | |
to: {value: to}, | |
//timing: {value: timing || Tween.linear}, | |
timing: {value: timing || Tween.ease}, | |
startAt: {value: null, writable: true}, | |
finished: {value: false, writable: true}, | |
}); | |
}; | |
Tween.prototype.constructor = Tween; | |
Tween.prototype.start = function (at) { | |
this.startAt = at || new Date(); | |
return this; | |
}; | |
Tween.prototype.update = function (at) { | |
if (!this.startAt) return this; | |
if (this.finished) return this; | |
at = at || new Date(); | |
var msec = at - this.startAt; | |
var t = Math.min(1, msec / this.span); | |
// linear interpolation | |
Object.keys(this.end).forEach(function (key) { | |
this.to[key] = this.timing(t, this.begin[key], this.end[key]); | |
}, this); | |
if (t === 1) this.finished; | |
return this; | |
}; | |
Tween.linear = function (t, p0, p3) { | |
return p0 * (1 - t) + p3 * t; | |
}; | |
// same implementation as "cubic-bezier" of CSS transition timing function | |
Tween.bezier = function (p1u, p2u, p1v, p2v) { | |
// cubic bezier with (p0u, p3u) = (0, 1): | |
// u(t) = 0*(1-t)^3 + p1u*3*(1-t)^2*t + p2u*3*(1-t)*t^2 + 1*t^3 | |
// = (1 + 3*p1u - 3*p2u)*t^3 + (-2*3*p1u + 3*p2u)*t^2 + 3*p1u*t | |
// = au*t^3 + bu*t^2 + cu*t | |
var au = 1 + 3*p1u - 3*p2u; | |
var bu = 3*p2u - 6*p1u; | |
var cu = 3*p1u; | |
var av = 1 + 3*p1v - 3*p2v; | |
var bv = 3*p2v - 6*p1v; | |
var cv = 3*p1v; | |
var ut = function (t) { | |
return au*t*t*t + bu*t*t + cu*t; | |
}; | |
var vt = function (t) { | |
return av*t*t*t + bv*t*t + cv*t; | |
}; | |
// d*u(t)/dt = 3*au*t^2 + 2*bu*t + cu | |
var dut = function (t) { | |
return 3*au*t*t + 2*bu*t + cu; | |
}; | |
// find st from t. the st satisfies: u(st) = t | |
var findT = function (t) { | |
if (t < 0) return 0; | |
if (t > 1) return 1; | |
var eps = 0.001; | |
// newton method for: u(st) - t = 0 | |
var st = t; | |
for (var i = 0; i < 8; i++) { | |
var u = ut(st) - t; | |
if (Math.abs(u) < eps) return st; | |
// d*(u(st)-t)/dst = d*u(st)/dst | |
var du = dut(st); | |
if (Math.abs(du) < 0.0000001) break; | |
st = st - u / du; | |
} | |
// fallback: find by binary search | |
var tl = 0, tr = 1; | |
var st = t; | |
while (tl < tr) { | |
var u = ut(st); | |
if (Math.abs(u - t) < eps) return st; | |
if (t > u) { | |
tl = st; | |
} else { | |
tr = st; | |
} | |
st = (tr - tl) / 2 + tl; | |
} | |
return st; | |
}; | |
return function (t, p0, p3) { | |
var s = vt(findT(t)); | |
return p0 * (1 - s) + p3 * s; | |
}; | |
}; | |
// same as css builtin timing functions | |
Tween.ease = Tween.bezier(0.25, 0.1, 0.25, 1); | |
Tween.easeIn = Tween.bezier(0.42, 0, 1, 1); | |
Tween.easeOut = Tween.bezier(0, 0, 0.58, 1); | |
Tween.easeInOut = Tween.bezier(0.42, 0, 0.58, 1); | |
// manager of tweens | |
var Tweener = function () { | |
return Object.create(Tweener.prototype, { | |
tweens: {value: [], writable: true}, | |
}); | |
}; | |
Tweener.prototype.constructor = Tweener; | |
Tweener.prototype.add = function (tween) { | |
this.tweens.push(tween); | |
}; | |
Tweener.prototype.update = function (at) { | |
at = at || new Date(); | |
this.tweens.forEach(function (tween) { | |
tween.update(at); | |
}); | |
this.tweens = this.tweens.filter(function (tween) { | |
return !tween.finished; | |
}); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html> | |
<head> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge;chrome=1" /> | |
<title>Three.js SameGame</title> | |
<script src="Three.js"></script> | |
<script src="utils.js"></script> | |
<script src="samegame-rules.js"></script> | |
<script src="ui.js"></script> | |
</head> | |
<body> | |
<h1>ES5 SameGame: Three.js/WebGL version</h1> | |
<div> | |
source code: <a href="https://gist.github.com/1715047">gist:1715047</a> | |
</div> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
checked with: chrome, firefox, (WebGL enabled) safari