* {
box-sizing: border-box;
}
body {
--space: clamp(1rem, 2vw, 2.5rem);
--shadow: 0.6rem 0.6rem 0;
font-family: 'VT323', monospace;
font-size: 1.2rem;
margin: 0;
padding: calc(var(--space) * 2) var(--space);
background: lightblue;
accent-color: turquoise;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
button {
font-family: inherit;
font-size: inherit;
background: orchid;
border: 0.22rem solid;
border-radius: 0.3rem;
padding: 0.2rem 1rem;
box-shadow: 0.2rem 0.2rem 0;
}
button:hover,
button:focus-visible {
background: orange;
}
input[type="color"] {
border: 0.22rem solid;
border-radius: 0.3rem;
box-shadow: 0.2rem 0.2rem 0;
margin-right: 2rem;
}
h1 {
font-family: 'Press Start 2P', cursive;
font-weight: 400;
margin: 0 0 var(--space);
text-shadow: 0.1em 0.1em 0 orange;
}
p {
margin: 0;
}
header {
outline: 0.3rem solid;
padding: var(--space);
margin-bottom: calc(var(--space) * 2);
box-shadow: var(--shadow);
background: peachpuff;
}
.wrapper {
width: 100%;
max-width: 800px;
margin: 0 auto;
}
@media (min-width: 70em) {
.wrapper {
max-width: 1200px;
display: grid;
grid-template-columns: 2fr 1fr;
gap: 0 2rem;
}
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1rem;
margin-bottom: calc(var(--space) * 1.5);
grid-column: 1;
}
.canvas {
--sizeX: var(--cellSizeCol, calc(100% / var(--colCount, 40)));
--sizeY: var(--cellSizeRow, calc(100% / var(--rowCount, 30)));
grid-column: 1;
position: relative;
outline: 0.3rem solid;
box-shadow: var(--shadow);
margin-bottom: calc(var(--space) * 2);
background: linear-gradient(to right, transparent calc(100% - 1px), lightgrey 0),
linear-gradient(to bottom, transparent calc(100% - 1px), lightgrey 0), white;
background-size: var(--sizeX) 100%, 100% var(--sizeY);
}
.draw-area {
position: relative;
aspect-ratio: 4 / 3;
background-repeat: no-repeat;
background-size: var(--sizeX) var(--sizeY);
}
.marker {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background-image: linear-gradient(to right, var(--bg, black), var(--bg, black));
aspect-ratio: 1;
background-size: var(--sizeX) var(--sizeY);
background-repeat: no-repeat;
opacity: 0;
}
.css label {
display: block;
}
textarea {
display: block;
width: 100%;
border: 0.3rem 0.3rem 0;
box-shadow: var(--shadow);
margin-bottom: var(--space);
padding: 0 1rem;
}
[data-undo] {
background: orange;
}const canvas = d3.select('.canvas')
const colorInput = d3.select('#colorPicker')
const marker = d3.select('.marker')
const clearButton = d3.select('[data-clear]')
const saveButton = d3.select('[data-save]')
const undoButton = d3.select('[data-undo]')
const copyButton = d3.select('[data-copy]')
const textArea = d3.select('#css')
const drawArea = d3.select('.draw-area')
const range = d3.select('#columns')
const width = drawArea.node().offsetWidth
const height = drawArea.node().offsetHeight
let columns = 30
const multiplier = height / width
let rows = columns * multiplier
let cellSize = 100 / columns
let rowSize = 100 / rows
let isPressed = false
const bisect = d3.bisector((d) => d)
const dx = (posX) => {
const stepX = 1 / (columns - 1)
const dataX = d3.range(0, 100, stepX)
const indexX = bisect.center(dataX, posX / width)
return (dataX[indexX] * 100).toFixed(2)
}
const dy = (posY) => {
const stepY = 1 / (rows - 1)
const dataY = d3.range(0, 1, stepY)
const indexY = bisect.center(dataY, posY / height)
return (dataY[indexY] * 100).toFixed(2)
}
let bg = []
let bgPosition = []
const draw = () => {
drawArea
.style('background-image', bg.join(','))
.style('background-position', bgPosition.join(','))
}
const updateText = () => {
textArea.html(`
aspect-ratio: 4 / 3;
background-image: ${bg.join(',')};
background-size: calc(100% / ${columns}) calc(100% / ${rows});
background-position: ${bgPosition.join(',')};
background-repeat: no-repeat;
`)
}
const shouldDraw = (newBgValue, newBgPositionValue) => {
if (!isPressed) return false
if (!bg.length) return true
if (bg[0] !== newBgValue) return true
return bgPosition[0] !== newBgPositionValue
}
canvas.on('click', (e) => {
const color = colorInput.node().value
const [posX, posY] = d3.pointer(e)
const x = dx(posX)
const y = dy(posY)
bg = [ `linear-gradient(${color}, ${color})`, ...bg ]
bgPosition = [ `${x}% ${y}%`, ...bgPosition]
draw()
updateText()
})
canvas
.on('mouseover', () => {
marker.style('opacity', 1)
})
.on('mouseout', () => {
marker.style('opacity', 0)
})
.on('mousedown', () => {
isPressed = true
})
.on('mouseup', () => {
isPressed = false
})
canvas.on('mousemove', (e) => {
const color = colorInput.node().value
const [posX, posY] = d3.pointer(e)
const x = dx(posX)
const y = dy(posY)
marker
.style('background-position', `${x}% ${y}%`)
const newBgValue = `linear-gradient(${color}, ${color})`
const newBgPositionValue = `${x}% ${y}%`
if (!shouldDraw(newBgValue, newBgPositionValue)) return
bg = [ newBgValue, ...bg ]
bgPosition = [ newBgPositionValue, ...bgPosition]
draw()
updateText()
})
colorInput.on('input', () => {
marker.style('--bg', colorInput.node().value)
})
const clear = () => {
bg = []
bgPosition = []
textArea.html('')
drawArea
.style('background-image', '')
.style('background-position', '')
.style('background-size', '')
}
clearButton.on('click', clear)
saveButton.on('click', () => {
const art = textArea.node().value
localStorage.setItem('art', art)
})
const restoreSavedArt = () => {
const savedArt = localStorage.getItem('art')
if (!savedArt) return
drawArea.attr('style', savedArt)
textArea.html(savedArt)
}
const setColumnCount = () => {
columns = range.node().value
rows = columns * multiplier
cellSize = 100 / columns
rowSize = 100 / rows
canvas.style('--cellSizeCol', `${cellSize}%`)
canvas.style('--cellSizeRow', `${rowSize}%`)
canvas.style('--colCount', columns)
}
setColumnCount()
restoreSavedArt()
window.addEventListener('resize', () => {
if (window.innerWidth > width + 100) return
setColumnCount()
})
range.on('input', setColumnCount)
copyButton.on('click', (e) => {
const copyText = textArea.node()
copyText.select()
copyText.setSelectionRange(0, 99999)
navigator.clipboard.writeText(copyText.value)
copyButton.text('Copied!')
setTimeout(() => {
copyButton.text('Copy to clipboard')
}, 3000)
})
undoButton.on('click', () => {
bg.splice(0, 1)
bgPosition.splice(0, 1)
draw()
updateText()
})
0 Reviews