Skip to content

Commit

Permalink
ultra-crisp canvas rendering done
Browse files Browse the repository at this point in the history
  • Loading branch information
qrohlf committed May 4, 2020
1 parent db94933 commit de44f2f
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 63 deletions.
9 changes: 8 additions & 1 deletion examples/basic-web-example.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
padding: 0 0;
text-align: center;
font-size: 0;
background: #000;
}

svg {
padding-bottom: 30px;
}
</style>
</head>
Expand All @@ -22,9 +27,11 @@
<script>
const pattern = trianglify({
width: window.innerWidth,
height: window.innerHeight
height: window.innerHeight / 3
})
document.body.appendChild(pattern.toSVG())

document.body.appendChild(pattern.toCanvas(null, {retina: true}))
</script>
</body>
</html>
2 changes: 1 addition & 1 deletion lib/pattern.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// conditionally load jsdom if we don't have a browser environment available.
var doc = (typeof document !== "undefined") ? document : require('jsdom').jsdom('<html/>');

export default function Pattern(polys, opts) {
export default function Pattern(points, polys, opts) {

// SVG rendering method
function render_svg(svgOpts) {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"build:module:node": "microbundle --format cjs,es --target node --no-compress",
"build:module:browser": "microbundle --format umd --target web --no-compress --output dist/trianglify.browser.js",
"build:standalone": "microbundle --external none --format umd --output dist/trianglify.bundle.js",
"dev": "microbundle watch --external none --format umd --output dist/trianglify.bundle.js",
"postinstall": "node scripts/postinstall.js"
},
"dependencies": {
Expand Down
104 changes: 72 additions & 32 deletions src/pattern.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as math from 'mathjs'
// conditionally load jsdom if we don't have a browser environment available.
// note that this is done via require(), which is less than ideal.
//
Expand All @@ -15,12 +14,14 @@ const s = (tagName, attrs = {}, parent) => {
}

export default class Pattern {
constructor (polys, opts) {
constructor (points, polys, opts) {
this.points = points
this.polys = polys
this.opts = opts
}

toSVG = (svgOpts) => {
const points = this.points
const {width, height} = this.opts
const svg = s('svg', {width, height})
const suppressNamespace = svgOpts && svgOpts.includeNamespace === false
Expand All @@ -31,46 +32,85 @@ export default class Pattern {
}

this.polys.forEach((poly, index) => {
// TODO - round to 1 decimal place
const normal = getNormal(poly)
poly.normal = normal
const xy = poly.vertices.map(v => v.slice(0, 2).join(','))
const d = "M" + xy.join("L") + "Z"
const xys = poly.vertexIndices.map(i => `${points[i][0]},${points[i][1]}`)
const d = "M" + xys.join("L") + "Z"
const fill = poly.color.css()
// shape-rendering crispEdges resolves the antialiasing issues
s('path', {
d,
fill,
'data-index': index,
'shape-rendering': 'crispEdges'
}, svg)
})

svg.addEventListener('mousemove', e => {
const LIGHT_LOCATION = [e.clientX, e.clientY, width / 3]
Array.from(svg.children).forEach(path => {
const poly = this.polys[parseInt(path.dataset.index, 10)]
const polyCenter = math.mean(poly.vertices, 0)
const lightVector = math.subtract(LIGHT_LOCATION, polyCenter)
const lightAngle = Math.max(0, math.dot(poly.normal, lightVector))
path.setAttribute('fill', poly.color.darken(0.5).brighten(lightAngle / 400).css())
})
})

return svg
}
}

const getNormal = (poly) => {
const a = poly.vertices[0]
const b = poly.vertices[1]
const c = poly.vertices[2]
const ab = math.subtract(b, a)
const ac = math.subtract(c, a)
// get cross product
const cross = math.cross(ac, ab)
// normalize
const length = Math.sqrt(cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2])
const norm = [cross[0] / length, cross[1] / length, cross[2] / length]
return norm
toCanvas = (destCanvas, _canvasOpts = {}) => {
const defaultCanvasOptions = {retina: true}
const canvasOpts = {...defaultCanvasOptions, _canvasOpts}
const {points, polys, opts} = this
const canvas = destCanvas || doc.createElement('canvas')

const ctx = canvas.getContext('2d')

if (canvasOpts.retina) {
// adapted from https://gist.github.com/callumlocke/cc258a193839691f60dd
const backingStoreRatio = (
ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1
)
const drawRatio = devicePixelRatio / backingStoreRatio
if (devicePixelRatio !== backingStoreRatio) {
// set the 'real' canvas size to the higher width/height
canvas.width = opts.width * drawRatio
canvas.height = opts.height * drawRatio

// ...then scale it back down with CSS
canvas.style.width = opts.width + 'px'
canvas.style.height = opts.height + 'px'
} else {
// this is a normal 1:1 device: don't apply scaling
canvas.width = opts.width
canvas.height = opts.height
canvas.style.width = ''
canvas.style.height = ''
}
ctx.scale(drawRatio, drawRatio)
}

// this works to fix antialiasing with two adjacent edges, but it fails
// horribly at corners...
// ctx.globalCompositeOperation = canvasOpts.compositing || 'lighter' // https://stackoverflow.com/a/53292886/381299

const drawPoly = (poly, fill, stroke) => {
const vertexIndices = poly.vertexIndices
ctx.lineJoin = 'round'
ctx.beginPath()
ctx.moveTo(points[vertexIndices[0]][0], points[vertexIndices[0]][1])
ctx.lineTo(points[vertexIndices[1]][0], points[vertexIndices[1]][1])
ctx.lineTo(points[vertexIndices[2]][0], points[vertexIndices[2]][1])
ctx.closePath()
if (fill) {
ctx.fillStyle = fill.color.css()
ctx.fill()
}
if (stroke) {
ctx.strokeStyle = stroke.color.css()
ctx.lineWidth = stroke.width
ctx.stroke()
}
}

// draw strokes at edge bounds to solve for white gaps while compositing
polys.forEach(poly => drawPoly(poly, null, {color: poly.color, width: 2}))

// draw fills
polys.forEach(poly => drawPoly(poly, {color: poly.color}, null))

return canvas
}
}
46 changes: 17 additions & 29 deletions src/trianglify.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const defaultOptions = {
yColors: 'match',
palette: colorbrewer,
colorSpace: 'lab',
stroke_width: 0,
strokeWidth: 0,
points: null
}

Expand Down Expand Up @@ -66,6 +66,8 @@ export default function trianglify (_opts) {
return opts.palette[colorOpt]
case colorOpt === 'random':
return randomFromPalette()
default:
throw TypeError(`Unrecognized color option: ${colorOpt}`)
}
}

Expand All @@ -74,43 +76,38 @@ export default function trianglify (_opts) {
? xColors
: processColorOpts(opts.yColors)

console.log(xColors, yColors)

const xScale = chroma.scale(xColors).mode(opts.colorSpace)
const yScale = chroma.scale(yColors).mode(opts.colorSpace)

// Our next step is to generate a pseudo-random grid of {x, y , z} points,
// Our next step is to generate a pseudo-random grid of {x, y} points,
// (or to simply utilize the points that were passed to us)
const points = opts.points || getPoints(opts, rand)
// window.document.body.appendChild(debugRender(opts, points))

// Once we have the points array, run the triangulation:
// Once we have the points array, run the triangulation
var geomIndices = Delaunator.from(points).triangles

// And generate geometry and color data:
// ...and then generate geometry and color data:

// use a different randomizer for the color function so that swapping
// out color functions, etc, doesn't change the pattern geometry itself
const colorRand = seedrandom(opts.seed ? opts.seed + 'salt' : undefined)
const polys = []
const triangleIndices = []

for (let i = 0; i < geomIndices.length; i += 3) {
const vertices = [
points[geomIndices[i]],
points[geomIndices[i + 1]],
points[geomIndices[i + 2]]
// convert shallow array-packed vertex indices into 3-tuples
const vertexIndices = [
geomIndices[i],
geomIndices[i + 1],
geomIndices[i + 2]
]

triangleIndices.push(
[geomIndices[i], geomIndices[i + 1], geomIndices[i + 2]]
)
// grab a copy of the actual vertices to use for calculations
const vertices = vertexIndices.map(i => points[i])

const {width, height} = opts
const norm = num => Math.max(0, Math.min(1, num))
const centroid = geom.getCentroid(vertices)
const xPercent = norm(centroid.x / width)
const yPercent = norm(centroid.y / height)
console.log(xPercent, yPercent)

const color = chroma.mix(
xScale(xPercent),
Expand All @@ -120,22 +117,13 @@ export default function trianglify (_opts) {
)

polys.push({
vertices,
vertexIndices,
centroid,
color, // chroma color object
normal: [0, 0, 0] // xyz normal vector
color // chroma color object
})
}

// return new Pattern(polys, opts)

const p = new Pattern(polys, opts)
// hackety hack
p.rawData = {
points,
triangleIndices
}
return p
return new Pattern(points, polys, opts)
}

const getPoints = (opts, random) => {
Expand Down

0 comments on commit de44f2f

Please sign in to comment.