Published
Edited
Feb 7, 2020
34 stars
Also listed in…
Games
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
image = {
const img = html`<img src="${source}" crossOrigin />`
return Generators.observe(notify => {
img.addEventListener('load', event => {
const { target } = event
const canvas = DOM.canvas(target.width, target.height),
ctx = canvas.getContext('2d')

ctx.drawImage(target, 0, 0, canvas.width, canvas.height)
const image = ctx.getImageData(0, 0, canvas.width, canvas.height)

notify(image)
})
})
}
Insert cell
Insert cell
Insert cell
imageWut = {
const array = [
// ↓ red: top left ↓ orange: top middle ↓ yellow: top right (wrap)
255, 128, 0, 255, 255, 0, 0, 255, 255, 255, 0, 255,
0, 255, 0, 255, 0, 0, 255, 255, 128, 0, 255, 255,
// ↑ green: bottom left ↑ blue: bottom middle ↑ purple: bottom right
]
return new ImageData(Uint8ClampedArray.from(array), 3, 2)
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function luminosity(r, g, b) {
return 0.2126 * r + 0.7125 * g + 0.0722 * b
}
Insert cell
function threshold(l) {
const thresholdValue = 212
return l >= thresholdValue ? 255 : 0
}
Insert cell
function thresholdImage(image) {
const result = new ImageData(image.width, image.height),
r = result.data,
d = image.data
for (let i = 0; i < d.length; i += 4) {
r[i] = r[i + 1] = r[i + 2] = threshold(luminosity(d[i], d[i + 1], d[i + 2]))
r[i + 3] = d[i + 3]
}

return result
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
contourFinder = require('contours@1.0.1')
Insert cell
cardContours = {
const area = image.width * image.height
return contourFinder(thresholded)
.filter(c => c.length > 4) // large enough to have area
.map(c => c.map(p => [p % image.width, Math.floor(p / image.width)])) // switch to [x, y]
.map(d3.polygonHull) // convex hulls
.filter(hull => area / 125 < d3.polygonArea(hull) && d3.polygonArea(hull) < area / 5)
.slice(0, 12)
}
Insert cell
Insert cell
Insert cell
Insert cell
cardRectangles = cardContours.map(points => {
let rect = points
while (rect.length > 4) {
const area = d3.polygonArea(rect)
let min = Infinity,
minRect = null
for (let i = 0; i < rect.length; i++) {
const smaller = rect.slice(0, i).concat(rect.slice(i + 1)),
difference = area - d3.polygonArea(smaller)
if (difference < min) {
min = difference
minRect = smaller
}
}
rect = minRect
}

// try to correct vertex order (counter-clockwise from top right corner (upside down is ok))
const edges = [
d3.polygonLength([rect[1], rect[0]]),
d3.polygonLength([rect[2], rect[1]]),
]
if (edges[1] > edges[0])
// need to rotate starting vertex
rect = rect.slice(1).concat(rect.slice(0, 1))

return rect
})
Insert cell
Insert cell
Insert cell
perspectiveTransform = require('perspective-transform@1.1.3')
Insert cell
cardImages = cardRectangles.map(rectangle => {
const crop = 2,
cw = 150,
ch = (cw / 3) * 2,
target = _.flatten([
[cw + crop, -crop],
[-crop, -crop],
[-crop, ch + crop],
[cw + crop, ch + crop],
])

const tx = perspectiveTransform(_.flatten(rectangle), target)

const card = new ImageData(cw, ch)
const cardBuffer = new Uint32Array(card.data.buffer),
imageBuffer = new Uint32Array(image.data.buffer)

for (let i = 0; i < cardBuffer.length; i++) {
const x = i % cw,
y = Math.floor(i / cw),
[xs, ys] = tx.transformInverse(x, y),
j = Math.round(ys) * image.width + Math.round(xs)
cardBuffer[i] = imageBuffer[j]
}

return card
})
Insert cell
Insert cell
Insert cell
Insert cell
cardWhiteBalanced = cardImages.map(image => {
const { data } = image

// average the top and bottom border for what should be white
const whitePoint = [0, 0, 0, 255]
for (let y of [0, image.height - 1]) {
for (let x = 0; x < image.width; x++) {
const i = (y * image.width + x) * 4
for (let d = 0; d < 3; d++) whitePoint[d] += data[i + d]
}
}
for (let d = 0; d < 3; d++) whitePoint[d] /= image.width * 2

// adjust in RGB space
const result = new ImageData(image.width, image.height)
const multiplier = whitePoint.map(x => 255 / x)
for (let i = 0; i < data.length; i += 4)
for (let d = 0; d < 4; d++) result.data[i + d] = data[i + d] * multiplier[d]

return result
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function* hues(image) {
const { data } = image
for (let i = 0; i < data.length; i += 4)
if (threshold(luminosity(data[i], data[i + 1], data[i + 2])) === 0)
yield d3.hsl(d3.rgb(data[i], data[i + 1], data[i + 2])).h
}
Insert cell
hue = d3.scaleThreshold()
.domain([70, 180, 340])
.range(['red', 'green', 'purple', 'red'])
Insert cell
cardColors = cardWhiteBalanced.map(image => {
const counts = { red: 0, green: 0, purple: 0 }

for (const h of hues(image))
counts[hue(h)]++
const space = counts.red + counts.green + counts.purple

// return counts
if (counts.purple / space > 0.2) return 'purple'
else if (counts.green / space > 0.5) return 'green'
else if (counts.red / space > 0.1) return 'red'
else return null
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
cardShapeContours = cardImages.map(image => {
const area = image.width * image.height
return contourFinder(thresholdImage(image))
.filter(c => c.length > 4) // large enough to have area
.map(c => c.map(p => [p % image.width, Math.floor(p / image.width)])) // switch to [x, y]
.filter(hull => area / 10 < Math.abs(d3.polygonArea(hull)) && Math.abs(d3.polygonArea(hull)) < area / 5)
})
Insert cell
numberScale = d3.scaleThreshold()
.domain([0.1, 0.44, 0.75, 0.99])
.range([null, 1, 2, 3, null])
Insert cell
cardPercentageWidths = _.zip(cardShapeContours, cardImages).map(([contours, {width}]) => {
let min = width,
max = 0

for (const shape of contours) {
for (const [x, y] of shape) {
if (x < min) min = x
if (x > max) max = x
}
}

// mirror a min or max to the other side for incomplete shape finding
if (width - min > max) max = width - min
if (width - max < min) min = width - max

return (max - min) / width
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
shadeScale = d3.scaleThreshold()
.domain([180, 240])
.range(['solid', 'striped', 'outlined'])
Insert cell
cardInteriorLumas = _.zip(cardWhiteBalanced, cardShapeContours).map(([image, contours]) => {
const { data } = image
let sum = 0, n = 0

for (const shape of contours) {
// sample interior
const xExtent = d3.extent(shape, p => p[0]),
yExtent = d3.extent(shape, p => p[1]),
dx = Math.floor((xExtent[1] - xExtent[0]) / 3),
dy = Math.floor((yExtent[1] - yExtent[0]) / 4)

for (let y = yExtent[0] + dy; y < yExtent[1] - dy; y++) {
for (let x = xExtent[0] + dx; x < xExtent[1] - dx; x++) {
const i = (y * image.width + x) * 4
sum += luminosity(data[i], data[i + 1], data[i + 2])
n++
}
}
}

return sum / n
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
shapeCentroids = [
{ shape: 'diamond', center: [0.53, 0.062] },
{ shape: 'squiggle', center: [0.715, 0.145] },
{ shape: 'oval', center: [0.85, 0.026] },
]
Insert cell
Insert cell
Insert cell
cardShapeAreas = _.zip(cardShapeContours, cardImages).map(([contours, image]) => {
return d3.mean(contours.map(contour => Math.abs(d3.polygonArea(contour)) / polygonBoxArea(contour)))
})
Insert cell
cardHullAreas = cardShapeContours.map(contours => {
return _.mean(contours.map(contour => {
const hullArea = Math.abs(d3.polygonArea(d3.polygonHull(contour)))
return (hullArea - Math.abs(d3.polygonArea(contour))) / hullArea
}))
})
Insert cell
cardShapes = _.zip(cardShapeAreas, cardHullAreas).map(point => {
const squares = _.sortBy(shapeCentroids, ({ center }) => {
const d = [point[0] - center[0], point[1] - center[1]]
return d[0] * d[0] + d[1] * d[1]
})
return squares[0].shape
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
combinations = require('https://bundle.run/combinations-generator@1.0.1')
Insert cell
Insert cell
function isSet(cards) {
return [ cardColors, cardNumbers, cardShades, cardShapes ].every(attribute => {
attribute = cards.map(i => attribute[i])
return allSame(attribute) || allDifferent(attribute)
})
}
Insert cell
function allSame(attributes) {
return attributes[0] === attributes[1] &&
attributes[1] === attributes[2]
}
Insert cell
function allDifferent(attributes) {
return attributes[0] !== attributes[1] &&
attributes[0] !== attributes[2] &&
attributes[1] !== attributes[2]
}
Insert cell
sets = possibleSets.filter(isSet)
Insert cell
Insert cell
Insert cell
Insert cell
result = {
const c = canvas(image),
ctx = c.getContext('2d')
ctx.lineWidth = 20
let n = 0
for (const set of sets) {
ctx.strokeStyle = d3.schemeCategory10[n++ % 10]
for (const contour of set.map(i => cardContours[i])) {
ctx.beginPath()
for (const [x, y] of contour) ctx.lineTo(x, y)
ctx.closePath()
ctx.stroke()
}
}
return c
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more