Skip to content

Instantly share code, notes, and snippets.

@JeffCave
Last active December 26, 2017 18:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JeffCave/8982073443d1cea21d9894c16d8a5286 to your computer and use it in GitHub Desktop.
Save JeffCave/8982073443d1cea21d9894c16d8a5286 to your computer and use it in GitHub Desktop.
Psychedelic Checkers II

Psychedelic Checkers

Sample of a fun renderer. We made these in my Grade 6 art class and I've always enjoyed them.

What I didn't realize at the time is they make an interesting graph. Assuming you have two domains of equal size, you can start at the top/left and work your way around the entire outer boundary counting off tick marks. Given a set of X/Y coordinates, you can map the intersection of any two domains.

It looks cool, but I'm not sure it communicates anything meaningfully.

'use strict';
class Point extends Array{
constructor(x,y,ord){
super();
if(Array.isArray(x)){
ord = x[2];
y = x[1];
x = x[0];
}
if(!ord){
ord = 0;
}
this.precision = 10**15;
this.push(0);
this.push(0);
this.push(0);
this.x = x;
this.y = y;
this.ord = ord;
}
set x(val){
val = parseFloat(val);
val = val * this.precision;
val = Math.round(val);
val = val/this.precision;
if(val === 0) val = 0;
this[0] = val;
}
get x(){
return this[0];
}
set y(val){
val = parseFloat(val);
val = val * this.precision;
val = Math.round(val);
val = val/this.precision;
this[1] = val;
}
get y(){
return this[1];
}
set ord(val){
val = Math.floor(parseFloat(val));
if(val === 0) val = 0;
this[2] = val;
}
get ord(){
return this[2];
}
}
class Segment{
constructor(segment){
this.segment = segment.map(function(d){
if(typeof d !== Point){
d = new Point(d);
}
return d;
});
this.privates = {};
}
get a(){
if(!this.privates.a){
this.privates.a = this.segment[0].y - this.segment[1].y;
}
return this.privates.a;
}
get b(){
if(!this.privates.b){
this.privates.b = this.segment[1].x - this.segment[0].x;
}
return this.privates.b;
}
get c(){
if(!this.privates.c){
this.privates.c = this.a * this.segment[0].x + this.b * this.segment[0].y;
}
return this.privates.c;
}
get theta(){
if(!this.privates.theta){
this.privates.theta = 1/Math.tan(this.a/this.b);
}
return this.privates.theta;
}
get midPoint(){
if(!this._midpoint){
this._midpoint = new Point(
(this.segment[0].x + this.segment[1].x)/2,
(this.segment[0].y + this.segment[1].y)/2
);
}
return this._midpoint;
}
solveY(x){
let y = (this.a*x + this.c)/this.b;
let line = [x,y];
return line;
}
solveX(y){
let x = (this.b * y + this.c) / this.a;
let line = [x,y];
return line;
}
findIntercept(line2){
/**
* http://zonalandeducation.com/mmts/intersections/intersectionOfTwoLines1/intersectionOfTwoLines1.html
*/
let line1 = this;
let det = line1.a*line2.b - line2.a*line1.b;
// If the lines are parrallel, then they will never intercept
if(det === 0) return null;
let intercept = [
(line2.b*line1.c - line1.b*line2.c)/det,
(line2.a*line1.c - line1.a*line2.c)/det,
];
// invert the Y axis
intercept[1] *= -1;
intercept = new Point(intercept);
// Because we are workign with line segments, it is possible
// that that the intercept point falls outside the bounds of
// the segment. If the intercept is outside the bounds, of the
// segemnts, there is no intercept
function isRangeValid(segment, intercept, axis){
let range = [
segment[0][axis],
segment[1][axis],
]
.sort(numericCompare)
;
let isValid = (range[0] <= intercept[axis] && range[1] >= intercept[axis]);
return isValid;
}
if(!isRangeValid(line1.segment, intercept, X)) return null;
if(!isRangeValid(line1.segment, intercept, Y)) return null;
if(!isRangeValid(line2.segment, intercept, X)) return null;
if(!isRangeValid(line2.segment, intercept, Y)) return null;
// if we have made it this far, we have found an intercept
return intercept;
}
}
<html>
<head>
<script type="text/javascript" src="./PsycheCheckers.js"></script>
<script type="text/javascript" src="./RenderPsycheCheckers.js"></script>
<script type="text/javascript" src="./utils.js"></script>
<script type="text/javascript" src="./Geometry.js"></script>
<style>
svg{
position:absolute;
height:100%;
width:100%;
}
svg#debug{
display:none;
}
main{
position: relative;
height:480px;
width:100%;
}
</style>
</head>
<body>
<main>
<svg></svg>
<svg id='debug'>
<line id='X' stroke='black' />
<line id='lastX' stroke='black' />
<line id='oldCut' stroke-width="2" stroke="orange" />
<line id='cut' stroke-width="2" stroke="red" />
<circle id='intercept1' r='5' stroke='black' fill='yellow' />
<circle id='intercept2' r='5' stroke='black' fill='white' />
<circle id='intercept3' r='5' stroke='black' fill='green' />
<circle id='intercept4' r='5' stroke='black' fill='blue' />
</svg>
</main>
<script>
'use strict';
/* global PsycheCheckers, global RenderPsycheCheckers*/
function redraw(renderer){
let ratio = renderer.rect.height / renderer.rect.width;
if(renderer.psyche.ratio === ratio) return false;
renderer.psyche.ratio = ratio;
if(DEBUGMODE) document.querySelector('#debug').style.display = 'block';
renderer.drawPlanes(function(d,x,y){
let colour = ['STEELBLUE','PALEVIOLETRED'];
let plane = JSON.parse(d['data-plane']);
colour = colour[plane.intersection[1]%colour.length];
colour = {
'stroke':colour,
'fill':colour,
};
return colour;
});
document.querySelector('#debug').style.display = 'none';
//renderer.drawLines({'stroke':'white'});
return true;
}
function main(){
let svg = document.querySelector('svg');
let checkers = new PsycheCheckers();
let renderer = new RenderPsycheCheckers(svg,checkers);
window.addEventListener('resize',function(){
redraw(renderer);
});
redraw(renderer);
}
main();
</script>
</body>
</html>
'use strict';
class PsycheCheckers{
constructor(opts){
if(!opts) opts = {};
opts = JSON.clone(opts);
if(!Array.isArray(opts.seedPoints)){
/*
seedPoints = [
[Math.random(), Math.random()],
[Math.random(), Math.random()],
];
*/
opts.seedPoints = [
[0.25, 1/3],
[0.75, 2/3],
];
}
if(isNaN(opts.ticks)){
opts.ticks = 250;
}
opts.ticks = Math.round(opts.ticks);
if(opts.ticks < 4) opts.ticks = 4;
if(isNaN(opts.ratio)){
opts.ratio = 480/640;
}
opts.ticks = parseInt(opts.ticks,10);
this.dirtyEvent();
this.opts = opts;
this.innerPoints = JSON.clone(opts.seedPoints);
this.ratio = opts.ratio;
}
get ratio(){
return this._ratio;
}
set ratio(newRatio){
if(isNaN(newRatio)) return;
if(isNaN(this._ratio)) this._ratio = 0;
newRatio = +newRatio;
if(this._ratio === newRatio) return;
this.dirtyEvent();
this._ratio = newRatio;
}
get outerPoints(){
if(this._outerPoints) return this._outerPoints;
let p = [];
let half = Math.ceil(this.opts.ticks / 2);
let vertical = Math.round(half * this.ratio/(this.ratio + 1));
let horizontal = half - vertical;
let dist = 0;
dist = 1/horizontal;
let tickCount = 0;
for(let x=0; x < 1; x += dist){
p.push([x ,0 ,tickCount ]);
p.push([1-x ,1 ,tickCount+half ]);
tickCount ++;
}
dist = 1/vertical;
for(let y=0; y < 1 ; y += dist){
p.push([0 , 1-y , tickCount+half ]);
p.push([1 , y , tickCount ]);
tickCount ++;
}
p.sort(function(a,b){
let diff = a[2] - b[2];
return diff;
});
this._outerPoints = p;
return this._outerPoints;
}
get lines(){
if(this._lines) return this._lines;
let outer = this.outerPoints;
this._lines = this.innerPoints.map(function(i){
let lines = outer
.map(function(o){
return [i,o];
});
return lines;
});
return this._lines;
}
/**
* List of all planes formed on draw area
*
*
*/
get planes(){
if(this._planes) return this._planes;
let x = this.lines[0];
let y = this.lines[1];
function basePlaneParser(x,xpos){
let direction = -1;
let testLine = new Segment([
innerPoint,
outerPoints[lastX.segment[1][2]]
]);
moveLine('#cut',testLine.segment);
if(testLine.findIntercept(x) && testLine.findIntercept(lastX)){
direction = -1;
}
else{
direction = +1;
}
let oldCutLine = [x.segment[1],lastX.segment[1]];
let radialPoint = Array(2).fill(lastX.segment[0]);
let newCutLine = null;
let o = x.segment[1][2];
if(direction < 0){
o=(direction+o)%outerPoints.length;
}
let ypos = -1;
for(; newCutLine !== radialPoint; o=(o+direction)%outerPoints.length){
ypos ++;
if(o < 0){
o += outerPoints.length;
}
testLine = new Segment([
innerPoint,
outerPoints[o]
]);
moveLine('#oldCut',oldCutLine);
moveLine('#cut',testLine.segment);
newCutLine = [
testLine.findIntercept(x),
testLine.findIntercept(lastX)
];
if(newCutLine[0] === null || newCutLine[1] === null){
newCutLine = radialPoint;
}
let plane = [
oldCutLine[0],
oldCutLine[1],
newCutLine[1],
newCutLine[0],
oldCutLine[0],
];
plane = plane.reduce(function(a,d){
d = JSON.stringify(d);
if(a[0] !== d){
a.unshift(d);
}
return a;
},[])
.map(function(d){
return JSON.parse(d);
});
document.querySelectorAll('#debug > circle').forEach(function(d,i){
moveCircle(d,plane[i]);
});
plane = {
position:[xpos,o],
intersection:[xpos,ypos],
segments:plane,
};
planes.push(plane);
oldCutLine = newCutLine;
}
}
// Because we are working in pairs of lines, we need to get two lines
// for every cycle. The easiest way to acheive this is to just remember
// the last line we used as we get the next. Unfortunately, that means
// we need to do something special either the first or last item in
// the list.
//
// In this case, we get the first item in the list and initialize our
// trailing item tracker. Then we move it to the end of the list to be
// reprocessed there.
x = x.map(function(x){return new Segment(x)});
let firstX = x[x.length-1];
y = y.map(function(y){return new Segment(y)});
let firstY = y[y.length-1];
function specialPlaneParser(line1,line2,xpos){
let lastY = firstY;
let A = line1;
let B = line2;
y.forEach(function(y,i){
moveLine("#oldCut",lastY.segment);
moveLine("#cut",y.segment);
moveLine("#X",B.segment);
moveLine("#lastX",A.segment);
// Sometimes, after many hours of attempting something clever
// I give up, and apply brute force
let plane =[];
let didAIntercept = false;
let didBIntercept = false;
let inter = y.findIntercept(A);
if(inter){
didAIntercept = true;
plane.push(inter);
}
else{
inter = y.findIntercept(B);
if(inter){
didBIntercept = true;
plane.push(inter);
}
}
plane.push(innerPoints[1]);
inter = lastY.findIntercept(A);
if(inter){
didAIntercept = true;
plane.push(inter);
}
else{
inter = lastY.findIntercept(B);
if(inter){
didBIntercept = true;
plane.push(inter);
}
}
if(didAIntercept && didBIntercept){
let isouter = plane.reduce(function(a,segment){
a.push(segment[0]);
a.push(segment[1]);
return a;
},[])
.some(function(axis){
let bool = (axis === 0 || axis === 1);
return bool;
})
;
if(!isouter){
plane.push(innerPoints[0]);
}
else{
if(isInsideTriangle(plane,innerPoints[0])){
plane = [];
}
}
}
plane = plane.filter(function(d){return d !== null;});
Array.from(document.querySelectorAll('circle')).forEach(function(d){
d.setAttribute('opacity',0);
});
plane.forEach(function(p,i){
let circle = document.querySelector('#intercept'+(i+1));
moveCircle(circle,p);
});
lastY = y;
plane = {
position:[xpos,i],
intersection:[xpos,i],
segments:plane,
};
planes.push(plane);
});
}
let innerPoints = this.innerPoints;
let innerPoint = this.innerPoints[1];
let outerPoints = this.outerPoints;
function isInsideTriangle(triangle, point){
triangle = JSON.clone(triangle);
triangle.push(triangle[0]);
let segments = [];
for(let i = 0; i<3; i ++){
segments.push(new Segment([
new Point(triangle[i]),
new Point(triangle[i+1])
]));
}
point = new Point(point);
moveLine('#lastX',segments[0].segment);
moveLine('#X',segments[1].segment);
moveLine('#oldCut',segments[2].segment);
let isOutside = segments.some((segment,i)=>{
let validation = new Segment([
segment.midPoint,
point
]);
moveLine('#cut',validation.segment);
let isInside = segments.every(function(edge){
if(edge === segment){
return true;
}
if(validation.findIntercept(edge)){
return false;
}
return true;
});
return !isInside;
});
return !isOutside;
}
let lastX = firstX;
let planes = [];
x.forEach(function(x,xpos){
moveLine('#lastX',lastX.segment);
moveLine('#X',x.segment);
let triangle = lastX.segment.slice(0);
triangle.push(x.segment[1]);
if(isInsideTriangle(triangle, innerPoints[1])){
specialPlaneParser(x,lastX);
}
else{
basePlaneParser(x,xpos);
}
lastX = x;
})
;
this._planes = planes.filter(function(plane){
return plane.segments.length > 2;
});
return this._planes;
}
dirtyEvent(){
this._ratio = 1;
this._outerPoints = null;
this._lines = null;
}
}
'use strict';
class RenderPsycheCheckers{
constructor(svg, checkers){
this.svg = svg;
this.psyche = checkers;
}
get rect(){
return this.svg.getClientRects()[0];
}
get dataLines(){
let width = this.rect.width;
let height = this.rect.height;
let data = this.psyche.lines
.reduce(function(a,d){
return a.concat(d);
},[])
.map(function(d){
d = JSON.clone(d);
d[0][0] *= width;
d[0][1] *= height;
d[1][0] *= width;
d[1][1] *= height;
return d;
})
;
return data;
}
get dataPlanes(){
let width = this.rect.width;
let height = this.rect.height;
let data = this.psyche.planes
.map(function(plane){
plane = JSON.clone(plane);
plane.segments = plane.segments.map(function(points){
return new Point([
Math.round(points[X] * width),
Math.round(points[Y] * height),
]);
});
return plane;
})
;
return data;
}
drawLines(attributes){
let data = this.dataLines;
let lines = Array.from(this.svg.querySelectorAll('line'));
while(lines.length < data.length){
lines.unshift(document.createElementNS("http://www.w3.org/2000/svg","line"));
this.svg.appendChild(lines[0]);
}
while(lines.length > data.length){
let line = lines.pop();
line.parentNode.removeChild(line);
}
lines.forEach(function(line,l){
let d = data[l];
line.setAttribute('x1',d[0][0]);
line.setAttribute('y1',d[0][1]);
line.setAttribute('x2',d[1][0]);
line.setAttribute('y2',d[1][1]);
Object.entries(attributes).forEach(function(attr){
line.setAttribute(attr[0],attr[1]);
});
});
}
drawPlanes(attributes){
attributes = attributes || function(){ return {} };
let data = this.dataPlanes;
let planes = Array.from(this.svg.querySelectorAll('path'));
while(planes.length < data.length){
planes.unshift(document.createElementNS("http://www.w3.org/2000/svg","path"));
this.svg.appendChild(planes[0]);
}
while(planes.length > data.length){
let quad = planes.pop();
quad.parentNode.removeChild(quad);
}
planes.forEach(function(quad,l){
setTimeout(function(){
let plane = data[l];
let d = "M " + plane.segments.map(function(d){
d = d.slice(0,2).join(',');
return d;
}).join(' L ');
quad.setAttribute('d',d);
quad['data-plane'] = JSON.stringify(plane);
Object.entries(attributes(quad,l,0)).forEach(function(attr){
quad.setAttribute(attr[0],attr[1]);
});
},Math.random()*20);
});
}
}
'use strict';
const X = 0;
const Y = 1;
const DEBUGMODE = false;
JSON.clone = JSON.clone || function(o){
let obj = JSON.stringify(o);
if(typeof obj === 'undefined'){
return undefined;
}
obj = JSON.parse(obj);
return obj;
};
function numericCompare(a,b){
a = parseFloat(a);
b = parseFloat(b);
let delta = a-b;
return delta;
}
function moveLine(id,points){
if(!DEBUGMODE) return;
let svg = document.querySelector(id);
let rect = svg.parentNode.getClientRects()[0];
if(!rect) return;
svg.setAttribute('x1',points[0][X]* rect.width);
svg.setAttribute('y1',points[0][Y]* rect.height);
svg.setAttribute('x2',points[1][X]* rect.width);
svg.setAttribute('y2',points[1][Y]* rect.height);
}
function moveCircle(svg,points){
if(!DEBUGMODE) return;
let rect = svg.parentNode.getClientRects()[0];
if(!rect || !points) return;
svg.setAttribute('cx',points[X]* rect.width);
svg.setAttribute('cy',points[Y]* rect.height);
svg.setAttribute('opacity',1);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment