Want to join? Log In or Join Now in seconds. English (United States)

CREATE A NEW ACCOUNT

{{registerModel.Error}}

Log In

/content/images/forum.png

Feedbacks

Get help from the knowledgeable Reddah Community and official Reddah Support!
/content/images/ambassador.png

Discuss on AD

A place for discussing the ads that reddah is currently running.
/content/images/contact_us.png

Contact Us

Get in touch with an Reddah Support technician. We are ready and willing to help you!
120

/*empty*/

.title{ color:black; } .item{ color:blue; }
submitted 5 years ago ago by 78cab8d8152049388b4b58a5621c5935
picture

Nicole's Favorite Poem Collection

  • Oscar Wilde
  • Maya Angelou
  • Robert Browning
  • Tagora
  • Carl Sandburg
121

/*empty*/

.title{ color:black; margin:10px; } .item{ color:blue; }
submitted 5 years ago ago by 78cab8d8152049388b4b58a5621c5935
picture

Nicole's Favorite Poem Collection

  • Oscar Wilde
  • Maya Angelou
  • Robert Browning
  • Tagora
  • Carl Sandburg
122

/*empty*/

.title{ color:black; margin:10px; } .item{ color:blue; }
submitted 5 years ago ago by 78cab8d8152049388b4b58a5621c5935
picture

Nicole's Favorite Poem Collection

  • Oscar Wilde
  • Maya Angelou
  • Robert Browning
  • Tagora
  • Carl Sandburg
123

/*empty*/ window["reddahApi"].loadCompleted()

.title{ color:green; margin:10px; } .item{ color:blue; }
submitted 5 years ago ago by 78cab8d8152049388b4b58a5621c5935
picture
Nicole's Favorite Poems
  • Oscar Wilde
  • Maya Angelou
  • Robert Browning
  • Tagora
  • Carl Sandburg
124

/** * 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.connect = function(maze) { this.connectOrientation(maze, 'noon'); this.connectOrientation(maze, 'three'); this.connectOrientation(maze, 'six'); this.connectOrientation(maze, 'nine'); }; proto.connectOrientation = function(maze, orientation) { var line = this[orientation]; maze.connect(this, orientation, line.a); maze.connect(this, orientation, line.b); }; 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.connect = function(maze) { maze.connect(this, 'noon', this.a); maze.connect(this, 'noon', this.b); maze.connect(this, 'three', this.a); maze.connect(this, 'three', this.b); maze.connect(this, 'six', this.a); maze.connect(this, 'six', this.b); maze.connect(this, 'nine', this.a); maze.connect(this, 'nine', this.b); }; 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.connect = function(maze) { this.connectOrientation(maze, 'noon'); this.connectOrientation(maze, 'three'); this.connectOrientation(maze, 'six'); this.connectOrientation(maze, 'nine'); }; proto.connectOrientation = function(maze, orientation) { var line = this[orientation]; if (maze.getIsPegOut(line.a) || maze.getIsPegOut(line.b)) { return; } maze.connect(this, orientation, line.a); maze.connect(this, orientation, line.b); }; 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(); }; // 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(); } // function eye( direction ) { // ctx.beginPath(); // ctx.arc( gridSize * 0.25 * direction, gridSize * 0.05, gridSize * 0.1, Math.PI, 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); // // eyes // ctx.fillStyle = 'hsla(0, 0%, 0%, 0.8)'; // eye( 1 ); // eye( -1 ); // // nose // ctx.beginPath(); // ctx.arc( 0, gridSize * 0.15, gridSize * 0.1, 0, Math.PI ); // ctx.fill(); // ctx.closePath(); ctx.restore(); }; /* globals FlyWheel, FreeSegment, FixedSegment, PivotSegment, cub */ function Maze() { this.freeSegments = []; this.fixedSegments = []; this.pivotSegments = []; 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) { var 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); segment.connect(this); this.freeSegments.push(segment); }; // vertical free segment proto['parse|'] = proto.addFreeVertSegment = function(pegX, pegY) { var segment = getVertSegment(pegX, pegY, FreeSegment); segment.connect(this); this.freeSegments.push(segment); }; // horizontal fixed segment proto['parse='] = proto.addFixedHorizSegment = function(pegX, pegY) { var segment = getHorizSegment(pegX, pegY, FixedSegment); segment.connect(this); this.fixedSegments.push(segment); }; // vertical fixed segment proto['parse!'] = proto.addFixedVertSegment = function(pegX, pegY) { var segment = getVertSegment(pegX, pegY, FixedSegment); segment.connect(this); 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 up segment proto['parse^'] = function(pegX, pegY) { var a = { x: pegX, y: pegY + 1 }; var b = { x: pegX, y: pegY - 1 }; var segment = new PivotSegment(a, b); segment.connect(this); 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); segment.connect(this); 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); segment.connect(this); 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); segment.connect(this); this.pivotSegments.push(segment); }; // 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; }; proto.connect = function(segment, orientation, position) { // flatten the key var key = orientation + ':' + position.x + ',' + position.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.getIsPegOut = function(peg) { return Math.abs(peg.x) > this.gridMax || Math.abs(peg.y) > this.gridMax; }; // -------------------------- -------------------------- // 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(item) { item.render(ctx, center, gridSize); }); // 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; // 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) { levelList.querySelector('[data-id="' + id + '"]').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; }
submitted 5 years ago ago by 346e369614c2418496fad67c1453fada
picture

    blurb: Tutorial
    instruction: Drag cub to star
    ---
    *=.=.
        !
    . . .
        !
    @=.=.
    
    blurb: Tutorial
    instruction: Drag grid to rotate. Cub and star moves with grid. Orange links stay in place.
    ---
    * . .
        !
    . . .
        !
    @=.=.
    
    blurb: ★
    ---
    @=. .
    
    . . .
        !
    *=. .
    
    blurb: Tutorial
    instruction: Blue links move with grid. Rotate grid to connect blue and orange links in different ways.
    ---
    @-. .
    !   | 
    . . .
        |
    *-.-.
    
    blurb: ★
    ---
    . . *
    | | | 
    . . .
    | | | 
    @ .=.
    
    blurb: ★
    ---
    *=.-.
     
    . . .
        | 
    @-. .
    
    blurb: ★
    ---
    . .=. .
      | !  
    . . .-*
      |    
    . . . .
           
    . @-. .
    
    blurb: ★
    ---
    . . . .
           
    * . . @
      | ! |
    . . . .
      !    
    . . . .
    
    blurb: ★
    ---
    . @ . .
    ! |    
    . . . .
          |
    .=.=.-.
    |       
    . * . .
    
    blurb: ★
    ---
    . . . .
           
    * . . .
        !  
    . . .-.
    !      
    .=.=. @
    
    blurb: ★
    ---
    .-.-.-.
    |       
    @ .-.-.
           
    * .=. .
    !   |   
    .-.-. .
    
    blurb: ★
    ---
    . * . .
    
    .-.=. .
      |
    . . . .
    !   |
    .=. @ .
    
    blurb: ★★
    ---
    . . *-.
           
    .-.=. .
          |
    .=. . .
      | |  
    @-.-.=.
    
    blurb: ★★
    ---
    .-@ .=.
           
    . . . .
        |  
    .-. .-*
      |    
    . .=.-.
    
    blurb: ★★
    ---
    . . .=.
      !    
    @-. .-.
         
    . .=. .
           
    . . * .
    
    blurb: ★★
    ---
    . @=. .
      |   
    . .-.-.
           
    .-.-.-.
    !     ! 
    . * . .
    
    . . . . .
      | !   
    . . .-. .
      |     
    . . . . *
      |     
    . . .=. .
      |     
    . @ . . .
    
    @-.-. .-.
        |    
    . . . . .
             
    . . .=. .
             
    . . . .=.
        |    
    . .=.-* .
    
    . . . . .
            
    . .=.-. @
    |       !
    . . . .-.
            
    .=. . .=.
    !       
    * . . . .
    
    . . . .-.
          ! 
    . .-. . .
      !     |
    .=. . . .
    |        
    . . . . *
    |        
    .-@=. .=.
    
    . . . . .
    
    . . .-. *
        !
    . . .-. .
    
    .=. . . .
        |
    . @-. . .
    
    . . .-.-.
    !   !    
    . .=.-. .
    |   
    . .-. .-@
    !
    * .=. . .
          |
    .=. .-.=.
    
    .=* . @=.
    |
    . .=. . .
    |   | |
    .=. . .-.
            |
    . . . .=.
    !    
    . .-.-. .
    
    . * . .-.
      |     
    . . .=.-.
    !       | 
    . . . . .
            
    . .-. .=.
            |
    . . .=.-@
    
    .-.-. . .
        |    
    . . . .-@
      !      
    * . .-. .
    |   !    
    .-. . .=.
        |   !
    . . .=. .
    
    . . . . .
             
    . . . .-@
      !      
    * . .=. .
    |   !    
    .-. . . .
             
    . . . . .
    
    . . . .=.
      |     
    . . . .=.
    |        
    . . .-. .
    ! |     
    . .=. . .
    |   !   !
    .-@ . * .
    
    . . .=.=.
            
    . . . . .
            
    . . . . @
            
    . . . . .
            
    * . .=.=.
    
    . . * . . .
      ! | |  
    . .-. .-. .
              |
    . . . . .-.
          | ! |
    . . .=. . .
        |
    @-.-. .-. .
              |
    . .=. . .-.
    
    @ .=. . .=.
      | | !
    . . . .=. .
      |     |
    . . . .-. .
    |   !
    . . . . . *
    |     |
    .=. .-. . .
      |   | |
    .-. . . .=.
    
    .=. .=.-.-*
      |        
    .-. . . . .
            | !
    . . .-.-. .
    !          
    .-. .=.=. .
               
    @ .=. . . .
      |     !
    . .-. .-. .
    
    instruction: Green links pivot with grid, but point in the same direction
    ---
    . .-* .
      |    
    . . . .
           
    . .>. .
           
    . @ . .
    
    . . .-.-@
            
    . .<. . .
            
    .>. . . .
    | !      
    .-.-. . *
      !     
    . . . . .
    
    . . . . .
          ^ 
    .<. . . *
            
    . . . . .
            
    @ . . .>.
      v     
    . . . . .
    
    . .-. . .
          ^ 
    . .<.=.=.
            
    .>. . .-@
            
    * . . .=.
            
    . . . . .
    
    .=. . .-*
        v   
    . . . . .
             
    . . .-.J.
             
    @-. . . .
        v   
    .<. . . .
    
    .-.-. @>.
    !     ^ 
    . . . . .
      |      
    . . . . .
      |     
    . . . .=*
        ^    
    . . .-. .>
    
    .-. . . *
            
    . .>. . .
    |       v
    .-. . . .
      ^      
    . . .-. .
          v 
    @=.=. . .
    
    . . .>. .
      ! |    
    @=. .-. .
            
    . . . .=.>
             
    . . . . .
             
    . *>.<. .
    
    * . @ . .
    v   |   
    . . . . .
          !  
    . . . . .
    ^     ! !
    . .-. . .
      !     
    . . . . .
        v
    
    . . . . . .
    | v         
    @ . . . . *
      | |      
    . . . . . .
    | !   ^ | K
    . . . .-.=.
    |          
    . .-. . . .
    v          
    .>. . . . .
    
    . @-. .>.-.
              
    . . . . . .
              |
    * .>. .=. .
        !      
    . . . . . .>
          |   ^
    . . . .=. .
               
    . .=. . .=.>
    
    . .-.-. .=.
          v   
    . . . . . .
      |     ! v
    .>. . . . *
        ^      
    . . . . . .
    |          
    . .-.<. . .
    ! |       |
    . . . .>.-@
    
    125

    /** * 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.connect = function(maze) { this.connectOrientation(maze, 'noon'); this.connectOrientation(maze, 'three'); this.connectOrientation(maze, 'six'); this.connectOrientation(maze, 'nine'); }; proto.connectOrientation = function(maze, orientation) { var line = this[orientation]; maze.connect(this, orientation, line.a); maze.connect(this, orientation, line.b); }; 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.connect = function(maze) { maze.connect(this, 'noon', this.a); maze.connect(this, 'noon', this.b); maze.connect(this, 'three', this.a); maze.connect(this, 'three', this.b); maze.connect(this, 'six', this.a); maze.connect(this, 'six', this.b); maze.connect(this, 'nine', this.a); maze.connect(this, 'nine', this.b); }; 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.connect = function(maze) { this.connectOrientation(maze, 'noon'); this.connectOrientation(maze, 'three'); this.connectOrientation(maze, 'six'); this.connectOrientation(maze, 'nine'); }; proto.connectOrientation = function(maze, orientation) { var line = this[orientation]; if (maze.getIsPegOut(line.a) || maze.getIsPegOut(line.b)) { return; } maze.connect(this, orientation, line.a); maze.connect(this, orientation, line.b); }; 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(); }; // 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(); } // function eye( direction ) { // ctx.beginPath(); // ctx.arc( gridSize * 0.25 * direction, gridSize * 0.05, gridSize * 0.1, Math.PI, 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); // // eyes // ctx.fillStyle = 'hsla(0, 0%, 0%, 0.8)'; // eye( 1 ); // eye( -1 ); // // nose // ctx.beginPath(); // ctx.arc( 0, gridSize * 0.15, gridSize * 0.1, 0, Math.PI ); // ctx.fill(); // ctx.closePath(); ctx.restore(); }; /* globals FlyWheel, FreeSegment, FixedSegment, PivotSegment, cub */ function Maze() { this.freeSegments = []; this.fixedSegments = []; this.pivotSegments = []; 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) { var 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); segment.connect(this); this.freeSegments.push(segment); }; // vertical free segment proto['parse|'] = proto.addFreeVertSegment = function(pegX, pegY) { var segment = getVertSegment(pegX, pegY, FreeSegment); segment.connect(this); this.freeSegments.push(segment); }; // horizontal fixed segment proto['parse='] = proto.addFixedHorizSegment = function(pegX, pegY) { var segment = getHorizSegment(pegX, pegY, FixedSegment); segment.connect(this); this.fixedSegments.push(segment); }; // vertical fixed segment proto['parse!'] = proto.addFixedVertSegment = function(pegX, pegY) { var segment = getVertSegment(pegX, pegY, FixedSegment); segment.connect(this); 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 up segment proto['parse^'] = function(pegX, pegY) { var a = { x: pegX, y: pegY + 1 }; var b = { x: pegX, y: pegY - 1 }; var segment = new PivotSegment(a, b); segment.connect(this); 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); segment.connect(this); 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); segment.connect(this); 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); segment.connect(this); this.pivotSegments.push(segment); }; // 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; }; proto.connect = function(segment, orientation, position) { // flatten the key var key = orientation + ':' + position.x + ',' + position.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.getIsPegOut = function(peg) { return Math.abs(peg.x) > this.gridMax || Math.abs(peg.y) > this.gridMax; }; // -------------------------- -------------------------- // 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(item) { item.render(ctx, center, gridSize); }); // 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; // 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(); window["reddahApi"].loadCompleted(); // -------------------------- 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) { levelList.querySelector('[data-id="' + id + '"]').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; }
    submitted 5 years ago ago by 346e369614c2418496fad67c1453fada
    picture

      blurb: Tutorial
      instruction: Drag cub to star
      ---
      *=.=.
          !
      . . .
          !
      @=.=.
      
      blurb: Tutorial
      instruction: Drag grid to rotate. Cub and star moves with grid. Orange links stay in place.
      ---
      * . .
          !
      . . .
          !
      @=.=.
      
      blurb: ★
      ---
      @=. .
      
      . . .
          !
      *=. .
      
      blurb: Tutorial
      instruction: Blue links move with grid. Rotate grid to connect blue and orange links in different ways.
      ---
      @-. .
      !   | 
      . . .
          |
      *-.-.
      
      blurb: ★
      ---
      . . *
      | | | 
      . . .
      | | | 
      @ .=.
      
      blurb: ★
      ---
      *=.-.
       
      . . .
          | 
      @-. .
      
      blurb: ★
      ---
      . .=. .
        | !  
      . . .-*
        |    
      . . . .
             
      . @-. .
      
      blurb: ★
      ---
      . . . .
             
      * . . @
        | ! |
      . . . .
        !    
      . . . .
      
      blurb: ★
      ---
      . @ . .
      ! |    
      . . . .
            |
      .=.=.-.
      |       
      . * . .
      
      blurb: ★
      ---
      . . . .
             
      * . . .
          !  
      . . .-.
      !      
      .=.=. @
      
      blurb: ★
      ---
      .-.-.-.
      |       
      @ .-.-.
             
      * .=. .
      !   |   
      .-.-. .
      
      blurb: ★
      ---
      . * . .
      
      .-.=. .
        |
      . . . .
      !   |
      .=. @ .
      
      blurb: ★★
      ---
      . . *-.
             
      .-.=. .
            |
      .=. . .
        | |  
      @-.-.=.
      
      blurb: ★★
      ---
      .-@ .=.
             
      . . . .
          |  
      .-. .-*
        |    
      . .=.-.
      
      blurb: ★★
      ---
      . . .=.
        !    
      @-. .-.
           
      . .=. .
             
      . . * .
      
      blurb: ★★
      ---
      . @=. .
        |   
      . .-.-.
             
      .-.-.-.
      !     ! 
      . * . .
      
      . . . . .
        | !   
      . . .-. .
        |     
      . . . . *
        |     
      . . .=. .
        |     
      . @ . . .
      
      @-.-. .-.
          |    
      . . . . .
               
      . . .=. .
               
      . . . .=.
          |    
      . .=.-* .
      
      . . . . .
              
      . .=.-. @
      |       !
      . . . .-.
              
      .=. . .=.
      !       
      * . . . .
      
      . . . .-.
            ! 
      . .-. . .
        !     |
      .=. . . .
      |        
      . . . . *
      |        
      .-@=. .=.
      
      . . . . .
      
      . . .-. *
          !
      . . .-. .
      
      .=. . . .
          |
      . @-. . .
      
      . . .-.-.
      !   !    
      . .=.-. .
      |   
      . .-. .-@
      !
      * .=. . .
            |
      .=. .-.=.
      
      .=* . @=.
      |
      . .=. . .
      |   | |
      .=. . .-.
              |
      . . . .=.
      !    
      . .-.-. .
      
      . * . .-.
        |     
      . . .=.-.
      !       | 
      . . . . .
              
      . .-. .=.
              |
      . . .=.-@
      
      .-.-. . .
          |    
      . . . .-@
        !      
      * . .-. .
      |   !    
      .-. . .=.
          |   !
      . . .=. .
      
      . . . . .
               
      . . . .-@
        !      
      * . .=. .
      |   !    
      .-. . . .
               
      . . . . .
      
      . . . .=.
        |     
      . . . .=.
      |        
      . . .-. .
      ! |     
      . .=. . .
      |   !   !
      .-@ . * .
      
      . . .=.=.
              
      . . . . .
              
      . . . . @
              
      . . . . .
              
      * . .=.=.
      
      . . * . . .
        ! | |  
      . .-. .-. .
                |
      . . . . .-.
            | ! |
      . . .=. . .
          |
      @-.-. .-. .
                |
      . .=. . .-.
      
      @ .=. . .=.
        | | !
      . . . .=. .
        |     |
      . . . .-. .
      |   !
      . . . . . *
      |     |
      .=. .-. . .
        |   | |
      .-. . . .=.
      
      .=. .=.-.-*
        |        
      .-. . . . .
              | !
      . . .-.-. .
      !          
      .-. .=.=. .
                 
      @ .=. . . .
        |     !
      . .-. .-. .
      
      instruction: Green links pivot with grid, but point in the same direction
      ---
      . .-* .
        |    
      . . . .
             
      . .>. .
             
      . @ . .
      
      . . .-.-@
              
      . .<. . .
              
      .>. . . .
      | !      
      .-.-. . *
        !     
      . . . . .
      
      . . . . .
            ^ 
      .<. . . *
              
      . . . . .
              
      @ . . .>.
        v     
      . . . . .
      
      . .-. . .
            ^ 
      . .<.=.=.
              
      .>. . .-@
              
      * . . .=.
              
      . . . . .
      
      .=. . .-*
          v   
      . . . . .
               
      . . .-.J.
               
      @-. . . .
          v   
      .<. . . .
      
      .-.-. @>.
      !     ^ 
      . . . . .
        |      
      . . . . .
        |     
      . . . .=*
          ^    
      . . .-. .>
      
      .-. . . *
              
      . .>. . .
      |       v
      .-. . . .
        ^      
      . . .-. .
            v 
      @=.=. . .
      
      . . .>. .
        ! |    
      @=. .-. .
              
      . . . .=.>
               
      . . . . .
               
      . *>.<. .
      
      * . @ . .
      v   |   
      . . . . .
            !  
      . . . . .
      ^     ! !
      . .-. . .
        !     
      . . . . .
          v
      
      . . . . . .
      | v         
      @ . . . . *
        | |      
      . . . . . .
      | !   ^ | K
      . . . .-.=.
      |          
      . .-. . . .
      v          
      .>. . . . .
      
      . @-. .>.-.
                
      . . . . . .
                |
      * .>. .=. .
          !      
      . . . . . .>
            |   ^
      . . . .=. .
                 
      . .=. . .=.>
      
      . .-.-. .=.
            v   
      . . . . . .
        |     ! v
      .>. . . . *
          ^      
      . . . . . .
      |          
      . .-.<. . .
      ! |       |
      . . . .>.-@
      
      View more: < Prev Next > or try a Random SubReddah