Vizard 7 » Tutorials & Examples » Example scripts » Demos » Web Browser
7.7

HTML WebSocket

\examples\html\htmlWebSocket.py

Shows how to use the vizhtml module to communicate with a remote browser.

"""
This script demonstrates how to use WebSockets with Vizard.
WebSockets allow bidirectional communication with external browsers.
This example displays an overhead view of the world with the current
view position overlayed. The browser client can click on the map
to move the viewpoint and can disorient the user and send messages.

Enter the following URL in your browser to communicate with the script:
{}
"""
import viz
import vizact
import vizcam
import vizmat
import vizdlg
import viztask
import vizhtml

import base64
import random

viz.setMultiSample(8)
viz.fov(60)
viz.go()

# Add environment
model = viz.addChild('maze.osgb')

# Initialize walk navigation
vizcam.WalkNavigate()

# Base 64 encoded png buffer of overhead image
overheadImage = None

# Overhead view range
ORTHO_RANGE = [-13,13]

# Overhead image size
IMAGE_SIZE = [512,512]

def CreateOverHeadImageTask():
    global overheadImage

    # Render overhead scene to texture
    texture = viz.addRenderTexture()
    cam = viz.addRenderNode(size=IMAGE_SIZE)
    cam.setInheritView(False)
    cam.setRenderTexture(texture)
    cam.setPosition([0,5,0])
    cam.setEuler([0,90,0])
    min_ortho, max_ortho = ORTHO_RANGE
    cam.setProjectionMatrix(viz.Matrix.ortho(min_ortho,max_ortho,min_ortho,max_ortho,0.1,100))
    cam.setAutoClip(False)

    # Wait a frame to allow texture to render
    yield None

    # Save texture to buffer base64 encoded png buffer
    overheadImage = base64.b64encode(texture.saveToBuffer('.png'))

    # Send image to existing clients
    vizhtml.sendAll('set_image',viz.Data(data=overheadImage))

    # Remove resources
    cam.remove()
    texture.remove()

viztask.schedule( CreateOverHeadImageTask() )

def PixelToWorld(pos):
    """Converts overhead image pixel coordinates to world coordinates"""
    px, py = pos
    x = vizmat.Interpolate(ORTHO_RANGE[0],ORTHO_RANGE[1],viz.clamp(float(px)/IMAGE_SIZE[0],0.0,1.0))
    y = vizmat.Interpolate(ORTHO_RANGE[0],ORTHO_RANGE[1],viz.clamp(float(py)/IMAGE_SIZE[1],0.0,1.0))
    return [x,y]

def WorldToPixel(pos):
    """Converts world coordinates to overhead image pixel coordinates"""
    x,= pos
    px = viz.clamp(vizmat.InverseInterpolate(ORTHO_RANGE[0],ORTHO_RANGE[1],x),0.0,1.0)
    py = viz.clamp(vizmat.InverseInterpolate(ORTHO_RANGE[0],ORTHO_RANGE[1],y),0.0,1.0)
    px = int(round(px*(IMAGE_SIZE[0]-1)))
    py = int(round(py*(IMAGE_SIZE[1]-1)))
    return [px,py]

def ClientConnect(e):
    """Send overhead image when new client connects"""
    if overheadImage is not None:
        e.client.send('set_image',viz.Data(data=overheadImage))
vizhtml.onConnect(ClientConnect)

def ClientSetView(e):
    """Set view position to specified overhead image pixel coordinates"""

    # Animate view to new position
    x,= PixelToWorld(e.data.pos)
    viz.MainView.runAction(vizact.moveTo([x,1.8,y],time=0.5,interpolate=vizact.easeInOutStrong))

vizhtml.onMessage('set_view',ClientSetView)

def ClientDisorient(e):
    """Disorient user by spinning around by random amount"""
    amount = random.uniform(1000.0,2000.0)
    euler = viz.MainView.getEuler()
    yaw_spin = vizact.mix(euler[0],euler[0]+amount,time=1.0,interpolate=vizact.easeOutStrong)
    viz.MainView.runAction(vizact.call(viz.MainView.setEuler,yaw_spin,euler[1],euler[2]),pool=1)

vizhtml.onMessage('disorient',ClientDisorient)

# Create panel for displaying messages
messagePanel = vizdlg.Panel(layout=vizdlg.LAYOUT_VERT_RIGHT,align=viz.ALIGN_RIGHT_TOP,border=False,background=False)
messagePanel.visible(False)
viz.link(viz.MainWindow.RightTop,messagePanel,offset=(-10,-10,0))

def DisplayMessageTask(message):
    """Displays a message for a limited amount of time"""

    # Add message to panel
    text = vizdlg.Panel()
    text.addItem(viz.addText(message))
    messagePanel.addItem(text)
    messagePanel.visible(True)

    # Wait for a few seconds
    yield viztask.waitTime(5.0)

    # Remove message
    messagePanel.removeItem(text)
    if not messagePanel.getItems():
        messagePanel.visible(False)

def ClientTextMessage(e):
    if e.data.message:
        viztask.schedule(DisplayMessageTask(e.data.message))
vizhtml.onMessage('send_message',ClientTextMessage)

def SendView():
    """Send current view position to all connected clients"""

    # Get position in pixel units
    x,y,= viz.MainView.getPosition()
    pos = WorldToPixel([x,z])

    # Get rotation
    rotation = int(round(viz.MainView.getEuler()[0]))

    vizhtml.sendAll('set_view',viz.Data(pos=pos,rotation=rotation))

vizact.ontimer(0,SendView)

# HTML code
code = """
<!DOCTYPE html>
<html>
<head>
    <title>vizhtml WebSocket Example</title>
    <script type="text/javascript" src="jquery.js"></script>
    <script type="text/javascript" src="vizhtml.js"></script>
    <style type="text/css">
    .overlay
    {
        position : absolute;
        visibility : hidden;
        z-index : 2;
    }
    </style>
</head>
<body>
<script language="javascript">

$(document).ready(function() {
    if(viz.isWebSocketSupported()) {

        var socket = new viz.WebSocket();

        socket.onevent('open', function(e){
            document.getElementById('status').innerHTML = 'connected';
            $('#input_controls :input').prop('disabled', false);
        })

        socket.onevent('close', function(e){
            document.getElementById('status').innerHTML = 'waiting for connection...';
            $('#input_controls :input').prop('disabled', true);
            document.getElementById('crosshair').style.visibility = 'hidden';
            socket.reconnect();
        })

        socket.onevent('set_image', function(e){
            document.getElementById('screen').src = 'data:image/png;base64,' + e.data.data;
        })

        socket.onevent('set_view', function(e){
            var screen = document.getElementById('screen');
            var crosshair = document.getElementById('crosshair');
            var pos_x = screen.offsetLeft + e.data.pos[0] - (crosshair.width / 2.0);
            var pos_y = screen.offsetTop + (screen.height - e.data.pos[1] - 1) - (crosshair.height / 2.0);
            crosshair.style.left = pos_x + "px";
            crosshair.style.top = pos_y + "px";
            crosshair.style.webkitTransform = "rotate(" + e.data.rotation + "deg)";
            crosshair.style.MozTransform = "rotate(" + e.data.rotation + "deg)";
            crosshair.style.transform = "rotate(" + e.data.rotation + "deg)";
            crosshair.style.visibility = 'visible';
        })

        $("#disorient").click(function() {
            socket.send( 'disorient' , viz.Data({}) );
        });

        function sendMessage()
        {
            var message = document.getElementById('message');
            socket.send('send_message', viz.Data({'message':message.value}) );
            message.value = "";
        }

        $("#send_message").click(function() {
            sendMessage();
        });

        $("#message").keyup(function(event) {
            if(event.keyCode == 13) {
                sendMessage();
            }
        });

        $("#screen").click(function(e) {
            if(socket.isConnected()) {
                var screen = document.getElementById('screen');
                var pos_x = e.offsetX?(e.offsetX):e.pageX-screen.offsetLeft;
                var pos_y = e.offsetY?(e.offsetY):e.pageY-screen.offsetTop;
                pos_y = screen.height - pos_y - 1;
                socket.send( 'set_view' , viz.Data({'pos':[pos_x,pos_y]}) );
            }
        });

    } else {
        $('#content').html('<h3>Your browser does not support WebSockets</h3>');
    }
});

</script>
<div id="content">
    <div>Status:</div><div id='status'>waiting for connection</div></br>
    <div>Screen (click on image to move user):</div>
    <img id="screen" ondragstart="return false;" /></br>
    <img id="crosshair" src="images/red_arrow.png" class="overlay" />
    </br>
    <div id='input_controls'>
        <input id='disorient' type=button value="Disorient User"></br>
        </br>
        Message:</br>
        <input id="message" type="text" value=""> <input id="send_message" type="button" value="Send">
    </div>
</div>
</body>
</html>
"""

# http://localhost:8080/vizhtml/websocket
vizhtml.registerCode('websocket',code, directory='')

# Insert URL in doc string and display in info panel
import vizinfo
vizinfo.InfoPanel(__doc__.format(vizhtml.getServerURL('websocket')))