Yet another ridiculous Chance the Rapper thing. Works best in Chrome or Safari. Firefox is a slightly subpar experience. It hardly works on mobile but it is somehow better than Firefox.
A Pen by HARUN PEHLİVAN on CodePen.
Yet another ridiculous Chance the Rapper thing. Works best in Chrome or Safari. Firefox is a slightly subpar experience. It hardly works on mobile but it is somehow better than Firefox.
A Pen by HARUN PEHLİVAN on CodePen.
| <main> | |
| <section id="entertainment"> | |
| <div id="faces"></div> | |
| <div id="chano"><div><div><div></div></div></div></div> | |
| </section> | |
| <section id="sequencer"> | |
| <div id="sequence"></div> | |
| <nav class="controls"> | |
| <ul> | |
| <li> | |
| <input id="bpm" type="range" min="80" max="140" /> | |
| </li> | |
| <li> | |
| <button id="go">Start</button> | |
| </li> | |
| </ul> | |
| </nav> | |
| </section> | |
| </main> |
| console.clear(); | |
| /******************** | |
| Instantiate Chano | |
| ********************/ | |
| var chance = new Chano({ | |
| bpm: 90, | |
| beats: 16, | |
| selectors: { | |
| bpm: "#bpm", | |
| go: "#go" | |
| } | |
| }); | |
| chance.load.then(function(chano) { | |
| //chano.play(); | |
| var sequence = new Sequence({ | |
| sequence: "#sequence", | |
| faces: "#faces", | |
| chano: chano | |
| }); | |
| var animation = new Animation({ | |
| chano: "#chano", | |
| frames: 13 | |
| }); | |
| chano.update = function(time, col) { | |
| sequence.$seq.className = "current-" + col; | |
| sequence.$faces.className = "current-" + chano.actives.join(""); | |
| setTimeout(function() { | |
| sequence.$faces.className = ""; | |
| }, 100 / chano.bpm * 64); | |
| animation.update(); | |
| }; | |
| }); | |
| /******************** | |
| Chance the Sampler: Player | |
| ********************/ | |
| function Chano(params) { | |
| var C = {}; | |
| init(); | |
| return C; | |
| /*********** | |
| initializer | |
| ***********/ | |
| function init() { | |
| drawPlaceholder(); // better screengrab | |
| setParams(); | |
| /********* | |
| instance.load.then(function(chano) { | |
| // do whatever with chano | |
| }); | |
| *********/ | |
| C.is_mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); | |
| C.click_event = C.is_mobile ? "touchstart" : "click"; | |
| if("-webkit-box-reflect" in document.body.style) { | |
| document.querySelector("html").className += "box-reflect "; | |
| } | |
| C.load = new Promise(function(resolve, reject) { | |
| // if mobile, this needs to be instantiated by a click. | |
| setMobile(go); | |
| function go() { | |
| var set_synth = setSynth(); | |
| set_synth.then(function() { | |
| removePlaceholder(); | |
| setDomValue(); | |
| setMethods(); | |
| setMatrix(); | |
| setTransport(); | |
| setEvents(); | |
| resolve(C); | |
| }); | |
| } | |
| }); | |
| } | |
| /*********** | |
| placeholder | |
| ***********/ | |
| function drawPlaceholder() { | |
| C.placeholder = document.createElement("div"); | |
| C.placeholder.style.backgroundImage = "url('https://res.cloudinary.com/tercuman-b-l-m-merkez/image/upload/v1606077366/logo_transparent_zkii7j.png')"; | |
| C.placeholder.style.backgroundPosition = "center"; | |
| C.placeholder.style.backgroundSize = "cover"; | |
| C.placeholder.style.position = "fixed"; | |
| C.placeholder.style.top = 0; | |
| C.placeholder.style.left = 0; | |
| C.placeholder.style.right = 0; | |
| C.placeholder.style.bottom = 0; | |
| document.body.appendChild(C.placeholder); | |
| } | |
| function removePlaceholder() { | |
| C.placeholder.remove(); | |
| } | |
| /*********** | |
| setters | |
| ***********/ | |
| function setMobile(callback) { | |
| // if mobile, we need to instantiate with a click | |
| if(C.is_mobile) { | |
| document.querySelector("html").className += "mobile "; | |
| var $element = document.createElement("div"); | |
| $element.className = "mobile-start"; | |
| document.body.appendChild($element); | |
| var $mobile_start = document.createElement("button"); | |
| $mobile_start.innerHTML = "Launch"; | |
| $mobile_start.addEventListener("touchend", function(e) { | |
| e.preventDefault(); | |
| Tone.startMobile(); | |
| $element.remove(); | |
| callback(); | |
| }); | |
| $element.appendChild($mobile_start); | |
| } else { | |
| callback(); | |
| } | |
| } | |
| function setParams() { | |
| C._set = { | |
| bpm: params.bpm || 90, | |
| beats: params.beats || 16 | |
| } | |
| C._el = { | |
| $bpm: document.querySelector(params.selectors.bpm), | |
| $go: document.querySelector(params.selectors.go) | |
| } | |
| } | |
| function setDomValue() { | |
| C._el.$bpm.value = C._set.bpm; | |
| } | |
| function setSynth() { | |
| return new Promise(function(resolve, reject) { | |
| var root = "https://s3-us-west-2.amazonaws.com/s.cdpn.io/111863/chance-the-sampler-"; | |
| C.keys = new Tone.PolySynth(4, Tone.Sampler, { | |
| sd1: root + "01.mp3", | |
| hh1: root + "02.mp3", | |
| sd2: root + "03.mp3", | |
| kd1: root + "04.mp3", | |
| }, { | |
| "volume" : -5, | |
| }).toMaster(); | |
| C.note_names = ["sd1", "hh1", "sd2", "kd1"]; | |
| var count = 4; | |
| var loaded = 1; | |
| for(var i = 0; i < count; i++) { | |
| var buffer = new Tone.Buffer(root + "0" + (i+1) + ".mp3", function(){ | |
| loaded++; | |
| if(loaded === count) { | |
| resolve("Audios Loaded"); | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| function setMatrix() { | |
| C.matrix = [ | |
| [1,0,0,1], | |
| [0,0,0,0], | |
| [1,0,0,0], | |
| [0,0,0,0], | |
| [1,1,0,0], | |
| [0,1,0,1], | |
| [1,1,0,0], | |
| [0,0,0,0], | |
| [0,0,0,1], | |
| [1,0,1,0], | |
| [0,0,0,1], | |
| [1,0,0,0], | |
| [0,0,0,0], | |
| [1,0,1,0], | |
| [0,0,0,1], | |
| [1,0,1,0] | |
| ]; | |
| } | |
| function setTransport() { | |
| var arr = []; | |
| for(var i = 0; i < C._set.beats; i++) arr.push(i); | |
| C.loop = new Tone.Sequence(function(time, col) { | |
| C.actives = []; | |
| var column = C.matrix[col]; | |
| for (var i = 0; i < 4; i++) { | |
| if (column[i] === 1) { | |
| C.actives.push(i); | |
| C.keys.triggerAttackRelease(C.note_names[i], "8n", time); | |
| } | |
| } | |
| C.update(time, col); | |
| }, arr, "16n"); | |
| C.updateBpm(C._set.bpm); | |
| Tone.Transport.start(); | |
| } | |
| function setMethods() { | |
| C.updateBpm = updateBpm; | |
| C.updateMatrix = updateMatrix; | |
| C.play = play; | |
| C.pause = pause; | |
| // provide update function | |
| C.update = function(time, col) {}; | |
| } | |
| function setEvents() { | |
| C._el.$bpm.addEventListener("change", function(e) { | |
| C.updateBpm(e.target.value); | |
| }); | |
| C._el.$go.addEventListener(C.click_event, togglePlay); | |
| } | |
| /*********** | |
| events | |
| ***********/ | |
| function updateMatrix(col, row, value) { | |
| C.matrix[col][row] = value; | |
| } | |
| function updateBpm(bpm) { | |
| Tone.Transport.bpm.value = bpm; | |
| var $html = document.querySelector("html"); | |
| var bpm_class = $html.className.match("bpm-"); | |
| $html.className = bpm_class ? $html.className.replace(/bpm-\d+/g,"bpm-"+bpm) : $html.className + "bpm-" + bpm; | |
| C.bpm = bpm; | |
| } | |
| function play() { | |
| C.loop.start(); | |
| document.body.className = "playing"; | |
| C.playing = true; | |
| } | |
| function pause() { | |
| C.loop.stop(); | |
| document.body.className = ""; | |
| C.playing = false; | |
| } | |
| function togglePlay(e) { | |
| if(C.playing) { | |
| C.pause(); | |
| if(e && e.target) e.target.innerHTML = "Start"; | |
| } else { | |
| C.play(); | |
| if(e && e.target) e.target.innerHTML = "Stop"; | |
| } | |
| } | |
| } | |
| /******************** | |
| Chance the Sampler: Sequencer | |
| ********************/ | |
| function Sequence(params) { | |
| var S = {}; | |
| init(); | |
| return S; | |
| /*********** | |
| initializer | |
| ***********/ | |
| function init() { | |
| setData(); | |
| createElements(); | |
| } | |
| /*********** | |
| private | |
| ***********/ | |
| function setData() { | |
| S.chano = params.chano; | |
| S.$seq = document.querySelector(params.sequence); | |
| S.$faces = document.querySelector(params.faces); | |
| } | |
| function createElements() { | |
| S.nodes = []; | |
| S.rows = []; | |
| for(var r = 0; r < S.chano.matrix[0].length; r++) { | |
| S.nodes.push([]); | |
| var $row = document.createElement("div"); | |
| $row.className = "row-" + r; | |
| S.$seq.appendChild($row); | |
| S.rows.push($row); | |
| } | |
| for(var r = 0; r < S.chano.matrix[0].length; r++) { | |
| var $face = document.createElement("div"); | |
| $face.className = "face-" + r; | |
| $face.setAttribute("data-key", r); | |
| S.$faces.appendChild($face); | |
| $face.addEventListener(S.chano.click_event, function(e) { | |
| var el = e.target; | |
| var key = parseInt(el.getAttribute("data-key")); | |
| S.$faces.className = "face-" + key; | |
| if(!S.chano.playing) { | |
| setTimeout(function() { | |
| S.$faces.className = ""; | |
| }, 100); | |
| } | |
| var note = S.chano.note_names[key]; | |
| S.chano.keys.triggerAttackRelease(note, "8n"); | |
| }); | |
| } | |
| for(var c = 0; c < S.chano.matrix.length; c++) { | |
| var col = S.chano.matrix[c]; | |
| for(var r = 0; r < col.length; r++) { | |
| var el = document.createElement("span"); | |
| el.setAttribute("data-row", r); | |
| el.setAttribute("data-col", c); | |
| el.setAttribute("data-val", col[r]); | |
| el.addEventListener(S.chano.click_event, handleNodeClick); | |
| S.nodes[r].push(el); | |
| S.rows[r].appendChild(el); | |
| } | |
| } | |
| } | |
| function handleNodeClick(e) { | |
| var el = e.target, | |
| col = el.getAttribute("data-col"), | |
| row = el.getAttribute("data-row"), | |
| val = el.getAttribute("data-val"); | |
| val = val === "1" ? 0 : 1; | |
| el.setAttribute("data-val", val); | |
| S.chano.updateMatrix(col, row, val); | |
| } | |
| } | |
| /******************** | |
| Chance the Sampler: Sprite Animation | |
| ********************/ | |
| function Animation(params) { | |
| var A = {}; | |
| init(); | |
| return A; | |
| /*********** | |
| initialize | |
| ***********/ | |
| function init() { | |
| setData(); | |
| setUpdate(); | |
| } | |
| /*********** | |
| private | |
| ***********/ | |
| function setData() { | |
| A.frame = 1; | |
| A.direction = "up"; | |
| A.max_frame = params.frames; | |
| A.$chano = document.querySelector(params.chano); | |
| } | |
| function setUpdate() { | |
| A.update = function() { | |
| A.$chano.className = "frame-" + A.frame; | |
| if(A.frame === 1) { | |
| A.direction = "up"; | |
| A.frame++; | |
| } else if(A.frame === A.max_frame) { | |
| A.direction = "down"; | |
| A.frame--; | |
| } else if(A.direction === "up") { | |
| A.frame++; | |
| } else { | |
| A.frame--; | |
| } | |
| } | |
| } | |
| } | |
| <script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/111863/Tone.min.js"></script> |
| $cols: 16; | |
| $rows: 4; | |
| $frame-h: 463px; | |
| $frame-w: 300px; | |
| $frame-count: 13; | |
| $node-bg-di: 118px; | |
| $row-h: $node-bg-di; | |
| $asset-root: "https://s3-us-west-2.amazonaws.com/s.cdpn.io/111863/"; | |
| $mobile-w: 668px; | |
| $c-bg: #101010; | |
| $c-primary: #D79C57; | |
| $c-primary-d: darken($c-primary, 10%); | |
| $c-secondary: #996888; | |
| $c-material: #444; | |
| $c-material-xl: lighten($c-material, 15%); | |
| $c-material-l: lighten($c-material, 5%); | |
| $c-material-d: darken($c-material, 5%); | |
| $c-material-xd: darken($c-material, 10%); | |
| $c-material-xxd: darken($c-material, 15%); | |
| $c-border: rgba(0,0,0,0.35); | |
| $c-border: $c-material-xxd; | |
| html, body { | |
| height: 100%; | |
| min-height: 422px; | |
| } | |
| main { | |
| width: 100%; | |
| position: absolute; | |
| top: 50%; left: 50%; | |
| transform: translate(-50%, -50%); | |
| } | |
| section { | |
| margin: 0 auto; | |
| } | |
| button { | |
| appearance: none; | |
| color: white; | |
| background: $c-primary; | |
| border: none; | |
| border-radius: 4px; | |
| text-shadow: -1px -1px 0px $c-primary-d; | |
| border: 1px solid $c-primary-d; | |
| &:hover { | |
| background: $c-primary-d; | |
| } | |
| } | |
| .mobile-start { | |
| position: fixed; | |
| top: 0; right: 0; bottom: 0; left: 0; | |
| background: rgba(0,0,0,0.9); | |
| button { | |
| position: absolute; | |
| top: 50%; left: 50%; | |
| transform: translate(-50%, -50%); | |
| font-size: 2rem; | |
| padding: 2rem 2rem; | |
| } | |
| } | |
| #entertainment { | |
| padding: 0 0 1rem 0; | |
| width: 150px; | |
| height: 150px; | |
| @media (min-width: $mobile-w) { | |
| width: 300px; | |
| height: 300px; | |
| } | |
| position: relative; | |
| #chano, #faces { | |
| position: absolute; | |
| } | |
| } | |
| #sequencer { | |
| width: 90%; | |
| max-width: 1200px; | |
| padding: 0.25rem; | |
| background: $c-material-xd; | |
| border-radius: 4px; | |
| box-shadow: 0px 2px 0px 2px $c-material-xxd; | |
| ul { | |
| border-radius: 4px; | |
| list-style: none; | |
| margin: 0.25rem 0 0; | |
| padding: 0.5rem; | |
| text-align: center; | |
| background: $c-material-d; | |
| border: 1px solid $c-border; | |
| li { | |
| display: inline-block; | |
| margin: 0; | |
| + li { margin-left: 1rem; } | |
| label { | |
| display: block; | |
| } | |
| } | |
| } | |
| button { | |
| box-sizing: border-box; | |
| padding: 0.5rem 0; | |
| } | |
| button, input { | |
| width: 100px; | |
| } | |
| } | |
| #faces { | |
| display: flex; | |
| flex-wrap: wrap; | |
| width: 100%; | |
| position: relative; | |
| box-sizing: border-box; | |
| background: $c-material-xd; | |
| box-shadow: 0px 2px 0px 2px $c-material-xxd; | |
| border-radius: 4px; | |
| padding: 0.5rem; | |
| div:nth-child(1) { border-radius: 4px 0 0 0; } | |
| div:nth-child(2) { border-radius: 0 4px 0 0; } | |
| div:nth-child(3) { border-radius: 0 0 0 4px; } | |
| div:nth-child(4) { border-radius: 0 0 4px 0; } | |
| [class^="face-"] { | |
| width: 50%; | |
| position: relative; | |
| cursor: pointer; | |
| overflow: hidden; | |
| &::before { | |
| display: block; | |
| content: ""; | |
| width: 100%; | |
| padding-top: 100%; | |
| } | |
| &::after { | |
| content: ""; | |
| cursor: pointer; | |
| position: absolute; | |
| top: 0; left: 0; bottom: 0; | |
| width: 100%; | |
| background-size: cover; | |
| filter: grayscale(1); | |
| } | |
| &:hover::after { | |
| filter: grayscale(0); | |
| } | |
| } | |
| } | |
| // the sequencer rows | |
| [class^="row-"] { | |
| width: 100%; | |
| display: flex; | |
| box-sizing: border-box; | |
| border-bottom: 1px solid $c-border; | |
| border-right: 1px solid $c-border; | |
| border-left: 1px solid $c-border; | |
| overflow: hidden; | |
| transform: translateZ(0); | |
| &:first-child { | |
| border-top: 1px solid $c-border; | |
| border-radius: 4px 4px 0 0; | |
| } | |
| &:last-child { | |
| border-radius: 0 0 4px 4px; | |
| } | |
| span { | |
| position: relative; | |
| display: block; | |
| box-sizing: border-box; | |
| width: 1 / $cols * 100%; | |
| cursor: pointer; | |
| overflow: hidden; | |
| background: $c-material-d; | |
| transform: translateZ(0); | |
| &::before { | |
| content: ""; | |
| display: block; | |
| width: 100%; | |
| padding-top: 100%; | |
| } | |
| + span { | |
| border-left: 1px solid $c-border; | |
| } | |
| &:nth-child(n+5):not(:nth-child(n+9)), | |
| &:nth-child(n+13) { | |
| background: $c-material-l; | |
| } | |
| &::after { | |
| content: ""; | |
| position: absolute; | |
| top: 0; left: 0; bottom: 0; | |
| width: 100%; | |
| background-size: cover; | |
| border: 1px solid $c-border; | |
| transform-origin: 50% 50%; | |
| @media (min-width: $mobile-w) { | |
| transition: transform 70ms ease-in-out, | |
| filter 70ms ease-in-out, | |
| border-radius 100ms ease-in-out; | |
| } | |
| } | |
| &[data-val="1"] { | |
| &::after { | |
| opacity: 0.8; | |
| transform: scale(1.05) translateZ(0); | |
| filter: grayscale(0); | |
| border: none; | |
| border-radius: 0px; | |
| } | |
| } | |
| &[data-val="0"] { | |
| &::after { | |
| opacity: 0.6; | |
| transform: scale(0.8) translateZ(0); | |
| filter: grayscale(1); | |
| border-radius: 50%; | |
| } | |
| } | |
| &:hover { | |
| background: $c-material-xl!important; | |
| &::after { | |
| opacity: 1; | |
| } | |
| } | |
| } | |
| } | |
| @for $row from 1 through $rows { | |
| [class^="face-#{$row - 1}"]::after, | |
| [class^="row-#{$row - 1}"] span::after { | |
| background-image: url(#{$asset-root}chance-the-sampler-face-#{$row}.jpg); | |
| } | |
| } | |
| @for $col from 1 through $cols { | |
| #faces[class*="#{$col - 1}"] .face-#{$col - 1}::after { | |
| background-position: -100% center; | |
| filter: grayscale(0); | |
| } | |
| .current-#{$col - 1} [data-col="#{$col - 1}"] { | |
| @media (min-width: $mobile-w) { | |
| &, &:hover { background: rgba(255,255,255,0.4)!important; } | |
| } | |
| &::after { | |
| opacity: 1!important; | |
| } | |
| &[data-val="1"] { | |
| &::after { | |
| background-position: -100% center; | |
| } | |
| } | |
| } | |
| } | |
| #chano div { | |
| display: none; | |
| @media (min-width: $mobile-w) { | |
| .box-reflect & { | |
| display: block; | |
| position: absolute; | |
| transform: translate(-100%, -50%) scale(0.5) rotate(15deg); | |
| -webkit-box-reflect: above; | |
| } | |
| } | |
| } | |
| #chano { | |
| transform: translate(-50%, -50%); | |
| @media (min-width: $mobile-w) { | |
| html:not(.box-reflect) & { | |
| display: none; | |
| } | |
| .box-reflect & { | |
| transform: translate(-300px, -50%); | |
| -webkit-box-reflect: right 300px; | |
| } | |
| } | |
| } | |
| :not(.mobile) #chano div, | |
| #chano { | |
| top: 50%; | |
| left: 50%; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 1000ms; | |
| .playing & { opacity: 1; } | |
| &::after { | |
| content: ""; | |
| display: block; | |
| background-image: url("#{$asset-root}chance-the-sprite.png"); | |
| height: $frame-h / 4; | |
| width: $frame-w / 4; | |
| background-size: ($frame-w/4) * $frame-count ($frame-h/4); | |
| @media (min-width: $mobile-w) { | |
| height: $frame-h / 2; | |
| width: $frame-w / 2; | |
| background-size: ($frame-w/2) * $frame-count ($frame-h/2); | |
| } | |
| } | |
| } | |
| @for $i from 1 through $frame-count { | |
| #chano.frame-#{$i} div::after, | |
| #chano.frame-#{$i}::after { | |
| $x: ($i - 1) / ($frame-count) * ($frame-w / 4 * -$frame-count); | |
| background-position: $x center; | |
| @media (min-width: $mobile-w) { | |
| $x: ($i - 1) / ($frame-count) * ($frame-w / 2 * -$frame-count); | |
| background-position: $x center; | |
| } | |
| } | |
| } | |
| body { | |
| color: white; | |
| background: $c-bg; | |
| // overflow: hidden; | |
| &::before { | |
| content: ""; | |
| position: fixed; | |
| top: 0; right: 0; bottom: 0; left: 0; | |
| background-size: cover; | |
| background-position: center; | |
| background-image: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/111863/chance-the-sampler-bg.jpg); | |
| opacity: 0.1; | |
| filter: grayscale(1); | |
| } | |
| .box-reflect &.playing::before { | |
| animation: bg 1s ease-in-out alternate infinite; | |
| } | |
| } | |
| @for $i from 80 through 140 { | |
| html.bpm-#{$i}.box-reflect body::before { | |
| animation-duration: (1 / ($i / 60)) * 1s; | |
| } | |
| } | |
| @keyframes bg { | |
| from { transform: scale(1); } | |
| to { transform: scale(1.05); } | |
| } |