/************************
Ordinal Three body Orbits
************************/
function calculateDistance(x1, y1, x2, y2) {
return Math.sqrt(Math.pow((x2 - x1), 2) + Math.pow((y2 - y1), 2));
}
class CanvasBasic {
constructor(canvasId, dpr) {
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext("2d");
this.dpr = dpr
}
clear() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
fade(coverColor) {
this.ctx.fillStyle = coverColor
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
setCanvas(size, zRatio) {
let z = zRatio * size;
this.canvas.width = size * this.dpr;
this.canvas.height = size * this.dpr;
this.canvas.style.width = size + 'px'
this.canvas.style.height = size + 'px'
this.canvas.style.position = "absolute";
this.canvas.style.left = "50%";
this.canvas.style.top = "50%";
this.canvas.style.position = "absolute";
this.canvas.style.transform = `translate3d(-50%,-50%,${z}px)`
}
resetCanvas(size, zRatio) {
this.setCanvas(size, zRatio);
this.clear();
}
setContextStyle(color, shadowColor, shadowBlur, lineWidth) {
this.ctx.strokeStyle = color;
this.ctx.fillStyle = color;
this.ctx.shadowColor = shadowColor;
this.ctx.shadowBlur = shadowBlur;
this.ctx.lineWidth = lineWidth * this.dpr;
}
}
class Canvas3DDashboard extends CanvasBasic {
constructor(canvasId, dpr) {
super(canvasId, dpr);
}
drawRect(gridLen, numOfGrids, corner) {
gridLen = gridLen * this.dpr;
const size = gridLen * numOfGrids;
if (typeof corner === 'undefined') {
this.ctx.setLineDash([])
} else {
let patten = Array(4).fill([(numOfGrids - corner * 2), corner * 2]).flat()
patten.unshift(corner)
this.ctx.setLineDash(patten.map(num => num * gridLen))
}
this.ctx.strokeRect(this.canvas.width / 2 - size / 2, this.canvas.height / 2 - size / 2, size, size);
}
drawLine(gridLen, numOfGrids, corner) {
gridLen = gridLen * this.dpr;
const size = gridLen * numOfGrids
this.ctx.setLineDash([gridLen * corner, gridLen * (1 - corner * 2), gridLen * corner, 0])
for (let i = 1; i < numOfGrids; i++) {
this.ctx.moveTo(this.canvas.width / 2 - size / 2, this.canvas.height / 2 - size / 2 + gridLen * i);
this.ctx.lineTo(this.canvas.width / 2 - size / 2 + size, this.canvas.height / 2 - size / 2 + gridLen * i);
this.ctx.moveTo(this.canvas.width / 2 - size / 2 + gridLen * i, this.canvas.height / 2 - size / 2);
this.ctx.lineTo(this.canvas.width / 2 - size / 2 + gridLen * i, this.canvas.height / 2 - size / 2 + size);
}
this.ctx.stroke();
}
drawDecoration(gridLen, numOfGrids, deltaR) {
gridLen = gridLen * this.dpr;
const size = gridLen * numOfGrids
const delta = gridLen * deltaR
this.ctx.setLineDash([]);
this.ctx.beginPath();
const drawSide = (sign) => {
this.ctx.moveTo(this.canvas.width / 2 + sign * (size / 2 + delta), this.canvas.height / 2 - (size * 0.2 + delta));
this.ctx.lineTo(this.canvas.width / 2 + sign * (size / 2), this.canvas.height / 2 - (size * 0.2));
this.ctx.lineTo(this.canvas.width / 2 + sign * (size / 2), this.canvas.height / 2 + (size * 0.2));
this.ctx.lineTo(this.canvas.width / 2 + sign * (size / 2 + delta), this.canvas.height / 2 + (size * 0.2 + delta));
}
drawSide(-1); // Left side
drawSide(1); // Right side
this.ctx.stroke();
}
}
class EffectBasic {
constructor(effectsSettings, seed) {
this.canvasHeight = window.innerHeight;
this.dpr = window.devicePixelRatio || 1;
this.seed = seed === undefined ? this.fetchBlockHeight() : seed;
this.settings = effectsSettings;
this.checkEra(this.seed)
this.createDiv()
this.UIBack = new Canvas3DDashboard('canvasUIBack', this.dpr);
this.UIMid = new Canvas3DDashboard('canvasUIMid', this.dpr);
this.UIFront = new Canvas3DDashboard('canvasUIFront', this.dpr);
this.normalXFor3D = 0;
this.normalYFor3D = 0;
this.degFactorFor3D = 0;
this.aimDegFactorFor3D = 0;
this.mouseHasLeft = false;
this.mouseInputPanel = document.getElementById('mouseInputPanel');
this.card3D = document.getElementById('card3D');
this.maxDis4card3DEffects = Math.min(window.innerHeight / 2, window.innerWidth / 2)
this.width = window.innerWidth
this.height = window.innerWidth
}
init() {
this.fitCanvas()
this.apply3DcardEffects()
}
checkEra(seed) {
const difficultyAdjustmentPeriod = 2016;
const halvingPeriod = 210000;
const era = Math.floor((seed - 1) / difficultyAdjustmentPeriod);
const epoch = Math.floor((seed - 1) / halvingPeriod);
this.firstBlockAfterDiffAdjust = ((seed - 1) % difficultyAdjustmentPeriod === 0)
this.firstBlockAfterHalving = ((seed - 1) % halvingPeriod === 0)
this.shouldResetEachPeriod = !(this.firstBlockAfterDiffAdjust || this.firstBlockAfterHalving)
console.log("era: ", era, 'havling epoch: ', epoch,
'firstBlockAfterDiffAdjust: ', this.firstBlockAfterDiffAdjust,
"firstBlockAfterHalving: ", this.firstBlockAfterHalving,
"shouldRestEachPeriod: ", this.shouldResetEachPeriod)
this.color = this.settings.colorSchemes[era % this.settings.colorSchemes.length];
this.solidColor = `rgb(${this.color[0]},${this.color[1]},${this.color[2]})`;
this.shallowColor = `rgba(${this.color[0]},${this.color[1]},${this.color[2]},0.7)`;
this.dimColor = `rgba(${this.color[0]},${this.color[1]},${this.color[2]},0.5)`;
this.loomColor = `rgba(${this.color[0]},${this.color[1]},${this.color[2]},0.2)`;
this.energetic = this.firstBlockAfterHalving
this.era = era
this.epoch = epoch
}
fitCanvas(canvasHeight) {
this.width = canvasHeight
this.height = canvasHeight
this.canvasHeight = canvasHeight
this.maxDis4card3DEffects = Math.min(window.innerHeight / 2, window.innerWidth / 2)
if (canvasHeight <= this.settings.canvasSizeThreshold) {
this.useCard3DEffects = false
this.closeDashboard()
} else {
this.useCard3DEffects = this.settings.sholdUseCard3DEffects
this.draw3DDashboardDecoration(canvasHeight)
}
}
onEachFrame() {
this.handleMouseLeaveFor3Dcard()
}
apply3DcardEffects() {
const updateEffect = (clientX, clientY) => {
if (this.useCard3DEffects) {
const dx = clientX - (this.card3D.getBoundingClientRect().left + this.card3D.clientWidth / 2);
const dy = clientY - (this.card3D.getBoundingClientRect().top + this.card3D.clientHeight / 2);
const angle = Math.atan2(dy, dx);
const distance = Math.sqrt(dx * dx + dy * dy);
this.normalXFor3D = Math.cos(angle);
this.normalYFor3D = Math.sin(angle);
this.aimDegFactorFor3D = Math.min(distance / this.maxDis4card3DEffects, 1);
}
}
const projectionEnd = (e) => {
this.energetic = this.firstBlockAfterHalving;
}
const projectionBegin = (e) => {
if (e.touches.length == 3) {
e.preventDefault();
this.energetic = true;
this.aimDegFactorFor3D = 0;
} else {
this.energetic = this.firstBlockAfterHalving;
}
}
const handleTouchMove = (e) => {
if (e.touches.length <= 1) {
e.preventDefault();
const firstTouch = e.touches[0];
updateEffect(firstTouch.clientX, firstTouch.clientY);
}
}
this.mouseInputPanel.addEventListener('mousemove', (e) => {
updateEffect(e.clientX, e.clientY);
});
this.mouseInputPanel.addEventListener('touchmove', handleTouchMove, { passive: false });
this.mouseInputPanel.addEventListener('touchstart', projectionBegin, { passive: false });
this.mouseInputPanel.addEventListener('touchend', projectionEnd, false);
this.mouseInputPanel.addEventListener('touchcancel', projectionEnd, false);
this.mouseInputPanel.onmouseenter = this.mouseInputPanel.ontouchstart = () => {
this.mouseHasLeft = false;
};
this.mouseInputPanel.onmouseleave = this.mouseInputPanel.ontouchend = () => {
this.mouseHasLeft = true;
};
}
handleMouseLeaveFor3Dcard() {
if (this.useCard3DEffects) {
if (this.mouseHasLeft) {
this.degFactorFor3D = Math.max(this.degFactorFor3D - this.settings.anglePerFrame4card3DEffects, 0);
} else {
this.degFactorFor3D = this.degFactorFor3D + this.settings.anglePerFrame4card3DEffects < this.aimDegFactorFor3D ?
this.degFactorFor3D + this.settings.anglePerFrame4card3DEffects : this.aimDegFactorFor3D;
}
this.card3D.style.transform = `rotate3d(${-this.normalYFor3D},${this.normalXFor3D},0,${this.degFactorFor3D * this.settings.max3DDegree}deg)`;
} else {
this.card3D.style.transform = `rotate3d(0,0,0,0deg)`;
}
}
createDiv() {
var divAll = document.createElement('div');
divAll.className = "ThreeBodyProblem-container isFullScreenWide isUnselectable";
divAll.style.cssText = `height: 100vh;background-color: ${this.settings.backgroundColor};`;
var divPanel = document.createElement('div');
divPanel.id = "panel";
divPanel.style.cssText = "perspective:1500px;position:absolute;left:50%;top: 50%;transform: translate(-50%,-50%);";
var divCard3D = document.createElement('div');
divCard3D.id = "card3D";
divCard3D.style.cssText = "position: relative;width:0px;height:0px; transform-style: preserve-3d;";
var divMouseInputPanel = document.createElement('div');
divMouseInputPanel.id = "mouseInputPanel";
divMouseInputPanel.style.cssText = "position: absolute; width: 100%; height: 100vh;";
var createCanvas = function (id) {
var canvas = document.createElement('canvas');
canvas.id = id;
return canvas;
};
var canvasIds = ['canvasUIBack', 'canvasUIMid', 'canvasUIFront', 'canvasTrace', 'canvasTail', 'canvasTop'];
var canvases = canvasIds.map(createCanvas);
for (var i = 0; i < canvasIds.length; i++) {
divCard3D.appendChild(canvases[i]);
}
divPanel.appendChild(divCard3D);
divAll.appendChild(divPanel);
divAll.appendChild(divMouseInputPanel);
document.body.appendChild(divAll);
document.body.style.margin = "0";
document.body.style.padding = "0";
document.body.style.overflow = "hidden";
}
closeDashboard() {
this.UIBack.clear();
this.UIMid.clear();
this.UIFront.clear();
}
draw3DDashboardDecoration(size) {
var gridLen = size / 18
this.UIBack.resetCanvas(size, 0);
this.UIBack.setContextStyle(this.loomColor, this.shallowColor, gridLen, gridLen * 0.1);
this.UIBack.drawRect(gridLen, 14, 9);
// this.UIBack.setContextStyle(this.loomColor, this.shallowColor, gridLen, gridLen * 0.1);
// this.UIBack.drawRect(gridLen, 13);
// this.UIBack.drawDecoration(gridLen, 14, 0.7);
this.UIMid.resetCanvas(size, 0.03);
this.UIMid.setContextStyle(this.loomColor, this.shallowColor, gridLen, gridLen * 0.03);
// this.UIMid.drawRect(gridLen, 12);
this.UIMid.setContextStyle(this.loomColor, this.shallowColor, gridLen, gridLen * 0.03);
this.UIMid.drawLine(gridLen, 12, 0.15);
this.UIFront.resetCanvas(size, 0.1);
this.UIFront.setContextStyle(this.solidColor, this.shallowColor, gridLen, gridLen * 0.1);
this.UIFront.drawRect(gridLen, 14, 9);
}
canvasNotSupported() {
if (!(window.requestAnimationFrame && this.canvasTop.canvas && this.canvasTop.canvas?.getContext)) {
console.log('Canvas not supported.')
return true
}
if (!this.ctxTop) {
console.log('Canvas not supported.')
return true
}
return false
}
showMessage(title, content) {
var canvasSize = this.canvasHeight
var mainContainer = document.querySelector(".ThreeBodyProblem-container");
var container = document.createElement('div');
container.id = 'container';
container.style.cssText = `position:absolute;top:55%;left:50%;width:${canvasSize}px;
height:${canvasSize}px;transform:translate3d(-50%,-50%,0px);`
container.innerHTML = `
`;
mainContainer.appendChild(container);
var style = document.createElement('style');
style.innerHTML = `
body { width: 100%; height: 100vh; overflow: hidden; }
.star-wars { display: flex; justify-content: center; position: relative; height: ${canvasSize}px; color: #ffffff;
font-family: 'Star Jedi'; font-size: ${canvasSize * 0.6}%; perspective: 400px; text-align: center; }
.crawl { position: relative; top: 0px; transform-origin: 50% 100%; animation: crawl ${55 / (canvasSize / 600)}s linear infinite; }
.crawl > .title { font-size: 90%; text-align: center; }
.crawl > .title h1 { margin: 0 0 100px; text-transform: uppercase; }
@keyframes crawl { 0% { top: 0; transform: rotateX(60deg) translateY(0); }
100% { top: 0; transform: rotateX(60deg) translateY(-5500px); } } `;
document.head.appendChild(style);
}
}
class Effect extends EffectBasic {
constructor(effectsSettings, seed) {
super(effectsSettings, seed);
this.canvasTail = new CanvasBasic("canvasTail", this.dpr)
this.ctxTail = this.canvasTail.ctx
this.canvasTrace = new CanvasBasic("canvasTrace", this.dpr)
this.ctxTrace = this.canvasTrace.ctx
this.canvasTop = new CanvasBasic("canvasTop", this.dpr)
this.ctxTop = this.canvasTop.ctx
this.drawPeriodCount = 0;
this.shouldDrawTail = !this.shouldResetEachPeriod
this.shouldBodyBlur = false
this.pause = 0
this.init();
}
setEra(seed) {
this.checkEra(seed)
this.shouldDrawTail = !this.shouldResetEachPeriod
this.setOrbitStyle(this.canvasHeight)
this.clearCanvas()
this.draw3DDashboardDecoration(this.canvasHeight)
}
pose(nobody = false) {
this.pause = 1000
this.closeDashboard()
if (nobody) {
this.canvasTop.clear()
}
}
clearCanvas() {
this.closeDashboard()
this.canvasTail.clear()
this.canvasTrace.clear()
this.canvasTop.clear()
}
setOrbitStyle(canvasHeight) {
this.canvasTail.setContextStyle(this.solidColor, null, 0, canvasHeight / 100)
this.canvasTrace.setContextStyle(this.dimColor, null, 0, 0.5)
this.canvasTop.setContextStyle(this.solidColor, this.shallowColor, 20, null)
this.bodySize = Math.max(2, canvasHeight * 0.005) * this.dpr
}
fitCanvas(canvasHeight) {
super.fitCanvas(canvasHeight);
this.canvasTail.resetCanvas(canvasHeight, 0.075)
this.canvasTrace.resetCanvas(canvasHeight, 0.075)
this.canvasTop.resetCanvas(canvasHeight, 0.075)
this.setOrbitStyle(canvasHeight)
this.ctxTail.globalCompositeOperation = 'xor';
this.drawPeriodCount = 0
this.minimalDrawDist = canvasHeight / 6000
if (canvasHeight <= this.settings.canvasSizeThreshold) {
this.shouldBodyBlur = false
this.currentFPS = this.settings.minFPS
this.zoomOut = this.settings.iframeZoomOut
} else {
this.shouldBodyBlur = true
this.currentFPS = this.settings.maxFPS
this.zoomOut = this.settings.fullScreenZoomOut
}
}
onEachPeriod() {
this.drawPeriodCount++;
}
onEachFrame() {
if (this.shouldDrawTail) {
this.canvasTail.fade(this.settings.tailCover);
}
super.onEachFrame()
}
draw(previous, positions, ibody) {
if (positions.length == 0) return
if (this.pause > 0) { this.pause--; return }
this.drawBody(positions[positions.length - 1].x, positions[positions.length - 1].y, ibody)
this.drawShadowLine(previous, positions, ibody)
if (this.shouldDrawTail) {
this.drawTail(previous, positions, ibody)
}
}
drawTail(previous, positions, ibody) {
this.ctxTail.beginPath()
this.ctxTail.moveTo(previous.x, previous.y)
for (let i = 0; i < positions.length; i++) {
this.ctxTail.lineTo(positions[i].x, positions[i].y)
}
this.ctxTail.stroke()
}
drawShadowLine(previous, positions, ibody) {
let px = previous.x
let py = previous.y
this.ctxTrace.beginPath()
this.ctxTrace.moveTo(previous.x, previous.y)
for (let i = 0; i < positions.length; i++) {
if (calculateDistance(px, py, positions[i].x, positions[i].y) > this.minimalDrawDist || i == positions.length - 1) {
this.ctxTrace.lineTo(positions[i].x, positions[i].y)
px = positions[i].x
py = positions[i].y
}
}
this.ctxTrace.stroke()
}
drawBody(x, y, ibody) {
if (ibody == 0) {
this.canvasTop.clear()
}
this.ctxTop.beginPath()
if (this.energetic) {
let color = this.settings.colorSchemes[(this.era + 1 + ibody) % this.settings.colorSchemes.length];
this.ctxTop.fillStyle = `rgb(${color[0]},${color[1]},${color[2]})`;
} else {
this.ctxTop.fillStyle = this.solidColor
}
this.ctxTop.arc(x, y, this.bodySize, 0, 2 * Math.PI)
this.ctxTop.fill()
this.ctxTop.beginPath()
this.ctxTop.fillStyle = "white"
if (this.energetic) {
this.ctxTop.arc(x, y, this.bodySize * 0.5, 0, 2 * Math.PI)
} else {
this.ctxTop.arc(x, y, this.bodySize * 0.7, 0, 2 * Math.PI)
}
this.ctxTop.fill()
}
}
class EffectsSettings {
constructor(maxFPS, max3DDegree) {
this.backgroundColor = "rgb(22,22,22)"
this.tailCover = "rgba(32,32,32,0.05)"
this.max3DDegree = max3DDegree ?? 30
this.anglePerFrame4card3DEffects = 0.1
this.maxFPS = maxFPS ?? 60
this.minFPS = 30
this.fullScreenZoomOut = 1.4
this.iframeZoomOut = 1.0
this.frameLostTolerance = 10
this.sholdUseCard3DEffects = true
this.canvasSizeThreshold = 200
this.colorSchemes = [
[170, 255, 0],
[255, 219, 76],
[65, 255, 181],
[55, 214, 255],
[255, 75, 100],
[255, 128, 0],
]
}
}
let effectsSettings = new EffectsSettings();