-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbinpacking.js
215 lines (190 loc) · 7.11 KB
/
binpacking.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
var binpacking = (function(){
// How much to move a Rect outward each step
var closenessStep = 0.1;
// How many degrees to travel to search for a new placement
// around the center Rect point
var radialStep = 5;
/**
* Packs rects within area and returns new list with rects in correct
* location, may remove some rects.
*
* @param area Rect: Area to pack rects within
* @param rects [Rect]: List of rects sorted in order of importance
* @param maxDistanceFactor: Size to distance ratio for rect to be from center
* ex) 2 means rect could not be more than 2 of it's
* size away
* @return Non-overlapping packed rects
**/
function pack(area, rects, maxDistanceFactor){
var placedRects = [];
var unplaced = 0;
for (var i = 0;i < rects.length;i++){
var placedRect = placeRect(rects[i], placedRects, area, maxDistanceFactor);
if (placedRect == null){
// Rect could not be placed
unplaced++;
}else{
placedRects.push(placedRect);
}
}
console.log(unplaced + " rects could not be placed");
return placedRects;
}
/**
* Return a newly placed rect given a list of existing rects and the
* area to place the rect within.
*
* @param rect Rect: A Rect to be placed in the area
* @param placedRects [Rect]: List of Rects that could be collided with
* @param area Rect: Area to place rect within
* @param dRatio num: Max distance/size ratio that the rect can be placed
* @return Rect: The place rect (not same object as rect parameter)
**/
function placeRect(rect, placedRects, area, dRatio){
// First check that rect is in the area and there is collision at the
// original location of the rect
if (checkRectWithinArea(rect, area) && !checkCollisions(rect, placedRects)){
// Nothing there! Just place the rect
return rect.clone();
}
// There was a collision at the rects center location, so
// we'll check in a circle around the rect with an expanding
// radius until we're too far away
var maxDistance = dRatio * rect.size;
var rotationOffset = Math.random() * 360;
var newRect = rect.clone();
newRect.originalx = rect.x;
newRect.originaly = rect.y;
for (var currentRadius = rect.size * closenessStep;
currentRadius < maxDistance;
currentRadius += rect.size * closenessStep){
for (var rotation = 0; rotation < 360; rotation += radialStep){
var dx = Math.cos(rotationOffset + rotation * (Math.PI/180)) * currentRadius;
var dy = Math.sin(rotationOffset + rotation * (Math.PI/180)) * currentRadius;
newRect.x = rect.x + dx;
newRect.y = rect.y + dy;
newRect.calculateBounds(false);
if (checkRectWithinArea(newRect, area) && !checkCollisions(newRect, placedRects)){
return newRect;
}
}
}
return null;
}
/**
* Checks collision of rect against a list of Rects
*
* @param rect Rect: Rect to check collisions against
* @param placedRects [Rect]: Rects to check collision
* @return: True if collision, false otherwise
**/
function checkCollisions(rect, placedRects){
for (var i = 0; i < placedRects.length; i++){
var placedRect = placedRects[i];
if (!(
placedRect.left > rect.right ||
placedRect.right < rect.left ||
placedRect.top > rect.bottom ||
placedRect.bottom < rect.top
)) return true;
}
return false;
}
/**
* Check that rect is completely within area
*
* @param rect Rect: Rect to check within area
* @param area Rect: Area to check that rect is within
* @return: True if completely within area, else false
**/
function checkRectWithinArea(rect, area){
return rect.left > area.left &&
rect.right < area.right &&
rect.top > area.top &&
rect.bottom < area.bottom;
}
/**
* Returns a Rect object that can be used for packing.
* Call with new! ex) new Rect(0,0,100,120);
*
* @param centerx, centery: Center of Rect
* @param width,height: Size of Rect
* @param properties: OPTIONAL Object w/ properties
* @return Rect object to be used for packing
**/
function Rect(centerx, centery, width, height, properties){
this.x = centerx;
this.y = centery;
this.width = width;
this.height = height;
this.properties = properties || {};
this.calculateBounds();
}
/**
* Calculates bounds of rectangle (call after changing x/y/width/height)
*
* @param recalculateSize bool: OPTIONAL if set to false, size will not be
* recalculated
**/
Rect.prototype.calculateBounds = function(recalculateSize){
this.left = this.x - this.width/2;
this.right = this.x + this.width/2;
this.top = this.y - this.height/2;
this.bottom = this.y + this.height/2;
if (recalculateSize !== false){
this.size = Math.sqrt(this.width * this.width + this.height * this.height);
}
}
/**
* Draws Rect to canvas.
*
* @param context CanvasRenderingContext2d: Context to draw to
**/
Rect.prototype.draw = function(context){
context.save();
context.strokeStyle = "#f00";
context.lineWidth = 2;
context.strokeRect(
this.x - this.width/2,
this.y - this.height/2,
this.width, this.height);
if (this.originalx !== undefined && this.originaly !== undefined){
context.globalAlpha = .2;
context.beginPath();
context.moveTo(this.originalx, this.originaly);
context.lineTo(this.x, this.y);
context.closePath();
context.stroke();
}
context.restore();
}
/**
* Creates new Rect from existing Rect, note that properties is not deep
* copied
**/
Rect.prototype.clone = function(){
return new Rect(this.x, this.y, this.width, this.height, this.properties);
};
/**
* Adds property to Rect object (often font size or text value)
*
* @param propName String: Property name
* @param propValue: Property value
**/
Rect.prototype.addProperty = function(propName, propValue){
this.properties[propName] = propValue;
};
/**
* Returns a previously stored property
**/
Rect.prototype.getProperty = function(propName){
return this.properties[propName];
}
return {
// Public Methods
pack: pack,
Rect: Rect,
setClosenessStep: function(v){ closenessStep = v; },
setRadialStep: function(v){ radialStep = v; }
};
})();