mixin cylinder(radius = 10, sides = 10, cut = [5, 10], top = true, bottom = true)
  - const innerAngle = (((sides - 2) * 180) / sides) * 0.5
  - const cosAngle = Math.cos(innerAngle * (Math.PI / 180))
  - const side =  2 * radius * Math.cos(innerAngle * (Math.PI / 180))
  - const projection = Math.sqrt(Math.pow(radius, 2) - Math.pow(side / 2, 2))
  //- Use the cut to determine how many sides get rendered and from what point

  .cylinder(style=`--side: ${side}; --sides: ${sides}; --radius: ${radius}; --projection: ${projection};`)
    if top
      .cylinder__end.cylinder__segment.cylinder__end--top
    if bottom
      .cylinder__end.cylinder__segment.cylinder__end--bottom
    - const [start, end] = cut
    - let i = start
    while i < end
      .cylinder__side.cylinder__segment(style=`--index: ${i};`)
      - i++
.scene
  +cylinder(20, 30, [0, 30])
View Compiled
*
  box-sizing border-box
  transform-style preserve-3d

body
  display grid
  place-items center
  min-height 100vh
  background hsl(0, 0%, 15%)
  overflow hidden

:root
  --height 40
  --radius 20
  --animate 1

@property --rotate-x
  syntax '<angle>'
  inherits false
  initial-value 0deg
@property --rotate-y
  syntax '<angle>'
  inherits false
  initial-value 0deg
@property --rotate-z
  syntax '<angle>'
  inherits false
  initial-value 0deg

.scene
  transform rotateZ(calc(var(--base-rotate-z, 0) * 1deg)) rotateX(calc(var(--base-rotate-x, -24) * 1deg)) rotateY(calc(var(--base-rotate-y, -24) * 1deg)) rotateY(var(--rotate-y)) rotateX(var(--rotate-x)) rotateZ(var(--rotate-z))
  animation tumble-x calc(var(--animate) * 3s) -1s infinite linear, tumble-y calc(var(--animate) * 4s) -6s infinite linear, tumble-z calc(var(--animate) * 5s) -3s infinite linear

@keyframes tumble-x
  to
    --rotate-x 360deg
@keyframes tumble-y
  to
    --rotate-y 360deg
@keyframes tumble-z
  to
    --rotate-z 360deg

.cylinder
  --bg 'hsl(%s, 80%, 50%)' % var(--hue)
  height calc(var(--height, 40) * 1vmin)
  position relative
  width calc(var(--radius, 40) * 2vmin)

  &__segment
    filter brightness(var(--b, 1))
    background var(--bg, hsl(0, 80%, 50%))
    position absolute

  // Top
  &__end
    --b 1.2
    top var(--t, 0)
    height calc(var(--radius, 40) * 2vmin)
    width calc(var(--radius, 40) * 2vmin)
    border-radius 50%
    transform translate(0, -50%) rotateX(90deg)

  &__end--bottom
    --b 0.8
    --t 100%

  &__side
    --b 0.89
    height calc(var(--height, 30) * 1vmin)
    width calc(var(--side) * 1vmin)
    top 0
    left 50%
    transform translate(-50%, 0) rotateY(calc((var(--index, 0) * 360 / var(--sides)) * 1deg)) translate3d(50%, 0, calc(var(--projection) * 1vmin))
View Compiled
import { GUI } from 'https://cdn.skypack.dev/dat.gui'

const CONFIG = {
  sides: 50,
  radius: 20,
  height: 30,
  hue: Math.random() * 360,
  start: 0,
  end: 50,
  animate: true,
  top: true,
  bottom: true,
  'base-rotate-x': -24,
  'base-rotate-y': -24,
  'base-rotate-z': 0,
}

let START
let END

const CYLINDER = document.querySelector('.cylinder')

const CTRL = new GUI()

const UPDATE = () => {
  const innerAngle = (((CONFIG.sides - 2) * 180) / CONFIG.sides) * 0.5
  const cosAngle = Math.cos(innerAngle * (Math.PI / 180))
  const side = 2 * CONFIG.radius * cosAngle
  const projection = Math.sqrt(
    Math.pow(CONFIG.radius, 2) - Math.pow(side / 2, 2)
  )

  for (const key of Object.keys(CONFIG)) {
    CYLINDER.style.setProperty(`--${key}`, CONFIG[key])
  }
  CYLINDER.style.setProperty('--projection', projection)
  CYLINDER.style.setProperty('--side', side)

  // Add the sides however you need to.
  CYLINDER.innerHTML = null

  const FRAG = new DocumentFragment()
  if (CONFIG.top) {
    const END = document.createElement('div')
    END.className = 'cylinder__end cylinder__segment cylinder__end--top'
    FRAG.appendChild(END)
  }
  if (CONFIG.bottom) {
    const END = document.createElement('div')
    END.className = 'cylinder__end cylinder__segment cylinder__end--bottom'
    FRAG.appendChild(END)
  }
  for (let s = CONFIG.start; s < CONFIG.end; s++) {
    const SIDE = document.createElement('div')
    SIDE.className = 'cylinder__side cylinder__segment'
    SIDE.style.setProperty('--index', s)
    FRAG.appendChild(SIDE)
  }

  CYLINDER.appendChild(FRAG)

  // Refresh control limits
  START.max(Math.min(CONFIG.sides, CONFIG.end) - 1)
  END.min(CONFIG.start + 1)
  END.max(CONFIG.sides)
}

const SIZE = CTRL.addFolder('Size')
SIZE.add(CONFIG, 'radius', 2, 50, 1)
  .onChange(UPDATE)
  .name('Radius')
SIZE.add(CONFIG, 'height', 5, 50, 1)
  .onChange(UPDATE)
  .name('Height')
const CYL = CTRL.addFolder('Cylinder Config')
CYL.add(CONFIG, 'top').name('Top').onChange(UPDATE)
CYL.add(CONFIG, 'bottom').name('Bottom').onChange(UPDATE)
CYL.add(CONFIG, 'sides', 5, 100, 1)
  .onChange(UPDATE)
  .name('# of Sides')
START = CYL.add(CONFIG, 'start', 0, Math.min(CONFIG.sides, CONFIG.end) - 1, 1)
  .onChange(UPDATE)
  .name('Start Side')
END = CYL.add(CONFIG, 'end', CONFIG.start + 1, CONFIG.sides, 1)
  .onChange(UPDATE)
  .name('End Side')
CYL.add(CONFIG, 'hue', 0, 360, 1)
  .onChange(UPDATE)
  .name('Hue')
const CAM = CTRL.addFolder('Camera')
CAM.add(CONFIG, 'base-rotate-x', -360, 360, 1)
  .onChange(val =>
    document.documentElement.style.setProperty('--base-rotate-x', val)
  )
  .name('Rotate X')
CAM.add(CONFIG, 'base-rotate-y', -360, 360, 1)
  .onChange(val =>
    document.documentElement.style.setProperty('--base-rotate-y', val)
  )
  .name('Rotate Y')
CAM.add(CONFIG, 'base-rotate-z', -360, 360, 1)
  .onChange(val =>
    document.documentElement.style.setProperty('--base-rotate-z', val)
  )
  .name('Rotate Z')
CAM.add(CONFIG, 'animate')
  .onChange(animate =>
    document.documentElement.style.setProperty('--animate', animate ? 1 : 0)
  )
  .name('Animate')
UPDATE()
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.