WIP WYSIWYG ~3D SVG editor.
0
fork

Configure Feed

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

Split zoodle.js into multiple files.

Starting to becoming a chore to scroll around everywhere.

+790 -784
+14
globals.js
··· 1 + const TAU = Zdog.TAU; 2 + 3 + const charcoal = "#333"; 4 + const raisin = "#534"; 5 + const plum = "#636"; 6 + const rose = "#C25"; 7 + const orange = "#E62"; 8 + const gold = "#EA0"; 9 + const lemon = "#ED0"; 10 + const peach = "#FDB"; 11 + const lace = "#FFF4E8"; 12 + const mint = "#CFD"; 13 + const lime = "#4A2"; 14 + const blueberry = "#359";
+4
index.html
··· 123 123 <footer> 124 124 Made with love, anime.js, and zdog by Jaromino. 125 125 </footer> 126 + <script src="globals.js"></script> 127 + <script src="presets.js"></script> 128 + <script src="tools.js"></script> 129 + <script src="properties.js"></script> 126 130 <script src="zoodle.js"></script> 127 131 <script src="input-enhancements.js"></script> 128 132 </body>
+109
presets.js
··· 1 + class Preset { 2 + load(illo) {} 3 + } 4 + 5 + class Solar extends Preset { 6 + load(illo) { 7 + let sun = new Zdog.Shape({ 8 + addTo: illo, 9 + stroke: 10, 10 + color: gold, 11 + }); 12 + 13 + let arrow = new Zdog.Cone({ 14 + addTo: sun, 15 + diameter: 1.5, 16 + length: 1.5, 17 + stroke: false, 18 + translate: { y: -7.5 }, 19 + rotate: { x: -TAU / 4 }, 20 + color: orange, 21 + }); 22 + 23 + new Zdog.Cylinder({ 24 + addTo: arrow, 25 + color: orange, 26 + stroke: false, 27 + length: 1, 28 + diameter: 0.75, 29 + translate: { z: -.5 }, 30 + }); 31 + 32 + new Zdog.Shape({ 33 + addTo: sun, 34 + stroke: 1, 35 + translate: { x: 6 }, 36 + color: raisin, 37 + }); 38 + 39 + let orbit = new Zdog.Anchor({ 40 + addTo: sun, 41 + rotate: { x: TAU / 4 }, 42 + }); 43 + 44 + let orbitalProps = { 45 + stroke: 0.5, 46 + color: charcoal, 47 + closed: false, 48 + }; 49 + 50 + let quadrant = new Zdog.Shape({ 51 + addTo: orbit, 52 + path: [ 53 + { x: 5.8, y: -1.55 }, 54 + { bezier: [ 55 + { x: 5.1, y: -4.17 }, 56 + { x: 2.71, y: -6 }, 57 + { x: 0, y: -6 }, 58 + ]}, 59 + ], 60 + }); 61 + 62 + Zdog.extend(quadrant, orbitalProps); 63 + 64 + quadrant.copy({ 65 + scale: { y: -1 }, 66 + }); 67 + 68 + quadrant = new Zdog.Shape({ 69 + addTo: orbit, 70 + path: [ 71 + { x: 0, y: -6 }, 72 + { arc: [ 73 + { x: -6, y: -6 }, 74 + { x: -6, y: 0 }, 75 + ]}, 76 + ] 77 + }); 78 + 79 + Zdog.extend(quadrant, orbitalProps); 80 + 81 + quadrant.copy({ 82 + scale: { y: -1 }, 83 + }); 84 + 85 + let middleOrbit = orbit.copyGraph({ 86 + rotate: { x: TAU / 5, z: TAU / 8 }, 87 + scale: 8/6, 88 + }); 89 + 90 + new Zdog.Shape({ 91 + addTo: middleOrbit, 92 + stroke: 2, 93 + translate: { x: 6 }, 94 + color: rose, 95 + }); 96 + 97 + let outerOrbit = orbit.copyGraph({ 98 + rotate: { x: TAU / 3, z: TAU / 4 }, 99 + scale: 10 / 6, 100 + }); 101 + 102 + new Zdog.Shape({ 103 + addTo: outerOrbit, 104 + stroke: 2, 105 + translate: { x: 6 }, 106 + color: blueberry, 107 + }); 108 + } 109 + }
+229
properties.js
··· 1 + class Properties { 2 + constructor(panel, editor) { 3 + this.panel = panel; 4 + this.editor = editor; 5 + this.header = this.panel.querySelector("h2"); 6 + this.panel.addEventListener("input", this.handleInput.bind(this)); 7 + this.panel.addEventListener("change", this.handleChange.bind(this)); 8 + this.command = null; 9 + } 10 + 11 + handleInput(event) { 12 + const propElem = event.target.closest(".prop"); 13 + if (!propElem) return; 14 + 15 + console.log("modifying", propElem.id); 16 + 17 + const oldValue = this.readProperty(propElem.id); 18 + if (Array.isArray(oldValue)) { 19 + return; 20 + } 21 + const newValue = this.readPanel(propElem); 22 + // start a new edit command 23 + if (!this.command) { 24 + this.command = new EditCommand(this.editor, this.editor.selection.slice(0), propElem.id, newValue, [oldValue]); 25 + } else { 26 + this.command.value = newValue; 27 + } 28 + this.writeProperty(propElem, newValue); 29 + this.editor.updateUI(); 30 + this.editor.updateHighlights(); 31 + } 32 + 33 + handleChange(event) { 34 + // as far as I know, change always fires after input. 35 + this.editor.did(this.command); 36 + this.command = null; 37 + } 38 + 39 + // Synchronize the properties panel with the selected objects 40 + updatePanel() { 41 + this.updateHeader(); 42 + this.updateBody(); 43 + } 44 + 45 + updateHeader() { 46 + const selected = this.editor.selection; 47 + if (selected.length === 0) { 48 + this.header.textContent = "No objects selected"; 49 + } else if (selected.length > 1) { 50 + this.header.textContent = `${selected.length} objects selected`; 51 + } else { 52 + let breadcrumbs = []; 53 + let target = selected[0]; 54 + for (let offset = 0; target.addTo; offset++) { 55 + breadcrumbs.unshift(`<span class="breadcrumb" data-parent-index="${offset}">${target.constructor.type}</span>`); 56 + target = target.addTo; 57 + } 58 + this.header.innerHTML = breadcrumbs.join(" / "); 59 + } 60 + } 61 + 62 + updateBody() { 63 + // Shortcut: Just hide all entries if nothing's selected. 64 + if (this.editor.selection.length === 0) { 65 + this.panel.classList.add("hidden"); 66 + return; 67 + } 68 + this.panel.classList.remove("hidden"); 69 + 70 + // Set properties from first selected object. 71 + this.updatePanelProps(this.editor.selection[0], true); 72 + 73 + // Merge in properties from the rest of the selected objects. 74 + for (let i = 1; i < this.editor.selection.length; i++) { 75 + this.updatePanelProps(this.editor.selection[i]); 76 + } 77 + } 78 + 79 + // Hides properties in the panel that are incompatible with the given object. 80 + // If overwrite is true, it updates the values in the panel from the object. 81 + // If overwrite is false, it hides properties whose values don't match the object. 82 + updatePanelProps(srcObj, overwrite = false) { 83 + let srcProps = srcObj.constructor.optionKeys; 84 + let destProps = this.panel.querySelectorAll(".prop"); 85 + destProps.forEach( (propElem) => { 86 + if (!srcProps.includes(propElem.id)) { 87 + propElem.classList.add("hidden"); 88 + return; 89 + } 90 + propElem.classList.remove("hidden"); 91 + 92 + if (overwrite) { 93 + this.updatePanelProp(srcObj, propElem); 94 + return; 95 + } 96 + 97 + const objValue = srcObj[propElem.id]; 98 + const panelValue = this.readPanel(propElem); 99 + // TODO: This won't work for vector types. 100 + if (panelValue != objValue) { 101 + propElem.classList.add("hidden"); 102 + } 103 + }); 104 + } 105 + 106 + // Writes a single property from the object to the properties panel. 107 + updatePanelProp(srcObj, propElem) { 108 + const type = this.readType(propElem); 109 + 110 + if (this.isOptional(propElem)) { 111 + document.getElementById(`${propElem.id}-enabled`).enabled = srcObj[propElem.id] !== false; 112 + } 113 + 114 + const input = document.getElementById(`${propElem.id}-value`); 115 + 116 + switch (type) { 117 + case "bool": 118 + input.checked = srcObj[propElem.id] != false; 119 + break; 120 + case "number": 121 + input.valueAsNumber = srcObj[propElem.id]; 122 + break; 123 + case "color": 124 + input.value = this.normalizeColor(srcObj[propElem.id]); 125 + break; 126 + case "vector": 127 + let {x, y, z} = srcObj[propElem.id]; 128 + document.getElementById(`${propElem.id}-x`).value = x; 129 + document.getElementById(`${propElem.id}-y`).value = y; 130 + document.getElementById(`${propElem.id}-z`).value = z; 131 + break; 132 + default: 133 + console.warn(`Unknown property type: ${type}`); 134 + } 135 + } 136 + 137 + normalizeColor(color) { 138 + if (/^#[0-9a-fA-F]{6}$/.test(color)) { 139 + return color; 140 + } 141 + 142 + if (/^#[0-9a-fA-F]{3}$/.test(color)) { 143 + return `#${color[1]}${color[1]}${color[2]}${color[2]}${color[3]}${color[3]}`; 144 + } 145 + 146 + return "#000000"; 147 + } 148 + 149 + // Read a property from the selected objects. 150 + // Returns an array if the property differs across the selection. 151 + readProperty(propId) { 152 + if (!this.editor.selection.length) { 153 + return null; 154 + } 155 + 156 + if (this.editor.selection.length === 1) { 157 + return this.editor.selection[0][propId]; 158 + } 159 + 160 + let values = this.editor.selection.map( (target) => target[propId]); 161 + let allEqual = values.every( (val) => val === values[0]); 162 + if (allEqual) { 163 + return values[0]; 164 + } 165 + return values; 166 + } 167 + 168 + // Write property to the selected objects 169 + // If value is null, it reads the value from the properties panel. 170 + writeProperty(propElem, value = null) { 171 + if (value === null) value = this.readPanel(propElem); 172 + let targets = this.editor.selection; 173 + targets.forEach( (target) => { 174 + target[propElem.id] = value; 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(); 177 + }); 178 + } 179 + 180 + // Get the type of a property from the properties panel 181 + readType(propElem) { 182 + let types = ["vector", "number", "color", "bool"]; 183 + for (let i = 0; i < types.length; i++) { 184 + if (propElem.classList.contains(types[i])) { 185 + return types[i]; 186 + } 187 + } 188 + return null; 189 + } 190 + 191 + isOptional(propElem) { 192 + return propElem.classList.contains("optional"); 193 + } 194 + 195 + isEnabled(propElem) { 196 + if (!this.isOptional(propElem)) { 197 + return true; 198 + } 199 + return document.getElementById(propElem.id + "-enabled").checked; 200 + } 201 + 202 + // Read the value of a property from the properties panel 203 + readPanel(propElem) { 204 + const type = this.readType(propElem); 205 + 206 + if (!this.isEnabled(propElem)) { 207 + return false; 208 + } 209 + 210 + const input = document.getElementById(`${propElem.id}-value`); 211 + 212 + switch (type) { 213 + case "bool": 214 + return input.checked; 215 + case "number": 216 + return input.valueAsNumber; 217 + case "color": 218 + return input.value; 219 + case "vector": 220 + return new Zdog.Vector({ 221 + x: document.getElementById(`${propElem.id}-x`).valueAsNumber, 222 + y: document.getElementById(`${propElem.id}-y`).valueAsNumber, 223 + z: document.getElementById(`${propElem.id}-z`).valueAsNumber, 224 + }); 225 + default: 226 + return null; 227 + } 228 + } 229 + }
+3 -1
styles.css
··· 210 210 flex-grow: 1; 211 211 } 212 212 213 - #properties.hidden .prop-group, #properties .hidden { 213 + #properties.hidden .prop-group, 214 + #properties .prop-group:not(:has(:not(.hidden))), 215 + #properties .hidden { 214 216 display: none; 215 217 } 216 218
+429
tools.js
··· 1 + class Tool { 2 + constructor(editor) { 3 + this.editor = editor; 4 + } 5 + start(ptr, target, x, y) {} 6 + move(ptr, target, x, y) {} 7 + end(ptr, target, x, y) {} 8 + drawWidget(targets) {} 9 + } 10 + 11 + // this tool performs the actions of another tool without hiding the widgets of the original 12 + class TemporaryTool extends Tool { 13 + constructor(editor, style, substance, autoRestore = true) { 14 + super(editor); 15 + this.style = style; 16 + this.substance = substance; 17 + this.autoRestore = autoRestore; 18 + } 19 + start(ptr, target, x, y) { 20 + this.substance.start(ptr, target, x, y); 21 + } 22 + move(ptr, target, x, y) { 23 + this.substance.move(ptr, target, x, y); 24 + } 25 + end(ptr, target, x, y) { 26 + this.substance.end(ptr, target, x, y); 27 + if (this.autoRestore) { 28 + this.editor.tool = this.style; 29 + } 30 + } 31 + drawWidget(targets) { 32 + this.style.drawWidget(targets); 33 + } 34 + } 35 + 36 + class OrbitTool extends Tool { 37 + constructor(editor) { 38 + super(editor); 39 + this.rotateStart = null; 40 + } 41 + start(ptr, target, x, y) { 42 + this.rotateStart = this.editor.scene.rotate.copy(); 43 + } 44 + move(ptr, target, x, y) { 45 + let displaySize = Math.min( this.editor.scene.width, this.editor.scene.height ); 46 + let moveRY = x / displaySize * Math.PI * Zdog.TAU; 47 + let moveRX = y / displaySize * Math.PI * Zdog.TAU; 48 + this.editor.scene.rotate.x = this.rotateStart.x - moveRX; 49 + this.editor.scene.rotate.y = this.rotateStart.y - moveRY; 50 + 51 + this.editor.syncLayers(); 52 + } 53 + } 54 + 55 + class TranslateTool extends Tool { 56 + constructor(editor) { 57 + super(editor); 58 + this.targets = null; 59 + this.startTranslate = null; 60 + this.mode = TranslateTool.MODE_NONE; 61 + this.widget = null; 62 + } 63 + start(ptr, target, x, y) { 64 + this.widget = target; 65 + this.targets = this.editor.selection.slice(0); 66 + this.mode = TranslateTool.MODE_NONE; 67 + if (!this.targets.length || target.layer !== Zoodle.LAYER_UI || !target.color) { 68 + return; 69 + } 70 + // Ensure our widget target is the base and not the tip. 71 + if (this.widget.diameter) 72 + this.widget = this.widget.addTo; 73 + this.startTranslate = this.targets.map((t) => t.translate.copy()); 74 + switch (target.color) { 75 + case rose: 76 + this.mode = TranslateTool.MODE_X; 77 + break; 78 + case lime: 79 + this.mode = TranslateTool.MODE_Y; 80 + break; 81 + case blueberry: 82 + this.mode = TranslateTool.MODE_Z; 83 + break; 84 + default: 85 + this.mode = TranslateTool.MODE_NONE; 86 + break; 87 + } 88 + } 89 + move(ptr, target, x, y) { 90 + if (!this.mode) { return; } 91 + let direction = this.widget.renderNormal; // TODO: Break out into a function. 92 + let delta = this.editor.getAxisDistance(x, y, Math.atan2(direction.y, direction.x)); 93 + delta /= -this.editor.scene.zoom; // TODO: Include pixel ratio as well. 94 + delta *= this.widget.addTo.addTo.scale.x; 95 + delta *= Math.abs(this.widget.renderNormal.magnitude2d()); 96 + // TODO: Oh I know where this is going wrong, we're not including how the view rotation is going to shorten the distance. 97 + // We need some sines or cosines or something or both in here. 98 + this.targets.forEach((t, i) => { 99 + t.translate[this.mode] = this.startTranslate[i][this.mode] + delta; 100 + }); 101 + this.editor.updateHighlights(); 102 + this.editor.updateUI(); 103 + this.editor.props.updatePanel(); 104 + } 105 + end(ptr, target, x, y) { 106 + if (!this.mode) { return; } 107 + // ensure any final adjustments are applied. 108 + this.move(ptr, target, x, y); 109 + let direction = this.widget.renderNormal; // TODO: Break out into a function. 110 + let delta = this.editor.getAxisDistance(x, y, Math.atan2(direction.y, direction.x)); 111 + delta /= -this.editor.scene.zoom; 112 + delta *= this.widget.addTo.addTo.scale.x; 113 + delta /= Math.abs(this.widget.renderNormal.magnitude2d()); 114 + let command = new TranslateCommand(this.editor, this.targets, new Zdog.Vector({[this.mode]: delta}), this.startTranslate); 115 + this.editor.did(command); 116 + } 117 + drawWidget(targets) { 118 + // Create anchors matching selected objects. 119 + targets = targets.map((target) => { 120 + // TODO: Double check this is correct. 121 + let parentTransforms = this.editor.getWorldTransforms(target.addTo); 122 + let childTranslate = target.translate.copy().rotate(parentTransforms.rotate); 123 + parentTransforms.translate.add(childTranslate); 124 + parentTransforms.translate.multiply(parentTransforms.scale); 125 + return new Zdog.Anchor({ 126 + addTo: this.editor.ui, 127 + ...parentTransforms, 128 + }); 129 + }); 130 + 131 + let origin = new Zdog.Shape({ 132 + stroke: .5, 133 + color: lace, 134 + }); 135 + let base = new Zdog.Shape({ 136 + path: [ { z: -1.5 }, { z: 1.5 } ], 137 + stroke: 1, 138 + translate: { z: 3 }, 139 + }); 140 + new Zdog.Cone({ 141 + addTo: base, 142 + diameter: 2, 143 + length: 1.5, 144 + stroke: .5, 145 + translate: { z: 1.5 }, 146 + }); 147 + let z = base.copyGraph({ 148 + color: blueberry, 149 + }); 150 + z.children[0].color = blueberry; 151 + let y = base.copyGraph({ 152 + color: lime, 153 + rotate: { x: -TAU/4 }, 154 + translate: { y: 3 }, 155 + }); 156 + y.children[0].color = lime; 157 + let x = base.copyGraph({ 158 + color: rose, 159 + rotate: { y: -TAU/4 }, 160 + translate: { x: 3 }, 161 + }); 162 + x.children[0].color = rose; 163 + targets.forEach(t => { 164 + origin.copyGraph({ addTo: t, scale: 1/t.scale.x }); 165 + z.copyGraph({ 166 + addTo: t, 167 + scale: 1/t.scale.x, 168 + translate: { z: 3/t.scale.x }, 169 + }); 170 + y.copyGraph({ 171 + addTo: t, 172 + scale: 1/t.scale.x, 173 + translate: { y: 3/t.scale.x }, 174 + }); 175 + x.copyGraph({ 176 + addTo: t, 177 + scale: 1/t.scale.x, 178 + translate: { x: 3/t.scale.x }, 179 + }); 180 + }); 181 + } 182 + 183 + static get MODE_NONE() { return ''; } 184 + static get MODE_X() { return 'x'; } 185 + static get MODE_Y() { return 'y'; } 186 + static get MODE_Z() { return 'z'; } 187 + static get MODE_VIEW() { return 'v'; } 188 + } 189 + 190 + // TODO: I think this is going to need to run on basis vectors like getWorldTransforms. 191 + class RotateTool extends Tool { 192 + constructor(editor) { 193 + super(editor); 194 + this.targets = null; 195 + this.startRotate = null; 196 + this.mode = RotateTool.MODE_NONE; 197 + this.widget = null; 198 + } 199 + 200 + start(ptr, target, x, y) { 201 + this.widget = target; 202 + this.targets = this.editor.selection.slice(0); 203 + this.mode = RotateTool.MODE_NONE; 204 + if (!this.targets.length || target.layer !== Zoodle.LAYER_UI || !target.color) { 205 + return; 206 + } 207 + 208 + this.startRotate = this.targets.map( t => t.rotate.copy() ); 209 + 210 + switch (target.color) { 211 + case rose: 212 + this.mode = RotateTool.MODE_X; 213 + break; 214 + case lime: 215 + this.mode = RotateTool.MODE_Y; 216 + break; 217 + case blueberry: 218 + this.mode = RotateTool.MODE_Z; 219 + break; 220 + } 221 + } 222 + move( ptr, target, x, y ) { 223 + if (!this.mode) { return; } 224 + 225 + let displaySize = Math.min( this.editor.scene.width, this.editor.scene.height ); 226 + x /= displaySize / Math.PI * Zdog.TAU; 227 + y /= displaySize / Math.PI * Zdog.TAU; 228 + let direction = this.widget.renderNormal; 229 + let delta = this.editor.getAxisDistance(x, y, Math.atan2(direction.y, direction.x) + TAU/4 ); 230 + this.targets.forEach((t, i) => { 231 + t.rotate[this.mode] = this.startRotate[i][this.mode] + delta; 232 + }); 233 + this.editor.updateHighlights(); 234 + this.editor.updateUI(); 235 + this.editor.props.updatePanel(); 236 + } 237 + // TODO: Let's stash `delta` somewhere so we don't have to recalculate it in end() 238 + end( ptr, target, x, y ) { 239 + if (!this.mode) { return; } 240 + // ensure any final adjustments are applied. 241 + this.move( ptr, target, x, y ); 242 + let displaySize = Math.min( this.editor.scene.width, this.editor.scene.height ); 243 + x /= displaySize * Math.PI * Zdog.TAU; 244 + y /= displaySize * Math.PI * Zdog.TAU; 245 + let direction = this.widget.renderNormal; 246 + let delta = this.editor.getAxisDistance( x, y, Math.atan2(direction.y, direction.x) + TAU/4 ); 247 + let command = new RotateCommand(this.editor, this.targets, new Zdog.Vector({[this.mode]: delta})); 248 + this.editor.did(command); 249 + } 250 + drawWidget(targets) { 251 + // Create anchors matching selected objects. 252 + targets = targets.map((target) => { 253 + return new Zdog.Anchor({ 254 + addTo: this.editor.ui, 255 + ...this.editor.getWorldTransforms(target), 256 + }); 257 + }); 258 + 259 + const widgetDiameter = 10; 260 + const widgetStroke = 0.75; 261 + 262 + let origin = new Zdog.Shape({ 263 + stroke: .5, 264 + color: lace, 265 + }); 266 + let zRing = new Zdog.Ellipse({ 267 + diameter: widgetDiameter, 268 + stroke: widgetStroke, 269 + color: blueberry, 270 + }); 271 + let yRing = zRing.copyGraph({ 272 + rotate: { x: TAU/4 }, 273 + color: lime, 274 + }); 275 + let xRing = zRing.copyGraph({ 276 + rotate: { y: TAU/4 }, 277 + color: rose, 278 + }); 279 + targets.forEach(t => { 280 + origin.copyGraph({ addTo: t, scale: 1/t.scale.x }); 281 + zRing.copyGraph({ addTo: t, scale: 1/t.scale.x }); 282 + yRing.copyGraph({ addTo: t, scale: 1/t.scale.x }); 283 + xRing.copyGraph({ addTo: t, scale: 1/t.scale.x }); 284 + }); 285 + } 286 + 287 + static get MODE_NONE() { return ''; } 288 + static get MODE_X() { return 'x'; } 289 + static get MODE_Y() { return 'y'; } 290 + static get MODE_Z() { return 'z'; } 291 + } 292 + 293 + class Command { 294 + constructor(editor) { 295 + this.editor = editor; 296 + } 297 + do() {} 298 + undo() {} 299 + } 300 + 301 + class SelectCommand extends Command { 302 + constructor(editor, target, replace = false) { 303 + super(editor); 304 + this.replace = replace; 305 + this.oldSelection = null; 306 + 307 + // If this is a compositeChild, find its parent 308 + while (target && target.compositeChild) { 309 + target = target.addTo; 310 + } 311 + // Don't select root elements. 312 + if (target && !target.addTo) { 313 + target = null; 314 + } 315 + this.target = target; 316 + } 317 + do() { 318 + this.oldSelection = this.editor.selection.slice( 0 ); 319 + if (!this.target) { 320 + this.editor.clearSelection(); 321 + } else if (this.replace) { 322 + this.editor.setSelection(this.target); 323 + } else { 324 + this.editor.toggleSelection(this.target); 325 + } 326 + this.refresh(); 327 + } 328 + undo() { 329 + this.editor.selection = this.oldSelection; 330 + this.refresh(); 331 + } 332 + refresh() { 333 + this.editor.updateHighlights(); 334 + this.editor.updateUI(); 335 + this.editor.props.updatePanel(); 336 + } 337 + } 338 + 339 + class TranslateCommand extends Command { 340 + constructor(editor, target, delta, oldTranslate = null) { 341 + super(editor); 342 + if (!Array.isArray(target)) { 343 + target = [target]; 344 + } 345 + this.target = target; 346 + this.delta = delta; 347 + this.oldTranslate = oldTranslate || target.map((t) => t.translate.copy()); 348 + } 349 + 350 + do() { 351 + if (!this.target) return console.error("Doing TranslateCommand with no target."); 352 + 353 + this.target.forEach((t, i) => { 354 + t.translate.set(this.oldTranslate[i]).add(this.delta); 355 + }); 356 + this.refresh(); 357 + } 358 + undo() { 359 + if (!this.target) return console.error("Undoing TranslateCommand with no target."); 360 + 361 + this.target.forEach((t, i) => { 362 + t.translate.set(this.oldTranslate[i]); 363 + }); 364 + this.refresh(); 365 + } 366 + refresh() { 367 + this.editor.updateHighlights(); 368 + this.editor.updateUI(); 369 + this.editor.props.updatePanel(); 370 + } 371 + } 372 + 373 + class RotateCommand extends Command { 374 + // TODO: Add oldTranslate and oldRotate to the constructor, or maybe a flag to tell it if it's getting new or old transforms. 375 + constructor(editor, target, delta) { 376 + super(editor); 377 + if (!Array.isArray(target)) { 378 + target = [target]; 379 + } 380 + this.target = target; 381 + this.delta = delta; 382 + this.oldRotate = target.map((t) => t.rotate.copy()); 383 + } 384 + 385 + do() { 386 + // TODO: Probably better to just throw in the constructor. 387 + if (!this.target) return console.error("Doing RotateCommand with no target."); 388 + 389 + this.target.forEach((t, i) => { 390 + t.rotate.set(this.oldRotate[i]).add(this.delta); 391 + }); 392 + } 393 + undo() { 394 + if (!this.target) return console.error("Undoing RotateCommand with no target."); 395 + 396 + this.target.forEach((t, i) => { 397 + t.rotate.set(this.oldRotate[i]); 398 + }); 399 + } 400 + } 401 + 402 + class EditCommand extends Command { 403 + constructor(editor, target, propId, value, oldValue = null) { 404 + super(editor); 405 + if (!target) { 406 + throw new Error("No target specified for EditCommand"); 407 + } 408 + 409 + if (!Array.isArray(target)) { 410 + target = [target]; 411 + } 412 + this.target = target; 413 + this.propId = propId; 414 + this.value = value; 415 + this.oldValue = oldValue || target.map( (t) => t[propId]); 416 + } 417 + do() { 418 + this.target.forEach( (t) => { 419 + t[this.propId] = this.value; 420 + if (t.updatePath) t.updatePath(); 421 + }); 422 + } 423 + undo() { 424 + this.target.forEach( (t, i) => { 425 + t[this.propId] = this.oldValue[i]; 426 + if (t.updatePath) t.updatePath(); 427 + }); 428 + } 429 + }
+2 -783
zoodle.js
··· 1 1 class Zoodle { 2 2 constructor(sceneElem, overlayElem, uiElem, panelElem) { 3 3 // TODO: Calculate appropriate zooms and sizes 4 - let zoom = 10; 5 - let rotate = { x: -Math.atan(1 / Math.sqrt(2)), y: TAU / 8 }; 4 + let zoom = 30; 5 + let rotate = { x: -Math.atan(1 / Math.sqrt(2)), y: Zdog.TAU / 8 }; 6 6 7 7 this.scene = new Zdog.Illustration({ 8 8 element: sceneElem, ··· 260 260 static get LAYER_UI() { return 3; } 261 261 } 262 262 263 - class Tool { 264 - constructor(editor) { 265 - this.editor = editor; 266 - } 267 - start(ptr, target, x, y) {} 268 - move(ptr, target, x, y) {} 269 - end(ptr, target, x, y) {} 270 - drawWidget(targets) {} 271 - } 272 - 273 - // this tool performs the actions of another tool without hiding the widgets of the original 274 - class TemporaryTool extends Tool { 275 - constructor(editor, style, substance, autoRestore = true) { 276 - super(editor); 277 - this.style = style; 278 - this.substance = substance; 279 - this.autoRestore = autoRestore; 280 - } 281 - start(ptr, target, x, y) { 282 - this.substance.start(ptr, target, x, y); 283 - } 284 - move(ptr, target, x, y) { 285 - this.substance.move(ptr, target, x, y); 286 - } 287 - end(ptr, target, x, y) { 288 - this.substance.end(ptr, target, x, y); 289 - if (this.autoRestore) { 290 - this.editor.tool = this.style; 291 - } 292 - } 293 - drawWidget(targets) { 294 - this.style.drawWidget(targets); 295 - } 296 - } 297 - 298 - class OrbitTool extends Tool { 299 - constructor(editor) { 300 - super(editor); 301 - this.rotateStart = null; 302 - } 303 - start(ptr, target, x, y) { 304 - this.rotateStart = this.editor.scene.rotate.copy(); 305 - } 306 - move(ptr, target, x, y) { 307 - let displaySize = Math.min( this.editor.scene.width, this.editor.scene.height ); 308 - let moveRY = x / displaySize * Math.PI * Zdog.TAU; 309 - let moveRX = y / displaySize * Math.PI * Zdog.TAU; 310 - this.editor.scene.rotate.x = this.rotateStart.x - moveRX; 311 - this.editor.scene.rotate.y = this.rotateStart.y - moveRY; 312 - 313 - this.editor.syncLayers(); 314 - } 315 - } 316 - 317 - class TranslateTool extends Tool { 318 - constructor(editor) { 319 - super(editor); 320 - this.targets = null; 321 - this.startTranslate = null; 322 - this.mode = TranslateTool.MODE_NONE; 323 - this.widget = null; 324 - } 325 - start(ptr, target, x, y) { 326 - this.widget = target; 327 - this.targets = this.editor.selection.slice(0); 328 - this.mode = TranslateTool.MODE_NONE; 329 - if (!this.targets.length || target.layer !== Zoodle.LAYER_UI || !target.color) { 330 - return; 331 - } 332 - // Ensure our widget target is the base and not the tip. 333 - if (this.widget.diameter) 334 - this.widget = this.widget.addTo; 335 - this.startTranslate = this.targets.map((t) => t.translate.copy()); 336 - switch (target.color) { 337 - case rose: 338 - this.mode = TranslateTool.MODE_X; 339 - break; 340 - case lime: 341 - this.mode = TranslateTool.MODE_Y; 342 - break; 343 - case blueberry: 344 - this.mode = TranslateTool.MODE_Z; 345 - break; 346 - default: 347 - this.mode = TranslateTool.MODE_NONE; 348 - break; 349 - } 350 - } 351 - move(ptr, target, x, y) { 352 - if (!this.mode) { return; } 353 - let direction = this.widget.renderNormal; // TODO: Break out into a function. 354 - let delta = this.editor.getAxisDistance(x, y, Math.atan2(direction.y, direction.x)); 355 - delta /= -this.editor.scene.zoom; // TODO: Include pixel ratio as well. 356 - delta *= this.widget.scale.x; 357 - this.targets.forEach((t, i) => { 358 - t.translate[this.mode] = this.startTranslate[i][this.mode] + delta; 359 - }); 360 - this.editor.updateHighlights(); 361 - this.editor.updateUI(); 362 - this.editor.props.updatePanel(); 363 - } 364 - end(ptr, target, x, y) { 365 - if (!this.mode) { return; } 366 - // ensure any final adjustments are applied. 367 - this.move(ptr, target, x, y); 368 - let direction = this.widget.renderNormal; // TODO: Break out into a function. 369 - let delta = this.editor.getAxisDistance(x, y, Math.atan2(direction.y, direction.x)); 370 - delta /= -this.editor.scene.zoom; 371 - delta *= this.widget.scale.x; 372 - let command = new TranslateCommand(this.editor, this.targets, new Zdog.Vector({[this.mode]: delta}), this.startTranslate); 373 - this.editor.did(command); 374 - } 375 - drawWidget(targets) { 376 - // Create anchors matching selected objects. 377 - targets = targets.map((target) => { 378 - // TODO: Double check this is correct. 379 - let parentTransforms = this.editor.getWorldTransforms(target.addTo); 380 - let childTranslate = target.translate.copy().rotate(parentTransforms.rotate); 381 - parentTransforms.translate.add(childTranslate); 382 - parentTransforms.translate.multiply(parentTransforms.scale); 383 - return new Zdog.Anchor({ 384 - addTo: this.editor.ui, 385 - ...parentTransforms, 386 - }); 387 - }); 388 - 389 - let origin = new Zdog.Shape({ 390 - stroke: .5, 391 - color: lace, 392 - }); 393 - let base = new Zdog.Shape({ 394 - path: [ { z: -1.5 }, { z: 1.5 } ], 395 - stroke: 1, 396 - translate: { z: 3 }, 397 - }); 398 - new Zdog.Cone({ 399 - addTo: base, 400 - diameter: 2, 401 - length: 1.5, 402 - stroke: .5, 403 - translate: { z: 1.5 }, 404 - }); 405 - let z = base.copyGraph({ 406 - color: blueberry, 407 - }); 408 - z.children[0].color = blueberry; 409 - let y = base.copyGraph({ 410 - color: lime, 411 - rotate: { x: -TAU/4 }, 412 - translate: { y: 3 }, 413 - }); 414 - y.children[0].color = lime; 415 - let x = base.copyGraph({ 416 - color: rose, 417 - rotate: { y: -TAU/4 }, 418 - translate: { x: 3 }, 419 - }); 420 - x.children[0].color = rose; 421 - targets.forEach(t => { 422 - origin.copyGraph({ addTo: t, scale: 1/t.scale.x }); 423 - z.copyGraph({ 424 - addTo: t, 425 - scale: 1/t.scale.x, 426 - translate: { z: 3/t.scale.x }, 427 - }); 428 - y.copyGraph({ 429 - addTo: t, 430 - scale: 1/t.scale.x, 431 - translate: { y: 3/t.scale.x }, 432 - }); 433 - x.copyGraph({ 434 - addTo: t, 435 - scale: 1/t.scale.x, 436 - translate: { x: 3/t.scale.x }, 437 - }); 438 - }); 439 - } 440 - 441 - static get MODE_NONE() { return ''; } 442 - static get MODE_X() { return 'x'; } 443 - static get MODE_Y() { return 'y'; } 444 - static get MODE_Z() { return 'z'; } 445 - static get MODE_VIEW() { return 'v'; } 446 - } 447 - 448 - // TODO: I think this is going to need to run on basis vectors like getWorldTransforms. 449 - class RotateTool extends Tool { 450 - constructor(editor) { 451 - super(editor); 452 - this.targets = null; 453 - this.startRotate = null; 454 - this.mode = RotateTool.MODE_NONE; 455 - this.widget = null; 456 - } 457 - 458 - start(ptr, target, x, y) { 459 - this.widget = target; 460 - this.targets = this.editor.selection.slice(0); 461 - this.mode = RotateTool.MODE_NONE; 462 - if (!this.targets.length || target.layer !== Zoodle.LAYER_UI || !target.color) { 463 - return; 464 - } 465 - 466 - this.startRotate = this.targets.map( t => t.rotate.copy() ); 467 - 468 - switch (target.color) { 469 - case rose: 470 - this.mode = RotateTool.MODE_X; 471 - break; 472 - case lime: 473 - this.mode = RotateTool.MODE_Y; 474 - break; 475 - case blueberry: 476 - this.mode = RotateTool.MODE_Z; 477 - break; 478 - } 479 - } 480 - move( ptr, target, x, y ) { 481 - if (!this.mode) { return; } 482 - 483 - let displaySize = Math.min( this.editor.scene.width, this.editor.scene.height ); 484 - x /= displaySize / Math.PI * Zdog.TAU; 485 - y /= displaySize / Math.PI * Zdog.TAU; 486 - let direction = this.widget.renderNormal; 487 - let delta = this.editor.getAxisDistance(x, y, Math.atan2(direction.y, direction.x) + TAU/4 ); 488 - this.targets.forEach((t, i) => { 489 - t.rotate[this.mode] = this.startRotate[i][this.mode] + delta; 490 - }); 491 - this.editor.updateHighlights(); 492 - this.editor.updateUI(); 493 - this.editor.props.updatePanel(); 494 - } 495 - // TODO: Let's stash `delta` somewhere so we don't have to recalculate it in end() 496 - end( ptr, target, x, y ) { 497 - if (!this.mode) { return; } 498 - // ensure any final adjustments are applied. 499 - this.move( ptr, target, x, y ); 500 - let displaySize = Math.min( this.editor.scene.width, this.editor.scene.height ); 501 - x /= displaySize * Math.PI * Zdog.TAU; 502 - y /= displaySize * Math.PI * Zdog.TAU; 503 - let direction = this.widget.renderNormal; 504 - let delta = this.editor.getAxisDistance( x, y, Math.atan2(direction.y, direction.x) + TAU/4 ); 505 - let command = new RotateCommand(this.editor, this.targets, new Zdog.Vector({[this.mode]: delta})); 506 - this.editor.did(command); 507 - } 508 - drawWidget(targets) { 509 - // Create anchors matching selected objects. 510 - targets = targets.map((target) => { 511 - return new Zdog.Anchor({ 512 - addTo: this.editor.ui, 513 - ...this.editor.getWorldTransforms(target), 514 - }); 515 - }); 516 - 517 - const widgetDiameter = 10; 518 - const widgetStroke = 0.75; 519 - 520 - let origin = new Zdog.Shape({ 521 - stroke: .5, 522 - color: lace, 523 - }); 524 - let zRing = new Zdog.Ellipse({ 525 - diameter: widgetDiameter, 526 - stroke: widgetStroke, 527 - color: blueberry, 528 - }); 529 - let yRing = zRing.copyGraph({ 530 - rotate: { x: TAU/4 }, 531 - color: lime, 532 - }); 533 - let xRing = zRing.copyGraph({ 534 - rotate: { y: TAU/4 }, 535 - color: rose, 536 - }); 537 - targets.forEach(t => { 538 - origin.copyGraph({ addTo: t, scale: 1/t.scale.x }); 539 - zRing.copyGraph({ addTo: t, scale: 1/t.scale.x }); 540 - yRing.copyGraph({ addTo: t, scale: 1/t.scale.x }); 541 - xRing.copyGraph({ addTo: t, scale: 1/t.scale.x }); 542 - }); 543 - } 544 - 545 - static get MODE_NONE() { return ''; } 546 - static get MODE_X() { return 'x'; } 547 - static get MODE_Y() { return 'y'; } 548 - static get MODE_Z() { return 'z'; } 549 - } 550 - 551 - class Command { 552 - constructor(editor) { 553 - this.editor = editor; 554 - } 555 - do() {} 556 - undo() {} 557 - } 558 - 559 - class SelectCommand extends Command { 560 - constructor(editor, target, replace = false) { 561 - super(editor); 562 - this.replace = replace; 563 - this.oldSelection = null; 564 - 565 - // If this is a compositeChild, find its parent 566 - while (target && target.compositeChild) { 567 - target = target.addTo; 568 - } 569 - // Don't select root elements. 570 - if (target && !target.addTo) { 571 - target = null; 572 - } 573 - this.target = target; 574 - } 575 - do() { 576 - this.oldSelection = this.editor.selection.slice( 0 ); 577 - if (!this.target) { 578 - this.editor.clearSelection(); 579 - } else if (this.replace) { 580 - this.editor.setSelection(this.target); 581 - } else { 582 - this.editor.toggleSelection(this.target); 583 - } 584 - this.refresh(); 585 - } 586 - undo() { 587 - this.editor.selection = this.oldSelection; 588 - this.refresh(); 589 - } 590 - refresh() { 591 - this.editor.updateHighlights(); 592 - this.editor.updateUI(); 593 - this.editor.props.updatePanel(); 594 - } 595 - } 596 - 597 - class TranslateCommand extends Command { 598 - constructor(editor, target, delta, oldTranslate = null) { 599 - super(editor); 600 - if (!Array.isArray(target)) { 601 - target = [target]; 602 - } 603 - this.target = target; 604 - this.delta = delta; 605 - this.oldTranslate = oldTranslate || target.map((t) => t.translate.copy()); 606 - } 607 - 608 - do() { 609 - if (!this.target) return console.error("Doing TranslateCommand with no target."); 610 - 611 - this.target.forEach((t, i) => { 612 - t.translate.set(this.oldTranslate[i]).add(this.delta); 613 - }); 614 - this.refresh(); 615 - } 616 - undo() { 617 - if (!this.target) return console.error("Undoing TranslateCommand with no target."); 618 - 619 - this.target.forEach((t, i) => { 620 - t.translate.set(this.oldTranslate[i]); 621 - }); 622 - this.refresh(); 623 - } 624 - refresh() { 625 - this.editor.updateHighlights(); 626 - this.editor.updateUI(); 627 - this.editor.props.updatePanel(); 628 - } 629 - } 630 - 631 - class RotateCommand extends Command { 632 - // TODO: Add oldTranslate and oldRotate to the constructor, or maybe a flag to tell it if it's getting new or old transforms. 633 - constructor(editor, target, delta) { 634 - super(editor); 635 - if (!Array.isArray(target)) { 636 - target = [target]; 637 - } 638 - this.target = target; 639 - this.delta = delta; 640 - this.oldRotate = target.map((t) => t.rotate.copy()); 641 - } 642 - 643 - do() { 644 - // TODO: Probably better to just throw in the constructor. 645 - if (!this.target) return console.error("Doing RotateCommand with no target."); 646 - 647 - this.target.forEach((t, i) => { 648 - t.rotate.set(this.oldRotate[i]).add(this.delta); 649 - }); 650 - } 651 - undo() { 652 - if (!this.target) return console.error("Undoing RotateCommand with no target."); 653 - 654 - this.target.forEach((t, i) => { 655 - t.rotate.set(this.oldRotate[i]); 656 - }); 657 - } 658 - } 659 - 660 - class EditCommand extends Command { 661 - constructor(editor, target, propId, value, oldValue = null) { 662 - super(editor); 663 - if (!target) { 664 - throw new Error("No target specified for EditCommand"); 665 - } 666 - 667 - if (!Array.isArray(target)) { 668 - target = [target]; 669 - } 670 - this.target = target; 671 - this.propId = propId; 672 - this.value = value; 673 - this.oldValue = oldValue || target.map( (t) => t[propId]); 674 - } 675 - do() { 676 - this.target.forEach( (t) => { 677 - t[this.propId] = this.value; 678 - if (t.updatePath) t.updatePath(); 679 - }); 680 - } 681 - undo() { 682 - this.target.forEach( (t, i) => { 683 - t[this.propId] = this.oldValue[i]; 684 - if (t.updatePath) t.updatePath(); 685 - }); 686 - } 687 - } 688 - 689 263 class History { 690 264 constructor() { 691 265 this.undoStack = []; ··· 717 291 this.undoStack.push(command); 718 292 } 719 293 } 720 - 721 - class Properties { 722 - constructor(panel, editor) { 723 - this.panel = panel; 724 - this.editor = editor; 725 - this.header = this.panel.querySelector("h2"); 726 - this.panel.addEventListener("input", this.handleInput.bind(this)); 727 - this.panel.addEventListener("change", this.handleChange.bind(this)); 728 - this.command = null; 729 - } 730 - 731 - handleInput(event) { 732 - const propElem = event.target.closest(".prop"); 733 - if (!propElem) return; 734 - 735 - console.log("modifying", propElem.id); 736 - 737 - const oldValue = this.readProperty(propElem.id); 738 - if (Array.isArray(oldValue)) { 739 - return; 740 - } 741 - const newValue = this.readPanel(propElem); 742 - // start a new edit command 743 - if (!this.command) { 744 - this.command = new EditCommand(this.editor, this.editor.selection.slice(0), propElem.id, newValue, [oldValue]); 745 - } else { 746 - this.command.value = newValue; 747 - } 748 - this.writeProperty(propElem, newValue); 749 - this.editor.updateUI(); 750 - this.editor.updateHighlights(); 751 - } 752 - 753 - handleChange(event) { 754 - // as far as I know, change always fires after input. 755 - this.editor.did(this.command); 756 - this.command = null; 757 - } 758 - 759 - // Synchronize the properties panel with the selected objects 760 - updatePanel() { 761 - this.updateHeader(); 762 - this.updateBody(); 763 - } 764 - 765 - updateHeader() { 766 - const selected = this.editor.selection; 767 - if (selected.length === 0) { 768 - this.header.textContent = "No objects selected"; 769 - } else if (selected.length > 1) { 770 - this.header.textContent = `${selected.length} objects selected`; 771 - } else { 772 - let breadcrumbs = []; 773 - let target = selected[0]; 774 - for (let offset = 0; target.addTo; offset++) { 775 - breadcrumbs.unshift(`<span class="breadcrumb" data-parent-index="${offset}">${target.constructor.type}</span>`); 776 - target = target.addTo; 777 - } 778 - this.header.innerHTML = breadcrumbs.join(" / "); 779 - } 780 - } 781 - 782 - updateBody() { 783 - // Shortcut: Just hide all entries if nothing's selected. 784 - if (this.editor.selection.length === 0) { 785 - this.panel.classList.add("hidden"); 786 - return; 787 - } 788 - this.panel.classList.remove("hidden"); 789 - 790 - // Set properties from first selected object. 791 - this.updatePanelProps(this.editor.selection[0], true); 792 - 793 - // Merge in properties from the rest of the selected objects. 794 - for (let i = 1; i < this.editor.selection.length; i++) { 795 - this.updatePanelProps(this.editor.selection[i]); 796 - } 797 - } 798 - 799 - // Hides properties in the panel that are incompatible with the given object. 800 - // If overwrite is true, it updates the values in the panel from the object. 801 - // If overwrite is false, it hides properties whose values don't match the object. 802 - updatePanelProps(srcObj, overwrite = false) { 803 - let srcProps = srcObj.constructor.optionKeys; 804 - let destProps = this.panel.querySelectorAll(".prop"); 805 - destProps.forEach( (propElem) => { 806 - if (!srcProps.includes(propElem.id)) { 807 - propElem.classList.add("hidden"); 808 - return; 809 - } 810 - propElem.classList.remove("hidden"); 811 - 812 - if (overwrite) { 813 - this.updatePanelProp(srcObj, propElem); 814 - return; 815 - } 816 - 817 - const objValue = srcObj[propElem.id]; 818 - const panelValue = this.readPanel(propElem); 819 - // TODO: This won't work for vector types. 820 - if (panelValue != objValue) { 821 - propElem.classList.add("hidden"); 822 - } 823 - }); 824 - } 825 - 826 - // Writes a single property from the object to the properties panel. 827 - updatePanelProp(srcObj, propElem) { 828 - const type = this.readType(propElem); 829 - 830 - if (this.isOptional(propElem)) { 831 - document.getElementById(`${propElem.id}-enabled`).enabled = srcObj[propElem.id] !== false; 832 - } 833 - 834 - const input = document.getElementById(`${propElem.id}-value`); 835 - 836 - switch (type) { 837 - case "bool": 838 - input.checked = srcObj[propElem.id] != false; 839 - break; 840 - case "number": 841 - input.valueAsNumber = srcObj[propElem.id]; 842 - break; 843 - case "color": 844 - input.value = this.normalizeColor(srcObj[propElem.id]); 845 - break; 846 - case "vector": 847 - let {x, y, z} = srcObj[propElem.id]; 848 - document.getElementById(`${propElem.id}-x`).value = x; 849 - document.getElementById(`${propElem.id}-y`).value = y; 850 - document.getElementById(`${propElem.id}-z`).value = z; 851 - break; 852 - default: 853 - console.warn(`Unknown property type: ${type}`); 854 - } 855 - } 856 - 857 - normalizeColor(color) { 858 - if (/^#[0-9a-fA-F]{6}$/.test(color)) { 859 - return color; 860 - } 861 - 862 - if (/^#[0-9a-fA-F]{3}$/.test(color)) { 863 - return `#${color[1]}${color[1]}${color[2]}${color[2]}${color[3]}${color[3]}`; 864 - } 865 - 866 - return "#000000"; 867 - } 868 - 869 - // Read a property from the selected objects. 870 - // Returns an array if the property differs across the selection. 871 - readProperty(propId) { 872 - if (!this.editor.selection.length) { 873 - return null; 874 - } 875 - 876 - if (this.editor.selection.length === 1) { 877 - return this.editor.selection[0][propId]; 878 - } 879 - 880 - let values = this.editor.selection.map( (target) => target[propId]); 881 - let allEqual = values.every( (val) => val === values[0]); 882 - if (allEqual) { 883 - return values[0]; 884 - } 885 - return values; 886 - } 887 - 888 - // Write property to the selected objects 889 - // If value is null, it reads the value from the properties panel. 890 - writeProperty(propElem, value = null) { 891 - if (value === null) value = this.readPanel(propElem); 892 - let targets = this.editor.selection; 893 - targets.forEach( (target) => { 894 - target[propElem.id] = value; 895 - // TODO: Add additional type information to props so we can check if we actually need to do this. 896 - if (t.updatePath) target.updatePath(); 897 - }); 898 - } 899 - 900 - // Get the type of a property from the properties panel 901 - readType(propElem) { 902 - let types = ["vector", "number", "color", "bool"]; 903 - for (let i = 0; i < types.length; i++) { 904 - if (propElem.classList.contains(types[i])) { 905 - return types[i]; 906 - } 907 - } 908 - return null; 909 - } 910 - 911 - isOptional(propElem) { 912 - return propElem.classList.contains("optional"); 913 - } 914 - 915 - isEnabled(propElem) { 916 - if (!this.isOptional(propElem)) { 917 - return true; 918 - } 919 - return document.getElementById(propElem.id + "-enabled").checked; 920 - } 921 - 922 - // Read the value of a property from the properties panel 923 - readPanel(propElem) { 924 - const type = this.readType(propElem); 925 - 926 - if (!this.isEnabled(propElem)) { 927 - return false; 928 - } 929 - 930 - const input = document.getElementById(`${propElem.id}-value`); 931 - 932 - switch (type) { 933 - case "bool": 934 - return input.checked; 935 - case "number": 936 - return input.valueAsNumber; 937 - case "color": 938 - return input.value; 939 - case "vector": 940 - return new Zdog.Vector({ 941 - x: document.getElementById(`${propElem.id}-x`).valueAsNumber, 942 - y: document.getElementById(`${propElem.id}-y`).valueAsNumber, 943 - z: document.getElementById(`${propElem.id}-z`).valueAsNumber, 944 - }); 945 - default: 946 - return null; 947 - } 948 - } 949 - } 950 - 951 - class Preset { 952 - load(illo) {} 953 - } 954 - 955 - class Solar extends Preset { 956 - load(illo) { 957 - let sun = new Zdog.Shape({ 958 - addTo: illo, 959 - stroke: 10, 960 - color: gold, 961 - }); 962 - 963 - let arrow = new Zdog.Cone({ 964 - addTo: sun, 965 - diameter: 1.5, 966 - length: 1.5, 967 - stroke: false, 968 - translate: { y: -7.5 }, 969 - rotate: { x: -TAU / 4 }, 970 - color: orange, 971 - }); 972 - 973 - new Zdog.Cylinder({ 974 - addTo: arrow, 975 - color: orange, 976 - stroke: false, 977 - length: 1, 978 - diameter: 0.75, 979 - translate: { z: -.5 }, 980 - }); 981 - 982 - new Zdog.Shape({ 983 - addTo: sun, 984 - stroke: 1, 985 - translate: { x: 6 }, 986 - color: raisin, 987 - }); 988 - 989 - let orbit = new Zdog.Anchor({ 990 - addTo: sun, 991 - rotate: { x: TAU / 4 }, 992 - }); 993 - 994 - let orbitalProps = { 995 - stroke: 0.5, 996 - color: charcoal, 997 - closed: false, 998 - }; 999 - 1000 - let quadrant = new Zdog.Shape({ 1001 - addTo: orbit, 1002 - path: [ 1003 - { x: 5.8, y: -1.55 }, 1004 - { bezier: [ 1005 - { x: 5.1, y: -4.17 }, 1006 - { x: 2.71, y: -6 }, 1007 - { x: 0, y: -6 }, 1008 - ]}, 1009 - ], 1010 - }); 1011 - 1012 - Zdog.extend(quadrant, orbitalProps); 1013 - 1014 - quadrant.copy({ 1015 - scale: { y: -1 }, 1016 - }); 1017 - 1018 - quadrant = new Zdog.Shape({ 1019 - addTo: orbit, 1020 - path: [ 1021 - { x: 0, y: -6 }, 1022 - { arc: [ 1023 - { x: -6, y: -6 }, 1024 - { x: -6, y: 0 }, 1025 - ]}, 1026 - ] 1027 - }); 1028 - 1029 - Zdog.extend(quadrant, orbitalProps); 1030 - 1031 - quadrant.copy({ 1032 - scale: { y: -1 }, 1033 - }); 1034 - 1035 - let middleOrbit = orbit.copyGraph({ 1036 - rotate: { x: TAU / 5, z: TAU / 8 }, 1037 - scale: 8/6, 1038 - }); 1039 - 1040 - new Zdog.Shape({ 1041 - addTo: middleOrbit, 1042 - stroke: 2, 1043 - translate: { x: 6 }, 1044 - color: rose, 1045 - }); 1046 - 1047 - let outerOrbit = orbit.copyGraph({ 1048 - rotate: { x: TAU / 3, z: TAU / 4 }, 1049 - scale: 10 / 6, 1050 - }); 1051 - 1052 - new Zdog.Shape({ 1053 - addTo: outerOrbit, 1054 - stroke: 2, 1055 - translate: { x: 6 }, 1056 - color: blueberry, 1057 - }); 1058 - } 1059 - } 1060 - 1061 - const TAU = Zdog.TAU; 1062 - 1063 - const charcoal = "#333"; 1064 - const raisin = "#534"; 1065 - const plum = "#636"; 1066 - const rose = "#C25"; 1067 - const orange = "#E62"; 1068 - const gold = "#EA0"; 1069 - const lemon = "#ED0"; 1070 - const peach = "#FDB"; 1071 - const lace = "#FFF4E8"; 1072 - const mint = "#CFD"; 1073 - const lime = "#4A2"; 1074 - const blueberry = "#359"; 1075 294 1076 295 const sceneElem = document.querySelector("#canvas"); 1077 296 const overlayElem = document.querySelector("#overlay");