WIP WYSIWYG ~3D SVG editor.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Initial commit of properties panel.

Displaying properties of individual selected objects works, but
currently multiple object property display is freaking out.

Editing displayed properties is currently unimplemented.

+282 -23
+89 -7
index.html
··· 12 12 <body> 13 13 <main> 14 14 <menu> 15 - <li><button id="select">Select</button></li> 16 - <li><button id="grab">Grab</button></li> 17 - <li><button id="rotate">Rotate</button></li> 18 - <li><button id="scale">Scale</button></li> 19 - <li><button id="delete">Delete</button></li> 20 - <li><button id="add">Add</button></li> 15 + <li><button id="tool-select">Select</button></li> 16 + <li><button id="tool-translate">Translate</button></li> 17 + <li><button id="tool-rotate">Rotate</button></li> 18 + <li><button id="tool-scale">Scale</button></li> 19 + <li><button id="tool-delete">Delete</button></li> 20 + <li><button id="tool-add">Add</button></li> 21 21 </menu> 22 22 <div class="canvas-container"> 23 23 <svg id="ui" class="canvas-layer" xmlns="http://www.w3.org/2000/svg"></svg> ··· 34 34 </svg> 35 35 </div> 36 36 <aside id="properties"> 37 - 37 + <h2>No objects selected</h2> 38 + <hr /> 39 + <div class="prop-group"> 40 + <h3>Transform</h3> 41 + <div id="translate" class="vector prop"> 42 + <h4>Translate</h4> 43 + <label for="translate-x">X</label> 44 + <input type="number" id="translate-x" /> 45 + <label for="translate-y">Y</label> 46 + <input type="number" id="translate-y" /> 47 + <label for="translate-z">Z</label> 48 + <input type="number" id="translate-z" /> 49 + </div> 50 + <div id="rotate" class="vector prop"> 51 + <h4>Rotate</h4> 52 + <label for="rotate-x">X</label> 53 + <input type="number" id="rotate-x" /> 54 + <label for="rotate-y">Y</label> 55 + <input type="number" id="rotate-y" /> 56 + <label for="rotate-z">Z</label> 57 + <input type="number" id="rotate-z" /> 58 + </div> 59 + <div id="scale" class="vector prop"> 60 + <h4>Scale</h4> <label for="scale-x">X</label> 61 + <input type="number" id="scale-x" /> 62 + <label for="scale-y">Y</label> 63 + <input type="number" id="scale-y" /> 64 + <label for="scale-z">Z</label> 65 + <input type="number" id="scale-z" /> 66 + </div> 67 + </div> 68 + <div class="prop-group"> 69 + <h3>Shape</h3> 70 + <div id="width" class="number prop"> 71 + <label for="width-value">Width</label> 72 + <input type="number" id="width-value" /> 73 + </div> 74 + <div id="height" class="number prop"> 75 + <label for="height-value">Height</label> 76 + <input type="number" id="height-value" /> 77 + </div> 78 + <div id="depth" class="number prop"> 79 + <label for="depth-value">Depth</label> 80 + <input type="number" id="depth-value" /> 81 + </div> 82 + <div id="length" class="number prop"> 83 + <label for="length-value">Length</label> 84 + <input type="number" id="length-value" /> 85 + </div> 86 + <div id="radius" class="number prop"> 87 + <label for="radius-value">Radius</label> 88 + <input type="number" id="radius-value" /> 89 + </div> 90 + <div id="diameter" class="number prop"> 91 + <label for="diameter-value">Diameter</label> 92 + <input type="number" id="diameter-value" /> 93 + </div> 94 + <div id="sides" class="number prop"> 95 + <label for="sides-value">Sides</label> 96 + <input type="number" id="sides-value" /> 97 + </div> 98 + </div> 99 + <div class="prop-group"> 100 + <h3>Appearance</h3> 101 + <div id="color" class="color prop"> 102 + <label for="color-value">Color</label> 103 + <input type="color" id="color-value" /> 104 + </div> 105 + <div id="stroke" class="color prop"> 106 + <label for="stroke-value">Stroke</label> 107 + <input type="number" id="stroke-value" /> 108 + </div> 109 + <div id="fill" class="optional color prop"> 110 + <label for="fill-enabled">Fill</label> 111 + <input type="checkbox" id="fill-enabled" /> 112 + <input type="color" id="fill-value" /> 113 + </div> 114 + <div id="backface" class="optional color prop"> 115 + <label for="backface-enabled">Backface</label> 116 + <input type="checkbox" id="backface-enabled" /> 117 + <input type="color" id="backface-value" /> 118 + </div> 119 + </div> 38 120 </aside> 39 121 </main> 40 122 <footer>
+48 -14
styles.css
··· 19 19 border: none; 20 20 } 21 21 22 - .hidden { 23 - display: none; 24 - } 25 - 26 22 /* Presets */ 27 23 :root { 28 24 --charcoal: #333; 29 25 --raisin: #534; 26 + --faded-raisin: #5342; 30 27 --plum: #636; 31 28 --rose: #C25; 32 29 --orange: #E62; ··· 91 88 main { 92 89 width: 100vw; 93 90 height: 100vh; 91 + 92 + display: grid; 93 + grid-template-columns: 1fr 20rem; 94 94 } 95 95 96 96 /* Canvas */ 97 97 .canvas-container { 98 98 position: relative; 99 - width: 100vw; 100 - height: 100vh; 99 + width: 100%; 100 + height: 100%; 101 101 } 102 102 103 103 .canvas-layer { 104 104 position: absolute; 105 - width: 100vw; 106 - height: 100vh; 105 + width: 100%; 106 + height: 100%; 107 107 pointer-events: none; 108 108 } 109 109 ··· 148 148 149 149 /* Properties */ 150 150 #properties { 151 - position: fixed; 152 - top: 1rem; 153 - bottom: 1rem; 154 - right: 1rem; 155 - width: 20rem; 151 + max-height: 100%; 152 + overflow-y: scroll; 153 + margin: 1rem; 156 154 padding: 1rem; 157 - resize: horizontal; 158 155 background: var(--lace); 159 156 color: var(--raisin); 160 157 border-radius: var(--roundness); 158 + } 159 + 160 + .prop-group { 161 + padding: .5rem; 162 + margin: .5rem; 163 + border: 2px solid var(--faded-raisin); 164 + border-radius: var(--roundness); 165 + } 166 + 167 + .prop { 168 + display: flex; 169 + flex-flow: row wrap; 170 + align-items: center; 171 + } 172 + 173 + .prop h4 { 174 + flex-basis: 100%; 175 + } 176 + 177 + .prop input[type="number"] { 178 + width: 1rem; 179 + flex-grow: 1; 180 + } 181 + 182 + #properties label { 183 + font-weight: bold; 184 + font-size: 1em; 185 + margin: 0 .25rem; 186 + } 187 + 188 + #properties.hidden .prop-group, #properties .hidden { 189 + display: none; 190 + } 191 + 192 + /* Hide disabled optional properties */ 193 + #properties .prop.optional input[type="checkbox"]:not(:checked) + input { 194 + display: none; 161 195 } 162 196 163 197 /* Footer */
+145 -2
zoodle.js
··· 1 1 class Zoodle { 2 - constructor(sceneElem, overlayElem, uiElem) { 2 + constructor(sceneElem, overlayElem, uiElem, props) { 3 3 // TODO: Calculate appropriate zooms and sizes 4 4 let zoom = 10; 5 5 let rotate = { x: -Math.atan(1 / Math.sqrt(2)), y: TAU / 8 }; ··· 35 35 }; 36 36 this.tool = this.tools.rotate; 37 37 38 + this.props = props; 39 + 38 40 this.presets = { 39 41 solar: new Solar(), 40 42 } ··· 62 64 uiElem.addEventListener("lostpointercapture", _ => uiElem.classList.remove("active")); 63 65 64 66 this.update(); 67 + this.updateProperties(); 65 68 } 66 69 67 70 update() { ··· 97 100 } else { 98 101 this.selection.push(target); 99 102 } 103 + this.updateProperties(); 100 104 this.updateHighlights(); 101 105 this.updateUI(); 102 106 } 103 107 104 108 clearSelection() { 105 109 this.selection = []; 110 + this.updateProperties(); 106 111 this.updateHighlights(); 107 112 this.updateUI(); 108 113 } ··· 144 149 }); 145 150 146 151 this.tool.drawWidget(targets); 152 + } 153 + 154 + updateProperties() { 155 + // Set properties header. 156 + let header = props.querySelector("h2"); 157 + if (this.selection.length === 0) { 158 + header.textContent = "No objects selected"; 159 + } else if (this.selection.length === 1) { 160 + let path = ""; 161 + let target = this.selection[0]; 162 + while (target.addTo) { 163 + path = `/${target.constructor.type}${path}`; 164 + target = target.addTo; 165 + } 166 + header.textContent = path; 167 + } else { 168 + header.textContent = `${this.selection.length} objects selected`; 169 + } 170 + 171 + // Shortcut: Just hide all entries if nothing's selected. 172 + if (this.selection.length === 0) { 173 + props.classList.add("hidden"); 174 + return; 175 + } 176 + props.classList.remove("hidden"); 177 + 178 + // Set properties from first selected object. 179 + this.setProperties(this.selection[0], true); 180 + 181 + // Condense properties from other selected objects. 182 + for (let i = 1; i < this.selection.length; i++) { 183 + let target = this.selection[i]; 184 + this.setProperties(target, false); 185 + } 186 + } 187 + 188 + setProperties(srcObj, updateValues = true) { 189 + let srcProps = srcObj.constructor.optionKeys; 190 + let destProps = this.props.querySelectorAll(".prop"); 191 + destProps.forEach(prop => { 192 + if (!srcProps.includes(prop.id)) { 193 + prop.classList.add("hidden"); 194 + return; 195 + } 196 + prop.classList.remove("hidden"); 197 + 198 + if (updateValues) { 199 + this.setProperty(srcObj, prop); 200 + return; 201 + } 202 + 203 + let newValue = this.getProperty(prop); 204 + let oldValue = srcObj[prop.id]; 205 + if (newValue != oldValue) { 206 + prop.classList.add("hidden"); 207 + } 208 + }); 209 + } 210 + 211 + getPropertyType(prop) { 212 + let types = ["vector", "number", "color", "bool"]; 213 + for (let i = 0; i < types.length; i++) { 214 + if (prop.classList.contains(types[i])) { 215 + return types[i]; 216 + } 217 + } 218 + return null; 219 + } 220 + 221 + setProperty(srcObj, prop) { 222 + let type = this.getPropertyType(prop); 223 + 224 + // check for optional properties that are either false or <value>. 225 + const optional = prop.classList.contains("optional"); 226 + if (optional) { 227 + document.getElementById(`${prop.id}-enabled`).enabled = srcObj[prop.id] !== false; 228 + } 229 + 230 + const input = document.getElementById(`${prop.id}-value`); 231 + 232 + switch (type) { 233 + case "bool": 234 + input.checked = srcObj[prop.id] != false; 235 + break; 236 + case "number": 237 + input.valueAsNumber = srcObj[prop.id]; 238 + break; 239 + case "vector": 240 + let {x, y, z} = srcObj[prop.id]; 241 + document.getElementById(`${prop.id}-x`).value = x; 242 + document.getElementById(`${prop.id}-y`).value = y; 243 + document.getElementById(`${prop.id}-z`).value = z; 244 + break; 245 + case "color": 246 + input.value = this.normalizeColor(srcObj[prop.id]); 247 + break; 248 + } 249 + } 250 + 251 + normalizeColor(color) { 252 + if (/^#[0-9a-fA-F]{6}$/.test(color)) { 253 + return color; 254 + } 255 + 256 + if (/^#[0-9a-fA-F]{3}$/.test(color)) { 257 + return `#${color[1]}${color[1]}${color[2]}${color[2]}${color[3]}${color[3]}`; 258 + } 259 + 260 + return "#000000"; 261 + } 262 + 263 + getProperty(prop) { 264 + let type = this.getPropertyType(prop); 265 + 266 + // check for optional properties that are either false or <value>. 267 + const optional = prop.classList.contains("optional"); 268 + if (optional && !document.getElementById(`${prop.id}-enabled`).checked) { 269 + return false; 270 + } 271 + 272 + const input = document.getElementById(`${prop.id}-value`); 273 + 274 + switch (type) { 275 + case "bool": 276 + return input.checked; 277 + case "number": 278 + return input.valueAsNumber; 279 + case "vector": 280 + return { 281 + x: document.getElementById(`${prop.id}-x`).valueAsNumber, 282 + y: document.getElementById(`${prop.id}-y`).valueAsNumber, 283 + z: document.getElementById(`${prop.id}-z`).valueAsNumber, 284 + }; 285 + case "color": 286 + return input.value; 287 + } 288 + return null; 147 289 } 148 290 149 291 syncLayers() { ··· 767 909 const sceneElem = document.querySelector("#canvas"); 768 910 const overlayElem = document.querySelector("#overlay"); 769 911 const uiElem = document.querySelector("#ui"); 770 - const zoodle = new Zoodle(sceneElem, overlayElem, uiElem); 912 + const props = document.querySelector("#properties"); 913 + const zoodle = new Zoodle(sceneElem, overlayElem, uiElem, props);