<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>让小熊吃到星星</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
overflow-x: hidden;
font-family: "Avenir Next", Avenir, sans-serif;
font-weight: 500;
font-size: 20px;
color: #555;
}
canvas {
cursor: move;
display: block;
position: absolute;
max-width: 100%;
left: 0;
top: 0;
}
.is-cub-hovered,
.is-cub-hovered canvas {
cursor: -webkit-grab;
cursor: grab;
}
.is-cub-dragging,
.is-cub-dragging canvas {
cursor: -webkit-grabbing;
cursor: grabbing;
}
.instruction {
padding: 0 10px;
text-align: center;
position: absolute;
width: 100%;
padding-bottom: 40px;
}
.button {
font-family: "Avenir Next", Avenir, sans-serif;
font-weight: 500;
font-size: 20px;
padding: 5px 15px;
margin: 10px;
background: #bbb;
color: white;
border-radius: 5px;
border: none;
cursor: pointer;
}
.button:hover {
background: #09f;
}
.top-bar {
position: absolute;
left: 0;
top: 0;
}
.level-select-button {
position: relative;
z-index: 2;
}
.next-level-button {
position: absolute;
left: 50%;
-webkit-transform: translateX(-110px) scale(0.5);
transform: translateX(-110px) scale(0.5);
opacity: 0;
background: #09f;
width: 200px;
height: 80px;
pointer-events: none;
-webkit-transition: -webkit-transform 0.2s, opacity 0.2s;
transition: transform 0.2s, opacity 0.2s;
}
.next-level-button:hover {
background: #2bf;
}
.next-level-button.is-open {
display: inline-block;
pointer-events: auto;
-webkit-transform: translateX(-110px) scale(1);
transform: translate(-110px) scale(1);
opacity: 1;
}
.level-list {
position: absolute;
background: #eee;
width: 100%;
min-height: 100%;
left: 0;
top: 0;
margin: 0;
list-style: none;
padding: 10px;
z-index: 3;
left: -100%;
transition: left 0.2s;
}
.level-list.is-open {
left: 0;
}
.level-list__item {
display: inline-block;
background: #ddd;
margin: 5px;
padding: 10px;
width: 80px;
height: 80px;
text-align: center;
border-radius: 10px;
position: relative;
}
.level-list__item:hover {
color: #09f;
cursor: pointer;
background: white;
}
.level-list__item.is-playing {
background: #09f;
color: white;
}
.level-list__item__number {
display: block;
font-size: 30px;
line-height: 35px;
}
.level-list__item__blurb {
display: block;
font-size: 16px;
}
.level-list__item__check {
position: absolute;
right: -10px;
top: -10px;
width: 30px;
line-height: 30px;
background: #555;
border-radius: 15px;
color: white;
display: none;
}
.level-list__item.did-complete .level-list__item__check {
display: block;
}
.levels {
display: none;
}
</style>
</head>
<body>
<div class="top-bar">
<button class="button level-select-button">关卡</button>
</div>
<ol class="level-list"></ol>
<canvas></canvas>
<p class="instruction"></p>
<button class="button next-level-button">下一关</button>
<div class="levels">
<pre id="intro-fixed1" data-blurb="Tutorial">
blurb: Tutorial
instruction: 拖动小熊到星星
---
*=.=.
!
. . .
!
@=.=.
</pre
>
<pre id="intro-fixed2" data-blurb="Tutorial">
blurb: Tutorial
instruction: 拖动网格来旋转。小熊和星星会随着网格一起移动。橙线会保持不动。
---
* . .
!
. . .
!
@=.=.
</pre
>
<pre id="intro-fixed3" data-blurb="★">
blurb: ★
---
@=. .
. . .
!
*=. .
</pre
>
<pre id="intro-free1" data-blurb="Tutorial">
blurb: Tutorial
instruction: 蓝线会随着网格移动。旋转网格来连接蓝线和橙线。
---
@-. .
! |
. . .
|
*-.-.
</pre
>
<pre id="m3x3-2-med" data-blurb="★">
blurb: ★
---
. . *
| | |
. . .
| | |
@ .=.
</pre
>
<pre id="m3x3-fixed-switch" data-blurb="★">
blurb: ★
---
*=.-.
. . .
|
@-. .
</pre
>
<pre id="m4x4-2" data-blurb="★">
blurb: ★
---
. .=. .
| !
. . .-*
|
. . . .
. @-. .
</pre
>
<pre id="m4x4-1" data-blurb="★">
blurb: ★
---
. . . .
* . . @
| ! |
. . . .
!
. . . .
</pre
>
<pre id="m4x4-3" data-blurb="★">
blurb: ★
---
. @ . .
! |
. . . .
|
.=.=.-.
|
. * . .
</pre
>
<pre id="m4x4-4" data-blurb="★">
blurb: ★
---
. . . .
* . . .
!
. . .-.
!
.=.=. @
</pre
>
<pre id="m4x4-5" data-blurb="★">
blurb: ★
---
.-.-.-.
|
@ .-.-.
* .=. .
! |
.-.-. .
</pre
>
<pre id="m4x4-6-med" data-blurb="★">
blurb: ★
---
. * . .
.-.=. .
|
. . . .
! |
.=. @ .
</pre
>
<pre id="m4x4-7-hard1" data-blurb="★★">
blurb: ★★
---
. . *-.
.-.=. .
|
.=. . .
| |
@-.-.=.
</pre
>
<pre id="m4x4-8-hard2" data-blurb="★★">
blurb: ★★
---
.-@ .=.
. . . .
|
.-. .-*
|
. .=.-.
</pre
>
<pre id="m4x4-9-hard1" data-blurb="★★">
blurb: ★★
---
. . .=.
!
@-. .-.
. .=. .
. . * .
</pre
>
<pre id="m4x4-10-hard1" data-blurb="★★">
blurb: ★★
---
. @=. .
|
. .-.-.
.-.-.-.
! !
. * . .
</pre
>
<pre id="m5x5-3" data-blurb="★">
. . . . .
| !
. . .-. .
|
. . . . *
|
. . .=. .
|
. @ . . .
</pre
>
<pre id="m5x5-1" data-blurb="★">
@-.-. .-.
|
. . . . .
. . .=. .
. . . .=.
|
. .=.-* .
</pre
>
<pre id="m5x5-2" data-blurb="★★">
. . . . .
. .=.-. @
| !
. . . .-.
.=. . .=.
!
* . . . .
</pre
>
<pre id="m5x5-4" data-blurb="★★">
. . . .-.
!
. .-. . .
! |
.=. . . .
|
. . . . *
|
.-@=. .=.
</pre
>
<pre id="m5x5-5" data-blurb="★★">
. . . . .
. . .-. *
!
. . .-. .
.=. . . .
|
. @-. . .
</pre
>
<pre id="m5x5-6" data-blurb="★★">
. . .-.-.
! !
. .=.-. .
|
. .-. .-@
!
* .=. . .
|
.=. .-.=.
</pre
>
<pre id="m5x5-7" data-blurb="★★★">
.=* . @=.
|
. .=. . .
| | |
.=. . .-.
|
. . . .=.
!
. .-.-. .
</pre
>
<pre id="m5x5-8" data-blurb="★★★">
. * . .-.
|
. . .=.-.
! |
. . . . .
. .-. .=.
|
. . .=.-@
</pre
>
<pre id="m5x5-9" data-blurb="★★★">
.-.-. . .
|
. . . .-@
!
* . .-. .
| !
.-. . .=.
| !
. . .=. .
</pre
>
<pre id="m5x5-10" data-blurb="★★">
. . . . .
. . . .-@
!
* . .=. .
| !
.-. . . .
. . . . .
</pre
>
<pre id="m5x5-11" data-blurb="★★★">
. . . .=.
|
. . . .=.
|
. . .-. .
! |
. .=. . .
| ! !
.-@ . * .
</pre
>
<pre id="m5x5-12" data-blurb="★★">
. . .=.=.
. . . . .
. . . . @
. . . . .
* . .=.=.
</pre
>
<pre id="m6x6-1-hard1" data-blurb="★★★">
. . * . . .
! | |
. .-. .-. .
|
. . . . .-.
| ! |
. . .=. . .
|
@-.-. .-. .
|
. .=. . .-.
</pre
>
<pre id="m6x6-2" data-blurb="★★★">
@ .=. . .=.
| | !
. . . .=. .
| |
. . . .-. .
| !
. . . . . *
| |
.=. .-. . .
| | |
.-. . . .=.
</pre
>
<pre id="m6x6-3" data-blurb="★★★">
.=. .=.-.-*
|
.-. . . . .
| !
. . .-.-. .
!
.-. .=.=. .
@ .=. . . .
| !
. .-. .-. .
</pre
>
<pre id="pivot-4x4-intro" data-blurb="Tutorial">
instruction: 绿线会随着网格旋转,但方向不变。
---
. .-* .
|
. . . .
. .>. .
. @ . .
</pre
>
<pre id="pivot-5x5-2" data-blurb="★★">
. . .-.-@
. .<. . .
.>. . . .
| !
.-.-. . *
!
. . . . .
</pre
>
<pre id="pivot-5x5-swirly" data-blurb="★★★">
. . . . .
^
.<. . . *
. . . . .
@ . . .>.
v
. . . . .
</pre
>
<pre id="pivot-5x5-1" data-blurb="★★★">
. .-. . .
^
. .<.=.=.
.>. . .-@
* . . .=.
. . . . .
</pre
>
<pre id="pivot-5x5-3" data-blurb="★★">
.=. . .-*
v
. . . . .
. . .-.J.
@-. . . .
v
.<. . . .
</pre
>
<pre id="pivot-5x5-4" data-blurb="★★★">
.-.-. @>.
! ^
. . . . .
|
. . . . .
|
. . . .=*
^
. . .-. .>
</pre
>
<pre id="pivot-5x5-5" data-blurb="★★★">
.-. . . *
. .>. . .
| v
.-. . . .
^
. . .-. .
v
@=.=. . .
</pre
>
<pre id="pivot-5x5-6" data-blurb="★★★">
. . .>. .
! |
@=. .-. .
. . . .=.>
. . . . .
. *>.<. .
</pre
>
<pre id="pivot-5x5-7" data-blurb="★★★">
* . @ . .
v |
. . . . .
!
. . . . .
^ ! !
. .-. . .
!
. . . . .
v
</pre
>
<pre id="pivot-6x6-1" data-blurb="★★★">
. . . . . .
| v
@ . . . . *
| |
. . . . . .
| ! ^ | K
. . . .-.=.
|
. .-. . . .
v
.>. . . . .
</pre
>
<pre id="pivot-6x6-3" data-blurb="★★★">
. @-. .>.-.
. . . . . .
|
* .>. .=. .
!
. . . . . .>
| ^
. . . .=. .
. .=. . .=.>
</pre
>
<pre id="pivot-6x6-2" data-blurb="★★★">
. .-.-. .=.
v
. . . . . .
| ! v
.>. . . . *
^
. . . . . .
|
. .-.<. . .
! | |
. . . .>.-@
</pre
>
<pre id="m44" data-blurb="★★">
. .=. *-.
. . .=. .
!
. . . . .
| !
. . . . .
| |
. @ . .=.
</pre
>
<pre id="m45" data-blurb="★★">
@ * .>. .
. .=.=. .
| |
.>. . . .
. . . .>.
|
.=. . .-.
</pre
>
<pre id="m46" data-blurb="★★★">
.-. . .
^
. . . .
.L. . .
!
@ . .-*
</pre
>
<pre id="m47" data-blurb="★★">
@ . . . . .
v v v v v v
. . . . . .
. . . . . .
. . . . . .
v v v v v
. . . . . .
. . . .=. *
v v v v v
</pre
>
<pre id="m48" data-blurb="★">
.-.<.>.=. .
W ! |
. . .A. . *
| |
. .=. . . .
^ !
. .D.-.=.=@
|
. . .-.-. .
|
.#.=. .<. .
v v
</pre
>
<pre id="m49" data-blurb="★★★">
. . .-@ .
|
. . . .J.
* . . . .
| ! !
. . . . .
v !
. . . .-.
</pre
>
<pre id="m50" data-blurb="★★★">
*=. . .
v
. . . .
^ |
. . . .
^ |
@ .>. .
</pre
>
<pre id="rotate-tut" data-blurb="Tutorial">
instruction: 红线会固定位置,但会随着网格旋转。
---
. . . .
@ .4. .
|
. . .-*
. . . .
</pre
>
<pre id="rotate1" data-blurb="★">
. . .-*
|
. . . .
5
.4. . .
|
@ . . .
</pre
>
<pre id="rotate2" data-blurb="★★">
@ .-.=.
|
. . .4.
|
* . . .
| |
. . . .
</pre
>
<pre id="rotate3" data-blurb="★★">
. . * .
! 5 v
. . . @
|
. .4. .
!
. . . .
</pre
>
<pre id="rotate3b" data-blurb="★★">
* . . .
! 5
. . . @
|
. .4. .
!
. . . .
</pre
>
<pre id="rotate-5x5-1" data-blurb="★★">
. . . .-@
8
. .=. . .
*=. . . .
. .-. . .
. . . . .
</pre
>
<pre id="rotate-5x5-2" data-blurb="★★">
. . . . .
. . . .6*
|
. . . .=.
|
.4. . . .
|
. . . .-@
</pre
>
<pre id="rotate-5x5-2b" data-blurb="★★★">
. . . . .
! |
.-.-. . .
v |
. . .-. .
@ . . . .
5
. . .=* .
</pre
>
<pre id="rotate-6x6-1" data-blurb="★★★">
@4.=. . . .
. . . . . .
v 8 |
.-.-. . . .
! ! ^
. . . . . .
. .>. . . .
!
* . .4. . .
</pre
>
<pre id="rotate-6x6-2" data-blurb="★★★">
. . *<. . .
.=. .-. . .
5
. . . .-. .
|
. . . . . .
. . . . . .
5 |
. .=. . @-.
</pre
>
<pre id="rotate-6x6-3" data-blurb="★★★">
.4. . . . @
!
.-. . .=. .
!
. . . . . .
!
.>.6. . . .
!
. . . .=.-.
^
. . . . * .
</pre
>
</div>
<script>
(function (global, factory) {
if (typeof define == "function" && define.amd) {
define(factory);
} else if (typeof module == "object" && module.exports) {
module.exports = factory();
} else {
global.EvEmitter = factory();
}
})(this, function () {
"use strict";
function EvEmitter() {}
var proto = EvEmitter.prototype;
proto.on = function (eventName, listener) {
if (!eventName || !listener) {
return;
}
var events = (this._events = this._events || {});
var listeners = (events[eventName] = events[eventName] || []);
if (listeners.indexOf(listener) == -1) {
listeners.push(listener);
}
return this;
};
proto.once = function (eventName, listener) {
if (!eventName || !listener) {
return;
}
this.on(eventName, listener);
var onceEvents = (this._onceEvents = this._onceEvents || {});
var onceListeners = (onceEvents[eventName] =
onceEvents[eventName] || {});
onceListeners[listener] = true;
return this;
};
proto.off = function (eventName, listener) {
var listeners = this._events && this._events[eventName];
if (!listeners || !listeners.length) {
return;
}
var index = listeners.indexOf(listener);
if (index != -1) {
listeners.splice(index, 1);
}
return this;
};
proto.emitEvent = function (eventName, args) {
var listeners = this._events && this._events[eventName];
if (!listeners || !listeners.length) {
return;
}
var i = 0;
var listener = listeners[i];
args = args || [];
var onceListeners = this._onceEvents && this._onceEvents[eventName];
while (listener) {
var isOnce = onceListeners && onceListeners[listener];
if (isOnce) {
this.off(eventName, listener);
delete onceListeners[listener];
}
listener.apply(this, args);
i += isOnce ? 0 : 1;
listener = listeners[i];
}
return this;
};
return EvEmitter;
});
(function (window, factory) {
if (typeof define == "function" && define.amd) {
define(["ev-emitter/ev-emitter"], function (EvEmitter) {
return factory(window, EvEmitter);
});
} else if (typeof module == "object" && module.exports) {
module.exports = factory(window, require("ev-emitter"));
} else {
window.Unipointer = factory(window, window.EvEmitter);
}
})(window, function factory(window, EvEmitter) {
"use strict";
function noop() {}
function Unipointer() {}
var proto = (Unipointer.prototype = Object.create(EvEmitter.prototype));
proto.bindStartEvent = function (elem) {
this._bindStartEvent(elem, true);
};
proto.unbindStartEvent = function (elem) {
this._bindStartEvent(elem, false);
};
proto._bindStartEvent = function (elem, isBind) {
isBind = isBind === undefined ? true : !!isBind;
var bindMethod = isBind ? "addEventListener" : "removeEventListener";
if (window.navigator.pointerEnabled) {
elem[bindMethod]("pointerdown", this);
} else if (window.navigator.msPointerEnabled) {
elem[bindMethod]("MSPointerDown", this);
} else {
elem[bindMethod]("mousedown", this);
elem[bindMethod]("touchstart", this);
}
};
proto.handleEvent = function (event) {
var method = "on" + event.type;
if (this[method]) {
this[method](event);
}
};
proto.getTouch = function (touches) {
for (var i = 0; i < touches.length; i++) {
var touch = touches[i];
if (touch.identifier == this.pointerIdentifier) {
return touch;
}
}
};
proto.onmousedown = function (event) {
var button = event.button;
if (button && button !== 0 && button !== 1) {
return;
}
this._pointerDown(event, event);
};
proto.ontouchstart = function (event) {
this._pointerDown(event, event.changedTouches[0]);
};
proto.onMSPointerDown = proto.onpointerdown = function (event) {
this._pointerDown(event, event);
};
proto._pointerDown = function (event, pointer) {
if (this.isPointerDown) {
return;
}
this.isPointerDown = true;
this.pointerIdentifier =
pointer.pointerId !== undefined
?
pointer.pointerId
: pointer.identifier;
this.pointerDown(event, pointer);
};
proto.pointerDown = function (event, pointer) {
this._bindPostStartEvents(event);
this.emitEvent("pointerDown", [event, pointer]);
};
var postStartEvents = {
mousedown: ["mousemove", "mouseup"],
touchstart: ["touchmove", "touchend", "touchcancel"],
pointerdown: ["pointermove", "pointerup", "pointercancel"],
MSPointerDown: ["MSPointerMove", "MSPointerUp", "MSPointerCancel"],
};
proto._bindPostStartEvents = function (event) {
if (!event) {
return;
}
var events = postStartEvents[event.type];
events.forEach(function (eventName) {
window.addEventListener(eventName, this);
}, this);
this._boundPointerEvents = events;
};
proto._unbindPostStartEvents = function () {
if (!this._boundPointerEvents) {
return;
}
this._boundPointerEvents.forEach(function (eventName) {
window.removeEventListener(eventName, this);
}, this);
delete this._boundPointerEvents;
};
proto.onmousemove = function (event) {
this._pointerMove(event, event);
};
proto.onMSPointerMove = proto.onpointermove = function (event) {
if (event.pointerId == this.pointerIdentifier) {
this._pointerMove(event, event);
}
};
proto.ontouchmove = function (event) {
var touch = this.getTouch(event.changedTouches);
if (touch) {
this._pointerMove(event, touch);
}
};
proto._pointerMove = function (event, pointer) {
this.pointerMove(event, pointer);
};
proto.pointerMove = function (event, pointer) {
this.emitEvent("pointerMove", [event, pointer]);
};
proto.onmouseup = function (event) {
this._pointerUp(event, event);
};
proto.onMSPointerUp = proto.onpointerup = function (event) {
if (event.pointerId == this.pointerIdentifier) {
this._pointerUp(event, event);
}
};
proto.ontouchend = function (event) {
var touch = this.getTouch(event.changedTouches);
if (touch) {
this._pointerUp(event, touch);
}
};
proto._pointerUp = function (event, pointer) {
this._pointerDone();
this.pointerUp(event, pointer);
};
proto.pointerUp = function (event, pointer) {
this.emitEvent("pointerUp", [event, pointer]);
};
proto._pointerDone = function () {
this.isPointerDown = false;
delete this.pointerIdentifier;
this._unbindPostStartEvents();
this.pointerDone();
};
proto.pointerDone = noop;
proto.onMSPointerCancel = proto.onpointercancel = function (event) {
if (event.pointerId == this.pointerIdentifier) {
this._pointerCancel(event, event);
}
};
proto.ontouchcancel = function (event) {
var touch = this.getTouch(event.changedTouches);
if (touch) {
this._pointerCancel(event, touch);
}
};
proto._pointerCancel = function (event, pointer) {
this._pointerDone();
this.pointerCancel(event, pointer);
};
proto.pointerCancel = function (event, pointer) {
this.emitEvent("pointerCancel", [event, pointer]);
};
Unipointer.getPointerPoint = function (pointer) {
return {
x: pointer.pageX,
y: pointer.pageY,
};
};
return Unipointer;
});
function FreeSegment(a, b) {
this.type = "FreeSegment";
this.a = a;
this.b = b;
this.noon = {
a: a,
b: b,
};
this.three = {
a: { x: -a.y, y: a.x },
b: { x: -b.y, y: b.x },
};
this.six = {
a: { x: -a.x, y: -a.y },
b: { x: -b.x, y: -b.y },
};
this.nine = {
a: { x: a.y, y: -a.x },
b: { x: b.y, y: -b.x },
};
}
var proto = FreeSegment.prototype;
proto.render = function (ctx, center, gridSize) {
var ax = this.a.x * gridSize;
var ay = this.a.y * gridSize;
var bx = this.b.x * gridSize;
var by = this.b.y * gridSize;
ctx.strokeStyle = "hsla(200, 80%, 50%, 0.7)";
ctx.lineWidth = gridSize * 0.6;
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(ax, ay);
ctx.lineTo(bx, by);
ctx.stroke();
ctx.closePath();
};
function FixedSegment(a, b) {
this.type = "FixedSegment";
this.a = a;
this.b = b;
this.noon = { a: a, b: b };
this.three = { a: a, b: b };
this.six = { a: a, b: b };
this.nine = { a: a, b: b };
}
var proto = FixedSegment.prototype;
proto.render = function (ctx, center, gridSize) {
var ax = this.a.x * gridSize;
var ay = this.a.y * gridSize;
var bx = this.b.x * gridSize;
var by = this.b.y * gridSize;
ctx.strokeStyle = "hsla(30, 100%, 40%, 0.6)";
ctx.lineWidth = gridSize * 0.8;
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(ax, ay);
ctx.lineTo(bx, by);
ctx.stroke();
ctx.closePath();
};
function PivotSegment(a, b) {
this.type = "FreeSegment";
this.a = a;
this.b = b;
var dx = b.x - a.x;
var dy = b.y - a.y;
this.delta = { x: dx, y: dy };
this.noon = {
a: a,
b: b,
};
this.three = {
a: { x: -a.y, y: a.x },
b: { x: -a.y + dx, y: a.x + dy },
};
this.six = {
a: { x: -a.x, y: -a.y },
b: { x: -a.x + dx, y: -a.y + dy },
};
this.nine = {
a: { x: a.y, y: -a.x },
b: { x: a.y + dx, y: -a.x + dy },
};
}
var proto = PivotSegment.prototype;
proto.render = function (ctx, center, gridSize, mazeAngle) {
var ax = this.a.x * gridSize;
var ay = this.a.y * gridSize;
var bx = this.delta.x * gridSize;
var by = this.delta.y * gridSize;
ctx.save();
ctx.translate(ax, ay);
ctx.rotate(-mazeAngle);
var color = "hsla(150, 100%, 35%, 0.7)";
ctx.strokeStyle = color;
ctx.lineWidth = gridSize * 0.4;
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(bx, by);
ctx.stroke();
ctx.closePath();
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(0, 0, gridSize * 0.4, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
ctx.restore();
};
var TAU = Math.PI * 2;
function RotateSegment(a, b) {
this.type = "RotateSegment";
this.a = a;
this.b = b;
var dx = b.x - a.x;
var dy = b.y - a.y;
this.delta = { x: dx, y: dy };
this.theta = Math.atan2(dy, dx);
this.noon = { a: a, b: b };
this.three = { a: a, b: this.getB(TAU / 4) };
this.six = { a: a, b: this.getB(TAU / 2) };
this.nine = { a: a, b: this.getB((TAU * 3) / 4) };
}
var proto = RotateSegment.prototype;
proto.getB = function (angle) {
return {
x: Math.round(this.a.x + Math.cos(this.theta + angle) * 2),
y: Math.round(this.a.y + Math.sin(this.theta + angle) * 2),
};
};
proto.render = function (ctx, center, gridSize, mazeAngle) {
var ax = this.a.x * gridSize;
var ay = this.a.y * gridSize;
ctx.save();
ctx.translate(ax, ay);
ctx.rotate(mazeAngle);
var color = "hsla(0, 100%, 50%, 0.6)";
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = gridSize * 0.8;
ctx.lineJoin = "round";
ctx.rotate(TAU / 8);
ctx.strokeRect(
-gridSize * 0.2,
-gridSize * 0.2,
gridSize * 0.4,
gridSize * 0.4
);
ctx.rotate(-TAU / 8);
ctx.lineWidth = gridSize * 0.8;
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(0, 0);
var bx = this.delta.x * gridSize;
var by = this.delta.y * gridSize;
ctx.lineTo(bx, by);
ctx.stroke();
ctx.closePath();
ctx.restore();
};
var TAU = Math.PI * 2;
function FlyWheel(props) {
this.angle = 0;
this.friction = 0.95;
this.velocity = 0;
for (var prop in props) {
this[prop] = props[prop];
}
}
var proto = FlyWheel.prototype;
proto.integrate = function () {
this.velocity *= this.friction;
this.angle += this.velocity;
this.normalizeAngle();
};
proto.applyForce = function (force) {
this.velocity += force;
};
proto.normalizeAngle = function () {
this.angle = ((this.angle % TAU) + TAU) % TAU;
};
proto.setAngle = function (theta) {
var velo = theta - this.angle;
if (velo > TAU / 2) {
velo -= TAU;
} else if (velo < -TAU / 2) {
velo += TAU;
}
var force = velo - this.velocity;
this.applyForce(force);
};
var cub = {
offset: { x: 0, y: 0 },
};
var pegOrienter = {
noon: function (peg) {
return peg;
},
three: function (peg) {
return { x: peg.y, y: -peg.x };
},
six: function (peg) {
return { x: -peg.x, y: -peg.y };
},
nine: function (peg) {
return { x: -peg.y, y: peg.x };
},
};
cub.setPeg = function (peg, orientation) {
peg = pegOrienter[orientation](peg);
this.peg = peg;
this.noon = { x: peg.x, y: peg.y };
this.three = { x: -peg.y, y: peg.x };
this.six = { x: -peg.x, y: -peg.y };
this.nine = { x: peg.y, y: -peg.x };
};
var offsetOrienter = {
noon: function (offset) {
return offset;
},
three: function (offset) {
return { x: offset.y, y: -offset.x };
},
six: function (offset) {
return { x: -offset.x, y: -offset.y };
},
nine: function (offset) {
return { x: -offset.y, y: offset.x };
},
};
cub.setOffset = function (offset, orientation) {
this.offset = offsetOrienter[orientation](offset);
};
cub.render = function (ctx, mazeCenter, gridSize, angle, isHovered) {
function circle(x, y, radius) {
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
}
var x = this.peg.x * gridSize + this.offset.x;
var y = this.peg.y * gridSize + this.offset.y;
ctx.save();
ctx.translate(mazeCenter.x, mazeCenter.y);
ctx.rotate(angle);
ctx.translate(x, y);
ctx.rotate(-angle);
ctx.fillStyle = "hsla(330, 100%, 40%, 1)";
var scale = isHovered ? 1.15 : 1;
ctx.scale(scale, scale);
circle(0, 0, gridSize * 0.6);
circle(gridSize * -0.45, gridSize * -0.35, gridSize * 0.3);
circle(gridSize * 0.45, gridSize * -0.35, gridSize * 0.3);
ctx.restore();
};
function Maze() {
this.freeSegments = [];
this.fixedSegments = [];
this.pivotSegments = [];
this.rotateSegments = [];
this.flyWheel = new FlyWheel({
friction: 0.8,
});
this.connections = {};
}
var proto = Maze.prototype;
proto.loadText = function (text) {
var sections = text.split("---\n");
var frontMatter = {};
if (sections.length > 1) {
frontMatter = getFrontMatter(sections[0]);
}
var instructElem = document.querySelector(".instruction");
instructElem.innerHTML = frontMatter.instruction || "";
var mazeSrc = sections[sections.length - 1];
var lines = mazeSrc.split("\n");
var gridCount = (this.gridCount = lines[0].length);
var gridMax = (this.gridMax = (gridCount - 1) / 2);
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
var chars = line.split("");
for (var j = 0; j < chars.length; j++) {
var character = chars[j];
var pegX = j - gridMax;
var pegY = i - gridMax;
var parseMethod = "parse" + character;
if (this[parseMethod]) {
this[parseMethod](pegX, pegY);
}
}
}
};
function getFrontMatter(text) {
if (!text) {
return;
}
var frontMatter = {};
text.split("\n").forEach(function (line) {
if (!line) {
return;
}
var parts = line.split(":");
var key = parts[0].trim();
var value = parts[1].trim();
if (value === "true") {
value = true;
} else if (value === "false") {
value = false;
} else if (value.match(/$\d+(\.\d+)?^/)) {
value = parseFloat(value, 10);
} else if (value.match(/$\d+\.\d+^/)) {
value = parseFloat(value);
}
frontMatter[key] = value;
});
return frontMatter;
}
proto["parse-"] = proto.addFreeHorizSegment = function (pegX, pegY) {
var segment = getHorizSegment(pegX, pegY, FreeSegment);
this.connectSegment(segment);
this.freeSegments.push(segment);
};
proto["parse|"] = proto.addFreeVertSegment = function (pegX, pegY) {
var segment = getVertSegment(pegX, pegY, FreeSegment);
this.connectSegment(segment);
this.freeSegments.push(segment);
};
proto["parse="] = proto.addFixedHorizSegment = function (pegX, pegY) {
var segment = getHorizSegment(pegX, pegY, FixedSegment);
this.connectSegment(segment);
this.fixedSegments.push(segment);
};
proto["parse!"] = proto.addFixedVertSegment = function (pegX, pegY) {
var segment = getVertSegment(pegX, pegY, FixedSegment);
this.connectSegment(segment);
this.fixedSegments.push(segment);
};
function getHorizSegment(pegX, pegY, Segment) {
var a = { x: pegX + 1, y: pegY };
var b = { x: pegX - 1, y: pegY };
return new Segment(a, b);
}
function getVertSegment(pegX, pegY, Segment) {
var a = { x: pegX, y: pegY + 1 };
var b = { x: pegX, y: pegY - 1 };
return new Segment(a, b);
}
proto["parse^"] = proto.addPivotUpSegment = function (pegX, pegY) {
var a = { x: pegX, y: pegY + 1 };
var b = { x: pegX, y: pegY - 1 };
var segment = new PivotSegment(a, b);
this.connectSegment(segment);
this.pivotSegments.push(segment);
};
proto.parsev = proto.addPivotDownSegment = function (pegX, pegY) {
var a = { x: pegX, y: pegY - 1 };
var b = { x: pegX, y: pegY + 1 };
var segment = new PivotSegment(a, b);
this.connectSegment(segment);
this.pivotSegments.push(segment);
};
proto["parse<"] = proto.addPivotLeftSegment = function (pegX, pegY) {
var a = { x: pegX + 1, y: pegY };
var b = { x: pegX - 1, y: pegY };
var segment = new PivotSegment(a, b);
this.connectSegment(segment);
this.pivotSegments.push(segment);
};
proto["parse>"] = proto.addPivotRightSegment = function (pegX, pegY) {
var a = { x: pegX - 1, y: pegY };
var b = { x: pegX + 1, y: pegY };
var segment = new PivotSegment(a, b);
this.connectSegment(segment);
this.pivotSegments.push(segment);
};
proto.parse8 = proto.addRotateUpSegment = function (pegX, pegY) {
var a = { x: pegX, y: pegY + 1 };
var b = { x: pegX, y: pegY - 1 };
var segment = new RotateSegment(a, b);
this.connectSegment(segment);
this.rotateSegments.push(segment);
};
proto.parse4 = proto.addRotateLeftSegment = function (pegX, pegY) {
var a = { x: pegX + 1, y: pegY };
var b = { x: pegX - 1, y: pegY };
var segment = new RotateSegment(a, b);
this.connectSegment(segment);
this.rotateSegments.push(segment);
};
proto.parse5 = proto.addRotateUpSegment = function (pegX, pegY) {
var a = { x: pegX, y: pegY - 1 };
var b = { x: pegX, y: pegY + 1 };
var segment = new RotateSegment(a, b);
this.connectSegment(segment);
this.rotateSegments.push(segment);
};
proto.parse6 = proto.addRotateRightSegment = function (pegX, pegY) {
var a = { x: pegX - 1, y: pegY };
var b = { x: pegX + 1, y: pegY };
var segment = new RotateSegment(a, b);
this.connectSegment(segment);
this.rotateSegments.push(segment);
};
proto["parse#"] = function (pegX, pegY) {
this.addFreeHorizSegment(pegX, pegY);
this.addFixedHorizSegment(pegX, pegY);
};
proto.parse$ = function (pegX, pegY) {
this.addFreeVertSegment(pegX, pegY);
this.addFixedVertSegment(pegX, pegY);
};
proto.parseI = function (pegX, pegY) {
this.addPivotUpSegment(pegX, pegY);
this.addFixedVertSegment(pegX, pegY);
};
proto.parseJ = function (pegX, pegY) {
this.addPivotLeftSegment(pegX, pegY);
this.addFixedHorizSegment(pegX, pegY);
};
proto.parseK = function (pegX, pegY) {
this.addPivotDownSegment(pegX, pegY);
this.addFixedVertSegment(pegX, pegY);
};
proto.parseL = function (pegX, pegY) {
this.addPivotRightSegment(pegX, pegY);
this.addFixedHorizSegment(pegX, pegY);
};
proto.parseW = function (pegX, pegY) {
this.addPivotUpSegment(pegX, pegY);
this.addFreeVertSegment(pegX, pegY);
};
proto.parseA = function (pegX, pegY) {
this.addPivotLeftSegment(pegX, pegY);
this.addFreeHorizSegment(pegX, pegY);
};
proto.parseS = function (pegX, pegY) {
this.addPivotDownSegment(pegX, pegY);
this.addFreeVertSegment(pegX, pegY);
};
proto.parseD = function (pegX, pegY) {
this.addPivotRightSegment(pegX, pegY);
this.addFreeHorizSegment(pegX, pegY);
};
proto["parse@"] = function (pegX, pegY) {
this.startPosition = { x: pegX, y: pegY };
cub.setPeg(this.startPosition, "noon");
};
proto["parse*"] = function (pegX, pegY) {
this.goalPosition = { x: pegX, y: pegY };
};
proto.updateItemGroups = function () {
var itemGroups = {};
this.items.forEach(function (item) {
if (itemGroups[item.type] === undefined) {
itemGroups[item.type] = [];
}
itemGroups[item.type].push(item);
});
this.itemGroups = itemGroups;
};
var orientations = ["noon", "three", "six", "nine"];
proto.connectSegment = function (segment) {
orientations.forEach(function (orientation) {
var line = segment[orientation];
if (this.getIsPegOut(line.a) || this.getIsPegOut(line.b)) {
return;
}
this.connectPeg(segment, orientation, line.a);
this.connectPeg(segment, orientation, line.b);
}, this);
};
proto.getIsPegOut = function (peg) {
return Math.abs(peg.x) > this.gridMax || Math.abs(peg.y) > this.gridMax;
};
proto.connectPeg = function (segment, orientation, peg) {
var key = orientation + ":" + peg.x + "," + peg.y;
var connection = this.connections[key];
if (!connection) {
connection = this.connections[key] = [];
}
if (connection.indexOf(segment) == -1) {
connection.push(segment);
}
};
proto.update = function () {
this.flyWheel.integrate();
var angle = this.flyWheel.angle;
if (angle < TAU / 8) {
this.orientation = "noon";
} else if (angle < (TAU * 3) / 8) {
this.orientation = "three";
} else if (angle < (TAU * 5) / 8) {
this.orientation = "six";
} else if (angle < (TAU * 7) / 8) {
this.orientation = "nine";
} else {
this.orientation = "noon";
}
};
proto.attractAlignFlyWheel = function () {
var angle = this.flyWheel.angle;
var target;
if (angle < TAU / 8) {
target = 0;
} else if (angle < (TAU * 3) / 8) {
target = TAU / 4;
} else if (angle < (TAU * 5) / 8) {
target = TAU / 2;
} else if (angle < (TAU * 7) / 8) {
target = (TAU * 3) / 4;
} else {
target = TAU;
}
var attraction = (target - angle) * 0.03;
this.flyWheel.applyForce(attraction);
};
var TAU = Math.PI * 2;
var orientationAngles = {
noon: 0,
three: TAU / 4,
six: TAU / 2,
nine: (TAU * 3) / 4,
};
proto.render = function (ctx, center, gridSize, angle) {
var orientationAngle = orientationAngles[angle];
var gridMax = this.gridMax;
angle = orientationAngle !== undefined ? orientationAngle : angle || 0;
ctx.save();
ctx.translate(center.x, center.y);
this.fixedSegments.forEach(function (segment) {
segment.render(ctx, center, gridSize);
});
this.rotateSegments.forEach(function (segment) {
segment.render(ctx, center, gridSize, angle);
});
ctx.rotate(angle);
ctx.lineWidth = gridSize * 0.2;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.lineWidth = gridSize * 0.2;
ctx.strokeStyle = "hsla(0, 0%, 50%, 0.2)";
ctx.save();
ctx.rotate(Math.PI / 4);
ctx.strokeRect(
-gridSize / 5,
-gridSize / 5,
(gridSize * 2) / 5,
(gridSize * 2) / 5
);
ctx.restore();
ctx.strokeStyle = "hsla(330, 100%, 50%, 0.3)";
ctx.lineWidth = gridSize * 0.15;
var startX = this.startPosition.x * gridSize;
var startY = this.startPosition.y * gridSize;
strokeCircle(ctx, startX, startY, gridSize * 0.5);
for (var pegY = -gridMax; pegY <= gridMax; pegY += 2) {
for (var pegX = -gridMax; pegX <= gridMax; pegX += 2) {
var pegXX = pegX * gridSize;
var pegYY = pegY * gridSize;
ctx.fillStyle = "hsla(0, 0%, 50%, 0.6)";
fillCircle(ctx, pegXX, pegYY, gridSize * 0.15);
}
}
this.freeSegments.forEach(function (segment) {
segment.render(ctx, center, gridSize);
});
this.pivotSegments.forEach(function (segment) {
segment.render(ctx, center, gridSize, angle);
});
var goalX = this.goalPosition.x * gridSize;
var goalY = this.goalPosition.y * gridSize;
ctx.lineWidth = gridSize * 0.3;
ctx.fillStyle = "hsla(50, 100%, 50%, 1)";
ctx.strokeStyle = "hsla(50, 100%, 50%, 1)";
renderGoal(ctx, goalX, goalY, angle, gridSize * 0.6, gridSize * 0.3);
ctx.restore();
};
function fillCircle(ctx, x, y, radius) {
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
}
function strokeCircle(ctx, x, y, radius) {
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.stroke();
ctx.closePath();
}
function renderGoal(ctx, x, y, mazeAngle, radiusA, radiusB) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(-mazeAngle);
ctx.beginPath();
for (var i = 0; i < 11; i++) {
var theta = (Math.PI * 2 * i) / 10 + Math.PI / 2;
var radius = i % 2 ? radiusA : radiusB;
var dx = Math.cos(theta) * radius;
var dy = Math.sin(theta) * radius;
ctx[i ? "lineTo" : "moveTo"](dx, dy);
}
ctx.fill();
ctx.stroke();
ctx.closePath();
ctx.restore();
}
function WinAnimation(x, y) {
this.x = x;
this.y = y;
this.startTime = new Date();
this.isPlaying = true;
}
var duration = 1000;
var proto = WinAnimation.prototype;
proto.update = function () {
if (!this.isPlaying) {
return;
}
this.t = (new Date() - this.startTime) / duration;
this.isPlaying = this.t <= 1;
};
proto.render = function (ctx) {
if (!this.isPlaying) {
return;
}
ctx.save();
ctx.translate(this.x, this.y);
this.renderBurst(ctx);
ctx.save();
ctx.scale(0.5, -0.5);
this.renderBurst(ctx);
ctx.restore();
ctx.restore();
};
proto.renderBurst = function (ctx) {
var t = this.t;
var dt = 1 - t;
var easeT = 1 - dt * dt * dt * dt * dt * dt * dt * dt;
var dy = easeT * -100;
var st = 2 - this.t * 2;
var scale = (1 - t * t * t) * 1.5;
var spin = Math.PI * 1 * t * t * t;
for (var i = 0; i < 5; i++) {
ctx.save();
ctx.rotate(((Math.PI * 2) / 5) * i);
ctx.translate(0, dy);
ctx.scale(scale, scale);
ctx.rotate(spin);
renderStar(ctx);
ctx.restore();
}
};
function renderStar(ctx) {
ctx.lineWidth = 8;
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.fillStyle = "hsla(50, 100%, 50%, 1)";
ctx.strokeStyle = "hsla(50, 100%, 50%, 1)";
ctx.beginPath();
for (var i = 0; i < 11; i++) {
var theta = (Math.PI * 2 * i) / 10 + Math.PI / 2;
var radius = i % 2 ? 20 : 10;
var dx = Math.cos(theta) * radius;
var dy = Math.sin(theta) * radius;
ctx[i ? "lineTo" : "moveTo"](dx, dy);
}
ctx.fill();
ctx.stroke();
ctx.closePath();
}
var docElem = document.documentElement;
var canvas = document.querySelector("canvas");
var ctx = canvas.getContext("2d");
var canvasSize = Math.min(window.innerWidth, window.innerHeight);
var canvasWidth = (canvas.width = window.innerWidth * 2);
var canvasHeight = (canvas.height = window.innerHeight * 2);
var maze;
var PI = Math.PI;
var TAU = PI * 2;
var dragAngle = null;
var cubDragMove = null;
var isCubHovered = false;
var isCubDragging = false;
var winAnim;
var unipointer = new Unipointer();
var gridSize = Math.min(40, canvasSize / 12);
var mazeCenter = {
x: canvasWidth / 4,
y: Math.min(gridSize * 8, canvasHeight / 4),
};
var instructElem = document.querySelector(".instruction");
instructElem.style.top = mazeCenter.y + gridSize * 5.5 + "px";
var levelList = document.querySelector(".level-list");
var levelsElem = document.querySelector(".levels");
var levels = [];
(function () {
var levelPres = levelsElem.querySelectorAll("pre");
var fragment = document.createDocumentFragment();
for (var i = 0; i < levelPres.length; i++) {
var pre = levelPres[i];
var listItem = document.createElement("li");
listItem.className = "level-list__item";
var id = pre.id;
listItem.innerHTML =
'<span class="level-list__item__number">' +
(i + 1) +
'</span> <span class="level-list__item__blurb">' +
pre.getAttribute("data-blurb") +
"</span>" +
'<span class="level-list__item__check">✔</span>';
listItem.setAttribute("data-id", id);
fragment.appendChild(listItem);
levels.push(id);
}
levelList.appendChild(fragment);
})();
var levelSelectButton = document.querySelector(".level-select-button");
var nextLevelButton = document.querySelector(".next-level-button");
levelSelectButton.addEventListener("click", function () {
levelList.classList.add("is-open");
});
nextLevelButton.style.top = mazeCenter.y + gridSize * 5.5 + "px";
levelList.addEventListener("click", function (event) {
var item = getParent(event.target, ".level-list__item");
if (!item) {
return;
}
var id = item.getAttribute("data-id");
loadLevel(id);
});
function getParent(elem, selector) {
var parent = elem;
while (parent != document.body) {
if (parent.matches(selector)) {
return parent;
}
parent = parent.parentNode;
}
}
function loadLevel(id) {
var pre = levelsElem.querySelector("#" + id);
maze = new Maze();
maze.id = id;
if (!pre) {
console.error("pre not found for " + id);
return;
}
maze.loadText(pre.textContent);
levelList.classList.remove("is-open");
nextLevelButton.classList.remove("is-open");
window.scrollTo(0, 0);
var previousItem = levelList.querySelector(".is-playing");
if (previousItem) {
previousItem.classList.remove("is-playing");
}
levelList
.querySelector('[data-id="' + id + '"]')
.classList.add("is-playing");
localStorage.setItem("currentLevel", id);
}
var initialLevel = localStorage.getItem("currentLevel") || levels[0];
loadLevel(initialLevel);
unipointer.bindStartEvent(canvas);
window.addEventListener("mousemove", onHoverMousemove);
animate();
var canvasLeft = canvas.offsetLeft;
var canvasTop = canvas.offsetTop;
var pointerBehavior;
var cubDrag = {};
var mazeRotate = {};
unipointer.pointerDown = function (event, pointer) {
event.preventDefault();
var isInsideCub = getIsInsideCub(pointer);
pointerBehavior = isInsideCub ? cubDrag : mazeRotate;
pointerBehavior.pointerDown(event, pointer);
this._bindPostStartEvents(event);
};
function getIsInsideCub(pointer) {
var position = getCanvasMazePosition(pointer);
var cubDeltaX = Math.abs(
position.x - cub[maze.orientation].x * gridSize
);
var cubDeltaY = Math.abs(
position.y - cub[maze.orientation].y * gridSize
);
var bound = gridSize * 1.5;
return cubDeltaX <= bound && cubDeltaY <= bound;
}
function getCanvasMazePosition(pointer) {
var canvasX = pointer.pageX - canvasLeft;
var canvasY = pointer.pageY - canvasTop;
return {
x: canvasX - mazeCenter.x,
y: canvasY - mazeCenter.y,
};
}
unipointer.pointerMove = function (event, pointer) {
pointerBehavior.pointerMove(event, pointer);
};
unipointer.pointerUp = function (event, pointer) {
pointerBehavior.pointerUp(event, pointer);
this._unbindPostStartEvents();
};
var dragStartPosition, dragStartPegPosition, rotatePointer;
cubDrag.pointerDown = function (event, pointer) {
var segments = getCubConnections();
if (!segments || !segments.length) {
return;
}
isCubDragging = true;
dragStartPosition = { x: pointer.pageX, y: pointer.pageY };
dragStartPegPosition = {
x: cub[maze.orientation].x * gridSize + mazeCenter.x,
y: cub[maze.orientation].y * gridSize + mazeCenter.y,
};
docElem.classList.add("is-cub-dragging");
};
cubDrag.pointerMove = function (event, pointer) {
if (!isCubDragging) {
return;
}
cubDragMove = {
x: pointer.pageX - dragStartPosition.x,
y: pointer.pageY - dragStartPosition.y,
};
};
cubDrag.pointerUp = function () {
cubDragMove = null;
docElem.classList.remove("is-cub-dragging");
isCubDragging = false;
cub.setOffset({ x: 0, y: 0 }, maze.orientation);
if (
cub.peg.x == maze.goalPosition.x &&
cub.peg.y == maze.goalPosition.y
) {
completeLevel();
console.log("win");
}
};
var dragStartAngle, dragStartMazeAngle, moveAngle;
var mazeRotate = {};
mazeRotate.pointerDown = function (event, pointer) {
dragStartAngle = moveAngle = getDragAngle(pointer);
dragStartMazeAngle = maze.flyWheel.angle;
dragAngle = dragStartMazeAngle;
rotatePointer = pointer;
};
function getDragAngle(pointer) {
var position = getCanvasMazePosition(pointer);
return normalizeAngle(Math.atan2(position.y, position.x));
}
mazeRotate.pointerMove = function (event, pointer) {
rotatePointer = pointer;
moveAngle = getDragAngle(pointer);
var deltaAngle = moveAngle - dragStartAngle;
dragAngle = normalizeAngle(dragStartMazeAngle + deltaAngle);
};
mazeRotate.pointerUp = function () {
dragAngle = null;
rotatePointer = null;
};
function animate() {
update();
render();
requestAnimationFrame(animate);
}
function update() {
dragCub();
if (dragAngle) {
maze.flyWheel.setAngle(dragAngle);
} else {
maze.attractAlignFlyWheel();
}
maze.update();
if (winAnim) {
winAnim.update();
}
}
function dragCub() {
if (!cubDragMove) {
return;
}
var segments = getCubConnections();
var dragPosition = {
x: dragStartPegPosition.x + cubDragMove.x,
y: dragStartPegPosition.y + cubDragMove.y,
};
var dragPeg = getDragPeg(segments, dragPosition);
cub.setPeg(dragPeg, maze.orientation);
var cubDragPosition = getDragPosition(segments, dragPosition);
var cubPosition = getCubPosition();
var offset = {
x: cubDragPosition.x - cubPosition.x,
y: cubDragPosition.y - cubPosition.y,
};
cub.setOffset(offset, maze.orientation);
}
function getCubPosition() {
return {
x: cub[maze.orientation].x * gridSize + mazeCenter.x,
y: cub[maze.orientation].y * gridSize + mazeCenter.y,
};
}
function getCubConnections() {
var pegX = cub[maze.orientation].x;
var pegY = cub[maze.orientation].y;
var key = maze.orientation + ":" + pegX + "," + pegY;
return maze.connections[key];
}
function getDragPosition(segments, dragPosition) {
if (segments.length == 1) {
return getSegmentDragPosition(segments[0], dragPosition);
}
var dragCandidates = segments.map(function (segment) {
var position = getSegmentDragPosition(segment, dragPosition);
return {
position: position,
distance: getDistance(dragPosition, position),
};
});
dragCandidates.sort(distanceSorter);
return dragCandidates[0].position;
}
function getSegmentDragPosition(segment, dragPosition) {
var line = segment[maze.orientation];
var isHorizontal = line.a.y == line.b.y;
var x, y;
if (isHorizontal) {
x = getSegmentDragCoord(line, "x", dragPosition);
y = line.a.y * gridSize + mazeCenter.y;
} else {
x = line.a.x * gridSize + mazeCenter.x;
y = getSegmentDragCoord(line, "y", dragPosition);
}
return { x: x, y: y };
}
function getSegmentDragCoord(line, axis, dragPosition) {
var a = line.a[axis];
var b = line.b[axis];
var min = a < b ? a : b;
var max = a > b ? a : b;
min = min * gridSize + mazeCenter[axis];
max = max * gridSize + mazeCenter[axis];
return Math.max(min, Math.min(max, dragPosition[axis]));
}
function distanceSorter(a, b) {
return a.distance - b.distance;
}
function getDragPeg(segments, dragPosition) {
var pegs = [];
segments.forEach(function (segment) {
var line = segment[maze.orientation];
addPegPoint(line.a, pegs);
addPegPoint(line.b, pegs);
});
var pegCandidates = pegs.map(function (pegKey) {
var parts = pegKey.split(",");
var peg = {
x: parseInt(parts[0], 10),
y: parseInt(parts[1], 10),
};
var pegPosition = {
x: peg.x * gridSize + mazeCenter.x,
y: peg.y * gridSize + mazeCenter.y,
};
return {
peg: peg,
distance: getDistance(dragPosition, pegPosition),
};
});
pegCandidates.sort(distanceSorter);
return pegCandidates[0].peg;
}
function getDistance(a, b) {
var dx = b.x - a.x;
var dy = b.y - a.y;
return Math.sqrt(dx * dx + dy * dy);
}
function addPegPoint(point, pegs) {
var key = point.x + "," + point.y;
if (pegs.indexOf(key) == -1) {
pegs.push(key);
}
}
function onHoverMousemove(event) {
var isInsideCub = getIsInsideCub(event);
if (isInsideCub == isCubHovered) {
return;
}
isCubHovered = isInsideCub;
var changeClass = isInsideCub ? "add" : "remove";
docElem.classList[changeClass]("is-cub-hovered");
}
function render() {
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
ctx.save();
ctx.scale(2, 2);
renderRotateHandle();
maze.render(ctx, mazeCenter, gridSize, maze.flyWheel.angle);
if (winAnim) {
winAnim.render(ctx);
}
var isHovered = isCubHovered || isCubDragging;
cub.render(ctx, mazeCenter, gridSize, maze.flyWheel.angle, isHovered);
ctx.restore();
}
function renderRotateHandle() {
if (!rotatePointer) {
return;
}
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.lineWidth = gridSize * 0.5;
var color = "#EEE";
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.beginPath();
var pieRadius = maze.gridMax * gridSize;
ctx.moveTo(mazeCenter.x, mazeCenter.y);
var pieDirection =
normalizeAngle(
normalizeAngle(moveAngle) - normalizeAngle(dragStartAngle)
) >
TAU / 2;
ctx.arc(
mazeCenter.x,
mazeCenter.y,
pieRadius,
dragStartAngle,
moveAngle,
pieDirection
);
ctx.lineTo(mazeCenter.x, mazeCenter.y);
ctx.stroke();
ctx.fill();
ctx.closePath();
}
var completedLevels = localStorage.getItem("completedLevels");
completedLevels = completedLevels ? completedLevels.split(",") : [];
completedLevels.forEach(function (id) {
var item = levelList.querySelector('[data-id="' + id + '"]');
if (item) {
item.classList.add("did-complete");
}
});
function completeLevel() {
var cubPosition = getCubPosition();
winAnim = new WinAnimation(cubPosition.x, cubPosition.y);
levelList
.querySelector('[data-id="' + maze.id + '"]')
.classList.add("did-complete");
if (completedLevels.indexOf(maze.id) == -1) {
completedLevels.push(maze.id);
localStorage.setItem("completedLevels", completedLevels.join(","));
}
if (getNextLevel()) {
setTimeout(function () {
nextLevelButton.classList.add("is-open");
}, 1000);
}
}
function getNextLevel() {
var index = levels.indexOf(maze.id);
return levels[index + 1];
}
nextLevelButton.addEventListener("click", function () {
var nextLevel = getNextLevel();
if (nextLevel) {
loadLevel(nextLevel);
}
});
function normalizeAngle(angle) {
return ((angle % TAU) + TAU) % TAU;
}
</script>
</body>
</html>