Task
Draw a rotating cube.

It should be oriented with one vertex pointing straight up, and its opposite vertex on the main diagonal (the one farthest away) straight down. It can be solid or wire-frame, and you can use ASCII art if your language doesn't have graphical capabilities. Perspective is optional.




Related tasks
 Draw a cuboid
 write language name in 3D ASCII


--
-- demo\rosetta\DrawRotatingCube.exw
-- =================================
--
-- credits: http://petercollingridge.appspot.com/3D-tutorial/rotating-objects
--          https://github.com/ssloy/tinyrenderer/wiki/Lesson-4:-Perspective-projection
--
-- Aside: low CPU usage, at least when using a 30ms timer (33 FPS, which is plenty).
--
with javascript_semantics
include pGUI.e

constant title = "Draw a Rotating Cube"
Ihandle dlg, canvas
cdCanvas cd_canvas

--
-- First, define 8 corners equidistant from {0,0,0}:
--
--          6-----2
--      5-----1   3
--      8-----4  
--
-- ie the right face is 1-2-3-4 clockwise, and the left face
--  is 5-6-7-8 counter-clockwise (unless using x-ray vision).
-- (since this is not drawing textures, clockwise-ness does 
--  not matter, as shown by the corrected orange face, but
--  it will if you (figure out how to) apply any textures.)
-- (a quick (online) study of opengl texture documentation
--  should convince you that stuff is best left to opengl.)
--
enum X, Y, Z
constant l = 100
constant corners = {{+l,+l,+l},     -- 1 (front top right)
                    {+l,+l,-l},     -- 2 (back top "right")
                    {+l,-l,-l},     -- 3 (back btm "right")
                    {+l,-l,+l},     -- 4 (front btm right)
                    {-l,+l,+l},     -- 5 (front top left)
                    {-l,+l,-l},     -- 6 (back top "left")
                    {-l,-l,-l},     -- 7 (back btm "left")
                    {-l,-l,+l}}     -- 8 (front btm left)
-- I put left/right in quotes for the back face as a reminder
-- those match the above diagram, but of course they would be
-- swapped were you looking "at" the face/rotated it by 180.

constant faces = {{CD_RED,          1,2,3,4},   -- right
                  {CD_YELLOW,       1,5,6,2},   -- top
                  {CD_DARK_GREEN,   1,4,8,5},   -- front
                  {CD_BLUE,         2,3,7,6},   -- back
                  {CD_WHITE,        3,4,8,7},   -- bottom
--                {CD_ORANGE,       5,6,7,8}}   -- left
                  {CD_ORANGE,       8,7,6,5}}   -- left

-- rotation angles, 0..359, on a timer
atom rx = 45,   -- initially makes cube like a H
     ry = 35,   --     "	   "    "	italic H
     rz = 0

constant naxes = {{Y,Z},    -- (rotate about the X-axis)
                  {X,Z},    -- (rotate about the Y-axis)
                  {X,Y}}    -- (rotate about the Z-axis)

function rotate(sequence points, atom angle, integer axis)
--
-- rotate points by the specified angle about the given axis
--
    atom radians = angle*CD_DEG2RAD,
         sin_t = sin(radians),
         cos_t = cos(radians)
    integer {nx,ny} = naxes[axis]
    for i=1 to length(points) do
        atom x = points[i][nx],
             y = points[i][ny]
        points[i][nx] = x*cos_t - y*sin_t
        points[i][ny] = y*cos_t + x*sin_t
    end for
    return points
end function

function projection(sequence points, atom d)
--
-- project points from {0,0,d} onto the perpendicular plane through {0,0,0}
--
    for i=1 to length(points) do
        atom {x,y,z} = points[i],
             denom = (1-z/d)
        points[i][X] = x/denom
        points[i][Y] = y/denom
    end for
    return points
end function

function nearest(sequence points)
--
-- return the index of the nearest point (highest z value)
--
    return largest(vslice(points,Z),true)
end function

procedure draw_cube(integer cx, cy)
    -- {cx,cy} is the centre point of the canvas
    sequence points = deep_copy(corners)
    points = rotate(points,rx,X)
    points = rotate(points,ry,Y)
    points = rotate(points,rz,Z)
    points = projection(points,1000)
    integer np = nearest(points)
    --
    -- find the three faces that contain the nearest point,
    -- then for each of those faces let diag be the point
    -- that is diagonally opposite said nearest point, and
    -- order by/draw those faces furthest diag away first.
    --  (one or two of them may be completely obscured due 
    --   to the effects of the perspective projection.)
    --  (you could of course draw all six faces, as long as
    --   the 3 furthest are draw first/obliterated, which
    --   is what that commented-out "else" would achieve.)
    --
    sequence faceset = {}
    for i=1 to length(faces) do
        sequence fi = faces[i]
        integer k = find(np,fi)       -- k:=2..5, or 0
        if k then
            integer diag = mod(k,4)+2 -- {2,3,4,5} --> {4,5,2,3}
                                      -- aka swap 2<=>4 & 3<=>5
            diag = fi[diag] -- 1..8, diagonally opp. np
            faceset = append(faceset,{points[diag][Z],i})
--      else
--          faceset = append(faceset,{-9999,i})
        end if
    end for
    faceset = sort(faceset)
    for i=1 to length(faceset) do
        sequence face = faces[faceset[i][2]]
        cdCanvasSetForeground(cd_canvas,face[1])
        -- first fill sides (with bresenham edges), then
        -- redraw edges, but anti-aliased aka smoother
        sequence modes = {CD_FILL,CD_CLOSED_LINES}
        for m=1 to length(modes) do
            cdCanvasBegin(cd_canvas,modes[m])
            for fdx=2 to 5 do
                sequence pt = points[face[fdx]]
                cdCanvasVertex(cd_canvas,cx+pt[X],cy-pt[Y])
            end for
            cdCanvasEnd(cd_canvas)
        end for
    end for
end procedure

function canvas_action_cb(Ihandle canvas)
    cdCanvasActivate(cd_canvas)
    cdCanvasClear(cd_canvas)
    integer {w, h} = IupGetIntInt(canvas, "DRAWSIZE")
    draw_cube(floor(w/2),floor(h/2))
    cdCanvasFlush(cd_canvas)
    return IUP_DEFAULT
end function

function canvas_map_cb(Ihandle canvas)
    IupGLMakeCurrent(canvas)
    if platform()=JS then
        cd_canvas = cdCreateCanvas(CD_IUP, canvas)
    else
        atom res = IupGetDouble(NULL, "SCREENDPI")/25.4
        cd_canvas = cdCreateCanvas(CD_GL, "10x10 %g", {res})
    end if
    cdCanvasSetBackground(cd_canvas, CD_PARCHMENT)
    return IUP_DEFAULT
end function

function canvas_resize_cb(Ihandle /*canvas*/)
    integer {canvas_width, canvas_height} = IupGetIntInt(canvas, "DRAWSIZE")
    atom res = IupGetDouble(NULL, "SCREENDPI")/25.4
    cdCanvasSetAttribute(cd_canvas, "SIZE", "%dx%d %g", {canvas_width, canvas_height, res})
    return IUP_DEFAULT
end function

function timer_cb(Ihandln /*ih*/)
    -- (feel free to add a bit more randomness here, maybe)
    rx = mod(rx+359,360)
    ry = mod(ry+359,360)
    rz = mod(rz+359,360)
    IupRedraw(canvas)
    return IUP_IGNORE
end function

procedure main()
    IupOpen()
    canvas = IupGLCanvas("RASTERSIZE=640x480")
    IupSetCallbacks(canvas, {"ACTION", Icallback("canvas_action_cb"),
                             "MAP_CB", Icallback("canvas_map_cb"),
                             "RESIZE_CB", Icallback("canvas_resize_cb")})
    dlg = IupDialog(canvas,`TITLE="%s"`,{title})
    IupShow(dlg)
    IupSetAttribute(canvas, "RASTERSIZE", NULL)
    Ihandle hTimer = IupTimer(Icallback("timer_cb"), 30)
    if platform()!=JS then
        IupMainLoop()
        IupClose()
    end if
end procedure

main()
