class State { constructor(x, y) { this.id = guid(); this.x = x; this.y = y; this.mouseOffsetX = 0; this.mouseOffsetY = 0; this.color = '#fff'; this.isActive = false; this.activeTime = 0; this.isAcceptState = false; this._text = ''; } get text() { return this._text; } set text(value) { documents[activeDocument].addChange('state', this.id, 'edit', ['_text'], [this._text], true); this._text = value; documents[activeDocument].addChange('state', this.id, 'edit', ['_text'], [this._text]); } setMouseStart(x, y) { this.mouseOffsetX = this.x - x; this.mouseOffsetY = this.y - y; } setAnchorPoint(x, y) { this.x = x + this.mouseOffsetX; this.y = y + this.mouseOffsetY; } draw() { const activeTimeDuration = Date.now() - this.activeTime; ctx.fillStyle = this.isActive && activeTimeDuration > simulationStepDuration ? '#0f0' : this.color; ctx.strokeStyle = settings.colors.getColor(this); ctx.beginPath(); ctx.arc(this.x, this.y, radius, 0, 2 * Math.PI); ctx.fill(); ctx.stroke(); ctx.closePath(); if(activeTimeDuration < simulationStepDuration) { let size = 0; const percent = Math.min(Math.max(activeTimeDuration / simulationStepDuration, 0), 1); if(this.isActive) { size = easeInOutCubic(percent) * radius; } else { size = easeInOutCubic(1 - percent) * radius; } ctx.fillStyle = '#0f0'; ctx.beginPath(); ctx.arc(this.x, this.y, size, 0, 2 * Math.PI); ctx.fill(); ctx.closePath(); } ctx.fillStyle = settings.colors.getColor(this); const width = ctx.measureText(convertLatexShortcuts(this.text)).width; if(width < radius * .9) ctx.drawText(this.text, this.x, this.y, null, selectedObject === this); else ctx.drawText(this.text, this.x, this.y + radius * 1.75, null, selectedObject === this); if(this.isAcceptState) { ctx.beginPath(); ctx.arc(this.x, this.y, radius - 6, 0, 2 * Math.PI); ctx.stroke(); ctx.closePath(); } } moveTo(x, y) { x = Math.min(Math.max(0, x), width); y = Math.min(Math.max(0, y), height); this.goal = { x, y }; const angle = this.direction(x, y); this.v = { x: settings.speed * Math.cos(angle), y: settings.speed * Math.sin(angle) }; } moveToStep() { this.x += this.v.x; this.y += this.v.y; if ((this.v.x > 0 && this.goal.x <= this.x) || (this.v.x < 0 && this.goal.x >= this.x)) { this.x = this.goal.x; this.v.x = 0; } if((this.v.y > 0 && this.goal.y <= this.y) || (this.v.y < 0 && this.goal.y >= this.y)) { this.y = this.goal.y; this.v.y = 0; } } intersection(state) { return -Math.hypot(this.x - state.x, this.y - state.y) + radius * 2; } intersects(state) { return this.intersection(state) > 0; } directionTo(state) { return this.direction(state.x, state.y); } direction(x, y) { return Math.atan2(this.y - y, this.x - x); } closestPointOnCircle(x, y) { const dx = x - this.x; const dy = y - this.y; const scale = Math.sqrt(dx ** 2 + dy ** 2); return { x: this.x + dx * radius / scale, y: this.y + dy * radius / scale, }; } containsPoint(x, y) { return (x - this.x) ** 2 + (y - this.y) ** 2 < radius ** 2; } }