-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
268 lines (238 loc) · 7.97 KB
/
index.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
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
/**
* 1. Get rows and columns
* 2. For each piece, get a smaller square piece
* 3. For each smaller square piece, draw hole or hill via bezier curve
* 4. store that piece somewhere
*/
// Try to draw bezier curve on given image
const JIGSAW_SHAPE = Object.freeze({
TAB: 0,
SLOT: 1,
STRAIGHT: 2,
})
class Piece {
shape;
containerWidth;
containerHeight;
width;
height;
row;
col;
imageSrc;
get widthPercentage() {
return this.width / this.containerWidth;
}
get heightPercentage() {
return this.height / this.containerHeight;
}
constructor(shape) {
this.shape = shape;
}
}
/**
* How to draw 1 square.
* If edge is at boundary draw a straight line
* If not at boundary since we're going left -> right and top bottom
* we draw left side complementary to the right side if any
* we draw right side freely
* we draw top side complementary to the bottom side if any
* we draw bottom side freely
* clip and save as a piece
* Shift path to new starting point (top left) and draw again
*
* Adjust the bezier curve generation to dynamic sizes
*/
export async function generatePuzzle(img, rows, columns) {
const canvas = document.createElement('canvas');
const puzzlePieces = [];
canvas.width = img.width;
canvas.height = img.height;
const context = canvas.getContext('2d');
context.drawImage(img, 0, 0, canvas.width, canvas.height);
const pieceWidth = img.width / columns;
const pieceHeight = img.height / rows;
const scalarWidth = pieceWidth / 100;
const scalarHeight = pieceHeight / 100;
const lowPeak = 8.75;
const highPeak = 20;
const newCanvasWidth = pieceWidth + lowPeak*scalarHeight + highPeak*scalarHeight;
const newCanvasHeight = pieceHeight + lowPeak*scalarWidth + highPeak*scalarWidth;
const puzzle = [];
// generate a 2D array of uninitialized values
for (let i = 0; i < rows; i++) {
const newRow = [];
for (let j = 0; j < columns; j++) {
newRow.push(null)
}
puzzle.push(newRow);
}
let globalX = 0;
let globalY = 0;
context.beginPath();
/**
* get the source image coordinates and clip the new small canvas object with bezier curves.
*/
for (let i = 0; i < rows; i++) {
globalX = 0;
for (let j = 0; j < columns; j++) {
const piece = new Piece();
piece.width = pieceWidth;
piece.height = pieceHeight;
piece.row = i;
piece.col = j;
piece.containerWidth = newCanvasWidth;
piece.containerHeight = newCanvasHeight;
const shape = {};
const newCanvas = document.createElement('canvas');
newCanvas.width = newCanvasWidth;
newCanvas.height = newCanvasHeight;
const newCanvasCtx = newCanvas.getContext('2d');
let x = j <= 0 ? 0 : (lowPeak*scalarHeight);
let y = i <= 0 ? 0 : (highPeak*scalarWidth);
// If edge = straight,
// If has initialized neighbor, build complementary
// If uninitilzied neighbor build whatever
// Build top
if (i === 0) {
shape.TOP = JIGSAW_SHAPE.STRAIGHT;
} else {
const topNeighbor = puzzle[i-1][j];
if (topNeighbor) {
shape.TOP = topNeighbor.shape.BOTTOM;
} else {
shape.TOP = JIGSAW_SHAPE.SLOT;
}
}
// Build bottom
if (i === rows-1) {
shape.BOTTOM = JIGSAW_SHAPE.STRAIGHT;
} else {
// Since we build left -> right, top -> bottom, no bottom neighbor yet.
shape.RIGHT = JIGSAW_SHAPE.SLOT;
}
// Build left
if (j === 0) {
shape.LEFT = JIGSAW_SHAPE.STRAIGHT;
} else {
const leftNeighbor = puzzle[i][j-1];
if (leftNeighbor) {
shape.LEFT = leftNeighbor.shape.RIGHT;
} else {
shape.LEFT = JIGSAW_SHAPE.SLOT;
}
}
// Build right
if (j === columns-1) {
shape.RIGHT = JIGSAW_SHAPE.STRAIGHT;
} else {
shape.RIGHT = JIGSAW_SHAPE.SLOT;
}
piece.shape = shape;
const curves = makeBeziers();
// Horiztonal curve creation
const hCurves = createScalarCurves(curves, piece.width / 100);
// Vertical curve creation
const vCurves = createScalarCurves(curves, piece.height / 100);
// Draw left -> right, top -> down, right -> left, down -> top
newCanvasCtx.moveTo(x, y);
// Draw top left -> top right
if (shape.TOP === JIGSAW_SHAPE.STRAIGHT) {
newCanvasCtx.lineTo(x+piece.width, y);
} else {
for (let ii = 0; ii < hCurves.length; ii++) {
const bi = hCurves[ii];
newCanvasCtx.bezierCurveTo(x+bi.cx1, y+bi.cy1, x+bi.cx2, y+bi.cy2, x+bi.ex, y+bi.ey);
}
}
x = x+piece.width;
// Draw top right -> bottom right
if (shape.RIGHT === JIGSAW_SHAPE.STRAIGHT) {
newCanvasCtx.lineTo(x, y+piece.height);
} else {
// At top right, need to draw curve downwards.
const dy = y + piece.height;
// Right slot, rotate (x,y) = (-y,x)
for (let ii = 0; ii < vCurves.length; ii++) {
const bi = vCurves[ii];
newCanvasCtx.bezierCurveTo(x-bi.cy1, y+bi.cx1, x-bi.cy2, y+bi.cx2, x-bi.ey, y+bi.ex);
}
}
y = y+piece.height;
// Draw bottom right -> bottom left
if (shape.BOTTOM === JIGSAW_SHAPE.STRAIGHT) {
newCanvasCtx.lineTo(x-piece.width, y);
} else {
for (let ii = 0; ii < hCurves.length; ii++) {
const bi = hCurves[ii];
newCanvasCtx.bezierCurveTo(x-bi.cx1, y+bi.cy1, x-bi.cx2, y+bi.cy2, x-bi.ex, y+bi.ey);
}
}
x = x-piece.width;
// Draw bottom left -> top left
if (shape.LEFT === JIGSAW_SHAPE.STRAIGHT) {
newCanvasCtx.lineTo(x, y-piece.height);
} else {
// Right slot, (x,y) = (-y,x), subtract delta y, which is x
for (let ii = 0; ii < vCurves.length; ii++) {
const bi = vCurves[ii];
newCanvasCtx.bezierCurveTo(x-bi.cy1, y-bi.cx1, x-bi.cy2, y-bi.cx2, x-bi.ey, y-bi.ex);
}
}
puzzle[i][j] = piece;
// clip this and make a new <canvas> draggable element.
newCanvasCtx.stroke();
newCanvasCtx.clip();
newCanvasCtx.drawImage(
img,
globalX-(lowPeak*scalarHeight) < 0 ? 0 : globalX-(lowPeak*scalarHeight),
globalY-(highPeak*scalarWidth) < 0 ? 0: globalY-(highPeak*scalarWidth),
newCanvas.width,
newCanvas.height,
0,
0,
newCanvas.width,
newCanvas.height
);
piece.imageSrc = newCanvas.toDataURL();
puzzlePieces.push(piece);
// move to top right corner which will be the new top left for the next piece.
globalX += piece.width;
}
globalY += pieceHeight;
}
return puzzlePieces;
}
function createScalarCurves(curve, scalar) {
return curve.map(e => {
return {
cx1: e.cx1*scalar, cy1: e.cy1*scalar, cx2:e.cx2*scalar,cy2:e.cy2*scalar, ex:e.ex*scalar, ey:e.ey*scalar
}
})
}
// slot
function makeBeziers() {
return([
{cx1:0, cy1:0, cx2:35,cy2:15, ex:37, ey:5}, // left shoulder
{cx1:37, cy1:5, cx2:40,cy2:0, ex:38, ey:-5}, // left neck
{cx1:38, cy1:-5, cx2:20,cy2:-20,ex:50, ey:-20}, // left head
{cx1:50, cy1:-20,cx2:80,cy2:-20,ex:62, ey:-5}, // right head
{cx1:62, cy1:-5, cx2:60,cy2:0, ex:63, ey:5}, // right neck
{cx1:63, cy1:5, cx2:65,cy2:15, ex:100,ey:0}, // right shoulder
]);
}
// tab, flip left neck, left head, right head and right neck y
function makeTab() {
return([
{cx1:0, cy1:0, cx2:35,cy2:-15, ex:37, ey:-5}, // left shoulder
{cx1:37, cy1:-5, cx2:40,cy2:0, ex:38, ey:5}, // left neck
{cx1:38, cy1:5, cx2:20,cy2:20,ex:50, ey:20}, // left head
{cx1:50, cy1:20,cx2:80,cy2:20,ex:62, ey:5}, // right head
{cx1:62, cy1:5, cx2:60,cy2:0, ex:63, ey:-5}, // right neck
{cx1:63, cy1:-5, cx2:65,cy2:-15, ex:100,ey:0}, // right shoulder
]);
}
//https://en.wikipedia.org/wiki/B%C3%A9zier_curve
function calcPeakY(cx0, cy0, cx1, cy1, ex, ey) {
const mid_t = 0.5;
return cy1 + ((1-mid_t)**2)*(cy0-cy1) + (mid_t**2)*(ey-cy1);
}