goban
Version:
[](https://opensource.org/licenses/Apache-2.0) [](https://deepwiki.com/online-go/goban)
1,287 lines (1,231 loc) • 941 kB
JavaScript
/*!
* Copyright (C) Online-Go.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else if(typeof exports === 'object')
exports["goban"] = factory();
else
root["goban"] = factory();
})(self, () => {
return /******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({
/***/ "./node_modules/eventemitter3/index.js":
/*!*********************************************!*\
!*** ./node_modules/eventemitter3/index.js ***!
\*********************************************/
/***/ ((module) => {
var has = Object.prototype.hasOwnProperty
, prefix = '~';
/**
* Constructor to create a storage for our `EE` objects.
* An `Events` instance is a plain object whose properties are event names.
*
* @constructor
* @private
*/
function Events() {}
//
// We try to not inherit from `Object.prototype`. In some engines creating an
// instance in this way is faster than calling `Object.create(null)` directly.
// If `Object.create(null)` is not supported we prefix the event names with a
// character to make sure that the built-in object properties are not
// overridden or used as an attack vector.
//
if (Object.create) {
Events.prototype = Object.create(null);
//
// This hack is needed because the `__proto__` property is still inherited in
// some old browsers like Android 4, iPhone 5.1, Opera 11 and Safari 5.
//
if (!new Events().__proto__) prefix = false;
}
/**
* Representation of a single event listener.
*
* @param {Function} fn The listener function.
* @param {*} context The context to invoke the listener with.
* @param {Boolean} [once=false] Specify if the listener is a one-time listener.
* @constructor
* @private
*/
function EE(fn, context, once) {
this.fn = fn;
this.context = context;
this.once = once || false;
}
/**
* Add a listener for a given event.
*
* @param {EventEmitter} emitter Reference to the `EventEmitter` instance.
* @param {(String|Symbol)} event The event name.
* @param {Function} fn The listener function.
* @param {*} context The context to invoke the listener with.
* @param {Boolean} once Specify if the listener is a one-time listener.
* @returns {EventEmitter}
* @private
*/
function addListener(emitter, event, fn, context, once) {
if (typeof fn !== 'function') {
throw new TypeError('The listener must be a function');
}
var listener = new EE(fn, context || emitter, once)
, evt = prefix ? prefix + event : event;
if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++;
else if (!emitter._events[evt].fn) emitter._events[evt].push(listener);
else emitter._events[evt] = [emitter._events[evt], listener];
return emitter;
}
/**
* Clear event by name.
*
* @param {EventEmitter} emitter Reference to the `EventEmitter` instance.
* @param {(String|Symbol)} evt The Event name.
* @private
*/
function clearEvent(emitter, evt) {
if (--emitter._eventsCount === 0) emitter._events = new Events();
else delete emitter._events[evt];
}
/**
* Minimal `EventEmitter` interface that is molded against the Node.js
* `EventEmitter` interface.
*
* @constructor
* @public
*/
function EventEmitter() {
this._events = new Events();
this._eventsCount = 0;
}
/**
* Return an array listing the events for which the emitter has registered
* listeners.
*
* @returns {Array}
* @public
*/
EventEmitter.prototype.eventNames = function eventNames() {
var names = []
, events
, name;
if (this._eventsCount === 0) return names;
for (name in (events = this._events)) {
if (has.call(events, name)) names.push(prefix ? name.slice(1) : name);
}
if (Object.getOwnPropertySymbols) {
return names.concat(Object.getOwnPropertySymbols(events));
}
return names;
};
/**
* Return the listeners registered for a given event.
*
* @param {(String|Symbol)} event The event name.
* @returns {Array} The registered listeners.
* @public
*/
EventEmitter.prototype.listeners = function listeners(event) {
var evt = prefix ? prefix + event : event
, handlers = this._events[evt];
if (!handlers) return [];
if (handlers.fn) return [handlers.fn];
for (var i = 0, l = handlers.length, ee = new Array(l); i < l; i++) {
ee[i] = handlers[i].fn;
}
return ee;
};
/**
* Return the number of listeners listening to a given event.
*
* @param {(String|Symbol)} event The event name.
* @returns {Number} The number of listeners.
* @public
*/
EventEmitter.prototype.listenerCount = function listenerCount(event) {
var evt = prefix ? prefix + event : event
, listeners = this._events[evt];
if (!listeners) return 0;
if (listeners.fn) return 1;
return listeners.length;
};
/**
* Calls each of the listeners registered for a given event.
*
* @param {(String|Symbol)} event The event name.
* @returns {Boolean} `true` if the event had listeners, else `false`.
* @public
*/
EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) {
var evt = prefix ? prefix + event : event;
if (!this._events[evt]) return false;
var listeners = this._events[evt]
, len = arguments.length
, args
, i;
if (listeners.fn) {
if (listeners.once) this.removeListener(event, listeners.fn, undefined, true);
switch (len) {
case 1: return listeners.fn.call(listeners.context), true;
case 2: return listeners.fn.call(listeners.context, a1), true;
case 3: return listeners.fn.call(listeners.context, a1, a2), true;
case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true;
case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true;
case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true;
}
for (i = 1, args = new Array(len -1); i < len; i++) {
args[i - 1] = arguments[i];
}
listeners.fn.apply(listeners.context, args);
} else {
var length = listeners.length
, j;
for (i = 0; i < length; i++) {
if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true);
switch (len) {
case 1: listeners[i].fn.call(listeners[i].context); break;
case 2: listeners[i].fn.call(listeners[i].context, a1); break;
case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break;
case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break;
default:
if (!args) for (j = 1, args = new Array(len -1); j < len; j++) {
args[j - 1] = arguments[j];
}
listeners[i].fn.apply(listeners[i].context, args);
}
}
}
return true;
};
/**
* Add a listener for a given event.
*
* @param {(String|Symbol)} event The event name.
* @param {Function} fn The listener function.
* @param {*} [context=this] The context to invoke the listener with.
* @returns {EventEmitter} `this`.
* @public
*/
EventEmitter.prototype.on = function on(event, fn, context) {
return addListener(this, event, fn, context, false);
};
/**
* Add a one-time listener for a given event.
*
* @param {(String|Symbol)} event The event name.
* @param {Function} fn The listener function.
* @param {*} [context=this] The context to invoke the listener with.
* @returns {EventEmitter} `this`.
* @public
*/
EventEmitter.prototype.once = function once(event, fn, context) {
return addListener(this, event, fn, context, true);
};
/**
* Remove the listeners of a given event.
*
* @param {(String|Symbol)} event The event name.
* @param {Function} fn Only remove the listeners that match this function.
* @param {*} context Only remove the listeners that have this context.
* @param {Boolean} once Only remove one-time listeners.
* @returns {EventEmitter} `this`.
* @public
*/
EventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) {
var evt = prefix ? prefix + event : event;
if (!this._events[evt]) return this;
if (!fn) {
clearEvent(this, evt);
return this;
}
var listeners = this._events[evt];
if (listeners.fn) {
if (
listeners.fn === fn &&
(!once || listeners.once) &&
(!context || listeners.context === context)
) {
clearEvent(this, evt);
}
} else {
for (var i = 0, events = [], length = listeners.length; i < length; i++) {
if (
listeners[i].fn !== fn ||
(once && !listeners[i].once) ||
(context && listeners[i].context !== context)
) {
events.push(listeners[i]);
}
}
//
// Reset the array, or remove it completely if we have no more listeners.
//
if (events.length) this._events[evt] = events.length === 1 ? events[0] : events;
else clearEvent(this, evt);
}
return this;
};
/**
* Remove all listeners, or those of the specified event.
*
* @param {(String|Symbol)} [event] The event name.
* @returns {EventEmitter} `this`.
* @public
*/
EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) {
var evt;
if (event) {
evt = prefix ? prefix + event : event;
if (this._events[evt]) clearEvent(this, evt);
} else {
this._events = new Events();
this._eventsCount = 0;
}
return this;
};
//
// Alias methods names because people roll like that.
//
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
EventEmitter.prototype.addListener = EventEmitter.prototype.on;
//
// Expose the prefix.
//
EventEmitter.prefixed = prefix;
//
// Allow `EventEmitter` to be imported as module namespace.
//
EventEmitter.EventEmitter = EventEmitter;
//
// Expose the module.
//
if (true) {
module.exports = EventEmitter;
}
/***/ }),
/***/ "./src/Goban/CanvasRenderer.ts":
/*!*************************************!*\
!*** ./src/Goban/CanvasRenderer.ts ***!
\*************************************/
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
/*
* Copyright (C) Online-Go.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.GobanCanvas = void 0;
const JGOF_1 = __webpack_require__(/*! ../engine/formats/JGOF */ "./src/engine/formats/JGOF.ts");
const MoveTree_1 = __webpack_require__(/*! ../engine/MoveTree */ "./src/engine/MoveTree.ts");
const themes_1 = __webpack_require__(/*! ./themes */ "./src/Goban/themes/index.ts");
const canvas_utils_1 = __webpack_require__(/*! ./canvas_utils */ "./src/Goban/canvas_utils.ts");
const translate_1 = __webpack_require__(/*! ../engine/translate */ "./src/engine/translate.ts");
const messages_1 = __webpack_require__(/*! ../engine/messages */ "./src/engine/messages.ts");
const util_1 = __webpack_require__(/*! ../engine/util */ "./src/engine/util/index.ts");
const callbacks_1 = __webpack_require__(/*! ./callbacks */ "./src/Goban/callbacks.ts");
const Goban_1 = __webpack_require__(/*! ./Goban */ "./src/Goban/Goban.ts");
const __theme_cache = {
black: {},
white: {},
};
const HOT_PINK = "#ff69b4";
class GobanCanvas extends Goban_1.Goban {
constructor(config, preloaded_data) {
var _a;
/* TODO: Need to reconcile the clock fields before we can get rid of this `any` cast */
super(config, preloaded_data);
this.__set_board_height = -1;
this.__set_board_width = -1;
this.ready_to_draw = false;
this.last_move_opacity = 1;
this.__borders_initialized = false;
this.autoplaying_puzzle_move = false;
this.byoyomi_label = "";
this.last_label_position = { i: NaN, j: NaN };
this.metrics = { width: NaN, height: NaN, mid: NaN, offset: NaN };
this.drawing_enabled = true;
this.themes = {
"board": "Plain",
"black": "Plain",
"white": "Plain",
"removal-graphic": "x",
"removal-scale": 1.0,
};
this.theme_black_stones = [];
this.theme_black_text_color = HOT_PINK;
this.theme_blank_text_color = HOT_PINK;
this.theme_faded_line_color = HOT_PINK;
this.theme_faded_star_color = HOT_PINK;
//private theme_faded_text_color:string;
this.theme_line_color = "";
this.theme_star_color = "";
this.theme_stone_radius = 10;
this.theme_white_stones = [];
this.theme_white_text_color = HOT_PINK;
// console.log("Goban canvas v 0.5.74.debug 5"); // GaJ: I use this to be sure I have linked & loaded the updates
if (config.board_div) {
this.parent = config["board_div"];
}
else {
this.no_display = true;
this.parent =
document.createElement("div"); /* let a div dangle in no-mans land to prevent null pointer refs */
}
this.title_div = config["title_div"];
this.board = (0, canvas_utils_1.createDeviceScaledCanvas)(10, 10);
this.board.setAttribute("id", "board-canvas");
this.board.className = "StoneLayer";
this.last_move_opacity = (_a = config["last_move_opacity"]) !== null && _a !== void 0 ? _a : 1;
const ctx = this.board.getContext("2d", { willReadFrequently: true });
if (ctx) {
this.ctx = ctx;
}
else {
throw new Error(`Failed to obtain drawing context for board`);
}
this.parent.appendChild(this.board);
this.bindPointerBindings(this.board);
this.move_tree_container = config.move_tree_container;
this.handleShiftKey = (ev) => {
try {
if (ev.shiftKey !== this.shift_key_is_down) {
this.shift_key_is_down = ev.shiftKey;
if (this.last_hover_square) {
this.__drawSquare(this.last_hover_square.x, this.last_hover_square.y);
}
}
}
catch (e) {
console.error(e);
}
};
window.addEventListener("keydown", this.handleShiftKey);
window.addEventListener("keyup", this.handleShiftKey);
// these are set in this.setThemes(..)
// this.theme_board
// this.theme_white
// this.theme_black
this.setTheme(this.getSelectedThemes(), true);
let first_pass = true;
const watcher = this.watchSelectedThemes((themes) => {
var _a, _b, _c;
if (!this.engine) {
return;
}
if (themes.black === "Custom" ||
themes.white === "Custom" ||
themes.board === "Custom") {
//first_pass = true;
}
(_a = __theme_cache.black) === null || _a === void 0 ? true : delete _a["Custom"];
(_b = __theme_cache.white) === null || _b === void 0 ? true : delete _b["Custom"];
(_c = __theme_cache.board) === null || _c === void 0 ? true : delete _c["Custom"];
this.setTheme(themes, first_pass ? true : false);
first_pass = false;
});
this.on("destroy", () => watcher.remove());
this.engine = this.post_config_constructor();
this.emit("engine.updated", this.engine);
this.ready_to_draw = true;
this.redraw(true);
}
setLastMoveOpacity(opacity) {
this.last_move_opacity = opacity;
}
enablePen() {
this.attachPenCanvas();
}
disablePen() {
this.detachPenCanvas();
}
destroy() {
super.destroy();
if (this.board && this.board.parentNode) {
this.board.parentNode.removeChild(this.board);
}
delete this.board;
delete this.ctx;
this.detachPenCanvas();
this.detachShadowLayer();
if (this.message_timeout) {
clearTimeout(this.message_timeout);
delete this.message_timeout;
}
window.removeEventListener("keydown", this.handleShiftKey);
window.removeEventListener("keyup", this.handleShiftKey);
this.theme_black_stones = [];
this.theme_white_stones = [];
delete this.theme_board;
delete this.theme_black;
delete this.theme_white;
delete this.message_div;
delete this.message_td;
delete this.message_text;
delete this.move_tree_container;
delete this.move_tree_inner_container;
delete this.move_tree_canvas;
delete this.title_div;
}
detachShadowLayer() {
if (this.shadow_layer) {
if (this.shadow_layer.parentNode) {
this.shadow_layer.parentNode.removeChild(this.shadow_layer);
}
delete this.shadow_layer;
delete this.shadow_ctx;
}
}
attachShadowLayer() {
if (!this.shadow_layer && this.parent) {
this.shadow_layer = (0, canvas_utils_1.createDeviceScaledCanvas)(this.metrics.width, this.metrics.height);
this.shadow_layer.setAttribute("id", "shadow-canvas");
this.shadow_layer.className = "ShadowLayer";
try {
this.parent.insertBefore(this.shadow_layer, this.board);
}
catch (e) {
// I'm not really sure how we ever get into this state, but sentry.io reports that we do
console.warn("Error inserting shadow layer before board");
console.warn(e);
try {
this.parent.appendChild(this.shadow_layer);
}
catch (e) {
console.error(e);
}
}
const ctx = this.shadow_layer.getContext("2d", { willReadFrequently: true });
if (ctx) {
this.shadow_ctx = ctx;
}
else {
//throw new Error(`Failed to obtain shadow layer drawing context`);
console.error(new Error(`Failed to obtain shadow layer drawing context`));
return;
}
this.bindPointerBindings(this.shadow_layer);
}
}
detachPenCanvas() {
if (this.pen_layer) {
if (this.pen_layer.parentNode) {
this.pen_layer.parentNode.removeChild(this.pen_layer);
}
delete this.pen_layer;
delete this.pen_ctx;
}
}
attachPenCanvas() {
if (!this.pen_layer) {
this.pen_layer = (0, canvas_utils_1.createDeviceScaledCanvas)(this.metrics.width, this.metrics.height);
this.pen_layer.setAttribute("id", "pen-canvas");
this.pen_layer.className = "PenLayer";
this.parent.appendChild(this.pen_layer);
const ctx = this.pen_layer.getContext("2d", { willReadFrequently: true });
if (ctx) {
this.pen_ctx = ctx;
}
else {
throw new Error(`Failed to obtain pen drawing context`);
}
this.bindPointerBindings(this.pen_layer);
}
}
bindPointerBindings(canvas) {
if (!this.interactive) {
return;
}
if (canvas.getAttribute("data-pointers-bound") === "true") {
return;
}
canvas.setAttribute("data-pointers-bound", "true");
let dragging = false;
let last_click_square = this.xy2ij(0, 0);
let pointer_down_timestamp = 0;
const pointerUp = (ev, double_clicked) => {
const press_duration_ms = performance.now() - pointer_down_timestamp;
try {
if (!dragging) {
/* if we didn't start the click in the canvas, don't respond to it */
return;
}
let right_click = false;
if (ev instanceof MouseEvent) {
if (ev.button === 2) {
right_click = true;
ev.preventDefault();
}
}
dragging = false;
if (this.scoring_mode) {
const pos = (0, canvas_utils_1.getRelativeEventPosition)(ev);
const pt = this.xy2ij(pos.x, pos.y);
if (pt.i >= 0 && pt.i < this.width && pt.j >= 0 && pt.j < this.height) {
if (this.score_estimator) {
this.score_estimator.handleClick(pt.i, pt.j, ev.ctrlKey || ev.metaKey || ev.altKey || ev.shiftKey, press_duration_ms);
}
this.emit("update");
}
return;
}
if (ev.ctrlKey || ev.metaKey || ev.altKey) {
try {
const pos = (0, canvas_utils_1.getRelativeEventPosition)(ev);
const pt = this.xy2ij(pos.x, pos.y);
if (callbacks_1.callbacks.addCoordinatesToChatInput) {
callbacks_1.callbacks.addCoordinatesToChatInput(this.engine.prettyCoordinates(pt.i, pt.j));
}
}
catch (e) {
console.error(e);
}
return;
}
if (this.mode === "analyze" && this.analyze_tool === "draw") {
/* might want to interpret this as a start/stop of a line segment */
}
else if (this.mode === "analyze" && this.analyze_tool === "score") {
// nothing to do here
}
else if (this.mode === "analyze" && this.analyze_tool === "removal") {
this.onAnalysisToggleStoneRemoval(ev);
}
else {
const pos = (0, canvas_utils_1.getRelativeEventPosition)(ev);
const pt = this.xy2ij(pos.x, pos.y);
if (!double_clicked) {
last_click_square = pt;
}
else {
if (last_click_square.i !== pt.i || last_click_square.j !== pt.j) {
this.onMouseOut(ev);
return;
}
}
this.onTap(ev, double_clicked, right_click, press_duration_ms);
this.onMouseOut(ev);
}
}
catch (e) {
console.error(e);
}
};
const pointerDown = (ev) => {
pointer_down_timestamp = performance.now();
try {
dragging = true;
if (this.mode === "analyze" && this.analyze_tool === "draw") {
this.onPenStart(ev);
}
else if (this.mode === "analyze" && this.analyze_tool === "label") {
if (ev.shiftKey) {
if (this.analyze_subtool === "letters") {
const label_char = prompt((0, translate_1._)("Enter the label you want to add to the board"), "");
if (label_char) {
this.label_character = label_char.substring(0, 3);
dragging = false;
return;
}
}
}
this.onLabelingStart(ev);
}
else if (this.mode === "analyze" && this.analyze_tool === "score") {
this.onAnalysisScoringStart(ev);
}
else if (this.mode === "analyze" && this.analyze_tool === "removal") {
// nothing to do here, we act on pointerUp
}
}
catch (e) {
console.error(e);
}
};
const pointerMove = (ev) => {
try {
if (this.mode === "analyze" && this.analyze_tool === "draw") {
if (!dragging) {
return;
}
this.onPenMove(ev);
}
else if (dragging && this.mode === "analyze" && this.analyze_tool === "label") {
this.onLabelingMove(ev);
}
else if (dragging && this.mode === "analyze" && this.analyze_tool === "score") {
this.onAnalysisScoringMove(ev);
}
else if (dragging && this.mode === "analyze" && this.analyze_tool === "removal") {
// nothing for moving
}
else {
this.onMouseMove(ev);
}
}
catch (e) {
console.error(e);
}
};
const pointerOut = (ev) => {
try {
dragging = false;
this.onMouseOut(ev);
}
catch (e) {
console.error(e);
}
};
let mouse_disabled = 0;
canvas.addEventListener("click", (ev) => {
if (!mouse_disabled) {
dragging = true;
pointerUp(ev, false);
}
ev.preventDefault();
return false;
});
canvas.addEventListener("dblclick", (ev) => {
if (!mouse_disabled) {
dragging = true;
pointerUp(ev, true);
}
ev.preventDefault();
return false;
});
canvas.addEventListener("mousedown", (ev) => {
if (!mouse_disabled) {
pointerDown(ev);
}
ev.preventDefault();
return false;
});
canvas.addEventListener("mousemove", (ev) => {
if (!mouse_disabled) {
pointerMove(ev);
}
ev.preventDefault();
return false;
});
canvas.addEventListener("mouseout", (ev) => {
if (!mouse_disabled) {
pointerOut(ev);
}
else {
ev.preventDefault();
}
return false;
});
canvas.addEventListener("contextmenu", (ev) => {
if (!mouse_disabled) {
pointerUp(ev, false);
}
else {
ev.preventDefault();
}
return false;
});
canvas.addEventListener("focus", (ev) => {
ev.preventDefault();
return false;
});
let lastX = 0;
let lastY = 0;
let startX = 0;
let startY = 0;
const onTouchStart = (ev) => {
try {
if (mouse_disabled) {
clearTimeout(mouse_disabled);
}
mouse_disabled = setTimeout(() => {
mouse_disabled = 0;
}, 5000);
(0, canvas_utils_1.getRelativeEventPosition)(ev); // enables tracking of last ev position so on touch end can always tell where we released from
if (ev.target === canvas) {
lastX = ev.touches[0].clientX;
lastY = ev.touches[0].clientY;
startX = ev.touches[0].clientX;
startY = ev.touches[0].clientY;
pointerDown(ev);
}
else if (dragging) {
pointerOut(ev);
}
}
catch (e) {
console.error(e);
}
};
const onTouchEnd = (ev) => {
try {
// Stop a touch screen device always auto scrolling to the chat input box if it is active when you make a move
const currentElement = document.activeElement;
if (ev.target === canvas &&
currentElement &&
currentElement instanceof HTMLElement &&
currentElement.tagName.toLowerCase() === "input") {
currentElement.blur();
}
if (mouse_disabled) {
clearTimeout(mouse_disabled);
}
mouse_disabled = setTimeout(() => {
mouse_disabled = 0;
}, 5000);
if (ev.target === canvas) {
if (Math.sqrt((startX - lastX) * (startX - lastX) +
(startY - lastY) * (startY - lastY)) > 10) {
pointerOut(ev);
}
else {
pointerUp(ev, false);
}
}
else if (dragging) {
pointerOut(ev);
}
}
catch (e) {
console.error(e);
}
};
const onTouchMove = (ev) => {
try {
if (mouse_disabled) {
clearTimeout(mouse_disabled);
}
mouse_disabled = setTimeout(() => {
mouse_disabled = 0;
}, 5000);
(0, canvas_utils_1.getRelativeEventPosition)(ev); // enables tracking of last ev position so on touch end can always tell where we released from
if (ev.target === canvas) {
lastX = ev.touches[0].clientX;
lastY = ev.touches[0].clientY;
if (this.mode === "analyze" && this.analyze_tool === "draw") {
pointerMove(ev);
ev.preventDefault();
return false;
}
}
else if (dragging) {
pointerOut(ev);
}
}
catch (e) {
console.error(e);
}
return undefined;
};
document.addEventListener("touchstart", onTouchStart);
document.addEventListener("touchend", onTouchEnd);
document.addEventListener("touchmove", onTouchMove);
const cleanup = () => {
document.removeEventListener("touchstart", onTouchStart);
document.removeEventListener("touchend", onTouchEnd);
document.removeEventListener("touchmove", onTouchMove);
this.off("destroy", cleanup);
};
this.on("destroy", cleanup);
}
clearAnalysisDrawing() {
this.pen_marks = [];
if (this.pen_ctx) {
this.pen_ctx.clearRect(0, 0, this.metrics.width, this.metrics.height);
}
}
xy2pen(x, y) {
const lx = this.draw_left_labels ? 0.0 : 1.0;
const ly = this.draw_top_labels ? 0.0 : 1.0;
return [
Math.round((x / this.square_size + lx) * 64),
Math.round((y / this.square_size + ly) * 64),
];
}
pen2xy(x, y) {
const lx = this.draw_left_labels ? 0.0 : 1.0;
const ly = this.draw_top_labels ? 0.0 : 1.0;
return [(x / 64 - lx) * this.square_size, (y / 64 - ly) * this.square_size];
}
setPenStyle(color) {
if (!this.pen_ctx) {
throw new Error(`setPenStyle called with null pen_ctx`);
}
this.pen_ctx.strokeStyle = color;
this.pen_ctx.lineWidth = Math.max(1, Math.round(this.square_size * 0.1));
this.pen_ctx.lineCap = "round";
}
onPenStart(ev) {
this.attachPenCanvas();
const pos = (0, canvas_utils_1.getRelativeEventPosition)(ev);
this.last_pen_position = this.xy2pen(pos.x, pos.y);
this.current_pen_mark = { color: this.analyze_subtool, points: this.xy2pen(pos.x, pos.y) };
this.pen_marks.push(this.current_pen_mark);
this.setPenStyle(this.analyze_subtool);
this.syncReviewMove({ pen: this.analyze_subtool, pp: this.xy2pen(pos.x, pos.y) });
}
onPenMove(ev) {
if (!this.pen_ctx) {
throw new Error(`onPenMove called with null pen_ctx`);
}
if (!this.last_pen_position || !this.current_pen_mark) {
throw new Error(`onPenMove called with invalid last pen position or current pen mark`);
}
const pos = (0, canvas_utils_1.getRelativeEventPosition)(ev);
const start = this.last_pen_position;
const s = this.pen2xy(start[0], start[1]);
const end = this.xy2pen(pos.x, pos.y);
const e = this.pen2xy(end[0], end[1]);
const dx = end[0] - start[0];
const dy = end[1] - start[1];
if (dx * dx + dy * dy < 64) {
return;
}
this.last_pen_position = end;
this.current_pen_mark.points.push(dx);
this.current_pen_mark.points.push(dy);
this.pen_ctx.beginPath();
this.pen_ctx.moveTo(s[0], s[1]);
this.pen_ctx.lineTo(e[0], e[1]);
this.pen_ctx.stroke();
this.syncReviewMove({ pp: [dx, dy] });
}
drawPenMarks(pen_marks) {
if (this.review_id && !this.done_loading_review) {
return;
}
if (!pen_marks.length) {
return;
}
this.attachPenCanvas();
if (!this.pen_ctx) {
throw new Error(`onPenMove called with null pen_ctx`);
}
this.clearAnalysisDrawing();
this.pen_marks = pen_marks;
for (let i = 0; i < pen_marks.length; ++i) {
const stroke = pen_marks[i];
this.setPenStyle(stroke.color);
let px = stroke.points[0];
let py = stroke.points[1];
this.pen_ctx.beginPath();
const pt = this.pen2xy(px, py);
this.pen_ctx.moveTo(pt[0], pt[1]);
for (let j = 2; j < stroke.points.length; j += 2) {
px += stroke.points[j];
py += stroke.points[j + 1];
const pt = this.pen2xy(px, py);
this.pen_ctx.lineTo(pt[0], pt[1]);
}
this.pen_ctx.stroke();
}
}
onTap(event, double_tap, right_click, press_duration_ms) {
if (!(this.stone_placement_enabled &&
(this.player_id ||
!this.engine.players.black.id ||
this.mode === "analyze" ||
this.mode === "puzzle"))) {
return;
}
// If there are modes where right click should not behave as if you clicked a placement on the canvas
// then return here instead of proceeding.
if (right_click && this.mode === "play") {
return;
}
const pos = (0, canvas_utils_1.getRelativeEventPosition)(event);
const xx = pos.x;
const yy = pos.y;
const pt = this.xy2ij(xx, yy);
const x = pt.i;
const y = pt.j;
if (x < 0 || y < 0 || x >= this.engine.width || y >= this.engine.height) {
return;
}
if (!this.double_click_submit) {
double_tap = false;
}
if (this.mode === "analyze" &&
event.shiftKey &&
/* don't warp to move tree position when shift clicking in stone edit mode */
!(this.analyze_tool === "stone" &&
(this.analyze_subtool === "black" || this.analyze_subtool === "white")) &&
/* nor when in labeling mode */
this.analyze_tool !== "label") {
const m = this.engine.getMoveByLocation(x, y, true);
if (m) {
this.engine.jumpTo(m);
this.emit("update");
}
return;
}
if (this.mode === "analyze" && this.analyze_tool === "label") {
return;
}
this.setSubmit(undefined);
const tap_time = Date.now();
let removed_count = 0;
const removed_stones = [];
const submit = () => {
const submit_time = Date.now();
if (!this.one_click_submit && (!this.double_click_submit || !double_tap)) {
/* then submit button was pressed, so check to make sure this didn't happen too quick */
const delta = submit_time - tap_time;
if (delta <= 50) {
console.info("Submit button pressed only ", delta, "ms after stone was placed, presuming bad click");
return;
}
}
const sent = this.sendMove({
game_id: this.game_id,
move: (0, util_1.encodeMove)(x, y),
});
if (sent) {
this.playMovementSound();
this.setTitle((0, translate_1._)("Submitting..."));
if (removed_count) {
this.debouncedEmitCapturedStones(removed_stones);
}
this.disableStonePlacement();
delete this.move_selected;
}
else {
console.log("Move not sent, not playing movement sound");
}
};
/* we disable clicking if we've been initialized with the view user,
* unless the board is a demo board (thus black_player_id is 0). */
try {
let force_redraw = false;
if (this.engine.phase === "stone removal" &&
this.engine.isActivePlayer(this.player_id) &&
this.engine.cur_move === this.engine.last_official_move) {
const { removed, group } = this.engine.toggleSingleGroupRemoval(x, y, event.shiftKey || press_duration_ms > 500);
if (group.length) {
this.socket.send("game/removed_stones/set", {
game_id: this.game_id,
removed: removed,
stones: (0, util_1.encodeMoves)(group),
});
}
}
else if (this.mode === "puzzle") {
let puzzle_mode = "place";
let color = 0;
if (this.getPuzzlePlacementSetting) {
const s = this.getPuzzlePlacementSetting();
puzzle_mode = s.mode;
if (s.mode === "setup") {
color = s.color;
if (this.shift_key_is_down || right_click) {
color = color === 1 ? 2 : 1;
}
}
}
if (puzzle_mode === "place") {
if (!double_tap) {
/* we get called for each tap, then once for the final double tap so we only want to process this x2 */
this.engine.place(x, y, true, false, true, false, false);
this.emit("puzzle-place", {
x,
y,
width: this.engine.width,
height: this.engine.height,
color: this.engine.colorToMove(),
});
}
}
if (puzzle_mode === "play") {
/* we get called for each tap, then once for the final double tap so we only want to process this x2 */
/* Also, if we just placed a piece and the computer is waiting to place it's piece (autoplaying), then
* don't allow anything to be placed. */
if (!double_tap && !this.autoplaying_puzzle_move) {
let calls = 0;
if (this.engine.puzzle_player_move_mode !== "fixed" ||
this.engine.cur_move.lookupMove(x, y, this.engine.player, false)) {
const puzzle_place = (mv_x, mv_y) => {
++calls;
removed_count = this.engine.place(mv_x, mv_y, true, false, true, false, false, removed_stones);
this.emit("puzzle-place", {
x: mv_x,
y: mv_y,
width: this.engine.width,
height: this.engine.height,
color: this.engine.colorToMove(),
});
if (this.engine.cur_move.wrong_answer) {
this.emit("puzzle-wrong-answer");
}
if (this.engine.cur_move.correct_answer) {
this.emit("puzzle-correct-answer");
}
if (this.engine.cur_move.branches.length === 0) {
const isobranches = this.engine.cur_move.findStrongIsobranches();
if (isobranches.length > 0) {
const w = (0, util_1.getRandomInt)(0, isobranches.length);
const which = isobranches[w];
console.info("Following isomorphism (" +
(w + 1) +
" of " +
isobranches.length +
")");
this.engine.jumpTo(which);
this.emit("update");
}
}
if (this.engine.cur_move.branches.length) {
const next = this.engine.cur_move.branches[(0, util_1.getRandomInt)(0, this.engine.cur_move.branches.length)];
if (calls === 1 &&
/* only move if it's the "ai" turn.. if we undo we can get into states where we
* are playing for the ai for some moves so don't auto-move blindly */ ((next.player ===
2 &&
this.engine.config.initial_player === "black") ||
(next.player === 1 &&
this.engine.config.initial_player === "white")) &&
this.engine.puzzle_opponent_move_mode !== "manual") {
this.autoplaying_puzzle_move = true;
setTimeout(() => {
this.autoplaying_puzzle_move = false;
puzzle_place(next.x, next.y);
this.emit("update");
}, this.puzzle_autoplace_delay);
}
}
else {
/* default to wrong answer, but only if there are no nodes prior to us that were marked
* as correct */
let c = this.engine.cur_move;
let parent_was_correct = false;
while (c) {
if (c.correct_answer) {
parent_was_correct = true;
break;
}
c = c.parent;
}
if (!parent_was_correct) {
/* default to wrong answer - we say ! here because we will have already emitted
* puzzle-wrong-answer if wrong_answer was true above. */
if (!this.engine.cur_move.wrong_answer) {
this.emit("puzzle-wrong-answer");
}
//break;
}
}
};
puzzle_place(x, y);
}
}
}
if (puzzle_mode === "setup") {
if (this.engine.board[y][x] === color) {
this.engine.initialStatePlace(x, y, 0);
}
else {
this.engine.initialStatePlace(x, y, color);
}
}
this.emit("update");
if (removed_count > 0) {
this.emit("audio-capture-stones", {
count: removed_count,
already_captured: 0,
});
this.debouncedEmitCapturedStones(removed_stones);
}
}
else if (this.engine.phase === "play" ||
(this.engine.phase === "finished" && this.mode === "analyze")) {
if (this.move_selected) {
if (this.mode === "play") {
this.engine.cur_move.removeIfNoChildren();
}
/* If same stone is clicked again, simply remove it */
let same_stone_clicked = false;
if (this.move_selected.x === x && this.move_selected.y === y) {
delete this.move_selected;
same_stone_clicked = true;
}
this.engine.jumpTo(this.engine.last_official_move);
/* If same stone is clicked again, simply remove it */
if (same_stone_clicked) {
this.updatePlayerToMoveTitle();
if (!double_tap) {
this.emit("update");
return;
}
}
}
this.move_selected = { x: x, y: y };
/* Place our stone */
try {
if (!(this.mode === "analyze" &&
this.analyze_tool === "stone" &&
this.analyze_subtool !== "alternate")) {
removed_count = this.engine.place(x, y, true, true, undefined, undefined, undefined, removed_stones);
if (this.mode === "analyze") {
if (this.engine.handicapMovesLeft() > 0) {
this.engine.place(-1, -1);
}
}
}
else {
if (!this.edit_color) {
throw new Error(`Edit place called with invalid edit_color value`);
}
let edit_color = this.engine.playerByColor(this.edit_color);
if (event.shiftKey && edit_color === 1) {
/* if we're going to place a black on an empty square but we're holding down shift, place white */
edit_color = 2;
}
else if (event.shiftKey && edit_color === 2) {
/* if we're going to place a black on an empty square but we're holding down shift, place white */
edit_color = 1;
}
if (this.engine.board[y][x] === edit_color) {
this.engine.editPlace(x, y, 0);
}
else {
this.engine.editPlace(x, y, edit_color);
}
}
if (this.mode === "analyze" && this.analyze_tool === "stone") {
let c = this.engine.cur_move;
while (c && !c.trunk) {
let mark = c.getMoveNumberDifferenceFromTrunk();
if (c.edited) {
mark = "triangle";
}
if (c.x >= 0 && c.y >= 0 && !this.engine.board[c.y][c.x]) {
this.clearTransientMark(c.x, c.y, mark);
}
else {
this.setTransientMark(c.x, c.y, mark, true);
}
c = c.parent