Created
September 29, 2018 03:31
-
-
Save tswaters/0719ab2cb1ada620bbed88f3f9dc31ad to your computer and use it in GitHub Desktop.
Cards on canvas
This file contains hidden or 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 charet="utf-8"/> | |
<style> | |
canvas { | |
image-rendering: crisp-edges; | |
margin: 5px; | |
} | |
canvas.horizontal { | |
display: block; | |
} | |
canvas.vertical { | |
display: inline-block; | |
} | |
</style> | |
</head> | |
<body> | |
<script src="./canvas.js"></script> | |
</body> | |
</html> |
This file contains hidden or 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
/* eslint-env browser */ | |
'use strict' | |
const radius = 10 | |
const width = 75 | |
const height = 97 | |
const offset = 25 | |
const cards = generateCards() | |
const cards_per_stack = 13 | |
const stacks = cards.reduce((memo, item, index) => { | |
if (index % cards_per_stack === 0) { | |
const newStack = [{blank: true}] | |
newStack.push(item) | |
memo.push(newStack) | |
} else { | |
memo[memo.length - 1].push(item) | |
} | |
return memo | |
}, []) | |
let header = null | |
header = document.createElement('h2') | |
header.innerText = 'horizontal stack' | |
document.body.appendChild(header) | |
for (let i = 0; i < stacks.length; i++) { | |
document.body.appendChild(drawStack(stacks[i], 'horizontal')) | |
} | |
header = document.createElement('h2') | |
header.innerText = 'vertical stack' | |
document.body.appendChild(header) | |
for (let i = 0; i < stacks.length; i++) { | |
document.body.appendChild(drawStack(stacks[i], 'vertical')) | |
} | |
header = document.createElement('h2') | |
header.innerText = 'all cards' | |
document.body.appendChild(header) | |
const canvas = document.createElement('canvas') | |
const ctx = canvas.getContext('2d') | |
canvas.width = cards_per_stack * width | |
canvas.height = (cards.length / cards_per_stack) * height | |
document.body.appendChild(canvas) | |
for (let i = 0; i < cards.length; i++) { | |
const offsetX = i % cards_per_stack * width | |
const offsetY = Math.floor(i / cards_per_stack) * height | |
drawCard(ctx, cards[i], {offsetX, offsetY, radius}) | |
} | |
function drawStack (stack, direction) { | |
const canvas = document.createElement('canvas') | |
canvas.classList.add(direction) | |
const last = stack.length - 1 | |
canvas.width = 1 + (direction === 'horizontal' ? offset * last + width : width) | |
canvas.height = 1 + (direction === 'horizontal' ? height : offset * last + height) | |
const ctx = canvas.getContext('2d') | |
ctx.translate(0.5, 0.5) | |
ctx.clearRect(0, 0, canvas.width, canvas.height) | |
for (let i = 0; i < stack.length; i++) { | |
const offsetX = direction === 'horizontal' ? i * offset : 0 | |
const offsetY = direction === 'horizontal' ? 0 : i * offset | |
const clipWidth = direction === 'horizontal' ? offset : width | |
const clipHeight = direction === 'horizontal' ? height : offset | |
if (i < stack.length - 1) { | |
ctx.save() | |
drawClipRegion(ctx, {offsetX, offsetY, radius, direction, clipWidth, clipHeight}) | |
} | |
drawCard(ctx, stack[i], {offsetX, offsetY, radius}) | |
if (i < stack.length - 1) { | |
ctx.restore() | |
} | |
} | |
return canvas | |
} | |
function drawClipRegion (ctx, {offsetX, offsetY, radius, direction, clipWidth, clipHeight}) { | |
ctx.beginPath() | |
if (direction === 'horizontal') { | |
ctx.moveTo(offsetX, offsetY) | |
ctx.lineTo(offsetX + clipWidth + radius, offsetY) | |
ctx.quadraticCurveTo(offsetX + clipWidth, offsetY, offsetX + clipWidth, offsetY + radius) | |
ctx.lineTo(offsetX + clipWidth, offsetY + clipHeight - radius) | |
ctx.quadraticCurveTo(offsetX + clipWidth, offsetY + clipHeight, offsetX + clipWidth + radius, offsetY + clipHeight) | |
ctx.lineTo(offsetX, offsetY + clipHeight) | |
} else { | |
ctx.moveTo(offsetX + clipWidth, offsetY) | |
ctx.lineTo(offsetX + clipWidth, offsetY + clipHeight + radius) | |
ctx.quadraticCurveTo(offsetX + clipWidth, offsetY + clipHeight, offsetX + clipWidth - radius, offsetY + clipHeight) | |
ctx.lineTo(offsetX + radius, offsetY + clipHeight) | |
ctx.quadraticCurveTo(offsetX, offsetY + clipHeight, offsetX, offsetY + clipHeight + radius) | |
ctx.lineTo(offsetX, offsetY) | |
} | |
ctx.closePath() | |
ctx.clip() | |
} | |
function drawBoxRadius (ctx, {offsetX, offsetY, radius}) { | |
ctx.beginPath() | |
ctx.moveTo(offsetX + radius, offsetY) | |
ctx.lineTo(offsetX + width - radius, offsetY) | |
ctx.quadraticCurveTo(offsetX + width, offsetY, offsetX + width, offsetY + radius) | |
ctx.lineTo(offsetX + width, offsetY + height - radius) | |
ctx.quadraticCurveTo(offsetX + width, offsetY + height, offsetX + width - radius, offsetY + height) | |
ctx.lineTo(offsetX + radius, offsetY + height) | |
ctx.quadraticCurveTo(offsetX, offsetY + height, offsetX, offsetY + height - radius) | |
ctx.lineTo(offsetX, offsetY + radius) | |
ctx.quadraticCurveTo(offsetX, offsetY, offsetX + radius, offsetY) | |
ctx.closePath() | |
} | |
function drawCard (ctx, card, {offsetY, offsetX, radius}) { | |
const {blank, value, suit, drawing} = card | |
drawBoxRadius(ctx, {offsetX, offsetY, radius}) | |
ctx.stroke() | |
if (blank) { | |
ctx.fillStyle = '#0aa' | |
ctx.fill() | |
return | |
} | |
ctx.fillStyle = drawing.color | |
ctx.textAlign = 'center' | |
ctx.textBaseline = 'top' | |
ctx.font = drawing.cornerFont | |
ctx.fillText(value, drawing.valueXOffset + offsetX, drawing.valueYOffset + offsetY, 12) | |
ctx.fillText(suit, drawing.suitXOffset + offsetX, drawing.suitYOffset + offsetY) | |
ctx.save() | |
ctx.translate(width, height) | |
ctx.rotate(Math.PI) | |
ctx.fillText(value, drawing.valueXOffset - offsetX, drawing.valueYOffset - offsetY, 12) | |
ctx.fillText(suit, drawing.suitXOffset - offsetX, drawing.suitYOffset - offsetY) | |
ctx.restore() | |
ctx.textBaseline = 'middle' | |
for (const pos of drawing.positions) { | |
const factor = pos.rotated ? -1 : 1 | |
ctx.textAlign = pos.textAlign | |
if (pos.rotated) { | |
ctx.save() | |
ctx.translate(width, height) | |
ctx.rotate(Math.PI) | |
} | |
ctx.font = `${drawing.fontSize} sans-serif` | |
ctx.fillText(suit, pos.left + offsetX * factor, pos.top + offsetY * factor) | |
if (pos.rotated) { | |
ctx.restore() | |
} | |
} | |
} | |
function generateCards () { | |
const ValueType = { | |
ace: 'A', | |
two: '2', | |
three: '3', | |
four: '4', | |
five: '5', | |
six: '6', | |
seven: '7', | |
eight: '8', | |
nine: '9', | |
ten: '10', | |
jack: 'J', | |
queen: 'Q', | |
king: 'K' | |
} | |
const SuitType = { | |
heart: '\u2665', | |
diamond: '\u2666', | |
spade: '\u2660', | |
club: '\u2663' | |
} | |
const ret = [] | |
for (const [, suit] of Object.entries(SuitType)) { | |
for (const [, value] of Object.entries(ValueType)) { | |
ret.push({ | |
suit, | |
value, | |
drawing: getDrawing(suit, value) | |
}) | |
} | |
} | |
function getDrawing (suit, value) { | |
const color = [ | |
SuitType.diamond, | |
SuitType.heart | |
].indexOf(suit) > -1 ? 'red' : 'black' | |
const fontSize = [ | |
ValueType.ace, | |
ValueType.jack, | |
ValueType.queen, | |
ValueType.king | |
].indexOf(value) > -1 ? '72px' : '20px' | |
const pos = [] | |
if ([ValueType.ace, ValueType.three, ValueType.five, ValueType.nine, ValueType.jack, ValueType.queen, ValueType.king].indexOf(value) > -1) { | |
pos.push({x: 1, y: 3}) | |
} | |
if ([ValueType.two, ValueType.three].indexOf(value) > -1) { | |
pos.push({x: 1, y: 0}, {x: 1, y: 6}) | |
} | |
if ([ValueType.four, ValueType.five, ValueType.six, ValueType.seven, ValueType.eight, ValueType.nine, ValueType.ten].indexOf(value) > -1) { | |
pos.push({x: 0, y: 0}, {x: 2, y: 0}, {x: 0, y: 6}, {x: 2, y: 6}) | |
} | |
if ([ValueType.six, ValueType.seven, ValueType.eight].indexOf(value) > -1) { | |
pos.push({x: 0, y: 3}, {x: 2, y: 3}) | |
} | |
if ([ValueType.seven, ValueType.ten, ValueType.eight].indexOf(value) > -1) { | |
pos.push({x: 1, y: 1}) | |
} | |
if ([ValueType.nine, ValueType.ten].indexOf(value) > -1) { | |
pos.push({x: 0, y: 2}, {x: 2, y: 2}, {x: 0, y: 4}, {x: 2, y: 4}) | |
} | |
if ([ValueType.ten, ValueType.eight].indexOf(value) > -1) { | |
pos.push({x: 1, y: 5}) | |
} | |
const getTop = y => { | |
switch (y) { | |
case 0: case 6: return height * 0.2 | |
case 1: case 5: return height * 0.3 | |
case 2: case 4: return height * 0.4 | |
case 3: return height * 0.5 | |
} | |
} | |
const getLeft = x => { | |
switch (x) { | |
case 0: return width * 0.25 | |
case 1: return width * 0.50 | |
case 2: return width * 0.75 | |
} | |
} | |
const getTextAlign = x => { | |
switch (x) { | |
case 0: return 'left' | |
case 1: return 'center' | |
case 2: return 'right' | |
} | |
} | |
const positions = pos.map(({x, y}) => { | |
return { | |
textAlign: getTextAlign(x), | |
rotated: y > 3, | |
left: getLeft(x), | |
top: getTop(y) | |
} | |
}) | |
return { | |
cornerFont: 'bold 15px sans-serif', | |
valueXOffset: 9, | |
valueYOffset: 2, | |
suitXOffset: 9, | |
suitYOffset: 12, | |
color, | |
fontSize, | |
positions | |
} | |
} | |
return ret | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment