WIP WYSIWYG ~3D SVG editor.
0
fork

Configure Feed

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

Add a new outliner panel to the editor.

Initial pass on adding an outliner, showing all objects in the scene
and allowing users to click to select them, even objects without a
physical representation on screen like Anchors and Groups.

+174 -16
+8
index.html
··· 34 34 </filter> 35 35 </svg> 36 36 </div> 37 + <aside id="outliner"> 38 + <h2>Outliner</h2> 39 + <hr /> 40 + <ul id="outliner-list"> 41 + <li>No objects</li> 42 + </ul> 43 + </aside> 37 44 <aside id="properties"> 38 45 <h2>No objects selected</h2> 39 46 <hr /> ··· 126 133 <script src="globals.js"></script> 127 134 <script src="presets.js"></script> 128 135 <script src="tools.js"></script> 136 + <script src="outliner.js"></script> 129 137 <script src="properties.js"></script> 130 138 <script src="zoodle.js"></script> 131 139 <script src="input-enhancements.js"></script>
+1 -5
input-enhancements.js
··· 13 13 this.input.addEventListener( "blur", this.disableDrag.bind(this) ); 14 14 } 15 15 enableDrag() { 16 - console.log(this.input + " focused"); 17 16 this.input.addEventListener( "pointerdown", this._dragStart ); 18 17 } 19 18 disableDrag() { 20 - console.log(this.input + " blurred"); 21 19 this.input.removeEventListener( "pointerdown", this._dragStart ); 22 20 this.input.removeEventListener( "pointermove", this._dragMove ); 23 21 this.input.removeEventListener( "pointerup", this._dragEnd ); 24 22 this.input.removeEventListener( "pointercancel", this._dragEnd ); 25 23 } 26 24 dragStart(event) { 27 - console.log(this.input + " dragging"); 28 25 this.startX = event.screenX; 29 26 this.startY = event.screenY; 30 27 this.startValue = this.input.valueAsNumber; ··· 38 35 const dx = event.screenX - this.startX; 39 36 const dy = event.screenY - this.startY; 40 37 let step = this.input.step ? parseFloat(this.input.step) : 1; 41 - this.input.valueAsNumber = Math.round( this.startValue + dy * -0.1 * step * 100 ) / 100; 38 + this.input.valueAsNumber = Math.round( (this.startValue + dy * -0.1 * step) * 100 ) / 100; 42 39 43 40 // Manually fire input event since updating it programmatically won't. 44 41 const inputEvent = new Event('input', { ··· 48 45 this.input.dispatchEvent(inputEvent); 49 46 } 50 47 dragEnd(event) { 51 - console.log(this.input + " released"); 52 48 this.startX = 0; 53 49 this.startY = 0; 54 50
+69
outliner.js
··· 1 + class Outliner { 2 + constructor(panel, editor) { 3 + this.panel = panel; 4 + this.editor = editor; 5 + } 6 + 7 + updatePanel() { 8 + // Loop over children of scene and populate lists 9 + const list = document.getElementById( "outliner-list" ); 10 + list.innerHTML = ""; // Clear the list 11 + 12 + const li = this.listify( this.editor.scene ); 13 + if ( li ) { 14 + list.appendChild( li ); 15 + } 16 + } 17 + 18 + // Take a Zdog object, produce a list element. 19 + // if this object has children, this list element has another list nested inside. 20 + // this function is called recursively. 21 + listify(obj) { 22 + if (obj.compositeChild) return null; 23 + const li = document.createElement( "li" ); 24 + li.setAttribute( "data-type", obj.constructor.type ); 25 + li.obj = obj; 26 + li.style.setProperty( "--color", obj.color || "#333" ); 27 + 28 + const icon = document.createElement( "span" ); 29 + icon.classList.add( "icon" ); 30 + 31 + const label = document.createElement( "span" ); 32 + label.classList.add( "name" ); 33 + 34 + const name = document.createTextNode( obj.name || obj.constructor.type ); 35 + 36 + label.appendChild( icon ); 37 + label.appendChild( name ); 38 + li.appendChild( label ); 39 + 40 + label.onclick = () => this.editor.do(new SelectCommand( this.editor, obj, true )); 41 + 42 + let children = obj.children.map( this.listify.bind( this ) ); 43 + children = children.filter( child => child !== null ); 44 + 45 + if ( children.length === 0 ) { 46 + return li; 47 + } 48 + 49 + const list = document.createElement( "ul" ); 50 + children.forEach( child => { 51 + list.appendChild( child ); 52 + }); 53 + li.appendChild( list ); 54 + 55 + return li; 56 + } 57 + 58 + updateHighlights() { 59 + const list = document.getElementById( "outliner-list" ); 60 + const items = list.querySelectorAll( "li" ); 61 + items.forEach( item => { 62 + if ( this.editor.selection.indexOf( item.obj ) !== -1 ) { 63 + item.classList.add( "selected" ); 64 + } else { 65 + item.classList.remove( "selected" ); 66 + } 67 + }); 68 + } 69 + }
+1 -1
properties.js
··· 173 173 targets.forEach( (target) => { 174 174 target[propElem.id] = value; 175 175 // TODO: Add additional type information to props so we can check if we actually need to do this. 176 - if (t.updatePath) target.updatePath(); 176 + if (target.updatePath) target.updatePath(); 177 177 }); 178 178 } 179 179
+86 -6
styles.css
··· 76 76 left: 0; 77 77 height: 100vh; 78 78 padding-left: 1rem; 79 + padding-top: 1rem; 79 80 display: flex; 80 81 flex-direction: column; 81 82 gap: .5em; ··· 91 92 92 93 display: grid; 93 94 grid-template-columns: 1fr 20rem; 95 + grid-template-rows: 20rem 1fr; 96 + grid-template-areas: 97 + "canvas outliner" 98 + "canvas properties"; 94 99 } 95 100 96 101 /* Canvas */ 97 102 .canvas-container { 98 103 position: relative; 99 - width: 100%; 100 - height: 100%; 104 + grid-area: canvas; 101 105 } 102 106 103 107 .canvas-layer { ··· 130 134 } 131 135 132 136 #overlay { 137 + opacity: 0.8; 133 138 z-index: 1; 134 139 } 135 140 136 - #overlay { 141 + /*#overlay:not(:empty) { 137 142 filter: url(#better-highlight); 138 - } 143 + }*/ 139 144 140 145 #ui { 141 146 z-index: 2; ··· 146 151 cursor: pointer; 147 152 } 148 153 149 - /* Properties */ 150 - #properties { 154 + /* Panels */ 155 + #properties, #outliner { 151 156 max-height: 100%; 152 157 overflow-y: scroll; 153 158 margin: 1rem; ··· 159 164 box-shadow: .5px .5px 0 .5px var(--raisin); 160 165 } 161 166 167 + #properties { 168 + grid-area: properties; 169 + margin-top: .5rem; 170 + } 171 + 172 + #outliner { 173 + grid-area: outliner; 174 + margin-bottom: .5rem; 175 + } 176 + 177 + /* Properties */ 162 178 .prop-group { 163 179 padding: .5rem; 164 180 margin: .5rem; ··· 220 236 #properties .prop.optional input[type="checkbox"]:not(:checked) + input { 221 237 display: none; 222 238 } 239 + 240 + /* Outliner */ 241 + #outliner ul { 242 + --color: #333; 243 + padding-left: 0; 244 + } 245 + 246 + #outliner li { 247 + list-style: none; 248 + padding: .1rem .5rem; 249 + padding-right: 0; 250 + font-size: 1.1rem; 251 + } 252 + 253 + #outliner .name { 254 + display: inline-block; 255 + width: 100%; 256 + } 257 + 258 + #outliner li.selected { 259 + background: var(--peach); 260 + color: #111; 261 + border-radius: var(--roundness); 262 + 263 + & > .name { 264 + font-weight: bold; 265 + color: #000; 266 + text-decoration: underline; 267 + } 268 + } 269 + 270 + #outliner .icon { 271 + display: inline-block; 272 + width: 1rem; 273 + height: 1rem; 274 + background-color: var(--color); 275 + vertical-align: middle; 276 + margin-right: .25rem; 277 + border-radius: .25rem; 278 + } 279 + 280 + /* Outliner icons */ 281 + /*#outliner .icon { 282 + clip-path: circle(50%); 283 + } 284 + 285 + #outliner [data-type] > .name > .icon { 286 + width: 32px; 287 + height: 32px; 288 + transform: scale(0.5); 289 + margin: -8px; 290 + margin-right: calc( .25rem - 8px ); 291 + } 292 + 293 + #outliner [data-type="Illustration"] > .name > .icon { 294 + clip-path: path("M30.5 6.3c0-1.8.5-5.1-6.9-5.1S2.5.5 1.4 1.6 1 17.4.9 20C.8 33.8.8 30.8 29.4 30.9s.4-21.1 1-24.7zM3.8 3.7c7.1-.1 14.2 0 21.3.6 1.3 7.2 1.4 14.5.4 21.7-7.1-.1-14.2-.3-21.3-.4-.3-6.1-.4-20.7-.4-21.9zm9.3 16.4c-1 .1-2.3.6-3.3.2-1.7-.8-.5-2.5 0-3.6 1.4-2.6 2.4-5.4 3.6-8 0-1.5 2-2 2.8-.7 1.7 2.8 3.5 5.5 5.1 8.4 2.2 4.1-6.1 3.1-8.3 3.8z"); 295 + } 296 + 297 + #outliner [data-type="Cone"] > .name > .icon { 298 + clip-path: path("M27.7,22.6c-3.4-5.9-6.4-11.9-9-18.2,0,0,0,0,0,0-.4-.9-.6-2.1-1.8-2.1-4.8-1.4-8.7,12.1-10.9,15.7-.4,2-3.8,4.7-1.8,6.5,3.9,3.3,25,5.9,23.5-1.9ZM10.1,16c1.6-3.6,3.3-7.4,5.8-10.5,2.2,5.2,4.6,10.2,7.3,15.1-4.8-1.3-10.1-1.7-14.9-.6.6-1.3,1.2-2.7,1.8-4Z"); 299 + } 300 + #outliner [data-type="Shape"] > .name > .icon { 301 + clip-path: path("M26.58 5.2c-2.01-5-9.77-1.5-8.26 3.29-9.06 2.82.9 13.49-6.37 15.01 1.51-6.12-7.26-9.07-8.02-2.1-.24 2.47 1.45 4.4 4.36 4.16 9 .93 9.75-1.72 8.85-10.88-.25-2.54 1.82-4.02 3.15-3.98 2.39.69 8.54-.26 6.29-5.5zM6.61 22.66c-1.14-2.55 2.18-3.31 2.7-1.4.45 1.64-1.84 2.96-2.7 1.4zM24.54 7.08c.46 1.93-3.4 2.59-3.73.65-.41-2.39 2.91-4.06 3.73-.65z"); 302 + }*/ 223 303 224 304 /* Footer */ 225 305 /* Animation */
+9 -4
zoodle.js
··· 1 1 class Zoodle { 2 - constructor(sceneElem, overlayElem, uiElem, panelElem) { 2 + constructor(sceneElem, overlayElem, uiElem, propElem, outlineElem) { 3 3 // TODO: Calculate appropriate zooms and sizes 4 4 let zoom = 30; 5 5 let rotate = { x: -Math.atan(1 / Math.sqrt(2)), y: Zdog.TAU / 8 }; ··· 35 35 }; 36 36 this.tool = this.tools.translate; 37 37 38 - this.props = new Properties(panelElem, this); 38 + this.props = new Properties(propElem, this); 39 + this.outliner = new Outliner(outlineElem, this); 39 40 40 41 this.presets = { 41 42 solar: new Solar(), ··· 72 73 this.selection = [this.scene.children[0].children[0]]; 73 74 74 75 this.props.updatePanel(); 76 + this.outliner.updatePanel(); 75 77 this.updateHighlights(); 76 78 this.updateUI(); 77 79 this.update(); ··· 136 138 } 137 139 138 140 updateHighlights() { 141 + this.outliner.updateHighlights(); 142 + 139 143 this.overlay.children = []; 140 144 this.selection.forEach((selected) => { 141 145 let highlight = selected.copyGraph({ ··· 295 299 const sceneElem = document.querySelector("#canvas"); 296 300 const overlayElem = document.querySelector("#overlay"); 297 301 const uiElem = document.querySelector("#ui"); 298 - const props = document.querySelector("#properties"); 299 - const zoodle = new Zoodle(sceneElem, overlayElem, uiElem, props); 302 + const propElem = document.querySelector("#properties"); 303 + const outlineElem = document.querySelector("#outliner"); 304 + const zoodle = new Zoodle(sceneElem, overlayElem, uiElem, propElem, outlineElem);