Tutorials

IMAGE WRAPPING

This tutorial will show how to give images the appearance of being wrapped around a cylindrical object, such as a mug. As with all 3D effects, the viewing distance makes a big difference to the results

In figure 1 the whole of the image (A2 to B2) can be seen from point I2, but if the viewing distance is small (I1) then much of the image is hidden by the curvature of the object so only A1 to B1 is visible.

Figure 1
viewable image portions @copy; Barry Andrew 2016)
You only draw the visible portion of the image.

The Drawing Process

In the previous exercise we were dealing with simple linear perspective. Now we are dealing with curves, the mathematics does get a little more complicated. I find it easiest to start at the centre of the image and work outwards, plotting the pixels until they would become hidden by the curvature. In our mug example, the cylinder is vertical so the positions of the x coordinates are distorted as we traverse around the surface.

X Coordinates

Starting at D we take each column of pixels in the image. As shown in figure 2, for each pixel column C along the image there is a sequence of calculations required to find the x position on the screen.

Figure 2

the drawing process © Barry Andrew 2016

Once you have the angle θ then you can find where the x position of C would be on the mug. Then, as in the previous tutorial, you can find where it would appear on the screen from the eye's viewpoint by calculating the offset EF.

The final problem is calculating when to stop.

The point at which the pixels become hidden is where the line from the eye to the image on the mug (IC) becomes a tangent. At this point, the angle GCI is a right-angle.

Figure 3
calculating the end position © Barry Andrew 2016
From the radius of the mug, and the eye distance, we can calculate the cosine of the angle σ. As θ increases (fig. 2) during our drawing process we now know we must stop once the angle θ reaches σ.

Y Coordinates

To give extra 3D appearance, the mug in the sample application below is assumed to be viewed from a slightly raised elevation. The Y coordinates are then offset in an ellipse to follow the curve of the rim.

y offset = 20cosθ

mug view © Barry Andrew 2016

Experimental Application

This application lets you specify an image and then view how it would look when wrapped around a cylinder. You can change the viewing angle, the viewing distance and how far the image should wrap around the cylinder (as a percentage). Try it (images "rings.jpg" and "flag.jpg" are available)

It is better to use an oversize image which needs scaling down. The resampling when resizing the image gives a smoother anti-aliased effect. If no image is specified, or if the specified file cannot be opened for some reason, then a test image, comprising an ellipse and two diagonal lines, will be generated

generated test image © Barry Andrew 2016

The application has two files. The first is for the form (shown below) which lets you enter the parameters and the second (imgwrap.php) uses the PHP GD library to create the image, mainipulating the image's pixels to give the wrapped effect.

Screenshot
application screenshot © Barry Andrew 2016

formwrap.php


<?php
session_start
();
$defaults = [
                
'vp'    => 3,            // view position
                
'vd'    => 2000,         // view distance
                
'pc'    => 50            // wrap percentage
            
];

foreach (
$defaults as $dn => $dv) {
    if (isset(
$_GET[$dn]) && ctype_digit($_GET[$dn])) {
        
$_SESSION[$dn] = $_GET[$dn];
    }
    elseif (!isset(
$_SESSION[$dn])) {
        
$_SESSION[$dn] = $dv;
    }
}
if (isset(
$_GET['image'])) {
    
$_SESSION['image'] = trim($_GET['image']);
}
elseif (!isset(
$_SESSION['image'])) {
    
$_SESSION['image'] = '';
}
if (!
$_SESSION['image'] || !file_exists($_SESSION['image'])) {
    
$_SESSION['image'] = '';
}

// wiew position image
$cx 55$cy 5$r 50;
$circs='';
for (
$i=180$j=1$i>=0$i-=45$j++) {
    
$a deg2rad($i);
    
$x $cx $r*cos($a);
    
$y $cy $r*sin($a);
    
$cclass $j==$_SESSION['vp'] ? 'ccirc' 'circ';
    
$circs .= "<circle cx='$x' cy='$y' r=4 class='$cclass' data-alpha='$j' />\n";
}

// distance labels
$dists '';
$label=0;
$y 70;
for (
$i=1$i<=4$i++) {
    
$ty $y 5;
    
$dists .= "<path d='M 50 $y h 10' stroke='#000' stroke-width='1' />\n";
    
$dists .= "<text x='48' y='$ty' text-anchor='end' class='txt'>$label</text>\n";
    
$y += 100;
    
$label += 1000;
}

$cdist $_SESSION['vd']/10;
$infinid 'inf';
if (
$cdist 367) {
    
$cdist=367;
    
$infinid 'infselected';
}
$dists .= "<rect x='53' y='70' width='4' height='$cdist' id='curdist' />\n";

// wrap percent image
$r 30;
$alpha M_PI * (100 $_SESSION['pc']) / 100;
$beta1 $alpha;
$beta2 = -3*M_PI/$alpha;
$wx1 35 $r cos($alpha);
$wy1 35 $r sin($alpha);
$wx2 35 $r cos(-$alpha);
$wy2 35 $r sin(-$alpha);
$long $_SESSION['pc'] > 50 0;
$wrappc_image "<svg width='70' height='70'>
            <circle cx='35' cy='35' r='
$r' fill='none' stroke='#fff' stroke-width='4' />
            <path d='M 
$wx1 $wy1 A $r $r 1 $long 1 $wx2 $wy2' fill='none' stroke='#369' stroke-width='4' transform='rotate(-90, 35,35)'/>
            <text x='35' y='28' text-anchor='middle' class='smalltxt'>Wrap</text>
            <text x='35' y='48' text-anchor='middle' class='txt'>
{$_SESSION['pc']}%</text>
            </svg>\n"
;
?>
<html>
<head>
<meta name="generator" content="PhpED 14.0 (Build 14039, 64bit)">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Image wrapping</title>
<meta name="author" content="Barry Andrew">
<style type='text/css'>
div {
    font-family: sans-serif;
    font-size: 10pt;
}
.circ {
    fill: white;
    stroke: black;
}
.ccirc {
    fill: red;
    stroke: black;
}
.circ:hover {
    fill: red;
    stroke: red;
    cursor: pointer;
}
#wrapper {
    max-width: 650px;
    padding: 10px;
    border: 1px solid white;
    margin-left: auto;
    margin-right: auto;
}
#cpanel {
    width: 110px;
    float: left;
}
#idist {
    cursor: pointer;
    fill: #888;
    fill-opacity: 0.5;
    stroke: #ccc;
    stroke-width: 1;
}
#curdist {
    fill: #f00;
}
.smalltxt {
    font-family: sans-serif;
    font-size: 8pt;
    fill: '#aaa';
}
.txt {
    font-family: sans-serif;
    font-size: 10pt;
}
.infintxt {
    font-family: sans-serif;
    font-size: 14pt;
}
#inf {
    fill: #ccc;
    fill-opacity: 0.2;
    stroke: #ccc;
    stroke-width: 1;
    cursor: pointer;
}
#infselected {
    fill: #c00;
    fill-opacity: 0.8;
    stroke: #ccc;
    stroke-width: 1;
    cursor: pointer;
}
div#form {
    margin-left: 20px;
}
#innerwrap {
    float: left;
    margin-left: auto;
    margin-right: auto;
}
#imagewrapper {
    width: 450px;
    height: 600px;
    margin-top: 20px;
    margin-left: auto;
    margin-right: auto;
    padding: 0 24px;
}
fieldset {
    float: left;
    height: 70px;
    background-color: #ccc;
}
input {
    font-size: 10pt;
}
</style>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<script type='text/javascript'>
    $().ready(function() {
        $("#idist").click(function(e) {
            var d = e.offsetY * 10;
            location.href="?vd="+d;
        })
        $("#inf").click(function(e) {
            location.href="?vd=10000000";
        })
        $(".circ").click(function(e) {
            var p = $(this).data("alpha");
            location.href="?vp="+p;
        })
    })
</script>
</head>
<body>
<div id='wrapper'>
<div id='cpanel'>
  <svg width='110' height='460' >
    <linearGradient id="grad" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="0" y2="460">
            <stop offset="0.15" style="stop-color:#fff"/>
            <stop offset="1.0" style="stop-color:#369"/>
    </linearGradient>    
    <rect x='0' y='0' width='110' height='460' id='main' fill="url(#grad)" />
    <g stroke='#000' stroke-width='2' fill='#fff' >
        <path d='M 5 5 a 50 50 0 0 0 100 0' />
        <?=$circs?>
    </g>
    <text class='smalltxt' x='55' y='16' text-anchor='middle'>View</text>
    <text class='smalltxt' x='55' y='30' text-anchor='middle'>direction</text>
    <?=$dists?>
    <text text-anchor='middle' class='txt' transform="translate(85,240) rotate(-90)">View distance</text>
    <rect x='52' y='70' width='6' height='370' id='idist'/>
    <text class='infintxt' x='55' y='455' text-anchor='middle' fill='#fff'>&infin;</text>
    <rect x='45' y='440' width='20' height='16' id='<?=$infinid?>' />
  </svg>
</div>

<div id='innerwrap'>
    <div id='form'>
    <form>
    <fieldset>
    Image path <input type="text" name="image" size="40" value="<?=$_SESSION['image']?>" >
    <br><br>
    Percentage wrap<input type="text" name="pc"  size="5" value='<?=$_SESSION['pc']?>'> 
    <input type="submit" name="btnsub" value="Refresh" style="float: right">
    </fieldset>
    <fieldset>
    <?=$wrappc_image?>
    </fieldset>
    <div style='clear:both;'></div>
    </form>
    </div>
    
    <div id='imagewrapper'>
        <img src="imgwrap.php?im=<?=$_SESSION['image']?>&amp;vp=<?=$_SESSION['vp']?>&amp;vd=<?=$_SESSION['vd']?>&amp;pc=<?=$_SESSION['pc']?>"/>
    </div>
</div>
<div style='clear:both;'></div>

</div>
</body>
</html>

imgwrap.php


<?php
$image 
= (isset($_GET['im'])) ? $_GET['im'] : '';

$idist = isset($_GET['vd'])?$_GET['vd']:2000;
$vp = isset($_GET['vp'])?$_GET['vp']:3;
$pc = isset($_GET['pc'])?$_GET['pc']:50;

if (
$pc 10 $pc 10;
if (
$pc 90 $pc 90;

$itype 0;
if (
$image && file_exists($image)) {
    list(
$oiw$oih$itype) = getimagesize($image);
}

switch (
$itype) {
    case 
2$orig imagecreatefromjpeg($image);
            break;
    case 
3$orig imagecreatefrompng($image);
            break;
    default: 
            
$oiw 800*M_PI;
            
$oih  1200;
            
$orig defaultImage($oiw$oih);
}

$circumf $oiw 100 $pc;
$mugdiam $circumf $pc / (M_PI 40);
$istart = ($circumf-$oiw)/2;
$iend $istart $oiw 1;
$arc $circumf/8;              // 45 degree arc


switch ($vp) {
    case 
1$mid $circumf/$arc*2
            break;
    case 
2$mid $circumf/$arc
            break;
    case 
3$mid $circumf/2
            break;
    case 
4$mid $circumf/$arc
            break;
    case 
5$mid $circumf/$arc*2
            break;
}
$llim $mid $arc*2;
$rlim $mid $arc*2;

$im imagecreatetruecolor($mugdiam$oih+40);
$bg imagecolorallocate($im,0xff,0xff,0xff);
$gy imagecolorallocate($im,0xcc,0xcc,0xff);
imagefill($im,0,0,$bg);

// TRANSFER IMAGE
$ytop 20;
$sigma acos($mugdiam// ($mugdiam/$idist));

// RIGHT HAND SIDE
for ($ox=$mid$ox<$rlim$ox++) {
    if ((
$ox $istart) || ($ox $iend)) continue;
    
$ox1 $ox $mid;
    
$theta $ox1/$mugdiam*2;
    
    if (
$theta $sigma) break;
    
    
$x $mugdiam/* (sin($theta));
    
    
$dx $mugdiam/sin($theta);
    
$cf $mugdiam/* (cos($theta));
    
$ef $cf $dx / ($cf $idist);
    
$x -= $ef;
    
    
$y ceil($ytop+20*cos($theta));
    
imagecopy($im,$orig,$x,$y,$ox-$istart,0,1,$oih);
}
// LEFT HAND SIDE
for ($ox=$mid$ox>=$llim$ox--) {
    if ((
$ox $istart) || ($ox $iend)) continue;
    
$ox1 $mid $ox;
    
$theta $ox1/$mugdiam*2;
    
    if (
$theta $sigma) break;
    
    
$x $mugdiam/* (sin($theta));
    
    
$dx $mugdiam/sin($theta);
    
$cf $mugdiam/* (cos($theta));
    
$ef $cf $dx / ($cf $idist);
    
$x += $ef;
    
    
$y ceil($ytop+20*cos($theta));
    
imagecopyresized($im,$orig,$x,$y,$ox-$istart,0,1,$oih,1,$oih);
}

$scale imagesx($im)/400;

$im2 imagecreatetruecolor(imagesx($im)/$scaleimagesy($im)/$scale);
$bg2=imagecolorallocate($im2,0xff,0xff,0xff);
imagefill($im2,0,0,$bg2);
imagecopyresampled($im2$im,0,0,0,0,imagesx($im2),imagesy($im2),imagesx($im),imagesy($im));
$trans imagecolorat($im2,0,0);
imagecolortransparent($im2,$trans);

header("Content-type: image/png");
imagepng($im2);
imagedestroy($im);
imagedestroy($im2);
imagedestroy($orig);

function 
defaultImage($w,$h)
{
    
$im imagecreatetruecolor($w,$h);
    
$black=imagecolorallocate($im,0,0,0);
    
$bg=imagecolorallocate($im,0xff,0xff,0xff);
    
imagefill($im,0,0,$bg);
    
imagefilledellipse($im,$w/2,$h/2,$w,$h,$black);
    
imagefilledellipse($im,$w/2,$h/2,$w-100,$h-100,$bg);
    
imagesetthickness($im,50);
    
imageline($im,0,0,$w,$h,$black);
    
imageline($im,0,$h,$w,0,$black);
    return 
$im;
}