HTML preprocessors can make writing HTML more powerful or convenient. For instance, Markdown is designed to be easier to write and read for text documents and you could write a loop in Pug.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. So you don't have access to higher-up elements like the <html>
tag. If you want to add classes there that can affect the whole document, this is the place to do it.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. If you need things in the <head>
of the document, put that code here.
The resource you are linking to is using the 'http' protocol, which may not work when the browser is using https.
CSS preprocessors help make authoring CSS easier. All of them offer things like variables and mixins to provide convenient abstractions.
It's a common practice to apply CSS to a page that styles elements such that they are consistent across all browsers. We offer two of the most popular choices: normalize.css and a reset. Or, choose Neither and nothing will be applied.
To get the best cross-browser support, it is a common practice to apply vendor prefixes to CSS properties and values that require them to work. For instance -webkit-
or -moz-
.
We offer two popular choices: Autoprefixer (which processes your CSS server-side) and -prefix-free (which applies prefixes via a script, client-side).
Any URLs added here will be added as <link>
s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.
You can apply CSS to your Pen from any stylesheet on the web. Just put a URL to it here and we'll apply it, in the order you have them, before the CSS in the Pen itself.
You can also link to another Pen here (use the .css
URL Extension) and we'll pull the CSS from that Pen and include it. If it's using a matching preprocessor, use the appropriate URL Extension and we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
JavaScript preprocessors can help make authoring JavaScript easier and more convenient.
Babel includes JSX processing.
Any URL's added here will be added as <script>
s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.
You can apply a script from anywhere on the web to your Pen. Just put a URL to it here and we'll add it, in the order you have them, before the JavaScript in the Pen itself.
If the script you link to has the file extension of a preprocessor, we'll attempt to process it before applying.
You can also link to another Pen here, and we'll pull the JavaScript from that Pen and include it. If it's using a matching preprocessor, we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
Search for and use JavaScript packages from npm here. By selecting a package, an import
statement will be added to the top of the JavaScript editor for this package.
Using packages here is powered by esm.sh, which makes packages from npm not only available on a CDN, but prepares them for native JavaScript ESM usage.
All packages are different, so refer to their docs for how they work.
If you're using React / ReactDOM, make sure to turn on Babel for the JSX processing.
If active, Pens will autosave every 30 seconds after being saved once.
If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.
If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.
Visit your global Editor Settings.
<div id=forPuzzle></div>
<ul id="menu">
<li>☰</li>
<li>default image</li>
<li>load image</li>
<li>shape: <select id="shape">
<option value="1" selected>classic</option>
<option value="2">triangle</option>
<option value="3">round</option>
<option value="4">straight</option>
</select></li>
<li>12 pieces</li>
<li>25 pieces</li>
<li>50 pieces</li>
<li>100 pieces</li>
<li>200 pieces</li>
</ul>
body {
font-family: Arial, Helvetica, "Liberation Sans", FreeSans, sans-serif;
background-color: #fff;
margin: 0;
padding: 0;
border-width: 0;
cursor: pointer;
}
#menu {
position: relative;
list-style-type: none;
padding-left: 5px;
z-index: 1000;
/* 1 */
display: inline-block;
text-align: center;
}
#menu li {
margin: 2px;
padding: 4px 10px;
border-radius: 5px;
background-color: #ffff80;
}
#menu li:hover {
background-color: #ffDD60;
}
#forPuzzle {
position: absolute;
width: 95vw;
height: 95vh;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #ffffdd;
overflow: hidden;
}
.polypiece {
display: block;
overflow: hidden;
position: absolute;
}
.moving {
transition-property: top, left;
transition-duration: 1s;
transition-timing-function: linear;
}
.gameCanvas {
display: none;
overflow: hidden;
position: absolute;
}
"use strict";
let puzzle, autoStart;
const mhypot = Math.hypot,
mrandom = Math.random,
mmax = Math.max,
mmin = Math.min,
mround = Math.round,
mfloor = Math.floor,
msqrt = Math.sqrt,
mabs = Math.abs;
//-----------------------------------------------------------------------------
function isMiniature() {
return location.pathname.includes('/fullcpgrid/');
}
//-----------------------------------------------------------------------------
function alea(min, max) {
// random number [min..max[ . If no max is provided, [0..min[
if (typeof max == 'undefined') return min * mrandom();
return min + (max - min) * mrandom();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function intAlea(min, max) {
// random integer number [min..max[ . If no max is provided, [0..min[
if (typeof max == 'undefined') {
max = min; min = 0;
}
return mfloor(min + (max - min) * mrandom());
} // intAlea
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function arrayShuffle(array) {
/* randomly changes the order of items in an array
only the order is modified, not the elements
*/
let k1, temp;
for (let k = array.length - 1; k >= 1; --k) {
k1 = intAlea(0, k + 1);
temp = array[k];
array[k] = array[k1];
array[k1] = temp;
} // for k
return array
} // arrayShuffle
//-----------------------------------------------------------------------------
// Point - - - - - - - - - - - - - - - - - - - -
class Point {
constructor(x, y) {
this.x = Number(x);
this.y = Number(y);
} // constructor
copy() {
return new Point(this.x, this.y);
}
distance(otherPoint) {
return mhypot(this.x - otherPoint.x, this.y - otherPoint.y);
}
} // class Point
// Segment - - - - - - - - - - - - - - - - - - - -
// those segments are oriented
class Segment {
constructor(p1, p2) {
this.p1 = new Point(p1.x, p1.y);
this.p2 = new Point(p2.x, p2.y);
}
dx() {
return this.p2.x - this.p1.x;
}
dy() {
return this.p2.y - this.p1.y;
}
length() {
return mhypot(this.dx(), this.dy());
}
// returns a point at a given distance of p1, positive direction beeing towards p2
pointOnRelative(coeff) {
// attention if segment length can be 0
let dx = this.dx();
let dy = this.dy();
return new Point(this.p1.x + coeff * dx, this.p1.y + coeff * dy);
}
} // class Segment
//-----------------------------------------------------------------------------
// one side of a piece
class Side {
constructor() {
this.type = ""; // "d" pour straight line or "z" pour classic
this.points = []; // real points or Bezier curve points
// this.scaledPoints will be added when we know the scale
} // Side
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
reversed() {
// returns a new Side, copy of current one but reversed
const ns = new Side();
ns.type = this.type;
ns.points = this.points.slice().reverse();
return ns;
} // Side.reversed
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
scale(puzzle) {
/* uses actual dimensions of puzzle to compute actual side points
these points are not shifted by the piece position : the top left corner is at (0,0)
*/
const coefx = puzzle.scalex;
const coefy = puzzle.scaley;
this.scaledPoints = this.points.map(p => new Point(p.x * coefx, p.y * coefy));
} //
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/*
draws the path corresponding to a side
Parameters :
ctx : canvas context
shiftx, shifty : position shift (used to create emboss effect)
withoutMoveTo : to decide whether to do a moveTo to the first point. Without MoveTo
must be done only for the first side of a piece, not for the following ones
*/
drawPath(ctx, shiftx, shifty, withoutMoveTo) {
if (!withoutMoveTo) {
ctx.moveTo(this.scaledPoints[0].x + shiftx, this.scaledPoints[0].y + shifty);
}
if (this.type == "d") {
ctx.lineTo(this.scaledPoints[1].x + shiftx, this.scaledPoints[1].y + shifty);
} else { // edge zigzag
for (let k = 1; k < this.scaledPoints.length - 1; k += 3) {
ctx.bezierCurveTo(this.scaledPoints[k].x + shiftx, this.scaledPoints[k].y + shifty,
this.scaledPoints[k + 1].x + shiftx, this.scaledPoints[k + 1].y + shifty,
this.scaledPoints[k + 2].x + shiftx, this.scaledPoints[k + 2].y + shifty);
} // for k
} // if jigsaw side
} // Side.drawPath
} // class Side
//-----------------------------------------------------------------------------
/* modifies a side
changes it from a straight line (type "d") to a complex one (type "z")
The change is done towards the opposite side (side between corners ca and cb)
*/
function twist0(side, ca, cb) {
const seg0 = new Segment(side.points[0], side.points[1]);
const dxh = seg0.dx();
const dyh = seg0.dy();
const seg1 = new Segment(ca, cb);
const mid0 = seg0.pointOnRelative(0.5);
const mid1 = seg1.pointOnRelative(0.5);
const segMid = new Segment(mid0, mid1);
const dxv = segMid.dx();
const dyv = segMid.dy();
const scalex = alea(0.8, 1);
const scaley = alea(0.9, 1);
const mid = alea(0.45, 0.55);
const pa = pointAt(mid - 1 / 12 * scalex, 1 / 12 * scaley);
const pb = pointAt(mid - 2 / 12 * scalex, 3 / 12 * scaley);
const pc = pointAt(mid, 4 / 12 * scaley);
const pd = pointAt(mid + 2 / 12 * scalex, 3 / 12 * scaley);
const pe = pointAt(mid + 1 / 12 * scalex, 1 / 12 * scaley);
side.points = [seg0.p1,
new Point(seg0.p1.x + 5 / 12 * dxh * 0.52,
seg0.p1.y + 5 / 12 * dyh * 0.52),
new Point(pa.x - 1 / 12 * dxv * 0.72,
pa.y - 1 / 12 * dyv * 0.72),
pa,
new Point(pa.x + 1 / 12 * dxv * 0.72,
pa.y + 1 / 12 * dyv * 0.72),
new Point(pb.x - 1 / 12 * dxv * 0.92,
pb.y - 1 / 12 * dyv * 0.92),
pb,
new Point(pb.x + 1 / 12 * dxv * 0.52,
pb.y + 1 / 12 * dyv * 0.52),
new Point(pc.x - 2 / 12 * dxh * 0.40,
pc.y - 2 / 12 * dyh * 0.40),
pc,
new Point(pc.x + 2 / 12 * dxh * 0.40,
pc.y + 2 / 12 * dyh * 0.40),
new Point(pd.x + 1 / 12 * dxv * 0.52,
pd.y + 1 / 12 * dyv * 0.52),
pd,
new Point(pd.x - 1 / 12 * dxv * 0.92,
pd.y - 1 / 12 * dyv * 0.92),
new Point(pe.x + 1 / 12 * dxv * 0.72,
pe.y + 1 / 12 * dyv * 0.72),
pe,
new Point(pe.x - 1 / 12 * dxv * 0.72,
pe.y - 1 / 12 * dyv * 0.72),
new Point(seg0.p2.x - 5 / 12 * dxh * 0.52,
seg0.p2.y - 5 / 12 * dyh * 0.52),
seg0.p2];
side.type = "z";
function pointAt(coeffh, coeffv) {
return new Point(seg0.p1.x + coeffh * dxh + coeffv * dxv,
seg0.p1.y + coeffh * dyh + coeffv * dyv)
} // pointAt
} // twist0
//-----------------------------------------------------------------------------
/* modifies a side
changes it from a straight line (type "d") to a complex one (type "z")
The change is done towards the opposite side (side between corners ca and cb)
*/
function twist1(side, ca, cb) {
const seg0 = new Segment(side.points[0], side.points[1]);
const dxh = seg0.dx();
const dyh = seg0.dy();
const seg1 = new Segment(ca, cb);
const mid0 = seg0.pointOnRelative(0.5);
const mid1 = seg1.pointOnRelative(0.5);
const segMid = new Segment(mid0, mid1);
const dxv = segMid.dx();
const dyv = segMid.dy();
const pa = pointAt(alea(0.3, 0.35), alea(-0.05, 0.05));
const pb = pointAt(alea(0.45, 0.55), alea(0.2, 0.3));
const pc = pointAt(alea(0.65, 0.78), alea(-0.05, 0.05));
side.points = [seg0.p1,
seg0.p1, pa, pa,
pa, pb, pb,
pb, pc, pc,
pc, seg0.p2, seg0.p2];
side.type = "z";
function pointAt(coeffh, coeffv) {
return new Point(seg0.p1.x + coeffh * dxh + coeffv * dxv,
seg0.p1.y + coeffh * dyh + coeffv * dyv)
} // pointAt
} // twist1
//-----------------------------------------------------------------------------
/* modifies a side
changes it from a straight line (type "d") to a complex one (type "z")
The change is done towards the opposite side (side between corners ca and cb)
*/
function twist2(side, ca, cb) {
const seg0 = new Segment(side.points[0], side.points[1]);
const dxh = seg0.dx();
const dyh = seg0.dy();
const seg1 = new Segment(ca, cb);
const mid0 = seg0.pointOnRelative(0.5);
const mid1 = seg1.pointOnRelative(0.5);
const segMid = new Segment(mid0, mid1);
const dxv = segMid.dx();
const dyv = segMid.dy();
const hmid = alea(0.45, 0.55);
const vmid = alea(0.4, 0.5)
const pc = pointAt(hmid, vmid);
let sega = new Segment(seg0.p1, pc);
const pb = sega.pointOnRelative(2 / 3);
sega = new Segment(seg0.p2, pc);
const pd = sega.pointOnRelative(2 / 3);
side.points = [seg0.p1, pb, pd, seg0.p2];
side.type = "z";
function pointAt(coeffh, coeffv) {
return new Point(seg0.p1.x + coeffh * dxh + coeffv * dxv,
seg0.p1.y + coeffh * dyh + coeffv * dyv)
} // pointAt
} // twist2
//-----------------------------------------------------------------------------
/* modifies a side
changes it from a straight line (type "d") to a complex one (type "z")
The change is done towards the opposite side (side between corners ca and cb)
*/
function twist3(side, ca, cb) {
side.points = [side.points[0], side.points[1]];
} // twist3
//-----------------------------------------------------------------------------
class Piece {
constructor(kx, ky) { // object with 4 sides
this.ts = new Side(); // top side
this.rs = new Side(); // right side
this.bs = new Side(); // bottom side
this.ls = new Side(); // left side
this.kx = kx;
this.ky = ky;
}
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
scale(puzzle) {
this.ts.scale(puzzle);
this.rs.scale(puzzle);
this.bs.scale(puzzle);
this.ls.scale(puzzle);
} // Piece.scale
} // class Piece
//--------------------------------------------------------------
//--------------------------------------------------------------
class PolyPiece {
// represents a group of pieces well positionned with respect to each other.
// pckxmin, pckxmax, pckymin and pckymax record the lowest and highest kx and ky
// creates a canvas to draw polypiece on, and appends this canvas to puzzle.container
constructor(initialPiece, puzzle) {
this.pckxmin = initialPiece.kx;
this.pckxmax = initialPiece.kx + 1;
this.pckymin = initialPiece.ky;
this.pckymax = initialPiece.ky + 1;
this.pieces = [initialPiece];
this.puzzle = puzzle;
this.listLoops();
this.canvas = document.createElement('CANVAS');
// size and z-index will be defined later
puzzle.container.appendChild(this.canvas);
this.canvas.classList.add('polypiece');
this.ctx = this.canvas.getContext("2d");
} // PolyPiece
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/*
this method
- adds pieces of otherPoly to this PolyPiece
- reorders the pieces inside the polypiece
- adjusts coordinates of new pieces to make them consistent with this polyPiece
- re-evaluates the z - index of the polyPieces
*/
merge(otherPoly) {
const orgpckxmin = this.pckxmin;
const orgpckymin = this.pckymin;
// remove otherPoly from list of polypieces
const kOther = this.puzzle.polyPieces.indexOf(otherPoly);
this.puzzle.polyPieces.splice(kOther, 1);
// remove other canvas from container
this.puzzle.container.removeChild(otherPoly.canvas);
for (let k = 0; k < otherPoly.pieces.length; ++k) {
this.pieces.push(otherPoly.pieces[k]);
// watch leftmost, topmost... pieces
if (otherPoly.pieces[k].kx < this.pckxmin) this.pckxmin = otherPoly.pieces[k].kx;
if (otherPoly.pieces[k].kx + 1 > this.pckxmax) this.pckxmax = otherPoly.pieces[k].kx + 1;
if (otherPoly.pieces[k].ky < this.pckymin) this.pckymin = otherPoly.pieces[k].ky;
if (otherPoly.pieces[k].ky + 1 > this.pckymax) this.pckymax = otherPoly.pieces[k].ky + 1;
} // for k
// sort the pieces by increasing kx, ky
this.pieces.sort(function (p1, p2) {
if (p1.ky < p2.ky) return -1;
if (p1.ky > p2.ky) return 1;
if (p1.kx < p2.kx) return -1;
if (p1.kx > p2.kx) return 1;
return 0; // should not occur
});
// redefine consecutive edges
this.listLoops();
this.drawImage();
this.moveTo(this.x + this.puzzle.scalex * (this.pckxmin - orgpckxmin),
this.y + this.puzzle.scaley * (this.pckymin - orgpckymin));
this.puzzle.evaluateZIndex();
} // merge
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ifNear(otherPoly) {
let p1, p2;
let puzzle = this.puzzle;
// coordinates of origin of full picture for this PolyPieces
let x = this.x - puzzle.scalex * this.pckxmin;
let y = this.y - puzzle.scaley * this.pckymin;
let ppx = otherPoly.x - puzzle.scalex * otherPoly.pckxmin;
let ppy = otherPoly.y - puzzle.scaley * otherPoly.pckymin;
if (mhypot(x - ppx, y - ppy) >= puzzle.dConnect) return false; // not close enough
// this and otherPoly are in good relative position, have they a common side ?
for (let k = this.pieces.length - 1; k >= 0; --k) {
p1 = this.pieces[k];
for (let ko = otherPoly.pieces.length - 1; ko >= 0; --ko) {
p2 = otherPoly.pieces[ko];
if (p1.kx == p2.kx && mabs(p1.ky - p2.ky) == 1) return true; // true neighbors found
if (p1.ky == p2.ky && mabs(p1.kx - p2.kx) == 1) return true; // true neighbors found
} // for k
} // for k
// nothing matches
return false;
} // ifNear
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/* algorithm to determine the boundary of a PolyPiece
input : a table of cells, hopefully defining a 'good' PolyPiece, i.e. all connected together
every cell is given as an object {kx: indice, ky: indice} representing an element of a 2D array.
returned value : table of Loops, because the boundary may be made of several
simple loops : there may be a 'hole' in a PolyPiece
every loop is a list of consecutive edges,
every edge if an object {kp: index, edge: b} where kp is the index of the cell ine
the input array, and edge the side (0(top), 1(right), 2(bottom), 3(left))
every edge contains kx and ky too, normally not used here
This method does not depend on the fact that pieces have been scaled or not.
*/
listLoops() {
// internal : checks if an edge given by kx, ky is common with another cell
// returns true or false
const that = this;
function edgeIsCommon(kx, ky, edge) {
let k;
switch (edge) {
case 0: ky--; break; // top edge
case 1: kx++; break; // right edge
case 2: ky++; break; // bottom edge
case 3: kx--; break; // left edge
} // switch
for (k = 0; k < that.pieces.length; k++) {
if (kx == that.pieces[k].kx && ky == that.pieces[k].ky) return true; // we found the neighbor
}
return false; // not a common edge
} // function edgeIsCommon
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// internal : checks if an edge given by kx, ky is in tbEdges
// return index in tbEdges, or false
function edgeIsInTbEdges(kx, ky, edge) {
let k;
for (k = 0; k < tbEdges.length; k++) {
if (kx == tbEdges[k].kx && ky == tbEdges[k].ky && edge == tbEdges[k].edge) return k; // found it
}
return false; // not found
} // function edgeIsInTbEdges
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
let tbLoops = []; // for the result
let tbEdges = []; // set of edges which are not shared by 2 pieces of input
let k;
let kEdge; // to count 4 edges
let lp; // for loop during its creation
let currEdge; // current edge
let tries; // tries counter
let edgeNumber; // number of edge found during research
let potNext;
// table of tries
let tbTries = [
// if we are on edge 0 (top)
[
{ dkx: 0, dky: 0, edge: 1 }, // try # 0
{ dkx: 1, dky: 0, edge: 0 }, // try # 1
{ dkx: 1, dky: -1, edge: 3 } // try # 2
],
// if we are on edge 1 (right)
[
{ dkx: 0, dky: 0, edge: 2 },
{ dkx: 0, dky: 1, edge: 1 },
{ dkx: 1, dky: 1, edge: 0 }
],
// if we are on edge 2 (bottom)
[
{ dkx: 0, dky: 0, edge: 3 },
{ dkx: - 1, dky: 0, edge: 2 },
{ dkx: - 1, dky: 1, edge: 1 }
],
// if we are on edge 3 (left)
[
{ dkx: 0, dky: 0, edge: 0 },
{ dkx: 0, dky: - 1, edge: 3 },
{ dkx: - 1, dky: - 1, edge: 2 }
],
];
// create list of not shared edges (=> belong to boundary)
for (k = 0; k < this.pieces.length; k++) {
for (kEdge = 0; kEdge < 4; kEdge++) {
if (!edgeIsCommon(this.pieces[k].kx, this.pieces[k].ky, kEdge))
tbEdges.push({ kx: this.pieces[k].kx, ky: this.pieces[k].ky, edge: kEdge, kp: k })
} // for kEdge
} // for k
while (tbEdges.length > 0) {
lp = []; // new loop
currEdge = tbEdges[0]; // we begin with first available edge
lp.push(currEdge); // add it to loop
tbEdges.splice(0, 1); // remove from list of available sides
do {
for (tries = 0; tries < 3; tries++) {
potNext = tbTries[currEdge.edge][tries];
edgeNumber = edgeIsInTbEdges(currEdge.kx + potNext.dkx, currEdge.ky + potNext.dky, potNext.edge);
if (edgeNumber === false) continue; // can't here
// new element in loop
currEdge = tbEdges[edgeNumber]; // new current edge
lp.push(currEdge); // add it to loop
tbEdges.splice(edgeNumber, 1); // remove from list of available sides
break; // stop tries !
} // for tries
if (edgeNumber === false) break; // loop is closed
} while (1); // do-while exited by break
tbLoops.push(lp); // add this loop to loops list
} // while tbEdges...
// replace components of loops by actual pieces sides
this.tbLoops = tbLoops.map(loop => loop.map(edge => {
let cell = this.pieces[edge.kp];
if (edge.edge == 0) return cell.ts;
if (edge.edge == 1) return cell.rs;
if (edge.edge == 2) return cell.bs;
return cell.ls;
}));
} // polyPiece.listLoops
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
drawPath(ctx, shiftx, shifty) {
// ctx.beginPath(); No, not for Path2D
this.tbLoops.forEach(loop => {
let without = false;
loop.forEach(side => {
side.drawPath(ctx, shiftx, shifty, without);
without = true;
});
ctx.closePath();
});
} // PolyPiece.drawPath
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
drawImage() {
/* resizes canvas to be bigger than if pieces were perfect rectangles
so that their shapes actually fit in the canvas
copies the relevant part of gamePicture clipped by path
adds shadow and emboss
*/
// if (this.pieces[0].kx!=1 ||this.pieces[0].ky!= 1) return;
puzzle = this.puzzle;
this.nx = this.pckxmax - this.pckxmin + 1;
this.ny = this.pckymax - this.pckymin + 1;
this.canvas.width = this.nx * puzzle.scalex;
this.canvas.height = this.ny * puzzle.scaley;
// difference between position in this canvas and position in gameImage
this.offsx = (this.pckxmin - 0.5) * puzzle.scalex;
this.offsy = (this.pckymin - 0.5) * puzzle.scaley;
this.path = new Path2D();
this.drawPath(this.path, -this.offsx, -this.offsy);
// make shadow
this.ctx.fillStyle = 'none';
this.ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
this.ctx.shadowBlur = 4;
this.ctx.shadowOffsetX = 4;
this.ctx.shadowOffsetY = 4;
this.ctx.fill(this.path);
this.ctx.shadowColor = 'rgba(0, 0, 0, 0)'; // stop shadow effect
this.pieces.forEach((pp, kk) => {
this.ctx.save();
const path = new Path2D();
const shiftx = -this.offsx;
const shifty = -this.offsy;
pp.ts.drawPath(path, shiftx, shifty, false);
pp.rs.drawPath(path, shiftx, shifty, true);
pp.bs.drawPath(path, shiftx, shifty, true);
pp.ls.drawPath(path, shiftx, shifty, true);
path.closePath();
this.ctx.clip(path);
// do not copy from negative coordinates, does not work for all browsers
const srcx = pp.kx ? ((pp.kx - 0.5) * puzzle.scalex) : 0;
const srcy = pp.ky ? ((pp.ky - 0.5) * puzzle.scaley) : 0;
const destx = (pp.kx ? 0 : puzzle.scalex / 2) + (pp.kx - this.pckxmin) * puzzle.scalex;
const desty = (pp.ky ? 0 : puzzle.scaley / 2) + (pp.ky - this.pckymin) * puzzle.scaley;
let w = 2 * puzzle.scalex;
let h = 2 * puzzle.scaley;
if (srcx + w > puzzle.gameCanvas.width) w = puzzle.gameCanvas.width - srcx;
if (srcy + h > puzzle.gameCanvas.height) h = puzzle.gameCanvas.height - srcy;
this.ctx.drawImage(puzzle.gameCanvas, srcx, srcy, w, h,
destx, desty, w, h);
this.ctx.translate(puzzle.embossThickness / 2, -puzzle.embossThickness / 2);
this.ctx.lineWidth = puzzle.embossThickness;
this.ctx.strokeStyle = "rgba(0, 0, 0, 0.35)";
this.ctx.stroke(path);
this.ctx.translate(-puzzle.embossThickness, puzzle.embossThickness);
this.ctx.strokeStyle = "rgba(255, 255, 255, 0.35)";
this.ctx.stroke(path);
this.ctx.restore();
});
} // PolyPiece.drawImage
moveTo(x, y) {
// sets the left, top properties (relative to container) of this.canvas
this.x = x;
this.y = y;
this.canvas.style.left = x + 'px';
this.canvas.style.top = y + 'px';
} //
moveToInitialPlace() {
const puzzle = this.puzzle;
this.moveTo(puzzle.offsx + (this.pckxmin - 0.5) * puzzle.scalex,
puzzle.offsy + (this.pckymin - 0.5) * puzzle.scaley);
}
} // class PolyPiece
//-----------------------------------------------------------------------------
class Puzzle {
/*
params contains :
container : mandatory - given by id (string) or element
it will not be resized in this script
ONLY ONE Puzzle object should be instanced.
only "container is mandatory, nbPieces and pictures may be provided to get
initial default values.
Once a puzzle is solved (and event if not solved) another game can be played
by changing the image file or the number of pieces, NOT by invoking new Puzzle
*/
constructor(params) {
this.autoStart = false;
this.container = (typeof params.container == "string") ?
document.getElementById(params.container) :
params.container;
/* the following code will add the event Handlers several times if
new Puzzle objects are created with same container.
the presence of previous event listeners is NOT detectable
*/
this.container.addEventListener("mousedown", event => {
event.preventDefault();
events.push({ event: 'touch', position: this.relativeMouseCoordinates(event) });
});
this.container.addEventListener("touchstart", event => {
event.preventDefault();
if (event.touches.length != 1) return;
let ev = event.touches[0];
events.push({ event: 'touch', position: this.relativeMouseCoordinates(ev) });
}, { passive: false });
this.container.addEventListener("mouseup", event => {
event.preventDefault();
handleLeave();
});
this.container.addEventListener("touchend", handleLeave);
this.container.addEventListener("touchleave", handleLeave);
this.container.addEventListener("touchcancel", handleLeave);
this.container.addEventListener("mousemove", event => {
event.preventDefault();
// do not accumulate move events in events queue - keep only current one
if (events.length && events[events.length - 1].event == "move") events.pop();
events.push({ event: 'move', position: this.relativeMouseCoordinates(event) })
});
this.container.addEventListener("touchmove", event => {
event.preventDefault();
if (event.touches.length != 1) return;
let ev = event.touches[0];
// do not accumulate move events in events queue - keep only current one
if (events.length && events[events.length - 1].event == "move") events.pop();
events.push({ event: 'move', position: this.relativeMouseCoordinates(ev) });
}, { passive: false });
/* create canvas to contain picture - will be styled later */
this.gameCanvas = document.createElement('CANVAS');
this.container.appendChild(this.gameCanvas)
this.srcImage = new Image();
this.imageLoaded = false;
this.srcImage.addEventListener("load", () => imageLoaded(this));
function handleLeave() {
events.push({ event: 'leave' }); //
}
} // Puzzle
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
getContainerSize() {
let styl = window.getComputedStyle(this.container);
/* dimensions of container */
this.contWidth = parseFloat(styl.width);
this.contHeight = parseFloat(styl.height);
}
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
create() {
this.container.innerHTML = ""; // forget contents
/* define the number of rows / columns to have almost square pieces
and a total number as close as possible to the requested number
*/
this.getContainerSize();
this.computenxAndny();
/* assuming the width of pieces is 1, computes their height
(computenxAndny aims at making relativeHeight as close as possible to 1)
*/
this.relativeHeight = (this.srcImage.naturalHeight / this.ny) / (this.srcImage.naturalWidth / this.nx);
this.defineShapes({ coeffDecentr: 0.12, twistf: [twist0, twist1, twist2, twist3][document.getElementById("shape").value - 1] });
this.polyPieces = [];
this.pieces.forEach(row => row.forEach(piece => {
this.polyPieces.push(new PolyPiece(piece, this));
}));
arrayShuffle(this.polyPieces);
this.evaluateZIndex();
} // Puzzle.create
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/* computes the number of lines and columns of the puzzle,
finding the best compromise between the requested number of pieces
and a square shap for pieces
result in this.nx and this.ny;
*/
computenxAndny() {
let kx, ky, width = this.srcImage.naturalWidth, height = this.srcImage.naturalHeight, npieces = this.nbPieces;
let err, errmin = 1e9;
let ncv, nch;
let nHPieces = mround(msqrt(npieces * width / height));
let nVPieces = mround(npieces / nHPieces);
/* based on the above estimation, we will try up to + / - 2 values
and evaluate (arbitrary) quality criterion to keep best result
*/
for (ky = 0; ky < 5; ky++) {
ncv = nVPieces + ky - 2;
for (kx = 0; kx < 5; kx++) {
nch = nHPieces + kx - 2;
err = nch * height / ncv / width;
err = (err + 1 / err) - 2; // error on pieces dimensions ratio)
err += mabs(1 - nch * ncv / npieces); // adds error on number of pieces
if (err < errmin) { // keep smallest error
errmin = err;
this.nx = nch;
this.ny = ncv;
}
} // for kx
} // for ky
} // computenxAndny
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
defineShapes(shapeDesc) {
// define shapes as if the width and height of a piece were 1
/* first, place the corners of the pieces
at some distance of their theoretical position, except for edges
*/
let { coeffDecentr, twistf } = shapeDesc;
const corners = [];
const nx = this.nx, ny = this.ny;
let np;
for (let ky = 0; ky <= ny; ++ky) {
corners[ky] = [];
for (let kx = 0; kx <= nx; ++kx) {
corners[ky][kx] = new Point(kx + alea(-coeffDecentr, coeffDecentr),
ky + alea(-coeffDecentr, coeffDecentr));
if (kx == 0) corners[ky][kx].x = 0;
if (kx == nx) corners[ky][kx].x = nx;
if (ky == 0) corners[ky][kx].y = 0;
if (ky == ny) corners[ky][kx].y = ny;
} // for kx
} // for ky
// Array of pieces
this.pieces = [];
for (let ky = 0; ky < ny; ++ky) {
this.pieces[ky] = [];
for (let kx = 0; kx < nx; ++kx) {
this.pieces[ky][kx] = np = new Piece(kx, ky);
// top side
if (ky == 0) {
np.ts.points = [corners[ky][kx], corners[ky][kx + 1]];
np.ts.type = "d";
} else {
np.ts = this.pieces[ky - 1][kx].bs.reversed();
}
// right side
np.rs.points = [corners[ky][kx + 1], corners[ky + 1][kx + 1]];
np.rs.type = "d";
if (kx < nx - 1) {
if (intAlea(2)) // randomly twisted on one side of the side
twistf(np.rs, corners[ky][kx], corners[ky + 1][kx]);
else
twistf(np.rs, corners[ky][kx + 2], corners[ky + 1][kx + 2]);
}
// left side
if (kx == 0) {
np.ls.points = [corners[ky + 1][kx], corners[ky][kx]];
np.ls.type = "d";
} else {
np.ls = this.pieces[ky][kx - 1].rs.reversed()
}
// bottom side
np.bs.points = [corners[ky + 1][kx + 1], corners[ky + 1][kx]];
np.bs.type = "d";
if (ky < ny - 1) {
if (intAlea(2)) // randomly twisted on one side of the side
twistf(np.bs, corners[ky][kx + 1], corners[ky][kx]);
else
twistf(np.bs, corners[ky + 2][kx + 1], corners[ky + 2][kx]);
}
} // for kx
} // for ky
} // Puzzle.defineShapes
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
scale() {
// we suppose we want the picture to fill 90% on width or height and less or same on other dimension
// this 90% might be changed and depend on the number of columns / rows.
const maxWidth = 0.95 * this.contWidth;
const maxHeight = 0.95 * this.contHeight;
// suppose image fits in height
this.gameHeight = maxHeight;
this.gameWidth = this.gameHeight * this.srcImage.naturalWidth / this.srcImage.naturalHeight;
if (this.gameWidth > maxWidth) { // too wide, fits in width
this.gameWidth = maxWidth;
this.gameHeight = this.gameWidth * this.srcImage.naturalHeight / this.srcImage.naturalWidth;
}
/* get a scaled copy of the source picture into a canvas */
// this.gameCanvas = document.createElement('CANVAS');
this.gameCanvas.width = this.gameWidth;
this.gameCanvas.height = this.gameHeight;
this.gameCtx = this.gameCanvas.getContext("2d");
this.gameCtx.drawImage(this.srcImage, 0, 0, this.gameWidth, this.gameHeight);
this.gameCanvas.classList.add("gameCanvas");
this.gameCanvas.style.zIndex = 500;
// this.container.appendChild(this.gameCanvas)
/* scale pieces */
this.scalex = this.gameWidth / this.nx; // average width of pieces
this.scaley = this.gameHeight / this.ny; // average height of pieces
this.pieces.forEach(row => {
row.forEach(piece => piece.scale(this));
}); // this.pieces.forEach
/* calculate offset for centering image in container */
this.offsx = (this.contWidth - this.gameWidth) / 2;
this.offsy = (this.contHeight - this.gameHeight) / 2;
/* computes the distance below which two pieces connect
depends on the actual size of pieces, with lower limit */
this.dConnect = mmax(10, mmin(this.scalex, this.scaley) / 10);
/* computes the thickness used for emboss effect */
// from 2 (scalex = 0) to 5 (scalex = 200), not more than 5
this.embossThickness = mmin(2 + this.scalex / 200 * (5 - 2), 5);
} // Puzzle.scale
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
relativeMouseCoordinates(event) {
/* takes mouse coordinates from mouse event
returns coordinates relative to container, even if page is scrolled or zoommed */
const br = this.container.getBoundingClientRect();
return {
x: event.clientX - br.x,
y: event.clientY - br.y
};
} // Puzzle.relativeMouseCoordinates
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
limitRectangle(rect) {
/* limits the possible position for the coordinates of a piece, to prevent it from beeing out of the
container */
rect.x0 = mmin(mmax(rect.x0, -this.scalex / 2), this.contWidth - 1.5 * this.scalex);
rect.x1 = mmin(mmax(rect.x1, -this.scalex / 2), this.contWidth - 1.5 * this.scalex);
rect.y0 = mmin(mmax(rect.y0, -this.scaley / 2), this.contHeight - 1.5 * this.scaley);
rect.y1 = mmin(mmax(rect.y1, -this.scaley / 2), this.contHeight - 1.5 * this.scaley);
}
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
spreadInRectangle(rect) {
this.limitRectangle(rect);
this.polyPieces.forEach(pp =>
pp.moveTo(alea(rect.x0, rect.x1), alea(rect.y0, rect.y1))
);
} // spreadInRectangle
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
spreadSetInRectangle(set, rect) {
this.limitRectangle(rect);
set.forEach(pp =>
pp.moveTo(alea(rect.x0, rect.x1), alea(rect.y0, rect.y1))
);
} // spreadInRectangle
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
optimInitial() {
/* based on :
- container dimensions
- picture dimensions
- piece dimensions
moves the pieces at the beginning of the game along one to four sides of the container
*/
// extreme values for 1 piece polypieces
const minx = -this.scalex / 2;
const miny = -this.scaley / 2;
const maxx = this.contWidth - 1.5 * this.scalex;
const maxy = this.contHeight - 1.5 * this.scaley;
// how much space left around image ?
let freex = this.contWidth - this.gameWidth;
let freey = this.contHeight - this.gameHeight;
let where = [0, 0, 0, 0]; // to record on which sides pieces will be moved
let rects = [];
// first evaluation
if (freex > 1.5 * this.scalex) {
where[1] = 1; // right
rects[1] = {
x0: this.gameWidth - 0.5 * this.scalex,
x1: maxx,
y0: miny, y1: maxy
};
}
if (freex > 3 * this.scalex) {
where[3] = 1; // left
rects[3] = {
x0: minx,
x1: freex / 2 - 1.5 * this.scalex,
y0: miny, y1: maxy
};
rects[1].x0 = this.contWidth - freex / 2 - 0.5 * this.scalex;
}
if (freey > 1.5 * this.scaley) {
where[2] = 1; // bottom
rects[2] = {
x0: minx, x1: maxx,
y0: this.gameHeight - 0.5 * this.scaley,
y1: this.contHeight - 1.5 * this.scaley
};
}
if (freey > 3 * this.scaley) {
where[0] = 1; // top
rects[0] = {
x0: minx, x1: maxx,
y0: miny,
y1: freey / 2 - 1.5 * this.scaley
};
rects[2].y0 = this.contHeight - freey / 2 - 0.5 * this.scaley;
}
if (where.reduce((sum, a) => sum + a) < 2) {
// if no place defined yet, or only one place
if (freex - freey > 0.2 * this.scalex || where[1]) {
// significantly more place horizontally : to right
this.spreadInRectangle({
x0: this.gameWidth - this.scalex / 2,
x1: maxx,
y0: miny,
y1: maxy
});
} else if (freey - freex > 0.2 * this.scalex || where[2]) {
// significantly more place vertically : to bottom
this.spreadInRectangle({
x0: minx,
x1: maxx,
y0: this.gameHeight - this.scaley / 2,
y1: maxy
});
} else {
if (this.gameWidth > this.gameHeight) {
// more wide than high : to bottom
this.spreadInRectangle({
x0: minx,
x1: maxx,
y0: this.gameHeight - this.scaley / 2,
y1: maxy
});
} else { // to right
this.spreadInRectangle({
x0: this.gameWidth - this.scalex / 2,
x1: maxx,
y0: miny,
y1: maxy
});
}
}
return;
}
/* more than one area to put the pieces
*/
let nrects = [];
rects.forEach(rect => {
nrects.push(rect);
});
let k0 = 0
const npTot = this.nx * this.ny;
for (let k = 0; k < nrects.length; ++k) {
let k1 = mround((k + 1) / nrects.length * npTot);
this.spreadSetInRectangle(this.polyPieces.slice(k0, k1), nrects[k]);
k0 = k1;
}
arrayShuffle(this.polyPieces);
this.evaluateZIndex();
} // optimInitial
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
evaluateZIndex() {
/* re-evaluates order of polypieces in puzzle after a merge
the polypieces must be in decreasing order of size(number of pieces),
preserving the previous order as much as possible
*/
for (let k = this.polyPieces.length - 1; k > 0; --k) {
if (this.polyPieces[k].pieces.length > this.polyPieces[k - 1].pieces.length) {
// swap pieces if not in right order
[this.polyPieces[k], this.polyPieces[k - 1]] = [this.polyPieces[k - 1], this.polyPieces[k]];
}
}
// re-assign zIndex
this.polyPieces.forEach((pp, k) => {
pp.canvas.style.zIndex = k + 10;
});
this.zIndexSup = this.polyPieces.length + 10; // higher than 'normal' zIndices
} // Puzzle.evaluateZIndex
} // class Puzzle
//-----------------------------------------------------------------------------
let loadFile;
{ // scope for loadFile
let options;
let elFile = document.createElement('input');
elFile.setAttribute('type', 'file');
elFile.style.display = 'none';
elFile.addEventListener("change", getFile);
function getFile() {
if (this.files.length == 0) {
// returnLoadFile ({fail: 'no file'});
return;
}
let file = this.files[0];
let reader = new FileReader();
reader.addEventListener('load', () => {
puzzle.srcImage.src = reader.result;
});
reader.readAsDataURL(this.files[0]);
} // getFile
loadFile = function (ooptions) {
elFile.setAttribute("accept", "image/*");
elFile.value = null; // else, re-selecting the same file does not trigger "change"
elFile.click();
} // loadFile
} // // scope for loadFile
function loadInitialFile() {
puzzle.srcImage.src = "https://assets.codepen.io/2574552/Mona_Lisa.jpg";
}
//-----------------------------------------------------------------------------
function imageLoaded(puzzle) {
events.push({ event: "srcImageLoaded" });
puzzle.imageLoaded = true;
} // imageLoaded
//-----------------------------------------------------------------------------
function fitImage(img, width, height) {
/* The image is a child of puzzle.container. It will be styled to be as big as possible, not wider than width,
not higher than height, centered in puzzle.container
(width and height must be less than or equal to the container dimensions)
*/
let wn = img.naturalWidth;
let hn = img.naturalHeight;
let w = width;
let h = w * hn / wn;
if (h > height) {
h = height;
w = h * wn / hn;
}
img.style.position = "absolute";
img.style.width = w + "px";
img.style.height = h + "px";
img.style.top = "50%";
img.style.left = "50%";
img.style.transform = "translate(-50%,-50%)";
}
//-----------------------------------------------------------------------------
let animate;
let events = []; // queue for events
{ // scope for animate
let state = 0;
let moving; // for information about moved piece
let tmpImage;
animate = function () {
requestAnimationFrame(animate);
let event;
if (events.length) event = events.shift(); // read event from queue
if (event && event.event == "reset") state = 0;
if (event && event.event == "srcImageLoaded") state = 0;
// resize event
if (event && event.event == "resize") {
// remember dimensions of container before resize
puzzle.prevWidth = puzzle.contWidth;
puzzle.prevHeight = puzzle.contHeight;
puzzle.getContainerSize();
if (state == 15 || state > 60) { // resize initial or final picture
puzzle.getContainerSize();
fitImage(tmpImage, puzzle.contWidth * 0.95, puzzle.contHeight * 0.95);
}
else if (state >= 25) { // resize pieces
puzzle.prevGameWidth = puzzle.gameWidth;
puzzle.prevGameHeight = puzzle.gameHeight;
puzzle.scale();
let reScale = puzzle.contWidth / puzzle.prevWidth;
puzzle.polyPieces.forEach(pp => {
// compute new position : game centered homothety
let nx = puzzle.contWidth / 2 - (puzzle.prevWidth / 2 - pp.x) * reScale;
let ny = puzzle.contHeight / 2 - (puzzle.prevHeight / 2 - pp.y) * reScale;
// enforce pieces to stay in game area
nx = mmin(mmax(nx, -puzzle.scalex / 2), puzzle.contWidth - 1.5 * puzzle.scalex);
ny = mmin(mmax(ny, -puzzle.scaley / 2), puzzle.contHeight - 1.5 * puzzle.scaley);
pp.moveTo(nx, ny);
pp.drawImage();
}); // puzzle.polypieces.forEach
}
return;
} // resize event
// if (event) console.log (event);
switch (state) {
/* initialisation */
case 0:
state = 10;
break;
/* wait for image loaded and other required parameters*/
case 10:
if (!puzzle.imageLoaded) return;
// if (!(puzzle.autoStart || event && event.event == "srcImageLoaded")) return;
// display centered initial image
puzzle.container.innerHTML = ""; // forget contents
tmpImage = document.createElement("img");
tmpImage.src = puzzle.srcImage.src;
puzzle.getContainerSize();
fitImage(tmpImage, puzzle.contWidth * 0.95, puzzle.contHeight * 0.95);
tmpImage.style.boxShadow = "4px 4px 4px rgba(0, 0, 0, 0.5)";
puzzle.container.appendChild(tmpImage);
state = 15;
break;
/* wait for choice of number of pieces */
case 15:
if (autoStart) event = { event: "nbpieces", nbpieces: 12 }; // auto start
autoStart = false; // not twice
if (!event) return;
if (event.event == "nbpieces") {
puzzle.nbPieces = event.nbpieces;
state = 20;
} else if (event.event == "srcImageLoaded") {
state = 10;
return;
} else return;
case 20:
menu.close();
/* prepare puzzle */
puzzle.create(); // create shape of pieces, independant of size
puzzle.scale();
puzzle.polyPieces.forEach(pp => {
pp.drawImage();
pp.moveToInitialPlace();
}); // puzzle.polypieces.forEach
puzzle.gameCanvas.style.top = puzzle.offsy + "px";
puzzle.gameCanvas.style.left = puzzle.offsx + "px";
puzzle.gameCanvas.style.display = "block";
state = 25;
break;
case 25: // spread pieces
puzzle.gameCanvas.style.display = "none"; // hide reference image
puzzle.polyPieces.forEach(pp => {
pp.canvas.classList.add("moving");
});
state = 30;
break;
case 30: // launch movement
puzzle.optimInitial(); // initial "optimal" spread position
/* this time out must be a bit longer than the css .moving transition-duration */
setTimeout(() => events.push({ event: "finished" }), 1200);
state = 35;
break;
case 35: // wait for end of movement
if (!event || event.event != "finished") return;
puzzle.polyPieces.forEach(pp => {
pp.canvas.classList.remove("moving");
});
state = 50;
break;
/* wait for user grabbing a piece or other action */
case 50:
if (!event) return;
if (event.event == "nbpieces") {
puzzle.nbPieces = event.nbpieces;
state = 20;
return;
}
if (event.event != "touch") return;
moving = {
xMouseInit: event.position.x,
yMouseInit: event.position.y
}
/* evaluates if contact inside a PolyPiece, by decreasing z-index */
for (let k = puzzle.polyPieces.length - 1; k >= 0; --k) {
let pp = puzzle.polyPieces[k];
if (pp.ctx.isPointInPath(pp.path, event.position.x - pp.x, event.position.y - pp.y)) {
moving.pp = pp;
moving.ppXInit = pp.x;
moving.ppYInit = pp.y;
// move selected piece to top of polypieces stack
puzzle.polyPieces.splice(k, 1);
puzzle.polyPieces.push(pp);
pp.canvas.style.zIndex = puzzle.zIndexSup; // to foreground
state = 55;
return;
}
} // for k
break;
case 55: // moving piece
if (!event) return;
switch (event.event) {
case "move":
moving.pp.moveTo(event.position.x - moving.xMouseInit + moving.ppXInit,
event.position.y - moving.yMouseInit + moving.ppYInit);
break;
case "leave":
// check if moved polypiece is close to a matching other polypiece
// check repeatedly since polypieces moved by merging may come close to other polypieces
let doneSomething;
do {
doneSomething = false;
for (let k = puzzle.polyPieces.length - 1; k >= 0; --k) {
let pp = puzzle.polyPieces[k];
if (pp == moving.pp) continue; // don't match with myself
if (moving.pp.ifNear(pp)) { // a match !
// compare polypieces sizes to move smallest one
if (pp.pieces.length > moving.pp.pieces.length) {
pp.merge(moving.pp);
moving.pp = pp; // memorize piece to follow
} else {
moving.pp.merge(pp);
}
doneSomething = true;
break;
}
} // for k
} while (doneSomething);
// not at its right place
puzzle.evaluateZIndex();
state = 50;
if (puzzle.polyPieces.length == 1) state = 60; // won!
return;
} // switch (event.event)
break;
case 60: // winning
puzzle.container.innerHTML = "";
puzzle.getContainerSize();
fitImage(tmpImage, puzzle.contWidth * 0.95, puzzle.contHeight * 0.95);
tmpImage.style.boxShadow = "4px 4px 4px rgba(0, 0, 0, 0.5)";
// tmpImage.style.top=(puzzle.polyPieces[0].y + puzzle.scaley / 2) / puzzle.contHeight * 100 + 50 + "%" ;
// tmpImage.style.left=(puzzle.polyPieces[0].x + puzzle.scalex / 2) / puzzle.contWidth * 100 + 50 + "%" ;
tmpImage.style.left = (puzzle.polyPieces[0].x + puzzle.scalex / 2 + puzzle.gameWidth / 2) / puzzle.contWidth * 100 + "%";
tmpImage.style.top = (puzzle.polyPieces[0].y + puzzle.scaley / 2 + puzzle.gameHeight / 2) / puzzle.contHeight * 100 + "%";
tmpImage.classList.add("moving");
setTimeout(() => tmpImage.style.top = tmpImage.style.left = "50%", 0);
puzzle.container.appendChild(tmpImage);
state = 65;
menu.open();
case 65: // wait for new number of pieces - of new picture
if (event && event.event == "nbpieces") {
puzzle.nbPieces = event.nbpieces;
state = 20;
return;
}
break;
case 9999: break;
default:
let st = state;
state = 9999; // to display message beyond only once
throw ("oops, unknown state " + st);
} // switch(state)
} // animate
} // scope for animate
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
/* analyze menu */
let menu = (function () {
let menu = { items: [] };
document.querySelectorAll("#menu li").forEach(menuEl => {
let kItem = menu.items.length;
let item = { element: menuEl, kItem: kItem };
menu.items[kItem] = item;
});
menu.open = function () {
menu.items.forEach(item => item.element.style.display = "block");
menu.opened = true;
}
menu.close = function () {
menu.items.forEach((item, k) => {
if (k > 0) item.element.style.display = "none"; // never hide element 0
});
menu.opened = false;
}
menu.items[0].element.addEventListener("click", () => {
if (menu.opened) menu.close(); else menu.open()
});
menu.items[1].element.addEventListener("click", loadInitialFile);
menu.items[2].element.addEventListener("click", loadFile);
menu.items[3].element.addEventListener("click", () => { });
for (let k = 4; k < menu.items.length; ++k) {
menu.items[k].element.addEventListener("click", () => events.push({ event: "nbpieces", nbpieces: [12, 25, 50, 100, 200][k - 4] }));
}
return menu;
})();
menu.close();
window.addEventListener("resize", event => {
// do not accumulate resize events in events queue - keep only current one
if (events.length && events[events.length - 1].event == "resize") return;;
events.push({ event: "resize" });
});
puzzle = new Puzzle({ container: "forPuzzle" });
autoStart = isMiniature(); // used for nice miniature in CodePen
loadInitialFile();
requestAnimationFrame(animate);
Also see: Tab Triggers