A Pen by Jamie Coulter on CodePen.
Created
March 9, 2021 15:27
-
-
Save yutkat/73f604df2f86e8d8fc47414674017344 to your computer and use it in GitHub Desktop.
VoCSSels - Easily create 3D CSS & HTML Voxel Models
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
._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 |
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
/* | |
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, """).replace(/'/g, "'"); | |
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 |
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="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> |
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
@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; | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
} |
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
<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