UNPKG

cannondice

Version:

3D-rendered dice for modern browsers with Cannon.js physics. Based on Teal's excellent 3D dice javascript component

450 lines 20.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var three_1 = require("three"); var cannon_1 = require("cannon"); var diceconsts_1 = require("./diceconsts"); var dicenotation_1 = require("./dicenotation"); var die_1 = require("./die"); var dicelib_1 = require("./dicelib"); var DiceBox = /** @class */ (function () { function DiceBox(diceFactory, container, dimensions) { this.diceFactory = diceFactory; this.animateSelector = true; this.barrierBodyMaterial = new cannon_1.Material('barrier'); this.deskBodyMaterial = new cannon_1.Material('desk'); this.dice = []; this.rolling = false; this.scene = new three_1.Scene(); this.useAdaptiveTimestamp = true; this.world = new cannon_1.World(); this.ambientLightColor = 0xf0f5fb; this.clientSize = new three_1.Vector2(0, 0); this.iteration = 0; this.lastTime = 0; this.running = 0; this.size = new three_1.Vector2(0, 0); this.windowSize = new three_1.Vector2(0, 0); var canvas = document.createElement('canvas'); var gl = canvas.getContext('webgl'); this.renderer = gl ? new three_1.WebGLRenderer({ antialias: true }) : new three_1.CanvasRenderer /*{ antialias: true }*/(); container.appendChild(this.renderer.domElement); if (gl) { var glRenderer = new three_1.WebGLRenderer({ antialias: true }); glRenderer.shadowMap.enabled = true; glRenderer.shadowMap.type = three_1.PCFShadowMap; glRenderer.setClearColor(0xffffff, 1); this.renderer = glRenderer; } this.reinit(container, dimensions); this.world.gravity.set(0, 0, -9.8 * 800); this.world.broadphase = new cannon_1.NaiveBroadphase(); this.world.solver.iterations = 16; var ambientLight = new three_1.AmbientLight(this.ambientLightColor); this.scene.add(ambientLight); this.world.addContactMaterial(new cannon_1.ContactMaterial(this.deskBodyMaterial, die_1.Die.diceBodyMaterial, { friction: 0.01, restitution: 0.5 })); this.world.addContactMaterial(new cannon_1.ContactMaterial(this.barrierBodyMaterial, die_1.Die.diceBodyMaterial, { friction: 0, restitution: 1.0 })); this.world.addContactMaterial(new cannon_1.ContactMaterial(die_1.Die.diceBodyMaterial, die_1.Die.diceBodyMaterial, { friction: 0, restitution: 0.5 })); var body = new cannon_1.Body({ mass: 0, material: this.deskBodyMaterial }); body.addShape(new cannon_1.Plane()); this.world.addBody(body); this.world.addBody(this.createBarrier(new cannon_1.Vec3(0, this.size.y * 0.93, 0), new cannon_1.Vec3(1, 0, 0), Math.PI / 2)); this.world.addBody(this.createBarrier(new cannon_1.Vec3(0, -this.size.y * 0.93, 0), new cannon_1.Vec3(1, 0, 0), -Math.PI / 2)); this.world.addBody(this.createBarrier(new cannon_1.Vec3(this.size.x * 0.93, 0, 0), new cannon_1.Vec3(0, 1, 0), -Math.PI / 2)); this.world.addBody(this.createBarrier(new cannon_1.Vec3(-this.size.x * 0.93, 0, 0), new cannon_1.Vec3(0, 1, 0), Math.PI / 2)); this.renderer.render(this.scene, this.camera); } DiceBox.prototype.bindMouse = function (container, notationGetter, beforeRoll, afterRoll) { var _this = this; var box = this; dicelib_1.bind(container, ['mousedown', 'touchstart'], function (ev) { ev.preventDefault(); box.mouseTime = new Date().getTime(); box.mouseStart = _this.getMousePosition(ev); }); dicelib_1.bind(container, ['mouseup', 'touchend'], function (ev) { if (box.rolling || box.mouseStart === undefined) { return; } ev.stopPropagation(); var m = _this.getMousePosition(ev); var vector = new three_1.Vector2(m.x - box.mouseStart.x, -(m.y - box.mouseStart.y)); // reset mouseStart box.mouseStart = undefined; var dist = Math.sqrt(vector.x * vector.x + vector.y * vector.y); if (dist >= Math.sqrt(box.size.x * box.size.y * 0.01)) { var interval = new Date().getTime() - box.mouseTime; if (interval > 2000) { interval = 2000; } var boost_1 = Math.sqrt((2500 - interval) / 2500) * dist * 2; dicelib_1.initRng(function () { _this.throwDice(vector, boost_1, dist, notationGetter, beforeRoll, afterRoll); }); } }); }; DiceBox.prototype.bindThrow = function (button, notationGetter, beforeRoll, afterRoll) { var box = this; dicelib_1.bind(button, ['mouseup', 'touchend'], function (ev) { ev.stopPropagation(); box.startThrow(notationGetter, beforeRoll, afterRoll); }); }; DiceBox.prototype.clear = function () { var _this = this; this.running = false; var dice; while ((dice = this.dice.pop())) { this.scene.remove(dice); if (dice.body) this.world.remove(dice.body); } if (this.pane) this.scene.remove(this.pane); this.renderer.render(this.scene, this.camera); setTimeout(function () { return _this.renderer.render(_this.scene, _this.camera); }, 100); }; DiceBox.prototype.drawSelector = function () { this.clear(); var step = this.size.x / 4.5; this.pane = new three_1.Mesh(new three_1.PlaneGeometry(this.size.x * 6, this.size.y * 6, 1, 1), new three_1.MeshPhongMaterial(diceconsts_1.DiceConsts.selectorBackColors)); this.pane.receiveShadow = true; this.pane.position.set(0, 0, 1); this.scene.add(this.pane); var mouseCaptured = false; for (var i = 0, pos = -3; i < diceconsts_1.DiceConsts.KNOWN_DICE.length; ++i, ++pos) { var sides = diceconsts_1.DiceConsts.KNOWN_DICE[i]; var die = this.diceFactory.createDie(sides); die.position.set(pos * step, 0, step * 0.5); die.castShadow = true; die.userData = "d" + sides; this.dice.push(die); this.scene.add(die); } this.running = new Date().getTime(); this.lastTime = 0; if (this.animateSelector) this.__selectorAnimate(this.running); else this.renderer.render(this.scene, this.camera); }; DiceBox.prototype.findDieAtMousePosition = function (ev) { var m = this.getMousePosition(ev); var intersects = new three_1.Raycaster(this.camera.position, new three_1.Vector3((m.x - this.clientSize.x) / this.aspect, (m.y - this.clientSize.y) / this.aspect, this.size.x / 9) .sub(this.camera.position) .normalize()).intersectObjects(this.dice); if (intersects.length) return intersects[0].object.userData; }; DiceBox.prototype.reinit = function (container, dimensions) { this.clientSize.set(container.clientWidth / 2, container.clientHeight / 2); if (dimensions) { this.size.x = dimensions.width; this.size.y = dimensions.height; } else { this.size.copy(this.clientSize); } this.aspect = Math.min(this.clientSize.x / this.size.x, this.clientSize.y / this.size.y); this.scale = Math.sqrt(this.size.x * this.size.x + this.size.y * this.size.y) / 13; this.renderer.setSize(this.clientSize.x * 2, this.clientSize.y * 2); this.windowSize.y = this.clientSize.y / this.aspect / Math.tan(10 * Math.PI / 180); if (this.camera) this.scene.remove(this.camera); this.camera = new three_1.PerspectiveCamera(20, this.clientSize.x / this.clientSize.y, 1, this.windowSize.y * 1.3); this.camera.position.z = this.windowSize.y; var mw = Math.max(this.size.x, this.size.y); if (this.light) { this.scene.remove(this.light); } var light = new three_1.SpotLight(diceconsts_1.DiceConsts.spotlightColor, 2.0); light.position.set(-mw / 2, mw / 2, mw * 2); light.target.position.set(0, 0, 0); light.distance = mw * 5; light.castShadow = true; light.shadowCameraNear = mw / 10; light.shadowCameraFar = mw * 5; light.shadowCameraFov = 50; light.shadowBias = 0.001; light.shadowDarkness = 1.1; light.shadowMapWidth = 1024; light.shadowMapHeight = 1024; this.light = light; this.scene.add(this.light); if (this.desk) { this.scene.remove(this.desk); } this.desk = new three_1.Mesh(new three_1.PlaneGeometry(this.size.x * 2, this.size.y * 2, 1, 1), new three_1.MeshPhongMaterial({ color: diceconsts_1.DiceConsts.deskColor })); this.desk.receiveShadow = true; this.scene.add(this.desk); this.renderer.render(this.scene, this.camera); }; DiceBox.prototype.roll = function (vectors, values, callback) { this.initRoll(vectors); if (values != undefined && values.length) { this.useAdaptiveTimestamp = false; var res = this.emulateThrow(); this.initRoll(vectors); for (var i in res) { this.shiftDieFaces(this.dice[i], values[i], res[i]); } } this.callback = callback; this.running = new Date().getTime(); this.lastTime = 0; this.__animate(this.running); }; DiceBox.prototype.throwDice = function (vector, boost, dist, notationGetter, beforeRoll, afterRoll) { var _this = this; var uat = this.useAdaptiveTimestamp; var roll = function (requestResults) { if (requestResults === void 0) { requestResults = false; } if (afterRoll) { _this.clear(); _this.roll(vectors, requestResults || notation.result, function (result) { if (afterRoll) { afterRoll(_this, notation, result); } _this.rolling = false; _this.useAdaptiveTimestamp = uat; }); } }; vector.x /= dist; vector.y /= dist; var notation = dicenotation_1.DiceNotation.parse(notationGetter()); if (notation.set.length == 0) return; var vectors = this.generateVectors(notation, vector, boost); this.rolling = true; if (beforeRoll) { beforeRoll(this, vectors, notation, roll); } else { roll(); } }; DiceBox.prototype.emulateThrow = function () { while (!this.isThrowFinished()) { ++this.iteration; this.world.step(diceconsts_1.DiceConsts.frameRate); } return this.dice.map(function (x) { return x.value; }); }; DiceBox.prototype.createBarrier = function (pos, axis, angle) { var barrier; barrier = new cannon_1.Body({ mass: 0, material: this.barrierBodyMaterial }); barrier.addShape(new cannon_1.Plane()); barrier.quaternion.setFromAxisAngle(axis, Math.PI / 2); barrier.position.set(pos.x, pos.y, pos.z); this.world.addBody(barrier); return barrier; }; DiceBox.prototype.generateVectors = function (notation, vector, boost) { var vectors = []; for (var _i = 0, _a = notation.set; _i < _a.length; _i++) { var sides = _a[_i]; var vec = dicelib_1.randomizeVector(vector); var pos = new three_1.Vector3(this.size.x * (vec.x > 0 ? -1 : 1) * 0.9, this.size.y * (vec.y > 0 ? -1 : 1) * 0.9, dicelib_1.rng() * 200 + 200); var projector = Math.abs(vec.x / vec.y); if (projector > 1.0) { pos.setY(pos.y / projector); } else { pos.setX(pos.x * projector); } var velvec = dicelib_1.randomizeVector(vector); var velocity = new three_1.Vector3(velvec.x * boost, velvec.y * boost, -10); var inertia = this.diceFactory.getStatic(sides).inertia; var angle = new three_1.Vector3(-(dicelib_1.rng() * vec.y * 5 + inertia * vec.y), dicelib_1.rng() * vec.x * 5 + inertia * vec.x, 0); var axis = new three_1.Vector4(dicelib_1.rng(), dicelib_1.rng(), dicelib_1.rng(), dicelib_1.rng()); vectors.push({ sides: sides, pos: pos, velocity: velocity, angle: angle, axis: axis }); } return vectors; }; DiceBox.prototype.addDice = function (sides, pos, velocity, angle, axis) { var die = this.diceFactory.createDie(sides); die.castShadow = true; die.body.position.set(pos.x, pos.y, pos.z); die.body.quaternion.setFromAxisAngle(new cannon_1.Vec3(axis.x, axis.y, axis.z), axis.a * Math.PI * 2); die.body.angularVelocity.set(angle.x, angle.y, angle.z); die.body.velocity.set(velocity.x, velocity.y, velocity.z); die.body.linearDamping = 0.1; die.body.angularDamping = 0.1; this.scene.add(die); this.dice.push(die); this.world.addBody(die.body); }; DiceBox.prototype.isThrowFinished = function () { var result = true; var e = 6; if (this.iteration < 10 / diceconsts_1.DiceConsts.frameRate) { for (var i = 0; i < this.dice.length; ++i) { var die = this.dice[i]; if (die.stopped === true) continue; var a = die.body.angularVelocity, v = die.body.velocity; if (Math.abs(a.x) < e && Math.abs(a.y) < e && Math.abs(a.z) < e && Math.abs(v.x) < e && Math.abs(v.y) < e && Math.abs(v.z) < e) { if (!!die.stopped) { if (this.iteration - die.stopped > 3) { die.stopped = true; continue; } } else die.stopped = this.iteration; result = false; } else { die.stopped = undefined; result = false; } } } return result; }; DiceBox.prototype.__animate = function (threadid) { var time = new Date().getTime(); var timeDiff = (time - this.lastTime) / 1000; if (timeDiff > 3) { timeDiff = diceconsts_1.DiceConsts.frameRate; } ++this.iteration; if (this.useAdaptiveTimestamp) { while (timeDiff > diceconsts_1.DiceConsts.frameRate * 1.1) { this.world.step(diceconsts_1.DiceConsts.frameRate); timeDiff -= diceconsts_1.DiceConsts.frameRate; } this.world.step(timeDiff); } else { this.world.step(diceconsts_1.DiceConsts.frameRate); } for (var i in this.scene.children) { var interact = this.scene.children[i]; if (interact.body !== undefined) { var p = interact.body.position; var q = interact.body.quaternion; interact.position.set(p.x, p.y, p.z); interact.quaternion.set(q.x, q.y, q.z, q.w); } } this.renderer.render(this.scene, this.camera); this.lastTime = this.lastTime ? time : new Date().getTime(); if (this.running == threadid && this.isThrowFinished()) { this.running = false; if (this.callback) { this.callback(this.dice.map(function (x) { return x.value; })); } } if (this.running == threadid) { (function (t, tid, uat) { if (!uat && timeDiff < diceconsts_1.DiceConsts.frameRate) { setTimeout(function () { requestAnimationFrame(function () { t.__animate(tid); }); }, (diceconsts_1.DiceConsts.frameRate - timeDiff) * 1000); } else requestAnimationFrame(function () { t.__animate(tid); }); })(this, threadid, this.useAdaptiveTimestamp); } }; DiceBox.prototype.initRoll = function (vectors) { this.clear(); this.iteration = 0; for (var i in vectors) { this.addDice(vectors[i].sides, vectors[i].pos, vectors[i].velocity, vectors[i].angle, vectors[i].axis); } }; DiceBox.prototype.shiftDieFaces = function (die, value, res) { var r = die.range; if (!(value >= r[0] && value <= r[1])) return; var num = value - res; var geom = die.geometry.clone(); for (var i = 0, l = geom.faces.length; i < l; ++i) { var matindex = geom.faces[i].materialIndex; if (matindex == 0) continue; matindex += num - 1; while (matindex > r[1]) matindex -= r[1]; while (matindex < r[0]) matindex += r[1]; geom.faces[i].materialIndex = matindex + 1; } die.geometry = geom; }; DiceBox.prototype.__selectorAnimate = function (threadid) { var time = new Date().getTime(); var timeDiff = (time - this.lastTime) / 1000; if (timeDiff > 3) timeDiff = diceconsts_1.DiceConsts.frameRate; var dAngle = 0.3 * timeDiff * Math.PI * Math.min(24000 + threadid - time, 6000) / 6000; if (dAngle < 0) this.running = false; for (var i in this.dice) { this.dice[i].rotation.y += dAngle; this.dice[i].rotation.x += dAngle / 4; this.dice[i].rotation.z += dAngle / 10; } this.lastTime = time; this.renderer.render(this.scene, this.camera); if (this.running == threadid) { (function (t, tid) { requestAnimationFrame(function () { t.__selectorAnimate(tid); }); })(this, threadid); } }; DiceBox.prototype.startThrow = function (notationGetter, beforeRoll, afterRoll) { var _this = this; if (this.rolling) return; dicelib_1.initRng(function () { var vector = new three_1.Vector2((dicelib_1.rng() * 2 - 1) * _this.size.x, -(dicelib_1.rng() * 2 - 1) * _this.size.y); var dist = Math.sqrt(vector.x * vector.x + vector.y * vector.y); var boost = (dicelib_1.rng() + 3) * dist; _this.throwDice(vector, boost, dist, notationGetter, beforeRoll, afterRoll); }); }; DiceBox.prototype.getMousePosition = function (ev) { var touchEvent = ev; if (touchEvent.changedTouches !== undefined) { return new three_1.Vector2(touchEvent.changedTouches[0].clientX, touchEvent.changedTouches[0].clientY); } return new three_1.Vector2(ev.clientX, ev.clientY); }; return DiceBox; }()); exports.DiceBox = DiceBox; //# sourceMappingURL=dicebox.js.map