590 lines
15 KiB
JavaScript
590 lines
15 KiB
JavaScript
import { SVG } from 'https://cdn.skypack.dev/@svgdotjs/svg.js'
|
|
import { Vec2 } from 'https://cdn.skypack.dev/wtc-math';
|
|
console.clear();
|
|
|
|
const config = {
|
|
drawingType: 1,
|
|
dimensions: (new Vec2(window.innerWidth, window.innerHeight)).scale(2),
|
|
breakRun: 2000,
|
|
maxDepth: 12,
|
|
maxPerRun: 100,
|
|
insertType: 1,
|
|
randomType: 0
|
|
};
|
|
const vars = {
|
|
drawing: null,
|
|
i:0,
|
|
running: true,
|
|
triangles: []
|
|
}
|
|
const setup = () => {
|
|
vars.running = false;
|
|
setTimeout(() => {
|
|
vars.running = true;
|
|
vars.triangles = [];
|
|
config.insertType = Math.floor(Math.random() * 3);
|
|
config.randomType = Math.floor(Math.random() * 3);
|
|
vars.i=0;
|
|
|
|
document.querySelector('#container').innerHTML = '';
|
|
vars.drawing = new Drawing(config.drawingType).addTo('#container').size(config.dimensions);
|
|
document.body.querySelector('#container>:first-child').addEventListener('click', () => {
|
|
setup();
|
|
});
|
|
|
|
draw();
|
|
}, 100);
|
|
};
|
|
|
|
window.addEventListener('resize', () => {
|
|
config.dimensions = ( new Vec2(window.innerWidth, window.innerHeight)).scale(2)
|
|
setup();
|
|
});
|
|
|
|
let depth = 0;
|
|
const ts = [
|
|
['a', 'b'],
|
|
['b', 'c'],
|
|
['c', 'a']
|
|
];
|
|
const drawStep = () => {
|
|
if(!vars.running) return;
|
|
if(vars.i > 2000) return;
|
|
let newTriangles = [];
|
|
vars.triangles.forEach((triangle,i) => {
|
|
if(i>config.breakRun) {
|
|
// newTriangles.splice(0,0,triangle);
|
|
return;
|
|
}
|
|
vars.drawing.polygon(triangle.points);
|
|
let c = triangle.randomCentroid;
|
|
if(config.randomType == 0) {
|
|
c = triangle.centroid;
|
|
}
|
|
|
|
// vars.drawing.circle(c, 5);
|
|
if(triangle.depth < config.maxDepth) {
|
|
ts.forEach(t => {
|
|
const tr = new Triangle(triangle[t[0]], triangle[t[1]], c);
|
|
tr.depth=triangle.depth+1;
|
|
newTriangles.push(tr);
|
|
});
|
|
}
|
|
});
|
|
if(config.insertType === 0) {
|
|
if(vars.triangles.length>config.breakRun) newTriangles = vars.triangles.splice(config.breakRun).concat(newTriangles);
|
|
} else if(config.insertType === 1) {
|
|
if(vars.triangles.length>config.breakRun) newTriangles = vars.triangles.splice(config.breakRun).reverse().concat(newTriangles);
|
|
} else {
|
|
newTriangles = newTriangles.concat(vars.triangles.splice(config.breakRun));
|
|
}
|
|
|
|
vars.triangles = newTriangles;
|
|
vars.i++;
|
|
requestAnimationFrame(drawStep);
|
|
}
|
|
let interval;
|
|
const draw = () => {
|
|
vars.drawing.linewidth = 1;
|
|
vars.drawing.fill = "#333";
|
|
vars.drawing.rect(new Vec2(0,0), config.dimensions);
|
|
vars.drawing.linewidth = 1;
|
|
vars.drawing.fill = null;
|
|
vars.drawing.stroke = 'rgba(255,255,255,.02)'
|
|
|
|
const r = Math.min(config.dimensions.x, config.dimensions.y) * .6;
|
|
const offset = r * 0.2333;
|
|
const points = [];
|
|
for(let i = 0.; i < Math.PI * 2; i += Math.PI * 2. / 3.) {
|
|
points.push( new Vec2(Math.cos(i+Math.PI*.5) * r + config.dimensions.x / 2, Math.sin(i+Math.PI*.5) * r + config.dimensions.y / 2 - offset) );
|
|
}
|
|
|
|
const t = new Triangle(...points);
|
|
t.depth=0;
|
|
vars.triangles.push(t);
|
|
|
|
drawStep();
|
|
}
|
|
|
|
class Triangle {
|
|
|
|
#points
|
|
|
|
constructor(a, b, c) {
|
|
this.points = [a, b, c];
|
|
}
|
|
|
|
set points(p) {
|
|
if(p.reduce((a, c) => c instanceof Vec2 ? a+1 : 0, 0) === 3) this.#points = p;
|
|
}
|
|
get points() {
|
|
return this.#points || [];
|
|
}
|
|
|
|
get a() {
|
|
return this.#points[0];
|
|
}
|
|
get b() {
|
|
return this.#points[1];
|
|
}
|
|
get c() {
|
|
return this.#points[2];
|
|
}
|
|
|
|
get circumcenter() {
|
|
const d = this.b.subtractNew(this.a);
|
|
const e = this.c.subtractNew(this.a);
|
|
|
|
const bl = d.x * d.x + d.y * d.y;
|
|
const cl = e.x * e.x + e.y * e.y;
|
|
const ds = 0.5 / (d.x * e.y - d.y * e.x);
|
|
|
|
return new Vec2(
|
|
this.a.x + (e.y * bl - d.y * cl) * ds,
|
|
this.a.y + (d.x * cl - e.x * bl) * ds
|
|
);
|
|
}
|
|
get randomCentroid() {
|
|
let a = floatRandomBetween(.2,1.8);
|
|
let b = floatRandomBetween(.2,1.8);
|
|
let c = floatRandomBetween(.2,1.8);
|
|
let abc = a+b+c;
|
|
const f = (3-abc)/3;
|
|
a += f;
|
|
b += f;
|
|
c += f;
|
|
return this.a.scaleNew(a).add(this.b.scaleNew(b)).add(this.c.scaleNew(c)).divideScalar(3);
|
|
}
|
|
get centroid() {
|
|
const a = floatRandomBetween(.5,1.5);
|
|
const b = floatRandomBetween(.5,1.5);
|
|
const c = floatRandomBetween(.5,1.5);
|
|
const abc = a+b+c;
|
|
return this.a.addNew(this.b).add(this.c).divideScalar(3);
|
|
}
|
|
|
|
get isComplete() {
|
|
return this.points.reduce((a, c) => c instanceof Vec2 ? a+1 : 0, 0) === 3;
|
|
}
|
|
|
|
get segments() {
|
|
return [
|
|
{
|
|
a: this.a,
|
|
b: this.b,
|
|
c: this.c,
|
|
segment: this.a.subtractNew(this.b)
|
|
},
|
|
{
|
|
a: this.b,
|
|
b: this.c,
|
|
c: this.a,
|
|
segment: this.b.subtractNew(this.c)
|
|
},
|
|
{
|
|
a: this.c,
|
|
b: this.a,
|
|
c: this.b,
|
|
segment: this.c.subtractNew(this.a)
|
|
}
|
|
];
|
|
}
|
|
|
|
get angles() {
|
|
let ab = this.b.subtractNew(this.a).normalise();
|
|
let ac = this.c.subtractNew(this.a).normalise();
|
|
|
|
let ba = this.a.subtractNew(this.b).normalise();
|
|
let bc = this.c.subtractNew(this.b).normalise();
|
|
|
|
const ta = (Math.acos(ab.dot(ac)));
|
|
const tb = (Math.acos(ba.dot(bc)));
|
|
const tc = (Math.acos(ac.dot(bc)));
|
|
|
|
return [ta,tb,tc];
|
|
}
|
|
|
|
get isEqualateral() {
|
|
return this.numEqualSides === 3;
|
|
}
|
|
get isIsosceles() {
|
|
return this.numEqualSides === 2;
|
|
}
|
|
get isScalene() {
|
|
return this.numEqualSides === 1;
|
|
}
|
|
get isRightAngle() {
|
|
return this.angles.reduce((n, s) => n || precisionRound(Math.abs(s * 180 / Math.PI), 3) === 90, false);
|
|
}
|
|
get isObtuse() {
|
|
return this.angles.reduce((n, s) => n || precisionRound(Math.abs(s * 180 / Math.PI), 3) > 90, false);
|
|
}
|
|
get isAcute() {
|
|
return !this.isObtuse;
|
|
}
|
|
|
|
get numEqualSides() {
|
|
const ls = this.segments.map(seg => precisionRound(seg.segment.length, 3));
|
|
const n = ls.map(l => ls.reduce((n, l1) => n + (l1 === l), 0));
|
|
return n.reduce((a, b) => b > a ? b : a, 0);
|
|
}
|
|
|
|
get hypot() {
|
|
const segs = this.segments;
|
|
let longest = 0;
|
|
let hypotenuse = {};
|
|
segs.forEach(seg => {
|
|
if(seg.segment.length > longest) {
|
|
longest = seg.segment.length;
|
|
hypotenuse = seg;
|
|
}
|
|
});
|
|
return hypotenuse;
|
|
}
|
|
}
|
|
|
|
setTimeout(() => {
|
|
setup();
|
|
}, 500);
|
|
|
|
class Drawing {
|
|
static #defaults = {
|
|
stroke: '#333',
|
|
pxratio: 1
|
|
}
|
|
static DT_CANVAS = 1;
|
|
static DT_SVG = 2;
|
|
#drawing;
|
|
#ctx;
|
|
#mode;
|
|
#instructions = [];
|
|
|
|
constructor(mode = Drawing.DT_CANVAS, settings) {
|
|
settings = Object.assign({}, Drawing.#defaults, settings);
|
|
|
|
this.mode = mode;
|
|
if(this.mode & Drawing.DT_CANVAS) {
|
|
this.#drawing = document.createElement('canvas');
|
|
} else if(this.mode & Drawing.DT_SVG) {
|
|
this.#drawing = SVG();
|
|
}
|
|
|
|
this.stroke = settings.stroke;
|
|
this.pxratio = settings.pxratio;
|
|
}
|
|
clear() {
|
|
if(this.mode & Drawing.DT_CANVAS) {
|
|
this.c.clearRect(0,0,...this.dimensions.array);
|
|
} else if(this.mode & Drawing.DT_SVG) {
|
|
this.drawing.clear();
|
|
}
|
|
}
|
|
rect(position, dimensions) {
|
|
if(this.saving) {
|
|
this.#instructions.push({
|
|
f: 'rect',
|
|
args: [position, dimensions]
|
|
});
|
|
}
|
|
position = position.scaleNew(this.pxratio);
|
|
dimensions = dimensions.scaleNew(this.pxratio);
|
|
if(this.mode & Drawing.DT_CANVAS) {
|
|
this.c.beginPath();
|
|
this.c.rect(...position.array, ...dimensions.array);
|
|
if(this.stroke) this.c.stroke();
|
|
if(this.fill) this.c.fill();
|
|
} else if(this.mode & Drawing.DT_SVG) {
|
|
return this.drawing.rect(dimensions.width, dimensions.height).move(...position.array).fill("none").stroke(this.strokeParams);
|
|
}
|
|
}
|
|
circle(position, radius) {
|
|
if(this.saving) {
|
|
this.#instructions.push({
|
|
f: 'circle',
|
|
args: [position, radius]
|
|
});
|
|
}
|
|
position = position.scaleNew(this.pxratio);
|
|
radius *= this.pxratio;
|
|
if(this.mode & Drawing.DT_CANVAS) {
|
|
if(!window.trace) {
|
|
console.log(this.fill)
|
|
window.trace = true;
|
|
}
|
|
if(this.fill) this.c.fillStyle = this.fill;
|
|
this.c.beginPath();
|
|
this.c.arc(position.x, position.y, radius, 0, 2 * Math.PI);
|
|
if(this.stroke) this.c.stroke();
|
|
if(this.fill) this.c.fill();
|
|
} else if(this.mode & Drawing.DT_SVG) {
|
|
return this.drawing.circle(radius*2).fill("none").stroke(this.strokeParams).move(...position.subtractScalarNew(radius).array);
|
|
}
|
|
}
|
|
line(a, b) {
|
|
if(this.saving) {
|
|
this.#instructions.push({
|
|
f: 'line',
|
|
args: [a, b]
|
|
});
|
|
}
|
|
a = a.scaleNew(this.pxratio);
|
|
b = b.scaleNew(this.pxratio);
|
|
if(this.mode & Drawing.DT_CANVAS) {
|
|
this.c.beginPath();
|
|
this.c.moveTo(a.x, a.y);
|
|
this.c.lineTo(b.x, b.y);
|
|
if(this.stroke) this.c.stroke();
|
|
} else if(this.mode & Drawing.DT_SVG) {
|
|
return this.drawing.line(...a.array, ...b.array).stroke(this.strokeParams);
|
|
}
|
|
}
|
|
polyline(points) {
|
|
if(this.saving) {
|
|
this.#instructions.push({
|
|
f: 'polyline',
|
|
args: points
|
|
});
|
|
}
|
|
if(this.mode & Drawing.DT_CANVAS) {
|
|
this.c.beginPath();
|
|
points.forEach((p, i) => {
|
|
p = p.scaleNew(this.pxratio);
|
|
if(i === 0) this.c.moveTo(p.x, p.y);
|
|
else this.c.lineTo(p.x, p.y);
|
|
})
|
|
if(this.stroke) this.c.stroke();
|
|
} else if(this.mode & Drawing.DT_SVG) {
|
|
return this.drawing.polyline(points.map(p => p.array)).fill('none').stroke(this.strokeParams);
|
|
}
|
|
}
|
|
path(path) {
|
|
if(this.mode & Drawing.DT_CANVAS) {
|
|
this.c.beginPath();
|
|
const p1 = new Path2D(path);
|
|
const m = document.createElementNS("http://www.w3.org/2000/svg", "svg").createSVGMatrix()
|
|
const p = new Path2D();
|
|
const t = m.scale(this.pxratio);
|
|
p.addPath(p1, t);
|
|
if(this.stroke) this.c.stroke(p);
|
|
} else if(this.mode & Drawing.DT_SVG) {
|
|
const _path = this.drawing.path().fill('none').stroke(this.strokeParams);
|
|
_path.plot(path);
|
|
return _path;
|
|
}
|
|
}
|
|
polygon(points) {
|
|
if(this.saving) {
|
|
this.#instructions.push({
|
|
f: 'polygon',
|
|
args: [points]
|
|
});
|
|
}
|
|
if(this.mode & Drawing.DT_CANVAS) {
|
|
this.c.beginPath();
|
|
points.forEach((p, i) => {
|
|
p = p.scaleNew(this.pxratio);
|
|
if(i === 0) this.c.moveTo(p.x, p.y);
|
|
else this.c.lineTo(p.x, p.y);
|
|
})
|
|
if(this.stroke) this.c.stroke();
|
|
if(this.fill) this.c.fill();
|
|
} else if(this.mode & Drawing.DT_SVG) {
|
|
return this.drawing.polygon(points.map(p => p.array)).fill('none').stroke(this.strokeParams);
|
|
}
|
|
}
|
|
|
|
download() {
|
|
let d;
|
|
if(this.mode & Drawing.DT_CANVAS) {
|
|
d = new Drawing(Drawing.DT_SVG).size(this.dimensions);
|
|
this.#instructions.forEach((i) => {
|
|
d[i.f](...i.args);
|
|
});
|
|
} else if(this.mode & Drawing.DT_SVG) {
|
|
d = this;
|
|
}
|
|
|
|
const fileName = "untitled.svg"
|
|
const url = "data:image/svg+xml;utf8," + encodeURIComponent(d.drawing.svg());
|
|
const link = document.createElement("a");
|
|
link.download = fileName;
|
|
link.href = url;
|
|
link.click();
|
|
}
|
|
|
|
addTo(element) {
|
|
if(typeof(element) === 'string') {
|
|
if(this.mode & Drawing.DT_CANVAS) {
|
|
document.body.querySelector(element).appendChild(this.drawing);
|
|
} else if(this.mode & Drawing.DT_SVG) {
|
|
this.drawing.addTo('#container');
|
|
}
|
|
}
|
|
return this;
|
|
}
|
|
|
|
size(d) {
|
|
if(this.mode & Drawing.DT_CANVAS) {
|
|
this.drawing.width = d.width * this.#pxratio;
|
|
this.drawing.height = d.height * this.#pxratio;
|
|
} else if(this.mode & Drawing.DT_SVG) {
|
|
this.drawing.viewbox(0, 0, d.x, d.y)
|
|
this.drawing.size(...d.scaleNew(this.#pxratio).array);
|
|
}
|
|
this.#dimensions = d;
|
|
return this;
|
|
}
|
|
|
|
#dimensions
|
|
set dimensions(v) {
|
|
if(v instanceof Vec2) {
|
|
this.#dimensions = v;
|
|
this.size(v);
|
|
}
|
|
}
|
|
get dimensions() {
|
|
return this.#dimensions;
|
|
}
|
|
|
|
#pxratio = 1;
|
|
set pxratio(v) {
|
|
if(v > 0) {
|
|
this.#pxratio = v;
|
|
}
|
|
}
|
|
get pxratio() {
|
|
return this.#pxratio || 1;
|
|
}
|
|
|
|
get drawing() {
|
|
return this.#drawing;
|
|
}
|
|
get c() {
|
|
if(this.mode & Drawing.DT_CANVAS) {
|
|
if(this.#ctx) return this.#ctx;
|
|
this.#ctx = this.drawing.getContext('2d');
|
|
return this.#ctx;
|
|
}
|
|
}
|
|
#stroke;
|
|
set stroke(v) {
|
|
this.#stroke = v;
|
|
if(this.mode & Drawing.DT_CANVAS) {
|
|
this.c.strokeStyle = v;
|
|
}
|
|
}
|
|
get stroke() {
|
|
return this.#stroke;
|
|
}
|
|
#fill = null;
|
|
set fill(v) {
|
|
this.#fill = v;
|
|
if(this.mode & Drawing.DT_CANVAS) {
|
|
this.c.fillStyle = v;
|
|
}
|
|
}
|
|
get fill() {
|
|
return this.#fill;
|
|
}
|
|
#linecap = null;
|
|
set linecap(v) {
|
|
if(['square', 'butt', 'round'].indexOf(v) > -1) {
|
|
this.#linecap = v;
|
|
if(this.mode & Drawing.DT_CANVAS) {
|
|
this.c.lineCap = v;
|
|
}
|
|
}
|
|
}
|
|
get linecap() {
|
|
return this.#linecap;
|
|
}
|
|
#lineWidth
|
|
set linewidth(v) {
|
|
if(v > 0) {
|
|
this.#lineWidth = v;
|
|
if(this.mode & Drawing.DT_CANVAS) {
|
|
this.c.lineWidth = v*this.pxratio;
|
|
}
|
|
}
|
|
}
|
|
get linewidth() {
|
|
return this.#lineWidth;
|
|
}
|
|
get strokeParams() {
|
|
const params = {};
|
|
if(this.#lineWidth) params.width = this.#lineWidth;
|
|
if(this.#stroke) params.color = this.#stroke;
|
|
if(this.#linecap) params.linecap = this.#linecap;
|
|
return params;
|
|
}
|
|
set mode(v) {
|
|
if(v & (Drawing.DT_CANVAS | Drawing.DT_SVG)) {
|
|
this.#mode = v;
|
|
}
|
|
}
|
|
get mode() {
|
|
return this.#mode || Drawing.DT_CANVAS;
|
|
}
|
|
#saving = false
|
|
set saving(v) {
|
|
this.#saving = v === true;
|
|
}
|
|
get saving() {
|
|
return this.#saving;
|
|
}
|
|
}
|
|
|
|
/// Create the download button/
|
|
/*
|
|
const dl = document.createElement('button');
|
|
dl.innerText = 'download';
|
|
dl.addEventListener('click', () => {
|
|
vars.drawing.download();
|
|
});
|
|
document.body.querySelector('#container').appendChild(dl);
|
|
*/
|
|
|
|
|
|
|
|
const floatRandomBetween = (min, max) => {
|
|
return Math.random() * (max - min) + min;
|
|
};
|
|
const clamp = function(a, b, v) {
|
|
return Math.min(b, Math.max(a, v));
|
|
}
|
|
const lerp = function(a, b, progress) {
|
|
return a + progress * (b - a);
|
|
}
|
|
const hash21 = (p) => {
|
|
const o = p.dot(new Vec2(27.609, 57.583));
|
|
return fract(Math.sin(o)*config.seed);
|
|
}
|
|
const precisionRound = (n, p) => {
|
|
const ip = Math.pow(10, p);
|
|
return Math.round(n*ip)/ip;
|
|
}
|
|
const fract = (n, p = 6) => {
|
|
const ip = Math.pow(10, p);
|
|
const _n = Math.abs(Math.floor(n*ip)/ip);
|
|
if(_n < 1) return _n;
|
|
return Math.floor(_n % Math.floor(_n)*ip)/ip;
|
|
}
|
|
const pal = ( t, a, b, c, d ) => {
|
|
const mp = c.scaleNew(t).add(d).scale(6.28318);
|
|
mp.x = Math.cos(mp.x);
|
|
mp.y = Math.cos(mp.y);
|
|
mp.z = Math.cos(mp.z);
|
|
return a.addNew(b.multiplyNew(mp));
|
|
}
|
|
const getColour = (d) => {
|
|
const col = pal(
|
|
d/70+.65,
|
|
new Vec3(0.5,0.5,0.5),
|
|
new Vec3(0.5,0.5,0.5),
|
|
new Vec3(1.0,1.0,1.0),
|
|
new Vec3(0.0,0.1,0.2) );
|
|
|
|
const colour = Math.floor(col.x * 255).toString(16) + Math.floor(col.y * 255).toString(16) + Math.floor(col.z * 255).toString(16);
|
|
const a = Math.floor((1.-d/30)*255).toString(16);
|
|
return colour+a;
|
|
} |