A Pen by cesare soldini on CodePen.
Forked from Apex Design Studio's Pen BB-8.
A Pen by cesare soldini on CodePen.
Forked from Apex Design Studio's Pen BB-8.
| <div class="message"> | |
| <h2>move mouse or tap.</h2> | |
| </div> | |
| <div class="sand"></div> | |
| <div class="bb8"> | |
| <div class="antennas"> | |
| <div class="antenna short"></div> | |
| <div class="antenna long"></div> | |
| </div> | |
| <div class="head"> | |
| <div class="stripe one"></div> | |
| <div class="stripe two"></div> | |
| <div class="eyes"> | |
| <div class="eye one"></div> | |
| <div class="eye two"></div> | |
| </div> | |
| <div class="stripe three"></div> | |
| </div> | |
| <div class="ball"> | |
| <div class="lines one"></div> | |
| <div class="lines two"></div> | |
| <div class="ring one"></div> | |
| <div class="ring two"></div> | |
| <div class="ring three"></div> | |
| </div> | |
| <div class="shadow"></div> | |
| </div> | |
| <div class="shameless"> | |
| by | |
| <a href="https://www.linkedin.com/in/caesarsol" target="_blank">caesarsol</a> | |
| - | |
| inspired by | |
| <a href="http://codepen.io/apexdesignstudio/pen/PPEJwz" target="_blank">BB-8 (apexstudio)</a> | |
| </div> |
| function loopFrames(func) { | |
| let loopControl = { | |
| _continueFlag: true, | |
| break: function() { this._continueFlag = false }, | |
| continued: function() { return (this._continueFlag === true) } | |
| } | |
| requestAnimationFrame(() => { | |
| func(loopControl) | |
| if (loopControl.continued()) loopFrames(func) | |
| }) | |
| } | |
| function switchCase(subject) { | |
| const noop = {when: () => noop, default: () => noop} | |
| return { | |
| when(compare, then) { | |
| if (subject === compare) { | |
| then() | |
| return noop | |
| } else { | |
| return this | |
| } | |
| }, | |
| default: (then) => then() | |
| } | |
| } | |
| $.fn.transform = function(transf, ...values) { | |
| let transforms = this.data('transforms') | |
| if (!transforms || transforms.length === 0) { | |
| transforms = new Map() | |
| } | |
| switchCase(transf) | |
| .when('translate', () => { | |
| let [x, y] = values | |
| transforms.set('translate', `${x}px, ${y}px`) | |
| }) | |
| .when('rotate', () => { | |
| let [degrees] = values | |
| transforms.set('rotate', `${degrees}deg`) | |
| }) | |
| .default(() => { | |
| throw ("Transformation not supported") | |
| }) | |
| let transfString = Array.from(transforms.entries()).map(([k,v]) => `${k}(${v})`).join(' ') | |
| this.css('transform', transfString) | |
| this.data('transforms', transforms) | |
| return this | |
| } | |
| $.fn.translate = function(x,y) { | |
| /* Position object absolutely */ | |
| requestAnimationFrame(() => | |
| this.transform('translate', x, y) | |
| ) | |
| return this | |
| }//initialPoint | |
| $.fn.rotate = function(degrees, {center} = {}) { | |
| if (center) { | |
| let originCss = `${center.x}px ${center.y}px` | |
| if (!this.css('transform-origin') !== originCss) | |
| this.css('transform-origin', originCss) | |
| } | |
| requestAnimationFrame(() => | |
| this.transform('rotate', degrees) | |
| ) | |
| return this | |
| } | |
| $.fn.translatePoint = function(point) { | |
| return this.translate(point.x, point.y) | |
| } | |
| class PointLogger { | |
| constructor(point, name, {color, centerOn}) { | |
| const width = 5; | |
| this.point = point | |
| this.name = name | |
| this.center = centerOn || new Point() | |
| this.$marker = $('<div>', { | |
| id: this.name, | |
| css: { | |
| position: 'absolute', | |
| top: 0, | |
| left: 0, | |
| width: 1 + width*2, | |
| height: 1 + width*2, | |
| margin: 0, | |
| padding: 0, | |
| background: color, | |
| marginLeft: -(1 + width), | |
| marginTop: -(width), | |
| } | |
| }) | |
| this.$label = $('<span>', { | |
| html: this.name, | |
| css: { | |
| display: 'inline-block', | |
| width: 120, | |
| fontFamily: 'sans-serif', | |
| fontSize: 13, | |
| }, | |
| append: $('<div>', { css: { background: color }}), | |
| }) | |
| this.$coordinateLoggerX = $('<input>', { | |
| width: 120, | |
| css: { | |
| backgroundColor: 'transparent', | |
| color: 'inherit', | |
| border: 'none', | |
| }, | |
| }) | |
| this.$coordinateLoggerY = $('<input>', { | |
| width: 120, | |
| css: { | |
| backgroundColor: 'transparent', | |
| color: 'inherit', | |
| border: 'none', | |
| }, | |
| }) | |
| this.$coordinateLoggerM = $('<input>', { | |
| width: 120, | |
| css: { | |
| backgroundColor: 'transparent', | |
| color: 'inherit', | |
| border: 'none', | |
| }, | |
| }) | |
| } | |
| watch() { | |
| this.point.addWatcher(() => this.log()) | |
| } | |
| mount() { | |
| this.$marker.appendTo($(document.body)) | |
| $('<div>', {className: 'point-logger', css: {color: '#ccc'}}).append([ | |
| this.$label, | |
| this.$coordinateLoggerX, | |
| this.$coordinateLoggerY, | |
| this.$coordinateLoggerM, | |
| ]).appendTo($(document.body)) | |
| this.watch() | |
| this.log() | |
| return this | |
| } | |
| log() { | |
| this.$marker.translatePoint(this.point.added(this.center)) | |
| this.$coordinateLoggerX.val(this.point.x) | |
| this.$coordinateLoggerY.val(this.point.y) | |
| this.$coordinateLoggerM.val(this.point.module()) | |
| } | |
| } | |
| class Point { | |
| constructor(x = 0, y = 0) { | |
| this._x = x | |
| this._y = y | |
| // list of callbacks we call after each change | |
| this._watchers = [] | |
| this._callWatchers() | |
| } | |
| addWatcher(callback) { | |
| this._watchers.push(callback.bind(this)) | |
| } | |
| _callWatchers() { | |
| this._watchers.forEach((callback) => callback(this)) | |
| } | |
| addUpdater(updaterFunc) { | |
| updaterFunc(this) | |
| } | |
| get x() { return this._x } | |
| get y() { return this._y } | |
| get position() { | |
| return [this.x, this.y] | |
| } | |
| set(x, y) { | |
| this._x = x | |
| this._y = y | |
| this._callWatchers() | |
| return this | |
| } | |
| setWith(transformX, transformY = null) { | |
| /* | |
| * Accepts one or two callbacks of the form: | |
| * function(coordinateValue, coordinateName) | |
| * | |
| */ | |
| if (!transformY) transformY = transformX | |
| this.set(transformX(this.x, 'x'), transformY(this.y, 'y')) | |
| return this | |
| } | |
| /* Other Points */ | |
| setOn(otherPoint) { | |
| this.set(otherPoint.x, otherPoint.y) | |
| return this | |
| } | |
| follow(otherPoint) { | |
| otherPoint.addWatcher(() => { | |
| this.setOn(otherPoint) | |
| }) | |
| return this | |
| } | |
| followAnimating(otherPoint, {dampening, maxVelocity, maxAcceleration}) { | |
| let velocity = 0 | |
| let remains = new Point() | |
| otherPoint.addWatcher(() => { | |
| if (velocity > 0) return | |
| loopFrames((loop) => { | |
| remains.setOn(otherPoint.subtracted(this)) | |
| if (remains.module() > dampening) { | |
| let oldVelocity = velocity | |
| let advance = remains.multiplied(dampening) | |
| velocity = advance.module() | |
| if (velocity > maxVelocity) { | |
| // Velocity: pixel/frame | |
| advance.setModule(maxVelocity) | |
| velocity = maxVelocity | |
| } | |
| if (velocity - oldVelocity > maxAcceleration) { | |
| velocity = oldVelocity + maxAcceleration | |
| advance.setModule(velocity) | |
| } | |
| this.add(advance) | |
| } else { | |
| this.setOn(otherPoint) | |
| velocity = 0 | |
| loop.break() | |
| } | |
| }) | |
| }) | |
| return this | |
| } | |
| /* Math */ | |
| add(otherPoint) { | |
| this.setWith((c, cn) => c + otherPoint[cn]) | |
| return this | |
| } | |
| subtract(otherPoint) { | |
| this.setWith((c, cn) => c - otherPoint[cn]) | |
| return this | |
| } | |
| multiply(scalar) { | |
| this.setWith(c => c * scalar) | |
| return this | |
| } | |
| normalize() { | |
| let m = this.module() | |
| this.setWith(c => c / m) | |
| return this | |
| } | |
| setModule(module) { | |
| this.normalize().multiply(module) | |
| return this | |
| } | |
| /* Immutable Math */ | |
| duplicated() { | |
| return new Point().setOn(this) | |
| } | |
| added(otherPoint) { | |
| return this.duplicated().add(otherPoint) | |
| } | |
| subtracted(otherPoint) { | |
| return this.duplicated().subtract(otherPoint) | |
| } | |
| multiplied(scalar) { | |
| return this.duplicated().multiply(scalar) | |
| } | |
| normalized() { | |
| return this.duplicated().normalize() | |
| } | |
| /* Scalars */ | |
| moduleSquared() { | |
| return Math.pow(this.x, 2) + Math.pow(this.y, 2) | |
| } | |
| module() { | |
| return Math.sqrt(this.moduleSquared()) | |
| } | |
| } | |
| /*********/ | |
| var position = new Point() | |
| var desired = new Point() | |
| var mouse = new Point() | |
| // Log the details if you desire! | |
| //new PointLogger(desired, 'desired', {color: 'blue'}).mount() | |
| //new PointLogger(position, 'position', {color: 'yellow'}).mount() | |
| mouse.addUpdater((pt) => { | |
| $(document).on("mousemove touchstart touchmove", (event) => { | |
| pt.setWith( | |
| x => event.pageX, | |
| y => event.pageY, | |
| ) | |
| }) | |
| }) | |
| desired.follow(mouse) | |
| position.followAnimating(desired, {dampening: 0.05, maxVelocity: 15, maxAcceleration: .5}) | |
| let $image = $('.bb8') | |
| let $wheel = $image.find('.ball') | |
| position.addWatcher((pt) => { | |
| $image.translate(pt.x, 0) | |
| $wheel.rotate(pt.x) | |
| if (position.x < desired.x) { | |
| $('.antennas, .eyes').addClass('right') | |
| } else { | |
| $('.antennas, .eyes').removeClass('right') | |
| } | |
| }) |
| <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script> | |
| <script src="https://s3-us-west-2.amazonaws.com/samsarajs.org/dist/0.2.0/samsara.js"></script> |
| @import url(https://fonts.googleapis.com/css?family=Raleway); | |
| *,*:before,*:after { box-sizing: border-box; } | |
| body { | |
| background: #869F9D; | |
| overflow: hidden; | |
| } | |
| h2 { | |
| color: #ccc; | |
| font-family: "raleway", sans-serif; | |
| opacity: .5; | |
| text-align: center; | |
| transition: opacity .8s; | |
| &.hide { | |
| opacity: 0; | |
| } | |
| } | |
| .sand { | |
| background: #B69C77; | |
| height: 30%; | |
| position: absolute; | |
| width: 100%; | |
| z-index: -1; | |
| right: 0; | |
| bottom: 0; | |
| left: 0; | |
| } | |
| // BB-8 | |
| $d: 140px; | |
| .bb8 { | |
| position: absolute; | |
| margin-left: -$d/2; | |
| width: $d; | |
| bottom: 20%; | |
| left: 0; | |
| } | |
| .antennas { | |
| position: absolute; | |
| transition: left .6s; | |
| left: 22%; | |
| &.right { | |
| left: 0%; | |
| } | |
| } | |
| .antenna { | |
| background: #e0d2be; | |
| position: absolute; | |
| width: 2px; | |
| &.short { | |
| height: 20px; | |
| top: -60px; | |
| left: 50px; | |
| } | |
| &.long { | |
| border-top: 6px solid #020204; | |
| border-bottom: 6px solid #020204; | |
| height: 36px; | |
| top: -78px; | |
| left: 56px; | |
| } | |
| } | |
| .head { | |
| background: #e0d2be; | |
| border-radius: 90px 90px 14px 14px; | |
| -moz-border-radius: 90px 90px 14px 14px; | |
| -webkit-border-radius: 90px 90px 14px 14px; | |
| height: 56px; | |
| margin-left: -45px; | |
| overflow: hidden; | |
| position: absolute; | |
| width: 90px; | |
| z-index: 1; | |
| top: -46px; | |
| left: 50%; | |
| .stripe { | |
| position: absolute; | |
| width: 100%; | |
| } | |
| .stripe.one { | |
| background: #999; | |
| height: 6px; | |
| opacity: .8; | |
| z-index: 1; | |
| top: 6px; | |
| } | |
| .stripe.two { | |
| background: #CD7640; | |
| height: 4px; | |
| top: 17px; | |
| } | |
| .stripe.three { | |
| background: #999; | |
| height: 4px; | |
| opacity: .5; | |
| bottom: 3px; | |
| } | |
| .eyes { | |
| display: block; | |
| height: 100%; | |
| position: absolute; | |
| width: 100%; | |
| transition: left .6s; | |
| left: 0%; | |
| } | |
| .eyes.right { | |
| left: 36%; | |
| } | |
| .eye { | |
| border-radius: 50%; | |
| display: block; | |
| position: absolute; | |
| &.one { | |
| background: #020204; | |
| border: 4px solid #e0d2be; | |
| height: 30px; | |
| width: 30px; | |
| top: 12px; | |
| left: 12%; | |
| } | |
| &.one:after { | |
| background: white; | |
| border-radius: 50%; | |
| content: ""; | |
| display: block; | |
| height: 3px; | |
| position: absolute; | |
| width: 3px; | |
| top: 4px; | |
| right: 4px; | |
| } | |
| &.two { | |
| background: #e0d2be; | |
| border: 1px solid #020204; | |
| height: 16px; | |
| width: 16px; | |
| top: 30px; | |
| left: 40%; | |
| &:after { | |
| background: #020204; | |
| border-radius: 50%; | |
| content: ""; | |
| display: block; | |
| height: 10px; | |
| position: absolute; | |
| width: 10px; | |
| top: 2px; | |
| left: 2px; | |
| } | |
| } | |
| } | |
| } | |
| .ball { | |
| background: #d1c3ad; | |
| border-radius: 50%; | |
| height: $d; | |
| overflow: hidden; | |
| position: relative; | |
| width: $d; | |
| } | |
| .lines { | |
| border: 2px solid #B19669; | |
| border-radius: 50%; | |
| height: 400px; | |
| opacity: .6; | |
| position: absolute; | |
| width: 400px; | |
| &.two { | |
| top: -10px; | |
| left: -250px; | |
| } | |
| } | |
| .ring { | |
| background: #CD7640; | |
| border-radius: 50%; | |
| height: 70px; | |
| margin-left: -35px; | |
| position: absolute; | |
| width: 70px; | |
| &:after { | |
| background: #d1c3ad; | |
| border-radius: 50%; | |
| content: ""; | |
| display: block; | |
| height: 70%; | |
| margin-top: -35%; | |
| margin-left: -35%; | |
| position: absolute; | |
| width: 70%; | |
| top: 50%; | |
| left: 50%; | |
| } | |
| &.one { | |
| margin-left: -40px; | |
| height: 80px; | |
| width: 90px; | |
| top: 6%; | |
| left: 50%; | |
| } | |
| &.two { | |
| height: 38px; | |
| width: 76px; | |
| -ms-transform: rotate(36deg); | |
| -webkit-transform: rotate(36deg); | |
| transform: rotate(36deg); | |
| top: 70%; | |
| left: 18%; | |
| &:after { | |
| top: 100%; | |
| } | |
| } | |
| &.three { | |
| height: 30px; | |
| -ms-transform: rotate(-50deg); | |
| -webkit-transform: rotate(-50deg); | |
| transform: rotate(-50deg); | |
| top: 66%; | |
| left: 90%; | |
| &:after { | |
| top: 110%; | |
| } | |
| } | |
| } | |
| .shadow { | |
| background: #3A271C; | |
| border-radius: 50%; | |
| height: $d/6; | |
| opacity: .7; | |
| position: absolute; | |
| width: $d; | |
| z-index: -1; | |
| left: 5px; | |
| bottom: -8px; | |
| } | |
| .shameless { | |
| position: absolute; | |
| bottom: 10px; | |
| right: 10px; | |
| color: #333; | |
| font-family: 'Raleway', sans-serif; | |
| opacity: .5; | |
| } | |
| a { | |
| text-decoration: none; | |
| color: #555; | |
| } | |
| a:visited { | |
| color: #666; | |
| } | |
| a:hover, | |
| a:focus { | |
| color: white; | |
| } |