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.
<!-- click to trigger a new transition -->
body {
font-family: Arial, Helvetica, "Liberation Sans", FreeSans, sans-serif;
background-color: #000;
margin: 0;
padding: 0;
border-width: 0;
cursor: pointer;
}
"use strict";
let canv, ctx; // canvas and context
// let maxx, maxy; // canvas dimensions
let tr;
let maxx, maxy;
let screenh, screenw;
let img1, img2;
let oimg1, oimg2;
// for animation
const messages = [];
// shortcuts for Math.
const mrandom = Math.random;
const mfloor = Math.floor;
const mround = Math.round;
const mceil = Math.ceil;
const mabs = Math.abs;
const mmin = Math.min;
const mmax = Math.max;
const mPI = Math.PI;
const mPIS2 = Math.PI / 2;
const mPIS3 = Math.PI / 3;
const m2PI = Math.PI * 2;
const m2PIS3 = (Math.PI * 2) / 3;
const msin = Math.sin;
const mcos = Math.cos;
const matan2 = Math.atan2;
const mhypot = Math.hypot;
const msqrt = Math.sqrt;
//------------------------------------------------------------------------
function alea(mini, maxi) {
// random number in given range
if (typeof maxi == "undefined") return mini * mrandom(); // range 0..mini
return mini + mrandom() * (maxi - mini); // range mini..maxi
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function intAlea(mini, maxi) {
// random integer in given range (mini..maxi - 1 or 0..mini - 1)
//
if (typeof maxi == "undefined") return mfloor(mini * mrandom()); // range 0..mini - 1
return mini + mfloor(mrandom() * (maxi - mini)); // range mini .. maxi - 1
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function lerp(p0, p1, alpha) {
const uma = 1 - alpha;
return {
x: uma * p0.x + alpha * p1.x,
y: uma * p0.y + alpha * p1.y
};
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function cutQ(quadra, alpha) {
// cut quadratic
/* quadra is a set of 3 points definining a quadratic Bézier curve
this function returns a set of 2 quadratic curves (sets of 3 points) repsenting a division of the 2 initial curves,
cut at an intermediate point defined by alpha (0..1)
the two sub-arcs share a common point, and re-use copies of quadra
caution : alpha is not a length ratio, it is only the value of the t variable of the parametric equation of the curve
*/
const pa = lerp(quadra[0], quadra[1], alpha);
const pb = lerp(quadra[1], quadra[2], alpha);
const pc = lerp(pa, pb, alpha);
return [
[quadra[0], pa, pc],
[pc, pb, quadra[2]]
];
} // cutQ
//------------------------------------------------------------------------
function generatePoints(t) {
return t.points.map((p) => ({ x: p.x, y: p.y })); // shallow copy
} // generate points
//------------------------------------------------------------------------
class SortedArray {
/* creates a sorted array of any kind of things, by intersting them into an initially empty array
the comparison function used for sorting is given to the constructor
Things are inserted using .insert method
sorted array is in the .tb property of the instance
*/
/*
the indexOf property lets you know if thing already existed according to fCompar, and at what index
just use doInsert(no parameters) after indexOf to insert thing at found (not -1) index
*/
/* CAUTION : if duplicates are allowed, indexOf is NOT garanteed to return the index of actual thing - only a thing
which returns 0 when compared with given thing. Use regular Array.indexOf on instance.tb instead.
*/
constructor(fCompar, keepDuplicates = false) {
/* fCompar is the function which will be called to compare the things that will be inserted
in this.tb
fCompar(a,b) must return number < 0 if a must be placed before b
== 0 if a and b are considered equal
> 0 if a must be placed after b
*/
this.tb = [];
this.fCompar = fCompar;
this.keepDuplicates = keepDuplicates;
}
indexOf(thing) {
this.thing = thing;
// search for place to insert thing, using comparison function this.fCompar
// if found, returns the index of thing in this.tb, else returns -1
// sets this.insertAt for future insertion
let cmp;
if (this.tb.length == 0) {
this.insertAt = 0;
return -1;
}
let a = 0,
c = this.tb.length - 1;
let b;
do {
b = Math.floor((a + c) / 2);
cmp = this.fCompar(this.tb[b], thing);
switch (true) {
case cmp < 0: // thing after b
if (b == c) {
// beyond c
this.insertAt = c + 1;
return -1;
}
if (a == b) ++b; // not to stay on same interval, if its length is 1 or 2
a = b; // after b : search (b, c) now
break;
case cmp == 0:
this.insertAt = b;
return b; // found !
default:
//thing before b
if (b == a) {
// before a
this.insertAt = a;
return -1;
}
c = b; // search (a, b) now
}
} while (true);
} // indexOf
doInsert() {
// DO NOT CALL TWICE WITHOUT getting new (!= -1) indexOf
this.tb.splice(this.insertAt, 0, this.thing);
}
insert(thing) {
/* inserts thing */
if (this.indexOf(thing) != -1 && !this.keepDuplicates) return; // already exists and refused
this.tb.splice(this.insertAt, 0, thing);
}
} // class SortedArray
//------------------------------------------------------------------------
class Edge {
constructor(p0, p1) {
if (p0.kp <= p1.kp) {
this.p0 = p0;
this.p1 = p1;
} else {
this.p0 = p1;
this.p1 = p0;
}
this.tris = []; // to record up to 2 triangles attached to this edge
}
attachTriangle(tri) {
// includes a triangle in an edge's tris array
// AND includes itself in this triangle's edges array
// AND more
if (!this.p0.tris.includes(tri)) this.p0.tris.push(tri);
if (!this.p1.tris.includes(tri)) this.p1.tris.push(tri);
if (!this.p0.edges.includes(this)) this.p0.edges.push(this);
if (!this.p1.edges.includes(this)) this.p1.edges.push(this);
if (tri.a == this.p0) {
if (tri.b == this.p1) {
this.tris[0] = tri;
tri.edges[0] = this;
} else {
this.tris[1] = tri;
tri.edges[2] = this;
}
return;
}
if (tri.b == this.p0) {
if (tri.c == this.p1) {
this.tris[0] = tri;
tri.edges[1] = this;
} else {
this.tris[1] = tri;
tri.edges[0] = this;
}
return;
}
if (tri.c == this.p0) {
if (tri.a == this.p1) {
this.tris[0] = tri;
tri.edges[2] = this;
} else {
this.tris[1] = tri;
tri.edges[1] = this;
}
}
}
} // class Edge
//------------------------------------------------------------------------
class Triangle {
constructor(a, b, c) {
this.a = a;
this.b = b;
this.c = c;
this.edges = [];
this.vertices = [this.a, this.b, this.c];
const m11 = 2 * (b.x - a.x);
const m21 = 2 * (c.x - a.x);
const m12 = 2 * (b.y - a.y);
const m22 = 2 * (c.y - a.y);
const c1 = b.x * b.x - a.x * a.x + b.y * b.y - a.y * a.y;
const c2 = c.x * c.x - a.x * a.x + c.y * c.y - a.y * a.y;
const det = m11 * m22 - m21 * m12;
this.xc = (c1 * m22 - c2 * m12) / det;
this.yc = (m11 * c2 - m21 * c1) / det;
this.r = Math.hypot(this.xc - this.a.x, this.yc - this.a.y);
} // constructor
inCircumCircle(p) {
return Math.hypot(p.x - this.xc, p.y - this.yc) < this.r;
}
hasEdge(p1, p2) {
// (was written before the above "Edge" class)
return (
(p1 == this.a || p1 == this.b || p1 == this.c) &&
(p2 == this.a || p2 == this.b || p2 == this.c)
);
}
listTris() {
let other;
this.tris = [];
this.edges.forEach((edge, kEdge) => {
other = edge.tris[0] == this ? edge.tris[1] : edge.tris[0];
if (other) this.tris[kEdge] = other;
});
this.g = {
x: (this.a.x + this.b.x + this.c.x) / 3,
y: (this.a.y + this.b.y + this.c.y) / 3
};
} // listTris
//------------------------------------------------------------------------
inTriangle(p) {
let va = { x: this.a.x - p.x, y: this.a.y - p.y };
let vb = { x: this.b.x - p.x, y: this.b.y - p.y };
let vc = { x: this.c.x - p.x, y: this.c.y - p.y };
if (va.x * vb.y - va.y * vb.x < 0) return false;
if (vb.x * vc.y - vb.y * vc.x < 0) return false;
if (vc.x * va.y - vc.y * va.x < 0) return false;
return true;
} // inTriangle
} // Triangle
//------------------------------------------------------------------------
function createPath(tr) {
tr.triangulation.forEach((tri) => {
delete tri.path;
delete tri.links;
});
let tri, l1, l2;
const path = [];
// begin with triangle close to the center - arbitrary choice
const picked = [
tr.triangulation.find((tri) =>
tri.inTriangle({ x: screenw / 2, y: screenh / 2 })
)
];
path.tr0 = picked[0]; // root of path
while (picked.length) {
tri = picked[intAlea(picked.length)];
pwet: while (true) {
let ntri = tri;
path.push(tri);
tri.path = path;
let possibles = [];
tri.edges.forEach((edge) => {
let otherTri = edge.tris[0] === tri ? edge.tris[1] : edge.tris[0];
if (!otherTri) return; // no other triangle here
if (
otherTri.vertices.find((vert) => !vert.tris.find((ttri) => ttri.path))
)
possibles.push(otherTri);
});
while (possibles.length) {
tri = possibles.splice(intAlea(possibles.length), 1)[0];
picked.push(tri);
// record which sides is connected to which triangle
// on tri side
let kedge = ntri.edges.findIndex((edge) => tri.edges.includes(edge));
ntri.links = ntri.links || [];
ntri.links.push((l1 = { kedge, tri: ntri }));
kedge = tri.edges.findIndex((edge) => ntri.edges.includes(edge));
tri.links = tri.links || [];
tri.links.push((l2 = { kedge, tri }));
l1.reciprocal = l2;
l2.reciprocal = l1;
if (ntri == path.tr0) break; // because we want tr0 to be at the end of a branch
continue pwet;
} // while possibles.length
picked.splice(picked.indexOf(ntri), 1); // no more possibilities on this triangles, throw it
break;
} // while true;
} // while (picked.length)
return path;
} // createPath
//------------------------------------------------------------------------
function smoothPath(path) {
path.forEach((tri) => (tri.picked = false));
// replace actual centroid of triangle by point at 1/2 lenth of median if only 2 links
path.forEach((tri) => {
if (tri.links.length != 2) return;
// base side is the one not in the kedges of the links
let kbase = [0, 1, 2];
kbase.splice(tri.links[0].kedge, 1);
kbase.splice(kbase.indexOf(tri.links[1].kedge), 1);
kbase = kbase[0];
tri.g = lerp(tri.vertices[kbase], tri.vertices[(kbase + 1) % 3], 0.5); // middle of base
tri.g = lerp(tri.g, tri.vertices[(kbase + 2) % 3], 0.5); // middle of base
});
(function smoothSubPath(tri) {
let ntri, pinterm, nlink;
if (tri.picked) return; // already processed
tri.picked = true; // will be processed now
if (tri.links.length == 2) tri.curved = true;
tri.links.forEach((link) => {
if (link.pinterm) return; // this link already studied coming from other side
ntri = link.reciprocal.tri;
pinterm = lerp(tri.g, ntri.g, 0.5);
link.pinterm = pinterm;
link.reciprocal.pinterm = pinterm;
smoothSubPath(ntri);
});
})(
// smoothSubPath
path.tr0
);
} // smoothPath
//------------------------------------------------------------------------
function createInnerPerimeter(path) {
/* "walks" around the path, starting from path.tr0.g and turning cw like the external perimeter
describes this "inner perimeter" as a list of links, the first being travelled from edge to center, the second from center to edge
remark : two consecutive links may be the same (U-turn at the end of branches)
*/
let nlink, tri;
let link = path.tr0.links[0]; // we know there is only one for this first
const innerp = [];
do {
innerp.push(link);
tri = link.tri; // current triangle
// find next link , beginning by the 1st clockwise
nlink = tri.links.find((l) => l.kedge == (link.kedge + 1) % 3);
if (!nlink) nlink = tri.links.find((l) => l.kedge == (link.kedge + 2) % 3);
if (!nlink) nlink = link; // (U turn)
innerp.push(nlink);
link = nlink.reciprocal;
} while (link !== path.tr0.links[0]);
return innerp;
} // createInnerPerimeter
//------------------------------------------------------------------------
function createPerimeter(tr, path) {
// after createPath, create a closed path with the edges of triangles surrounding the path
let tri, kp;
if (path.length == 1) return; // special case, will be studied later
const perim = [];
tri = path.tr0;
// find starting point, vertex of 1st triangle which does not belong to neighbor triangle
kp = tri.vertices.findIndex(
(p) => !tri.links[0].reciprocal.tri.vertices.includes(p)
);
let p0 = tri.vertices[kp];
perim.push(p0);
p0.ktriafter = p0.ktribefore = p0.tris.indexOf(tri);
p0.kper = 0;
let p = p0;
while (true) {
// search for the point which comes after p = tri.vertices[kp]
if (
(!p.edges[0].tris[0] || !p.edges[0].tris[1]) &&
p.tris[0].path == path
) {
kp = 1;
tri = p.tris[0];
p.ktriafter = 0;
} else {
tri = p.tris.find(
(tri, k) =>
tri.path == path &&
(!p.tris[(k + p.tris.length - 1) % p.tris.length] ||
p.tris[(k + p.tris.length - 1) % p.tris.length].path != path)
);
p.ktriafter = p.tris.indexOf(tri);
}
kp = (tri.vertices.indexOf(p) + 1) % 3;
p = tri.vertices[kp];
p.ktribefore = p.tris.indexOf(tri);
if (p == p0) break;
p.kper = perim.length; // index of point in perimeter
perim.push(p);
} // while (true)
let prevp = perim[perim.length - 1];
perim.forEach((p, k) => {
let n = p.ktribefore - p.ktriafter;
if (n < 0) n += p.tris.length;
p.nbTrisPer = n + 1; // number of triangles involved in perimeter
let np = lerp(prevp, p, 0.5);
prevp.pafter = np;
p.pbefore = np;
prevp = p;
});
return perim;
} // createPerimeter
//------------------------------------------------------------------------
function matchPerimeters(inner, outer) {
/* will attach every point in outer the list of correponding links from inner
(this list may be empty in the case of a U-turn of the perimeter)
*/
let anim = [],
anim1,
arcs;
/* let us begin as if entering simulate we have just processed the last link of inner perimeter */
let kinner = 1;
outer.forEach((pouter) => {
pouter.inner = [];
if (pouter.nbTrisPer == 1) {
// special case for U-turns
let tri = pouter.tris[pouter.ktribefore];
anim.push([
[tri.g, tri.g, tri.g],
[pouter.pbefore, pouter, pouter.pafter]
]);
} else {
/* cut the outer perimeter bezier quadratic into as many parts as required to match every inner perimeter corresponding part
*/
let nbParts = (pouter.nbTrisPer - 1) * 2;
let initArc = [pouter.pbefore, pouter, pouter.pafter];
for (let k = 0; k < nbParts; ++k) {
if (k == nbParts - 1) {
// no need to cut for the last piece !
anim1 = [initArc];
} else {
arcs = cutQ(initArc, 1 / (nbParts - k));
anim1 = [arcs[0]];
initArc = arcs[1];
}
/* done with the outer part. inner, now */
let link = inner[kinner++];
if (kinner >= inner.length) kinner = 0; // only useful for last point
if (link.tri.links.length != 2) {
// inner is a straight line segment
let pts = [link.tri.g, link.pinterm];
if (k & 1) pts.reverse(); // for link entering the triangle (tested by parity of k...)
anim1.unshift([pts[0], lerp(pts[0], pts[1], 0.5), pts[1]]);
} else {
// inner is a quadratic
let outlink =
link.tri.links[0] == link ? link.tri.links[1] : link.tri.links[0];
arcs = cutQ([link.pinterm, link.tri.g, outlink.pinterm], 0.5); // will be done twice, sorry
if (!(k & 1)) arcs[0].reverse();
anim1.unshift(arcs[0]);
} // inner is a quadratic
anim.push(anim1);
} // for k
} // not U_turn
}); // outer.forEach;
return anim;
} // matchPerimeters
//------------------------------------------------------------------------
function createExternalPaths(tr) {
/* make list of "external triangles" not involved in path created at createPath */
let l1, l2;
const listeTris = tr.triangulation.filter((tri) => !tri.path);
const externalPaths = [];
while (listeTris.length) {
/* pick a triangle */
let tri = listeTris[intAlea(listeTris.length)];
const picked = [tri]; // list of triangles to start from to continue a path being created
const path = [];
// simulate we are coming from some side - (fake)
while (picked.length) {
tri = picked[intAlea(picked.length)];
pwet: while (true) {
let ntri = tri;
if (!path.includes(tri)) path.push(tri);
tri.path = path;
let possibles = [];
tri.edges.forEach((edge) => {
let otherTri = edge.tris[0] === tri ? edge.tris[1] : edge.tris[0];
if (!otherTri) return; // no other triangle here
if (otherTri.path) return;
possibles.push(otherTri);
});
while (possibles.length) {
tri = possibles.splice(intAlea(possibles.length), 1)[0];
if (!picked.includes(tri)) picked.push(tri);
// record which sides is connected to which triangle
// on tri side
let kedge = ntri.edges.findIndex((edge) => tri.edges.includes(edge));
ntri.links = ntri.links || [];
ntri.links.push((l1 = { kedge, tri: ntri }));
kedge = tri.edges.findIndex((edge) => ntri.edges.includes(edge));
tri.links = tri.links || [];
tri.links.push((l2 = { kedge, tri }));
l1.reciprocal = l2;
l2.reciprocal = l1;
continue pwet;
} // while possibles.length
picked.splice(picked.indexOf(ntri), 1); // no more possibilities on this triangles, throw it
break;
} // while true;
} // while (picked.length)
externalPaths.push(path);
path.forEach((tri) => listeTris.splice(listeTris.indexOf(tri), 1)); // remove its triangles from listeTris
/* check for singularity and reject if found
singularity : at leat one one of the vertices inside path in completely surrounded by triangles of path
*/
if (
path.some((tri) =>
tri.vertices.some(
(v) =>
v.tris.length == v.edges.length &&
v.tris.every((t) => t.path == tri.path)
)
)
) {
return false;
}
} // while (listeTris.length)
/* search suitable starting triangle for each path */
externalPaths.forEach((path) => {
path.tr0 = path.find((tri) => tri.links && tri.links.length == 1);
/* path.tr0 MAY be undefined (if and only if path.length == 1) */
});
return externalPaths;
} // createExternalPaths
//------------------------------------------------------------------------
class Delaunay {
/* based on https://en.wikipedia.org/wiki/Bowyer%E2%80%93Watson_algorithm
*/
/* version which does not only return the triangles, but describes the relations between vertices, edges, and triangles
*/
constructor(points, maxx, maxy) {
/* maxx and maxy are given here ONLY to determine the initial triangle, assuming
all points are in a (0, 0)-(maxx, maxy) rectangle
actual extreme coordinates of points could be used instead of maxx and maxy
maxx and maxy are both supposed > 0
*/
let triangulation, badTriangles, polygon;
/* triangulation := empty triangle mesh data structure*/
/* add super-triangle to triangulation // must be large enough to completely contain all the points in pointList */
/* the super-triangle has a vertex in [-1, -1] and 2 sides parallel to the axis
The 3rd side is a 45 degrees slanted line passing by [maxx + 1, maxy + 1]
/!\ CAUTION : all triangles generated will have the same orientation as this initial super-triangle
*/
const numPts = points.length;
const pts = points.map((p, kp) => ({
x: p.x,
y: p.y,
kp,
tris: [],
edges: []
})); // array of points - future vertices
this.points = pts;
let supert = [
{ x: -1, y: maxx + maxy + 3 }, // points turning clockwise on a JS 2D canvas
{ x: -1, y: -1 },
{ x: maxx + maxy + 3, y: -1 }
];
triangulation = [new Triangle(...supert)];
/*
for each point in pointList do // add all the points one at a time to the triangulation
*/
for (let kp = 0; kp < numPts; ++kp) {
let point = pts[kp];
/*
badTriangles := empty set
*/
badTriangles = [];
/*
for each triangle in triangulation do // first find all the triangles that are no longer valid due to the insertion
*/
for (let kt = 0; kt < triangulation.length; ++kt) {
if (triangulation[kt].inCircumCircle(point))
badTriangles.push(triangulation[kt]);
} // for kt
polygon = [];
for (let kt = 0; kt < badTriangles.length; ++kt) {
let tri = badTriangles[kt];
if (
!badTriangles.some(
(othertri) => othertri !== tri && othertri.hasEdge(tri.a, tri.b)
)
)
polygon.push([tri.a, tri.b]);
if (
!badTriangles.some(
(othertri) => othertri !== tri && othertri.hasEdge(tri.b, tri.c)
)
)
polygon.push([tri.b, tri.c]);
if (
!badTriangles.some(
(othertri) => othertri !== tri && othertri.hasEdge(tri.c, tri.a)
)
)
polygon.push([tri.c, tri.a]);
} // for kt
/* remove bad triangles from triangulation */
for (let kt = 0; kt < badTriangles.length; ++kt) {
let tri = badTriangles[kt];
triangulation.splice(triangulation.indexOf(tri), 1);
} // for kt
/* add triangulation new triangles built on point and polygon
*/
polygon.forEach((edge) =>
triangulation.push(new Triangle(point, edge[0], edge[1]))
);
} // points.forEach
/* remove super-triangle */
for (let kt = triangulation.length - 1; kt >= 0; --kt) {
let tri = triangulation[kt];
if (supert.includes(tri.a)) {
triangulation.splice(kt, 1);
continue;
}
if (supert.includes(tri.b)) {
triangulation.splice(kt, 1);
continue;
}
if (supert.includes(tri.c)) {
triangulation.splice(kt, 1);
continue;
}
}
this.triangulation = triangulation;
} // constructor
//------------------------------------------------------------------------
analyze() {
this.points.forEach((p) => {
// delete values - only useful for 2nd pass of analyze
p.edges = [];
p.tris = [];
});
this.edgesList = new SortedArray((e0, e1) => {
if (e0.p0.kp - e1.p0.kp) return e0.p0.kp - e1.p0.kp;
else return e0.p1.kp - e1.p1.kp;
});
this.triangulation.forEach((tri) => {
let ed = new Edge(tri.a, tri.b);
let kedge = this.edgesList.indexOf(ed);
if (kedge == -1) this.edgesList.doInsert();
else ed = this.edgesList.tb[kedge];
ed.attachTriangle(tri);
ed = new Edge(tri.b, tri.c);
kedge = this.edgesList.indexOf(ed);
if (kedge == -1) this.edgesList.doInsert();
else ed = this.edgesList.tb[kedge];
ed.attachTriangle(tri);
ed = new Edge(tri.c, tri.a);
kedge = this.edgesList.indexOf(ed);
if (kedge == -1) this.edgesList.doInsert();
else ed = this.edgesList.tb[kedge];
ed.attachTriangle(tri);
});
/* sort triangles around every point */
this.points.forEach((p) => {
const newEdges = [];
const newTris = [];
let edge0, tri;
if (p.tris.length != p.edges.length) {
// if point is on edge of complete figure
edge0 = p.edges.find(
(edge) =>
(edge.p0 == p && edge.tris[0] && !edge.tris[1]) ||
(edge.p1 == p && edge.tris[1] && !edge.tris[0])
);
if (edge0 === undefined) edge0 = p.edges[0];
} else {
edge0 = p.edges[0];
}
while (true) {
/* find triangle with vertex p and edge edge0 starting from p and turning clockwise */
newEdges.push(edge0);
tri = edge0.tris[edge0.p0 == p ? 0 : 1];
if (tri === undefined) break; // p was on perimeter, reached end
newTris.push(tri);
if (newEdges.length == p.edges.length) break; // made full revolution around p
/* find other side of tri ending at p */
switch (p) {
case tri.a:
edge0 = tri.edges[2];
break; // ca
case tri.b:
edge0 = tri.edges[0];
break; // ab
case tri.c:
edge0 = tri.edges[1];
break;
} // switch
} // while (true)
p.tris = newTris;
p.edges = newEdges;
});
} // analyze
//------------------------------------------------------------------------
deleteSharps() {
/* remove triangles with too sharp angles from the periphery of the triangulation */
let sharps = [];
let cosangle;
let p0, p1, dx, dy, len;
this.triangulation.forEach((tri) => {
dx = [];
dy = [];
len = [];
for (let k = 0; k < 3; ++k) {
p0 = tri.vertices[k];
p1 = tri.vertices[(k + 1) % 3];
dx[k] = p1.x - p0.x;
dy[k] = p1.y - p0.y;
len[k] = mhypot(dx[k], dy[k]);
}
for (let k = 0; k < 3; ++k) {
let k1 = (k + 1) % 3;
cosangle = -(dx[k1] * dx[k] + dy[k] * dy[k1]) / len[k] / len[k1];
if (cosangle > 0.9) {
sharps.push(tri);
break;
} // very sharp angle
}
}); //
while (sharps.length) {
let found, kfound; // changed nothing (yet) during this loop
kfound = sharps.findIndex(
(tri) => tri.tris.filter(() => true).length < 3
);
if (kfound == -1) break; // no more triangle to remove
found = sharps[kfound]; // found one to delete ...
sharps.splice(kfound, 1); // remove from list
/* remove this triangle from
- its neighbors' neighborhood
- its edges
if an edge becomes empty, remove it from edges AND points'edges
- its vertices
*/
found.tris.forEach((neigh) => {
// neighbors' neighborhood
let kn = neigh.tris.indexOf(found);
delete neigh.tris[kn];
});
found.edges.forEach((ed) => {
// edges
let kn = ed.tris.indexOf(found);
delete ed.tris[kn];
if (ed.tris.reduce((sum, el) => (el ? 1 : 0), 0) == 0) {
// edge becomes empty
this.edgesList.tb.splice(this.edgesList.tb.indexOf(ed), 1);
ed.p0.edges.splice(ed.p0.edges.indexOf(ed), 1);
ed.p1.edges.splice(ed.p1.edges.indexOf(ed), 1);
// the disappearance of the points will be processed when deleting vertices. Maybe, not sure it is useful
}
});
found.vertices.forEach((p) => {
// vertices
p.tris.splice(p.tris.indexOf(found), 1);
if (p.tris.length == 0) this.points.splice(this.points.indexOf(p), 1); // indices of points become pointless
});
this.triangulation.splice(this.triangulation.indexOf(found), 1);
} // while
/* now, got to re-index edges and tris at vertices on perimeter, so that edges on the external side is between indices 0 and max */
this.analyze();
} // deleteSharps
} // Delaunay
//------------------------------------------------------------------------
//------------------------------------------------------------------------
class BridsonPoint {
constructor(brid, x, y) {
this.x = x;
this.y = y;
this.kx = Math.floor(x / brid.square);
this.ky = Math.floor(y / brid.square);
}
distance(p) {
return mhypot(this.x - p.x, this.y - p.y);
} // distance
} // BridsonPoint
//------------------------------------------------------------------------
class Bridson {
constructor(width, height, dist, nbTries) {
/* area is 0..width, 0..height
creates a set of points in this area */
this.width = width;
this.height = height;
this.dist = dist;
this.nbTries = nbTries;
// create a grid with the right dimensions
this.square = this.dist / Math.sqrt(2);
this.nbx = mceil(width / this.square);
this.nby = mceil(height / this.square);
const terrain = new Array(this.nby)
.fill(0)
.map((v, ky) => new Array(this.nbx).fill(-1));
this.terrain = terrain;
const points = [];
this.points = points;
const list = [];
this.list = list;
// pick 1st point at random and record it
let x = alea(width);
let y = alea(height);
list.push(new BridsonPoint(this, x, y)); // initialize list with a point
points.push(list[0]);
terrain[list[0].ky][list[0].kx] = list[0];
while (list.length) {
let posp = intAlea(list.length);
let p = list[posp];
let found = false;
for (let k = 0; k < nbTries; ++k) {
let p1 = this.rndr2r();
p1 = new BridsonPoint(this, p.x + p1.x, p.y + p1.y);
if (this.isAcceptable(p1)) {
found = true;
}
} // for k
if (!found) list.splice(posp, 1);
} // while list.length
// add each BridsonPoint its index in "points"
points.forEach((p, k) => (p.kList = k));
} // constructor
rndr2r() {
/* returns a random point at a distance from origin between this.dist and 2 * this.dist
with constant density
*/
//
let rnd = Math.random();
rnd *= rnd;
const r = this.dist * (1 + rnd * rnd); // rnd ^ 4 : higher density at lower radius
const th = Math.PI * Math.random() * 2;
return { x: r * Math.cos(th), y: r * Math.sin(th) };
} // rndr2r
isAcceptable(p) {
if (p.x < 0 || p.x >= this.width || p.y < 0 || p.y >= this.height)
return false; // out of area !
let kx0 = mmax(0, p.kx - 2),
kx1 = mmin(p.kx + 2, this.nbx - 1);
loop2: for (
let kky = mmax(0, p.ky - 2);
kky <= mmin(p.ky + 2, this.nby - 1);
++kky
) {
for (let kkx = kx0; kkx <= kx1; ++kkx) {
if (this.terrain[kky][kkx] != -1) {
if (
mhypot(
p.x - this.terrain[kky][kkx].x,
p.y - this.terrain[kky][kkx].y
) <= this.dist
)
return false;
}
} // for kkx
} // for kky
// this point is acceptable
this.terrain[p.ky][p.kx] = p; // record new point
this.list.push(p);
this.points.push(p);
return true;
}
closest(p) {
/* finds closest point */
/* tries points in a +/- n squares area around the point */
let n = 1;
let prox = 9e99;
let closep;
let p1, d;
while (true) {
for (let k = mmax(0, p.kx - n); k <= mmin(p.kx + n, this.nbx - 1); ++k) {
if (p.ky - n >= 0 && (p1 = this.terrain[p.ky - n][k])) check();
if (p.ky + n < this.nby && (p1 = this.terrain[p.ky + n][k])) check();
} // for k
for (
let k = mmax(0, p.ky - n + 1);
k <= mmin(p.ky + n - 1, this.nby - 1);
++k
) {
if (p.kx - n >= 0 && (p1 = this.terrain[k][p.kx - n])) check();
if (p.kx + n < this.nbx && (p1 = this.terrain[k][p.kx + n])) check();
} // for k
if (closep) break;
++n;
} // while
return closep;
function check() {
d = p.distance(p1);
if (d < prox) {
closep = p1;
prox = d;
}
} // check;
}
} // Bridson
//------------------------------------------------------------------------
function drawAnimPath(anim, alpha, path) {
// just the canvas path - no stroke, no fill
let s0, s1, p, pa, pb;
anim.forEach((seg, k) => {
(s0 = seg[0]), (s1 = seg[1]);
if (k == 0) {
p = lerp(s0[0], s1[0], alpha);
path.moveTo(p.x, p.y);
}
if (alpha != 0) {
pa = lerp(s0[1], s1[1], alpha);
pb = lerp(s0[2], s1[2], alpha);
path.quadraticCurveTo(pa.x, pa.y, pb.x, pb.y);
}
});
path.closePath();
return path;
} // drawAnimPath
//--------------------------------------------------------------------
function img1Loaded() {
if (img1.width == 0) return; // abnormal, give up
oimg1.width = img1.width;
oimg1.height = img1.height;
anyLoaded();
} // img1Loaded
//--------------------------------------------------------------------
function img2Loaded() {
if (img1.width == 0) return; // abnormal, give up
oimg2.width = img2.width;
oimg2.height = img2.height;
anyLoaded();
} // img2Loaded
//--------------------------------------------------------------------
function anyLoaded() {
let refx, refy;
let w, h, ofsx, ofsy;
if (oimg1.width) {
refx = oimg1.width;
refy = oimg1.height;
} else {
refx = oimg2.width;
refy = oimg2.height;
}
maxx = window.innerWidth;
maxy = window.innerHeight;
if (refx / refy > maxx / maxy) {
screenw = maxx;
screenh = (maxx / refx) * refy;
} else {
screenh = maxy;
screenw = (maxy / refy) * refx;
}
canv.width = screenw;
canv.height = screenh;
canv.style.left = (maxx - screenw) / 2 + "px";
canv.style.top = (maxy - screenh) / 2 + "px";
oimg1.canv = oimg1.canv || document.createElement("canvas");
oimg2.canv = oimg2.canv || document.createElement("canvas");
oimg1.canv.width = oimg2.canv.width = screenw;
oimg1.canv.height = oimg2.canv.height = screenh;
oimg1.ctx = oimg1.canv.getContext("2d");
oimg2.ctx = oimg2.canv.getContext("2d");
// prepare fake black picture
oimg1.ctx.setfillStyle = "black";
oimg1.ctx.fillRect(0, 0, screenw, screenh);
oimg2.ctx.setfillStyle = "black";
oimg2.ctx.fillRect(0, 0, screenw, screenh);
// copy and resize picture to context
if (img1.width) {
if (img1.width / img1.height > screenw / screenh) {
h = img1.height;
w = (screenw * h) / screenh;
} else {
w = img1.width;
h = (screenh * w) / screenw;
}
ofsx = (img1.width - w) / 2;
ofsy = (img1.height - h) / 2;
oimg1.ctx.drawImage(img1, ofsx, ofsy, w, h, 0, 0, screenw, screenh);
}
oimg1.pattern = oimg1.ctx.createPattern(oimg1.canv, "repeat");
if (img2.width) {
if (img2.width / img2.height > screenw / screenh) {
h = img2.height;
w = (screenw * h) / screenh;
} else {
w = img2.width;
h = (screenh * w) / screenw;
}
ofsx = (img2.width - w) / 2;
ofsy = (img2.height - h) / 2;
oimg2.ctx.drawImage(img2, ofsx, ofsy, w, h, 0, 0, screenw, screenh);
}
oimg2.pattern = oimg2.ctx.createPattern(oimg2.canv, "repeat");
messages.push({ message: "ready" });
} // anyLoaded
//------------------------------------------------------------------------
//------------------------------------------------------------------------
let animate;
{
// scope for animate
let animState = 0;
let anim0;
let anims;
let alpha;
let outer;
let inner;
let external;
animate = function (tStamp) {
let message;
let p;
let s0, s1, pa, pb;
let anim;
let path;
message = messages.shift();
if (message && message.message == "ready" && img1.width && img2.width)
animState = 1;
window.requestAnimationFrame(animate);
switch (animState) {
case 0:
break;
case 1:
startOver();
path = createPath(tr);
smoothPath(path);
outer = createPerimeter(tr, path); // must be done before createExternalPaths
external = createExternalPaths(tr);
if (!external) {
animState = 1;
return;
}
inner = createInnerPerimeter(path);
anim0 = matchPerimeters(inner, outer);
++animState;
break;
case 2:
anims = [];
external.forEach((path) => {
if (path.length > 1) {
let perim = createPerimeter(tr, path);
smoothPath(path);
inner = createInnerPerimeter(path);
anim = matchPerimeters(inner, perim);
} else {
// only one triangle : directly build anim
const tri = path[0];
const pa = lerp(tri.a, tri.b, 0.5);
const pb = lerp(tri.b, tri.c, 0.5);
const pc = lerp(tri.c, tri.a, 0.5);
anim = [
[
[tri.g, tri.g, tri.g],
[pa, tri.b, pb]
],
[
[tri.g, tri.g, tri.g],
[pb, tri.c, pc]
],
[
[tri.g, tri.g, tri.g],
[pc, tri.a, pa]
]
];
}
anims.push(anim);
});
alpha = 0.05;
++animState;
break;
case 3:
ctx.rect(0, 0, screenw, screenh);
ctx.fillStyle = oimg1.pattern;
ctx.fill();
path = drawAnimPath(anim0, alpha, new Path2D());
ctx.fillStyle = oimg2.pattern;
ctx.fill(path);
alpha += 0.02;
if (alpha > 1.01) {
++animState;
alpha = 1.01;
}
break;
case 4:
ctx.rect(0, 0, screenw, screenh);
ctx.fillStyle = oimg1.pattern;
ctx.fill();
path = new Path2D();
anims.forEach((anim) => {
drawAnimPath(anim, alpha, path);
});
path.rect(-1, -1, screenw + 2, screenh + 2);
ctx.fillStyle = oimg2.pattern;
ctx.fill(path, "evenodd");
alpha -= 0.02;
if (alpha <= 0) {
ctx.rect(0, 0, screenw, screenh);
ctx.fillStyle = oimg2.pattern;
ctx.fill();
++animState;
}
break;
case 5:
if (message && message.message == "click") {
[oimg1, oimg2] = [oimg2, oimg1];
animState = 1;
break;
}
} // switch
}; // animate
} // scope for animate
//------------------------------------------------------------------------
//------------------------------------------------------------------------
function startOver() {
// canvas dimensions
/*
maxx = window.innerWidth;
maxy = window.innerHeight;
canv.width = screenw;
canv.height = screenh;
canv.style.left = (maxx - screenw) / 2 + 'px';
canv.style.top = (maxy - screenh) / 2 + 'px';
/*
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
*/
const distBridson = msqrt(screenw * screenh) / alea(20, 40);
const margin = 1.5 * distBridson; // distBrinson or 0 or slightly negative values make sense
let t = new Bridson(
screenw + 2 * margin,
screenh + 2 * margin,
distBridson,
30
);
let points = generatePoints(t);
tr = new Delaunay(points, screenw + 2 * margin, screenh + 2 * margin);
tr.points.forEach((p) => {
p.x -= margin;
p.y -= margin;
});
tr.triangulation.forEach((tri) => {
tri.xc -= margin;
tri.yc -= margin;
});
tr.analyze();
tr.triangulation.forEach((tri) => tri.listTris());
tr.deleteSharps();
return true;
} // startOver
//------------------------------------------------------------------------
function mouseClick(event) {
messages.push({ message: "click", event });
} // mouseClick
//------------------------------------------------------------------------
//------------------------------------------------------------------------
// beginning of execution
{
canv = document.createElement("canvas");
canv.style.position = "absolute";
document.body.appendChild(canv);
ctx = canv.getContext("2d");
canv.setAttribute("title", "click me");
} // creation CANVAS
canv.addEventListener("click", mouseClick);
img1 = new Image();
oimg1 = {}; // associated object is empty
img1.addEventListener("load", img1Loaded);
img2 = new Image();
oimg2 = {}; // associated object is empty
img2.addEventListener("load", img2Loaded);
requestAnimationFrame(animate);
// img1.src = './Mona_Lisa.jpg'
// img2.src = './Meisje_met_de_parel.jpg';
img1.src = "https://assets.codepen.io/2574552/Mona_Lisa.jpg?format=auto";
img2.src =
"https://assets.codepen.io/2574552/Meisje_met_de_parel.jpg?format=auto";
Also see: Tab Triggers