Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@tompng
Last active October 28, 2020 16:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tompng/1bc627c95f9391016d96a4ed855858f7 to your computer and use it in GitHub Desktop.
Save tompng/1bc627c95f9391016d96a4ed855858f7 to your computer and use it in GitHub Desktop.
css nazca cat
<style>
body{
background-image: url(nazca.png);
background-size: 1024px;
background-repeat: no-repeat;
}
</style>
<script src='curve.js'></script>
<script>
onload = () => {
const lineWidth = 16
parentSelector = '#a'
// stomach
addPath([{x:680,y:357},{x:640,y:375},{x:596,y:388},{x:546,y:405},{x:533,y:457},{x:571,y:480},{x:626,y:474},{x:674,y:448},{x:695,y:405},{x:704,y:371}], lineWidth, true)
// front leg
addPath([{x:737,y:401},{x:737,y:447},{x:715,y:498},{x:686,y:527},{x:678,y:563},{x:718,y:561},{x:752,y:522},{x:769,y:487}], lineWidth)
addPath([{x:826,y:441},{x:819,y:487},{x:787,y:537}], lineWidth)
// face
addPath([{x:792,y:123},{x:818,y:120},{x:848,y:78},{x:872,y:31},{x:897,y:40},{x:896,y:97},{x:897,y:159},{x:893,y:205},{x:908,y:251},{x:915,y:306},{x:899,y:351},{x:876,y:390},{x:825,y:407},{x:774,y:395},{x:731,y:366},{x:710,y:326},{x:689,y:290},{x:684,y:252},{x:697,y:204},{x:697,y:153},{x:697,y:90},{x:694,y:37},{x:722,y:24},{x:754,y:67}], lineWidth, true)
addPath([{x:761,y:313},{x:770,y:347},{x:806,y:352},{x:831,y:334},{x:828,y:304},{x:794,y:304}], lineWidth, true)
addPath([{x:762,y:201},{x:747,y:215},{x:747,y:243},{x:776,y:248},{x:793,y:234},{x:789,y:209}], lineWidth, true)
addPath([{x:850, y: 205}, {x:824,y:212},{x:820,y:237},{x:828,y:255},{x:860,y:256},{x:869,y:234},{x:863,y:215}], lineWidth, true)
// tail pattern
addPath([{x:83,y:454},{x:90,y:460},{x:122,y:444},{x:148,y:425},{x:176,y:415},{x:202,y:420},{x:201,y:434}], lineWidth*5/8)
addPath([{x:118,y:460},{x:110,y:480}], lineWidth*5/8)
addPath([{x:145,y:442},{x:140,y:461}], lineWidth*5/8)
addPath([{x:160,y:453},{x:164,y:432}], lineWidth*5/8)
addPath([{x:182,y:431},{x:180,y:445}], lineWidth*5/8)
// back leg
addPath([{x:398,y:470},{x:475,y:450},{x:494,y:390},{x:410,y:373},{x:378,y:420},{x:367,y:476},{x:353,y:585},{x:453,y:708},{x:523,y:634},{x:463,y:562}], lineWidth)
// tail leg
addPath([{x:275,y:529},{x:257,y:606},{x:294,y:685}], lineWidth)
addPath([{x:182,y:516},{x:145,y:566},{x:105,y:615}], lineWidth)
// tail
addPath([{x:273,y:344},{x:231,y:359},{x:163,y:377},{x:107,y:406},{x:50,y:452},{x:50,y:502},{x:82,y:506},{x:157,y:494},{x:213,y:503},{x:256,y:510}], lineWidth)
// back
addPath([{x:670,y:238},{x:622,y:230},{x:595,y:240},{x:570,y:274},{x:548,y:297},{x:498,y:295},{x:442,y:296},{x:394,y:294},{x:311,y:323}], lineWidth)
const arr = []
document.body.onclick=e=>{
const p = { x: e.pageX, y: e.pageY }
arr.push(p)
console.log(JSON.stringify(arr).replace(/"/g,''))
addLineCap(p, lineWidth)
applyStyles()
}
const style = document.createElement('style')
function applyStyles() {
const baseStyles = `
i{position:absolute;overflow:hidden;}
i:before{content:'';position:absolute;box-sizing:border-box;border:0px solid red;transform-origin: center;}
`
const bgStyles = `
.background{
background:${curveBackgrounds.join(',')};
}
`
style.textContent = baseStyles + '\n' + curveStyles.join('\n') + '\n' + bgStyles
document.head.appendChild(style)
}
applyStyles()
}
</script>
<div id='view'>
<div id='a'></div>
<div id='b'></div>
</div>
<div class='background' style='width:1024px;height:800px;position:absolute;top:800px;'></div>
const curveStyles = []
const curveBackgrounds = []
let curveColor = 'red'
function solve2(a, b, c, d, x, y) {
const det = a * d - b * c
return [(d * x - b * y) / det, (a * y - c * x) / det]
}
function normalizeRadian(th, from = -Math.PI) {
return th - Math.floor((th - from) / 2 / Math.PI) * 2 * Math.PI
}
function thetaDiff(t1, t2) {
return normalizeRadian(t2 - t1)
}
function splitCircle({ x, y, r, thStart, thEnd }) {
if (thStart > thEnd) [thStart, thEnd] = [thEnd, thStart]
const circles = []
while (thStart < thEnd) {
const t2 = Math.min(thEnd, (Math.floor(thStart / (Math.PI / 2)) + 1)* Math.PI / 2)
circles.push({ x, y, r, thStart, thEnd: t2 })
thStart = t2
}
return circles
}
function curveToArcs(p1, p2) {
const th = Math.atan2(p2.y - p1.y, p2.x - p1.x)
const cos1 = Math.cos(p1.th)
const sin1 = Math.sin(p1.th)
const cos2 = Math.cos(p2.th)
const sin2 = Math.sin(p2.th)
const length = Math.hypot(p2.x - p1.x, p2.y - p1.y)
const s1 = Math.sin(p1.th - th)
const c1 = Math.cos(p1.th - th)
const s2 = Math.sin(p2.th - th)
const c2 = Math.cos(p2.th - th)
if (s1 * s2 < 0) {
// r1*s1+r2*(-s2)=length
// r1*(1-c1)=r2*(1-c2)
// a+b=length
// a*x=b*y
const a = s2 * (1 - c1)
const b = -s1 * (1 - c2)
const r1 = length * b / (a + b) / s1
const r2 = -length * a / (a + b) / s2
return [
{
x: p1.x + r1 * sin1,
y: p1.y - r1 * cos1,
r: Math.abs(r1),
...(
r1 > 0 ? {
thStart: th + Math.PI / 2,
thEnd: th + Math.PI / 2 + normalizeRadian(p1.th - th)
} : {
thStart: p1.th - Math.PI / 2,
thEnd: p1.th - Math.PI / 2 + normalizeRadian(th - p1.th)
}
)
},
{
x: p2.x + r2 * sin2,
y: p2.y - r2 * cos2,
r: Math.abs(r2),
...(
r2 > 0 ? {
thStart: p2.th + Math.PI / 2,
thEnd: p2.th + Math.PI / 2 + normalizeRadian(th - p2.th)
} : {
thStart: th - Math.PI / 2,
thEnd: th - Math.PI / 2 + normalizeRadian(p2.th - th)
}
)
}
]
}
// |(p1+v1*r1)-(p2-v2*r2)|=2*r
// a*r*r+b*r+c=0
const a = 4 - (cos1 + cos2) ** 2 - (sin1 + sin2) ** 2
const b = 2 * ((p2.x - p1.x) * (sin1 + sin2) - (p2.y - p1.y) * (cos1 + cos2))
const c = -(length ** 2)
const ra = b / a / 2
const rb = Math.sqrt(b * b - 4 * a * c) / a / 2
const r = Math.abs(a) < 1e-8 ? c / b : ra > 0 ? ra - rb : ra + rb
const circle1 = { x: p1.x - r * sin1, y: p1.y + r * cos1, r: Math.abs(r) }
const circle2 = { x: p2.x + r * sin2, y: p2.y - r * cos2, r: Math.abs(r) }
const th2 = Math.atan2(circle2.y - circle1.y, circle2.x - circle1.x)
return [
{
...circle1,
...(
r > 0 ? {
thStart: p1.th - Math.PI / 2,
thEnd: p1.th - Math.PI / 2 + normalizeRadian(th2 - p1.th + Math.PI / 2, 0 * Math.PI),
} : {
thStart: p1.th + Math.PI / 2,
thEnd: p1.th + Math.PI / 2 + normalizeRadian(th2 - p1.th - Math.PI / 2, -1 * Math.PI),
}
),
},
{
...circle2,
...(
r > 0 ? {
thStart: p2.th + Math.PI / 2,
thEnd: p2.th + Math.PI / 2 + normalizeRadian(th2 - p2.th + Math.PI / 2, 0 * Math.PI)
} : {
thStart: th2 - Math.PI,
thEnd: th2 - Math.PI + normalizeRadian(p2.th - th2 + Math.PI / 2, 0 * Math.PI)
}
),
}
]
}
function curveToSegments(p1, p2, recursive = true) {
const cos1 = Math.cos(p1.th)
const sin1 = Math.sin(p1.th)
const cos2 = Math.cos(p2.th)
const sin2 = Math.sin(p2.th)
const th = Math.atan2(p2.y - p1.y, p2.x - p1.x)
const length = Math.hypot(p2.x - p1.x, p2.y - p1.y)
const thDiff = normalizeRadian(p2.th - p1.th)
if (Math.max(Math.abs(normalizeRadian(p2.th - th)), Math.abs(normalizeRadian(p1.th - th))) < 1e-5) {
return [[{ ...p1, length }], []]
}
let [la, lb] = solve2(cos1, cos2, sin1, sin2, p2.x - p1.x, p2.y - p1.y)
if (la < length / 8 || lb < length / 8 && recursive) {
const a = { x: p1.x + cos1 * length / 3, y: p1.y + sin1 * length / 3 }
const b = { x: p2.x - cos2 * length / 3, y: p2.y - sin2 * length / 3 }
const th2 = Math.atan2(b.y - a.y, b.x - a.x)
const center = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, th: th2 }
const [ls1, cs1] = curveToSegments(p1, center, false)
const [ls2, cs2] = curveToSegments(center, p2, false)
return [[...ls1, ...ls2], [...cs1, ...cs2]]
}
if (la < 0 || lb < 0) return [[{ ...p1, length }], []]
const r = Math.min(la, lb) * Math.tan((Math.PI - Math.abs(thDiff)) / 2)
const base = la < lb ? p1 : p2
const dir = thDiff > 0 ? 1 : -1
const cx = base.x - r * dir * Math.sin(base.th)
const cy = base.y + r * dir * Math.cos(base.th)
let thStart = normalizeRadian(base.th - dir * Math.PI / 2)
let thEnd = thStart + (la < lb ? 1 : -1) * thDiff
const line = { ...(la < lb ? p2 : p1), length: la - lb }
const circles = splitCircle({ x: cx, y: cy, r, thStart, thEnd })
return [[line], circles]
}
let parentSelector = null
function addCSSElement(base, before) {
const el = document.createElement('i')
const parent = document.querySelector(parentSelector || 'body')
parent.appendChild(el)
const selector = `${parentSelector ? parentSelector + ' ' : ''}i:nth-child(${parent.querySelectorAll('i').length})`
function cssToString(css) {
const rules = []
for (const key in css) {
const value = ['left', 'top', 'width', 'height'].includes(key) ? round(css[key]) + 'px' : css[key]
rules.push(key.replace(/[A-Z]/, v => '-' + v.toLocaleLowerCase()) + ':' + value + ';')
}
return rules.join('')
}
curveStyles.push(`${selector}{${cssToString(base)}}`)
curveStyles.push(`${selector}:before{${cssToString(before)}}`)
}
function addLineCap({ x, y }, w) {
const lx = x - w / 2
const ly = y - w / 2
addCSSElement(
{ left: Math.floor(lx), top: Math.floor(ly), width: w + 1, height: w + 1 },
{
left: 0, top: 0, width: w, height: w,
borderRadius: '50%',
transform: `translate(${lx - Math.floor(lx)}px,${ly - Math.floor(ly)}px)`,
border: 'none',
background: curveColor
}
)
addCircleBackground(x - w / 2, y - w / 2, w, w, x, y, w / 2)
}
function round(v, n = 100) {
return Math.round(v * n) / n
}
function addArc({ x, y, r, thStart, thEnd }, w) {
const xs = []
const ys = []
;[thStart, thEnd].forEach(th => {
[-1, 1].forEach(wdir => {
xs.push(x + (r + wdir * w / 2) * Math.cos(th))
ys.push(y + (r + wdir * w / 2) * Math.sin(th))
})
})
const left = Math.floor(Math.min(...xs))
const top = Math.floor(Math.min(...ys))
const cx = x - left - r - w / 2
const cy = y - top - r - w / 2
const csize = 2 * r + w
const icx = Math.floor(cx)
const icy = Math.floor(cy)
const icsize = Math.floor(csize)
addCSSElement(
{ left, top, width: Math.ceil(Math.max(...xs) - left), height: Math.ceil(Math.max(...ys) - top) },
{
left: icx, top: icy, width: icsize, height: icsize,
transform: `translate(${round(cx - icx + (csize - icsize) / 2)}px,${round(cy - icy + (csize - icsize) / 2)}px)scale(${round(csize / icsize, 10000)})`,
borderWidth: `${w}px`,
borderRadius: '50%',
}
)
addCircleBackground(
Math.min(...xs),
Math.min(...ys),
Math.max(...xs) - Math.min(...xs),
Math.max(...ys) - Math.min(...ys),
x,
y,
r,
w
)
}
function addCircleBackground(x, y, w, h, cx, cy, r, lw) {
const d = 0.25
let color = `blue ${round(r - d)}px,transparent ${round(r + d)}px`
const ix = Math.floor(x)
const iy = Math.floor(y)
const iw = Math.ceil(x + w) - ix
const ih = Math.ceil(y + h) - iy
if (lw !== undefined){
const r1 = round(r - lw / 2)
const r2 = round(r + lw / 2)
color = `transparent ${round(r - lw / 2 - d)}px,blue ${round(r - lw / 2 + d)}px,blue ${round(r + lw / 2 - d)}px,transparent ${round(r + lw / 2 + d)}px`
}
curveBackgrounds.push(`radial-gradient(circle at ${round(cx-ix)}px ${round(cy-iy)}px,${color}) ${ix}px ${iy}px/${iw}px ${ih}px no-repeat`)
}
function addLineWithCap({ x, y, length, th }, w){
const cos = Math.cos(th)
const sin = Math.sin(th)
const xs = [x, x + length * cos]
const ys = [y, y + length * sin]
const left = Math.floor(Math.min(...xs) - w / 2)
const top = Math.floor(Math.min(...ys) - w / 2)
const center = { x: x + cos * length / 2, y: y + sin * length / 2 }
const width = Math.ceil(Math.abs(length) + w)
const lx = center.x - left - width / 2
const ly = center.y - top - w / 2
addCSSElement(
{ left, top, width: Math.ceil(Math.max(...xs) + w - left), height: Math.ceil(Math.max(...ys) + w - top) },
{
left: Math.floor(lx),
top: Math.floor(ly),
width,
height: w,
borderRadius: `${w / 2}px`,
transform: `translate(${round(lx - Math.floor(lx))}px,${round(ly - Math.floor(ly))}px)rotate(${round(th * 180 / Math.PI, 1000)}deg)`,
background: curveColor,
border: 'none',
}
)
;[0, 1].forEach(i => {
const x = xs[i]
const y = ys[i]
addCircleBackground(x - w / 2, y - w / 2, w, w, x, y, w / 2)
})
addLineBackground(x, y, length, th, w)
}
function addLine({ x, y, length, th }, w, cap = false){
const xs = []
const ys = []
const cos = Math.cos(th)
const sin = Math.sin(th)
;[0, 1].forEach(t => {
[-1, 1].forEach(wdir => {
xs.push(x + t * length * cos + wdir * w / 2 * sin)
ys.push(y + t * length * sin - wdir * w / 2 * cos)
})
})
const left = Math.floor(Math.min(...xs))
const top = Math.floor(Math.min(...ys))
const center = { x: x + cos * length / 2, y: y + sin * length / 2 }
const width = Math.ceil(Math.abs(length) + w)
const lx = center.x - left - width / 2
const ly = center.y - top - w / 2
addCSSElement(
{ left, top, width: Math.ceil(Math.max(...xs) - left), height: Math.ceil(Math.max(...ys) - top) },
{
left: Math.floor(lx),
top: Math.floor(ly),
width,
height: w,
transform: `translate(${round(lx - Math.floor(lx))}px,${round(ly - Math.floor(ly))}px)rotate(${round(th * 180 / Math.PI, 1000)}deg)`,
background: curveColor,
border: 'none',
}
)
addLineBackground(x, y, length, th, w)
}
function addLineBackground(x, y, length, th, w) {
const xs = []
const ys = []
const cos = Math.cos(th)
const sin = Math.sin(th)
;[0, 1].forEach(t => {
[-1, 1].forEach(wdir => {
xs.push(x + t * length * cos + wdir * w / 2 * sin)
ys.push(y + t * length * sin - wdir * w / 2 * cos)
})
})
const left = Math.floor(Math.min(...xs))
const top = Math.floor(Math.min(...ys))
const right = Math.ceil(Math.max(...xs))
const bottom = Math.ceil(Math.max(...ys))
const pxs = []
;[left, right].forEach(px => {
;[top, bottom].forEach(py => {
pxs.push((py - y) * cos - (px - x) * sin)
})
})
const min = Math.min(...pxs)
const stop1 = -w / 2 - min
const stop2 = w / 2 - min
const d = 0.25
const color = `transparent ${round(stop1 - d)}px,blue ${round(stop1 + d)}px,blue ${round(stop2 - d)}px,transparent ${round(stop2 + d)}px`
curveBackgrounds.push(`linear-gradient(${round((th * 180 / Math.PI))}deg,${color}) ${left}px ${top}px/${right - left}px ${bottom - top}px no-repeat`)
}
function addPath(points, w, closed = false) {
if (points.length === 2) {
const dx = points[1].x - points[0].x
const dy = points[1].y - points[0].y
addLineWithCap({ ...points[0], length: Math.hypot(dx, dy), th: Math.atan2(dy, dx) }, w)
return
}
const size = points.length
var params = points.map(({ x, y }) => ({ x, y, dx: 0, dy: 0 }))
for (let n = 0; n < 4; n++) {
for (let i = 0; i < size; i++) {
let ia = (i - 1 + size) % size
let ib = (i + 1) % size
let k = 4
if (!closed) {
if (i === 0) { ia = i; k = 2 }
if (i === size - 1) { ib = i; k = 2 }
}
const cx = 3 * (params[ib].x - params[ia].x)
const cy = 3 * (params[ib].y - params[ia].y)
const pa = params[ia]
const pb = params[ib]
params[i].dx = (cx - pa.dx - pb.dx) / k
params[i].dy = (cy - pa.dy - pb.dy) / k
}
}
const pointsWithTheta = params.map(({ x, y, dx, dy }) => ({ x, y, th: Math.atan2(dy, dx) }))
for (let i = 0; i < size - (closed ? 0 : 1); i++) {
const a = pointsWithTheta[i]
const b = pointsWithTheta[(i + 1) % size]
const circles = curveToArcs(a, b)
circles.forEach(c => splitCircle(c).forEach(c => addArc(c, w)))
}
if (!closed) {
addLineCap(points[0], w)
addLineCap(points[points.length - 1], w)
}
}
<style>
#view{
transform-style: preserve-3d;
position:absolute;left:0;top:0;width:1000px;height:600px;
background-color:gray;
}
body{perspective:1000px;}
#view>i{
transform-style: flat;
/* background:radial-gradient(red,blue) 0px 0px/32px 32px; */
background: #aaa;
}
#view>i:nth-child(n+58):nth-child(-n+84){
background: #ccc;
}
</style>
<body>
<script src='polygon.js'></script>
<div id='view' class='background'>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
<i></i><i></i><i></i>
</div>
function round(v, n=100) {
return Math.round(v*n)/n
}
function solve2(a, b, c, d, x, y) {
const det = a * d - b * c
return [(d * x - b * y) / det, (a * y - c * x) / det]
}
function polygon(pa, pb, pc, selectors) {
const ab = { x: (pa.x + pb.x) / 2, y: (pa.y + pb.y) / 2, z: (pa.z + pb.z) / 2 + Math.random() }
const bc = { x: (pb.x + pc.x) / 2, y: (pb.y + pc.y) / 2, z: (pb.z + pc.z) / 2 + Math.random() }
const ca = { x: (pc.x + pa.x) / 2, y: (pc.y + pa.y) / 2, z: (pc.z + pa.z) / 2 + Math.random() }
square(pa, ab, ca, selectors[0])
square(pb, bc, ab, selectors[1])
square(pc, ca, bc, selectors[2])
// if ((pb.x - pa.x) * (pc.y - pa.y) - (pb.y - pa.y) * (pc.x - pa.x) < 0) {
// return polygon(pa, pc, pb, selector)
// }
// const xs = [pa.x, pb.x, pc.x]
// const ys = [pa.y, pb.y, pc.y]
// const left = Math.floor(Math.min(...xs))
// const right = Math.ceil(Math.max(...xs))
// const top = Math.floor(Math.min(...ys))
// const bottom = Math.ceil(Math.max(...ys))
}
function square(pa, pb, pc, selector) {
const size = 100
xf = { x: (pb.x - pa.x) / size, y: (pb.y - pa.y) / size }
yf = { x: (pc.x - pa.x) / size, y: (pc.y - pa.y) / size }
const [ax, ay] = solve2(pb.x - pa.x, pb.y - pa.y, pc.x - pa.x, pc.y - pa.y, pb.z - pa.z, pc.z - pa.z)
// matrix3d = [
// 1,0,0,0,
// 0,1,0,0,
// ax,ay,1,pa.z-ax*pa.x-ay*pa.y,
// 0,0,0,1
// ]
// matrix2d = [
// xf.x, yf.x, 0, pa.x,
// xf.y, yf.y, 0, pa.y,
// 0, 0, 1, 0,
// 0, 0, 0, 1,
// ]
const matrix = [
xf.x, xf.y, ax * xf.x + ay * xf.y, 0,
yf.x, yf.y, ax * yf.x + ay * yf.y, 0,
0, 0, 1, 0,
pa.x, pa.y, ax * pa.x + ay * pa.y + pa.z - ax * pa.x - ay * pa.y, 1
]
const styles = []
styles.push(`${selector}{
position:absolute;
left:0;
top:0;
width:${size}px;
height:${size}px;
overflow:hidden;
/*background-color:#${Math.floor(0x100+Math.random()*0xeff).toString(16)};*/
transform-origin:0 0;
transform:matrix3d(${matrix.map(v => round(v, 10000000000)).join(',')});
}`)
const patternEnabled = false
if (patternEnabled) {
// P = xf * x + yf * y + pa
// P - pa = [xf,yf] * p
// p = [xf,yf]**(-1) * (P - pa)
const det = xf.x * yf.y - xf.y * yf.x
// |yf.y/det, -yf.x/det| * |-pa.x|
// |-xf.y/det, xf.x/det| * |-pa.y|
const mat2 = [
yf.y / det, -xf.y / det,
-yf.x / det, xf.x / det,
(-pa.x * yf.y + pa.y * yf.x) / det, (xf.y * pa.x - xf.x * pa.y) / det
]
const pd = {
x: pb.x + pc.x - pa.x,
y: pb.y + pc.y - pa.y
}
styles.push(`${selector}>i{
content:'';
position:absolute;
left:0;
top:0;
width:${Math.ceil(Math.max(pa.x, pb.x, pc.x, pd.x))}px;
height:${Math.ceil(Math.max(pa.y, pb.y, pc.y, pd.y))}px;
transform:matrix(${mat2.map(v => round(v, 10000000000)).join(',')});
transform-origin:0 0;
}`)
}
const style = document.createElement('style')
style.textContent=styles.join('')
document.head.appendChild(style)
}
// polygon(
// { x: 100, y: 120, z: 100 },
// { x: 400, y: 210, z: 100 },
// { x: 150, y: 520, z: 100 },
// ['#view>i:nth-child(1)','#view>i:nth-child(2)','#view>i:nth-child(3)']
// )
// polygon(
// { x: 150, y: 520, z: 100 },
// { x: 400, y: 210, z: 100 },
// { x: 500, y: 400, z: 100 },
// ['#view>i:nth-child(4)','#view>i:nth-child(5)','#view>i:nth-child(6)']
// )
// polygon(
// { x: 200, y: 200, z: 200 },
// { x: 400, y: 400, z: 100 },
// { x: 400, y: 200, z: 0 },
// ['#view>i:nth-child(7)','#view>i:nth-child(8)','#view>i:nth-child(9)']
// )
const outline = [
{ x: 650, y: 500, z: 0 },
{ x: 300, y: 500, z: 0 },
{ x: 0, y: 400, z: 0 },
{ x: 100, y: 200, z: 0 },
{ x: 320, y: 20, z: 0 },
{ x: 700, y: 0, z: 0 },
{ x: 800, y: 50, z: 0 },
{ x: 1000, y: 250, z: 0 },
{ x: 900, y: 450, z: 0 },
{ x: 665, y: 505, z: 0 }
]
const hratio = 3 / 5
const stairs = [
[{ x: 650, y: 500 }, { x: 670, y: 440 }],
[{ x: 690, y: 420 }, { x: 780, y: 390 }],
[{ x: 780, y: 390 }, { x: 710, y: 180 }]
].map(ps => ps.map(({ x, y }) => ({ x, y, z: (500 - y) * hratio })))
const stairs2 = stairs.map(ps => ps.map(({ x, y }) => {
const dx = ps[1].x - ps[0].x
const dy = ps[1].y - ps[0].y
const dr = Math.hypot(dx, dy) / Math.sqrt(250)
let lx = -Math.round(dy / dr)
let ly = +Math.round(dx / dr)
return {
x: x + lx,
y: y + ly,
z: (500 - y) * hratio
}
}))
console.log(stairs)
console.log(stairs2)
let selectorCount = 0
function nextSelector() {
return `#view>i:nth-child(${++selectorCount})`
}
function nextSelectors() {
return [nextSelector(), nextSelector(), nextSelector()]
}
const center = { x: 250, y: 250, z: 250 * hratio }
const cs = [stairs[2][1], stairs[1][1], stairs[1][0], stairs[0][1], ...outline.slice(0, 5)]
for (let i = 0; i < cs.length - 1; i++) {
polygon(cs[i], cs[i+1], center, nextSelectors())
}
const center2 = stairs[2][1]
const center3 = stairs2[2][1]
polygon(outline[4], center2, center, nextSelectors())
polygon(outline[4], outline[5], center2, nextSelectors())
polygon(outline[5], outline[6], center2, nextSelectors())
polygon(center3, outline[6], center2, nextSelectors())
polygon(center3, outline[6], outline[7], nextSelectors())
polygon(center3, outline[7], stairs2[2][0], nextSelectors())
polygon(outline[8], outline[7], stairs2[2][0], nextSelectors())
polygon(outline[8], stairs2[1][1], stairs2[2][0], nextSelectors())
polygon(outline[8], stairs2[1][1], stairs2[1][0], nextSelectors())
polygon(outline[8], stairs2[0][1], stairs2[1][0], nextSelectors())
polygon(outline[8], outline[9], stairs2[0][1], nextSelectors())
polygon(stairs[0][0], stairs[0][1], stairs2[0][0], nextSelectors())
polygon(stairs2[0][1], stairs[0][1], stairs2[0][0], nextSelectors())
polygon(stairs[0][1], stairs2[0][1], stairs[1][0], nextSelectors())
polygon(stairs[1][0], stairs2[0][1], stairs2[1][0], nextSelectors())
polygon(stairs[1][0], stairs[1][1], stairs2[1][0], nextSelectors())
polygon(stairs2[1][1], stairs[1][1], stairs2[1][0], nextSelectors())
polygon(stairs2[1][1], stairs[1][1], stairs2[2][0], nextSelectors())
polygon(stairs[2][0], stairs[2][1], stairs2[2][0], nextSelectors())
polygon(stairs2[2][1], stairs[2][1], stairs2[2][0], nextSelectors())
<script src='curve.js'></script>
<style>
#scene{
position:absolute;
left: 0;
top: 0;
width: 1024px;
height: 768px;
box-shadow: 0 0 1px black;
overflow: hidden;
perspective: 2000px;
}
#view{
position: absolute;
left: -512px;
top: -512px;
width: 2048px;
height: 2048px;
transform-style: preserve-3d;
background:#dcb;
animation: camera 20s linear 0s infinite;
}
#bottom{
position:absolute;
left:512px;top:512px;
width:1024px;
height:1024px;
border-top-left-radius: 100%;
border-bottom-right-radius: 80%;
background:#cba;
transform:scaleY(1.5)skewX(-10deg)rotate(45deg);
}
#edge{
position:absolute;
left:512px;top:512px;
width:1024px;
height:1024px;
border-top-left-radius: 100%;
border-bottom-right-radius: 60%;
background:#cba;
transform:scaleZ(0.8)rotateX(90deg)skewX(10deg)rotate(45deg);
}
#cat1 i,#cat2 i{position:absolute;overflow:hidden;}
#cat1 i:before,#cat2 i:before{content:'';position:absolute;box-sizing:border-box;border:0px solid #dcb;transform-origin: center;}
#cat1,#cat2{transform-origin:0 0;}
#cat1{transform:translate3d(600px,1240px,355px)rotateX(-45deg)scale(0.7)}
#cat2{transform:translate3d(600px,1240px,355px)rotateX(-45deg)scale(0.7)translate(290px,250px)rotateZ(5deg)rotateY(-15deg)rotateZ(-5deg)translate(-290px,-250px)}
</style>
<script>
curveColor='#dcb'
function round(v) { return Math.round(v * 100) / 100}
const styles = []
;(() => {
const frames = []
for ( let i = 0; i <= 100; i+= 2) {
const t = i / 100
const th = 2 * Math.PI * t
frames.push(`${i}%{transform:rotateX(${round(70 + 5 * Math.cos(th))}deg)rotateZ(${round(25 * Math.sin(th))}deg)translate(${round(-Math.sin(th)*200)}px,-800px);}`)
}
const style = document.createElement('style')
styles.push('@keyframes camera{', ...frames, '}')
})()
function addZPole(p, h, color) {
const el = document.createElement('i')
const div = document.querySelector('#items')
div.appendChild(el)
styles.push(`#items i:nth-child(${div.children.length}){
position:absolute;
left:-1px;
top:0;
width: 2px;
height: ${h}px;
background:${color};
transform-origin:50% 0;
transform:translate3d(${round(p.x)}px,${round(p.y)}px,${round(p.z)}px)rotateX(90deg);
}`)
}
function addLine(p1, p2, color, lw = 2) {
const len3 = Math.hypot(p2.x - p1.x, p2.y - p1.y, p2.z - p1.z)
const el = document.createElement('i')
const div = document.querySelector('#items')
div.appendChild(el)
const len2 = Math.hypot(p2.x - p1.x, p2.y - p1.y)
const thz = Math.atan2(p2.z - p1.z, len2)
const th = Math.atan2(p2.y - p1.y, p2.x - p1.x)
const c = {
x: (p1.x + p2.x) / 2,
y: (p1.y + p2.y) / 2,
z: (p1.z + p2.z) / 2
}
styles.push(`#items i:nth-child(${div.children.length}){
position:absolute;
left:${-len3 / 2}px;
top:-${lw / 2}px;
width: ${round(len3)}px;
height: ${lw}px;
background:${color};
transform:translate3d(${round(c.x)}px,${round(c.y)}px,${round(c.z)}px)rotateZ(${round(th * 180 / Math.PI)}deg)rotateY(${round(-thz * 180 / Math.PI)}deg);
}`)
}
function applyStyles() {
const style = document.createElement('style')
style.textContent = styles.join('\n') + curveStyles.join('\n')
document.head.appendChild(style)
}
function mixPoint(a, b, t) {
const s = 1 - t
return { x: a.x * s + t * b.x, y: a.y * s + t * b.y, z: a.z * s + t * b.z }
}
onload = () => {
const coords1 = [
{ x: 1100, y: 1580, z: 0 },
{ x: 1110, y: 1540, z: 50 },
{ x: 1200, y: 1510, z: 60 },
{ x: 1430, y: 1250, z: 150 },
{ x: 1380, y: 1180, z: 240 },
{ x: 1310, y: 1100, z: 320 },
]
const coords2 = [
{ x: 1120, y: 1576, z: 0 },
{ x: 1127, y: 1549, z: 50 },
{ x: 1210, y: 1520, z: 60 },
{ x: 1450, y: 1250, z: 150 },
{ x: 1393, y: 1170, z: 240 },
{ x: 1330, y: 1100, z: 320 },
]
;[coords1, coords2].forEach(coords => {
const points = []
for (let i = 0; i < coords1.length - 1; i++) {
const a = coords[i]
const b = coords[i + 1]
const len = Math.hypot(b.x - a.x, b.y - a.y)
const n = Math.ceil(len / 20)
for (let j = 0; j < n + (i == 2 ? 1 : 0); j++) {
points.push(mixPoint(a, b, j / n))
}
}
points.push(coords[coords.length - 1])
points.forEach(p => addZPole(p, 16, '#aaa'))
for (let i = 0; i < points.length - 1; i++) {
addLine(...[points[i], points[i + 1]].map(a => ({ ...a, z: a.z + 12 })), '#aaa')
}
})
for (let i = 0; i < coords1.length - 1; i++) {
const a1 = coords1[i]
const a2 = coords1[i + 1]
const b1 = coords2[i]
const b2 = coords2[i + 1]
const len = (Math.hypot(a2.x - a1.x, a2.y - a1.y) + Math.hypot(b2.x - b1.x, b2.y - b1.y)) / 2
const n = Math.ceil(len / 3)
for (let j = 0; j < n + (i == 2 ? 1 : 0); j++) {
const t = j / n
addLine(
mixPoint(a1, a2, t),
mixPoint(b1, b2, t),
'#ccc',
4
)
}
}
addCat()
applyStyles()
const styles = document.querySelectorAll('style')
const cssData = [...styles].map(s => s.textContent).join('\n').replace(/\n +|\n+/g, '\n')
const html = "<link rel='stylesheet' href='style.css'></style>\n" + document.querySelector('#scene').outerHTML.replace(/\n +|\n+/g, '\n')
const downloads = [['style.css', cssData], ['index.html', html]]
const dlDiv = document.createElement('div')
downloads.forEach(([fileName, data]) => {
const url = URL.createObjectURL(new Blob([data], { type : 'text/html' }))
const a = document.createElement('a')
a.textContent = fileName
a.download = fileName
a.href = url
a.style.margin = '20px'
dlDiv.appendChild(a)
})
dlDiv.style.position = 'fixed'
document.body.appendChild(dlDiv)
}
function addCat() {
const lineWidth = 16
parentSelector = '#cat1'
// face
addPath([{x:792,y:123},{x:818,y:120},{x:848,y:78},{x:872,y:31},{x:897,y:40},{x:896,y:97},{x:897,y:159},{x:893,y:205},{x:908,y:251},{x:915,y:306},{x:899,y:351},{x:876,y:390},{x:825,y:407},{x:774,y:395},{x:731,y:366},{x:710,y:326},{x:689,y:290},{x:684,y:252},{x:697,y:204},{x:697,y:153},{x:697,y:90},{x:694,y:37},{x:722,y:24},{x:754,y:67}], lineWidth, true)
addPath([{x:761,y:313},{x:770,y:347},{x:806,y:352},{x:831,y:334},{x:828,y:304},{x:794,y:304}], lineWidth, true)
addPath([{x:762,y:201},{x:747,y:215},{x:747,y:243},{x:776,y:248},{x:793,y:234},{x:789,y:209}], lineWidth, true)
addPath([{x:850, y: 205}, {x:824,y:212},{x:820,y:237},{x:828,y:255},{x:860,y:256},{x:869,y:234},{x:863,y:215}], lineWidth, true)
// front leg
addPath([{x:737,y:401},{x:737,y:447},{x:715,y:498},{x:686,y:527},{x:678,y:563},{x:718,y:561},{x:752,y:522},{x:769,y:487}], lineWidth)
addPath([{x:826,y:441},{x:819,y:487},{x:787,y:537}], lineWidth)
// stomach
addPath([{x:680,y:357},{x:640,y:375},{x:596,y:388},{x:546,y:405},{x:533,y:457},{x:571,y:480},{x:626,y:474},{x:674,y:448},{x:695,y:405},{x:704,y:371}], lineWidth, true)
// back
addPath([{x:670,y:238},{x:622,y:230},{x:595,y:240},{x:570,y:274},{x:548,y:297},{x:498,y:295},{x:442,y:296},{x:394,y:294},{x:311,y:323}], lineWidth)
// back leg
addPath([{x:398,y:470},{x:475,y:450},{x:494,y:390},{x:410,y:373},{x:378,y:420},{x:367,y:476},{x:353,y:585},{x:453,y:708},{x:523,y:634},{x:463,y:562}], lineWidth)
parentSelector = '#cat2'
// tail leg
addPath([{x:275,y:529},{x:257,y:606},{x:294,y:685}], lineWidth)
addPath([{x:182,y:516},{x:145,y:566},{x:105,y:615}], lineWidth)
// tail
addPath([{x:273,y:344},{x:231,y:359},{x:163,y:377},{x:107,y:406},{x:50,y:452},{x:50,y:502},{x:82,y:506},{x:157,y:494},{x:213,y:503},{x:256,y:510}], lineWidth)
// tail pattern
addPath([{x:83,y:454},{x:90,y:460},{x:122,y:444},{x:148,y:425},{x:176,y:415},{x:202,y:420},{x:201,y:434}], lineWidth*5/8)
addPath([{x:118,y:460},{x:110,y:480}], lineWidth*5/8)
addPath([{x:145,y:442},{x:140,y:461}], lineWidth*5/8)
addPath([{x:160,y:453},{x:164,y:432}], lineWidth*5/8)
addPath([{x:182,y:431},{x:180,y:445}], lineWidth*5/8)
}
</script>
<div id='scene'>
<div id='view'>
<div id='bottom'></div>
<div id='edge'></div>
<div id='items'></div>
<div id='cat1'></div>
<div id='cat2'></div>
</div>
</div>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment