WIP WYSIWYG ~3D SVG editor.
0
fork

Configure Feed

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

Initial commit Selection and orbiting is working, no actual editing though.

It's more than it looks like. Getting selection and orbiting working
required implementing my own input layer since Zdog doesn't easily
allow you to grab input events on its objects. And selection
highlighting was a whole can of worms itself.

Different55 ca067275

+593
+4
.gitignore
··· 1 + *.ai 2 + *.blend 3 + zdog.js 4 + zfetch.js
+48
index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>Zoodle</title> 7 + <link rel="stylesheet" href="styles.css"> 8 + <link rel="stylesheet" href="https://use.typekit.net/mcn0osz.css"> 9 + <script src="zdog.js"></script> 10 + <script src="zfetch.js"></script> 11 + <!-- unpkg zdog --> 12 + <!--<script src="https://unpkg.com/zdog@1.1.3/dist/zdog.dist.js"></script>--> 13 + </head> 14 + <body> 15 + <main> 16 + <menu> 17 + <li><button id="select">Select</button></li> 18 + <li><button id="grab">Grab</button></li> 19 + <li><button id="rotate">Rotate</button></li> 20 + <li><button id="scale">Scale</button></li> 21 + <li><button id="delete">Delete</button></li> 22 + <li><button id="add">Add</button></li> 23 + </menu> 24 + <div class="canvas-container"> 25 + <svg id="canvas" class="canvas-layer" xmlns="http://www.w3.org/2000/svg"></svg> 26 + <svg id="overlay" class="canvas-layer" xmlns="http://www.w3.org/2000/svg"> 27 + <svg id="widgets" class="canvas-layer" xmlns="http://www.w3.org/2000/svg"></svg> 28 + </svg> 29 + <svg id="defs"> 30 + <filter id="better-highlight" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse"> 31 + <feMorphology operator="dilate" radius="2" result="outer" /> 32 + <feComposite in="SourceGraphic" operator="xor" result="outline" /> 33 + <feFlood flood-color="#E628" /> 34 + <feComposite in2="SourceGraphic" operator="in" /> 35 + <feComposite in2="outline" operator="over" /> 36 + </filter> 37 + </svg> 38 + </div> 39 + <aside id="properties"> 40 + 41 + </aside> 42 + </main> 43 + <footer> 44 + Made with love, anime.js, and zdog by Jaromino. 45 + </footer> 46 + <script src="zoodle.js"></script> 47 + </body> 48 + </html>
+176
styles.css
··· 1 + /* Global styles */ 2 + *, *::before, *::after { 3 + box-sizing: border-box; 4 + margin: 0; 5 + line-height: calc(1em + 0.5rem); 6 + } 7 + 8 + body { 9 + -webkit-font-smoothing: antialiased; 10 + } 11 + 12 + img, picture, video, canvas, svg { 13 + display: block; 14 + max-width: 100%; 15 + } 16 + 17 + input, button, textarea, select { 18 + font: inherit; 19 + border: none; 20 + } 21 + 22 + .hidden { 23 + display: none; 24 + } 25 + 26 + /* Presets */ 27 + :root { 28 + --charcoal: #333; 29 + --raisin: #534; 30 + --plum: #636; 31 + --rose: #C25; 32 + --orange: #E62; 33 + --gold: #EA0; 34 + --lemon: #ED0; 35 + --peach: #FDB; 36 + --lace: #FFF4E8; 37 + --blueberry: #359; 38 + --lime: #4A2; 39 + --mint: #CFD; 40 + 41 + --roundness: .5rem; 42 + } 43 + 44 + /* Fonts */ 45 + body { 46 + font-family: "din-2014-rounded-variable", sans-serif; 47 + font-variation-settings: "wght" 400; 48 + color: var(--raisin); 49 + } 50 + 51 + /* Toolbar */ 52 + button { 53 + width: 2rem; 54 + height: 2rem; 55 + border-radius: var(--roundness); 56 + 57 + color: var(--raisin); 58 + background: var(--lace); 59 + box-shadow: 0 0 0 0.25rem transparent; 60 + 61 + cursor: pointer; 62 + font-size: 0; 63 + transition: box-shadow 0.2s, background 0.2s; 64 + } 65 + 66 + button:first-letter { 67 + font-size: 1.5rem; 68 + line-height: 2rem; 69 + font-variation-settings: "wght" 600; 70 + } 71 + 72 + button:hover { 73 + background: var(--peach); 74 + box-shadow: 0 1px 0.25rem 0 color-mix(in srgb, var(--raisin) 10%, transparent); 75 + } 76 + 77 + menu { 78 + position: fixed; 79 + left: 0; 80 + height: 100vh; 81 + padding-left: 1rem; 82 + display: flex; 83 + flex-direction: column; 84 + gap: .5em; 85 + } 86 + 87 + menu li { 88 + list-style: none; 89 + } 90 + 91 + main { 92 + width: 100vw; 93 + height: 100vh; 94 + } 95 + 96 + /* Canvas */ 97 + .canvas-container { 98 + position: relative; 99 + width: 100vw; 100 + height: 100vh; 101 + } 102 + 103 + .canvas-layer { 104 + position: absolute; 105 + width: 100vw; 106 + height: 100vh; 107 + pointer-events: none; 108 + } 109 + 110 + #canvas { 111 + pointer-events: auto; 112 + z-index: 0; 113 + } 114 + 115 + /* 116 + #canvas .selected { 117 + filter: drop-shadow(0 0 0.2px #E62) 118 + drop-shadow(0 0 0.1px #E62) 119 + drop-shadow(0 0 0.05px #E62) 120 + drop-shadow(0 0 0.025px #E62) 121 + drop-shadow(0 0 0.0125px #E62); 122 + } 123 + */ 124 + 125 + #overlay { 126 + z-index: 1; 127 + } 128 + 129 + #overlay { 130 + filter: url(#better-highlight); 131 + } 132 + 133 + #widgets { 134 + z-index: 2; 135 + } 136 + 137 + /* Properties */ 138 + #properties { 139 + position: fixed; 140 + top: 1rem; 141 + bottom: 1rem; 142 + right: 1rem; 143 + width: 20rem; 144 + padding: 1rem; 145 + resize: horizontal; 146 + background: var(--lace); 147 + color: var(--raisin); 148 + border-radius: var(--roundness); 149 + } 150 + 151 + /* Footer */ 152 + /* Animation */ 153 + @keyframes toast { 154 + 0% { 155 + transform: translateY(100%); 156 + } 157 + 10%, 90% { 158 + transform: translateY(0); 159 + } 160 + 100% { 161 + transform: translateY(100%); 162 + } 163 + } 164 + footer { 165 + display: inline-block; 166 + position: fixed; 167 + bottom: 0; 168 + left: 0; 169 + padding: .25rem; 170 + background: var(--lace); 171 + color: var(--raisin); 172 + border-top-right-radius: .25rem; 173 + animation: toast 5s; 174 + animation-delay: 2s; 175 + transform: translateY(100%); 176 + }
+365
zoodle.js
··· 1 + function Zoodle(scene, overlay, widgets) { 2 + this.scene = scene; 3 + this.overlay = overlay; 4 + this.widgets = widgets; 5 + 6 + this.selection = []; 7 + this.tool = "orbit"; 8 + this.tools = { 9 + orbit: new OrbitTool(this), 10 + translate: new TranslateTool(this), 11 + }; 12 + 13 + this.click = (ptr, target, x, y) => { 14 + this.select(target); 15 + }; 16 + 17 + this.select = (target) => { 18 + // If this is a compositeChild, find its parent 19 + while (target.compositeChild) { 20 + target = target.addTo; 21 + } 22 + 23 + // Don't highlight if target is the scene element 24 + if (!target || target.element === this.scene.element) { 25 + this.clearSelection(); 26 + return; 27 + } 28 + 29 + this.toggleSelection(target); 30 + }; 31 + 32 + this.toggleSelection = (target) => { 33 + const index = this.selection.indexOf(target); 34 + 35 + if (index !== -1) { 36 + this.selection.splice(index, 1); 37 + } else { 38 + this.selection.push(target); 39 + } 40 + 41 + this.updateSelection(); 42 + }; 43 + 44 + this.clearSelection = () => { 45 + this.selection = []; 46 + 47 + this.updateSelection(); 48 + }; 49 + 50 + this.updateSelection = () => { 51 + this.overlay.children = []; 52 + 53 + for (let i = 0; i < this.selection.length; i++) { 54 + let currentObj = this.selection[i]; 55 + 56 + let highlight = currentObj.copyGraph({ 57 + addTo: this.overlay, 58 + color: "#E62", 59 + backface: "#E62", 60 + }); 61 + 62 + for (let j = 0; j < highlight.flatGraph.length; j++) { 63 + highlight.flatGraph[j].color = "#E62"; 64 + highlight.flatGraph[j].backface = "#E62"; 65 + } 66 + 67 + Zdog.extend(highlight, this.getWorldTransforms(currentObj)); 68 + 69 + } 70 + 71 + this.syncLayers(); 72 + }; 73 + 74 + this.getWorldTransforms = (target) => { 75 + let translate = new Zdog.Vector(); 76 + let rotate = new Zdog.Vector(); 77 + let scale = new Zdog.Vector({x: 1, y: 1, z: 1}); 78 + 79 + let right = new Zdog.Vector({x: 1, y: 0, z: 0}); 80 + let down = new Zdog.Vector({x: 0, y: 1, z: 0}); 81 + let forward = new Zdog.Vector({x: 0, y: 0, z: 1}); 82 + 83 + // Condense transforms from all ancestors except the root. 84 + while (target.addTo) { 85 + translate.transform(target.translate, target.rotate, target.scale); 86 + forward.rotate(target.rotate); 87 + down.rotate(target.rotate); 88 + right.rotate(target.rotate); 89 + scale.multiply(target.scale); 90 + 91 + target = target.addTo; 92 + } 93 + 94 + if (Math.abs(forward.x) > 0.99999) { 95 + // Gimbal lock case 96 + rotate.x = 0; 97 + rotate.y = forward.x > 0 ? -Math.PI/2 : Math.PI/2; 98 + rotate.z = Math.atan2(right.y, down.y); 99 + } else { 100 + rotate.y = -Math.asin(forward.x); 101 + rotate.x = Math.atan2(-forward.y, forward.z); 102 + rotate.z = Math.atan2(-down.x, right.x); 103 + } 104 + return {translate: translate, rotate: rotate, scale: scale}; 105 + }; 106 + 107 + this.dragStart = (ptr, target, x, y) => { 108 + if (!target || target.element === this.scene.element) { 109 + this.tool = "orbit"; 110 + } 111 + 112 + this.tools[this.tool].start(ptr, target, x, y); 113 + }; 114 + 115 + this.dragMove = (ptr, target, x, y) => { 116 + this.tools[this.tool].move(ptr, target, x, y); 117 + }; 118 + 119 + this.dragEnd = (ptr, target, x, y) => { 120 + this.tools[this.tool].end(ptr, target, x, y); 121 + }; 122 + 123 + this.fetch = new Zfetch({ 124 + scene: this.scene, 125 + click: this.click, 126 + dragStart: this.dragStart, 127 + dragMove: this.dragMove, 128 + dragEnd: this.dragEnd, 129 + }); 130 + 131 + this.syncLayers = () => { 132 + scene = this.scene; 133 + targets = [this.overlay, this.widgets]; 134 + 135 + targets.forEach(target => { 136 + target.zoom = scene.zoom; 137 + target.scale = scene.scale; 138 + target.rotate = scene.rotate; 139 + target.translate = scene.translate; 140 + target.updateRenderGraph(); 141 + }); 142 + } 143 + 144 + this.syncLayers(); 145 + } 146 + 147 + class Tool { 148 + constructor(editor) { 149 + this.editor = editor; 150 + } 151 + start(ptr, target, x, y) {} 152 + move(ptr, target, x, y) {} 153 + end(ptr, target, x, y) {} 154 + } 155 + 156 + class OrbitTool extends Tool { 157 + start(ptr, target, x, y) { 158 + this.editor.fetch.rotateStart(ptr, target, x, y); 159 + } 160 + move(ptr, target, x, y) { 161 + this.editor.fetch.rotateMove(ptr, target, x, y); 162 + } 163 + } 164 + 165 + class TranslateTool extends Tool { 166 + constructor(editor) { 167 + super(editor); 168 + this.startPosition = new Zdog.Vector(); 169 + } 170 + start(ptr, target, x, y) { 171 + this.startPosition = this.editor.selected.translate.copy(); 172 + } 173 + move(ptr, target, x, y) { 174 + let delta = new Zdog.Vector({ 175 + x: x / this.editor.zoom, 176 + y: y / this.editor.zoom, 177 + }); 178 + this.editor.selected.translate = this.startPosition 179 + .copy() 180 + .add(delta) 181 + .rotate(this.editor.scene.rotate); 182 + } 183 + } 184 + 185 + const TAU = Zdog.TAU; 186 + 187 + const charcoal = "#333"; 188 + const raisin = "#534"; 189 + const plum = "#636"; 190 + const rose = "#C25"; 191 + const orange = "#E62"; 192 + const gold = "#EA0"; 193 + const lemon = "#ED0"; 194 + const peach = "#FDB"; 195 + const lace = "#FFF4E8"; 196 + const mint = "#CFD"; 197 + const lime = "#4A2"; 198 + const blueberry = "#359"; 199 + 200 + function init() { 201 + window.illoElem = document.getElementById("canvas"); 202 + window.widgetsElem = document.getElementById("widgets"); 203 + window.overlayElem = document.getElementById("overlay"); 204 + 205 + illoElem.setAttribute("width", illoElem.clientWidth); 206 + illoElem.setAttribute("height", illoElem.clientHeight); 207 + 208 + widgetsElem.setAttribute("width", illoElem.clientWidth); 209 + widgetsElem.setAttribute("height", illoElem.clientHeight); 210 + overlayElem.setAttribute("width", illoElem.clientWidth); 211 + overlayElem.setAttribute("height", illoElem.clientHeight); 212 + 213 + window.illo = new Zdog.Illustration({ 214 + element: illoElem, 215 + zoom: 10, 216 + rotate: { x: -Math.atan(1 / Math.sqrt(2)), y: TAU / 8 }, 217 + dragRotate: false, 218 + }); 219 + 220 + window.widgets = new Zdog.Illustration({ 221 + element: widgetsElem, 222 + zoom: 10, 223 + dragRotate: false, 224 + }); 225 + 226 + window.overlay = new Zdog.Illustration({ 227 + element: overlayElem, 228 + zoom: 10, 229 + dragRotate: false, 230 + }); 231 + 232 + let sun = new Zdog.Shape({ 233 + addTo: window.illo, 234 + stroke: 10, 235 + color: gold, 236 + }); 237 + 238 + new Zdog.Cone({ 239 + addTo: sun, 240 + diameter: 1.5, 241 + length: 1.5, 242 + stroke: 0.1, 243 + translate: { y: -7.5 }, 244 + rotate: { x: -TAU / 4 }, 245 + color: orange, 246 + }); 247 + 248 + /*window.horn = new Zdog.Horn({ 249 + addTo: illo, 250 + frontDiameter: 10, 251 + rearDiameter: 2, 252 + length: 20, 253 + fill: true, 254 + stroke: 0, 255 + color: '#C25', 256 + translate: { y: -9 }, 257 + });*/ 258 + 259 + new Zdog.Cylinder({ 260 + addTo: sun, 261 + color: orange, 262 + stroke: false, 263 + length: 1, 264 + diameter: 0.75, 265 + translate: { y: -8 }, 266 + rotate: { x: -TAU / 4 }, 267 + }); 268 + 269 + new Zdog.Shape({ 270 + addTo: sun, 271 + stroke: 1, 272 + translate: { x: 6 }, 273 + color: raisin, 274 + }); 275 + 276 + let orbit = new Zdog.Anchor({ 277 + addTo: sun, 278 + rotate: { x: TAU / 4 }, 279 + }); 280 + 281 + let orbitalProps = { 282 + stroke: 0.5, 283 + color: charcoal, 284 + closed: false, 285 + }; 286 + 287 + let quadrant = new Zdog.Shape({ 288 + addTo: orbit, 289 + path: [ 290 + { x: 5.8, y: -1.55 }, 291 + { 292 + bezier: [ 293 + { x: 5.1, y: -4.17 }, 294 + { x: 2.71, y: -6 }, 295 + { x: 0, y: -6 }, 296 + ] 297 + }, 298 + ], 299 + }); 300 + 301 + Zdog.extend(quadrant, orbitalProps); 302 + 303 + quadrant.copy({ 304 + scale: { y: -1 }, 305 + }); 306 + 307 + quadrant = new Zdog.Shape({ 308 + addTo: orbit, 309 + path: [ 310 + { x: 0, y: -6 }, 311 + { 312 + arc: [ 313 + { x: -6, y: -6 }, 314 + { x: -6, y: 0 }, 315 + ] 316 + }, 317 + ] 318 + }); 319 + 320 + Zdog.extend(quadrant, orbitalProps); 321 + 322 + quadrant.copy({ 323 + scale: { y: -1 }, 324 + }); 325 + 326 + new Zdog.Shape({ 327 + addTo: sun, 328 + stroke: 2, 329 + translate: { x: Math.cos(TAU/8)*8, z: Math.sin(TAU/8)*8 }, 330 + color: rose, 331 + }); 332 + 333 + orbit.copyGraph({ 334 + rotate: { x: TAU / 4, z: TAU / 8 }, 335 + scale: 8/6, 336 + }); 337 + 338 + new Zdog.Shape({ 339 + addTo: sun, 340 + stroke: 2, 341 + translate: { x: Math.cos(TAU/4)*10, z: Math.sin(TAU/4)*10 }, 342 + color: blueberry, 343 + }); 344 + 345 + orbit.copyGraph({ 346 + rotate: { x: TAU / 4, z: TAU / 4 }, 347 + scale: 10 / 6, 348 + }); 349 + 350 + window.zoodle = new Zoodle(illo, overlay, widgets); 351 + } 352 + 353 + // TODO: Only update when we have something to update. 354 + function update() { 355 + window.illo.updateRenderGraph(); 356 + window.widgets.updateRenderGraph(); 357 + window.overlay.updateRenderGraph(); 358 + window.illoElem.setAttribute("data-zdog", JSON.stringify(window.illo)); 359 + requestAnimationFrame(update); 360 + } 361 + 362 + init(); 363 + 364 + update(); 365 +