Skip to content

Instantly share code, notes, and snippets.

@b0c0de
Last active November 20, 2019 03:32
Show Gist options
  • Save b0c0de/f5f90a7b8ab573b8a4ccd314eb1331bd to your computer and use it in GitHub Desktop.
Save b0c0de/f5f90a7b8ab573b8a4ccd314eb1331bd to your computer and use it in GitHub Desktop.
HTML5 Drag and Drop Solitaire

HTML5 Drag and Drop Solitaire

Solitaire game using the HTML5 Drag and Drop API. IE11+

Fully featured game including hinting, auto-stacking, undoing, animation and even the beloved Windows Solitaire win animation.

Scoring system is unfinished.

A Pen by Bojan on CodePen.

License.

<div id="controls">
<button type="button" data-action="undo">Undo</button>
<button type="button" data-action="hint">Hint</button>
<button type="button" data-action="cheat">Force Win</button>
<button type="button" data-action="start">Restart</button>
<div id="score">Score: 0</div>
</div>
<div id="container"></div>
(function(global) {
'use strict'
var win = window,
doc = document,
body = doc.body;
/**
* Default configuration properties
* @type {Object}
*/
var defaultConfig = {
};
/**
* Object.assign polyfill
* @param {Object} target
* @param {Object} args
* @return {Object}
*/
var extend = function(r, t) {
for (var e = Object(r), n = 1; n < arguments.length; n++) {
var a = arguments[n];
if (null != a)
for (var o in a) Object.prototype.hasOwnProperty.call(a, o) && (e[o] = a[o])
}
return e
};
/**
* Add event listener to target
* @param {Object} el
* @param {String} e
* @param {Function} fn
*/
var on = function(el, e, fn) {
el.addEventListener(e, fn, false);
};
/**
* Iterator helper
* @param {(Array|Object)} arr Any object, array or array-like collection.
* @param {Function} f The callback function
* @param {Object} s Change the value of this
* @return {Void}
*/
var each = function(arr, fn, s) {
if ("[object Object]" === Object.prototype.toString.call(arr)) {
for (var d in arr) {
if (Object.prototype.hasOwnProperty.call(arr, d)) {
fn.call(s, d, arr[d]);
}
}
} else {
for (var e = 0, f = arr.length; e < f; e++) {
fn.call(s, e, arr[e]);
}
}
};
/**
* Mass assign style properties
* @param {Object} t
* @param {(String|Object)} e
* @param {String|Object}
*/
var style = function(t, e) {
var i = t && t.style,
n = "[object Object]" === Object.prototype.toString.call(e);
if (i) {
if (!e) return win.getComputedStyle(t);
n && each(e, function(t, e) {
t in i || (t = "-webkit-" + t), i[t] = e + ("string" == typeof e ? "" : "opacity" === t ? "" : "px")
})
}
};
/**
* Get an element's DOMRect relative to the document instead of the viewport.
* @param {Object} t HTMLElement
* @param {Boolean} e Include margins
* @return {Object} Formatted DOMRect copy
*/
var rect = function(e) {
var t = win,
o = e.getBoundingClientRect(),
b = doc.documentElement || body.parentNode || body,
d = (void 0 !== t.pageXOffset) ? t.pageXOffset : b.scrollLeft,
n = (void 0 !== t.pageYOffset) ? t.pageYOffset : b.scrollTop;
return {
left: o.left + d,
top: o.top + n,
height: Math.round(o.height),
width: Math.round(o.width)
}
};
var getRandomInt = function(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min)) + min;
};
/* EMITTER */
var Emitter = function() {};
Emitter.prototype = {
on: function(event, fct){
this._events = this._events || {};
this._events[event] = this._events[event] || [];
this._events[event].push(fct);
},
off: function(event, fct){
this._events = this._events || {};
if( event in this._events === false ) return;
this._events[event].splice(this._events[event].indexOf(fct), 1);
},
emit: function(event /* , args... */){
this._events = this._events || {};
if( event in this._events === false ) return;
for(var i = 0; i < this._events[event].length; i++){
this._events[event][i].apply(this, Array.prototype.slice.call(arguments, 1));
}
}
};
Emitter.mixin = function(obj) {
var props = ['on', 'off', 'emit'];
for(var i = 0; i < props.length; i ++){
if( typeof obj === 'function' ){
obj.prototype[props[i]] = Emitter.prototype[props[i]];
}else{
obj[props[i]] = Emitter.prototype[props[i]];
}
}
return obj;
};
// VECTOR
function Vector(x, y) {
this.x = x;
this.y = y;
}
Vector.prototype = {
add: function(v) {
this.x += v.x, this.y += v.y;
},
};
// CARD
function Card(value, suit) {
this.value = value;
this.suit = suit;
this.flipped = false;
this.picture = this.value > 10;
switch (this.suit) {
case "hearts":
case "diamonds":
this.color = "red";
break;
case "clubs":
case "spades":
this.color = "black";
break;
}
var cards = ["A",2,3,4,5,6,7,8,9,10,"J","Q","K"];
var template = [
"<div class='front'><div class='value'>",cards[this.value-1],"</div><div class='value'>",cards[this.value-1],"</div><div class='middle'>"
];
if (!this.picture) {
for (var i = 0; i < this.value; i++) {
template.push("<span></span>");
}
}
template.push("</div></div><div class='rear'></div>");
var card = doc.createElement("div");
card.className = `card ${this.suit} card-${(this.picture ? cards[this.value-1] : this.value)}`;
card.innerHTML = template.join("");
if ( this.picture ) {
card.classList.add("picture");
}
card.card = true;
this.el = card;
}
Card.prototype.flip = function() {
this.el.classList.toggle("flipped", !this.flipped);
this.el.draggable = !this.flipped;
this.flipped = !this.flipped;
if ( !this.flipped ) {
this.el.style.transform = "";
}
};
// PACK
function Pack() {
this.cards = [];
this.suits = ["hearts", "spades", "diamonds", "clubs"];
var count = 0;
each(this.suits, function(i, suit) {
for ( var i = 1; i < 14; i++ ) {
var card = new Card(i, suit);
card.el.idx = count;
this.cards.push(card);
count++;
}
}, this);
}
Pack.prototype.shuffle = function() {
var m = this.cards.length,
t, i;
while (m) {
i = Math.floor(Math.random() * m--);
t = this.cards[m];
this.cards[m] = this.cards[i];
this.cards[i] = t;
this.cards[i].el.idx = i;
this.cards[m].el.idx = m;
}
};
// GAME
function Game(el, options) {
if ( typeof el === "string" ) {
el = document.querySelector(el);
}
this.el = el;
this.options = extend(defaultConfig, options);
this.score = 0;
this.animationInterval = 250;
this.stackToColumn = false;
this.history = [];
this.pack = new Pack();
Emitter.mixin(this);
this.render();
}
Game.prototype.render = function() {
var frag = document.createDocumentFragment();
this.columns = doc.createElement("div");
this.columns.className = "columns";
this.stacks = doc.createElement("div");
this.stacks.className = "stacks";
/* create stacks */
for (var i = 0; i < 4; i++) {
var stack = doc.createElement("div");
stack.className = "stack";
this.stacks.appendChild(stack);
}
/* Create columns */
for (var i = 0; i < 7; i++) {
var column = doc.createElement("div");
column.className = "column";
this.columns.appendChild(column);
}
this.dealer = doc.createElement("div");
this.dealer.className = "dealer";
this.packArea = doc.createElement("div");
this.packArea.className = "pack";
this.dealArea = doc.createElement("div");
this.dealArea.className = "dealt";
this.dealer.appendChild(this.packArea);
this.dealer.appendChild(this.dealArea);
frag.appendChild(this.dealer);
frag.appendChild(this.stacks);
frag.appendChild(this.columns);
this.el.appendChild(frag);
this.mouse = {
x: 0, y: 0
};
var id = "https://s3-us-west-2.amazonaws.com/s.cdpn.io/86186/cards-{t}.png";
this.images = {
clubs: id.replace("{t}", "clubs"),
spades: id.replace("{t}", "spades"),
diamonds: id.replace("{t}", "diamonds"),
hearts: id.replace("{t}", "hearts")
};
each(this.images, function(i, src) {
var image = new Image();
image.onload = function() {
//
};
image.src = src;
this.images[i] = image;
}, this);
this.events = {
click: this.click.bind(this),
mousedown: this.mousedown.bind(this),
keydown: this.keydown.bind(this),
mouseup: this.mouseup.bind(this),
dragstart: this.dragstart.bind(this),
dragenter: this.dragenter.bind(this),
dragover: this.dragover.bind(this),
dragend: this.dragend.bind(this)
};
on(this.dealer, "click", this.events.click);
on(this.el, "mousedown", this.events.mousedown);
on(doc, "keydown", this.events.keydown);
on(doc, "mouseup", this.events.mouseup);
on(doc, "dragstart", this.events.dragstart);
on(doc, "dragenter", this.events.dragenter);
on(doc, "dragover", this.events.dragover);
on(doc, "dragend", this.events.dragend);
};
Game.prototype.click = function(e) {
var t = e.target;
if ( t.classList.contains("pack") ) {
e.stopImmediatePropagation();
this.deal();
}
};
Game.prototype.keydown = function(e) {
var k = e.key;
if ( e.ctrlKey ) {
switch(k) {
case "z":
this.undo();
break;
}
}
};
Game.prototype.mousedown = function(e) {
var t = e.target.closest(".card");
if ( t && t.card ) {
this.siblings = [];
var card = this.pack.cards[t.idx];
var prev = t.previousElementSibling;
var next = t.nextElementSibling;
card.checked = false;
card.origin = {
x: e.pageX,
y: e.pageY,
};
card.el.classList.add("dragging");
this.activeCard = card;
this.startParent = card.el.parentNode;
if ( next ) {
var p = next.parentNode;
var idx = [].slice.call(p.children).indexOf(next);
for (var i = idx; i < p.childElementCount; i++) {
var c = p.children[i];
c.classList.add("dragging");
this.siblings.push(c);
}
}
}
};
Game.prototype.dragstart = function(e) {
e.dataTransfer.effectAllowed = 'copy';
e.dataTransfer.setData('text/html', '');
// Create blank image to hide the ghost
var dragIcon = doc.createElement('img');
e.dataTransfer.setDragImage(dragIcon, -10, -10);
this.dragging = true;
};
Game.prototype.dragenter = function(e) {
var t = e.target;
var column = t.classList.contains("column");
var stack = t.classList.contains("stack");
var canDrop = t.card || column || stack;
if ( this.activeColumn ) {
this.activeColumn.classList.remove("over");
}
if ( canDrop ) {
if ( column || stack ) {
this.activeColumn = t;
} else {
this.activeColumn = t.parentNode;
}
this.activeColumn.classList.add("over");
}
};
Game.prototype.dragover = function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'over';
// Physically drag the card instead of using the D&D ghost
if ( this.activeCard && this.dragging ) {
var c = this.activeCard;
var x = e.pageX - c.origin.x;
var y = e.pageY - c.origin.y;
var css = "pointer-events: none; transform: scale(1.05, 1.05) rotateX(0deg) translate3d("+x+"px, "+y+"px, 0px);";
this.activeCard.el.style.cssText = css;
if ( this.siblings.length ) {
each(this.siblings, function(i, card) {
card.style.cssText = css;
}, this);
}
}
};
Game.prototype.dragend = function(e) {
if ( this.activeCard && this.dragging ) {
var c = this.activeCard;
c.el.classList.remove("dragging");
var x = e.pageX - c.origin.x;
var y = e.pageY - c.origin.y;
c.el.style.cssText = "";
if ( this.siblings.length ) {
each(this.siblings, function(i, card) {
card.classList.remove("dragging");
card.style.cssText = "";
}, this);
}
if ( this.activeColumn ) {
this.activeColumn.classList.remove("over");
}
if ( this.isLegalMove() ) {
var prev = c.el.previousElementSibling;
// Flip the last card
if ( prev ) {
var card = this.pack.cards[prev.idx];
if ( !card.flipped ) {
card.prevState = card.flipped;
card.flip();
this.score += 5;
}
}
this.stackToColumn = c.el.parentNode.classList.contains("stack");
this.pickCount = c.el.parentNode.childElementCount;
this.dropCount = this.activeColumn.childElementCount;
this.activeColumn.appendChild(c.el);
this.updateScore();
if ( this.siblings.length ) {
each(this.siblings, function(i, card) {
if ( this.activeCard.value === 13 &&
this.dropCount === 0 &&
!this.startParent.classList.contains("dealt") &&
c.el.parentNode.firstElementChild === c.el ) {
} else {
this.score += 5;
}
c.el.parentNode.appendChild(card);
card.classList.remove("dragging");
}, this);
}
this.updateHistory();
this.startParent.classList.toggle("empty", !this.startParent.childElementCount);
this.activeColumn.classList.toggle("empty", !this.activeColumn.childElementCount);
this.emit("change");
}
}
if ( !this.stackToColumn ) {
this.check();
}
};
Game.prototype.mouseup = function(e) {
if ( this.activeCard ) {
this.activeCard.el.classList.remove("dragging");
this.activeCard = false;
if ( this.siblings.length ) {
each(this.siblings, function(i, card) {
card.classList.remove("dragging");
}, this);
}
}
this.hinted = false;
this.emit("change");
};
Game.prototype.updateHistory = function(card, start, end, siblings) {
var obj = {};
if ( Array.isArray(card) ) {
obj.deal = true;
} else {
card = card || this.activeCard;
start = start || this.startParent;
end = end || this.activeColumn;
siblings = siblings || this.siblings;
// Max moves to store
var max = 10;
var cards = this.pack.cards;
var prev = card.el.previousElementSibling;
obj = {
card: card, // the card that was moved
start: start, // the original column
end: end, // the column the card was dropped in
siblings: siblings // any siblings
};
if ( prev ) {
obj.prevSibling = {
card: cards[prev.idx],
flipped: cards[prev.idx].flipped // was it hidden?
};
}
}
// Add the move to the history
this.history.push(obj);
// If the number of stored moves exceeds the max allowed
// remove the oldest moves until we're at the max allowed
if ( this.history.length > max ) {
this.history.splice(0, this.history.length - max);
}
};
Game.prototype.updateScore = function(start, stop) {
start = start || this.startParent;
stop = stop || this.activeColumn;
// Moving Kings from empty column to empty column
if ( this.dropCount === 0 &&
this.activeCard.value === 13 &&
!start.classList.contains("dealt") &&
this.activeCard.el.parentNode.firstElementChild === this.activeCard.el ) {
return false;
}
// Moving from deck to column
if ( start.classList.contains("dealt") ) {
if ( stop.classList.contains("column") ) {
this.score += 5;
}
// Moving from column to column
} else if ( start.classList.contains("column") ) {
if ( stop.classList.contains("column") ) {
this.score += 3;
}
}
// Moving to suit stack
if ( stop.classList.contains("stack") ) {
this.score += 10;
}
// Moving from stacks to columns
if ( start.classList.contains("stack") && stop.classList.contains("column") ) {
this.score -= 10;
}
};
Game.prototype.isLegalMove = function(active, column) {
active = active || this.activeCard;
column = column || this.activeColumn;
var last = false;
var legalMove = false;
var lastEl = column.lastElementChild;
var isColumn = column.classList.contains("column");
var isPlaceholder = column.classList.contains("stack");
if ( lastEl ) {
last = this.pack.cards[lastEl.idx];
}
if ( isColumn ) {
if ( !column.childElementCount ) {
legalMove = active.value === 13;
} else {
legalMove = active.color !== last.color && active.value === last.value - 1;
}
} else if ( isPlaceholder ) {
if ( !column.childElementCount ) {
legalMove = active.value === 1;
} else {
legalMove = active.color === last.color && active.suit === last.suit && active.value === last.value + 1;
}
}
return legalMove;
};
Game.prototype.undo = function() {
var index = this.history.length - 1;
if ( index > -1 ) {
var obj = this.history[index];
if ( obj.deal ) {
// Last move was a deal
var cards = [].slice.call(this.dealArea.children);
var diff = this.dealArea.childElementCount - this.dealCount;
var last = cards.splice(diff, this.dealCount);
last.forEach(function(el) {
var card = this.pack.cards[el.idx];
if ( card.flipped ) {
card.flip();
this.startParent.classList.toggle("empty", !this.startParent.childElementCount);
}
this.packArea.appendChild(el);
}, this);
} else {
var card = obj.card;
var last = obj.start.lastElementChild;
// Hide the last card if it was flipped by moving the subsequent card
if ( last ) {
var lastCard = this.pack.cards[last.idx];
if ( obj.prevSibling ) {
if ( !obj.prevSibling.prevState && lastCard.flipped ) {
lastCard.flip();
}
}
}
// Move the card back to it's original column...
obj.start.appendChild(card.el);
// .. as well as it's siblings
if ( obj.siblings.length ) {
obj.siblings.forEach(function(el) {
obj.start.appendChild(el);
}, this);
}
card.checked = false;
obj.start.classList.toggle("empty", !obj.start.childElementCount);
obj.end.classList.toggle("empty", !obj.end.childElementCount);
}
// Remove the move from the history
this.history.splice(index, 1);
}
};
Game.prototype.deal = function() {
var _ = this;
var frag = document.createDocumentFragment();
var pack = [].slice.call(this.packArea.children);
var count = pack.length;
if ( !count ) {
while (this.dealArea.childElementCount) {
var card = this.pack.cards[this.dealArea.lastElementChild.idx];
card.flip();
frag.appendChild(card.el);
}
this.packArea.appendChild(frag);
return false;
}
var items;
if ( count > 3 ) {
items = pack.slice(Math.max(count - 3, 1));
} else {
items = pack;
}
this.dealCount = items.length;
each(items, function(i, c) {
if ( c ) {
var card = this.pack.cards[c.idx];
var crect = rect(card.el);
var prect = rect(this.dealArea);
var x = crect.left - prect.left;
var y = crect.top - prect.top;
this.dealArea.appendChild(c);
card.el.style.cssText = "transform: translate3d("+x+"px,"+y+"px,0px) rotateY(180deg);";
setTimeout(function() {
card.el.style.cssText = "transform-origin: 50% 50%;transform: translate3d(0px,0px,0px) rotateY(0deg); transition: transform "+_.animationInterval+"ms;";
card.flip();
card.el.style.cssText = "";
}, this.animationInterval * i);
}
}, this);
this.updateHistory([]);
};
Game.prototype.check = function() {
var _ = this;
this.checked = false;
var columns = [].slice.call(this.columns.children);
var holders = this.stacks.children;
columns.push(this.dealArea);
each(columns, function(i,column) {
var c = column.lastElementChild;
if ( c ) {
var card = this.pack.cards[c.idx];
var start = card.el.parentNode;
each(holders, function(i,holder) {
if ( this.isLegalMove(card, holder) && !card.checked ) {
this.checked = true;
card.checked = true;
var prev = card.el.previousElementSibling;
if ( prev ) {
var prevCard = this.pack.cards[prev.idx];
if ( !prevCard.flipped ) {
prevCard.flip();
this.score += 5;
}
}
this.updateHistory(card, card.el.parentNode, holder);
var crect = rect(card.el);
var prect = rect(holder);
var x = crect.left - prect.left;
var y = crect.top - prect.top;
this.updateScore(card.el.parentNode, holder);
holder.appendChild(card.el);
start.classList.toggle("empty", !start.childElementCount);
card.el.style.cssText = "transform: translate3d("+x+"px,"+y+"px,0px);";
// Repaint
card.el.offsetTop;
card.el.style.cssText = "transform: translate3d(0px,0px,0px); transition: transform "+this.animationInterval+"ms;";
setTimeout(function() {
card.el.style.transform = "";
}, this.animationInterval)
this.emit("change");
}
}, this);
}
}, this);
var count = 0;
each(this.stacks.children, function(i, stack) {
count += stack.childElementCount;
});
this.won = false;
if ( count === 52 ) {
setTimeout(function() {
_.win();
}, this.animationInterval);
return false;
}
if ( this.checked ) {
setTimeout(function() {
_.check();
}, this.animationInterval);
}
};
Game.prototype.start = function() {
var columns = 7;
var current = 0;
var start = 0;
// Minimize DOM changes
var columns = this.columns;
var pack = this.packArea;
this.reset();
// Shuffle
this.pack.shuffle();
for (var i = 0; i < 28; i++) {
var card = this.pack.cards[i];
/* append the card to the column */
columns.children[current].appendChild(card.el);
/* flip the card if it is the first one */
if (start === current) {
card.flip();
}
/* increment the column we're dropping the card in to */
current++;
/* increment start position */
if (current === 7) {
start++;
current = start;
}
}
for (var i = 28; i < 52; i++) {
pack.appendChild(this.pack.cards[i].el);
}
this.packArea.parentNode.replaceChild(pack, this.packArea);
this.columns.parentNode.replaceChild(columns, this.columns);
this.packArea = pack;
this.columns = columns;
this.emit("start");
};
Game.prototype.hint = function() {
this.hinted = false;
var columns = [].slice.call(this.columns.children);
columns.push(this.dealArea);
each(this.stacks, function(i, stack) {
columns.push(stack);
});
each(columns, function(i,column) {
var c;
if ( column === this.dealArea ) {
c = column.lastElementChild;
} else {
c = column.getElementsByClassName("flipped")[0];
}
if ( c ) {
var card = this.pack.cards[c.idx];
var isLast, siblings = [];
var nodeIndex = [].slice.call(card.el.parentNode.children).indexOf(card.el);
if ( card.el.previousElementSibling ) {
if ( card.el.parentNode === this.dealArea ) {
isLast = true;
} else {
isLast = !card.el.previousElementSibling.classList.contains("flipped")
}
}
if ( card.value === 1 || card.el.parentNode.childElementCount === 1 ) {
isLast = true;
}
if ( card.value === 13 && card.el.parentNode.classList.contains("column") && card.el.parentNode.childElementCount === 1 ) {
return false;
}
each(card.el.parentNode.children, function(i, node) {
if ( i > nodeIndex ) {
siblings.push(node);
}
});
each(columns, function(idx,col) {
if ( this.isLegalMove(card, col) && isLast && !this.hinted ) {
var lastCard, last = col.lastElementChild;
if ( last ) {
lastCard = this.pack.cards[last.idx].el;
} else {
if ( card.value === 13 ) {
lastCard = col;
}
}
card.el.classList.add("hint");
if ( siblings.length ) {
each(siblings, function(i, node) {
node.classList.add("hint");
});
}
setTimeout(function() {
card.el.classList.remove("hint");
if ( siblings.length ) {
each(siblings, function(i, node) {
node.classList.remove("hint");
});
}
lastCard.classList.add("hint");
setTimeout(function() {
lastCard.classList.remove("hint");
}, 500);
}, 500);
this.hinted = true;
this.score -= 20;
}
}, this);
}
}, this);
};
Game.prototype.reset = function() {
this.score = 0;
this.history = [];
if ( this.won ) {
this.won = false;
document.body.removeChild(this.canvas);
}
this.pack.cards.forEach(function(card) {
if ( card.flipped ) {
card.flip();
}
card.checked = false;
});
[].slice.call(this.columns.children).forEach(function(column) {
column.classList.remove("empty");
});
[].slice.call(this.stacks.children).forEach(function(stack) {
stack.classList.remove("empty");
});
};
Game.prototype.win = function() {
if ( this.won ) {
return false;
}
this.won = true;
var rects = [];
var suits = [];
this.pack.cards.forEach(function(card) {
card.el.style.transform = "";
});
each(this.stacks.children, function(i, stack) {
rects.push(rect(stack));
var last = stack.lastElementChild;
var card = this.pack.cards[last.idx];
suits.push(card.suit);
}, this);
this.canvas = document.createElement("canvas");
var that = this;
var ctx = this.canvas.getContext("2d");
var w = this.canvas.width = window.innerWidth;
var h = this.canvas.height = window.innerHeight;
var gravity, wind;
var pos = new Vector(rects[0].left, rects[0].top);
var vel = new Vector(0, -getRandomInt(25,30));
var sWidth = 125;
var sHeight = 188;
var frame = null;
var x = 0;
var sx = sWidth * 12;
var sy = 0;
var init = function() {
document.body.appendChild(that.canvas);
setGravity();
setWind();
draw();
}
var setGravity = function() {
gravity = new Vector(0, getRandomInt(1, 9));
}
var setWind = function() {
var a = [-1,1];
var r = a[Math.floor(Math.random() * a.length)]
var w = getRandomInt(5,15);
wind = new Vector(w * r, 0);
}
var outline = function(p, w, h) {
var r = 5;
var points = [
[p.x + r, p.y],
[p.x + w - r, p.y],
[p.x + w, p.y + r],
[p.x + w, (p.y + h) - r],
[p.x + w - r, p.y + h],
[p.x + r, p.y + h],
[p.x, (p.y + h) - r],
[p.x, p.y + r],
];
ctx.beginPath();
ctx.lineWidth = 2;
ctx.strokeStyle = "#333";
// Top
ctx.moveTo(points[0][0], points[0][1]);
ctx.lineTo(points[1][0], points[1][1]);
// Top right corner
ctx.arc(points[1][0], points[2][1], r, 1.5 * Math.PI, 2 * Math.PI);
// Right side
ctx.moveTo(points[2][0], points[2][1]);
ctx.lineTo(points[3][0], points[3][1]);
// Bottom right corner
ctx.arc(points[4][0], points[3][1], r, 2 * Math.PI, 2.5 * Math.PI);
// Bottom
ctx.moveTo(points[4][0], points[4][1]);
ctx.lineTo(points[5][0], points[5][1]);
// Bottom left corner
ctx.arc(p.x + r, p.y + h - r, r, 2.5 * Math.PI, 3 * Math.PI);
// Left side
ctx.moveTo(points[6][0], points[6][1]);
ctx.lineTo(points[7][0], points[7][1]);
// Top left
ctx.arc(points[5][0], points[7][1], r, 3 * Math.PI, 3.5 * Math.PI);
ctx.stroke();
};
var draw = function() {
frame = requestAnimationFrame(draw);
var img = that.images[suits[x]];
var dWidth = sWidth;
var dHeight = sHeight;
vel.add(gravity);
pos.add(vel);
pos.add(wind);
if (pos.y >= h - sHeight) {
pos.y = h - sHeight;
vel.y = -vel.y;
}
ctx.fillStyle="#FFFFFF";
ctx.drawImage(img, sx, sy, sWidth, sHeight, pos.x, pos.y, dWidth, dHeight);
outline(pos, sWidth, sHeight);
if ( pos.x < 0 - sWidth || pos.x > w ) {
if ( x < 3 ) {
x++;
} else {
x = 0;
sx -= sWidth;
}
if ( sx > 0 - sWidth ) {
pos = new Vector(rects[x].left, rects[x].top);
vel = new Vector(0, -getRandomInt(25,30));
setGravity();
setWind();
}
}
}
init();
setGravity();
setWind();
};
Game.prototype.cheat = function() {
var that = this;
this.checked = false;
var columns = [].slice.call(this.columns.children);
var holders = this.stacks.children;
columns.push(this.dealArea);
each(this.pack.suits, function(i, suit) {
var el, card, s;
for( var n = 1; n < 14; n++ ) {
s = n;
if ( n > 10 ) {
switch(n) {
case 11:
s = "J";
break;
case 12:
s = "Q";
break;
case 13:
s = "K";
break;
}
}
el = document.querySelector(`.card.${suit}.card-${s}`);
card = this.pack.cards[el.idx];
var prev = card.el.previousElementSibling;
if ( !card.flipped && card.el.parentNode !== this.packArea ) {
card.flip();
}
if ( prev && card.el.parentNode !== this.packArea ) {
var prevCard = this.pack.cards[prev.idx];
if ( !prevCard.flipped ) {
prevCard.flip();
this.score += 5;
}
}
var crect = rect(card.el);
var prect = rect(holders[i]);
var x = crect.left - prect.left;
var y = crect.top - prect.top;
holders[i].appendChild(card.el);
card.el.style.cssText = "transform: translate3d("+x+"px,"+y+"px,0px);";
// Repaint
card.el.offsetTop;
card.el.style.cssText = "transform: translate3d(0px,0px,0px); transition: transform "+this.animationInterval+"ms;";
setTimeout(function() {
card.el.style.transform = "";
}, this.animationInterval)
}
}, this);
setTimeout(function() {
that.win();
}, this.animationInterval);
};
global.Game = Game;
}(this));
var controls = document.getElementById("controls");
var score = document.getElementById("score");
var game = new Game("#container");
game.start();
game.on("start", function() {
score.textContent = "Score: " + this.score;
});
game.on("change", function() {
score.textContent = "Score: " + this.score;
});
controls.addEventListener("click", function(e) {
var t = e.target;
if ( t.nodeName === "BUTTON" ) {
var action = t.getAttribute("data-action");
game[action]();
}
}, false);
$width: 125px;
$height: $width * 1.5;
$radius: $width / 20;
$red: #c0392b;
$black: #2c3e50;
$green: #008000;
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
@font-face {
src: url("https://s3-us-west-2.amazonaws.com/s.cdpn.io/86186/gothamrnd-medium.ttf");
font-family: "Gotham Rounded";
}
body {
width: 100vw;
height: 100vh;
margin: 0;
font-family: "Gotham Rounded";
overflow: hidden;
background-color: $green;
}
canvas {
position: fixed;
z-index: 10000;
left: 0;
top: 0;
pointer-events: none;
}
button {
color: #fff;
font-family: inherit;
font-weight: 500;
padding: 6px 12px;
font-size: 16px;
cursor: pointer;
background-color: transparent;
border: none;
&:hover {
background-color: rgba(0,0,0,0.3);
border-color: rgba(0,0,0,0.3);
}
}
#container::after,
.clearfix::after {
clear: both;
content: "";
display: table;
}
$w: $width * 7 + 70;
#container {
height: 80vh;
width: $w;
text-align: center;
margin: 10% auto;
}
#controls {
position: absolute;
width: $w;
left: calc((100vw - #{$w}) / 2);
top: 0;
box-sizing: border-box;
background-color: rgba(0,0,0,0.3);
margin-top: 20px;
}
#score {
color: #fff;
float: right;
padding: 6px 12px;
}
.columns {
width: 100%;
}
.column {
float: left;
position: relative;
width: $width;
height: $height;
// perspective: 800px;
}
$offset: $width / 5;
@for $i from 1 through 20 {
$pos: ($offset * $i) - $offset;
.column .card:nth-child(#{$i}), .column .ghost:nth-child(#{$i}) {
top: $pos;
z-index: $i + 10
}
}
.pack, .dealt {
position: relative;
width: $width;
height: $height;
float: left
}
.stack {
width: $width;
height: $height;
border-radius: $radius;
display: inline-block;
position: relative;
box-sizing: border-box;
}
.stack.over {
// border: 1px dashed #fff;
}
.dealer,
.stacks {
float: left;
}
.dealer {
width: 30%;
text-align: left;
}
.stacks {
width: 70%;
text-align: right;
}
.pack, .dealt, .stack, .column {
background-color: rgba(0,0,0,0.3);
margin: 0 10px 10px 0;
border-radius: $radius;
}
/* THE CARDS */
.card {
width: $width;
height: $height;
border: 2px solid $black;
border-radius: $radius;
position: relative;
display: inline-block;
position: absolute;
background-color: #fff;
cursor: pointer;
left: 0;
top: 0;
transform: rotateY( 180deg );
z-index: 1;
}
.column .card.flipped {
box-shadow: 0 0 20px -5px #000;
}
.card, .card * {
pointer-events: none;
}
.dealt .card {
pointer-events: none;
}
.dealt .card:last-child, .column .card.flipped, .stack .card.flipped:last-child {
pointer-events: inherit;
}
.card {
transform-style: preserve-3d;
transition: transform 250ms;
}
.card > div {
display: block;
height: 100%;
width: 100%;
position: absolute;
backface-visibility: hidden;
border-radius: $radius;
}
.card .front {
// transform: rotateY( 0deg );
}
.card .rear {
background-color: #fff;
background-image: url("https://s3-us-west-2.amazonaws.com/s.cdpn.io/86186/back.png");
background-position: center center;
background-repeat: no-repeat;
background-size: 82% 88%;
transform: rotateY( 180deg );
}
.card.flipped {
transform: rotateY( 0deg );
z-index: 2;
}
.card::after, .column::after {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
content: "";
opacity: 0;
transition: opacity 250ms;
pointer-events: none;
}
.card::before {
pointer-events: none;
}
.column.hint {
box-shadow: 0 0 0px 5px #fff inset;
}
.card.hint {
transform: scale(1.05, 1.05);
}
.column.over {
&.empty::before,
& > .card:not(.dragging):last-child::before {
content: "";
width: 100%;
height: 100%;
background-color: rgba(255,255,255,0.2);
position: absolute;
left: 0;
top: 0;
border-radius: $radius;
z-index: 100;
border: 2px dashed #fff;
box-sizing: border-box;
}
& > .card:not(.dragging):last-child::before {
top: $width / 5;
}
}
.card.dragging {
z-index: 1000 !important;
box-shadow: 0 0 20px -5px #000;
transition: none;
transform: scale(1.05, 1.05);
}
.card .value {
position: absolute;
font-size: $width / 6.5;
width: $width / 6.5;
height: $width / 3;
text-align: center;
}
.card .value::after {
position: absolute;
font-size: $width / 6;
}
.card .value:nth-child(1) {
top: 0px;
left: 0px;
}
.card .value:nth-child(2) {
bottom: 0px;
right: 0px;
transform: rotate(180deg);
}
.card .value:nth-child(1)::after,
.card .value:nth-child(2)::after {
position: absolute;
top: $width / 8;
left: 50%;
transform: translate3d(-50%, 0px, 0px);
}
.hearts,
.diamonds {
color: $red;
}
.clubs,
.spades {
color: $black;
}
.hearts .value::after,
.hearts span::after {
content: "♥";
}
.diamonds .value::after,
.diamonds span::after {
content: "♦";
}
.clubs .value::after,
.clubs span::after {
content: "♣";
}
.spades .value::after,
.spades span::after {
content: "♠";
}
/* Center symbol positions */
.middle {
height: 85%;
left: 7.5%;
position: absolute;
top: 7.5%;
width: 85%;
}
.middle span {
position: absolute;
display: block;
height: $width / 4;
width: $width / 4;
}
.middle span::after {
font-size: $width / 2.5;
@include flex-center;
width: 100%;
height: 100%;
position: absolute;
top: -$width / 20;
}
.picture .middle {
height: 80%;
left: 16%;
top: 10%;
width: 68%;
border: 1px solid #999;
background-image: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/86186/cards.jpg);
background-size: $width * 2.05 $width * 4.8;
}
$club: 0px;
$heart: -150px;
$spade: -300px;
$diamond: -450px;
$jack: 0px;
$queen: -86px;
$king: -170px;
.diamonds.card-J .middle {
background-position: $jack $diamond;
}
.hearts.card-J .middle {
background-position: $jack $heart;
}
.clubs.card-J .middle {
background-position: $jack $club;
}
.spades.card-J .middle {
background-position: $jack $spade;
}
.diamonds.card-Q .middle {
background-position: $queen $diamond;
}
.hearts.card-Q .middle {
background-position: $queen $heart;
}
.clubs.card-Q .middle {
background-position: $queen $club;
}
.spades.card-Q .middle {
background-position: $queen $spade;
}
.diamonds.card-K .middle {
background-position: $king $diamond;
}
.hearts.card-K .middle {
background-position: $king $heart;
}
.clubs.card-K .middle {
background-position: $king $club;
}
.spades.card-K .middle {
background-position: $king $spade;
}
/* no clicky on child elements */
.card:not(.flipped),
.card .value,
.card .middle,
.card span {
// pointer-events: none;
}
// .hearts .middle span::after {
// font-size: $width / 3;
// }
$offset: $width / 10;
$offset-alt: 0;
/* ACE */
.card-1 .middle {
@include flex-center;
font-size: $width / 2;
}
.card-1 .middle span {
position: relative;
height: $width / 2;
width: $width / 2;
}
.card-1 .middle span::after {
font-size: $width / 2;
}
/* TWO */
.card-2 .middle span {
left: 50%;
transform: translate3d(-50%, 0, 0);
}
.card-2 .middle span:nth-child(1) {
top: $offset-alt;
}
.card-2 .middle span:nth-child(2) {
bottom: $offset-alt;
}
.card-2 .middle span:nth-child(2) {
transform: rotate(180deg);
transform-origin: 25% 50% 0;
}
/* THREEE */
.card-3 .middle span {
left: 50%;
transform: translate3d(-50%, 0, 0);
}
.card-3 .middle span:nth-child(1) {
top: $offset-alt;
}
.card-3 .middle span:nth-child(2) {
top: 50%;
transform: translate3d(-50%, -50%, 0);
}
.card-3 .middle span:nth-child(3) {
bottom: $offset-alt;
}
.card-3 .middle span:nth-child(3) {
transform: rotate(180deg);
transform-origin: 25% 50% 0;
}
/* FOUR */
.card-4 .middle span:nth-child(1),
.card-4 .middle span:nth-child(3) {
left: $offset;
}
.card-4 .middle span:nth-child(2),
.card-4 .middle span:nth-child(4) {
right: $offset;
}
.card-4 .middle span:nth-child(1),
.card-4 .middle span:nth-child(2) {
top: $offset-alt;
}
.card-4 .middle span:nth-child(3),
.card-4 .middle span:nth-child(4) {
bottom: $offset-alt;
transform: rotate(180deg);
}
/* FIVE */
.card-5 .middle span:nth-child(1),
.card-5 .middle span:nth-child(3) {
left: $offset;
}
.card-5 .middle span:nth-child(2),
.card-5 .middle span:nth-child(4) {
right: $offset;
}
.card-5 .middle span:nth-child(1),
.card-5 .middle span:nth-child(2) {
top: $offset-alt;
}
.card-5 .middle span:nth-child(3),
.card-5 .middle span:nth-child(4) {
bottom: $offset-alt;
transform: rotate(180deg);
}
.card-5 .middle span:nth-child(5) {
left: 50%;
top: 50%;
transform: translate3d(-50%, -50%, 0);
}
/* SIX */
.card-6 .middle span:nth-child(1),
.card-6 .middle span:nth-child(3),
.card-6 .middle span:nth-child(5) {
left: $offset;
}
.card-6 .middle span:nth-child(2),
.card-6 .middle span:nth-child(4),
.card-6 .middle span:nth-child(6) {
right: $offset;
}
.card-6 .middle span:nth-child(1),
.card-6 .middle span:nth-child(2) {
top: $offset-alt;
}
.card-6 .middle span:nth-child(5),
.card-6 .middle span:nth-child(6) {
bottom: $offset-alt;
transform: rotate(180deg);
}
.card-6 .middle span:nth-child(3),
.card-6 .middle span:nth-child(4) {
top: 50%;
transform: translate3d(0, -50%, 0);
}
/* SEVEN */
.card-7 .middle span:nth-child(1),
.card-7 .middle span:nth-child(3),
.card-7 .middle span:nth-child(5) {
left: $offset;
}
.card-7 .middle span:nth-child(2),
.card-7 .middle span:nth-child(4),
.card-7 .middle span:nth-child(6) {
right: $offset;
}
.card-7 .middle span:nth-child(1),
.card-7 .middle span:nth-child(2) {
top: $offset-alt;
}
.card-7 .middle span:nth-child(5),
.card-7 .middle span:nth-child(6) {
bottom: $offset-alt;
transform: rotate(180deg);
}
.card-7 .middle span:nth-child(3),
.card-7 .middle span:nth-child(4) {
top: 50%;
transform: translate3d(0, -50%, 0);
}
.card-7 .middle span:nth-child(7) {
top: 25%;
left: 50%;
transform: translate3d(-50%, -25%, 0);
}
/* EIGHT */
.card-8 .middle span:nth-child(1),
.card-8 .middle span:nth-child(3),
.card-8 .middle span:nth-child(5) {
left: $offset;
}
.card-8 .middle span:nth-child(2),
.card-8 .middle span:nth-child(4),
.card-8 .middle span:nth-child(6) {
right: $offset;
}
.card-8 .middle span:nth-child(1),
.card-8 .middle span:nth-child(2) {
top: $offset-alt;
}
.card-8 .middle span:nth-child(5),
.card-8 .middle span:nth-child(6) {
bottom: $offset-alt;
transform: rotate(180deg);
}
.card-8 .middle span:nth-child(3),
.card-8 .middle span:nth-child(4) {
top: 50%;
transform: translate3d(0, -50%, 0);
}
.card-8 .middle span:nth-child(7) {
top: 25%;
left: 50%;
transform: translate3d(-50%, -25%, 0);
}
.card-8 .middle span:nth-child(8) {
top: 75%;
left: 50%;
transform: translate3d(-50%, -75%, 0) rotate(180deg);
}
/* NINE */
.card-9 .middle span:nth-child(1),
.card-9 .middle span:nth-child(3),
.card-9 .middle span:nth-child(5),
.card-9 .middle span:nth-child(7) {
left: $offset;
}
.card-9 .middle span:nth-child(2),
.card-9 .middle span:nth-child(4),
.card-9 .middle span:nth-child(6),
.card-9 .middle span:nth-child(8) {
right: $offset;
}
.card-9 .middle span:nth-child(1),
.card-9 .middle span:nth-child(2) {
top: $offset-alt;
}
.card-9 .middle span:nth-child(3),
.card-9 .middle span:nth-child(4) {
top: 25%;
}
.card-9 .middle span:nth-child(5),
.card-9 .middle span:nth-child(6) {
bottom: 25%;
}
.card-9 .middle span:nth-child(7),
.card-9 .middle span:nth-child(8) {
bottom: $offset-alt;
}
.card-9 .middle span:nth-child(5),
.card-9 .middle span:nth-child(6),
.card-9 .middle span:nth-child(7),
.card-9 .middle span:nth-child(8) {
transform: rotate(180deg);
}
.card-9 .middle span:nth-child(9) {
left: 50%;
top: 50%;
transform: translate3d(-50%, -50%, 0);
}
/* TEN */
.card-10 .middle span:nth-child(1),
.card-10 .middle span:nth-child(3),
.card-10 .middle span:nth-child(5),
.card-10 .middle span:nth-child(7) {
left: $offset;
}
.card-10 .middle span:nth-child(2),
.card-10 .middle span:nth-child(4),
.card-10 .middle span:nth-child(6),
.card-10 .middle span:nth-child(8) {
right: $offset;
}
.card-10 .middle span:nth-child(1),
.card-10 .middle span:nth-child(2) {
top: $offset-alt;
}
.card-10 .middle span:nth-child(3),
.card-10 .middle span:nth-child(4) {
top: 25%;
}
.card-10 .middle span:nth-child(5),
.card-10 .middle span:nth-child(6) {
bottom: 25%;
}
.card-10 .middle span:nth-child(7),
.card-10 .middle span:nth-child(8) {
bottom: $offset-alt;
}
.card-10 .middle span:nth-child(5),
.card-10 .middle span:nth-child(6),
.card-10 .middle span:nth-child(7),
.card-10 .middle span:nth-child(8) {
transform: rotate(180deg);
}
.card-10 .middle span:nth-child(9) {
left: 50%;
top: 16.6667%;
transform: translate3d(-50%, -16.6667%, 0);
}
.card-10 .middle span:nth-child(10) {
left: 50%;
bottom: 16.6667%;
transform: translate3d(-50%, 16.6667%, 0) rotate(180deg);
}
<link href="https://fonts.googleapis.com/css?family=Roboto:400,500" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment