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
startTime
stopTime
cycleInterval
loop
/>
Events out:
isActive_changed
time_changed
fraction_changed
=
=
=
=
=
'TRUE'
0.0
0.0
1.0
'FALSE'
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
Output Format
xyz
OrientationInterpolator
xyza
ColorInterpolator
rgb
ScalarInterpolator
i
All have this form:
<PositionInterpolator
key
= ''
keyValue
= ''
/>
Event in:
set_fraction
Event out:
value_changed
positioninterpolator.x3d
PositionInterpolator
Example
<Scene DEF='scene'>
<Group>
<Transform DEF='Cube'>
<Shape>
<Appearance>
<Material/>
</Appearance>
<Box size='1 1 1'/>
</Shape>
</Transform>
<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.x3d
OrientationInterpolator
Example
<Scene DEF='scene'>
<Group>
<Transform DEF='Column'>
<Shape>
<Appearance>
<Material/>
</Appearance>
<Cylinder height='1' radius='0.2'/>
</Shape>
</Transform>
<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)
<Scene DEF='scene'>
<Group>
<Shape>
<Appearance DEF='White'>
<Material/>
</Appearance>
<Sphere/>
</Shape>
<Transform DEF='Planet1' center='-2 0 0' translation='2 0 0'>
<Shape>
<Appearance USE='White'/>
<Sphere radius='0.2'/>
</Shape>
</Transform>
<Transform DEF='Planet2' center='-3 0 0' translation='3 0 0'>
<Shape>
<Appearance USE='White'/>
<Sphere radius='0.3'/>
</Shape>
</Transform>
<Transform DEF='Planet3' center='-4 0 0' translation='4 0 0'>
<Shape>
<Appearance USE='White'/>
<Sphere radius='0.5'/>
</Shape>
</Transform>
planets.x3d
Multiple Animated
Transforms (2/2)
<TimeSensor DEF='Clock1'
<TimeSensor DEF='Clock2'
<TimeSensor DEF='Clock3'
<OrientationInterpolator
<OrientationInterpolator
<OrientationInterpolator
</Group>
<ROUTE
<ROUTE
<ROUTE
<ROUTE
<ROUTE
<ROUTE
</Scene>
planets.x3d
cycleInterval='2' loop='true'/>
cycleInterval='3.5' loop='true'/>
cycleInterval='5' loop='true'/>
DEF='PlanetPath1' key='0 0.5 1' keyValue='0 0 1 0, 0 0 1 3.14, 0 0 1 6.28'/>
DEF='PlanetPath2' key='0 0.5 1' keyValue='0 0 1 0, 0 0 1 3.14, 0 0 1 6.28'/>
DEF='PlanetPath3' key='0 0.5 1' keyValue='0 0 1 0, 0 0 1 3.14, 0 0 1 6.28'/>
fromNode='Clock1' fromField='fraction_changed' toNode='PlanetPath1' toField='set_fraction'/>
fromNode='Clock2' fromField='fraction_changed' toNode='PlanetPath2' toField='set_fraction'/>
fromNode='Clock3' fromField='fraction_changed' toNode='PlanetPath3' toField='set_fraction'/>
fromNode='PlanetPath1' fromField='value_changed' toNode='Planet1' toField='set_rotation'/>
fromNode='PlanetPath2' fromField='value_changed' toNode='Planet2' toField='set_rotation'/>
fromNode='PlanetPath3' fromField='value_changed' toNode='Planet3' toField='set_rotation'/>
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'>
<Shape>
<Box/>
<Appearance DEF='App'>
<Material diffuseColor='0.8 0.4 0.2'/>
</Appearance>
</Shape>
</Transform>
<Transform translation='4.0 0.0 0.0'>
<Shape>
<Sphere/>
<Appearance USE='App'/>
</Shape>
</Transform>
<Transform translation='8.0 0.0 0.0'>
<Shape>
<Cylinder/>
<Appearance USE='App'/>
</Shape>
</Transform>
<Transform translation='12.0 0.0 0.0'>
<Shape>
<Cone/>
<Appearance USE='App'/>
</Shape>
</Transform>
<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
Description
CylinderSensor
Transforms mouse input into cylindrical motion (rotation)
PlaneSensor
Transforms mouse input into motion along the coordinate
system’s X-Y plane (translation)
SphereSensor
Transforms mouse input into spherical motion (rotation)
ProximitySensor
Detects when the user enters a region around the object
VisibilitySensor
Detects if an object is currently visible to the user
TouchSensor
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'>
<Shape>
<Appearance>
<Material/>
</Appearance>
<Box/>
</Shape>
</Transform>
<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'>
<Shape>
<Appearance>
<Material/>
</Appearance>
<Box/>
</Shape>
</Transform>
<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'>
<Shape>
<Appearance DEF='White'>
<Material/>
</Appearance>
<Box/>
</Shape>
</Transform>
<SphereSensor DEF='Shape1Sensor'/>
</Group>
<Group>
<Transform DEF='Shape2' translation='2.5 0 0'>
<Shape>
<Appearance USE='White'/>
<Cone/>
</Shape>
</Transform>
<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)
Group
PlaneSensor
“MoveLamp”
Transform
“Lamp”
Shape
Group
(Lamp Base)
(First Arm Joint)
<Scene DEF='scene'>
SphereSensor Transform
<Group>
“MoveFirstArm” “FirstArm”
<PlaneSensor DEF='MoveLamp'/>
Shape
Group
<Transform DEF='Lamp'>
(Lamp Arm)(2nd Arm Joint)
<Shape>
SphereSensor Transform
<Appearance DEF='White'>
“MoveSecondArm”
“SecondArm”
<Material/>
Shape
Group
</Appearance>
(2nd Lamp Arm)(Shade Joint)
<Cylinder height='0.01' radius='0.1'/>
SphereSensor Transform
</Shape>
“MoveLampShade”
“LampShade”
<Group>
<SphereSensor DEF='MoveFirstArm' offset='1 0 0 -0.7'/>
Shape
Transform
(Shade)
(LightBulb)
<Transform DEF='FirstArm' center='0 -0.15 0' rotation='1 0 0 -0.7' translation='0 0.15 0'>
<Shape DEF='LampArm'>
Shape
<Appearance USE='White'/>
(Bulb)
<Cylinder height='0.3' radius='0.01'/>
</Shape>
<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'>
<Shape>
<Appearance USE='White'/>
<Cone solid='false' bottomRadius='0.12' height='0.15' bottom='false'/>
</Shape>
<Transform translation='0 -0.05 0'>
<Shape>
<Appearance USE='White'/>
<Sphere radius='0.05'/>
</Shape>
desklamp.x3d
Nested Sensors: Desk Lamp (2/2)
</Transform>
Group
</Transform>
</Group>
</Transform>
PlaneSensor
Transform
</Group>
“MoveLamp”
“Lamp”
</Transform>
</Group>
</Transform>
Shape
Group
</Group>
(Lamp Base) (First Arm Joint)
<ROUTE fromNode='MoveFirstArm' fromField='rotation_changed'
toNode='FirstArm' toField='set_rotation'/>
<ROUTE fromNode='MoveLamp' fromField='translation_changed'
SphereSensor
Transform
toNode='Lamp' toField='set_translation'/>
“MoveFirstArm”
“FirstArm”
<ROUTE fromNode='MoveSecondArm'
fromField='rotation_changed' toNode='SecondArm'
Shape
Group
toField='set_rotation'/>
nd Arm Joint)
(Lamp
Arm)
(2
<ROUTE fromNode='MoveLampShade'
fromField='rotation_changed' toNode='LampShade'
toField='set_rotation'/>
SphereSensor
Transform
</Scene>
“MoveSecondArm”
“SecondArm”
Shape
(2nd Lamp Arm)
Group
(Shade Joint)
SphereSensor
“MoveLampShade”
Shape
(Shade)
Transform
“LampShade”
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'>
<Shape>
<Appearance>
<Material/>
</Appearance>
<Cylinder/>
</Shape>
</Transform>
</Transform>
<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 (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>
function draw(xoffset,yoffset) {
var canvas = document.getElementById("canvas");
var context = canvas.getContext('2d');
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);
}
}
}
// 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)”
© Copyright 2025 Paperzz