Skip to content

Instantly share code, notes, and snippets.

@carolineartz
Created December 11, 2017 15:45
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save carolineartz/3fd7f94ec37b8a4fb99b64ae419c9aea to your computer and use it in GitHub Desktop.
Save carolineartz/3fd7f94ec37b8a4fb99b64ae419c9aea to your computer and use it in GitHub Desktop.
Cub n Pup - puzzle game demo

Cub n Pup - puzzle game demo

How to play: Drag cub to star, Drag grid to rotate.

Also available at cubnpup.com

This is a proof-of-concept for a game. Basic art, no sound, no options, no polish. But the core game-play is there. It's more of a mobile game, focused on dragging — inspired by Threes. I'm looking to see if its any fun. Let me know!

I've always wanted to make a video game. This could be the one. My previous attempts never got past isolated demos because they were aiming for bigger ideas. They grew complex and unwieldy. So this game is designed to be simple. A game that I can actually make.

A Pen by David DeSandro on CodePen.

License.

<div class="top-bar">
<button class="button level-select-button">Levels</button>
</div>
<ol class="level-list"></ol>
<canvas></canvas>
<p class="instruction"></p>
<button class="button next-level-button">Next level</button>
<div class="levels">
<pre id="intro-fixed1" data-blurb="Tutorial">
blurb: Tutorial
instruction: Drag cub to star
---
*=.=.
!
. . .
!
@=.=.
</pre>
<pre id="intro-fixed2" data-blurb="Tutorial">
blurb: Tutorial
instruction: Drag grid to rotate. Cub and star moves with grid. Orange links stay in place.
---
* . .
!
. . .
!
@=.=.
</pre>
<pre id="intro-fixed3" data-blurb="★">
blurb: ★
---
@=. .
. . .
!
*=. .
</pre>
<pre id="intro-free1" data-blurb="Tutorial">
blurb: Tutorial
instruction: Blue links move with grid. Rotate grid to connect blue and orange links in different ways.
---
@-. .
! |
. . .
|
*-.-.
</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: Green links pivot with grid, but point in the same direction
---
. .-* .
|
. . . .
. .>. .
. @ . .
</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: Red links are fixed in place, but rotate with grid
---
. . . .
@ .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>
/**
* EvEmitter v1.0.2
* Lil' event emitter
* MIT License
*/
/* jshint unused: true, undef: true, strict: true */
( function( global, factory ) {
// universal module definition
/* jshint strict: false */ /* globals define, module */
if ( typeof define == 'function' && define.amd ) {
// AMD - RequireJS
define( factory );
} else if ( typeof module == 'object' && module.exports ) {
// CommonJS - Browserify, Webpack
module.exports = factory();
} else {
// Browser globals
global.EvEmitter = factory();
}
}( this, function() {
"use strict";
function EvEmitter() {}
var proto = EvEmitter.prototype;
proto.on = function( eventName, listener ) {
if ( !eventName || !listener ) {
return;
}
// set events hash
var events = this._events = this._events || {};
// set listeners array
var listeners = events[ eventName ] = events[ eventName ] || [];
// only add once
if ( listeners.indexOf( listener ) == -1 ) {
listeners.push( listener );
}
return this;
};
proto.once = function( eventName, listener ) {
if ( !eventName || !listener ) {
return;
}
// add event
this.on( eventName, listener );
// set once flag
// set onceEvents hash
var onceEvents = this._onceEvents = this._onceEvents || {};
// set onceListeners object
var onceListeners = onceEvents[ eventName ] = onceEvents[ eventName ] || {};
// set flag
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 || [];
// once stuff
var onceListeners = this._onceEvents && this._onceEvents[ eventName ];
while ( listener ) {
var isOnce = onceListeners && onceListeners[ listener ];
if ( isOnce ) {
// remove listener
// remove before trigger to prevent recursion
this.off( eventName, listener );
// unset once flag
delete onceListeners[ listener ];
}
// trigger listener
listener.apply( this, args );
// get next listener
i += isOnce ? 0 : 1;
listener = listeners[i];
}
return this;
};
return EvEmitter;
}));
/*!
* Unipointer v2.1.0
* base class for doing one thing with pointer event
* MIT license
*/
/*jshint browser: true, undef: true, unused: true, strict: true */
( function( window, factory ) {
// universal module definition
/* jshint strict: false */ /*global define, module, require */
if ( typeof define == 'function' && define.amd ) {
// AMD
define( [
'ev-emitter/ev-emitter'
], function( EvEmitter ) {
return factory( window, EvEmitter );
});
} else if ( typeof module == 'object' && module.exports ) {
// CommonJS
module.exports = factory(
window,
require('ev-emitter')
);
} else {
// browser global
window.Unipointer = factory(
window,
window.EvEmitter
);
}
}( window, function factory( window, EvEmitter ) {
'use strict';
function noop() {}
function Unipointer() {}
// inherit EvEmitter
var proto = Unipointer.prototype = Object.create( EvEmitter.prototype );
proto.bindStartEvent = function( elem ) {
this._bindStartEvent( elem, true );
};
proto.unbindStartEvent = function( elem ) {
this._bindStartEvent( elem, false );
};
/**
* works as unbinder, as you can ._bindStart( false ) to unbind
* @param {Boolean} isBind - will unbind if falsey
*/
proto._bindStartEvent = function( elem, isBind ) {
// munge isBind, default to true
isBind = isBind === undefined ? true : !!isBind;
var bindMethod = isBind ? 'addEventListener' : 'removeEventListener';
if ( window.navigator.pointerEnabled ) {
// W3C Pointer Events, IE11. See https://coderwall.com/p/mfreca
elem[ bindMethod ]( 'pointerdown', this );
} else if ( window.navigator.msPointerEnabled ) {
// IE10 Pointer Events
elem[ bindMethod ]( 'MSPointerDown', this );
} else {
// listen for both, for devices like Chrome Pixel
elem[ bindMethod ]( 'mousedown', this );
elem[ bindMethod ]( 'touchstart', this );
}
};
// trigger handler methods for events
proto.handleEvent = function( event ) {
var method = 'on' + event.type;
if ( this[ method ] ) {
this[ method ]( event );
}
};
// returns the touch that we're keeping track of
proto.getTouch = function( touches ) {
for ( var i=0; i < touches.length; i++ ) {
var touch = touches[i];
if ( touch.identifier == this.pointerIdentifier ) {
return touch;
}
}
};
// ----- start event ----- //
proto.onmousedown = function( event ) {
// dismiss clicks from right or middle buttons
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 );
};
/**
* pointer start
* @param {Event} event
* @param {Event or Touch} pointer
*/
proto._pointerDown = function( event, pointer ) {
// dismiss other pointers
if ( this.isPointerDown ) {
return;
}
this.isPointerDown = true;
// save pointer identifier to match up touch events
this.pointerIdentifier = pointer.pointerId !== undefined ?
// pointerId for pointer events, touch.indentifier for touch events
pointer.pointerId : pointer.identifier;
this.pointerDown( event, pointer );
};
proto.pointerDown = function( event, pointer ) {
this._bindPostStartEvents( event );
this.emitEvent( 'pointerDown', [ event, pointer ] );
};
// hash of events to be bound after start event
var postStartEvents = {
mousedown: [ 'mousemove', 'mouseup' ],
touchstart: [ 'touchmove', 'touchend', 'touchcancel' ],
pointerdown: [ 'pointermove', 'pointerup', 'pointercancel' ],
MSPointerDown: [ 'MSPointerMove', 'MSPointerUp', 'MSPointerCancel' ]
};
proto._bindPostStartEvents = function( event ) {
if ( !event ) {
return;
}
// get proper events to match start event
var events = postStartEvents[ event.type ];
// bind events to node
events.forEach( function( eventName ) {
window.addEventListener( eventName, this );
}, this );
// save these arguments
this._boundPointerEvents = events;
};
proto._unbindPostStartEvents = function() {
// check for _boundEvents, in case dragEnd triggered twice (old IE8 bug)
if ( !this._boundPointerEvents ) {
return;
}
this._boundPointerEvents.forEach( function( eventName ) {
window.removeEventListener( eventName, this );
}, this );
delete this._boundPointerEvents;
};
// ----- move event ----- //
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 );
}
};
/**
* pointer move
* @param {Event} event
* @param {Event or Touch} pointer
* @private
*/
proto._pointerMove = function( event, pointer ) {
this.pointerMove( event, pointer );
};
// public
proto.pointerMove = function( event, pointer ) {
this.emitEvent( 'pointerMove', [ event, pointer ] );
};
// ----- end event ----- //
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 );
}
};
/**
* pointer up
* @param {Event} event
* @param {Event or Touch} pointer
* @private
*/
proto._pointerUp = function( event, pointer ) {
this._pointerDone();
this.pointerUp( event, pointer );
};
// public
proto.pointerUp = function( event, pointer ) {
this.emitEvent( 'pointerUp', [ event, pointer ] );
};
// ----- pointer done ----- //
// triggered on pointer up & pointer cancel
proto._pointerDone = function() {
// reset properties
this.isPointerDown = false;
delete this.pointerIdentifier;
// remove events
this._unbindPostStartEvents();
this.pointerDone();
};
proto.pointerDone = noop;
// ----- pointer cancel ----- //
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 );
}
};
/**
* pointer cancel
* @param {Event} event
* @param {Event or Touch} pointer
* @private
*/
proto._pointerCancel = function( event, pointer ) {
this._pointerDone();
this.pointerCancel( event, pointer );
};
// public
proto.pointerCancel = function( event, pointer ) {
this.emitEvent( 'pointerCancel', [ event, pointer ] );
};
// ----- ----- //
// utility function for getting x/y coords from event
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;
// orientations
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;
// orientations
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 };
// orientations
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)'
// line
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();
// circle
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;
// orientations
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;
// axle
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);
// line
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();
};
// rotational physics model
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 ) {
// flip y because its rendering
return { x: offset.y, y: -offset.x };
},
six: function( offset ) {
return { x: -offset.x, y: -offset.y };
},
nine: function( offset ) {
// flip y because its rendering
return { x: -offset.y, y: offset.x };
},
};
cub.setOffset = function( offset, orientation ) {
this.offset = offsetOrienter[ orientation ]( offset );
};
// ----- render ----- //
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();
};
/* globals FlyWheel, FreeSegment, FixedSegment, PivotSegment, RotateSegment, cub */
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 ) {
// separate --- sections, YAML front matter first, maze source second;
var sections = text.split('---\n');
// YAML front matter
var frontMatter = {};
if ( sections.length > 1 ) {
frontMatter = getFrontMatter( sections[0] );
}
// set instruction
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; // boolean true
} else if ( value === 'false' ) {
value = false; // boolean false
} else if ( value.match(/$\d+(\.\d+)?^/) ) {
value = parseFloat( value, 10 ); // number
} else if ( value.match(/$\d+\.\d+^/) ) {
value = parseFloat( value ); // float
}
frontMatter[ key ] = value;
});
return frontMatter;
}
// -------------------------- parsers -------------------------- //
// horizontal free segment
proto['parse-'] = proto.addFreeHorizSegment = function( pegX, pegY ) {
var segment = getHorizSegment( pegX, pegY, FreeSegment );
this.connectSegment( segment );
this.freeSegments.push( segment );
};
// vertical free segment
proto['parse|'] = proto.addFreeVertSegment = function( pegX, pegY ) {
var segment = getVertSegment( pegX, pegY, FreeSegment );
this.connectSegment( segment );
this.freeSegments.push( segment );
};
// horizontal fixed segment
proto['parse='] = proto.addFixedHorizSegment = function( pegX, pegY ) {
var segment = getHorizSegment( pegX, pegY, FixedSegment );
this.connectSegment( segment );
this.fixedSegments.push( segment );
};
// vertical fixed 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 );
}
// ----- pivot ----- //
// pivot up segment
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 );
};
// pivot down 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 );
};
// pivot left 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 );
};
// pivot right 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 );
};
// ----- rotate ----- //
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 );
};
// ----- combos ----- //
// free & fixed horizontal
proto['parse#'] = function( pegX, pegY ) {
this.addFreeHorizSegment( pegX, pegY );
this.addFixedHorizSegment( pegX, pegY );
};
// free & fixed vertical
proto.parse$ = function( pegX, pegY ) {
this.addFreeVertSegment( pegX, pegY );
this.addFixedVertSegment( pegX, pegY );
};
// pivot up + fixed vertical
proto.parseI = function( pegX, pegY ) {
this.addPivotUpSegment( pegX, pegY );
this.addFixedVertSegment( pegX, pegY );
};
// pivot left + fixed horizontal
proto.parseJ = function( pegX, pegY ) {
this.addPivotLeftSegment( pegX, pegY );
this.addFixedHorizSegment( pegX, pegY );
};
// pivot down + fixed vertical
proto.parseK = function( pegX, pegY ) {
this.addPivotDownSegment( pegX, pegY );
this.addFixedVertSegment( pegX, pegY );
};
// pivot right + fixed horizontal
proto.parseL = function( pegX, pegY ) {
this.addPivotRightSegment( pegX, pegY );
this.addFixedHorizSegment( pegX, pegY );
};
// pivot up + free vertical
proto.parseW = function( pegX, pegY ) {
this.addPivotUpSegment( pegX, pegY );
this.addFreeVertSegment( pegX, pegY );
};
// pivot left + free horizontal
proto.parseA = function( pegX, pegY ) {
this.addPivotLeftSegment( pegX, pegY );
this.addFreeHorizSegment( pegX, pegY );
};
// pivot down + free vertical
proto.parseS = function( pegX, pegY ) {
this.addPivotDownSegment( pegX, pegY );
this.addFreeVertSegment( pegX, pegY );
};
// pivot right + free horizontal
proto.parseD = function( pegX, pegY ) {
this.addPivotRightSegment( pegX, pegY );
this.addFreeHorizSegment( pegX, pegY );
};
// start position
proto['parse@'] = function( pegX, pegY ) {
this.startPosition = { x: pegX, y: pegY };
cub.setPeg( this.startPosition, 'noon' );
};
// goal position
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 ];
// check that pegs are not out of maze
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 ) {
// flatten the key
var key = orientation + ':' + peg.x + ',' + peg.y;
var connection = this.connections[ key ];
// create connections array if not already there
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() {
// attract towards
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 );
// fixed segments
this.fixedSegments.forEach( function( segment ) {
segment.render( ctx, center, gridSize );
});
// rotate segments
this.rotateSegments.forEach( function( segment ) {
segment.render( ctx, center, gridSize, angle );
});
// rotation
ctx.rotate( angle );
ctx.lineWidth = gridSize * 0.2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
// axle
ctx.lineWidth = gridSize * 0.2;
ctx.strokeStyle = 'hsla(0, 0%, 50%, 0.2)';
// strokeCircle( ctx, 0, 0, gridSize/2 );
ctx.save();
ctx.rotate( Math.PI/4 );
ctx.strokeRect( -gridSize/5, -gridSize/5, gridSize*2/5, gridSize*2/5 );
ctx.restore();
// start position
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 );
// pegs
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 );
}
}
// free segments
this.freeSegments.forEach( function( segment ) {
segment.render( ctx, center, gridSize );
});
// pivot segments
this.pivotSegments.forEach( function( segment ) {
segment.render( ctx, center, gridSize, angle );
});
// goal position
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;
}
// length of animation in milliseconds
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 );
// big burst
this.renderBurst( ctx );
// small burst
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;
// scale math
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();
}
/* globals cub, WinAnimation, Unipointer, Maze */
var docElem = document.documentElement;
var canvas = document.querySelector('canvas');
var ctx = canvas.getContext('2d');
// size canvas;
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();
// ----- config ----- //
var gridSize = Math.min( 40, canvasSize/12 );
var mazeCenter = {
x: canvasWidth/4,
y: Math.min( gridSize * 8, canvasHeight/4 )
};
// ----- instruction ----- //
var instructElem = document.querySelector('.instruction');
instructElem.style.top = ( mazeCenter.y + gridSize * 5.5 ) + 'px';
// ----- build level select, levels array ----- //
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 );
})();
// ----- levels button ----- //
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';
// ----- level list ----- //
levelList.addEventListener( 'click', function( event ) {
var item = getParent( event.target, '.level-list__item' );
if ( !item ) {
return;
}
// load level from id
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;
}
}
// ----- load level ----- //
function loadLevel( id ) {
var pre = levelsElem.querySelector( '#' + id );
maze = new Maze();
maze.id = id;
if ( !pre ) {
console.error( 'pre not found for ' + id );
return;
}
// load maze level from pre text
maze.loadText( pre.textContent );
// close ui
levelList.classList.remove('is-open');
nextLevelButton.classList.remove('is-open');
window.scrollTo( 0, 0 );
// highlight list
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 );
}
// ----- init ----- //
var initialLevel = localStorage.getItem('currentLevel') || levels[0];
loadLevel( initialLevel );
unipointer.bindStartEvent( canvas );
window.addEventListener( 'mousemove', onHoverMousemove );
animate();
// -------------------------- drag rotation -------------------------- //
var canvasLeft = canvas.offsetLeft;
var canvasTop = canvas.offsetTop;
var pointerBehavior;
// ----- 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 ----- //
unipointer.pointerMove = function( event, pointer ) {
pointerBehavior.pointerMove( event, pointer );
};
unipointer.pointerUp = function( event, pointer ) {
pointerBehavior.pointerUp( event, pointer );
this._unbindPostStartEvents();
};
// ----- cubDrag ----- //
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;
// set at peg
cub.setOffset( { x: 0, y: 0 }, maze.orientation );
// check level complete
if ( cub.peg.x == maze.goalPosition.x && cub.peg.y == maze.goalPosition.y ) {
completeLevel();
console.log('win');
}
};
// ----- rotate ----- //
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;
};
// ----- animate ----- //
function animate() {
update();
render();
requestAnimationFrame( animate );
}
// ----- update ----- //
function update() {
// drag cub
dragCub();
// rotate grid
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,
};
// set peg position
var dragPeg = getDragPeg( segments, dragPosition );
cub.setPeg( dragPeg, maze.orientation );
// set drag offset
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 );
}
// get closest segments positions
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 ) {
// revert string back to object with integers
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 ) {
// use strings to prevent dupes
var key = point.x + ',' + point.y;
if ( pegs.indexOf( key ) == -1 ) {
pegs.push( key );
}
}
// ----- hover ----- //
function onHoverMousemove( event ) {
var isInsideCub = getIsInsideCub( event );
if ( isInsideCub == isCubHovered ) {
return;
}
// change
isCubHovered = isInsideCub;
var changeClass = isInsideCub ? 'add' : 'remove';
docElem.classList[ changeClass ]('is-cub-hovered');
}
// ----- render ----- //
function render() {
ctx.clearRect( 0, 0, canvasWidth, canvasHeight );
ctx.save();
ctx.scale( 2, 2 );
renderRotateHandle();
// maze
maze.render( ctx, mazeCenter, gridSize, maze.flyWheel.angle );
// win animation
if ( winAnim ) {
winAnim.render( ctx );
}
// cub
var isHovered = isCubHovered || isCubDragging;
cub.render( ctx, mazeCenter, gridSize, maze.flyWheel.angle, isHovered );
ctx.restore();
}
function renderRotateHandle() {
// rotate handle
if ( !rotatePointer ) {
return;
}
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.lineWidth = gridSize * 0.5;
var color = '#EEE';
ctx.strokeStyle = color;
ctx.fillStyle = color;
// pie slice
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();
}
// -------------------------- completeLevel -------------------------- //
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 ];
}
// -------------------------- next level -------------------------- //
nextLevelButton.addEventListener( 'click', function() {
var nextLevel = getNextLevel();
if ( nextLevel ) {
loadLevel( nextLevel );
}
});
// -------------------------- utils -------------------------- //
function normalizeAngle( angle ) {
return ( ( angle % TAU ) + TAU ) % TAU;
}
* { 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; /* above canvas */
}
.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 ---- */
.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; /* above canvas, level select button */
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;
}
/* ---- level pres ---- */
.levels { display: none; }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment