Created
September 18, 2017 05:38
-
-
Save trusktr/c8d0468c1398422e0fda5f0e586ef5f6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Tell Babel to compile JSX to Preact's form, because we're using Preact for | |
// this app: | |
/** @jsx Preact.createElement */ | |
// pointer events polyfill | |
import 'pepjs' | |
import Preact from 'preact-compat' | |
import TWEEN from 'tween.js' | |
import geometry from 'csg' | |
import color from 'tinycolor2' | |
import Motor from 'infamous/core/Motor' | |
import 'infamous/html' | |
import sleep from 'awaitbox/timers/sleep' | |
import RippleFlip from './rippleFlip' | |
const colors = { | |
skyblue: color('#1a95d9'), | |
hotpink: color('#d11482'), | |
limegreen: color('#90e818'), | |
yellow: color('#fdb833'), | |
teal: color('#28c9f6'), | |
} | |
export default | |
class App extends Preact.Component { | |
constructor(props) { | |
super(props) | |
this.audioAnalyser = null | |
this.circle1Radius = 105 | |
this.circle3Radius = 34 | |
this.circle2OuterRadius = this.circle1Radius - 5 | |
this.circle2InnerRadius = this.circle3Radius + 8 | |
this.circle4Radius = 22 | |
this.circle1Range = _.range(48) | |
this.circle2Range = _.range(24) | |
this.circle3Range = _.range(24) | |
this.circle4Range = _.range(12) | |
this.circle2TriangleRings = _.range(5) | |
const innerTriangleSize = 4 | |
const outerTriangleSize = 14 | |
this.innerTriangleSizes = _.range(4, 14, 2) | |
const spacing = 4 | |
this.circle2triangleRadii = [ | |
this.circle2InnerRadius, | |
this.circle2InnerRadius + spacing*1 + 4, | |
this.circle2InnerRadius + spacing*2 + 4 + 6, | |
this.circle2InnerRadius + spacing*3 + 4 + 6 + 8, | |
this.circle2InnerRadius + spacing*4 + 4 + 6 + 8 + 10, | |
] | |
this.startAudio() | |
this.state = this.state || {} | |
const outerTrapezoidRingZPos = 50 | |
const innerQuadRingZPos = -50 | |
Object.assign(this.state, { | |
ready: false, // nothing will render until this is true | |
triangleColumnAnimParam: 0, | |
outerTrapezoidRingZPos, | |
innerQuadRingZPos, | |
audioDataArray: new Uint8Array(this.audioBufferLength), | |
color1AnimParam: 0.5, | |
}) | |
this.triangleRingPositions = [] | |
const individualQuadFlipRotations = this.individualQuadFlipRotations = [] | |
this.calcTriangleRingPositions(innerQuadRingZPos, outerTrapezoidRingZPos) | |
// init values in individualQuadFlipRotations | |
let i = 48 | |
// TODO which is faster? | |
while (i--) individualQuadFlipRotations[i] = 0 | |
//_.times(i, () => individualQuadFlipRotations.push(0)) | |
} | |
render(props, state) { | |
const { | |
triangleColumnAnimParam, | |
outerTrapezoidRingZPos, | |
innerQuadRingZPos, | |
audioDataArray, | |
color1AnimParam, | |
ready, | |
} = state | |
const { | |
triangleRingPositions, | |
individualQuadFlipRotations, | |
} = this | |
let { | |
skyblue, | |
hotpink, | |
teal, | |
limegreen, | |
yellow, | |
} = colors | |
this.calcTriangleRingPositions(innerQuadRingZPos, outerTrapezoidRingZPos) | |
///////////// AUDIO | |
const circle1TrapezoidAudioDatum = mapAudioDataToFewerUnits(audioDataArray, this.circle1Range.length) | |
const circle3QuadAudioDatum = mapAudioDataToFewerUnits(audioDataArray, this.circle3Range.length) | |
///////////// | |
///////////////// COLOR | |
const colorRotation = color1AnimParam * 360 - 180 | |
hotpink = hotpink.clone().spin(colorRotation) | |
skyblue = skyblue.clone().spin(colorRotation) | |
yellow = yellow.clone().spin(colorRotation) | |
teal = teal.clone().spin(colorRotation) | |
limegreen = limegreen.clone().spin(colorRotation) | |
const circle1Colors = discreteGradient( | |
this.circle1Range.length, | |
hotpink, skyblue, hotpink, | |
) | |
const circle2Colors = discreteGradient( | |
this.circle2Range.length, | |
skyblue, hotpink, yellow, hotpink, skyblue, | |
) | |
const circle3Colors = discreteGradient( | |
this.circle3Range.length, | |
teal, limegreen, yellow, limegreen, teal | |
) | |
const circle4Colors = discreteGradient( | |
this.circle4Range.length, | |
teal, limegreen, yellow, limegreen, teal | |
) | |
//////////////// | |
return ( | |
<div ref="container" style={{visibility: ready ? 'visible' : 'hidden', width:'100%', height:'100%', position: 'relative'}}> | |
<div className="rippleFlip" style={{ | |
position: 'absolute', | |
top: '0', | |
left: '0', | |
width:'100%', | |
height:'100%', | |
zIndex:'0' | |
}}> | |
{/* | |
<RippleFlip color={hotpink} /> | |
*/} | |
</div> | |
<div className="rotation" style={{ | |
position: 'absolute', | |
top: '0', | |
left: '0', | |
width:'100%', | |
height:'100%', | |
zIndex:'1' | |
}}> | |
<motor-scene ref="scene" webglenabled="true" background={`${colorToString(hotpink.clone().darken(38))} 1`} > | |
<motor-node ref='outer' id='outer' sizemode='proportional proportional' proportionalsize='1 1' > | |
<motor-node ref='circleRoot' position='0 0 -50'> | |
{/* outer tiny triangle ring */} | |
<motor-node ref='outerTinyTriangles' | |
position={`0 0 ${outerTrapezoidRingZPos}`} | |
rotation="0 0 90" | |
> | |
{this.circle1Range.map(n => ( | |
<motor-node | |
key={n} | |
rotation={`0 0 ${n * 360/48 + 360/48/2}`} | |
> | |
<motor-node | |
color={colorToString(limegreen)} | |
mesh='isotriangle' | |
absolutesize='4.6 4.6' | |
//position={`0 ${this.circle1Radius + 25} 0`} | |
position={`0 ${this.circle1Radius + 25} ${1 * ((circle1TrapezoidAudioDatum[n]-1) * 120 + 1)}`} | |
rotation='0 0 180' | |
> | |
</motor-node> | |
</motor-node> | |
))} | |
</motor-node> | |
{/*trapezoids*/} | |
<motor-node | |
ref='circle1' | |
position={`0 0 ${outerTrapezoidRingZPos}`} | |
//position={`0 0 ${outerTrapezoidRingZPosWithVibration}`} | |
rotation='0 0 90' | |
> | |
{this.circle1Range.map(n => ( | |
<motor-node | |
key={n} | |
rotation={`0 0 ${n * 360/48}`} | |
> | |
<motor-node | |
rotation={`0 ${individualQuadFlipRotations[n]} 0`} | |
> | |
<motor-node | |
color={colorToString(circle1Colors[n])} | |
//color={colorToString(hotpink)} | |
mesh='symtrap' | |
//absolutesize='10 16' | |
absolutesize={`10 ${16 * ((circle1TrapezoidAudioDatum[n]-1) * 5 + 1)}`} | |
position={`0 ${this.circle1Radius} 0`} | |
rotation='60 0 0' | |
> | |
</motor-node> | |
</motor-node> | |
</motor-node> | |
))} | |
</motor-node> | |
{/* inner tiny triangle ring */} | |
<motor-node ref='innerTinyTriangles' | |
position={`0 0 ${outerTrapezoidRingZPos}`} | |
rotation="0 0 90" | |
> | |
{this.circle2Range.map(n => ( | |
<motor-node | |
key={n} | |
rotation={`0 0 ${n * 360/24 + 360/24/2}`} | |
> | |
<motor-node | |
color={colorToString(limegreen.clone().setAlpha(1))} | |
mesh='isotriangle' | |
absolutesize='4.6 4.6' | |
//position={`0 ${this.circle1Radius + -10} 0`} | |
position={`0 ${this.circle1Radius + -10} ${1 * ((circle3QuadAudioDatum[n]-1) * 60 + 1)}`} | |
> | |
</motor-node> | |
</motor-node> | |
))} | |
</motor-node> | |
{/*triangle rings*/} | |
{this.circle2TriangleRings.map(t => { | |
const triangleRotation = columnTriangleRotation(t, triangleColumnAnimParam) + 60 | |
return ( | |
<motor-node key={t} position={`0 0 ${triangleRingPositions[t]}`} rotation='0 0 90'> | |
{this.circle2Range.map(n => ( | |
<motor-node | |
key={n} | |
rotation={`0 0 ${n * 360/24}`} | |
> | |
<motor-node | |
rotation={`${triangleRotation} 0 0`} | |
//rotation='60 0 0' | |
position={`0 ${this.circle2triangleRadii[t]} 0`} | |
//position={`0 ${this.circle2triangleRadii[t]} ${Math.abs(4 * ((circle3QuadAudioDatum[n]-1) * 5 + 1))}`} | |
absolutesize={`${this.innerTriangleSizes[t]} ${this.innerTriangleSizes[t] * 1.10} 0`} | |
mesh="isotriangle" | |
color={colorToString(circle2Colors[n])} | |
//color={colorToString(skyblue)} | |
> | |
</motor-node> | |
</motor-node> | |
))} | |
</motor-node> | |
) | |
})} | |
{/*little quads*/} | |
<motor-node ref='circle3' position={`0 0 ${innerQuadRingZPos}`} rotation='0 0 90'> | |
{this.circle3Range.map(n => ( | |
<motor-node key={n} rotation={`0 0 ${n * 360/24}`}> | |
<motor-node mesh='quad' | |
position={`0 ${this.circle3Radius} 0`} | |
//absolutesize='6 4' | |
absolutesize={`6 ${4 * ((circle3QuadAudioDatum[n]-1) * 5 + 1)}`} | |
color={colorToString(circle3Colors[n])} | |
//color={colorToString(yellow)} | |
> | |
</motor-node> | |
</motor-node> | |
))} | |
</motor-node> | |
{/* inner triangles*/} | |
<motor-node ref='circle4' rotation='0 0 -90' position={`0 0 ${innerQuadRingZPos}`}> | |
{this.circle4Range.map(n => ( | |
<motor-node key={n} rotation={`0 0 ${n * 360/12}`}> | |
<motor-node mesh='isotriangle' absolutesize='5 5' position={`0 ${this.circle4Radius} 0`} | |
color={colorToString(circle4Colors[n])} | |
//color={colorToString(teal)} | |
> | |
</motor-node> | |
</motor-node> | |
))} | |
</motor-node> | |
</motor-node> | |
</motor-node> | |
</motor-scene> | |
</div> | |
</div> | |
) | |
} | |
calcTriangleRingPositions(innerQuadRingZPos, outerTrapezoidRingZPos) { | |
const { | |
triangleRingPositions, | |
} = this | |
const zInterval = (outerTrapezoidRingZPos - innerQuadRingZPos) / 5 | |
let n = 5 | |
while (n--) | |
triangleRingPositions[n] = innerQuadRingZPos + zInterval/2 + n * zInterval | |
} | |
async componentDidMount() { | |
await sleep(1000) | |
this.showVisual() | |
} | |
async showVisual() { | |
const { state } = this | |
const { audioDataArray } = state | |
const { individualQuadFlipRotations } = this | |
const { container, circleRoot, outerTinyTriangles, innerTinyTriangles } = this.refs | |
let deviceOrientation1 = { x: 0, y: 0, z: 0, } | |
let deviceOrientation2 = { x: 0, y: 0, z: 0, } | |
let deviceOrientation3 = { x: 0, y: 0, z: 0, } | |
//this.receiveBroadcastOrientations(deviceOrientation1, deviceOrientation2, deviceOrientation3) | |
let mouseXRatio = 0 | |
let mouseYRatio = 0 | |
container.setAttribute('touch-action', 'none') // polyfill | |
container.style['touch-events'] = 'none' // native | |
container.addEventListener('mousemove', e => { | |
}) | |
container.addEventListener('pointermove', e => { | |
e.preventDefault() // just in case | |
mouseXRatio = e.clientX / window.innerWidth | |
mouseYRatio = e.clientY / window.innerHeight | |
}) | |
await circleRoot.mountPromise | |
const triangleColumnTween = new TWEEN.Tween(state) | |
triangleColumnTween.__done = false | |
triangleColumnTween.__started = false | |
triangleColumnTween | |
.to({triangleColumnAnimParam:1}, 2000) | |
//.to({p:-1}, 2000) | |
.easing(TWEEN.Easing.Cubic.InOut) | |
.onComplete(() => triangleColumnTween.__done = true) | |
.onStart(() => triangleColumnTween.__started = true) | |
.repeat(Infinity) | |
.yoyo(true) // how? | |
.start() | |
.update(performance.now()) // actually starts it. | |
Motor.addRenderTask(time => { | |
this.audioAnalyser.getByteTimeDomainData(audioDataArray) | |
circleRoot.rotation.x = mouseYRatio * 60 - 30; | |
circleRoot.rotation.y = mouseXRatio * 60 - 30; | |
if (triangleColumnTween.__started && !triangleColumnTween.__done) | |
triangleColumnTween.update(time) | |
this.setState({ | |
//color1AnimParam: deviceOrientation2.z / 360, | |
color1AnimParam: mouseXRatio, | |
//outerTrapezoidRingZPos: deviceOrientation3.y, | |
//innerQuadRingZPos: -deviceOrientation3.y, | |
outerTrapezoidRingZPos: mouseYRatio * 90 - 45, | |
innerQuadRingZPos: -mouseYRatio * 90 - 45, | |
ready: true, | |
}) | |
}) | |
} | |
startAudio() { | |
///////////////////////////////// AUDIO | |
const audio = new AudioContext | |
// make audio source node | |
const audioElement = document.createElement('audio') | |
audioElement.setAttribute('src', '/UnionMystica.mp3') | |
audioElement.setAttribute('autoplay', 'true') | |
document.body.appendChild(audioElement) | |
const source = audio.createMediaElementSource(audioElement) | |
// create an analyser node to analize the data, and connect source to | |
// it. We don't need to output to the AudioContext destination node, | |
// since it is already playing from the audio element. | |
this.audioAnalyser = audio.createAnalyser() | |
this.audioAnalyser.fftSize = 2048; // default 2048 | |
this.audioBufferLength = this.audioAnalyser.frequencyBinCount; | |
//this.audioBufferLength = this.audioAnalyser.fftSize; | |
source.connect(this.audioAnalyser) | |
// connect to the speakers | |
this.audioAnalyser.connect(audio.destination) | |
///////////////////////////////////////// | |
} | |
receiveBroadcastOrientations(deviceOrientation1, deviceOrientation2, deviceOrientation3) { | |
const streamer = new Meteor.Streamer('orientation') | |
streamer.on('orientation1', data => { | |
Object.assign(deviceOrientation1, data) | |
}) | |
streamer.on('error', e => {throw e}) | |
streamer.on('ready', () => console.log('streamer client ready')) | |
streamer.on('orientation2', data => { | |
Object.assign(deviceOrientation2, data) | |
}) | |
streamer.on('error', e => {throw e}) | |
streamer.on('ready', () => console.log('broadcast client ready')) | |
streamer.on('orientation3', data => { | |
Object.assign(deviceOrientation3, data) | |
}) | |
streamer.on('error', e => {throw e}) | |
streamer.on('ready', () => console.log('broadcast3 client ready')) | |
//////////////////////////////////////// | |
//const broadcast1 = new Meteor.Broadcast('orientation1') | |
//broadcast1.on('data', data => { | |
//Object.assign(deviceOrientation1, data) | |
//}) | |
//broadcast1.on('error', e => {throw e}) | |
//broadcast1.on('ready', () => console.log('broadcast1 client ready')) | |
//const broadcast2 = new Meteor.Broadcast('orientation2') | |
//broadcast2.on('data', data => { | |
//Object.assign(deviceOrientation2, data) | |
//}) | |
//broadcast2.on('error', e => {throw e}) | |
//broadcast2.on('ready', () => console.log('broadcast2 client ready')) | |
//const broadcast3 = new Meteor.Broadcast('orientation3') | |
//broadcast3.on('data', data => { | |
//Object.assign(deviceOrientation3, data) | |
//}) | |
//broadcast3.on('error', e => {throw e}) | |
//broadcast3.on('ready', () => console.log('broadcast3 client ready')) | |
} | |
} | |
function colorToString(color) { | |
color = color.toRgb() | |
return `${color.r/255} ${color.g/255} ${color.b/255} ${color.a}` | |
} | |
function columnTriangleRotation( | |
index, | |
animParam = 0, | |
numItems = 5, | |
startAngle = 0, | |
endAngle = 360 | |
) { | |
const interval = 1 / numItems | |
const startPoint = interval * index | |
const endPoint = startPoint + interval | |
let angle = 0 | |
if (animParam >= startPoint && animParam < endPoint) { | |
const intervalPortion = (animParam - startPoint) / interval | |
angle = (endAngle - startAngle) * intervalPortion + startAngle | |
} | |
else if (animParam < startPoint) { | |
angle = startAngle | |
} | |
else if (animParam >= endPoint) { | |
angle = endAngle | |
} | |
return angle | |
} | |
function getUserAudio() { | |
let resolve, reject | |
const promise = new Promise((res, rej) => {resolve = res; reject = rej}) | |
navigator.getUserMedia ( | |
{ audio: true }, | |
stream => resolve(stream), | |
err => reject(err), | |
) | |
return promise | |
} | |
const { | |
skyblue, | |
hotpink, | |
teal, | |
yellow, | |
limegreen, | |
} = colors | |
// XXX We can further improve perf by accepting an array to put values into. | |
// We can also cache the interval calculations of the conditional check in the | |
// inner loop. | |
function discreteGradient(n, ...colors) { | |
const numberOfColors = colors.length | |
const numberOfColorTransitions = numberOfColors - 1 | |
const interval = Math.floor(n / numberOfColorTransitions) | |
const discreteColors = [] | |
// for each discrete color that we will have | |
for (let i=0; i<n; i+=1) { | |
// see which color interval the the discrete color will fall in so we | |
// know which two colors to mix with `.mix()`. | |
for (let j=0; j<numberOfColorTransitions; j+=1) { | |
if (i >= j*interval && i < (j+1)*interval) { | |
discreteColors.push( | |
// mix this color with the next color by a certain percent | |
// based on the interval | |
color.mix(colors[j], colors[j+1], 100 / (interval - 1) * (i % interval)) | |
) | |
} | |
} | |
} | |
return discreteColors | |
} | |
// Is mapAudioDataToFewerUnits a good name for this? | |
function mapAudioDataToFewerUnits(audioDataArray, numberOfUnits) { | |
const newAudioDatum = [] | |
const audioDatumPerUnit = Math.floor(audioDataArray.length / numberOfUnits) | |
// normalize. (based off MDN tutorials, I'm guessing 128 is the max size of the values?). | |
for (let i=0; i<numberOfUnits; i+=1) { | |
let audioDatumSumForUnit = 0 | |
for (let j=i*audioDatumPerUnit, l2=j+audioDatumPerUnit; j<l2; j+=1) { | |
audioDatumSumForUnit += audioDataArray[j] / 128 / audioDatumPerUnit | |
} | |
newAudioDatum.push(audioDatumSumForUnit) | |
} | |
return newAudioDatum | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment