projected cube map shader, white line between edges - html

I'm trying to write a shader which renders a cube map / cube texture as an equirectangular projection.
The main part of this is done however I get white lines between the faces.
My methodology is:
Starting from UV ([0,1]x[0,1])
Transform to [-1,1]x[-1,1] and than to [-180,180]x[-90,90]
These are now long lat, which can be transformed into 3D (xyz)
Get the face they belong to, as well as their position within this face ([-1,1]x[-1,1])
Transform this face position to a UV within the cube texture
At first I thought the output of step 4 was wrong and that I was sampling from outside the texture, but even after multiplying the face coordinates by 1/2, I still get the white lines.
reference: https://codepen.io/coutteausam/pen/jOKKYYy
float max3(vec3 v) {
return max(max(v.x, v.y), v.z);
}
vec2 sample_cube_map_1(vec3 xyz, out float faceIndex) {
xyz /= length(xyz);
float m = max3(abs(xyz));
if (abs(xyz.x) == m) {
faceIndex = sign(xyz.x);
return xyz.yz / abs(xyz.x);
}
if (abs(xyz.y) == m) {
faceIndex = 2. * sign(xyz.y);
return xyz.xz / abs(xyz.y);
}
if (abs(xyz.z) == m) {
faceIndex = 3. * sign(xyz.z);
return xyz.xy / abs(xyz.z);
}
faceIndex = 1.0;
return vec2(0., 0.);
}
vec2 sample_cube_map(vec3 xyz) {
float face;
vec2 xy = sample_cube_map_1(xyz, face);
xy = (xy + 1.) / 2.; // [-1,1] -> [0,1]
xy.x = clamp(xy.x, 0., 1.);
xy.y = clamp(xy.y, 0., 1.);
if (face == 1.) {
// front
xy += vec2(1., 1.);
}
else if (face == -1.) {
//back
xy.x = 1. - xy.x;
xy += vec2(3., 1.);
}
else if (face == 2.) {
// right
xy.x = 1. - xy.x;
xy += vec2(2., 1.);
}
else if (face == -2.) {
// left
xy += vec2(0., 1.);
}
else if (face == 3.) {
// top
xy = vec2(xy.y, 1. - xy.x);
xy += vec2(1., 2.);
}
else if (face == -3.) {
// bottom
xy = xy.yx;
xy += vec2(1., 0.);
}
else {
xy += vec2(1., 0.);
}
return xy / vec2(4., 3.); // [0,4]x[0,3] -> [0,1]x[0,1]
}
// projects
// uv:([0,1] x [0,1])
// to
// xy:([ -2, 2 ] x [ -1, 1 ])
vec2 uv_2_xy(vec2 uv) {
return vec2(uv.x * 4. - 2., uv.y * 2. - 1.);
}
// projects
// xy:([ -2, 2 ] x [ -1, 1 ])
// to
// longlat: ([ -pi, pi ] x [-pi/2,pi/2])
vec2 xy_2_longlat(vec2 xy) {
float pi = 3.1415926535897932384626433832795;
return xy * pi / 2.;
}
vec3 longlat_2_xyz(vec2 longlat) {
return vec3(cos(longlat.x) * cos(longlat.y), sin(longlat.x) * cos(longlat.y), sin(longlat.y));
}
vec3 uv_2_xyz(vec2 uv) {
return longlat_2_xyz(xy_2_longlat(uv_2_xy(uv)));
}
vec3 roty(vec3 xyz, float alpha) {
return vec3(cos(alpha) * xyz.x + sin(alpha) * xyz.z, xyz.y, cos(alpha) * xyz.z - sin(alpha) * xyz.x);
}
varying vec2 vUv;
uniform sampler2D image;
uniform float time;
void main() {
vec3 xyz = uv_2_xyz(vUv);
xyz = roty(xyz, time);
vec2 uv = sample_cube_map(xyz);
vec4 texturePixel = texture2D(image, vec2(clamp(uv.x, 0., 1.), clamp(uv.y, 0., 1.)));
gl_FragColor = texturePixel;
}

The math behind your shader looks sound. However, you need to consider how texture sampling behaves when dealing with sub-pixels. Take a look at your source texture:
When you cross the boundary between Top to Right, your sampler will try to squeeze all the texture between magenta and teal into a single pixel. Since there's lots of white in that area, that squeezed pixel will be mostly white. Notice this doesn't happen between Top to Front, because there's no white area between those two faces. (Read: mipmapping to see how textures behave when scaled down)
Solutions:
You might be able to sample the nearest full pixel, instead of trying to blend in between them, by turning off mipmapping. To do so, you can change the texture's minification filter to linear filtering.
const imgTexture = new THREE.TextureLoader().load('https://i.imgur.com/tBzfYG5.png');
imgTexture.minFilter = THREE.LinearFilter;
The only downside is that you might get some hard edges along the boundaries.
If your project allows it, you could simply break up your texture into six images, then use the cubemap method offered by Three.js

Related

(Processing) Fuction that draws a ring with the radius I assign

I am trying to create a function (ring) that allows me to draw multiple rings around the black circle by only changing the radius, but even when I call it multiple times in draw() it only draws one. I have tried checking if there is something wrong that restricts it to one, but I can't find it. Thank you for your time :)
//VOID
//Set of written numbers
String numbers = "87237462835465598709986654374649";
PFont font;
float r = 40; // Radius of the circle of the written numbers
void setup()
{
size(640, 480);
font = loadFont("AgencyFB-Reg-48.vlw"); //Create futuristic font
textFont(font, 14);
smooth();
}
void draw()
{
background(255);
//Black circle (center of the void)
fill(0);
noStroke();
ellipse(550,80,r*2,r*2);
//FUNCTION for the numbers
ring(40);
}
void ring(float r) {
// Circle for the written numbers
translate(550, 80);
noFill();
noStroke();
ellipse(0, 0, r, r);
// We must keep track of our position along the curve
float arclength = 0;
// For every box
for (int i = 0; i < numbers.length(); i=i+1)
{
// Instead of a constant width, we check the width of each character.
char currentChar = numbers.charAt(i);
float w = textWidth(currentChar);
// Each box is centered so we move half the width
arclength += w/2;
// Angle in radians is the arclength divided by the radius
// Starting on the left side of the circle by adding PI
float theta = PI + arclength / r;
pushMatrix();
// Polar to cartesian coordinate conversion
translate(r*cos(theta), r*sin(theta));
// Rotate the box
rotate(theta+PI/2); // rotation is offset by 90 degrees
// Display the character
fill(0);
text(currentChar,0,0);
popMatrix();
// Move halfway again
arclength += w/2;
}
}
If you call ring() multiple times from draw() your code repeatedly calls translate(). Each one of these calls are additive, so the text just keeps getting further and further away. One way to stop this from happening is to reset it at the bottom of the ring function. This is done by using a reverse sign, e.g., translate(x,y) is changed to translate(-x,-y) at the end of the function to put it back where it was to start with. This change in code will allow you to see multiple rings of text.
float r = 40; // Radius of the circle of the written numbers
String numbers = "87237462835465598709986654374649";
void setup() {
size(400, 400);
smooth();
}
void draw() {
background(255);
fill(0);
noStroke();
ellipse(180, 180, r*2, r*2);
for(int x = 40; x < 100; x+=10){
ring(x);
}
}
void ring(float r) {
translate(180, 180); // Move numbers to location of circle
float arclength = 0;
for (int i = 0; i < numbers.length(); i=i+1) {
char currentChar = numbers.charAt(i);
float w = textWidth(currentChar);
arclength += w/2;
float theta = PI + arclength / r;
pushMatrix();
// Polar to cartesian coordinate conversion
translate(r*cos(theta), r*sin(theta));
// Rotate the box
rotate(theta+PI/2); // rotation is offset by 90 degrees
// Display the character
fill(0);
text(currentChar, 0, 0);
popMatrix();
// Move halfway again
arclength += w/2;
}
translate(-180, -180); // Move it back where it was to start
}

html5 - Get device orientation rotation in relative coordinate

I'm trying to get the change in orientation between two deviceorientation events along the left-right axis, and top-bottom axis, those axis being usually defined as the phone x and y axis (https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Orientation_and_motion_data_explained)
ie between instants t1 and t2 where those phone axis move from (x1, y1) to (x2, y2), It'd like to get (angle(x2-x1), angle(y1-y2)).
When the device is in portrait mode (in opposition to landscape mode), those axis seems to correspond to the beta and gamma. However when the phone is vertical (bottom facing the ground), the gamma value becomes extremely instable, and jumps from 90 to -90 degrees (at the same occasion, the alpha jumps by 180 degrees) You can easily see that here on your phone
I'd like to avoid that, and also get values in the 360 range. Here is what I have so far:
// assuming portrait mode
var beta0, gamma0;
window.addEventListener('deviceorientation', function(orientation) {
if (typeof beta0 === 'undefined') {
beta0 = beta;
gamma0 = gamma;
}
console.log('user has moved to the left by', gamma - gamma0, ' and to the top by', beta - beta0);
});
That works ok when the device is mostly horizontal, and not at all when it is vertical
All right. First, a simple explanation of the device orientation input:
The absolute coordinate system, (X, Y, Z) is such that X is East, Y is North and Z is up. The device relative coordinate system, (x, y, z) is such that x is right, y is top and z is up. Then the orientation angles, (alpha, beta, gamma) are the angles that describe the succession of three simple rotations that change (X, Y, Z) to (x, y, z) as so:
rotate around Z by alpha degrees, which transforms (X, Y, Z) to (X', Y', Z') with Z' = Z
rotate around X' by beta degrees, which transforms (X', Y', Z') to (X'', Y'', Z'') with X'' = X'
rotate around Y'' by gamma degrees, which transforms (X'', Y'', Z'') to (x, y, z) with y = Y''
(they are called intrinsic Tait-Bryan angles of type Z-X'-Y'')
Now we can get the corresponding rotation matrix by composing simple rotation matrix that each correspond to one of the three rotations.
[ cC 0 sC ] [ 1 0 0 ] [ cA -sA 0 ]
R(A, B, C) = Ry(C)*Rx(B)*Rz(A) = | 0 1 0 |*| 0 cB -sB |*[ sA cA 0 ]
[ -sC 0 cC ] [ 0 sB cB ] [ 0 0 1 ]
where A, B, C are short for alpha, beta, gamma and s, c for sin, cos.
Now, we are interested in the angles of the right-left (y axis) and top-down (x axis) rotations deltas between two positions (x, y, z) and (x', y', z') that correspond to the orientations (A, B, C) and (A', B', C')
The coordinates of (x', y', z') in term of (x, y, z) are given by R(A', B', C') * R(A, B, C)^-1 = R(A', B', C') * R(A, B, C)^T since the inverse is the transpose for orthogonal (rotation) matrix. Finally, if z' = p*x + q*y + r*z, the angle of those rotations are p around the right-left axis and q around the top-down one (this is true for small angles, which assume frequent orientation update, else asin(p) and asin(r) are closer from the truth)
So here is some javascript to get the rotation matrix:
/*
* gl-matrix is a nice library that handles rotation stuff efficiently
* The 3x3 matrix is a 9 element array
* such that indexes 0-2 correspond to the first column, 3-5 to the second column and 6-8 to the third
*/
import {mat3} from 'gl-matrix';
let _x, _y, _z;
let cX, cY, cZ, sX, sY, sZ;
/*
* return the rotation matrix corresponding to the orientation angles
*/
const fromOrientation = function(out, alpha, beta, gamma) {
_z = alpha;
_x = beta;
_y = gamma;
cX = Math.cos( _x );
cY = Math.cos( _y );
cZ = Math.cos( _z );
sX = Math.sin( _x );
sY = Math.sin( _y );
sZ = Math.sin( _z );
out[0] = cZ * cY + sZ * sX * sY, // row 1, col 1
out[1] = cX * sZ, // row 2, col 1
out[2] = - cZ * sY + sZ * sX * cY , // row 3, col 1
out[3] = - cY * sZ + cZ * sX * sY, // row 1, col 2
out[4] = cZ * cX, // row 2, col 2
out[5] = sZ * sY + cZ * cY * sX, // row 3, col 2
out[6] = cX * sY, // row 1, col 3
out[7] = - sX, // row 2, col 3
out[8] = cX * cY // row 3, col 3
};
and now we get the angular deltas:
const deg2rad = Math.PI / 180; // Degree-to-Radian conversion
let currentRotMat, previousRotMat, inverseMat, relativeRotationDelta,
totalRightAngularMovement=0, totalTopAngularMovement=0;
window.addEventListener('deviceorientation', ({alpha, beta, gamma}) => {
// init values if necessary
if (!previousRotMat) {
previousRotMat = mat3.create();
currentRotMat = mat3.create();
relativeRotationDelta = mat3.create();
fromOrientation(currentRotMat, alpha * deg2rad, beta * deg2rad, gamma * deg2rad);
}
// save last orientation
mat3.copy(previousRotMat, currentRotMat);
// get rotation in the previous orientation coordinate
fromOrientation(currentRotMat, alpha * deg2rad, beta * deg2rad, gamma * deg2rad);
mat3.transpose(inverseMat, previousRotMat); // for rotation matrix, inverse is transpose
mat3.multiply(relativeRotationDelta, currentRotMat, inverseMat);
// add the angular deltas to the cummulative rotation
totalRightAngularMovement += Math.asin(relativeRotationDelta[6]) / deg2rad;
totalTopAngularMovement += Math.asin(relativeRotationDelta[7]) / deg2rad;
}
Finally, to account for screen orientation, we have to replace
_z = alpha;
_x = beta;
_y = gamma;
by
const screen = window.screen;
const getScreenOrientation = () => {
const oriented = screen && (screen.orientation || screen.mozOrientation);
if (oriented) switch (oriented.type || oriented) {
case 'landscape-primary':
return 90;
case 'landscape-secondary':
return -90;
case 'portrait-secondary':
return 180;
case 'portrait-primary':
return 0;
}
return window.orientation|0; // defaults to zero if orientation is unsupported
};
const screenOrientation = getScreenOrientation();
_z = alpha;
if (screenOrientation === 90) {
_x = - gamma;
_y = beta;
}
else if (screenOrientation === -90) {
_x = gamma;
_y = - beta;
}
else if (screenOrientation === 180) {
_x = - beta;
_y = - gamma;
}
else if (screenOrientation === 0) {
_x = beta;
_y = gamma;
}
Note that the cumulative right-left and top-bottom angles will depend of the path chosen by the user, and cannot be infer directly from the device orientation but have to be tracked through the movement. You can arrive to the same position with different movements:
method 1:
keep your phone horizontal and rotate by 90 degrees clockwise. (this is neither a left-right nor a top-bottom rotation)
keep your phone in landscape mode and rotate by 90 toward you. (this is neither a 90 degrees left-right rotation)
keep your phone facing you and rotate by 90 so that it's up. (this is neither a 90 degrees left-right rotation)
method 2:
rotate the phone by 90 degrees so that it faces you and is vertical (this is a 90 degrees top-bottom rotation)

Efficiently XOR two images in Flash compile target

I need to XOR two BitmapData objects together.
I'm writing in Haxe, using the flash.* libraries and the AS3 compile target.
I've investigated HxSL and PixelBender, and neither one seems to have a bitwise XOR operator, nor do they have any other bitwise operators that could be used to create XOR (but am I missing something obvious? I'd accept any answer which gives a way to do a bitwise XOR using only the integer/float operators and functions available in HxSL or PixelBlender).
None of the predefined filters or shaders in Flash that I can find seem to be able to do a XOR of two images (but again, am I missing something obvious? Can XOR be done with a combination of other filters).
I can find nothing like a XOR drawmode for drawing things onto other things (but that doesn't mean it doesn't exist! That would work too, if it exists!)
The only way I can find at the moment is a pixel-by-pixel loop over the image, but this takes a couple of seconds per image even on a fast machine, as opposed to filters, which I use for my other image processing operations, which are about a hundred times faster.
Is there any faster method?
Edit:
Playing around with this a bit more I found that removing the conditional and extra Vector access in the loop speeds it up by about 100ms on my machine.
Here's the previous XOR loop:
// Original Vector XOR code:
for (var i: int = 0; i < len; i++) {
// XOR.
result[i] = vec1[i] ^ vec2[i];
if (ignoreAlpha) {
// Force alpha of FF so we can see the result.
result[i] |= 0xFF000000;
}
}
Here is the updated XOR loop for the Vector solution:
if (ignoreAlpha) {
// Force alpha of FF so we can see the result.
alphaMask = 0xFF000000;
}
// Fewer Vector accessors makes it quicker:
for (var i: int = 0; i < len; i++) {
// XOR.
result[i] = alphaMask | (vec1[i] ^ vec2[i]);
}
Answer:
Here are the solutions that I've tested to XOR two images in Flash.
I found that the PixelBender solution is about 6-10 slower than doing it in straight ActionScript.
I don't know if it's because I have a slow algorithm or it's just the limits of trying to fake bitwise operations in PixelBender.
Results:
PixelBender: ~6500ms
BitmapData.getVector(): ~480-500ms
BitmapData.getPixel32(): ~1200ms
BitmapData.getPixels(): ~1200ms
The clear winner is use BitmapData.getVector() and then XOR the two streams of pixel data.
1. PixelBender solution
This is how I implemented the bitwise XOR in PixelBender, based on the formula given on Wikipedia: http://en.wikipedia.org/wiki/Bitwise_operation#Mathematical_equivalents
Here is a Gist of the final PBK: https://gist.github.com/Coridyn/67a0ff75afaa0163f673
On my machine running an XOR on two 3200x1400 images this takes about 6500-6700ms.
I first converted the formula to JavaScript to check that it was correct:
// Do it for each RGBA channel.
// Each channel is assumed to be 8bits.
function XOR(x, y){
var result = 0;
var bitCount = 8; // log2(x) + 1
for (var n = 0; n < bitCount; n++) {
var pow2 = pow(2, n);
var x1 = mod(floor(x / pow2), 2);
var y1 = mod(floor(y / pow2), 2);
var z1 = mod(x1 + y1, 2);
result += pow2 * z1;
}
console.log('XOR(%s, %s) = %s', x, y, result);
console.log('%s ^ %s = %s', x, y, (x ^ y));
return result;
}
// Split out these functions so it's
// easier to convert to PixelBender.
function mod(x, y){
return x % y;
}
function pow(x, y){
return Math.pow(x, y);
}
function floor(x){
return Math.floor(x);
}
Confirm that it's correct:
// Test the manual XOR is correct.
XOR(255, 85); // 170
XOR(170, 85); // 255
XOR(170, 170); // 0
Then I converted the JavaScript to PixelBender by unrolling the loop using a series of macros:
// Bitwise algorithm was adapted from the "mathematical equivalents" formula on Wikipedia:
// http://en.wikipedia.org/wiki/Bitwise_operation#Mathematical_equivalents
// Macro for 2^n (it needs to be done a lot).
#define POW2(n) pow(2.0, n)
// Slight optimisation for the zeroth case - 2^0 = 1 is redundant so remove it.
#define XOR_i_0(x, y) ( mod( mod(floor(x), 2.0) + mod(floor(y), 2.0), 2.0 ) )
// Calculations for a given "iteration".
#define XOR_i(x, y, i) ( POW2(i) * ( mod( mod(floor(x / POW2(i)), 2.0) + mod(floor(y / POW2(i)), 2.0), 2.0 ) ) )
// Flash doesn't support loops.
// Unroll the loop by defining macros that call the next macro in the sequence.
// Adapted from: http://www.simppa.fi/blog/category/pixelbender/
// http://www.simppa.fi/source/LoopMacros2.pbk
#define XOR_0(x, y) XOR_i_0(x, y)
#define XOR_1(x, y) XOR_i(x, y, 1.0) + XOR_0(x, y)
#define XOR_2(x, y) XOR_i(x, y, 2.0) + XOR_1(x, y)
#define XOR_3(x, y) XOR_i(x, y, 3.0) + XOR_2(x, y)
#define XOR_4(x, y) XOR_i(x, y, 4.0) + XOR_3(x, y)
#define XOR_5(x, y) XOR_i(x, y, 5.0) + XOR_4(x, y)
#define XOR_6(x, y) XOR_i(x, y, 6.0) + XOR_5(x, y)
#define XOR_7(x, y) XOR_i(x, y, 7.0) + XOR_6(x, y)
// Entry point for XOR function.
// This will calculate the XOR the current pixels.
#define XOR(x, y) XOR_7(x, y)
// PixelBender uses floats from 0.0 to 1.0 to represent 0 to 255
// but the bitwise operations above work on ints.
// These macros convert between float and int values.
#define FLOAT_TO_INT(x) float(x) * 255.0
#define INT_TO_FLOAT(x) float(x) / 255.0
XOR for each channel of the current pixel in the evaluatePixel function:
void evaluatePixel()
{
// Acquire the pixel values from both images at the current location.
float4 frontPixel = sampleNearest(inputImage, outCoord());
float4 backPixel = sampleNearest(diffImage, outCoord());
// Set up the output variable - RGBA.
pixel4 result = pixel4(0.0, 0.0, 0.0, 1.0);
// XOR each channel.
result.r = INT_TO_FLOAT ( XOR(FLOAT_TO_INT(frontPixel.r), FLOAT_TO_INT(backPixel.r)) );
result.g = INT_TO_FLOAT ( XOR(FLOAT_TO_INT(frontPixel.g), FLOAT_TO_INT(backPixel.g)) );
result.b = INT_TO_FLOAT ( XOR(FLOAT_TO_INT(frontPixel.b), FLOAT_TO_INT(backPixel.b)) );
// Return the result for this pixel.
dst = result;
}
ActionScript Solutions
2. BitmapData.getVector()
I found the fastest solution is to extract a Vector of pixels from the two images and perform the XOR in ActionScript.
For the same two 3200x1400 this takes about 480-500ms.
package diff
{
import flash.display.Bitmap;
import flash.display.DisplayObject;
import flash.display.IBitmapDrawable;
import flash.display.BitmapData;
import flash.geom.Rectangle;
import flash.utils.ByteArray;
/**
* #author Coridyn
*/
public class BitDiff
{
/**
* Perform a binary diff between two images.
*
* Return the result as a Vector of uints (as used by BitmapData).
*
* #param image1
* #param image2
* #param ignoreAlpha
* #return
*/
public static function diffImages(image1: DisplayObject,
image2: DisplayObject,
ignoreAlpha: Boolean = true): Vector.<uint> {
// For simplicity get the smallest common width and height of the two images
// to perform the XOR.
var w: Number = Math.min(image1.width, image2.width);
var h: Number = Math.min(image1.height, image2.height);
var rect: Rectangle = new Rectangle(0, 0, w, h);
var vec1: Vector.<uint> = BitDiff.getVector(image1, rect);
var vec2: Vector.<uint> = BitDiff.getVector(image2, rect);
var resultVec: Vector.<uint> = BitDiff.diffVectors(vec1, vec2, ignoreAlpha);
return resultVec;
}
/**
* Extract a portion of an image as a Vector of uints.
*
* #param drawable
* #param rect
* #return
*/
public static function getVector(drawable: DisplayObject, rect: Rectangle): Vector.<uint> {
var data: BitmapData = BitDiff.getBitmapData(drawable);
var vec: Vector.<uint> = data.getVector(rect);
data.dispose();
return vec;
}
/**
* Perform a binary diff between two streams of pixel data.
*
* If `ignoreAlpha` is false then will not normalise the
* alpha to make sure the pixels are opaque.
*
* #param vec1
* #param vec2
* #param ignoreAlpha
* #return
*/
public static function diffVectors(vec1: Vector.<uint>,
vec2: Vector.<uint>,
ignoreAlpha: Boolean): Vector.<uint> {
var larger: Vector.<uint> = vec1;
if (vec1.length < vec2.length) {
larger = vec2;
}
var len: Number = Math.min(vec1.length, vec2.length),
result: Vector.<uint> = new Vector.<uint>(len, true);
var alphaMask = 0;
if (ignoreAlpha) {
// Force alpha of FF so we can see the result.
alphaMask = 0xFF000000;
}
// Assume same length.
for (var i: int = 0; i < len; i++) {
// XOR.
result[i] = alphaMask | (vec1[i] ^ vec2[i]);
}
if (vec1.length != vec2.length) {
// Splice the remaining items.
result = result.concat(larger.slice(len));
}
return result;
}
}
}
3. BitmapData.getPixel32()
Your current approach of looping over the BitmapData with BitmapData.getPixel32() gave a similar speed of about 1200ms:
for (var y: int = 0; y < h; y++) {
for (var x: int = 0; x < w; x++) {
sourcePixel = bd1.getPixel32(x, y);
resultPixel = sourcePixel ^ bd2.getPixel(x, y);
result.setPixel32(x, y, resultPixel);
}
}
4. BitmapData.getPixels()
My final test was to try iterating over two ByteArrays of pixel data (very similar to the Vector solution above). This implementation also took about 1200ms:
/**
* Extract a portion of an image as a Vector of uints.
*
* #param drawable
* #param rect
* #return
*/
public static function getByteArray(drawable: DisplayObject, rect: Rectangle): ByteArray {
var data: BitmapData = BitDiff.getBitmapData(drawable);
var pixels: ByteArray = data.getPixels(rect);
data.dispose();
return pixels;
}
/**
* Perform a binary diff between two streams of pixel data.
*
* If `ignoreAlpha` is false then will not normalise the
* alpha to make sure the pixels are opaque.
*
* #param ba1
* #param ba2
* #param ignoreAlpha
* #return
*/
public static function diffByteArrays(ba1: ByteArray,
ba2: ByteArray,
ignoreAlpha: Boolean): ByteArray {
// Reset position to start of array.
ba1.position = 0;
ba2.position = 0;
var larger: ByteArray = ba1;
if (ba1.bytesAvailable < ba2.bytesAvailable) {
larger = ba2;
}
var len: Number = Math.min(ba1.length / 4, ba2.length / 4),
result: ByteArray = new ByteArray();
// Assume same length.
var resultPixel:uint;
for (var i: uint = 0; i < len; i++) {
// XOR.
resultPixel = ba1.readUnsignedInt() ^ ba2.readUnsignedInt();
if (ignoreAlpha) {
// Force alpha of FF so we can see the result.
resultPixel |= 0xFF000000;
}
result.writeUnsignedInt(resultPixel);
}
// Seek back to the start.
result.position = 0;
return result;
}
There are a few possible options depending on what you want to achieve (e.g. is the XOR per channel or is it just any pixel that is non-black?).
There is the BitmapData.compare() method which can give you a lot of information about the two bitmaps. You could BitmapData.threshold() the input data before comparing.
Another option would be to use the draw method with the BlendMode.DIFFERENCE blend mode to draw your two images into the same BitmapData instance. That will show you the difference between the two images (equivalent to the Difference blending mode in Photoshop).
If you need to check if any pixel is non-black then you can try running a BitmapData.threshold first and then draw the result with the difference blend mode as above for the two images.
Are you doing this for image processing or something else like per-pixel hit detection?
To start with I'd have a look at BitmapData and see what is available to play with.

Canvas 3d graph

I have done a 3d graph to plot points in it. I have drawn x,y and z axis. Also you can rotate the axis by pressing arrow keys.Now my problem marking the axis as x,y and z .I tried to add text in canvas by using filltext.But the text gets added to canvas but its not rotating.It is because i have not set rotation effect for it i guess.So how can i set the rotation to text so that when axis rotates the text also rotates together.Below is my code.
<!DOCTYPE html>
<html>
<head>
<title>Canvas Surface Rotation</title>
<style>
body {
text-align: center;
}
canvas {
border: 1px solid black;
}
</style>
<script>
var constants = {
canvasWidth: 600, // In pixels.
canvasHeight: 600, // In pixels.
leftArrow: 37,
upArrow: 38,
rightArrow: 39,
downArrow: 40,
xMin: -10, // These four max/min values define a square on the xy-plane that the surface will be plotted over.
xMax: 10,
yMin: -10,
yMax: 10,
xDelta: 0.01, // Make smaller for more surface points.
yDelta: 0.01, // Make smaller for more surface points.
colorMap: ["#060"], // There are eleven possible "vertical" color values for the surface, based on the last row of http://www.cs.siena.edu/~lederman/truck/AdvanceDesignTrucks/html_color_chart.gif
pointWidth: 2, // The size of a rendered surface point (i.e., rectangle width and height) in pixels.
dTheta: 0.05, // The angle delta, in radians, by which to rotate the surface per key press.
surfaceScale: 24 // An empirically derived constant that makes the surface a good size for the given canvas size.
};
// These are constants too but I've removed them from the above constants literal to ease typing and improve clarity.
var X = 0;
var Y = 1;
var Z = 2;
// -----------------------------------------------------------------------------------------------------
var controlKeyPressed = false; // Shared between processKeyDown() and processKeyUp().
var surface = new Surface(); // A set of points (in vector format) representing the surface.
// -----------------------------------------------------------------------------------------------------
function point(x, y, z)
/*
Given a (x, y, z) surface point, returns the 3 x 1 vector form of the point.
*/
{
return [x, y, z]; // Return a 3 x 1 vector representing a traditional (x, y, z) surface point. This vector form eases matrix multiplication.
}
// -----------------------------------------------------------------------------------------------------
function Surface()
/*
A surface is a list of (x, y, z) points, in 3 x 1 vector format. This is a constructor function.
*/
{
this.points = []; // An array of surface points in vector format. That is, each element of this array is a 3 x 1 array, as in [ [x1, y1, z1], [x2, y2, z2], [x3, y3, z3], ... ]
}
// -----------------------------------------------------------------------------------------------------
Surface.prototype.equation = function(x, y)
/*
Given the point (x, y), returns the associated z-coordinate based on the provided surface equation, of the form z = f(x, y).
*/
{
var d = Math.sqrt(x*x + y*y); // The distance d of the xy-point from the z-axis.
return 4*(Math.sin(d) / d); // Return the z-coordinate for the point (x, y, z).
}
// -----------------------------------------------------------------------------------------------------
Surface.prototype.generate = function()
/*
Creates a list of (x, y, z) points (in 3 x 1 vector format) representing the surface.
*/
{
var i = 0;
for (var x = constants.xMin; x <= constants.xMax; x += constants.xDelta)
{
for (var y = constants.yMin; y <= constants.yMax; y += constants.yDelta)
{
this.points[i] = point(x, y, this.equation(x, y)); // Store a surface point (in vector format) into the list of surface points.
++i;
}
}
}
// -----------------------------------------------------------------------------------------------------
Surface.prototype.color = function()
/*
The color of a surface point is a function of its z-coordinate height.
*/
{
var z; // The z-coordinate for a given surface point (x, y, z).
this.zMin = this.zMax = this.points[0][Z]; // A starting value. Note that zMin and zMax are custom properties that could possibly be useful if this code is extended later.
for (var i = 0; i < this.points.length; i++)
{
z = this.points[i][Z];
if (z < this.zMin) { this.zMin = z; }
if (z > this.zMax) { this.zMax = z; }
}
var zDelta = Math.abs(this.zMax - this.zMin) / constants.colorMap.length;
for (var i = 0; i < this.points.length; i++)
{
this.points[i].color = constants.colorMap[ Math.floor( (this.points[i][Z]-this.zMin)/zDelta ) ];
}
/* Note that the prior FOR loop is functionally equivalent to the follow (much less elegant) loop:
for (var i = 0; i < this.points.length; i++)
{
if (this.points[i][Z] <= this.zMin + zDelta) {this.points[i].color = "#060";}
else if (this.points[i][Z] <= this.zMin + 2*zDelta) {this.points[i].color = "#090";}
else if (this.points[i][Z] <= this.zMin + 3*zDelta) {this.points[i].color = "#0C0";}
else if (this.points[i][Z] <= this.zMin + 4*zDelta) {this.points[i].color = "#0F0";}
else if (this.points[i][Z] <= this.zMin + 5*zDelta) {this.points[i].color = "#9F0";}
else if (this.points[i][Z] <= this.zMin + 6*zDelta) {this.points[i].color = "#9C0";}
else if (this.points[i][Z] <= this.zMin + 7*zDelta) {this.points[i].color = "#990";}
else if (this.points[i][Z] <= this.zMin + 8*zDelta) {this.points[i].color = "#960";}
else if (this.points[i][Z] <= this.zMin + 9*zDelta) {this.points[i].color = "#930";}
else if (this.points[i][Z] <= this.zMin + 10*zDelta) {this.points[i].color = "#900";}
else {this.points[i].color = "#C00";}
}
*/
}
// -----------------------------------------------------------------------------------------------------
function appendCanvasElement()
/*
Creates and then appends the "myCanvas" canvas element to the DOM.
*/
{
var canvasElement = document.createElement('canvas');
canvasElement.width = constants.canvasWidth;
canvasElement.height = constants.canvasHeight;
canvasElement.id = "myCanvas";
canvasElement.getContext('2d').translate(constants.canvasWidth/2, constants.canvasHeight/2); // Translate the surface's origin to the center of the canvas.
document.body.appendChild(canvasElement); // Make the canvas element a child of the body element.
}
//------------------------------------------------------------------------------------------------------
Surface.prototype.sortByZIndex = function(A, B)
{
return A[Z] - B[Z]; // Determines if point A is behind, in front of, or at the same level as point B (with respect to the z-axis).
}
// -----------------------------------------------------------------------------------------------------
Surface.prototype.draw = function()
{
var myCanvas = document.getElementById("myCanvas"); // Required for Firefox.
var ctx = myCanvas.getContext("2d");
this.points = surface.points.sort(surface.sortByZIndex); // Sort the set of points based on relative z-axis position. If the points are visibly small, you can sort of get away with removing this step.
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
ctx.font="20px Arial";
ctx.fillText("X",250,0);
for (var i = 0; i < this.points.length; i++)
{
ctx.fillStyle = this.points[i].color;
ctx.fillRect(this.points[i][X] * constants.surfaceScale, this.points[i][Y] * constants.surfaceScale, constants.pointWidth, constants.pointWidth);
}
}
// -----------------------------------------------------------------------------------------------------
Surface.prototype.multi = function(R)
/*
Assumes that R is a 3 x 3 matrix and that this.points (i.e., P) is a 3 x n matrix. This method performs P = R * P.
*/
{
var Px = 0, Py = 0, Pz = 0; // Variables to hold temporary results.
var P = this.points; // P is a pointer to the set of surface points (i.e., the set of 3 x 1 vectors).
var sum; // The sum for each row/column matrix product.
for (var V = 0; V < P.length; V++) // For all 3 x 1 vectors in the point list.
{
Px = P[V][X], Py = P[V][Y], Pz = P[V][Z];
for (var Rrow = 0; Rrow < 3; Rrow++) // For each row in the R matrix.
{
sum = (R[Rrow][X] * Px) + (R[Rrow][Y] * Py) + (R[Rrow][Z] * Pz);
P[V][Rrow] = sum;
}
}
}
// -----------------------------------------------------------------------------------------------------
Surface.prototype.erase = function()
{
var myCanvas = document.getElementById("myCanvas"); // Required for Firefox.
var ctx = myCanvas.getContext("2d");
ctx.clearRect(-constants.canvasWidth/2, -constants.canvasHeight/2, myCanvas.width, myCanvas.height);
}
// -----------------------------------------------------------------------------------------------------
Surface.prototype.xRotate = function(sign)
/*
Assumes "sign" is either 1 or -1, which is used to rotate the surface "clockwise" or "counterclockwise".
*/
{
var Rx = [ [0, 0, 0],
[0, 0, 0],
[0, 0, 0] ]; // Create an initialized 3 x 3 rotation matrix.
Rx[0][0] = 1;
Rx[0][1] = 0; // Redundant but helps with clarity.
Rx[0][2] = 0;
Rx[1][0] = 0;
Rx[1][1] = Math.cos( sign*constants.dTheta );
Rx[1][2] = -Math.sin( sign*constants.dTheta );
Rx[2][0] = 0;
Rx[2][1] = Math.sin( sign*constants.dTheta );
Rx[2][2] = Math.cos( sign*constants.dTheta );
this.multi(Rx); // If P is the set of surface points, then this method performs the matrix multiplcation: Rx * P
this.erase(); // Note that one could use two canvases to speed things up, which also eliminates the need to erase.
this.draw();
}
// -----------------------------------------------------------------------------------------------------
Surface.prototype.yRotate = function(sign)
/*
Assumes "sign" is either 1 or -1, which is used to rotate the surface "clockwise" or "counterclockwise".
*/
{
var Ry = [ [0, 0, 0],
[0, 0, 0],
[0, 0, 0] ]; // Create an initialized 3 x 3 rotation matrix.
Ry[0][0] = Math.cos( sign*constants.dTheta );
Ry[0][1] = 0; // Redundant but helps with clarity.
Ry[0][2] = Math.sin( sign*constants.dTheta );
Ry[1][0] = 0;
Ry[1][1] = 1;
Ry[1][2] = 0;
Ry[2][0] = -Math.sin( sign*constants.dTheta );
Ry[2][1] = 0;
Ry[2][2] = Math.cos( sign*constants.dTheta );
this.multi(Ry); // If P is the set of surface points, then this method performs the matrix multiplcation: Rx * P
this.erase(); // Note that one could use two canvases to speed things up, which also eliminates the need to erase.
this.draw();
}
// -----------------------------------------------------------------------------------------------------
Surface.prototype.zRotate = function(sign)
/*
Assumes "sign" is either 1 or -1, which is used to rotate the surface "clockwise" or "counterclockwise".
*/
{
var Rz = [ [0, 0, 0],
[0, 0, 0],
[0, 0, 0] ]; // Create an initialized 3 x 3 rotation matrix.
Rz[0][0] = Math.cos( sign*constants.dTheta );
Rz[0][1] = -Math.sin( sign*constants.dTheta );
Rz[0][2] = 0; // Redundant but helps with clarity.
Rz[1][0] = Math.sin( sign*constants.dTheta );
Rz[1][1] = Math.cos( sign*constants.dTheta );
Rz[1][2] = 0;
Rz[2][0] = 0
Rz[2][1] = 0;
Rz[2][2] = 1;
this.multi(Rz); // If P is the set of surface points, then this method performs the matrix multiplcation: Rx * P
this.erase(); // Note that one could use two canvases to speed things up, which also eliminates the need to erase.
this.draw();
}
// -----------------------------------------------------------------------------------------------------
function processKeyDown(evt)
{
if (evt.ctrlKey)
{
switch (evt.keyCode)
{
case constants.upArrow:
// No operation other than preventing the default behavior of the arrow key.
evt.preventDefault(); // This prevents the default behavior of the arrow keys, which is to scroll the browser window when scroll bars are present. The user can still scroll the window with the mouse.
break;
case constants.downArrow:
// No operation other than preventing the default behavior of the arrow key.
evt.preventDefault();
break;
case constants.leftArrow:
// console.log("ctrl+leftArrow");
surface.zRotate(-1); // The sign determines if the surface rotates "clockwise" or "counterclockwise".
evt.preventDefault();
break;
case constants.rightArrow:
// console.log("ctrl+rightArrow");
surface.zRotate(1);
evt.preventDefault();
break;
}
return; // When the control key is pressed, only the left and right arrows have meaning, no need to process any other key strokes (i.e., bail now).
}
// Assert: The control key is not pressed.
switch (evt.keyCode)
{
case constants.upArrow:
// console.log("upArrow");
surface.xRotate(1);
evt.preventDefault();
break;
case constants.downArrow:
// console.log("downArrow");
surface.xRotate(-1);
evt.preventDefault();
break;
case constants.leftArrow:
// console.log("leftArrow");
surface.yRotate(-1);
evt.preventDefault();
break;
case constants.rightArrow:
// console.log("rightArrow");
surface.yRotate(1);
evt.preventDefault();
break;
}
}
// -----------------------------------------------------------------------------------------------------
Surface.prototype.plot = function(x, y, z)
/*
add the point (x, y, z) (in 3 x 1 vector format) to the surface.
*/
{
this.points.push(point(x, y, z)); // Store a surface point
var x=0;
for (var x = constants.xMin; x <= constants.xMax; x += constants.xDelta)
{
this.points.push(point(x, 0, 0));
}
/*for (var x = constants.xMax+1; x <= constants.xMax+2; x += constants.xDelta)
{
this.points.push(point(11, 0, 0))
}*/
for (var x = constants.xMin; x <= constants.xMax; x += constants.yDelta)
{
this.points.push(point(0, x, 0));
}
for (var x = constants.xMin; x <= constants.xMax; x += constants.yDelta)
{
this.points.push(point(0,0,x));
}
}
function onloadInit()
{
appendCanvasElement(); // Create and append the canvas element to the DOM.
surface.draw(); // Draw the surface on the canvas.
document.addEventListener('keydown', processKeyDown, false); // Used to detect if the control key has been pressed.
}
// -----------------------------------------------------------------------------------------------------
//surface.generate(); // Creates the set of points reprsenting the surface. Must be called before color().
surface.plot(1,1,1);
surface.color(); // Based on the min and max z-coordinate values, chooses colors for each point based on the point's z-ccordinate value (i.e., height).
window.addEventListener('load', onloadInit, false); // Perform processing that must occur after the page has fully loaded.
</script>
</head>
<body>
<p>The z-axis extends out from the center of the screen.<br>
To rotate about the x-axis, press the up/down arrow keys.
To rotate about the y-axis, press the left/right arrow keys.
To rotate about the z-axis, press the ctrl+left/ctrl+down arrow keys.
Note that pressing an arrow key down continuously will not rotate the surface. The surface is rotated once per key press.</p>
<!-- The canvas element is append to the DOM here. -->
</body>
</html>
Text is drawn on a rectangular plane. Let the co-ordinates of top left be (xtl,ytl,ztl)
to right corner be (xtr,ytr,ztr) and bottom left be (xbl,ybl,zbl) then any transformations have to be applied to these coordinates and then the coordoinates for the 2D projection onto the canvas have to be calculated. This will produce a parallelogram into which the text can be drawn but would also need to be transformed.
The simplest would be to calculate the top left corner transformation and draw standard text at that point, perhaps reducing text size depending on z.

Expand fill of convex polygon

I have a convex polygon P1 of N points. This polygon could be any shape or proportion (as long as it is still convex).
I need to compute another polygon P2 using the original polygons geometry, but "expanded" by a given number of units. What might the algorithm be for expanding a convex polygon?
To expand a convex polygon, draw a line parallel to each edge and the given number of units away. Then use the intersection points of the new lines as the vertices of the expanded polygon. The javascript/canvas at the end follows this functional breakdown:
Step 1: Figure out which side is "out"
The order of the vertices (points) matters. In a convex polygon, they can be listed in a clockwise (CW), or a counter-clockwise (CCW) order. In a CW polygon, turn one of the edges 90 degrees CCW to obtain an outward-facing normal. On a CCW polygon, turn it CW instead.
If the turn direction of the vertices is not known in advance, examine how the second edge turns from the first. In a convex polygon, the remaining edges will keep turning in the same direction:
Find the CW normal of the first edge. We don't know yet whether it's facing inward or outward.
Compute the dot product of the second edge with the normal we computed. If the second edge turns CW, the dot product will be positive. It will be negative otherwise.
Math:
// in vector terms:
v01 = p1 - p0 // first edge, as a vector
v12 = p2 - p1 // second edge, as a vector
n01 = (v01.y, -v01.x) // CW normal of first edge
d = v12 * n01 // dot product
// and in x,y terms:
v01 = (p1.x-p0.x, p1.y-p0.y) // first edge, as a vector
v12 = (p2.x-p1.x, p2.y-p1.y) // second edge, as a vector
n01 = (v01.y, -v01.x) // CW normal of first edge
d = v12.x * n01.x + v12.y * n01.y; // dot product: v12 * n01
if (d > 0) {
// the polygon is CW
} else {
// the polygon is CCW
}
// and what if d==0 ?
// -- that means the second edge continues in the same
// direction as a first. keep looking for an edge that
// actually turns either CW or CCW.
Code:
function vecDot(v1, v2) {
return v1.x * v2.x + v1.y * v2.y;
}
function vecRot90CW(v) {
return { x: v.y, y: -v.x };
}
function vecRot90CCW(v) {
return { x: -v.y, y: v.x };
}
function polyIsCw(p) {
return vecDot(
vecRot90CW({ x: p[1].x - p[0].x, y: p[1].y - p[0].y }),
{ x: p[2].x - p[1].x, y: p[2].y - p[1].y }) >= 0;
}
var rot = polyIsCw(p) ? vecRot90CCW : vecRot90CW;
Step 2: Find lines parallel to the polygon edges
Now that we know which side is out, we can compute lines parallel to each polygon edge, at exactly the required distance. Here's our strategy:
For each edge, compute its outward-facing normal
Normalize the normal, such that its length becomes one unit
Multiply the normal by the distance we want the expanded polygon to be from the original
Add the multiplied normal to both ends of the edge. That will give us two points on the parallel line. Those two points are enough to define the parallel line.
Code:
// given two vertices pt0 and pt1, a desired distance, and a function rot()
// that turns a vector 90 degrees outward:
function vecUnit(v) {
var len = Math.sqrt(v.x * v.x + v.y * v.y);
return { x: v.x / len, y: v.y / len };
}
function vecMul(v, s) {
return { x: v.x * s, y: v.y * s };
}
var v01 = { x: pt1.x - pt0.x, y: pt1.y - pt0.y }; // edge vector
var d01 = vecMul(vecUnit(rot(v01)), distance); // multiplied unit normal
var ptx0 = { x: pt0.x + d01.x, y: pt0.y + d01.y }; // two points on the
var ptx1 = { x: pt1.x + d01.x, y: pt1.y + d01.y }; // parallel line
Step 3: Compute the intersections of the parallel lines
--these will be the vertices of the expanded polygon.
Math:
A line going through two points P1, P2 can be described as:
P = P1 + t * (P2 - P1)
Two lines can be described as
P = P1 + t * (P2 - P1)
P = P3 + u * (P4 - P3)
And their intersection has to be on both lines:
P = P1 + t * (P2 - P1) = P3 + u * (P4 - P3)
This can be massaged to look like:
(P2 - P1) * t + (P3 - P4) * u = P3 - P1
Which in x,y terms is:
(P2.x - P1.x) * t + (P3.x - P4.x) * u = P3.x - P1.x
(P2.y - P1.y) * t + (P3.y - P4.y) * u = P3.y - P1.y
As the points P1, P2, P3 and P4 are known, so are the following values:
a1 = P2.x - P1.x a2 = P2.y - P1.y
b1 = P3.x - P4.x b2 = P3.y - P4.y
c1 = P3.x - P1.x c2 = P3.y - P1.y
This shortens our equations to:
a1*t + b1*u = c1
a2*t + b2*u = c2
Solving for t gets us:
t = (b1*c2 - b2*c1)/(a2*b1 - a1*b2)
Which lets us find the intersection at P = P1 + t * (P2 - P1).
Code:
function intersect(line1, line2) {
var a1 = line1[1].x - line1[0].x;
var b1 = line2[0].x - line2[1].x;
var c1 = line2[0].x - line1[0].x;
var a2 = line1[1].y - line1[0].y;
var b2 = line2[0].y - line2[1].y;
var c2 = line2[0].y - line1[0].y;
var t = (b1*c2 - b2*c1) / (a2*b1 - a1*b2);
return {
x: line1[0].x + t * (line1[1].x - line1[0].x),
y: line1[0].y + t * (line1[1].y - line1[0].y)
};
}
Step 4: Deal with special cases
There is a number of special cases that merit attention. Left as an exercise to the reader...
When there's a very sharp angle between two edges, the expanded vertex can be very far from the original one. You might want to consider clipping the expanded edge if it goes beyond some threshold. At the extreme case, the angle is zero, which suggests that the expanded vertex is at infinity, causing division by zero in the arithmetic. Watch out.
When the first two edges are on the same line, you can't tell if it's a CW or a CCW polygon by looking just at them. Look at more edges.
Non convex polygons are much more interesting... and are not tackled here.
Full sample code
Drop this in a canvas-capable browser. I used Chrome 6 on Windows. The triangle and its expanded version should animate.
canvas { border: 1px solid #ccc; }
$(function() {
var canvas = document.getElementById('canvas');
if (canvas.getContext) {
var context = canvas.getContext('2d');
// math for expanding a polygon
function vecUnit(v) {
var len = Math.sqrt(v.x * v.x + v.y * v.y);
return { x: v.x / len, y: v.y / len };
}
function vecMul(v, s) {
return { x: v.x * s, y: v.y * s };
}
function vecDot(v1, v2) {
return v1.x * v2.x + v1.y * v2.y;
}
function vecRot90CW(v) {
return { x: v.y, y: -v.x };
}
function vecRot90CCW(v) {
return { x: -v.y, y: v.x };
}
function intersect(line1, line2) {
var a1 = line1[1].x - line1[0].x;
var b1 = line2[0].x - line2[1].x;
var c1 = line2[0].x - line1[0].x;
var a2 = line1[1].y - line1[0].y;
var b2 = line2[0].y - line2[1].y;
var c2 = line2[0].y - line1[0].y;
var t = (b1*c2 - b2*c1) / (a2*b1 - a1*b2);
return {
x: line1[0].x + t * (line1[1].x - line1[0].x),
y: line1[0].y + t * (line1[1].y - line1[0].y)
};
}
function polyIsCw(p) {
return vecDot(
vecRot90CW({ x: p[1].x - p[0].x, y: p[1].y - p[0].y }),
{ x: p[2].x - p[1].x, y: p[2].y - p[1].y }) >= 0;
}
function expandPoly(p, distance) {
var expanded = [];
var rot = polyIsCw(p) ? vecRot90CCW : vecRot90CW;
for (var i = 0; i < p.length; ++i) {
// get this point (pt1), the point before it
// (pt0) and the point that follows it (pt2)
var pt0 = p[(i > 0) ? i - 1 : p.length - 1];
var pt1 = p[i];
var pt2 = p[(i < p.length - 1) ? i + 1 : 0];
// find the line vectors of the lines going
// into the current point
var v01 = { x: pt1.x - pt0.x, y: pt1.y - pt0.y };
var v12 = { x: pt2.x - pt1.x, y: pt2.y - pt1.y };
// find the normals of the two lines, multiplied
// to the distance that polygon should inflate
var d01 = vecMul(vecUnit(rot(v01)), distance);
var d12 = vecMul(vecUnit(rot(v12)), distance);
// use the normals to find two points on the
// lines parallel to the polygon lines
var ptx0 = { x: pt0.x + d01.x, y: pt0.y + d01.y };
var ptx10 = { x: pt1.x + d01.x, y: pt1.y + d01.y };
var ptx12 = { x: pt1.x + d12.x, y: pt1.y + d12.y };
var ptx2 = { x: pt2.x + d12.x, y: pt2.y + d12.y };
// find the intersection of the two lines, and
// add it to the expanded polygon
expanded.push(intersect([ptx0, ptx10], [ptx12, ptx2]));
}
return expanded;
}
// drawing and animating a sample polygon on a canvas
function drawPoly(p) {
context.beginPath();
context.moveTo(p[0].x, p[0].y);
for (var i = 0; i < p.length; ++i) {
context.lineTo(p[i].x, p[i].y);
}
context.closePath();
context.fill();
context.stroke();
}
function drawPolyWithMargin(p, margin) {
context.fillStyle = "rgb(255,255,255)";
context.strokeStyle = "rgb(200,150,150)";
drawPoly(expandPoly(p, margin));
context.fillStyle = "rgb(150,100,100)";
context.strokeStyle = "rgb(200,150,150)";
drawPoly(p);
}
var p = [{ x: 100, y: 100 }, { x: 200, y: 120 }, { x: 80, y: 200 }];
setInterval(function() {
for (var i in p) {
var pt = p[i];
if (pt.vx === undefined) {
pt.vx = 5 * (Math.random() - 0.5);
pt.vy = 5 * (Math.random() - 0.5);
}
pt.x += pt.vx;
pt.y += pt.vy;
if (pt.x < 0 || pt.x > 400) { pt.vx = -pt.vx; }
if (pt.y < 0 || pt.y > 400) { pt.vy = -pt.vy; }
}
context.clearRect(0, 0, 800, 400);
drawPolyWithMargin(p, 10);
}, 50);
}
});
<html>
<head>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
</head>
<body>
<canvas id="canvas" width="400" height="400"></canvas>
</body>
</html>
sample code disclaimers:
the sample sacrifices some efficiency for the sake of clarity. In your code, you may want to compute each edge's expanded parallel just once, and not twice as in here
the canvas's y coordinate grows downward, which inverts the CW/CCW logic. Things keep on working though as we just need to turn the outward normals in a direction opposite to the polygon's -- and both get flipped.
For each line segment of the original, find the midpoint m and (unit length) outward normal u of the segment. The corresponding segment of the expanded polygon will then lie on the line through m+n*u (where you want to expand the original by n) with normal u. To find the vertices of the expanded polygon you then need to find the intersection of pairs of successive lines.
If the polygon is centered on the origin simply multiply each of the points by a common scaling factor.
If the polygon is not centered on the origin then first translate so the center is on the origin, scale, and then translate it back to where it was.
After your comment
It seems you want all points to be moved the same distance away from the origin.
You can do this for each point by getting the normalised vector to this point. multiplying this by your 'expand constant' and adding the resulting vector back onto the original point.
n.b. You will still have to translate-modify-translate if the center is not also the origin for this solution.
Let the points of the polygon be A1, B1, C1... Now you have lines from A1 to B1, then from B1 to C1... We want to compute points A2, B2, C2 of the polygon P2.
If you bisect angle, for example A1 B1 C1, you will have a line which goes in the direction you want. Now you can find a point B2 on it which is the appropriate distance from B1 on bisector line.
Repeat this for all points of the polygon P1.
Look at straight skeletons. As has been implied here there are a number of tricky issues with non convex polygons that you have been mecifully spared!