Skip to content

Instantly share code, notes, and snippets.

@bellbind
Created February 1, 2012 04:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bellbind/1715047 to your computer and use it in GitHub Desktop.
Save bellbind/1715047 to your computer and use it in GitHub Desktop.
[html5][threejs][webgl][javascript]3D view samegame

samegame with Three.js UI

required browser environment

  • ECMAScript5 compatible JavaScript
  • WebGL enabled canvas

required external resources

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);
// 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;
});
};
<!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>
@bellbind
Copy link
Author

bellbind commented Feb 1, 2012

checked with: chrome, firefox, (WebGL enabled) safari

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment