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
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
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
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θ
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
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
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/2 - $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 ? 1 : 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'>∞</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']?>&vp=<?=$_SESSION['vp']?>&vd=<?=$_SESSION['vd']?>&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/2 - $arc*2;
break;
case 2: $mid = $circumf/2 - $arc;
break;
case 3: $mid = $circumf/2;
break;
case 4: $mid = $circumf/2 + $arc;
break;
case 5: $mid = $circumf/2 + $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/2 / ($mugdiam/2 + $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/2 * (1 + sin($theta));
$dx = $mugdiam/2 * sin($theta);
$cf = $mugdiam/2 * (1 - 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/2 * (1 - sin($theta));
$dx = $mugdiam/2 * sin($theta);
$cf = $mugdiam/2 * (1 - 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)/$scale, imagesy($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;
}