Last active
October 28, 2020 16:32
-
-
Save tompng/1bc627c95f9391016d96a4ed855858f7 to your computer and use it in GitHub Desktop.
css nazca cat
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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