Skip to content

Instantly share code, notes, and snippets.

@yutkat
Created March 9, 2021 15:27
Show Gist options
  • Save yutkat/73f604df2f86e8d8fc47414674017344 to your computer and use it in GitHub Desktop.
Save yutkat/73f604df2f86e8d8fc47414674017344 to your computer and use it in GitHub Desktop.
VoCSSels - Easily create 3D CSS & HTML Voxel Models
._audio
.l
.p3d{"@mousemove" => "colorPicker($event)"}
.p3d_colorPicker{"v-if" => "picker.active", ":style" => "`background: #${picker.color};left: ${picker.x}px; top: ${picker.y}px;`", "@click" => "picker.active = !picker.active, picker.color = '#25273E'"}
.p3d_loader
.p3d_loader__inner
%img{:src => 'https://assets.codepen.io/217233/p3d_logoLoading_2.svg'}
.b
.f
.p3d_main
.p3d_overlay{":class" => "{open : modalOpen}", "@click" => "closeModals()"}
.p3d_load.modal{":class" => "{open : modalOpen && modal == 'share'}"}
.p3d_load__inner.modal
.close{"@click" => "closeModals(), enJin.audioController.play('pop6')"}
X
%h1{"v-if" => "submitted"} Creation shared!
%h1{"v-if" => "!submitted"} Share your creation
.wrap{"v-if" => "submitted"}
%p Amazing! Thanks for being part of the community. If your VoCSSel is approved, everyone will be able to view it in the community section. I really hope you enjoyed using VoCSSels!
.wrap{"v-if" => "!submitted"}
%p Want other people to be able to load in your VoCSSel? If approved, your VoCSSel will be added to our community section.
.row
.row-half
%label{:for => 'name'} VoCSSel name
%input#name{":value" => "exportSettings.name", "v-model" => "exportSettings.name", :type => 'text'}
.row-half
%label{:for => 'name'} Author name
%input#name{":value" => "author", "v-model" => "author", :type => 'text'}
.b{"v-if" => "!submitted"}
%button{"@click" => "shareModel()", ":class" => "{submitting : submitting}"}
%img.loader{:src => 'https://assets.codepen.io/217233/Dual+Ring-1s-200px.gif', :draggable => 'false'}
Share it
%button{"@click" => "closeModals(), enJin.audioController.play('pop6')"} No thanks
.p3d_load.modal.community{":class" => "{open : modalOpen && modal == 'community'}"}
.p3d_load__inner.modal
.close{"@click" => "closeModals(), enJin.audioController.play('pop6')"}
X
%h1 Community content
%p Take a look at some of the incredible VoCSSels people have made
.s-wrap
.x-wrap
.p3d_load__model.empty{"v-if" => "communityContent.length == 0"}
%h4 Sorry, we seem to be having a problem loading content. Please try again later
.p3d_load__model{":key" => "model", "v-for" => "(model, index) in communityContent", "@click" => "load(model, true), closeModals(), enJin.audioController.play('pop6')"}
.m_image{":style" => "`background: url(${model.image})`"}
.m_details
%p.date {{model.date}}
%p.name {{model.name}}
%p.author by {{model.author}}
%p.voxels
%span
{{model.voxels}}
voxels |
%span
{{model.vertices}}
vertices
.p3d_load.modal{":class" => "{open : modalOpen && modal == 'load'}"}
.p3d_load__inner.modal
.close{"@click" => "closeModals(), enJin.audioController.play('pop6')"}
X
%h1 Load a VoCSSel
%p Select or manage your saved VoCSSels
.s-wrap
.p3d_load__model.empty{"v-if" => "savedModelsMeta.length == 0"}
%h4 You don't have any VoCSSels saved. Go and make one first
.p3d_load__model{":key" => "model", "v-for" => "(model, index) in savedModelsMeta", "@click" => "load(index, false), closeModals(), enJin.audioController.play('pop6')"}
.m_image{":style" => "`background: url(${getMetaData('image', model)})`"}
.m_details
%p.date {{getMetaData('date', model)}}
%p.name {{getMetaData('name', model)}}
%p.author by {{getMetaData('author', model)}}
%p.voxels
%span
{{getMetaData('voxels', model)}}
voxels |
%span
{{getMetaData('vertices', model)}}
vertices
.delete{"@click" => "deleteModel(index), enJin.audioController.play('pop5')"} Delete
.p3d_main__header
.header_left
%img{:src => 'https://assets.codepen.io/217233/p3d_logo_new.svg', :draggable => 'false'}
.header_right
.name
%input{":value" => "exportSettings.name", "v-model" => "exportSettings.name"}
.buttons
.save.button{"@click" => "newModel(), enJin.audioController.play('pop6')"}
%img{:src => 'https://assets.codepen.io/217233/p3d_load_1.svg', :draggable => 'false', ":class" => "{saving : saving}"}
.tooltip
New
.save.button{"@click" => "save(), enJin.audioController.play('pop6')"}
%img.icon{:src => 'https://assets.codepen.io/217233/p3d_save_1.svg', :draggable => 'false', ":class" => "{saving : saving}"}
%img.loader{:src => 'https://assets.codepen.io/217233/Dual+Ring-1s-200px.gif', :draggable => 'false', ":class" => "{saving : saving}"}
.tooltip
Save
.load.button{"@click" => "openModal('load'), enJin.audioController.play('pop6')"}
%img{:src => 'https://assets.codepen.io/217233/p3d_new.svg', :draggable => 'false'}
.tooltip
Load
.community.button{"@click" => "openModal('community'), enJin.audioController.play('pop6')"}
Community
.options
.button{"@click" => "toggleAudio()", ":class" => "{active : muted}"}
%img.icon{:src => 'https://assets.codepen.io/217233/p3d_mute.svg', :draggable => 'false'}
.tooltip
Audio
.button{"@click" => "toggleMotion()", ":class" => "{active : !motion}"}
%img{:src => 'https://assets.codepen.io/217233/p3d_nomotion.svg', :draggable => 'false'}
.tooltip
Motion
.p3d_main__editor
.editor_left
.editor_left__header{":class" => "{export : mode == 'export'}"}
.e_buttons{":class" => "{hide : mode != 'drawing'}"}
.button{":class" => "{active : symMode == ''}", "@click" => "symMode = ''"}
%img{:src => 'https://assets.codepen.io/217233/p3d_sym_none_1.svg', :draggable => 'false'}
.tooltip
No Symmetry
.button{":class" => "{active : symMode == 'x'}", "@click" => "symMode = 'x'"}
%img{:src => 'https://assets.codepen.io/217233/p3d_sym_x_1.svg', :draggable => 'false'}
.tooltip
X Symmetry
.button{":class" => "{active : symMode == 'y'}", "@click" => "symMode = 'y'"}
%img{:src => 'https://assets.codepen.io/217233/p3d_sym_y_1.svg', :draggable => 'false'}
.tooltip
Y Symmetry
.button{":class" => "{active : symMode == 'xy'}", "@click" => "symMode = 'xy'"}
%img{:src => 'https://assets.codepen.io/217233/p3d_sym_xy_1.svg', :draggable => 'false'}
.tooltip
XY Symmetry
.e_tabs
.e_tabs__tab.button{"@click" => "swapMode('drawing'), drawMode = 'draw', exportSettings.scale = 0.8, updateOrientation(0, 90, 0, false)", ":class" => "{active : mode == 'drawing'}"}
Draw
.tooltip
Draw pixel art
.e_tabs__tab.button{"@click" => "swapMode('extrude'), drawMode = 'extrude', exportSettings.scale = 0.8, updateOrientation(-30, 140, 0, false)", ":class" => "{active : mode == 'extrude'}"}
Extrude
.tooltip
Give depth
.e_tabs__tab.button{"@click" => "swapMode('paint'), drawMode = 'draw', exportSettings.scale = 0.8, updateOrientation(0, 90, 0, false), orientationButton = 'front'", ":class" => "{active : mode == 'paint'}"}
Paint
.tooltip
Paint voxels
.e_tabs__tab.button{"@click" => "swapMode('export'), updateOrientation(0, 90, 0, true)", ":class" => "{active : mode == 'export'}"}
Export
.tooltip
Export for web
.e_sym
.editor_left__main{"@mouseleave" => "picker.active = false"}
.m_palette{":class" => "{hide : mode == 'extrude'}"}
.buttons
.button.draw{"@click" => "drawMode = 'draw', enJin.audioController.play('pop6')", ":class" => "{active : drawMode == 'draw'}"}
%img{:src => 'https://assets.codepen.io/217233/p3d_pencil.svg', :draggable => 'false'}
.tooltip
Draw
.button.erase{"v-if" => "mode != 'paint'", "@click" => "picker.active = false, drawMode = 'erase', enJin.audioController.play('pop6')", ":class" => "{active : drawMode == 'erase'}"}
%img{:src => 'https://assets.codepen.io/217233/p3d_eraser_2.svg', :draggable => 'false'}
.tooltip
Erase
.cPicker{"@click" => "picker.active = !picker.active", ":class" => "{active : picker.active}"}
%svg{:fill => "none", :height => "18", :viewbox => "0 0 18 18", :width => "18", :xmlns => "http://www.w3.org/2000/svg"}
%path{:d => "M16.3 0.75C15.3 -0.25 13.7 -0.25 12.7 0.75L10.9 2.55L10.2 1.85C9.8 1.45 9.2 1.45 8.8 1.85L8 2.55C7.6 2.95 7.6 3.55 8 3.95L13 8.95C13.4 9.35 14 9.35 14.4 8.95L15.1 8.25C15.5 7.85 15.5 7.25 15.1 6.85L14.5 6.15L16.3 4.35C17.3 3.35 17.3 1.75 16.3 0.75V0.75ZM2.9 10.55C0.7 12.75 2 13.75 0 16.35L0.7 17.05C3.3 15.05 4.3 16.35 6.5 14.15L11.6 9.05L8 5.45L2.9 10.55Z", :fill => "black"}
#colorPicker
.m_palette.palette--depth{":class" => "{hide : mode != 'extrude'}"}
.buttons
.button.erase{"@click" => "drawMode = 'extrude', enJin.audioController.play('pop6')", ":class" => "{active : drawMode == 'extrude'}"}
%img{:src => 'https://assets.codepen.io/217233/p3d_extrude.svg?x=ghfh', :draggable => 'false'}
.tooltip
Extrude
.button.erase{"@click" => "drawMode = 'hollow', enJin.audioController.play('pop6')", ":class" => "{active : drawMode == 'hollow'}"}
%img{:src => 'https://assets.codepen.io/217233/p3d_hollow.svg?x=ghfh', :draggable => 'false', :draggable => 'false'}
.tooltip
Shell
.dp-outer
.hollowTip{":class" => "{hide : drawMode == 'extrude'}"}
.help
Shell a group
%img.icon{:src => 'https://assets.codepen.io/217233/p3d_help.svg', :draggable => 'false'}
.gif
.desc
%h4 Shell tool
Use the shell toggle to remove the voxels between the first and last in the row. Handy for optimising your model and removing voxels that cannot be seen. Also great for creating legs.
%img{:src => 'https://assets.codepen.io/217233/shell.gif', :draggable => 'false'}
.button{"@click" => "shell = true, enJin.audioController.play('pop6')", ":class" => "{active : shell}"}
On
.button{"@click" => "shell = false, enJin.audioController.play('pop6')", ":class" => "{active : !shell}"}
Off
.dpgrid{":class" => "{hide : drawMode == 'hollow'}"}
.help
Select a depth
%img.icon{:src => 'https://assets.codepen.io/217233/p3d_help.svg', :draggable => 'false'}
.gif
.desc
%h4 Extrude tool
Select a level of depth, then click on any pixel to extrude by that amount. New voxels will be created along the z axis which can be individually painted.
%img{:src => 'https://assets.codepen.io/217233/depth.gif', :draggable => 'false'}
-(1..9).each do | index |
.dp{':data-depth' => index, "@click" => "setDepth($event), enJin.audioController.play('pop6')", ":class" => "{active : currentDepth == #{index}}"} #{index}
.m_grid{"@mousedown" => "drawing = true", "@mouseup" => "drawing = false", "@mouseup.right" => "handleRightClick()", "@mouseleave" => "drawing = false", ":class" => "{hide : mode == 'paint', loading : loading}"}
.helper-x{":class" => "{active : symMode == 'x' || symMode == 'xy'}"}
.helper-y{":class" => "{active : symMode == 'y' || symMode == 'xy'}"}
%img.loader{:src => 'https://assets.codepen.io/217233/Dual+Ring-1s-200px.gif', :draggable => 'false', ":class" => "{saving : saving}"}
.m_grid__pixel{":data-index" => "index", ":data-y" => "Math.floor(`${index / canvasSize + 1}`)", ":data-x" => "Math.ceil(`${index % canvasSize}`)", "@click" => "selectColor()", "@mouseenter" => "drawPixel($event)", "@mousedown" => "drawPixel($event)", "@contextmenu.prevent" => '', ":key" => "voxel.x", "v-for" => "(voxel, index) in voxels", ":style" => ""}
.p{"v-if" => "voxel.c", ":style" => "`background: #${voxel.c[0][1]}`"}
.d{":class" => "{show : mode == 'extrude' && drawMode == 'extrude'}"}{{voxel.d}}
.d{":class" => "{show : mode == 'extrude' && drawMode == 'hollow' && voxel.h}"} S
.editor_right
.editor_right__export{":class" => "{show : mode == 'export'}"}
%h3 Export settings
%p.sub Export your VoCSSel for web, or as a png based on the current view
.form
.row
.row-half
%label{:for => 'name'} VoCSSel name
%input#name{":value" => "exportSettings.name", "v-model" => "exportSettings.name", :type => 'text'}
.row-half
%label{:for => 'name'} Author name
%input#name{":value" => "author", "v-model" => "author", :type => 'text'}
.save.button{"@click" => "save(), enJin.audioController.play('pop6')"}
%img.icon{:src => 'https://assets.codepen.io/217233/p3d_save_1.svg', :draggable => 'false', ":class" => "{saving : saving}"}
%img.loader{:src => 'https://assets.codepen.io/217233/Dual+Ring-1s-200px.gif', :draggable => 'false', ":class" => "{saving : saving}"}
.tooltip
Save
%br
.row
%label{:for => 'exportBg'} Background color
%input#exportBg
%img{:src => 'https://assets.codepen.io/217233/p3d_resetColour.svg', :draggable => 'false', "@click" => "resetBgColor()"}
.row
%label{:for => 'perspective'}
Perspective:
%span {{exportSettings.perspective}}px
%input#perspective{":value" => "exportSettings.perspective", :min => 100, :max => 3000, :type => 'range', "v-model" => "exportSettings.perspective"}
.row
%label{:for => 'animate'}
Animate
%span
%span Turn off for viewport control
%input#animate{":value" => "exportSettings.animate", :type => 'checkbox', "v-model" => "exportSettings.animate"}
.row{"v-if" => "exportSettings.animate"}
%label{:for => 'speed'}
Speed:
%span {{exportSettings.spinSpeed}}s
%input#speed{":value" => "exportSettings.spinSpeed", :type => 'range', :min => 0.1, :max => 20, :step => 0.1, "v-model" => "exportSettings.spinSpeed"}
.row{"v-if" => "!exportSettings.animate"}
.row-half
%label{:for => 'rotateX'}
Rotate X:
%span {{exportSettings.x}}deg
%input#rotateX{":value" => "exportSettings.x", :type => 'range', :min => -180, :max => 180, :step => 10, "v-model" => "exportSettings.x"}
.row-half
%label{:for => 'rotateY'}
Rotate Y:
%span {{exportSettings.y}}deg
%input#rotateY{":value" => "exportSettings.y", :type => 'range', :min => -180, :max => 180, :step => 10, "v-model" => "exportSettings.y"}
.row{"v-if" => "!exportSettings.animate"}
.row-half
%label{:for => 'rotateZ'}
Rotate Z:
%span {{exportSettings.z}}deg
%input#rotateZ{":value" => "exportSettings.z", :type => 'range', :min => -180, :max => 180, :step => 10, "v-model" => "exportSettings.z"}
.row-half
%label{:for => 'scale'}
Scale:
%span {{exportSettings.scale}}
%input#scale{":value" => "exportSettings.scale", :type => 'range', :min => 0.01, :max => 3, :step => 0.01, "v-model" => "exportSettings.scale"}
.buttons
%button{"@click" => "openModal('share'), enJin.audioController.play('pop6')"} Share with community
%button.cp{"@click" => "processForExport()", ":class" => "{exporting : exporting}"}
%img.loader{:src => 'https://assets.codepen.io/217233/Dual+Ring-1s-200px.gif', :draggable => 'false', ":class" => "{saving : saving}"}
Export to CodePen
%p{":class" => "{hide : mode == 'export' || mode == 'paint'}"} HTML / CSS Output
%img{:src => 'https://assets.codepen.io/217233/p3d_resetColour.svg', :draggable => 'false', "@click" => "resetBgColor()", ":class" => "{hide : mode == 'export'}"}
%input#colorBg{":class" => "{hide : mode == 'export'}"}
.editor_right__preview{":class" => "{export : mode == 'export', paint : mode == 'paint'}"}
.p_inner#model{":style" => "`background: ${exportSettings.bgColor};`"}
.button.zooms.first{"@click" => "handleZoom(0.05), enJin.audioController.play('pop6')", ":class" => "{hide : mode == 'export'}"}
%img{:src => 'https://assets.codepen.io/217233/p3d_zoomIn.svg', :draggable => 'false'}
.button.zooms{"@click" => "handleZoom(-0.05), enJin.audioController.play('pop6')", ":class" => "{hide : mode == 'export'}"}
%img{:src => 'https://assets.codepen.io/217233/p3d_zoomOut.svg', :draggable => 'false'}
.button.zooms{"@click" => "zoomLevel = .8, enJin.audioController.play('pop6')", ":class" => "{hide : mode == 'export'}"}
%img{:src => 'https://assets.codepen.io/217233/p3d_resetZoom.svg', :draggable => 'false'}
.views{":class" => "{show : mode == 'paint'}"}
.button{"@click" => "updateOrientation(0, 90, 0, false), orientationButton = 'front'", ":class" => "{active : orientationButton == 'front'}"} Front
.button{"@click" => "updateOrientation(0, -90, 0, false), orientationButton = 'back'", ":class" => "{active : orientationButton == 'back'}"} Back
.button{"@click" => "updateOrientation(0, 180, 0, false), orientationButton = 'left'", ":class" => "{active : orientationButton == 'left'}"} Left
.button{"@click" => "updateOrientation(0, 0, 0, false), orientationButton = 'right'", ":class" => "{active : orientationButton == 'right'}"} Right
.button{"@click" => "updateOrientation(-90, 0, 0, false), orientationButton = 'top'", ":class" => "{active : orientationButton == 'top'}"} Top
.button{"@click" => "updateOrientation(90, 0, 0, false), orientationButton = 'bottom'", ":class" => "{active : orientationButton == 'bottom'}"} Bottom
.button{"@click" => "updateOrientation(-30, 50, 0, false), orientationButton = 'iso1'", ":class" => "{active : orientationButton == 'iso1'}"} Isometric 1
.button{"@click" => "updateOrientation(-30, 140, 0, false), orientationButton = 'iso2'", ":class" => "{active : orientationButton == 'iso2'}"} Isometric 2
.button{"@click" => "updateOrientation(-30, 230, 0, false), orientationButton = 'iso3'", ":class" => "{active : orientationButton == 'iso3'}"} Isometric 3
.button{"@click" => "updateOrientation(-30, 310, 0, false), orientationButton = 'iso4'", ":class" => "{active : orientationButton == 'iso4'}"} Isometric 4
.zoom{"@wheel" => "zoom($event)", ":style" => "`transform: scale(${zoomLevel})`"}
.exportWrap
.model{"v-if" => "voxels", ":class" => "{extrude : mode == 'extrude', spin : mode == 'export' && exportSettings.animate, export : mode == 'export', paint : mode == 'paint'}", ":style" => "`transform: scale(${exportSettings.scale}) rotateZ(${exportSettings.z}deg) rotateX(${exportSettings.x}deg) rotateY(${exportSettings.y}deg); animation-duration: ${exportSettings.spinSpeed}s`"}
.voxel-group{"v-if" => "voxelGroup.length != 0", ":key" => "voxelGroup.x", "v-for" => "(voxelGroup, index) in voxels", "stagger" => "50", ":class" => "`g-${voxelGroup.d}`", ":data-index" => "`${voxelGroup.index}`"}
.voxel{"v-if" => "voxelGroup.c", ":key" => "index", "v-for" => "(voxel, index) in voxelGroup.d", ":class" => "`d-${index}`"}
.v{"v-if" => "voxelGroup.c[index] != ''", ":class" => "`x-${voxelGroup.x} y-${voxelGroup.y}`", ":data-index" => "`${index}`"}
.f.f--f{"v-if" => "voxelGroup?.c?.[index]?.[0]", ":data-vertex" => "0" ,":style" => "[voxelGroup.c ? { background: `#${voxelGroup.c[index][0]}` } : null]", ":class" => "{paintable : mode == 'paint'}", "@click" => "paint($event)"}
.f.f--ba{"v-if" => "voxelGroup?.c?.[index]?.[0]", ":data-vertex" => "1" ,":style" => "[voxelGroup.c ? { background: `#${voxelGroup.c[index][1]}` } : null]", ":class" => "{paintable : mode == 'paint'}", "@click" => "paint($event)"}
.f.f--t{"v-if" => "voxelGroup?.c?.[index]?.[0]", ":data-vertex" => "2" ,":style" => "[voxelGroup.c ? { background: `#${voxelGroup.c[index][2]}` } : null]", ":class" => "{paintable : mode == 'paint'}", "@click" => "paint($event)"}
.f.f--b{"v-if" => "voxelGroup?.c?.[index]?.[0]", ":data-vertex" => "3" ,":style" => "[voxelGroup.c ? { background: `#${voxelGroup.c[index][3]}` } : null]", ":class" => "{paintable : mode == 'paint'}", "@click" => "paint($event)"}
.f.f--l{"v-if" => "voxelGroup?.c?.[index]?.[0]", ":data-vertex" => "4" ,":style" => "[voxelGroup.c ? { background: `#${voxelGroup.c[index][4]}` } : null]", ":class" => "{paintable : mode == 'paint'}", "@click" => "paint($event)"}
.f.f--r{"v-if" => "voxelGroup?.c?.[index]?.[0]", ":data-vertex" => "5" ,":style" => "[voxelGroup.c ? { background: `#${voxelGroup.c[index][5]}` } : null]", ":class" => "{paintable : mode == 'paint'}", "@click" => "paint($event)"}
%p.voxelCount{":class" => "{hide : mode == 'export' || mode == 'paint'}"}
%span
{{getVoxelsCount()}}
Voxels |
%span
{{getVerticesCount() - 1}}
Vertices
%span.block{":class" => "{show : getVerticesCount() > 750}"}
%img.icon{:src => 'https://assets.codepen.io/217233/p3d_warning.svg', :draggable => 'false'}
Performance will decrease as the vertices count increases. Make sure to shell out any unused voxels for maximum performance.
.p3d_main_footer
%p
Made with love by
%a{:href => 'https://www.codepen.io/jcoulterdesign', :target => "_blank"} Jamie Coulter
/*
Dependencies
https://codepen.io/jcoulterdesign/pen/1e3ed378fed68f1a43bc4f73f9964945
https://codepen.io/jcoulterdesign/pen/6c44bfdc74442457826e062bc719c586
https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js
https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js
https://cdnjs.cloudflare.com/ajax/libs/spectrum/1.8.1/spectrum.js
https://cdnjs.cloudflare.com/ajax/libs/dom-to-image/2.6.0/dom-to-image.min.js
*/
// Master audio array. This is for audio that will not be manipulated by any HTML5 filters.
// For audio needing low, high pass effects, add them to the _specialAudio array.
const _masterAudio = [
{
'name' : 'pop1',
'source' : 'https://assets.codepen.io/217233/p3d_pop1_t.mp3',
'stackSize' : 15
},
{
'name' : 'pop2',
'source' : 'https://assets.codepen.io/217233/p3d_pop2_t.mp3',
'stackSize' : 15
},
{
'name' : 'pop3',
'source' : 'https://assets.codepen.io/217233/p3d_pop3_t.mp3',
'stackSize' : 3
},
{
'name' : 'pop4',
'source' : 'https://assets.codepen.io/217233/p3d_pop4_t.mp3',
},
{
'name' : 'pop5',
'source' : 'https://assets.codepen.io/217233/p3d_pop5_t.mp3',
},
{
'name' : 'pop6',
'source' : 'https://assets.codepen.io/217233/p3d_pop7.mp3',
},
{
'name' : 'save',
'source' : 'https://assets.codepen.io/217233/p3d_save.wav',
}
];
// Create new instance of EnJin (normally used in my browser games, but using the audio module for it)
const enJin = new Enjin();
// Create audio controller
enJin.createAudioController(_masterAudio);
// Voxel
// Class containing all our volxel data including its position and vertex information
// This technically represents a voxel group as well, as vue renders out a voxel for each entry in the colors array
class Voxel {
constructor(x, y, depth, color, index) {
this.x = x;
this.y = y;
this.d = depth;
this.h = false;
this.c = [ // Color array for each side of the voxel
{
0: lightenDarkenColor(color, 0),
1: lightenDarkenColor(color, -5),
2: lightenDarkenColor(color, -10),
3: lightenDarkenColor(color, -15),
4: lightenDarkenColor(color, -20),
5: lightenDarkenColor(color, -25)
}
]
this.index = index;
}
}
// Lighten darken any hex color by amt
function lightenDarkenColor(col, amt) {
col = col.replace(/^#/, '')
if (col.length === 3) col = col[0] + col[0] + col[1] + col[1] + col[2] + col[2]
let [r, g, b] = col.match(/.{2}/g);
([r, g, b] = [parseInt(r, 16) + amt, parseInt(g, 16) + amt, parseInt(b, 16) + amt])
r = Math.max(Math.min(255, r), 0).toString(16)
g = Math.max(Math.min(255, g), 0).toString(16)
b = Math.max(Math.min(255, b), 0).toString(16)
const rr = (r.length < 2 ? '0' : '') + r
const gg = (g.length < 2 ? '0' : '') + g
const bb = (b.length < 2 ? '0' : '') + b
return `${rr}${gg}${bb}`
}
// Rot13 function. Not critical to ser but used as a way of tracking what has been made with the tool
// If I added a unique string to this CodePen, which then passes it on to the exported pen, I could use the search to find all the models exported with this tool. The
// issue ofcourse is that the string would also exist on this pen, and so i would be supplied with all the forks. Doing it with a simple rot13 means the string will
// appear only on the exported pen.
function rot13(str) {
var input = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
var output = 'NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm';
var index = x => input.indexOf(x);
var translate = x => index(x) > -1 ? output[index(x)] : x;
return str.split('').map(translate).join('');
}
// Vue app
new Vue({
el: '.p3d',
data() {
return {
gridElement : '.m_grid',
voxelElement : '.m_grid__pixel',
canvasSize : 17, // In voxel units
voxels : [],
currentColor : 'fff',
currentDepth : 1,
mode : 'drawing',
drawMode : 'draw',
drawing : false,
author : 'VoCSSels',
zoomLevel : .8, // Initial zoom level of our models preview
maxZoom : 3, // The maximum you can zoom in
minZoom : .3, // The maximum you can zoom out
orientation : '',
orientationButton : 'front',
savedModels : '',
savedModelsMeta : '',
loading : false,
saving : false,
shell : true,
exporting : false,
motion : true,
muted : false,
exportSettings: {
'name' : 'My VoCSSel model',
'perspective' : 3000,
'bgColor' : '#25273E',
'scale' : 1,
'uid' : 'madewithvocssels',
'spinSpeed' : 10,
'animate' : true,
'x' : 0,
'y' : 90,
'z' : 0
},
metaPrefix : 'p3d_metadata_',
modelPrefix : 'p3d_modeldata_',
modal : '',
modalOpen : false,
symMode : '',
picker: {
'x' : 0,
'y' : 0,
'active' : false,
'color' : 'ffffff'
},
submitting : false,
submitted : false,
communityContent : []
}
},
methods: {
// Main draw function. Name is deceiving. Handles all interaction with pixels such as colour depth and shell
// debounced
drawPixel: _.debounce(function(event) {
event.stopPropagation();
let target = event.target;
let data = $(target).data(); // Get pixel data
let x = data.x; // Get pixel column
let y = data.y; // Get pixel row
let index = data.index; // Get index of pixel
if(event.which == 3 && this.mode == "drawing") {
this.drawMode = 'erase'
}
var v = _.get(this.voxels[index], 'c[0][0]', 'undefined');
if(v != 'undefined') {
this.picker.color = this.voxels[index].c[0][0]
}
// Fire when in drawing mode and not picking a colour
if(this.mode == 'drawing' && this.drawing && !this.picker.active) {
// Set colorpicker to current color
$("#colorPicker").spectrum("set", this.currentColor);
// Create a new voxel instance
if(this.drawMode == 'draw') {
// If the user is currently in draw mode using draw
var v = _.isEmpty(this.voxels[index].c);
if(v) {
// If no voxel, make one and store in array
var voxel = new Voxel(x, y, 1, this.currentColor, index);
// Vue set to keep reactive
Vue.set(this.voxels, index, voxel);
// Play audio
enJin.audioController.play('pop1');
} else {
// If the pixel we clicked on already has a voxel associated with it
// Firstly, set the color array to empty
let v = _.get(this.voxels[index], 'c', 'undefined');
if(v != 'undefined') {
this.voxels[index].h = false
_.omit(this.voxels[index], 'd')
// Generate each voxel (colour group)
this.generateColors(index);
// Only play a sound if the colour is different
enJin.audioController.play('pop1')
}
}
} else {
// If not in draw mode, then we must be in erase mode
if(this.voxels[index].c != []) { // No point deleting nothing
// Set the voxel back to an empty array
Vue.set(this.voxels, index, []);
// Play audio
enJin.audioController.play('pop2');
}
}
// If we are not in drawing mode, then check if in extrude mode
} else if(this.mode == 'extrude' && this.drawing) {
// Check if extruding and not shelling
if(this.drawMode == 'extrude') {
// Check to make sure the selected extrusion is not on a voxel that doesnt exist
var v = _.get(this.voxels[index], 'c', 'undefined');
if(v != 'undefined') {
// Start extrude
// Set the d property of the voxel to the selected depth
this.voxels[index].d = this.currentDepth;
// Reset any shell props on the voxel
this.voxels[index].h = false;
// Generate each voxel (colour group)
this.generateColors(index);
// play audio
enJin.audioController.play('pop2')
}
} else {
// If not extrude, then must be hollow
var v = _.get(this.voxels[index], 'c', 'undefined');
if(v != 'undefined') {
// If shell is on
if(this.shell == true) {
// Check to make sure the selected extrusion is not on a voxel that doesnt exist
var v = _.get(this.voxels[index], 'c', 'undefined');
if(v != 'undefined') {
// set the voxels hollow prop to true
this.voxels[index].h = true;
// Get ctx
that = this;
var length = this.voxels[index].c.length;
let color = this.voxels[index].c[0][0];
let shelledColors = [];
for(let i = 0; i < length; i++) {
if(i == 0 || i == (length - 1)) {
var s = {
0: lightenDarkenColor(color, 0),
1: lightenDarkenColor(color, -5),
2: lightenDarkenColor(color, -10),
3: lightenDarkenColor(color, -15),
4: lightenDarkenColor(color, -20),
5: lightenDarkenColor(color, -25)
}
} else {
var s = '';
}
shelledColors.push(s)
}
Vue.set(this.voxels[index], 'c', shelledColors);
}
} else {
// If shell is off
// Check to make sure the selected extrusion is not on a voxel that doesnt exist
var v = _.get(this.voxels[index], 'c', 'undefined');
if(v != 'undefined') {
// set the voxels hollow prop to false
this.voxels[index].h = false
// Generate each voxel (colour group)
this.generateColors(index);
}
}
// Play audio only if the voxel is shelled
if(this.voxels[index].h) {
enJin.audioController.play('pop4') ;
}
}
}
}
}, 1),
generateColors(voxel) {
var v = _.get(this.voxels[voxel], 'c[0][0]', 'undefined');
if(v != 'undefined') {
if(this.mode == 'extrude') {
var color = this.voxels[voxel].c[0][0];
} else {
var color = this.currentColor;
}
// Then set all colours to [].
this.voxels[voxel].c = [];
// Now create an empty object for our colours
let newColours = [];
// Loop through and create colours for each voxel
for(let i = 0; i < this.currentDepth; i++) {
let colours = {
0: lightenDarkenColor(color, 0),
1: lightenDarkenColor(color, -5),
2: lightenDarkenColor(color, -10),
3: lightenDarkenColor(color, -15),
4: lightenDarkenColor(color, -20),
5: lightenDarkenColor(color, -25)
}
// Add colours to array
newColours.push(colours);
}
// Push this array to the voxel
Vue.set(this.voxels[voxel], 'c', newColours);
}
},
paint: _.debounce(function(event) {
let target = event.target;
let index = $(target).closest('.voxel-group').data().index // Get index of pixel
let voxelIndex = $(target).parent().data().index // Get index of pixel
let vertexIndex = $(target).data().vertex // Get index of pixel
enJin.audioController.play('pop1');
Vue.set(this.voxels[index].c[voxelIndex], vertexIndex, this.currentColor);
}, 15),
swapMode(mode) {
this.orientation = {}
this.zoomLevel = .8;
this.mode = mode;
enJin.audioController.play('pop3')
},
setDepth(event) {
let target = event.target;
let depth = $(target).data().depth;
this.currentDepth = depth;
},
handleZoom(amount) {
if(amount > 0) {
// in
if(this.zoomLevel < this.maxZoom) {
this.zoomLevel += amount;
} else {
this.zoomLevel = this.maxZoom;
}
} else {
// out
if(this.zoomLevel > this.minZoom) {
this.zoomLevel += amount;
} else {
this.zoomLevel = this.minZoom;
}
}
},
zoom(event) {
if(this.mode != 'export') {
const deltaY = event.deltaY;
if(deltaY < 0) {
this.handleZoom(0.03);
} else {
this.handleZoom(-0.03);
}
}
},
updateOrientation(x, y, z, exp) {
enJin.audioController.play('pop4')
this.exportSettings.x = x;
this.exportSettings.y = y;
this.exportSettings.z = z;
},
processForExport() {
enJin.audioController.play('pop6');
this.exporting = true;
var hamlArray = '- @voxels = [';
that.voxels.forEach(function(v) {
if(v.length != 0) {
let colors = '[';
v.c.forEach(function(c) {
let colourString = '{'
for (let key of Object.keys(c)) {
let col = c[key];
let index = key;
var comma;
if(key != 0) {
comma = ','
} else {
comma = ''
}
colourString = `${colourString} ${comma} ${index} => '${col}'`
}
colourString = colourString + '}'
colors = colors + colourString + ','
})
let haml = `{:x => ${v.x}, :y => ${v.y}, :d => ${v.d}, :c => ${colors}]},`
hamlArray += haml
}
})
const data = {
title : `${this.exportSettings.name} - A 3D Pure CSS & HTML Model Made With VoCSSels`,
css : `$voxelSize: 26; // We want to work as 1 unit is 1 voxel occupying one space. So here we are setting the the amount of pixels a voxel takes. This will need to be the same as the voxel size in the vue data object.
$maxDepth: 17;
$spinSpeed: ${this.exportSettings.spinSpeed}s;
$uid: ${rot13(this.exportSettings.uid)};
// Return the true px size of a voxel based on the passed unit.
@function getVoxelSize($size, $operator) {
@return unquote($operator + $size * $voxelSize + px);
}
// Function to set the orientation of a side
@function setFaceOrientation($tx, $ty, $tz, $sx, $sy, $rx, $ry, $rz) {
@return rotateX($rx + deg) rotateY($ry + deg) rotateZ($rz + deg) scaleX($sx) scaleY($sy) translate3d(unquote($tx + ',' + $ty + ',' + $tz));
}
%voxel {
position: absolute;
top: 50%;
transform-style: preserve-3d;
left: 0;
right: 0;
margin: auto;
width: 10px;
}
%face {
width: $voxelSize + px;
height: $voxelSize + px;
position: absolute;
transform-style: preserve-3d;
transform-origin: 50% 50%;
}
body {
height: 100vh;
overflow: hidden;
background: ${this.exportSettings.bgColor};
.p3d_playground {
height: 100vh;
perspective: ${this.exportSettings.perspective}px;
transform: scale(${this.exportSettings.scale});
.model {
height: 100vh;
transform-style: preserve-3d;
${this.exportSettings.animate ? 'animation: spin $spinSpeed infinite linear;' : ''}
transform-origin: 50% 50% getVoxelSize($maxDepth / 2, '');
transform: scale(.6) rotateZ(${this.exportSettings.z}deg) rotatex(${this.exportSettings.x}deg) rotateY(-${this.exportSettings.y}deg) translateY(getVoxelSize($maxDepth / 2, '-'));
transition: all .3s;
@media only screen and (max-width: 700px) {
transform: scale(.5) rotateZ(${this.exportSettings.z}deg) rotatex(${this.exportSettings.x}deg) rotateY(-${this.exportSettings.y}deg) translateY(getVoxelSize($maxDepth / 2, '-'));
}
@keyframes spin {
from { transform: scale(1) rotateY(0deg) rotateZ(0deg) rotatex(0deg) translateY(getVoxelSize($maxDepth / 2, '-'));}
to { transform: scale(1) rotateY(360deg) rotateZ(0deg) rotatex(0deg) translateY(getVoxelSize($maxDepth / 2, '-'));}
}
.voxel,
.voxel-group,
.v {
@extend %voxel;
}
.f {
@extend %face;
}
// Utility classes
@for $x from 0 through 20 {
@for $y from 0 through 20 {
.x-#{$x}.y-#{$y} {
transform: translateZ(getVoxelSize($x - 0.1, '')) translateY(getVoxelSize($y - 0.1, ''));
}
}
}
// Depths
// This can be done on a normal loop as depth will be limited
.f {
@for $v from 1 through 6 {
&:nth-of-type(#{$v}) {
$operator: if($v % 2 == 0, '', '-');
@if $v == 1 or $v == 2 {
transform: setFaceOrientation(0, 0, getVoxelSize(1 / 2, $operator), 1, 1, 0, 90, 0);
}
@if $v == 3 or $v == 4 {
transform: setFaceOrientation(0, 0, getVoxelSize(1 / 2, $operator),1 , 1, 0, 0, 0);
}
@if $v == 5 or $v == 6 {
transform: setFaceOrientation(0, 0, getVoxelSize(1 / 2, $operator), 1, 1, 90, 0, 0);
}
}
}
}
@for $i from 0 through 14 {
.d-#{$i} {
transform: translateX(23px * $i);
}
.g-#{$i} {
transform: translateX(-23px * ($i / 2));
}
}
}
}
}`,
css_pre_processor : "scss",
html_pre_processor : "haml",
html : `${hamlArray}]
-# Created using VoCSSels by Jamie Coulter https://codepen.io/jcoulterdesign/pen/vYyzZdo
.p3d_playground
.model
- @voxels.each do | voxel |
.voxel-group{:class => "g-#{voxel[:d]}"}
-(1..voxel[:d]).each do | index |
.voxel{:class => "d-#{index}"}
.v{:class => "x-#{voxel[:x]} y-#{voxel[:y]}"}
-(1..6).each do | v |
.f.f--t{:style => "background-color: ##{voxel[:c][index - 1][v]}"}`
};
const JSONstring = JSON.stringify(data).replace(/"/g, "&quot;").replace(/'/g, "&apos;");
const form = `<form class="export" action="https://codepen.io/pen/define" method="POST" target="_blank"><input class="change" type="hidden" name="data" value='${JSONstring}'><input type="submit" width="40" height="40" value="Export to new pen"></form>`;
$('.editor_right__export .form').html('');
$('.editor_right__export .form').append(form);
that = this;
setTimeout(function() {
that.exporting = false;
$('.export').submit();
}, 1500)
},
/* -------------------------------------------------------------------------
Save model to local storage
------------------------------------------------------------------------- */
save() {
// Work out todays date
let today = new Date();
let dd = String(today.getDate()).padStart(2, '0');
let mm = String(today.getMonth() + 1).padStart(2, '0');
let yyyy = today.getFullYear();
let date = mm + '.' + dd + '.' + yyyy;
// Save model to local storage
// Start by creating a metadata object for our models information
let metaData = {
'date' : date,
'author' : this.author,
'name' : this.exportSettings.name, // Get the model name
'voxels' : $('.v').length, // Get the total voxels in the model
'vertices' : $('.f').length, // Get the total vertices in the model
'image' : '', // Set a blank string for the base64 image data
'bgColor' : this.exportSettings.bgColor
}
// Set a new item in local storage for the model data and assing the JSON to it
window.localStorage.setItem(this.modelPrefix + this.exportSettings.name.toLowerCase().replace(/\s/g, ''), JSON.stringify(this.voxels));
// Set saving flag to show saving icon
this.saving = !this.saving;
// Reset the zoom level for the snap shot
this.zoomLevel = 0.8;
// Take snapshop of model node
const node = document.getElementById('model');
// Hide preview buttons
$('.zooms').hide();
domtoimage.toPng(node)
.then(function (dataUrl) {
var that = this; // Get context
var img = new Image();
// Set image in meta data to data url
metaData.image = dataUrl;
// Set local storage item to metadata
window.localStorage.setItem(that.metaPrefix + that.exportSettings.name.toLowerCase().replace(/\s/g, ''), JSON.stringify(metaData));
// Set saving flag back to false after a reasonable delay
setTimeout(function() {
// Show preview buttons
$('.zooms').show();
// Reset saving flag
that.saving = !that.saving;
// Update the saved models arrays so they are accessible in the modal straight away
that.getSavedModels();
// Play sound feedback
enJin.audioController.play('save')
}, 1000)
}.bind(this));
},
handleRightClick() {
if(this.drawMode == 'erase') {
this.drawMode = 'draw'
}
},
shareModel() {
// Work out todays date
let today = new Date();
let dd = String(today.getDate()).padStart(2, '0');
let mm = String(today.getMonth() + 1).padStart(2, '0');
let yyyy = today.getFullYear();
let date = mm + '.' + dd + '.' + yyyy;
let metaData = {
'date' : date,
'author' : this.author,
'name' : this.exportSettings.name, // Get the model name
'voxels' : $('.v').length, // Get the total voxels in the model
'vertices' : $('.f').length, // Get the total vertices in the model
'image' : '', // Set a blank string for the base64 image data
'bgColor' : this.exportSettings.bgColor
}
metaData.modelData = [this.voxels];
// Reset the zoom level for the snap shot
this.zoomLevel = 0.8;
// Take snapshop of model node
const node = document.getElementById('model');
// Hide preview buttons
$('.zooms').hide();
this.submitting = !this.submitting;
domtoimage.toPng(node)
.then(function (dataUrl) {
var that = this; // Get context
var img = new Image();
// Set image in meta data to data url
metaData.image = dataUrl;
// Set local storage item to metadata
window.localStorage.setItem(that.metaPrefix + that.exportSettings.name.toLowerCase().replace(/\s/g, ''), JSON.stringify(metaData));
if(that.submitting == false) {
// Set saving flag back to false after a reasonable delay
setTimeout(function() {
// Show preview buttons
$('.zooms').show();
// Post data to zapier webhook
var xhr = new XMLHttpRequest();
xhr.open("POST", 'https://hooks.zapier.com/hooks/catch/708069/onctccu', true);
xhr.send(JSON.stringify(metaData));
setTimeout(function() {
that.submitted = true;
that.submitting = false;
enJin.audioController.play('save')
}, 2000)
}, 1000)
}
}.bind(this));
},
/* -------------------------------------------------------------------------
Get models from local storage
------------------------------------------------------------------------- */
getSavedModels() {
// First, clear any data from the saved models and meta arrays
let savedModels = [];
let savedModelsMeta = [];
// Iterate over localStorage and insert the keys that meet the condition into arr
for (let i = 0; i < window.localStorage.length; i++) {
// Check for p3d_modeldata prefix
if (window.localStorage.key(i).substring(0, this.modelPrefix.length) == this.modelPrefix) {
savedModels.push(window.localStorage.key(i));
}
// Check for p3d_modeldata prefix
if (window.localStorage.key(i).substring(0, this.metaPrefix.length) == this.metaPrefix) {
savedModelsMeta.push(window.localStorage.key(i));
}
}
// Set the arrays to the model data and meta keys. This will then update vue loop in the load modal
let meta = savedModelsMeta.sort();
let data = savedModels.sort();
this.savedModelsMeta = meta;
this.savedModels = data;
},
/* -------------------------------------------------------------------------
Get meta data entry for saved model
------------------------------------------------------------------------- */
getMetaData(key, index) {
let data = JSON.parse(window.localStorage.getItem(index));
let metaData = data[key];
return metaData;
},
/* -------------------------------------------------------------------------
Load model from local storage
------------------------------------------------------------------------- */
load(model, community) {
if(!community) {
var target = this.savedModels[model];
var metaTarget = this.savedModelsMeta[model];
var modelData = window.localStorage.getItem(target);
var metaData = JSON.parse(window.localStorage.getItem(metaTarget));
} else {
var metaData = model
var modelData = model.modelData[0];
}
// Remove all voxel information
this.voxels = [];
this.exportSettings.name = metaData.name;
this.mode = 'drawing';
// Re get context
that = this;
this.loading = !this.loading;
setTimeout(function() {
$("#colorBg, #exportBg").spectrum("set", metaData.bgColor);
that.exportSettings.bgColor = metaData.bgColor;
if(!community) {
that.voxels = JSON.parse(modelData);
} else {
that.voxels = modelData;
}
that.loading = !that.loading;
}, 1000)
},
newModel() {
this.voxels = [];
this.mode = 'drawing';
$("#colorBg, #exportBg").spectrum("set", '#25273E');
$("#colorPicker").spectrum("set", '#ffffff');
that.exportSettings.bgColor = '#25273E';
for(let i = 0; i < Math.pow(this.canvasSize, 2); i++) {
this.voxels.push([]);
}
},
/* -------------------------------------------------------------------------
Open modal
------------------------------------------------------------------------- */
openModal(modal) {
this.modalOpen = true;
this.modal = modal;
},
/* -------------------------------------------------------------------------
Close modals
------------------------------------------------------------------------- */
closeModals() {
this.modalOpen = false;
this.modal = '';
that = this;
setTimeout(function() {
that.submitted = false;
}, 1000)
},
/* -------------------------------------------------------------------------
Open modal
------------------------------------------------------------------------- */
colorPicker(event) {
this.picker.x = event.clientX;
this.picker.y = event.clientY;
},
selectColor: _.debounce(function() {
if(this.picker.active) {
this.picker.active = !this.picker.active;
this.currentColor = this.picker.color;
$("#colorPicker").spectrum("set", this.picker.color);
}
}, 10),
/* -------------------------------------------------------------------------
Reset all of the colour pickers to their default values
------------------------------------------------------------------------- */
getVoxelsCount(model) {
return $('.v').length;
},
/* -------------------------------------------------------------------------
Reset all of the colour pickers to their default values
------------------------------------------------------------------------- */
getVerticesCount(model) {
return $('.f').length;
},
/* -------------------------------------------------------------------------
Reset all of the colour pickers to their default values
------------------------------------------------------------------------- */
resetBgColor() {
$("#colorBg, #exportBg").spectrum("set", '#25273E');
this.exportSettings.bgColor = '#25273E';
},
/* -------------------------------------------------------------------------
Get all community models from GitHub gists
------------------------------------------------------------------------- */
getCommunityModels() {
let xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.github.com/users/jcoulterdesign/gists');
xhr.send();
// Store contenxt
that = this;
xhr.onload = function() {
if (xhr.status == 200) {
let gists = JSON.parse(xhr.response);
let urls = []; // Raw urls array
gists.forEach(function(g) {
let files = g.files;
for (let key of Object.keys(files)) {
let raw_url = files[key].raw_url;
urls.push(raw_url);
}
})
// Now we have a list of all approved gists. Loop through and get the content of each one and
// store to our vue instance
var xhrReq = [];
urls.forEach(function(u, i) {
xhrReq[i] = new XMLHttpRequest();
xhrReq[i].open('GET', u);
xhrReq[i].send();
ctx = that;
xhrReq[i].onload = function() {
if (xhrReq[i].status == 200) {
ctx.communityContent.push(JSON.parse(xhrReq[i].response));
}
}
})
$('.community .x-wrap').width(urls.length * 265 + 'px')
}
};
},
/* -------------------------------------------------------------------------
Delete model from local storage
------------------------------------------------------------------------- */
deleteModel(model) {
// Get the relevant entry in storage
let target = this.savedModels[model];
let targetMeta = this.savedModelsMeta[model];
// Remove both the meta and the model data
window.localStorage.removeItem(target);
window.localStorage.removeItem(targetMeta);
// Update the UI by updating models array
this.getSavedModels();
},
/* -------------------------------------------------------------------------
Toggle the audio controller muted prop
------------------------------------------------------------------------- */
toggleAudio() {
enJin.audioController.play('pop6');
enJin.audioController.muted = !enJin.audioController.muted;
this.muted = !this.muted
},
/* -------------------------------------------------------------------------
Quick and dirty way to reduce motion in the app. Add a class to everything that
has transition-duration 0
------------------------------------------------------------------------- */
toggleMotion() {
enJin.audioController.play('pop6');
this.motion = !this.motion;
if(this.motion) {
$('*').removeClass('noMotion');
} else {
$('*').addClass('noMotion');
}
}
},
mounted() {
// Get all the community VoCSSels for github gists
this.getCommunityModels();
// Get all saved models from local storage
this.getSavedModels();
// Prepare an empty voxels array
for(let i = 0; i < Math.pow(this.canvasSize, 2); i++) {
this.voxels.push([]);
}
/* -------------------------------------------------------------------------
Set up our spectrums
------------------------------------------------------------------------- */
// Main colour selector
$("#colorPicker").spectrum({
color: "ffffff",
preferredFormat: "hex",
flat: true,
showInput: true,
showPalette: true,
palette: [],
showSelectionPalette: true, // true by default
selectionPalette: [],
clickoutFiresChange: true,
maxSelectionSize: 15,
move(color) {
that.currentColor = color.toHexString().substring(1); // #ff0000
}
});
// Auto switch to draw when selecting a colour
$('#colorPicker').on("dragstart.spectrum", function(e, color) {
that.drawMode = 'draw'
});
// Auto switch to draw when selecting a colour
$('#colorPicker').on("move.spectrum", function(e, color) {
that.drawMode = 'draw'
});
// Background color selectors for preview panel and export
$("#colorBg, #exportBg").spectrum({
color: "25273E",
containerClassName: 'bg',
move(color) {
that.bgColor = color.toHexString(); // #ff0000
that.exportSettings.bgColor = color.toHexString();
$("#colorBg, #exportBg").spectrum("set", color.toHexString());
}
});
// Make sure the first palette entry is selected
$('.sp-thumb-inner').click();
// Simple preloader
setTimeout(function() {
$('.p3d_loader').fadeOut();
}, 4000)
}
});
// Symmetry
// Fix intermittent bug
// Optimize
<script src="https://codepen.io/jcoulterdesign/pen/1e3ed378fed68f1a43bc4f73f9964945?editors=1010"></script>
<script src="https://codepen.io/jcoulterdesign/pen/6c44bfdc74442457826e062bc719c586?editors=0110"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/spectrum/1.8.1/spectrum.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dom-to-image/2.6.0/dom-to-image.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/rasterizehtml/1.3.0/rasterizeHTML.allinone.js"></script>
@import url('https://fonts.googleapis.com/css2?family=Baloo+2:wght@400;500;600;700&display=swap');
$primary: #25273E;
$secondary: #E5513A;
$borderRadius: 14px;
$voxelSize: 26; // We want to work as 1 unit is 1 voxel occupying one space. So here we are setting the the amount of pixels a voxel takes. This will need to be the same as the voxel size in the vue data object.
$shadingMixTolerance: 4; // How agressive the SASS mix is on each side of the voxel. Lower is less noticeable.
$maximumDepth: 15;
$maxX: 40;
$maxY: 40;
* {
user-select: none;
}
body {
background: $primary;
color: white;
font-family: 'Baloo 2', cursive;
margin: 0;
padding: 0;
overflow: hidden;
height: 100vh;
.l {
position: fixed;
width: 50px;
height: 10px;
background: red;
right: 358px;
top: -10px;
box-shadow: 0 0 20px 1px #f44336;
animation: love 2s infinite;
}
@keyframes love {
0%{box-shadow: 0 0 20px 1px #f44336;}
50%{box-shadow: 0 0 26px 3px #f44336;}
100%{box-shadow: 0 0 20px 1px #f44336;}
}
.noMotion {
transition-duration: 0s !important;
div {
transition-duration: 0s !important;
}
}
.hide {
pointer-events: none;
}
// Spectrum
.sp-palette .sp-thumb-light.sp-thumb-active .sp-thumb-inner,
.sp-palette .sp-thumb-dark.sp-thumb-active .sp-thumb-inner {
background-image: none;
}
.sp-flat {
z-index: 0;
}
.sp-input {
font-size: 12px !important;
border: 1px inset;
padding: 8px 10px;
margin: -20px 0 0 0;
width: calc(100% - 56px);
background: rgba(0, 0, 0, 0.3);
border-radius: 8px;
font-family: "Baloo 2", cursive;
font-weight: 700;
color: white;
position: relative;
top: -11px;
left: 2px;
border: 0 !important;
outline: none !important;
&:focus {
box-shadow: 0 0 0 3px $secondary inset;
}
}
.sp-container {
background: transparent;
border: 0;
width: 170px;
margin-left: 6px;
margin-top: 22px;
}
.sp-container.bg {
border: 0;
height: 130px;
border-radius: 10px;
width: 170px;
margin-left: 6px;
margin-top: 22px;
transform: translate(-363px, 12px) !important;
}
.sp-thumb-el {
border-radius: 10px;
transition: all .2s;
&:hover {
transform: scale(1.1);
}
}
.sp-thumb-inner {
box-shadow: 0 0 0 0 inset white;
transition: all .3s;
background: none;
}
.sp-thumb-active .sp-thumb-inner {
background: none;
box-shadow: 0 0 0 3px inset white;
}
.sp-top-inner {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 140px;
height: 110px;
right: 0;
}
.sp-palette .sp-thumb-inner {
background-position: 50% 50%;
background-repeat: no-repeat;
border-radius: 10px;
}
.sp-color,
.sp-hue,
.sp-clear {
border: 0;
}
.sp-palette-container {
border: 0;
}
.sp-palette .sp-thumb-el {
width: 40px;
height: 40px;
margin: 5px;
border: 0;
background: transparent;
}
.sp-hue {
width: 10px;
border-radius: 10px;
margin-left: 11px;
}
.sp-slider, .sp-dragger {
position: absolute;
top: 0;
cursor: pointer;
height: 10px;
width: 10px;
transform: translate(4px, 4px);
left: -6px;
top: 4px;
border-radius: 16px;
border: 2px solid #fff;
background: transparent;
opacity: 1;
}
.sp-button-container.sp-cf {
display: none;
}
.sp-picker-container {
width: 172px;
border-left: solid 0px #fff;
}
.sp-val {
box-shadow: 0 0 0 3px inset white;
border-radius: 10px;
padding: 10px;
}
.sp-color {
border-radius: 10px;
overflow: hidden;
}
.sp-top {
margin-left: 4px;
}
.p3d {
height: 100vh;
width: 100vw;
&_colorPicker {
width: 40px;
height: 40px;
background: red;
position: absolute;
z-index: 2;
border-radius: 30px;
transform: translate(-20px, -20px);
pointer-events: none;
transition: background .1s;
border: 3px solid white;
}
&_loader {
position: fixed;
background: $primary;
width: 100%;
height: 100%;
z-index: 1000;
&__inner {
position: absolute;
left: 0;
right: 0;
margin: auto;
top: 50%;
transform: translateY(-50%) scale(0);
width: 100px;
animation: loadIn 2.75s 1s cubic-bezier(0.035, 1.495, 0.625, 0.890) forwards;
.b, .f {
position: absolute;
width: 45px;
height: 45px;
background: #0000001c;
border-radius: 10px;
top: -13px;
left: 30px;
z-index: -1;
}
.f {
background: $secondary;
animation: load 1.5s 1.5s forwards;
clip-path: polygon(0 100%, 100% 100%, 100% 100%, 0 100%);
}
@keyframes load {
from {clip-path: polygon(0 100%, 100% 100%, 100% 100%, 0 100%);}
to {clip-path: polygon(0 100%, 100% 100%, 100% 0, 0 0);}
}
@keyframes loadIn {
0%{transform: translateY(-50%) scale(0); opacity: 1}
10%{transform: translateY(-50%) scale(1); opacity: 1}
90%{transform: translateY(-50%) scale(1); opacity: 1}
100%{transform: translateY(-50%) scale(0); opacity: 0}
}
}
}
&_overlay {
background: rgb(16 14 29 / 80%);
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 1;
opacity: 0;
transition: all .2s .2s;
pointer-events: none;
&.open {
opacity: 1;
pointer-events: all;
transition: all .2s 0s;
}
}
.modal {
.row {
padding: 10px;
&-half {
margin-bottom: 10px;
width: 50%;
float: left;
}
label {
font-size: 14px;
margin-bottom: 6px;
display: block;
}
}
input[type=text] {
padding: 14px 20px;
font-weight: 600;
width: calc(100% - 50px);
border-radius: 10px;
color: white;
border: none;
font-family: "Baloo 2", cursive;
background: #00000024;
outline: none;
box-shadow: 0 0 0 0px #ffc215 inset;
transition: all 0.3s;
&:focus {
box-shadow: 0 0 0 3px #ffc215 inset;
}
}
.b {
padding: 11px;
margin-top: 10px;
clear: both;
float: left;
button {
color: #6d581e;
background: #f3c032;
border: 0;
border-radius: 10px;
font-size: 14px;
padding: 16px 24px;
font-weight: 700;
outline: none;
font-family: "Baloo 2", cursive;
position: relative;
float: left;
transition: all 0.3s;
cursor: pointer;
img {
mix-blend-mode: luminosity;
}
&.submitting {
padding-left: 46px;
img {
transform: scale(1);
}
}
&:nth-of-type(1) {
margin-right: 16px;
&:hover {
transform: scale(1.1);
}
}
&:nth-of-type(2) {
margin-right: 16px;
color: #ffffff;
background: transparent;
box-shadow: 0 0 0 3px inset white;
&:hover {
background: white;
color: $primary;
}
}
img {
transform: scale(0);
position: absolute;
left: 13px;
top: 13px;
height: 26px;
transition: all 0.3s 0.1s;
}
}
}
}
&_load {
position: fixed;
left: 0;
right: 0;
margin: auto;
z-index: 10;
transform: scale(0);
height: 100%;
width: 0;
pointer-events: none;
transition: all .3s 0s cubic-bezier(0.035, 1.495, 0.625, 0.890);
&.open {
transform: scale(1);
pointer-events: all;
transition: all .3s .2s cubic-bezier(0.035, 1.495, 0.625, 0.890);
}
h4 {
color: $secondary;
}
.close {
position: absolute;
right: -24px;
top: -24px;
background: $primary;
border-radius: 100px;
width: 40px;
height: 40px;
line-height: 42px;
text-align: center;
border: 3px solid white;
cursor: pointer;
transition: all .3s;
&:hover {
background: white;
color: $primary;
}
}
&__inner {
position: fixed;
left: 0;
right: 0;
margin: auto;
z-index: 10;
background: $primary;
border: 3px solid white;
width: 440px;
border-radius: 20px;
padding: 40px;
font-weight: 600;
top: 50%;
transform: translateY(-50%) translateX(-50%);
h1 {
margin: -10px 0px 0px 10px;
}
p {
margin: 0 0 20px 10px;
}
.delete,
.load {
cursor: pointer;
}
.delete {
color: $secondary;
}
.s-wrap {
max-height: 315px;
overflow-x: hidden;
padding-right: 10px;
overflow-y: scroll;
/* width */
&::-webkit-scrollbar {
width: 4px;
border-radius: 10px;
}
/* Track */
&::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
}
/* Handle */
&::-webkit-scrollbar-thumb {
background: $secondary;
border-radius: 10px;
}
/* Handle on hover */
&::-webkit-scrollbar-thumb:hover {
//background: #555;
}
}
.p3d_load__model {
overflow: hidden;
background: transparent;
border-radius: 10px;
padding: 10px;
transition: all .3s;
position: relative;
&:last-child {
&:not(.empty) {
margin-bottom: 0;
}
}
&:not(.empty) {
cursor: pointer;
&:hover {
background: rgba(0, 0, 0, 0.2);
.delete {
opacity: 1;
top: 0;
}
}
}
.m_image {
width: 35%;
float: left;
height: 142px;
overflow: hidden;
box-sizing: border-box;
border: 3px solid white;
border-radius: 10px;
background-size: 190% !important;
background-position: center center !important;
img {
width: 100%;
border-radius: 10px;
display: block;
}
}
.m_details {
width: 50%;
float: left;
padding-left: 15px;
padding-top: 14px;
.load {
margin: 10px;
background: #ffc720;
position: absolute;
right: 10px;
top: 50%;
width: 40px;
height: 40px;
cursor: pointer;
transition: all .3s;
border-radius: 40px;
transform: scale(1);
&:hover {
transform: scale(1.1);
}
img {
position: relative;
left: 12px;
top: 8px;
opacity: 0.5;
}
}
.delete {
font-size: 14px;
opacity: 0;
transition: all .2s;
margin-top: 16px;
position: absolute;
right: 20px;
top: 10px;
&:hover {
text-decoration: underline;
}
}
p.name {
margin-bottom: 0px;
font-size: 22px;
margin-top: 8px;
text-transform: capitalize;
}
p.date {
margin-bottom: -6px;
font-size: 12px;
opacity: 0.3;
margin-top: 10px;
}
p.author {
font-size: 12px;
margin: -7px 0 7px 10px;
}
p.voxels {
margin-bottom: 0px;
font-size: 12px;
span {
color: $secondary;
}
}
}
}
}
&.community {
.p3d_load__inner {
width: 800px;
}
.p3d_load__model {
overflow: hidden;
background: transparent;
border-radius: 10px;
padding: 10px;
float: left;
transition: all 0.3s;
width: 244px;
position: relative;
}
.p_name {
margin-bottom: 1px;
font-size: 22px;
margin-top: 6px;
text-transform: capitalize;
}
.m_image {
width: 100%;
float: left;
height: 220px;
}
.m_details {
width: 100%;
float: left;
padding-left: 0;
padding-top: 24px;
margin-left: -10px;
}
.x-wrap {
width: 10000px;
}
.s-wrap {
max-height: 390px;
overflow-x: hidden;
padding-right: 0;
overflow-y: hidden;
overflow-x: scroll;
padding-bottom: 20px;
}
}
}
&_main {
&_footer {
position: absolute;
bottom: 0;
padding: 0 0 20px 50px;
font-size: 12px;
a {
color: $secondary;
font-weight: 700;
}
span {
opacity: 0.4;
}
}
.help {
font-size: 16px;
font-weight: 700;
margin-bottom: 14px;
.icon {
width: 8px;
border: 2px solid white;
padding: 4px 6px;
border-radius: 20px;
transform: scale(0.7);
cursor: pointer;
&:hover {
& + div {
opacity: 1;
transform: translateY(0px);
}
}
}
.gif {
transition: transform 0.3s 0.2s cubic-bezier(0.035, 1.495, 0.625, 0.89), opacity 0.2s 0.2s;
opacity: 0;
position: absolute;
pointer-events: none;
width: 740px;
transform: translateY(20px);
z-index: 3;
top: 48px;
border: 3px solid white;
border-radius: 10px;
padding: 16px;
background: #151c29;
.desc {
width: 200px;
float: left;
padding: 30px;
line-height: 20px;
h4 {
margin: 0;
font-size: 20px;
margin-bottom: 10px;
}
}
img {
width: 480px;
display: block;
border-radius: 10px;
float: right;
}
}
}
.button {
line-height: 42px;
border-radius: 10px;
cursor: pointer;
transition: all .3s;
font-weight: 800;
position: relative;
height: 40px;
img.icon {
transform: scale(1);
transition: all .1s .1s;
&.saving {
transition: all .1s 0s;
transform: scale(0);
}
}
img.loader {
transition: all .1s 0s;
position: absolute;
left: -4px;
height: 28px;
top: -3px;
transform: scale(0);
&.saving {
transition: all .1s .1s;
transform: scale(1);
}
}
&:hover {
& > .tooltip {
opacity: 1;
top: -54px;
transition: all .3s .2s;
}
}
img {
margin: 9px 10px;
height: 20px;
}
& > .tooltip {
transition: all .23s 0s;
left: 0;
opacity: 0;
pointer-events: none;
top: -50px;
color: #25273E;
border-radius: 10px;
position: absolute;
background: white;
width: 140px;
font-size: 14px;
text-align: center;
&:after {
position: absolute;
width: 10px;
height: 10px;
background: white;
content: "";
margin: auto;
display: block;
left: 0;
bottom: -3px;
right: 0;
transform: rotate(45deg);
}
}
&:after,
&:before {
border-radius: 10px;
position: absolute;
content: "";
width: 100%;
height: 100%;
background: rgb(0 0 0 / 30%);
top: 0;
left: 0;
z-index: -1;
transform: scale(0);
transition: all 0.3s cubic-bezier(0.035, 1.495, 0.625, 0.89);
}
&:before {
background: $secondary;
}
&.active {
&:before {
transform: scale(1);
}
}
&:not(.active):hover {
color: $secondary;
&:after {
transform: scale(1);
}
}
}
&__header {
padding: 40px;
.header_left,
.header_right {
width: 50%;
float: left;
}
.header_left {
width: 200px;
}
.header_right {
margin-top: 6px;
text-align: left;
width: calc(100% - 200px);
float: right;
.button {
width: 40px;
text-align: center;
&::before {
display: block;
content: "";
position: absolute;
height: 3px;
width: 0px;
background: $secondary;
z-index: 1;
left: 5px;
border-radius: 10px;
top: 17px;
transform: scale(1);
}
&.active {
img {
opacity: 0.5;
}
&::before {
width: 30px;
}
}
}
@keyframes pulse {
from {box-shadow: 0 0 0 0 rgba(243, 192, 50, 1)}
to {box-shadow: 0 0 0 12px rgba(243, 192, 50, 0)}
}
.buttons {
float: left;
.community {
background: #f3c032;
width: 120px;
position: relative;
font-size: 15px;
height: 44px;
line-height: 46px;
top: -14px;
color: #504014;
margin-left: 10px;
transform: scale(1);
transition: all .3s;
animation: pulse 1.3s infinite;
&::after,
&::before {
display: none;
}
&:hover {
background: #ffbc00;
transform: scale(1.05);
}
}
}
.options {
float: right;
}
input {
float: left;
width: 300px;
font-family: 'Baloo 2', cursive;
background: transparent;
font-weight: 700;
color: white;
border: 0;
border-bottom: 2px solid white;
outline: none;
padding: 0 0 8px 0;
margin-right: 16px;
}
.button {
display: inline-block;
&:hover {
.tooltip {
bottom: -50px;
}
}
.tooltip {
left: -20px;
width: 80px;
top: auto;
bottom: -44px;
&:after {
bottom: auto;
top: -3px;
}
}
}
}
}
&__editor {
height: 100px;
width: 1300px;
margin: 0 auto;
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%) scale(1);
height: 600px;
@for $i from 1 through 40 {
@media only screen and (max-height: #{970 - ($i * 8)}px) {
transform: translateY(-50%) scale(calc(1 - (0.007 * #{$i})));
}
}
.editor {
&_right {
width: 35%;
float: right;
position: relative;
&__export {
opacity: 0;
pointer-events: none;
float: left;
position: absolute;
padding-left: 70px;
padding-top: 70px;
transition: all .1s 0s;
.buttons {
clear: both;
float: left;
margin-top: 30px;
}
input[type="text"] {
padding: 14px 20px;
font-weight: 600;
border-radius: 10px;
color: white;
border: none;
font-family: 'Baloo 2', cursive;
background: #00000024;
outline: none;
box-shadow: 0 0 0 0px #ffc215 inset;
transition: all .3s;
&:focus {
box-shadow: 0 0 0 3px #ffc215 inset
}
}
input {
margin-top: 8px;
}
p.sub {
margin: -17px 0 16px 0px;
font-size: 14px;
line-height: 20px;
font-weight: 500;
opacity: 0.5;
}
button
{
color: #6d581e;
background: #f3c032;
border: 0;
border-radius: 10px;
font-size: 14px;
padding: 16px 19px;
font-weight: 700;
outline: none;
font-family: "Baloo 2", cursive;
float: left;
transition: all .3s;
cursor: pointer;
animation: pulse 1.3s infinite;
&:hover {
background: #ffbc00;
}
}
input[type="submit"] {
opacity: 0;
position: absolute;
left: -10000px;
top: -1000000px;
}
button.cp {
margin-top: 0px;
margin-left: 16px;
background: #694a7d;
color: #fff;
position: relative;
animation: none;
img {
transform: scale(0);
position: absolute;
left: 13px;
top: 13px;
height: 26px;
transition: all .3s .1s;
}
&.exporting {
padding-left: 46px;
img {
transform: scale(1);
}
}
&:hover {
background: #8f4db9;
}
}
.row {
margin-bottom: 8px;
position: relative;
clear: both;
float: left;
transition: all .3s;
&.inactive {
opacity: 0.4;
pointer-events: none;
}
.sp-replacer + img {
height: 15px;
opacity: 0.2;
cursor: pointer;
position: relative;
transition: all 0.3s;
top: 5px;
&.hide {
opacity: 0;
}
&:hover {
opacity: 1;
}
}
&-half {
float: left;
width: 50%;
input {
width: calc(100% - 50px);
}
input[type='range'] {
width: calc(100% - 22px) !important;
}
& + .save {
position: absolute;
right: -50px;
top: 44px;
width: 40px;
.tooltip {
left: -19px;
width: 80px;
}
}
}
input[type='checkbox'] {
margin-top: 8px;
appearance: none;
width: 20px;
height: 20px;
margin-left: -1px;
border-radius: 7px;
border: 3px solid white;
outline: none;
cursor: pointer;
overflow: hidden;
position: relative;
&::after {
display: block;
content: "";
width: 8px;
height: 8px;
left: 3px;
top: 3px;
border-radius: 2px;
background: #E5513A;
position: absolute;
transition: all 0.3s cubic-bezier(0.035, 1.495, 0.625, 0.89);
transform: scale(0);
}
&:checked {
&::after {
transform: scale(1);
}
}
}
input[type='range'] {
overflow: hidden;
height: 6px;
border-radius: 10px;
outline: none;
width: 200px;
-webkit-appearance: none;
background-color: rgba(0, 0, 0, 0.3);
}
input[type='range']::-webkit-slider-runnable-track {
height: 10px;
-webkit-appearance: none;
color: #13bba4;
margin-top: -1px;
}
input[type='range']::-webkit-slider-thumb {
width: 10px;
-webkit-appearance: none;
height: 10px;
cursor: ew-resize;
background: #fff;
box-shadow: -180px 0 0 180px $secondary;
}
label {
display: block;
font-weight: 700;
font-size: 14px;
display: block;
margin-top: 10px;
span {
// color: $secondary;
font-weight: 700;
span {
font-size: 12px;
font-weight: 500;
opacity: 0.3;
}
}
}
}
&.show {
opacity: 1;
pointer-events: all;
transition: all .3s .2s;
}
}
#colorBg {
& + div {
opacity: 1;
transition: all .29s;
}
&.hide {
& + div {
opacity: 0;
pointer-events: none;
}
}
}
p {
font-weight: 700;
margin: 10px 0px 20px 40px;
opacity: 1;
transition: all .2s;
transform: translateY(0px);
&.hide {
transform: translateY(-12px);
opacity: 0;
}
& + img {
position: absolute;
right: 59px;
top: 19px;
height: 15px;
opacity: 0.2;
cursor: pointer;
transition: all .3s;
&.hide {
opacity: 0;
}
&:hover {
opacity: 1;
}
}
}
p.voxelCount {
clear: both;
float: right;
position: absolute;
top: 476px;
left: 0px;
font-size: 14px;
transform: translateY(0px);
transition: all .2s .2s;
&.hide {
transition: all .002s;
pointer-events: none;
}
span {
color: $secondary;
&.block {
opacity: 0;
color: rgba(255, 255, 255, 0.5);
font-size: 12px;
display: block;
font-weight: 400;
margin-top: 10px;
&.show {
opacity: 1;
}
img {
width: 16px;
margin-top: -1px;
margin-right: 9px;
position: relative;
top: 3px;
margin-bottom: 13px;
float: left;
}
}
}
}
#colorBg + div ,
#exportBg + div {
position: absolute;
right: -6px;
top: 10px;
border: 0;
background: transparent;
.sp-preview {
position: relative;
width: 25px;
height: 20px;
border: solid 3px #fff;
margin-right: 8px;
border-radius: 6px;
float: left;
z-index: 0;
}
.sp-preview-inner {
border-radius: 3px;
}
.sp-dd {
padding: 6px 0;
height: 20px;
line-height: 12px;
float: left;
color: white;
font-size: 10px;
transform: scaleY(.6);
}
}
#exportBg + div {
margin-top: 8px;
position: static
}
&__preview {
box-shadow: 0 0 0 3px inset white;
background: $primary;
height: 415px;
width: 415px;
left: calc(65% + 40px);
float: right;
border-radius: 14px;
z-index: 1;
position: fixed;
transition: all .3s cubic-bezier(0.035, 1.495, 0.625, 0.890);
&.export {
position: fixed;
width: calc(100% - 460px);
left: 20px;
height: calc(100% + 34px);
}
&.paint {
position: fixed;
width: calc(100% - 210px);
left: 210px;
height: calc(100% + 34px);
}
// Return the true px size of a voxel based on the passed unit.
@function getVoxelSize($size, $operator) {
@return unquote($operator + $size * $voxelSize + px);
}
// Function to set the orientation of a side
@function setFaceOrientation($tx, $ty, $tz, $sx, $sy, $rx, $ry, $rz) {
@return rotateX($rx + deg) rotateY($ry + deg) rotateZ($rz + deg) scaleX($sx) scaleY($sy) translate3d(unquote($tx + ',' + $ty + ',' + $tz));
}
.p_inner {
position: absolute;
left: 3px;
top: 3px;
overflow: hidden;
width: calc(100% - 6px);
height: calc(100% - 6px);
border-radius: 10px;
.button.zooms {
background: #25273E;
display: block;
color: #fff;
border: 0;
float: right;
margin: 4px 18px;
width: 30px;
clear: both;
text-align: center;
height: 30px;
line-height: 28px;
position: relative;
z-index: 2;
transform: scale(1);
transition: all 0.2s;
img {
margin: 5px 1px;
width: 16px;
}
&.hide {
opacity: 0;
pointer-events: none;
}
&:hover {
transform: scale(1.1);
}
&:after,
&:before {
display: none;
}
&.first {
margin-top: 19px;
}
}
.views {
position: absolute;
top: 12px;
left: 15px;
z-index: 1;
padding: 0 4px;
&.show {
background: $primary;
border-radius: 10px;
.button {
opacity: 1;
transform: translateY(0px);
@for $i from 1 through 14 {
&:nth-of-type(#{$i}) {
transition: color .3s, transform .5s ($i - 1) / 20 + .2s, opacity .5s ($i - 1) / 20 + .2s;
}
}
}
}
.button {
font-size: 15px;
display: inline-block;
margin: 0 10px;
transition: all .3s;
opacity: 0;
transform: translateY(16px);
@for $i from 1 through 14 {
&:nth-of-type(#{$i}) {
transition: color .3s, transform .0s ($i - 1) / 1222 + 0s, opacity .0s ($i - 1) / 1222 + 0s;
}
}
&.active {
color: $secondary;
}
&:after,
&:before {
display: none;
}
}
}
.resetZoom {
position: absolute;
z-index: 1;
right: 10px;
top: 8px;
}
.spin {
position: absolute;
z-index: 1;
right: 10px;
bottom: 8px;
}
.exportWrap {
height: 100%;
transform-origin: 50% 365px;
transform-style: preserve-3d;
}
.zoom {
transform: scale(1);
transform-origin: 50% calc(50% + -60px);
height: 415px;
transition: all .3s;
}
.model {
position: absolute;
left: -3px;
top: -96px;
width: 415px;
height: 140px;
transform-style: preserve-3d;
transition: all .3s cubic-bezier(0.035, 1.495, 0.625, 0.890);
transform: rotateY(90deg) rotateZ(0) rotatex(0deg);
transform-origin: 50% calc(905px / 2) calc(370px / 2);
.f {
@for $v from 1 through 6 {
$width: 1;
$height: 1;
$depth: 1;
&--f {
transform: setFaceOrientation(0, 0, getVoxelSize($depth / 2, ''), $height, $width, 0, 90, 0);
}
&--ba {
transform: setFaceOrientation(0, 0, getVoxelSize($depth / 2, '-'), $height, $width, 0, 90, 0);
}
&--t {
transform: setFaceOrientation(0, 0, getVoxelSize($height / 2, ''), $depth, $width, 0, 0, 0);
}
&--b {
transform: setFaceOrientation(0, 0, getVoxelSize($height / 2, '-'), $depth, $width, 0, 0, 0);
}
&--l {
transform: setFaceOrientation(0, 0, getVoxelSize($width / 2, ''), $depth, $height, 90, 0, 0);
}
&--r {
transform: setFaceOrientation(0, 0, getVoxelSize($width / 2, '-'), $depth, $height, 90, 0, 0);
}
}
&.paintable {
cursor: pointer;
box-shadow: 0 0 0 0px $secondary inset;
transition: all .3s;
&:hover {
box-shadow: 0 0 0 4px $secondary inset;
}
}
}
&.export,
&.paint {
position: fixed;
width: calc(100% + -60px);
left: 0;
height: calc(100% + 10px);
}
&.export {
width: calc(100%);
}
&.spin {
animation: spin 6s infinite linear;
}
&.extrude {
transform: scale(.8) rotateX(-30deg) rotateY(140deg) rotateZ(0deg);
top: -144px;
}
@keyframes spin {
from { transform: scale(1) rotateY(140deg) rotateZ(0deg) rotatex(0deg);}
to { transform: scale(1) rotateY(500deg) rotateZ(0deg) rotatex(0deg);}
}
%voxel {
position: absolute;
top: 50%;
transform-style: preserve-3d;
left: 0;
right: 0;
margin: auto;
width: 10px;
}
%face {
width: $voxelSize + px;
height: $voxelSize + px;
position: absolute;
transform-style: preserve-3d;
transform-origin: 50% 50%;
background: transparent; // Default color
}
.voxel,
.voxel-group {
@extend %voxel;
}
.v {
@extend %voxel;
& .f {
@extend %face;
}
}
// Utility classes
// Colors
$colors: [#de2b14, #d5713f, #ffda25, #2c4c35, #503d1f];
@each $color in $colors {
$class: #{$color};
$class: unquote(str_slice($class, 2));
.c-#{$class} {
.f {
@for $i from 1 through 6 {
&:nth-of-type(#{$i}) {
background-color: mix(white, $color, ($i - 1) * $shadingMixTolerance);
}
}
}
}
}
@for $x from 0 through $maxX {
@for $y from 0 through $maxY {
.x-#{$x}.y-#{$y} {
transform: translateZ(getVoxelSize($x - 1, '')) translateY(getVoxelSize($y - 1, ''));
}
}
}
// Depths
// This can be done on a normal loop as depth will be limited
@for $i from 1 through $maximumDepth {
.d-#{$i} {
transform: translateX(23px * $i)
}
.g-#{$i} {
transform: translateX(-23px * ($i / 2))
}
}
}
}
}
}
&_left {
width: 65%;
float: left;
&__header {
width: 634px;
transition: all .3s 0s;
float: right;
position: relative;
left: 0;
&.export {
left: -196px;
z-index: 2;
}
.e_buttons {
float: right;
position: relative;
display: block;
&.hide {
.button {
opacity: 0;
transform: translateY(0px);
@for $i from 1 through 5 {
&:nth-of-type(#{$i}) {
transition: transform .3s ($i - 1) / 30 + 0s, opacity .2s ($i - 1) / 30 + 0s;
transform: translateY(-30px);
}
}
}
}
.button {
display: inline-block;
width: 40px;
img {
width: 20px;
position: relative;
top: 1px;
left: 1px;
}
&:nth-of-type(1) {
img {
top: 0px;
left: 0px;
}
}
.tooltip {
left: -50px;
}
@for $i from 1 through 5 {
&:nth-of-type(#{$i}) {
transition: transform .3s ($i - 1) / 30 + .3s, opacity .2s ($i - 1) / 30 + .3s;
}
}
}
}
.e_tabs {
float: left;
&__tab {
display: inline-block;
margin-bottom: 16px;
margin-right: 8px;
padding-left: 12px;
padding-right: 12px;
width: 64px;
text-align: center;
.tooltip {
left: -25px;
}
}
}
}
&__main {
.m {
&_palette {
float: left;
width: 211px;
transform: translateX(0px);
position: relative;
z-index: 1;
transition: transform .3s .2s cubic-bezier(0.035, 1.495, 0.625, 0.890), opacity .2s .2s;
.cPicker {
position: absolute;
border: 3px solid white;
border-radius: 7px;
height: 24px;
width: 24px;
line-height: 30px;
right: 42px;
cursor: pointer;
z-index:1;
text-align: center;
bottom: 36px;
svg {
height: 14px;
}
&.active {
svg path {
fill: $secondary;
}
}
svg path {
fill: white;
}
}
.buttons {
margin: 51px 0 10px 16px;
}
.button {
display: inline-block;
margin: 5px;
.tooltip {
left: -50px;
}
}
&.palette--depth {
.hollowTip {
position: absolute;
width: 140px;
.button {
width: 40px;
text-align: center;
}
p {
font-size: 13px;
color: $secondary;
margin: 10px 0 0 6px;
}
}
.dpgrid,
.hollowTip{
opacity: 1;
transform: translateY(0);
transition: transform .3s 0s cubic-bezier(0.035, 1.495, 0.625, 0.890), opacity .2s 0s;
&.hide {
transform: translateY(20px);
opacity: 0;
pointer-events: none;
}
}
.hollowTip {
transition: transform .3s .2s cubic-bezier(0.035, 1.495, 0.625, 0.890), opacity .2s .2s;
&.hide {
transition: transform 0s 0s cubic-bezier(0.035, 1.495, 0.625, 0.890), opacity 0s 0s;
}
}
.dp-outer {
width: 150px;
padding-left: 15px;
margin-top: 52px;
}
.dp {
width: 40px;
height: 40px;
margin: 5px;
border-radius: 10px;
box-shadow: 0 0 0 3px white inset;
text-align: center;
float: left;
position: relative;
cursor: pointer;
line-height: 42px;
font-weight: 700;
transition: all .2s;
&:not(.active):hover {
transform: scale(1.1);
color: $primary;
background: white;
}
&.active {
color: $primary;
background: white;
}
}
}
&.hide {
opacity: 0;
pointer-events: none;
transform: translateX(-40px);
position: absolute;
transition: transform .3s 0s cubic-bezier(0.035, 1.495, 0.625, 0.890), opacity .2s 0s;
}
}
&_grid {
width: 634px;
box-shadow: 0 0 0 3px inset white;
border-radius: $borderRadius;
overflow: hidden;
transition: opacity 0.3s .1s;
opacity: 1;
position: relative;
.helper-x {
width: 316px;
height: 640px;
border-right: 2px dashed rgb(255 255 255 / 8%);
position: absolute;
pointer-events: none;
transition: all .3s;
z-index: 1;
&.active {
border-right: 2px dashed $secondary;
}
}
.helper-y {
width: 640px;
height: 316px;
border-bottom: 2px dashed rgb(255 255 255 / 8%);
position: absolute;
pointer-events: none;
transition: all .3s;
z-index: 1;
&.active {
border-bottom: 2px dashed $secondary;
}
}
&.loading {
width: 633px;
height: 633px;
img.loader {
transform: translateY(-50%) scale(1);
}
}
img.loader {
position: absolute;
left: 0;
right: 0;
margin: auto;
width: 30px;
z-index: 1;
top: 50%;
transform: translateY(-50%) scale(0);
transition: all 0.3s 0s cubic-bezier(0.035, 1.495, 0.625, 0.89);
}
&.hide {
opacity: 0;
transition: opacity 0.3s 0s;
}
&__pixel {
box-shadow: 0 0 0 1px inset rgba(255, 255, 255, 0.1);
float: left;
position: relative;
cursor: pointer;
width: 37.2px;
height: 37.2px;
&:after {
display: block;
content: '';
position: absolute;
width: 100%;
height: 100%;
box-shadow: 0 0 0 3px #fff;
border-radius: 5px;
z-index: 2;
pointer-events: none;
opacity: 0;
transition: all .01s;
}
&:hover {
&:after {
opacity: 1;
}
}
.p {
position: absolute;
pointer-events: none;
width: 100%;
height: 100%;
left: 0;
top: 0;
}
.d {
position: absolute;
width: 100%;
text-align: center;
top: 50%;
transform: translateY(-50%);
display: none;
color: black;
font-weight: 700;
font-size: 16px;
pointer-events: none;
&.show {
display: block;
}
}
}
}
}
}
}
}
}
}
}
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/spectrum/1.8.1/spectrum.css" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment