<text-packing></text-packing>
body {
margin: 0;
display: flex;
align-items: center;
justify-content: center;
width: 100vw;
height: 100vh;
}
text-packing {
transform: scale(3, 3);
border-radius: 4px;
width: 120px;
height: 120px;
border-radius: 50%;
box-shadow: 0 0 1px 1px rgba(0,0,0,0.15);
overflow: hidden;
}
class Circle {
constructor(x, y, maxWidth, maxHeight) {
this.x = x
this.y = y
this.maxWidth = maxWidth
this.maxHeight = maxHeight
this.zoom = 1 //+ Math.round(Math.random() * 10)
this.r = Math.random() * this.zoom
this.valid = true
this.padding = 0.9 //Math.round(Math.random() * 10) / 10
this.growthRate = 0.001 //+ Math.round(Math.random() * 10) / 10
}
create() {
this.el = document.createElement("div")
this.update()
}
update() {
this.el.style.transform = `translate(${this.x}px, ${this.y}px)
scale(${this.r * 2 * this.padding}, ${this.r * 2 * this.padding})`
}
grow() {
this.r = Math.round((this.r + this.growthRate) * 1E4) / 1E4;
}
edges() {
return (
this.x - this.r < 0 ||
this.x + this.r > this.maxWidth ||
this.y - this.r < 0 ||
this.y + this.r > this.maxHeight
);
}
}
const textPackingTemplate = document.createElement('template')
textPackingTemplate.innerHTML = `
<style>
:host {
display: block;
--color: black;
}
div {
--size: 1px;
position: absolute;
left: 0;
top: 0;
width: var(--size);
height: var(--size);
border-radius: 50%;
}
</style>
`
class TextPacking extends HTMLElement {
static get colorCombinations() {
return [
[{ h: 35, s: 90, l: 50 }, { h: 110, s: 50, l: 40 }],
[{ h: 80, s: 50, l: 40 }, { h: 25, s: 90, l: 70 }],
[{ h: 100, s: 20, l: 40 }, { h: 355, s: 80, l: 70 }]
]
}
get delta() {
return [
Math.round(-10 + Math.random() * 20),
Math.round(-20 + Math.random() * 40),
Math.round(-20 + Math.random() * 40)
]
}
constructor() {
super()
this.attachShadow({ mode: 'open' })
this.shadowRoot.appendChild(textPackingTemplate.content.cloneNode(true))
const canvas = document.createElement('canvas')
this.context = canvas.getContext('2d')
const idx = Math.floor(TextPacking.colorCombinations.length * Math.random())
this.colorCombination = TextPacking.colorCombinations[idx]
}
getPixels() {
this.context.font = 'italic bold 75px Times'
this.context.textAlign = 'center'
this.context.textBaseline = 'middle'
const number = Math.round(Math.random()*100)
const text = this.textContent.length > 0 ? this.textContent : number
this.context.fillText(text, this.width / 2, this.height / 2)
const pixels = this.context.getImageData(0, 0, this.width, this.height)
const data = pixels.data
let textCoords = []
let bgdCoords = []
for (let i = 0; i < data.length; i += 4) {
const r = data[i]
const g = data[i + 1]
const b = data[i + 2]
const a = data[i + 3]
const index = i / 4
const y = Math.floor(index / this.width)
const x = index - (y * this.width)
if (r + g + b === 0 && a > 0) {
textCoords.push([x, y])
} else {
bgdCoords.push([x, y])
}
}
return [textCoords, bgdCoords]
}
getRandomCoord(idx) {
const index = Math.round(Math.random() * this.pixels[idx].length)
const coord = this.pixels[idx][index]
this.pixels[idx].splice(index, 1)
return coord
}
checkValidity(circle, idx) {
const circles = idx === 0 ? this.textCircles : this.bgdCircles
for (const c of circles) {
if (c !== circle) {
const dist = Math.sqrt(
Math.pow(circle.x - c.x, 2) + Math.pow(circle.y - c.y, 2)
);
if (dist < c.r + circle.r) {
circle.valid = false;
break;
}
}
}
}
newCircle(idx) {
const [x, y] = this.getRandomCoord(idx)
const circle = new Circle(x, y, this.width, this.height)
this.checkValidity(circle, idx)
return circle
}
getColor(hsl) {
const delta = this.delta
return `hsl(
${hsl.h + delta[0]},
${hsl.s + delta[1]}%,
${hsl.l + delta[2]}%
)`
}
circlePacking() {
let i = 0
while (i < 5) {
const textCircle = this.newCircle(0)
if (textCircle.valid) {
textCircle.create()
textCircle.el.style.backgroundColor = this.getColor(this.colorCombination[1])
this.shadowRoot.appendChild(textCircle.el)
this.textCircles.push(textCircle)
}
for (const c of this.textCircles) {
this.checkValidity(c)
if (!c.edges() && c.valid) {
c.grow()
c.update()
}
}
i++
}
let j = 0
while (j < 15) {
const bgdCircle = this.newCircle(1)
if (bgdCircle.valid) {
bgdCircle.create()
bgdCircle.el.style.backgroundColor = this.getColor(this.colorCombination[0])
this.shadowRoot.appendChild(bgdCircle.el)
this.bgdCircles.push(bgdCircle)
}
for (const c of this.bgdCircles) {
this.checkValidity(c)
if (!c.edges() && c.valid) {
c.grow()
c.update()
}
}
j++
}
if (this.textCircles.length < 1000 && this.bgdCircles.length < 3000)
requestAnimationFrame(this.circlePacking)
}
init() {
this.textCircles = []
const [textX, textY] = this.getRandomCoord(0)
this.textCircles.push(new Circle(textX, textY, this.width, this.height))
this.textCircles[0].create()
this.shadowRoot.appendChild(this.textCircles[0].el)
this.bgdCircles = []
const [bgdX, bgdY] = this.getRandomCoord(1)
this.bgdCircles.push(new Circle(bgdX, bgdY, this.width, this.height))
this.bgdCircles[0].create()
this.shadowRoot.appendChild(this.bgdCircles[0].el)
}
connectedCallback() {
this.width = this.context.canvas.width = this.shadowRoot.host.clientWidth
this.height = this.context.canvas.height = this.shadowRoot.host.clientHeight
this.pixels = this.getPixels()
this.init()
// this.newCircle = this.newCircle.bind(this)
this.getRandomCoord = this.getRandomCoord.bind(this)
this.circlePacking = this.circlePacking.bind(this)
this.circlePacking()
}
}
customElements.define('text-packing', TextPacking)
This Pen doesn't use any external CSS resources.