HTML preprocessors can make writing HTML more powerful or convenient. For instance, Markdown is designed to be easier to write and read for text documents and you could write a loop in Pug.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. So you don't have access to higher-up elements like the <html>
tag. If you want to add classes there that can affect the whole document, this is the place to do it.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. If you need things in the <head>
of the document, put that code here.
The resource you are linking to is using the 'http' protocol, which may not work when the browser is using https.
CSS preprocessors help make authoring CSS easier. All of them offer things like variables and mixins to provide convenient abstractions.
It's a common practice to apply CSS to a page that styles elements such that they are consistent across all browsers. We offer two of the most popular choices: normalize.css and a reset. Or, choose Neither and nothing will be applied.
To get the best cross-browser support, it is a common practice to apply vendor prefixes to CSS properties and values that require them to work. For instance -webkit-
or -moz-
.
We offer two popular choices: Autoprefixer (which processes your CSS server-side) and -prefix-free (which applies prefixes via a script, client-side).
Any URLs added here will be added as <link>
s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.
You can apply CSS to your Pen from any stylesheet on the web. Just put a URL to it here and we'll apply it, in the order you have them, before the CSS in the Pen itself.
You can also link to another Pen here (use the .css
URL Extension) and we'll pull the CSS from that Pen and include it. If it's using a matching preprocessor, use the appropriate URL Extension and we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
JavaScript preprocessors can help make authoring JavaScript easier and more convenient.
Babel includes JSX processing.
Any URL's added here will be added as <script>
s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.
You can apply a script from anywhere on the web to your Pen. Just put a URL to it here and we'll add it, in the order you have them, before the JavaScript in the Pen itself.
If the script you link to has the file extension of a preprocessor, we'll attempt to process it before applying.
You can also link to another Pen here, and we'll pull the JavaScript from that Pen and include it. If it's using a matching preprocessor, we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
Search for and use JavaScript packages from npm here. By selecting a package, an import
statement will be added to the top of the JavaScript editor for this package.
Using packages here is powered by esm.sh, which makes packages from npm not only available on a CDN, but prepares them for native JavaScript ESM usage.
All packages are different, so refer to their docs for how they work.
If you're using React / ReactDOM, make sure to turn on Babel for the JSX processing.
If active, Pens will autosave every 30 seconds after being saved once.
If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.
If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.
Visit your global Editor Settings.
<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>
* { 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; }
/**
* 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;
}
Also see: Tab Triggers