{"id":13857722,"url":"https://github.com/oganm/rdog","last_synced_at":"2025-12-24T22:52:33.709Z","repository":{"id":80646801,"uuid":"194182691","full_name":"oganm/rdog","owner":"oganm","description":"Porting zdog pseudo-3d engine to R for reasons...","archived":false,"fork":false,"pushed_at":"2019-12-19T23:56:36.000Z","size":24827,"stargazers_count":32,"open_issues_count":3,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-11-22T15:41:18.539Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://oganm.github.io/rdog","language":"R","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/oganm.png","metadata":{"files":{"readme":"README.Rmd","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-06-28T00:59:37.000Z","updated_at":"2024-02-06T13:34:11.000Z","dependencies_parsed_at":"2023-03-12T11:41:51.998Z","dependency_job_id":null,"html_url":"https://github.com/oganm/rdog","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/oganm/rdog","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oganm%2Frdog","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oganm%2Frdog/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oganm%2Frdog/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oganm%2Frdog/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/oganm","download_url":"https://codeload.github.com/oganm/rdog/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oganm%2Frdog/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265218200,"owners_count":23729496,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-05T03:01:45.166Z","updated_at":"2025-12-24T22:52:33.698Z","avatar_url":"https://github.com/oganm.png","language":"R","funding_links":[],"categories":["R"],"sub_categories":[],"readme":"---\noutput: github_document\neditor_options: \n  chunk_output_type: console\n---\n\n```{r setup, include=FALSE}\nknitr::opts_chunk$set(echo = TRUE,\n                      fig.path = \"man/figures/\")\ndevtools::load_all()\n```\n\n# Rdog \u003cimg src=\"man/figures/logo.gif\" align=\"right\" height=\"150\"/\u003e\n\nThis is a port of the [zdog](https://zzz.dog/) pseudo 3D engine for R.\n\nTable of Contents\n=================\n\n   * [Rdog](#rdog-)\n   * [Table of Contents](#table-of-contents)\n      * [Installation](#installation)\n      * [Why?](#why)\n      * [Basic usage](#basic-usage)\n      * [Rendering SVG paths](#rendering-svg-paths)\n      * [Rendering 3D STL files](#rendering-stl-files)\n      * [Rendering maps](#rendering-maps)\n      * [Use in shiny](#use-in-shiny)\n      * [Use as shiny inputs](#use-as-shiny-inputs)\n      * [Anatomy of an animation](#anatomy-of-an-animation)\n      * [Notes](#notes)\n         * [Scoping](#scoping)\n         * [Using colors](#using-colors)\n      * [Todo](#todo)\n      * [Rdog hex logo](#rdog-hex-logo)\n\n\n## Installation\n\n``` r\ndevtools::install_github('oganm/rdog')\n```\n\nInstall the latest Rstudio to make sure it works well with the built in\nviewer. Some elements appears to misbehave in older versions.\n\n## Why?\n\nAs the zdog's author states\n\n\u003e Zdog is a 3D JavaScript engine for canvas and SVG. With Zdog, you can design and render simple 3D models on the Web. Zdog is a pseudo-3D engine. Its geometries exist in 3D space, but are rendered as flat shapes. This makes Zdog special.\n\nCan rdog make renderings that look better than [rayshader](https://github.com/tylermorganwall/rayshader) or [rayrender](https://github.com/tylermorganwall/rayrender)? No. We have no perspective,\nno distance blurring, no lighting simulation, no shadows. You have objects in a 3D space and they\ntend to look cute with minimal effort. It has a nice natural aesthetic that I happen\nto like.\n\nOne advantage is that output of rdog is an htmlwidget and the rendering is done through\njavascript objects by the browser. Images are rendered into a canvas or an svg context.\nThis is particularly nice for shiny apps since \nrendering is outsourced to the users' browser and the rendered objects can be manipulated\nwith relative ease through javascript or wrapper functions provided in the package.\n\n## Basic usage\n\nTaking a look at the [zdog api](https://zzz.dog/api) will give you a good idea how\nthings work. Variable names and defaults are preserved. The only major difference\nis the use of piping to create a single illustration object with all the elements.\n\nAn animated illustration can be created by adding elements to an illustration\nusing `%\u003e%`. Each shape can be added to the illustartion itself (default), or\nto another existing shape which will be used as a reference point. Here a box is\ncreated placed on `y = -20`. The next element is an ellipse which is added to the \nbox which places it in the middle of the box. All shape functions start with `shape`\n\nTo add text, a font should be added first. `zfont_font(id = 'font')` creates one\nwith the default font, \"Roboto-Regular\". Different fonts can be used if you have\nthe `ttf` files.\n\nFinally, animations are also added to elements or the entire illustration. Here\n`animation_rotate` is added to the ellipse and `animation_ease_in` is added to the\nbox which makes them rotate at different rates. `animation_none` will not move the\nobjects but creates the loop required for `dragRotate` to work.\n\n``` {r cornell_box}\nillustration('illo',width = 250,height = 250, dragRotate = TRUE) %\u003e% \n    shape_box(id ='cornell',\n              width = 150,\n              height = 150,\n              depth = 150,\n              translate = c(y = '-20'),\n              rotate = c(x = -tau/20,y = tau/16),\n              stroke = 1,\n              color = '#C25',\n              leftFace = 'red',\n              rightFace =  'darkgreen',\n              topFace =  'white',\n              bottomFace =  'white',\n              frontFace =  FALSE,\n              rearFace= 'lightgray') %\u003e% \n    shape_ellipse(\n        addTo = 'cornell',\n        id = \"ellipse\",\n        diameter = 80,\n        stroke = 20,\n        color = '#636',fill = FALSE\n    ) %\u003e% \n    zfont_font(id = 'font') %\u003e% \n    zfont_text(zfont = 'font', text = 'Cornell Box',fontSize = 24,translate = c(y = 120),textAlign = 'center') %\u003e% \n    animation_rotate(addTo = 'ellipse',id = 'rotate',rotate = c(y = 0.05)) %\u003e% \n    animation_ease_in(id = 'ease',radiansPerCycle = tau/2,addTo='cornell',framesPerCycle = 200,power = 3) %\u003e% \n    record_gif(duration = 10)\n```\n\n\n`record_gif` is not required for interactive usage or html renderings. By default, the\noutput is an htmlwidget, that can be automatically displayed in the viewer. This doesn't work\nwith github_document's due to restrictions on github so rendering into a gif or an image (`save_image`) is necesary.\n\n\n`anchors` are invisible elements that one can add shapes to and be used as a reference point.\n\n```{r anchor}\nillustration('illo') %\u003e% \n  anchor(id ='anchor', translate = c(x = 100)) %\u003e% \n  shape_shape(addTo = 'anchor', translate = c(y = - 100), stroke = 20) %\u003e% \n  save_image()\n```\n\n`copy` and `copy_graph` functions can be used to duplicate objects. `copy_graph`\n will also copy any children object that an object has. You can change properties\n of the object you copy by passing additional arguments.\n \n```{r copy}\nillustration('illo') %\u003e% \n  shape_shape(id ='point1', translate = c(x = 100),stroke = 20, color = 'blue') %\u003e% \n  shape_shape(addTo = 'point1', translate = c(y = - 100), stroke = 20, color = 'red') %\u003e%\n  copy(id = 'copyOfPoint1', what = 'point1',color = 'darkgreen',translate = c(x = -100)) %\u003e%\n  save_image()\n```\n\n```{r copyGraph}\nillustration('illo') %\u003e% \n  shape_shape(id ='point1', translate = c(x = 100),stroke = 20, color = 'blue') %\u003e% \n  shape_shape(addTo = 'point1', translate = c(y = - 100), stroke = 20, color = 'red') %\u003e%\n  copy_graph(id = 'copyOfPoint1', what = 'point1',color = 'darkgreen',translate = c(x = -100)) %\u003e%\n  save_image()\n```\n\n\n## Rendering SVG paths\n\nPaths from svg files can also be displayed. [Chess bishop icon](https://game-icons.net/1x1/skoll/chess-bishop.html) used as\nan example here was made by by Skoll under [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/).\n\n```{r bishop}\n# get an svg file\nsvgFile = system.file('chess-bishop.svg',package = 'rdog')\n# parse the svg file\nsvg = XML::xmlParse(svgFile) %\u003e% XML::xmlToList()\n# extract path\npath = svg$g$path['d']\n\n# animate and record gif\nillustration('illo',width = 256,height = 256) %\u003e%\n    svg_path_to_shape(svgWidth = 512, svgHeight = 512,stroke = 1,svgPath = path,scale = .7,fill =FALSE,closed = FALSE)   %\u003e% \n    save_image()\n```\n\n`stroke` variable can be used to give some depth to svg paths.\n\n```{r stroke_bishop}\nillustration('illo',width = 256,height = 256, rotate = c(y =tau/10, x = -tau/10)) %\u003e%\n    svg_path_to_shape(svgWidth = 512, svgHeight = 512,stroke =5,svgPath = path,scale = .7,fill =FALSE,closed = FALSE)   %\u003e% \n    save_image()\n```\n\nNote that setting `fill = TRUE` will cause you to lose enclosed shapes in an svg path\n\n```{r fill_bishop}\nillustration('illo',width = 256,height = 256, rotate = c(y =tau/10, x = -tau/10)) %\u003e%\n    svg_path_to_shape(svgWidth = 512, svgHeight = 512,stroke =5,svgPath = path,scale = .7,fill =TRUE,closed = FALSE)   %\u003e% \n    save_image()\n```\n\n\nBut some fiddling can still generate a decent looking 3D structure\n\n```{r threed_bishop}\nrd = illustration('illo',width = 256,height = 256 ,rotate = c(y =tau/15, x = -tau/15))\ncolfunc \u003c- colorRampPalette(c(\"gray60\",\"gray20\"))\n\nzAxis = seq(from = -25, to=25 ,by = 1)\nfor(i in seq_along(zAxis)){\n    rd %\u003c\u003e%\n        svg_path_to_shape(svgWidth = 512,svgHeight = 512,\n                          translate = c(z = zAxis[i]),color = colfunc(length(zAxis))[i],\n                          stroke = 2,svgPath = path,scale = .7,fill =FALSE,closed = FALSE)\n}\n\nzAxis2 = seq(from = -15, to=15 ,by = 1)\nfor(i in seq_along(zAxis2)){\n    rd %\u003c\u003e%\n        svg_path_to_shape(svgWidth = 512,svgHeight = 512,\n                          translate = c(z = zAxis2[i]),color = colfunc(length(zAxis))[zAxis %in% zAxis2[i]],\n                          stroke = 2,svgPath = path,scale = .7,fill =TRUE,closed = FALSE)\n}\n\nrd %\u003e% save_image()\n\n```\n\n\n## Rendering 3D STL files\n\nStl files can be read and rendered using the `stl_to_shape` function. Note that\nadding objects with too many polygons will likely hurt performance drastically.\n\nWhile not necessary, you can use `binary_to_ascii_stl` function to convert any binary\nstl to ascii stl files. This conversion happens internally if you don't so it's every\nso slightly more efficient to use it.\n\n[GEB Logo](https://www.thingiverse.com/thing:3265) used in this example was made by [guru](https://www.thingiverse.com/guru), licensed under [Creative Commons - Attribution - Share Alike](https://creativecommons.org/licenses/by-sa/3.0/) license. The origin of the logo\nis \"[Gödel, Escher, Bach](https://en.wikipedia.org/wiki/G%C3%B6del,_Escher,_Bach)\".\n\n```{r stl}\nstl = system.file('GEB.stl',package = 'rdog')\n\nstl_bounds = get_stl_bounds(stl)\n\n# we need to subtract 20 from the z axis to center this file\nstl_bounds\n\n# center_stl function can be used to center the objects automatically\ncenter_stl(stl_bounds)\n\n# use objectOffset to center the variable\nillustration(width = 150,height = 150,displayType = 'canvas',scale = 2,rotate = c(y = tau/2)) %\u003e%\n  stl_to_shape(stl = stl,colorAxis = 'z',colorMin = 'gray90',colorMax = 'black',\n               objectOffset = center_stl(stl_bounds),\n               stroke = 1) %\u003e% \n  animation_rotate(rotate =c(y = .01,x = .02)) %\u003e% record_gif(duration = 10)\n\n\n```\n\nLet's take a look at the platonic solids.\n\n```{r gc, include=FALSE}\ngc()\n```\n\n```r\nplatonic = list(\n  tetrahedron =  readLines(system.file('tetrahedron.stl',package = 'rdog')) %\u003e% paste(collapse = '\\n'),\n  cube = readLines(system.file('cube.stl',package = 'rdog')) %\u003e% paste(collapse = '\\n'),\n  octatohedron = readLines(system.file('octahedron.stl',package = 'rdog')) %\u003e% paste(collapse = '\\n'),\n  dodecahedron = readLines(system.file('dodecahedron.stl',package = 'rdog')) %\u003e% paste(collapse = '\\n'),\n  icosahedron = readLines(system.file('icosahedron.stl',package = 'rdog')) %\u003e% paste(collapse = '\\n')\n)\n\n\nillo = illustration(width = 150*5,height = 150,displayType = 'canvas',centered = TRUE)\n\nanchorCenters = seq(-300,300,by = 150)\n\n\nfor(i in seq_along(platonic)){\n  illo %\u003c\u003e% \n    anchor(id = paste0(names(platonic)[i],'_anchor'), translate = c(x = anchorCenters[i])\n           ) %\u003e% \n     stl_to_shape(id = names(platonic)[i],\n                  addTo = paste0(names(platonic)[i],'_anchor'),\n                  stl = platonic[[i]],\n                  colorMode = 'ordered',colorMin = 'gray90',colorMax = 'black',\n               objectOffset = center_stl(get_stl_bounds(platonic[[i]])),\n               stroke = 1, scale = 5) %\u003e% \n    animation_rotate(addTo = names(platonic)[i], rotate = c(y = .01, x = 0.02))\n  \n  \n}\n\nillo %\u003e% record_gif(duration = 10)\n\n\n```\n![](man/figures/platonic.gif)\n\n\nUse `objectOffset` instead of `translate` when centering an STL file otherwise rotation\ncenter won't be correctly aligned.\n\nThe coloring of the triangles that make up an object is controlled by `colorMode`,\n`colorAxis`, `colorMin` and `colorMax`. `colorMin` and `colorMax` is used to create\na color gradient that is used to color the triangles. `colorMode` controls how the \ncolors are distributed. The default method (\"mean\", seen in the GEB example) gets the mean `colorAxis` coordinate\nof each triangle and assigns a color based on that. \"extreme\" method assigns color based on the\nminimum and maximum `colorAxis` coordinate. \"ordered\" method (seen in the platonic solids example) assigns color in the \norder the triangles appear in the file so that each triangle gets a unique color.\n\n\n## Rendering maps\n\nrdog's probably not the best tool for this but here's a contour based visualization.\nNote that individual contours are split into smaller pieces and added as shapes.\nOtherwise long contours tend to have weird z-fighting and become visible when they\nshouldn't. Having less shapes is better for the frame rate though so the increasing\nthe `groupBy` variable in the code below will be good for performance.\n\nI also use `subsample` to get rid of a portion of contour points to make is easier\nto render and with the stroke volume, it doesn't drastically change how the map looks.\n\nNote that for this type of rendering, you definetaly don't want to use an SVG context if\nyou want any animation.\nHaving this many objects in an SVG really slows things down.\n\n```r\n# get the elevation data\nloadzip = tempfile()\ndownload.file(\"https://tylermw.com/data/dem_01.tif.zip\", loadzip)\nlocaltif = raster::raster(unzip(loadzip, \"dem_01.tif\"))\nunlink(loadzip)\n\n#And convert it to a matrix:\nelmat = matrix(raster::extract(localtif, raster::extent(localtif), buffer = 1000),\n               nrow = ncol(localtif), ncol = nrow(localtif))\n\n# scale values to be between -50 and 50 to match the z coordinates they'll get\nelmat %\u003c\u003e% scale_to_int(-50,50)\ncontours = contourLines(z = elmat, nlevels = 30)\n\n# get contour levels\nlevels = contours %\u003e% purrr::map_dbl('level') %\u003e% unique\n# get the difference between contour levels to set the stroke volume\nlevelDif = levels[2] - levels[1]\n\n# empty illustration with a group to control the contours\nrdog = illustration('illo',width = 340,height = 300, dragRotate = TRUE, rotate = c( y= tau/8, x = -tau/10)) %\u003e%\n    group('contours',updateSort = TRUE)\n\nsubsample = 8\ngroupBy = 32\n\ncolors = terrain.colors(length(levels))\n\nfor(i in seq_along(contours)){\n    level =contours[[i]]$level\n    color = colors[levels== level]\n    path = seq_along(contours[[i]]$x) %\u003e% lapply(function(j){\n        c(z = contours[[i]]$y[j]* 250 -125, x = contours[[i]]$x[j] * 250 - 125, y = -level)\n    })\n    \n    # get rid of a portion of points for better frame rate\n    path = path[unique(c(seq(from = 1, to = length(path), by = subsample), length(path)))]\n    \n    \n    # split the contour into multiple shapes to help with z-fighting\n    for(j in unique(seq(from = 1, to = length(path), by = groupBy), length(path))[-length(path)]){\n        rdog %\u003c\u003e% shape_shape(\n            stroke = levelDif,\n            color = color,\n            closed = FALSE,\n            fill = FALSE,\n            addTo = 'contours',\n            path = path[j:(min(j+groupBy,length(path)))]\n        )\n    }\n    \n}\n\nrdog %\u003e%\n    animation_rotate(rotate = c(y = .01)) %\u003e% \n    record_gif(duration = 12)\n```\n![](man/figures/elmat.gif)\n\n\nAn alternative way relies on an external utility: [hmm](https://github.com/fogleman/hmm) to generate stl files from heightmaps with relatively low number of triangles.\n\n```r\n# get the elevation data\nloadzip = tempfile()\ndownload.file(\"https://tylermw.com/data/dem_01.tif.zip\", loadzip)\nlocaltif = raster::raster(unzip(loadzip, \"dem_01.tif\"))\nunlink(loadzip)\n\n#And convert it to a matrix:\nelmat = matrix(raster::extract(localtif, raster::extent(localtif), buffer = 1000),\n               nrow = ncol(localtif), ncol = nrow(localtif))\n\n# create a png file to use with hmm\nheightmap = tempfile(fileext = '.png')\nelmat %\u003e% scale_to_int(0,1) %\u003e% \n    OpenImageR::writeImage(heightmap)\n\nstl = tempfile(fileext = '.stl')\n\n# use hmm to convert the heightmap to stl\nsystem(glue::glue('hmm {heightmap} {stl} -z 400 -t 1500'))\n\nstl_bounds = get_stl_bounds(stl)\n\n# use objectOffset to center the variable\nillustration(width = 400,height = 400,displayType = 'canvas',scale = 2,\n             rotate = c( y= tau/8, x = -tau/10)\n             ) %\u003e%\n    stl_to_shape(stl = stl,colorAxis = 'z',colorMode = 'extreme',colorMin = 'gray90',colorMax = 'black',\n                 objectOffset = center_stl(stl_bounds),\n                 stroke = 1,scale = .27, rotate = c(x = tau/4)) %\u003e% \n    animation_rotate(rotate = c(y = .01)) %\u003e% \n    record_gif(duration = 12)\n```\n![](man/figures/elmatPoly.gif)\n\n\n\n## Use in shiny\n\nrdog functions return a shiny widget which can be used in shiny applications using\n`renderRdog` and `rdogOutput` functions. Below is a basic app where radius of an\nellipse is controlled by a `sliderInput` and a spin animation can be triggered\nby pressing a button. This is done by a `animation_ease_in` command with a finite\nnumber of frames getting triggered by an `observe` code block. Any rdog command can be decoupled from the `renderRdog` command\nblock so you can modify individual elements of the of the illustration without re-creating the whole thing. \n\nNote that `useShinyjs()` command in the UI is required for decoupled commands to work. \nI know I should have used something like `session$sendCustomMessage`\nbut this was easier for now. This requirement may be removed later.\n\n```r\nlibrary(shiny)\ndevtools::load_all()\nui \u003c- fluidPage(\n    shinyjs::useShinyjs(),\n    shiny::actionButton(inputId = 'anim',label = 'Animate'),\n    shiny::sliderInput(min = 0, max = 140, inputId = 'slider',label = '',value = 80),\n    rdogOutput('dogy',height = 240,width = 240)\n)\n\nserver \u003c- function(input, output) {\n    output$dogy = renderRdog({\n        illustration('illo',width = 250,height = 250,dragRotate = TRUE) %\u003e%\n            shape_ellipse(\n                addTo = 'illo',\n                id = \"ellipse\",\n                diameter = 80,\n                stroke = 20,\n                color = '#636',fill = FALSE\n            ) %\u003e%\n            animation_none(id = 'none')\n    })\n    \n    observe({\n        print(input$anim)\n        if(input$anim\u003e0){\n            animation_ease_in(id = 'ease',rdog = 'illo',frames = 120,radiansPerCycle = tau/2,addTo='illo',framesPerCycle = 120,power = 3)\n        }\n    })\n    \n    observe({\n        print(input$slider)\n        update_property(rdog = 'illo',elementId = 'ellipse', diameter = input$slider)\n    })\n    \n}\n\nshinyApp(ui = ui, server = server)\n```\n\n![](man/figures/basicApp.gif)\n\nNote that in the example above, the diameter control did not have to have it's own `observe` block. We could have set `diameter = input$slider` within the `renderRdog` function as well. However, this would have caused the entire\nillustration to be re-created at each manipulation of the slider. Not great for performance\n\n\n## Use as shiny inputs\n\nIllustartions can be used as shiny inputs. At each mouse click at an illustration `input$illustrationId` will\nbe set to a list that contains `x` and `y` coordinates of the mouse relative to the canvas, along with a random `nonce`\nvalue to force trigger updates. This allows you to use rdog illustrations as buttons or make them interactive. A list of all\nactive animations is also returned in the input which allows one to avoid triggering the same animation multiple times.\n\nIf the `displayType` is a `canvas` and you click on an object, you'll also get the id of the object that you clicked along\nwith a non-zero `objectNo`. `objectNo`s start from 1 and incremented in the order you add the new elements. If you\nadd child elements however, numbers will be shifted since counting happens by iterating over the object\ntree.\n\nNote that objects\ncreated with single commands may occupy multiple ids for their different sides so you may have to experiment a little. For instance `shape_box` will have different IDs for each of their sides. \n\nText added by `zfont_text` has no object ids and clicking\non text will return the id of the object below that text if there is any. If you want to detect text,\nyou can create a transparent (not invisible) shape that encompasses the text and use that id instead.\n\n```r\nlibrary(shiny)\ndevtools::load_all()\nui \u003c- fluidPage(\n    shinyjs::useShinyjs(),\n    rdogOutput('check',height = 100,width = 100),\n    verbatimTextOutput('textOut')\n)\n\nserver \u003c- function(input, output) {\n    output$check = renderRdog({\n        illustration('illo',height = 100, width = 100,dragRotate = FALSE) %\u003e% \n            shape_ellipse(id = 'button',diameter = 80, color = 'darkgreen', backface = 'red',stroke = 4) %\u003e% \n            zfont_font(id= 'font') %\u003e% \n            zfont_text(zfont = 'font', text = \"YES\",color = 'white',textAlign = 'center',fontSize = 30,translate = c(z = 20, y = 10),stroke = 2) %\u003e% \n            zfont_text(zfont = 'font', text = \"NO\",color = 'white',textAlign = 'center',fontSize = 30,rotate = c(y = pi),translate = c(z = -20, y = 10),stroke = 2)\n        \n    })\n    \n    output$textOut = renderText({\n        \n        if(!is.null(input$check)){\n            if(is.null(input$check$animations$ease) || input$check$animations$ease == 0){\n                animation_ease_in(id = 'ease',rdog = 'illo',frames = 60,radiansPerCycle = tau/2,addTo='illo',framesPerCycle = 60,power = 3)\n            }\n            paste0(\"x:\",input$check$x,' y:', input$check$y, \" object:\", input$check$objectId, \" #:\", input$check$objectNo) \n        }\n    })\n}\nshinyApp(ui = ui, server = server)\n```\n![](man/figures/toggle.gif)\n\n## Anatomy of an animation\n\nBulk of animating is handled by javascript. If you want to write your custom animations, you'll also want to use\njavascript even though it is technically possible to handle it in R by using `update_property`. Handling animations\nthrough javascript will be lighter on the R session and offload that job to the browser.\n\nBelow is the R code for `animation_rotate`\n\n```r\nanimation_rotate = function(rdog = NULL,\n                            id = NULL,\n                            addTo = NULL,\n                            frames = Inf,\n                            rotate = c(x = 0, y = 0, z = 0)){\n\n    # standard processing of rdog inputs.\n    c(addTo,id,illoId) %\u003c-% process_id_inputs(rdog, addTo, id)\n\n    # in javascript, Infinity is Inf\n    if(is.infinite(frames)){\n        frames = 'Infinity'\n    }\n    \n    # preparing the input arguments\n    coords = c('x','y','z')\n    rotate[coords[!coords %in% names(rotate)]] = 0\n    \n    # create the animation script. this will run the javascript code for the animation function\n    animationScript = glue::glue(\n        '\n        Rdog_variables.built_in.animation_rotate(\"\u003cid\u003e\",\"\u003caddTo\u003e\",\"\u003cilloId\u003e\",\u003cframes\u003e,\u003crotate[\"x\"]\u003e,\u003crotate[\"y\"]\u003e,\u003crotate[\"z\"]\u003e);\n        ',.open = '\u003c',.close = '\u003e')\n\n\n    # standard processing of rdog outputs \n    if('htmlwidget' %in% class(rdog)){\n        rdog$x$jsCode %\u003c\u003e% paste0('\\n',animationScript)\n        return(rdog)\n    } else if(is.character(rdog)){\n        if(shiny::isRunning()){\n            shinyjs::runjs(animationScript)\n        }\n        animationScript\n    }\n}\n```\n\nMost of this can be copied and re-used for another animation function as it \nis standardized. You need to change the input variables needed for your animation\nfunction and that is about it. The javascript end is a little more complicated.\n\n\n```js\nRdog_variables.built_in.animation_rotate = function(id, add_to, illo_id, frames, x, y, z){\n    // standard set-up\n    Rdog_variables.utils.set_up_vars(id, add_to, illo_id);\n\n    Rdog_variables.animFuns[id] = function(){\n\n        if(Rdog_variables.utils.terminationCheck(id, frames)){\n            return;\n        }\n        frames -= 1;\n\n        window[add_to].rotate.x += x;\n        window[add_to].rotate.y += y;\n        window[add_to].rotate.z += z;\n\n\n        window[illo_id].updateRenderGraph();\n        requestAnimationFrame( Rdog_variables.animFuns[id] );\n    };\n    Rdog_variables.animFuns[id]();\n\n};\n```\n\nIn the code above `Rdog_variables.utils.set_up_vars` is a utility function that sets up some memory\nfor the animation and tries to re-create the animation state if the animation is re-called.\nThis is needed because changes to rdog objects in reactive shiny settings will re-create the whole\nillustration, resetting rotation and translation states. After that a function is defined to deal with\nthe animation frame by frame. Each function should start with\n\n```js\nif(Rdog_variables.utils.terminationCheck(id, frames)){\n  return;\n}\nframes -= 1;\n```\n\nThe if clause checks whether or not animation should be terminated. An animation\nloop is terminated if `frames` reaches 0 or another animation with the same ID is launched.\nThis prevents more animation loops to be created whenever there is an update to a illustration\nin a reactive context, progressively increasing animation speed.\n\n\nThe next lines apply a single frame of animation to the target object\n```js\nwindow[add_to].rotate.x += x;\nwindow[add_to].rotate.y += y;\nwindow[add_to].rotate.z += z;\n```\n\n\nThe next two lines create the infinite loop for the animation. They should be at the end of\nevery animation function\n\n```js\nwindow[illo_id].updateRenderGraph();\nrequestAnimationFrame( Rdog_variables.animFuns[id] );\n```\n\nAnd finally the function is called to start the animation loop\n\n```js\nRdog_variables.animFuns[id]();\n```\n\nCurrently all existing animations work by counting frames which may cause objects to have variable speeds in \ndifferent machines . I will be adding options to control the\ntime passed instead of frame rates. If you want to do this yourself, you need to decrement time instead of frames and pass the remaining time to `Rdog_variables.utils.terminationCheck` \ninstead of frames. You should ensure that your animation has reached it's final stage \nbefore the time counter reaches 0, for instance if a full radian turn is\nrequested, the turn should complete before termination. A possible way to do this is manually setting the object properties to the expected result \nif the timer becomes negative. Still thinking of the best way to implement that\nmyself.\n\n## Notes\n\n### Scoping \n\nCare should be taken when naming objects. Currently every object gets a variable under `window`. Good for easy manipulation, not great for collusions and stuff. If you name any element `Zdog` for instance whole thing\ngoes down so you should make sure that you are not colliding with any existing javascript object.\n\n### Using colors\n\nWhen naming colors, you can use named R colors (`red`) or 6 character hexes (`#FF0000`, or `#FF0000FF` with transparency), 3 digit hexes (`#F00` or `#F00F` with transparency) should also work for most cases but there are\ngaps in my javascript that doesn't account for them. Expect things to break if using\nmore complicated functions like `stl_to_shape`\n\n## Todo\n\n- Time based animations instead of frame count based ones. \n- A basic framework for custom animations\n- Return more information about the state of the canvas in shiny\n- Framework for custom draggers\n- A way to keep the `window` cleaner.\n\n\n## Rdog hex logo\n\nRdog hex logo is also made in rdog.\n\n\n```{r rdog}\n# create polygon lines\nr = 120\n1:6 %\u003e% lapply(function(i){\n    x = r * cos(2*pi*i/6)\n    y = r * sin(2*pi*i/6)\n    return(c(x = x ,y = y))\n}) -\u003e polyEdges\n\nillustration(width = 250,height = 250,dragRotate = TRUE) %\u003e%\n  # i use individual shapes instead of a polygon because polygon edges do not \n  # z fight correctly and appear below the dog head on a sideway view.\n  # shape_polygon(id = 'hex',sides = 6, stroke = 10,radius = 120,fill = FALSE,color = '#636') %\u003e%\n  anchor(id = 'hexAnchor',rotate = c(z=tau/12)) %\u003e% # rotate the hex to appear the right angle\n  shape_shape(id = 'hex1',addTo = 'hexAnchor',stroke = 10,color = '#636',path = polyEdges[1:2]) %\u003e%\n  shape_shape(id = 'hex2',addTo = 'hexAnchor',stroke = 10,color = '#636',path = polyEdges[2:3]) %\u003e%\n  shape_shape(id = 'hex3',addTo = 'hexAnchor',stroke = 10,color = '#636',path = polyEdges[3:4]) %\u003e%\n  shape_shape(id = 'hex3',addTo = 'hexAnchor',stroke = 10,color = '#636',path = polyEdges[4:5]) %\u003e%\n  shape_shape(id = 'hex4',addTo = 'hexAnchor',stroke = 10,color = '#636',path = polyEdges[5:6]) %\u003e%\n  shape_shape(id = 'hex4',addTo = 'hexAnchor',stroke = 10,color = '#636',path = polyEdges[c(6,1)]) %\u003e%\n  # create an anchor for the dog head and shit it up and move it to the front a little.\n  # create a second anchor to rotate a little. if I added the rotation to the previous \n  # anchor, it would be rotating over the origin point.\n  anchor(id='dogMainAnchor', translate =  c(y = -10,z = 50)) %\u003e%\n  anchor(id = 'dogAnchor',addTo = 'dogMainAnchor',translate = c(x = -10),rotate = c(y = 1*pi/16)) %\u003e%\n  # dogs nose\n  shape_box(id = 'headFront',\n            addTo = 'dogAnchor',\n            width = 40, depth = 20,height =25,\n            stroke = 10,leftFace = '#EA0', rightFace = '#EA0', color = '#E62',\n            translate = c(z = -10, y = 5)) %\u003e%\n  # dogs head\n  shape_box(id = 'headBack',\n            addTo = 'dogAnchor',\n            width = 50, depth = 40,height =60,\n            stroke = 10,leftFace = '#EA0', rightFace = '#EA0', color = '#E62',\n            translate = c(z = -50)) %\u003e%\n  # create a group for the eye to control their z-fighting\n  group(id ='eyeGroup',addTo='dogAnchor') %\u003e%\n  shape_shape(id = 'leftEye', addTo = 'eyeGroup',color= '#636',\n              translate = c(y = -20, z= -20, x = -20),stroke = 10) %\u003e%\n  # copy the leftEye to create the right eye\n  copy(id = 'rightEye',what = 'leftEye', translate = c(y = -20, z = -20, x = 20)) %\u003e% \n  # a boopable snoot\n  shape_ellipse(id = 'nose', addTo = 'dogAnchor',color = '#636f',\n                quarters = 2, translate = c(z = 10) ,diameter = 20,\n                stroke = 10,rotate = c(z = tau/4)) %\u003e%\n  # an anchor for ears\n  anchor(id = 'mainEarAnchor',addTo = 'dogAnchor',\n         translate = c(y = -20, z = -45)) %\u003e%\n  # an anchor for left ear to control poisition and rotation\n  anchor(id = 'leftEarAnchor', addTo = 'mainEarAnchor', \n         translate = c(x = -40),rotate = c(z = tau/16)) %\u003e%\n  # create left ear\n  shape_ellipse(id = 'leftEar', addTo = 'leftEarAnchor', color = '#636', quarters = 2,\n                diameter = 40,\n                stroke = 10, rotate = c(y = -tau/4, x = -tau/16, z = -tau/16)) %\u003e%\n  # copy the leftEarAnchor and the ear itself and flip it around to make the right ear\n  copy_graph(id = 'rightEarAnchor', what = 'leftEarAnchor', translate = c(x = 40),\n            rotate = c(z = -tau/16)) %\u003e% \n  # code for the tongue\n  anchor(id = 'tongueAnchor',addTo = 'dogAnchor',translate = c(z = -20,y = 25)) %\u003e%\n  shape_shape(addTo = 'tongueAnchor', id = 'tongue',stroke = 10, color = '#636',\n              path = list(c(y = 0,x = 10,z = 0),\n                          c(y = 0, x = -10,z = 0),\n                          c(y = 25, x = -10, z = 0),\n                          arc = list(c(y = 35, x = 0, z = 0),\n                                     c(y = 25, x = 10, z = 0))),\n              rotate= c(x = tau/8)) %\u003e%\n  # add the font and the text\n  zfont_font(id = 'font') %\u003e%\n  zfont_text(zfont = 'font',text = 'rdog',color = '#E62',\n             stroke = 2,fontSize = 35,translate = c(y = 85),\n             textAlign = 'center') %\u003e% \n  save_image()\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foganm%2Frdog","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Foganm%2Frdog","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foganm%2Frdog/lists"}