{"id":"e09c1d29d3075ba6","slug":"zdog-helpers","trashed":false,"description":"","likes":23,"publish_level":"public","forks":0,"fork_of":null,"has_importers":true,"update_time":"2021-04-27T16:04:39.281Z","first_public_version":null,"paused_version":null,"publish_time":"2019-07-08T20:54:51.757Z","publish_version":790,"latest_version":790,"thumbnail":"91fcfadbfff028d28f3d375835202158b2ce3769520bb0bf51b4ed873bbf32f1","default_thumbnail":"0a7643e675c1230947446032ebb9dacb0e7fe79254b7dc8e58361d31096ebc36","roles":[],"sharing":null,"owner":{"id":"898d35f8860c7f03","avatar_url":"https://avatars.observableusercontent.com/avatar/0d7defa821f38094c03bad23b9b360a5364e6e97e21fc238c39ddc48db7994ad","login":"mootari","name":"Fabian Iwand","bio":"Web dev and tinkerer.","home_url":"https://twitter.com/mootari","type":"team","tier":"starter_2024"},"creator":{"id":"07362516b5994994","avatar_url":"https://avatars.observableusercontent.com/avatar/0d7defa821f38094c03bad23b9b360a5364e6e97e21fc238c39ddc48db7994ad","login":"mootari","name":"Fabian Iwand","bio":"Web dev and tinkerer.","home_url":"https://mootari.de/","tier":"pro"},"authors":[{"id":"07362516b5994994","avatar_url":"https://avatars.observableusercontent.com/avatar/0d7defa821f38094c03bad23b9b360a5364e6e97e21fc238c39ddc48db7994ad","name":"Fabian Iwand","login":"mootari","bio":"Web dev and tinkerer.","home_url":"https://mootari.de/","tier":"pro","approved":true,"description":""}],"collections":[{"id":"e3842c7642c32750","type":"public","slug":"tools","title":"Tools & Techniques","description":"Helpers, tricks and algorithms.","update_time":"2019-08-23T21:42:53.858Z","pinned":true,"ordered":false,"custom_thumbnail":null,"default_thumbnail":"4da17ac6e199a83f15c4c6aa428f63f1af08f18b2a1ff08157c191e61af8709f","thumbnail":"4da17ac6e199a83f15c4c6aa428f63f1af08f18b2a1ff08157c191e61af8709f","listing_count":60,"parent_collection_count":0,"owner":{"id":"898d35f8860c7f03","avatar_url":"https://avatars.observableusercontent.com/avatar/0d7defa821f38094c03bad23b9b360a5364e6e97e21fc238c39ddc48db7994ad","login":"mootari","name":"Fabian Iwand","bio":"Web dev and tinkerer.","home_url":"https://twitter.com/mootari","type":"team","tier":"starter_2024"}},{"id":"fca71a3ea0c0360d","type":"public","slug":"3d","title":"3D","description":"","update_time":"2019-06-05T20:56:44.900Z","pinned":true,"ordered":false,"custom_thumbnail":null,"default_thumbnail":"9fd94dc322cc6c795fa73de1e816f20c15820eac375c5414fd38d24d819720d9","thumbnail":"9fd94dc322cc6c795fa73de1e816f20c15820eac375c5414fd38d24d819720d9","listing_count":7,"parent_collection_count":0,"owner":{"id":"898d35f8860c7f03","avatar_url":"https://avatars.observableusercontent.com/avatar/0d7defa821f38094c03bad23b9b360a5364e6e97e21fc238c39ddc48db7994ad","login":"mootari","name":"Fabian Iwand","bio":"Web dev and tinkerer.","home_url":"https://twitter.com/mootari","type":"team","tier":"starter_2024"}}],"files":[],"comments":[],"commenting_lock":null,"suggestion_from":null,"suggestions_to":[],"version":790,"title":"Zdog Helpers","license":"isc","copyright":"Copyright 2020 Fabian Iwand","nodes":[{"id":0,"value":"md`# Zdog Helpers\n\n[Zdog](https://zzz.dog/) illustrations can be hard to understand due to their orthographic projection and lack of shading. This notebook offers some utilities for authors of zdog scenes, taking inspiration from three.js' [collection of helpers](https://github.com/mrdoob/three.js/tree/master/src/helpers).\n\nWork in progress – feedback and merge requests are welcome!\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":458,"value":"viewof display = {\n  const cb = (n, l=n, s=1) => html`<label style=\"margin-right:.5em\">\n    <input style=\"margin-right:.5em;vertical-align:baseline\" type=checkbox name=\"${n}\" ${s?'checked':''}>${l}`;\n  const f = html`<form>\n    <p>Display:</p>\n    ${cb('line_grid', 'line grid', 0)}\n    ${cb('rect_grid', 'rect grid')}\n    ${cb('axes', 'axes')}\n    ${cb('rotation', 'rotation')}\n    ${cb('box', 'box')}\n    ${cb('perspective', 'perspective (experimental)')}\n  `;\n  f.oninput = () => {\n    f.value = Array.from(f.elements).reduce((o, n) => (o[n.name] = n.checked, o), {});\n  }\n  f.value = f.elements;\n  f.oninput();\n  return f;\n  \n}","pinned":false,"mode":"js","data":null,"name":null},{"id":6,"value":"{\n  // Can't use max-width because zdog forces explicit width and height,\n  // breaking aspect ratio on mobile Chrome (Android).\n  const w = Math.min(width, 600), h = w/6*4, PI = Math.PI;\n  const element = html`<canvas width=${w} height=${h}\">`;\n\n  const perspective = perspectiveHelper({fov: 300});\n  \n  let drag = false;\n  const illo = new Zdog.Illustration({\n    element,\n    dragRotate: true,\n    rotate: {x:-.5+PI, z: 0, y:.5+PI},\n    translate: {y: 50},\n    scale: 2,\n    onDragStart() { drag = true },\n    onDragEnd() { drag = false },\n    onPrerender() {\n      clearColor(this, 'hsl(200,20%,97%)');\n      if(display.perspective) perspective.onPrerender(this);\n    },\n  });\n  zoomable(illo, {\n    onZoom: (zoom, delta) => { element.value.zoom = illo.zoom }\n  });\n\n  if(display.axes) illo.addChild(axesHelper({stroke: 2,x:null,z:null}));\n  if(display.line_grid) illo.addChild(gridHelper({size:200, divisions: 20, stroke: 1.5}));\n  if(display.rect_grid) illo.addChild(gridRectHelper({size:200, divisions: 10, stroke: 2}));\n  if(display.rotation) illo.addChild(rotationHelper({x:null,z:null, size: 30}));\n  //illo.addChild(gridHelper({translate: {x:50, z: 50}}));\n  //illo.addChild(gridHelper({translate: {x:50, y: 50}, rotate: {x: Math.PI/2}}));\n  //illo.addChild(gridHelper({translate: {y:50, z: 50}, rotate: {z: Math.PI/2}}));\n\n  const box = new Zdog.Box({\n    addTo: illo,\n    width: 80,\n    height: 70,\n    depth: 60,\n    rotate: {x: 1.6, y: -.4, z: -2.3},\n    translate: {x: -60, y: 50, z: -10},\n    scale: .8,\n    color:     'hsla(200,80%,20%,.5)',\n    leftFace:  'hsla(200,80%,40%,.5)',\n    rightFace: 'hsla(200,80%,60%,.5)',\n    rearFace:  'hsla(200,80%,80%,.5)',\n    bottomFace: false,\n    topFace: false,\n    ...(display.box ? {} : {fill: false, stroke: false}),\n  });\n  if(display.rotation) rotationHelper({addTo: box, size: 20});\n  if(display.axes) axesHelper({addTo: box, size: 70});\n\n  if(this) {\n    const {illo: _illo, box: _box} = this.value;\n    illo.zoom = _illo.zoom;\n    illo.rotate = _illo.rotate;\n    box.rotate = _box.rotate;\n  }\n  element.value = { illo, box };\n  \n  const a = .003 * Zdog.TAU;\n  while(true) {\n    if(drag) box.rotate.x -= a*2 * .1;\n    else {\n      illo.rotate.y += a;\n      box.rotate.x -= a;\n    }\n    illo.updateRenderGraph();\n    if(display.perspective) perspective.resetStroke(illo);\n    yield element;\n  }\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":164,"value":"md`---\n## Helpers\n\nTo avoid problems, import with your local <code>Zdog</code> instance:\n\\`\\`\\`javascript\nimport {axesHelper, gridHelper, rotationHelper} with {Zdog} from '@mootari/zdog-helpers'\n\\`\\`\\`\n\nAll helpers use an [<code>Anchor</code>](https://zzz.dog/api#anchor) instance as root element. Any properties that are not directly consumed by a helper get passed to the anchor constructor (e.g. <code>translate</code>, <code>addTo</code>).\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":30,"value":"// Adds an element that displays the axis directions.\nfunction axesHelper({size = 100, head = 10, stroke = 1, x = 'red', y = 'green', z = 'blue', ...options} = {}) {\n  const PIH = Math.PI / 2;\n  const line = (color, rotate) => {\n    const l = new Zdog.Shape({color, path: [{}, {x: size}], fill: false, stroke, rotate});\n    if(head) new Zdog.Cone({\n      addTo: l,\n      color, stroke: false, fill: true, length: head,\n      backface: 'black',\n      diameter: head * .66,\n      rotate: {y: -PIH},\n      translate: {x: size},\n    });\n    return l;\n  }\n  \n  const a = new Zdog.Anchor(options);\n  if(x) a.addChild(line(x, {}));\n  if(y) a.addChild(line(y, {z: PIH}));\n  if(z) a.addChild(line(z, {y: PIH}));\n  return a;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":82,"value":"// Adds a grid constructed from a single path.\nfunction gridHelper({size = 100, divisions = 10, color = '#888', stroke = 1, ...options} = {}) {\n  const sh = size/2;\n  const path = [];\n  for(let i = 0; i < divisions+1; i++) {\n    const o = size/divisions*i-sh;\n    path.push(\n      {move: {z: -sh, x: o}},\n      {line: {z: sh, x: o}},\n      {move: {x: -sh, z: o}},\n      {line: {x: sh, z: o}}\n    );\n  }\n  const a = new Zdog.Anchor(options);\n  new Zdog.Shape({path, stroke: 1, fill: false, color, addTo: a});\n  return a;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":755,"value":"// Adds a grid constructed from multiple rects.\n// Better alternative to gridHelper when perspectiveHelper is used with stroke scaling.\nfunction gridRectHelper({size = 100, divisions = 10, color = '#888', stroke = 1, ...options} = {}) {\n  const sh = size/2, s = size/divisions;\n  const addTo = new Zdog.Anchor(options);\n  \n  for(let z = 0; z < divisions; z++) {\n    const oz = s*z - sh;\n    for(let x = 0; x < divisions; x++) {\n       const ox = size/divisions * x - sh;\n        new Zdog.Shape({stroke, fill: false, color, closed: true, addTo, path: [\n          {move: {z: oz, x: ox}},\n          {line: {z: oz, x: ox + s}},\n          {line: {z: oz + s, x: ox + s}},\n          {line: {z: oz + s, x: ox}},\n        ]});\n    }\n  }\n  \n  return addTo;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":303,"value":"// Adds an element that displays the rotation direction for each axis.\nfunction rotationHelper({size: r = 1, hole = .5, range = 1, x = 'red', y = 'green', z = 'blue', ...options} = {}) {\n  const mix = (a, b, t) => a + t * (b - a);\n  const root = new Zdog.Anchor(options), Q = Zdog.TAU*.25;\n  const r1 = r * hole, r2 = mix(r1, r, range); \n  const path = (r, r1, r2) => [\n    {move: {x: r, y: 0}},\n    {arc: [ {x: r, y: r}, {x: 0, y: r} ]},\n    {line: {x: 0, y: r1}},\n    {arc: [ {x: mix(r1,r2,.5), y: mix(r1,r2,.5)}, {x: r2, y: 0} ]},\n  ];\n  \n  for(let {color, colorTop = color, rotate} of [\n    x && {color: x, rotate: {y:Q}},\n    y && {color: y, rotate: {x:Q}},\n    z && {color: z, rotate: {z:Q}},\n  ].filter(v => v)) {\n    const ring = new Zdog.Anchor({addTo: root, rotate});\n    for(let i = 0; i < 4; i++) {\n      new Zdog.Shape({\n        addTo: ring,\n        path: path(r, mix(r1, r2, i/4), mix(r1, r2, (i+1)/4)),\n        rotate: {z: -Q*i},\n        closed: true,\n        fill: true,\n        stroke: false,\n        color,\n      });\n    }\n  }\n  \n  return root;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":295,"value":"md`---\n## Utilities\n\nFunctions that are useful outside of debugging.\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":286,"value":"// Fills the canvas with the specified color. Call in onPrerender.\nfunction clearColor(illo, color) {\n  const ctx = illo.ctx;\n  ctx.save();\n  ctx.resetTransform();\n  ctx.fillStyle = color;\n  ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);\n  ctx.restore();\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":688,"value":"// Temporarily resets tranforms and calls drawFn, passing the illustration context.\nfunction drawRaw(illo, drawFn) {\n  const ctx = illo.ctx;\n  ctx.save();\n  ctx.resetTransform();\n  drawFn(ctx);\n  ctx.restore();  \n}","pinned":false,"mode":"js","data":null,"name":null},{"id":601,"value":"// Adds zooming via mousewheel / trackpad.\nfunction zoomable(illo, options = {}) {\n  Object.assign(illo, {\n    setZoomable(zoomable) {\n      zoomable\n        ? this.element.addEventListener('wheel', this)\n        : this.element.removeEventListener('wheel', this);\n    },\n    zoomSpeed: .001,\n    minZoom: .1,\n    onwheel(event) {\n      event.preventDefault();\n      const z0 = this.zoom, z = z0 - event.deltaY * this.zoomSpeed;\n      // Max boundary handled by Zdog.\n      this.zoom = z < this.minZoom ? this.minZoom : z;\n      if(this.onZoom) this.onZoom(this.zoom, this.zoom - z0);\n    }\n  }, options);\n  illo.setZoomable(illo.zoomSpeed != 0);\n  return illo;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":692,"value":"// Emulates a perspective projection.\nfunction perspectiveHelper({fov = 100, zOffset = 0, scaleStroke = true} = {}) {\n  \n  function walk(illo, callback) {\n    const top = new Set(illo.flatGraph);\n    for(const g of top) _walk(g);\n    \n    function _walk(item) {\n      callback(item);\n      for(const c of item.children)\n        if(!top.has(c)) _walk(c);\n    }\n  }\n  \n  return {\n    fov,\n    zOffset,\n    scaleStroke,\n    \n    getScale(p) {\n      return this.fov / (this.fov - p.z + this.zOffset);\n    },\n    \n    scalePoint(p) {\n      const s = this.getScale(p);\n      p.x *= s;\n      p.y *= s;\n    },\n    \n    onPrerender(illo) {\n      walk(illo, e => {\n        this.scalePoint(e.renderOrigin);\n\n        if(e.pathCommands && e.pathCommands.length) {\n          if(this.scaleStroke && e.stroke) {\n            if(e._stroke == null) e._stroke = e.stroke;\n            e.stroke = e._stroke * this.getScale(e.pathCommands[0].endRenderPoint);\n          }\n          \n          for(const c of e.pathCommands)\n            for(const p of c.renderPoints)\n              this.scalePoint(p);\n        }\n          \n      });\n    },\n    \n    resetStroke(illo) {\n      walk(illo, e => {\n        if(e._stroke !== undefined) {\n          e.stroke = e._stroke;\n          e._stroke = undefined;\n        }\n      });\n    }\n  };\n  \n}","pinned":false,"mode":"js","data":null,"name":null},{"id":208,"value":"md`---\n## Dependencies\n\nNote that you can pass in your own Zdog instances.\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":2,"value":"Zdog = require('zdog@1.1.2/dist/zdog.dist.min.js')","pinned":false,"mode":"js","data":null,"name":null}],"resolutions":[],"schedule":null,"last_view_time":null}