CT336/CT404 Graphics & Image Processing Animation and Interactivity (X3D, Canvas)
The X3D Event Model X3D provides an event model which allows certain events occurring to cause changes to objects. The events are generated by sensors : Time Sensors User-Input Sensors Visibility Sensors You define routes between pairs of nodes This allows the first node to send a message ('event') containing a value to the second node which then changes ones of its field values in response Complex animation/interactivity can be created by routing multiple nodes together
Keyframe Animation Keyframe animation is the technique by which you specify values at a number of key positions only The in between positions are automatically generated through interpolation ( tweening) Analogy with the approach used for cartoons This approach can be applied to any numerical value, and therefore allows animation of position, orientation, scale, colour, transparency, etc. e.g. to animate a car moving smoothly from left to right, only two keyframes are needed: one at a specified time is at the left, and another at a second specified time is at the right.
X3D Keyframe Animation The TimeSensor node is used to control animation: it generates events as time passes, and its eventouts can be routed for example to a transform node s eventins in order to make alterations periodically In most cases, a PositionInterpolator or OrientationInterpolator node sits between the TimeSensor node and the Transform node, and turns the simple event 'pulse' into a vector of the correct type for the eventin field type
The TimeSensor Node <TimeSensor enabled = 'TRUE' starttime = 0.0 stoptime = 0.0 cycleinterval = 1.0 loop = 'FALSE' /> Events out: isactive_changed time_changed fraction_changed The time eventout provides real-world Unix-style ( absolute ) time The starttime and stoptime fields define the absolute time at which a TimeSensor becomes enabled/disabled A TimeSensor also outputs fractional time values via the fraction_changed eventout, allowing animations to be controlled independently of the actual time/date Fractional time starts at 0.0 and runs towards 1.0. The cycleinterval field defines the number of seconds you want it to take fractional time to get from 0.0 to 1.0
The Interpolator Nodes Interpolator PositionInterpolator OrientationInterpolator ColorInterpolator ScalarInterpolator Output Format x y z x y z a r g b i All have this form: <PositionInterpolator key = '' keyvalue = '' /> Event in: set_fraction Event out: value_changed
PositionInterpolator Example positioninterpolator.x3d <Scene DEF='scene'> <Group> <Transform DEF='Cube'> <Appearance> <Material/> </Appearance> <Box size='1 1 1'/> <TimeSensor DEF='Clock' cycleinterval='4' loop='true'/> <PositionInterpolator DEF='CubePath' key='0 0.11 0.17 0.22 0.33 0.44 0.5 0.55 0.66 0.77 0.83 0.88 0.99' keyvalue='0 0 0, 1 1.96 1, 1.5 2.21 1.5, 2 1.96 2, 3 0 3, 2 1.96 3, 1.5 2.21 3, 1 1.96 3, 0 0 3, 0 1.96 2, 0 2.21 1.5, 0 1.96 1, 0 0 0'/> </Group> <ROUTE fromnode='clock' fromfield='fraction_changed' tonode='cubepath' tofield='set_fraction'/> <ROUTE fromnode='cubepath' fromfield='value_changed' tonode='cube' tofield='set_translation'/> </Scene> Note the ROUTE nodes at the end, which define data-flows between nodes when changes happen to specific fields
OrientationInterpolator Example orientationinterpolator.x3d <Scene DEF='scene'> <Group> <Transform DEF='Column'> <Appearance> <Material/> </Appearance> <Cylinder height='1' radius='0.2'/> <TimeSensor DEF='Clock' cycleinterval='4' loop='true'/> <OrientationInterpolator DEF='ColumnPath' key='0 0.5 1' keyvalue='0 0 1 0, 0 0 1 3.14, 0 0 1 6.28'/> </Group> <ROUTE fromnode='clock' fromfield='fraction_changed' tonode='columnpath' tofield='set_fraction'/> <ROUTE fromnode='columnpath' fromfield='value_changed' tonode='column' tofield='set_rotation'/> </Scene>
Multiple Animated Transforms (1/2) planets.x3d <Scene DEF='scene'> <Group> <Appearance DEF='White'> <Material/> </Appearance> <Sphere/> <Transform DEF='Planet1' center='-2 0 0' translation='2 0 0'> <Appearance USE='White'/> <Sphere radius='0.2'/> <Transform DEF='Planet2' center='-3 0 0' translation='3 0 0'> <Appearance USE='White'/> <Sphere radius='0.3'/> <Transform DEF='Planet3' center='-4 0 0' translation='4 0 0'> <Appearance USE='White'/> <Sphere radius='0.5'/>
Multiple Animated Transforms (2/2) planets.x3d <TimeSensor DEF='Clock1' cycleinterval='2' loop='true'/> <TimeSensor DEF='Clock2' cycleinterval='3.5' loop='true'/> <TimeSensor DEF='Clock3' cycleinterval='5' loop='true'/> <OrientationInterpolator DEF='PlanetPath1' key='0 0.5 1' keyvalue='0 0 1 0, 0 0 1 3.14, 0 0 1 6.28'/> <OrientationInterpolator DEF='PlanetPath2' key='0 0.5 1' keyvalue='0 0 1 0, 0 0 1 3.14, 0 0 1 6.28'/> <OrientationInterpolator DEF='PlanetPath3' key='0 0.5 1' keyvalue='0 0 1 0, 0 0 1 3.14, 0 0 1 6.28'/> </Group> <ROUTE fromnode='clock1' fromfield='fraction_changed' tonode='planetpath1' tofield='set_fraction'/> <ROUTE fromnode='clock2' fromfield='fraction_changed' tonode='planetpath2' tofield='set_fraction'/> <ROUTE fromnode='clock3' fromfield='fraction_changed' tonode='planetpath3' tofield='set_fraction'/> <ROUTE fromnode='planetpath1' fromfield='value_changed' tonode='planet1' tofield='set_rotation'/> <ROUTE fromnode='planetpath2' fromfield='value_changed' tonode='planet2' tofield='set_rotation'/> <ROUTE fromnode='planetpath3' fromfield='value_changed' tonode='planet3' tofield='set_rotation'/> </Scene>
Controlling the camera in X3D The Viewpoint node allows you to set multiple camera bookmark points for the user (the first one defined in your x3d file is the default) Their browser/renderer will provide a list of the defined Viewpoints that the user can pick from in order to switch the camera to a defined position/orientation/fov <Viewpoint description= 'Default Camera Pos' fieldofview='0.785' position='0 0 10' orientation='0 1 0 0' />
Animated camera example Note that the initial viewpoint is not the animated one! (animatedcamera.x3d) <Scene> <Transform translation='0.0 0.0 0.0'> <Box/> <Appearance DEF='App'> <Material diffusecolor='0.8 0.4 0.2'/> </Appearance> <Transform translation='4.0 0.0 0.0'> <Sphere/> <Appearance USE='App'/> <Transform translation='8.0 0.0 0.0'> <Cylinder/> <Appearance USE='App'/> <Transform translation='12.0 0.0 0.0'> <Cone/> <Appearance USE='App'/> <Viewpoint description='default Camera Pos' fieldofview='0.785' position='0 0 10' orientation='0 1 0 0' /> <Viewpoint DEF='CameraViewpoint' description='camera Path' fieldofview='0.785' position='6 0 10' orientation='0 1 0 0' /> <TimeSensor DEF='Clock' cycleinterval='20' loop='true'/> <PositionInterpolator DEF='CamPath' key='0.0 0.25 0.5 0.75 1.0' keyvalue='6 0 10, 6 0-5, 15 0-5, 15 0 10, 6 0 10'/> <OrientationInterpolator DEF='CamRotPath' key='0.0 0.25 0.5 0.75 1.0' keyvalue='0 1 0 0, 0 1 0 3.14, 0 1 0 1.57, 0 1 0 0, 0 1 0 0'/> <ROUTE fromnode='clock' fromfield='fraction_changed' tonode='campath' tofield='set_fraction'/> <ROUTE fromnode='campath' fromfield='value_changed' tonode='cameraviewpoint' tofield='set_position'/> <ROUTE fromnode='clock' fromfield='fraction_changed' tonode='camrotpath' tofield='set_fraction'/> <ROUTE fromnode='camrotpath' fromfield='value_changed' tonode='cameraviewpoint' tofield='set_orientation'/> </Scene>
Interactivity: The Sensor Nodes Sensor CylinderSensor PlaneSensor SphereSensor ProximitySensor VisibilitySensor TouchSensor Description Transforms mouse input into cylindrical motion (rotation) Transforms mouse input into motion along the coordinate system s X-Y plane (translation) Transforms mouse input into spherical motion (rotation) Detects when the user enters a region around the object Detects if an object is currently visible to the user Detects when an object is clicked Sensors are added to a group of children nodes within a Transform or Group: the siblings of the sensor define the geometry to which it applies.
TouchSensor Example <Scene DEF='scene'> <Group> <Transform DEF='Cube'> <Appearance> <Material/> </Appearance> <Box/> <TouchSensor DEF='Touch'/> <TimeSensor DEF='Clock' cycleinterval='4'/> <OrientationInterpolator DEF='CubePath' key='0 0.5 1' keyvalue='0 1 0 0, 0 1 0 3.14, 0 1 0 6.28'/> </Group> <ROUTE fromnode='clock' fromfield='fraction_changed' tonode='cubepath' tofield='set_fraction'/> <ROUTE fromnode='touch' fromfield='touchtime' tonode='clock' tofield='set_starttime'/> <ROUTE fromnode='cubepath' fromfield='value_changed' tonode='cube' tofield='set_rotation'/> </Scene>
PlaneSensor Example <Scene DEF='scene'> <Group> <Transform DEF='Cube'> <Appearance> <Material/> </Appearance> <Box/> <PlaneSensor DEF='Sensor'/> </Group> <ROUTE fromnode='sensor' fromfield='translation_changed' tonode='cube' tofield='set_translation'/> </Scene> Note that this is the x/y plane of the coordinate system in which the PlaneSensor is located, rather than the camera s x/y plane You can see this if you move the camera around to the side of the cube
SphereSensor Example <Scene DEF='scene'> <Group> <Group> <Transform DEF='Shape1'> <Appearance DEF='White'> <Material/> </Appearance> <Box/> <SphereSensor DEF='Shape1Sensor'/> </Group> <Group> <Transform DEF='Shape2' translation='2.5 0 0'> <Appearance USE='White'/> <Cone/> <SphereSensor DEF='Shape2Sensor'/> </Group> </Group> <ROUTE fromnode='shape1sensor' fromfield='rotation_changed' tonode='shape1' tofield='set_rotation'/> <ROUTE fromnode='shape2sensor' fromfield='rotation_changed' tonode='shape2' tofield='set_rotation'/> </Scene>
Nested Sensors: An Adjustable and Moveable Desk Lamp (1/2) <Scene DEF='scene'> <Group> <PlaneSensor DEF='MoveLamp'/> <Transform DEF='Lamp'> <Appearance DEF='White'> <Material/> </Appearance> <Cylinder height='0.01' radius='0.1'/> <Group> <SphereSensor DEF='MoveFirstArm' offset='1 0 0-0.7'/> <Transform DEF='FirstArm' center='0-0.15 0' rotation='1 0 0-0.7' translation='0 0.15 0'> <Shape DEF='LampArm'> <Appearance USE='White'/> <Cylinder height='0.3' radius='0.01'/> <Group> <SphereSensor DEF='MoveSecondArm' offset='1 0 0 1.9'/> <Transform DEF='SecondArm' center='0-0.15 0' rotation='1 0 0 1.9' translation='0 0.3 0'> <Shape USE='LampArm'/> <Group> <SphereSensor DEF='MoveLampShade' offset='1 0 0-1.25'/> <Transform DEF='LampShade' center='0 0.075 0' rotation='1 0 0-1.25' translation='0 0.075 0'> <Appearance USE='White'/> <Cone solid='false' bottomradius='0.12' height='0.15' bottom='false'/> <Transform translation='0-0.05 0'> <Appearance USE='White'/> <Sphere radius='0.05'/> PlaneSensor MoveLamp Group Transform Lamp Shape Group (Lamp Base) (First Arm Joint) SphereSensor MoveFirstArm Transform FirstArm Shape Group (Lamp Arm) (2 nd Arm Joint) SphereSensor Transform MoveSecondArm SecondArm Shape Group (2 nd Lamp Arm) (Shade Joint) SphereSensor Transform MoveLampShade LampShade Shape (Shade) desklamp.x3d Transform (LightBulb) Shape (Bulb)
Nested Sensors: Desk Lamp (2/2) </Group> </Group> PlaneSensor MoveLamp </Group> </Group> <ROUTE fromnode='movefirstarm' fromfield='rotation_changed' tonode='firstarm' tofield='set_rotation'/> <ROUTE fromnode='movelamp' fromfield='translation_changed' tonode='lamp' tofield='set_translation'/> <ROUTE fromnode='movesecondarm' fromfield='rotation_changed' tonode='secondarm' tofield='set_rotation'/> <ROUTE fromnode='movelampshade' fromfield='rotation_changed' tonode='lampshade' tofield='set_rotation'/> </Scene> Group Shape (Lamp Base) Transform Lamp SphereSensor MoveFirstArm Group (First Arm Joint) Shape (Lamp Arm) Transform FirstArm SphereSensor MoveSecondArm Group (2 nd Arm Joint) Transform SecondArm Shape (2 nd Lamp Arm) Group (Shade Joint) SphereSensor MoveLampShade Transform LampShade Shape (Shade) Transform (LightBulb) Shape (Bulb)
VisibilitySensor The VisibilitySensor generates an event whenever it becomes visible or invisible from the current viewpoint Typical usage: so you can ensure that the only scripts running are those attached to objects that are actually visible. Example: two TimeSensors are used to move a Cylinder: one gives it a large motion and one gives it a small motion. A VisibilitySensor is used to disable the small-motion TimeSensor when the object is out of view See next slide
VisibilitySensor Example <Scene DEF='scene'> <Transform DEF='T1'> <VisibilitySensor DEF='VS' size='1.6 4.6 1.6'/> <Transform DEF='T2'> <Appearance> <Material/> </Appearance> <Cylinder/> <TimeSensor DEF='TS1' cycleinterval='50' loop='true'/> <PositionInterpolator DEF='PI1' key='0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1' keyvalue='0 0 30, -10 5 20, -20 0 10, -30 5 10, -20 7 20, -10 4 10, 0 6 20, 20 4 0, 30 2 20, 10 0 20, 0 0-30'/> <TimeSensor DEF='TS2' cycleinterval='5' loop='true'/> <PositionInterpolator DEF='PI2' key='0 0.2 0.4 0.6 0.8 1' keyvalue='0 0 0, 0 1 0, 0 2 0, 0 3 0, 0 1.8 0, 0 0 0'/> <OrientationInterpolator DEF='OI' key='0 0.33 0.66 1' keyvalue='1 0 0, 0 1 0, 0 2.09 1, 0 0 4.19, 1 0 0 0'/> <Viewpoint DEF='V' description='initial View' position='0 1.6 15'/> <ROUTE fromnode='ts1' fromfield='fraction_changed' tonode='pi1' tofield='set_fraction'/> <ROUTE fromnode='pi1' fromfield='value_changed' tonode='t1' tofield='set_translation'/> <ROUTE fromnode='ts2' fromfield='fraction_changed' tonode='pi2' tofield='set_fraction'/> <ROUTE fromnode='ts2' fromfield='fraction_changed' tonode='oi' tofield='set_fraction'/> <ROUTE fromnode='pi2' fromfield='value_changed' tonode='t2' tofield='set_translation'/> <ROUTE fromnode='oi' fromfield='value_changed' tonode='t2' tofield='set_rotation'/> <ROUTE fromnode='vs' fromfield='isactive' tonode='ts2' tofield='set_enabled'/> </Scene>
Animation & Interactivity in Canvas 2D (with JavaScript) Handling the keyboard Recognise keypresses and update graphics in response Handling the mouse Recognise mouse click/drag and update graphics in response Time-based animation Update graphics irrespective of user s actions => much better for any kind of multimedia/animation/games
Keyboard handling (Canvas/JavaScript) canvaswithkeyboardexample.html <html> <head> <script> function attachevents() { document.onkeypress = function(event) { var xoffset=10*parseint(string.fromcharcode(event.keycode event.charcode)); draw(xoffset); function draw(xoffset) { var canvas = document.getelementbyid("canvas"); var context = canvas.getcontext('2d'); // remove previous translation if any context.save(); // over-write previous content, with a white rectangle context.fillstyle="#ffffff"; context.fillrect(0,0,300,300); // translate based on numerical keypress context.translate(xoffset,0); // purple rectangle context.fillstyle="#cc00ff"; context.fillrect(0,0,50,50); context.restore(); </script> </head> <body onload="attachevents();"> <canvas id="canvas" width="300" height="300"></canvas> </body> </html>
Mouse Handling (Canvas/JavaScript) canvaswithmouseexample.html <html> <head> <script> var ismousedown=false; function attachevents() { document.onmousedown = function(event) { ismousedown=true; draw(event.clientx, event.clienty); document.onmouseup = function(event) { ismousedown=false; document.onmousemove = function(event) { if ( ismousedown ) { draw(event.clientx, event.clienty); function draw(xoffset,yoffset) { var canvas = document.getelementbyid("canvas"); var context = canvas.getcontext('2d'); // remove previous translation if any context.save(); // over-write previous content, with a grey rectangle context.fillstyle="#dddddd"; context.fillrect(0,0,600,600); // translate based on position of mouseclick/drag context.translate(xoffset,yoffset); // purple rectangle context.fillstyle="#cc00ff"; context.fillrect(-25,-25,50,50); // centred on coord system context.restore(); </script> </head> <body onload="attachevents(); draw(0,0);"> <canvas id="canvas" width="600" height="600"></canvas> </body> </html>
Time-based animation using window.settimeout canvasanimationexample1.html <html> <head> <script> var x=0, y=0; var dx=4, dy=5; function draw() { var canvas = document.getelementbyid("canvas"); var context = canvas.getcontext('2d'); // remove previous translation if any context.save(); // over-write previous content, with a grey rectangle context.fillstyle="#dddddd"; context.fillrect(0,0,600,600); // perform movement, and translate to position x+=dx; y+=dy; if (x<=0) dx=4; else if (x>=550) dx=-4; if (y<=0) dy=5; else if (y>=550) dy=-5; context.translate(x,y); // purple rectangle context.fillstyle="#cc00ff"; context.fillrect(0,0,50,50); context.restore(); // do it all again in 1/30th of a second window.settimeout(draw, 1000/30); </script> </head> <body onload="draw();"> <canvas id="canvas" width="600" height="600"></canvas> </body> </html>
Another Canvas timedanimation example canvasanimationexample2.html <html> <head> <script> var boxes=new Array(); function attachevents() { document.onmousedown = function(event) { // adds a new box at the mouse position // step 1: find a spare index in the sparse array var idx=math.floor(math.random()*1000); while (typeof boxes[idx]!="undefined") idx=math.floor(math.random()*1000); // step 2: create a new box object and add to the array // setting up its data properties boxes[idx]=new Object(); boxes[idx].x = event.clientx; boxes[idx].y = event.clienty; var r = Math.floor(Math.random()*256); var g = Math.floor(Math.random()*256); var b = Math.floor(Math.random()*256); boxes[idx].colr = "rgb("+r+","+g+","+b+")"; boxes[idx].dy = Math.floor(1+Math.random()*8); function draw() { var canvas = document.getelementbyid("canvas"); var context = canvas.getcontext('2d'); // over-write previous content, with a grey rectangle context.fillstyle="#dddddd"; context.fillrect(0,0,600,600); // iterate thru the objects in our sparse array // the for..in construct obtains *indices* rather than *data values* for (var idx in boxes) { var y=boxes[idx].y+boxes[idx].dy; // animate box downwards if (y<600) { context.save(); boxes[idx].y=y; context.translate(boxes[idx].x, y); context.fillstyle=boxes[idx].colr; context.fillrect(0,0,20,20); context.restore(); else delete boxes[idx]; // box has passed offscreen so delete it from array // do it all again in 1/30th of a second window.settimeout(draw, 1000/30); </script> </head> <body onload="attachevents(); draw();"> <canvas id="canvas" width="600" height="600"></canvas> </body> </html> e.g. rgb(200,130,120)
Canvas2D drag-drop demo See canvasclickanddrag.pdf
Canvas2D Asteroids Game.. (incomplete) see canvasasteroids.pdf