UNPKG

react-native-blobular

Version:

The Man in Blue's awesome Blobular, translated to React Native.

979 lines (956 loc) 25.6 kB
// ,-----. ,--. ,--. ,--. // | |) /_ | | ,---. | |-. ,--.,--.| | ,--,--.,--.--. // | .-. \| || .-. || .-. '| || || |' ,-. || .--' // | '--' /| |' '-' '| `-' |' '' '| |\ '-' || | // `------' `--' `---' `---' `----' `--' `--`--'`--' // Created by Cameron Adams, 2007. @themaninblue <https://themaninblue.com> // Nodified by Alex Thomas, 2019. @cawfree <https://cawfree.com> const uuidv4 = require('uuid/v4'); const EVENT_TYPE_DRAG = 'event_drag'; const EVENT_TYPE_SEPARATE = 'event_separate'; const EVENT_TYPE_JOIN = 'event_join'; const EVENT_TYPE_JOIN_ALT = 'event_joinAlt'; class Blob { constructor(id, radius, viscosity, smallestRadius) { this.id = id; this.radius = radius; this.viscosity = viscosity; this.smallestRadius = smallestRadius; } getId() { return this.id; } getRadius() { return this.radius; } getViscosity() { return this.viscosity; } getSmallestRadius() { return this.smallestRadius; } } class Blobular { constructor( callback, ) { this.onPointerUp = this.onPointerUp.bind(this); this.onPointerMoved = this.onPointerMoved.bind(this); this.onPointerDown = this.onPointerDown.bind(this); this.putBlob = this.putBlob.bind(this); this.callback = callback; this.blobs = []; this.context = {}; this.eventListeners = {}; } onPointerDown(x, y) { const blob = this.__getBlobs() .map((e, i, arr) => (arr[arr.length - 1 - i])) .reduce( (b, p) => { const id = p .getId(); const context = this.__getContext()[id]; const { bigCircleR, bigCircleH, bigCircleK, } = context; const dx = x - bigCircleH; const dy = y - bigCircleK; const dist = Math .sqrt( Math.pow(dx, 2) + Math.pow(dy, 2) ); return b || ((dist <= bigCircleR) && p); }, null, ); if (blob) { const context = this.__getContext()[blob.getId()]; const { bigCircleH, bigCircleK, bigCircleR, } = context; const originDistance = Math.sqrt( Math.pow( x - bigCircleH, 2, ) + Math.pow( y - bigCircleK, 2, ), ); const smallCircleR = bigCircleR - originDistance; Object.assign( context, { bigCircleOriginH: bigCircleH, bigCircleOriginK: bigCircleK, originDistance, smallCircleR, pointerCoords: [ // mousedownCoords x, y, ], }, ); if (originDistance < 20) { this.__addEventListener( EVENT_TYPE_DRAG, blob, ); } else { const bigCircleArea = Math.PI * Math.pow( bigCircleR, 2, ); const smallCircleArea = Math.PI * Math.pow( smallCircleR, 2, ); const afterCircleArea = bigCircleArea - smallCircleArea; Object.assign( context, { bigCircleRMax: bigCircleR, bigCircleRMin: Math.sqrt( afterCircleArea / Math.PI, ), }, ); this.__addEventListener( EVENT_TYPE_SEPARATE, blob, ); } } } getCircleYForX(h, r, x) { return Math.sqrt( Math.pow( r, 2, ) - Math.pow( x - h, 2, ), ); } calculateAngle(origin, point) { const angle = Math.atan((point[1] - origin[1]) / (point[0] - origin[0])) / Math.PI * 180 + 90; return angle + ((point[0] < origin[0]) ? 180 : 0); } render(blob, distance, angle, mode) { // join, separation const context = this.__getContext()[blob.getId()]; const { bigCircleH, bigCircleK, } = context; Object.assign( context, { smallCircleK: - context.bigCircleRMax + context.smallCircleR - distance, }, ); if (mode === 'join') { Object.assign( context, { joinCircleRMin: 1, joinCircleRMax: 200, }, ); } else if (mode === 'separation') { Object.assign( context, { joinCircleR: blob.getViscosity(), }, ); } const startK = (mode === 'join') ? - context.bigCircleRMin - context.smallCircleR : - context.bigCircleRMax + context.smallCircleR - 1; const finalK = (mode === 'join') ? - context.bigCircleRMax + context.smallCircleR - 1: - context.bigCircleRMin - context.joinCircleR * 2 - context.smallCircleR; const differenceK = startK - finalK; const currDifferenceK = context.smallCircleK - finalK; const differencePercentage = currDifferenceK / differenceK; if (mode === 'join') { Object.assign( context, { bigCircleR: context.bigCircleRMax - (context.bigCircleRMax - context.bigCircleRMin) * differencePercentage, joinCircleR: context.joinCircleRMax - (context.joinCircleRMax - context.joinCircleRMin) * differencePercentage, }, ); } else if (mode === 'separation') { Object.assign( context, { bigCircleR: context.bigCircleRMin + (context.bigCircleRMax - context.bigCircleRMin) * differencePercentage, }, ); } const triangleA = context.bigCircleR + context.joinCircleR; const triangleB = context.smallCircleR + context.joinCircleR; const triangleC = Math.abs( context.smallCircleK, ); const triangleP = (triangleA + triangleB + triangleC) * 0.5; const e = (triangleP * (triangleP - triangleA) * (triangleP - triangleB) * (triangleP - triangleC)); const triangleArea = Math.sqrt(mode === 'join' ? e : Math.abs(e)); const isBigger = (triangleC >= triangleA); const triangleH = isBigger ? 2 * triangleArea / triangleC : 2 * triangleArea / triangleA; const triangleD = isBigger ? Math.sqrt(Math.pow(triangleA, 2) - Math.pow(triangleH, 2)) : Math.sqrt(Math.pow(triangleC, 2) - Math.pow(triangleH, 2)); const bigCircleTan = triangleH / triangleD; const bigCircleAngle = Math.atan(bigCircleTan); const bigCircleSin = Math.sin(bigCircleAngle); const bigCircleIntersectX = bigCircleSin * context.bigCircleR; const bigCircleCos = Math.cos(bigCircleAngle); const bigCircleIntersectY = bigCircleCos * context.bigCircleR; const joinCircleH = bigCircleSin * (context.bigCircleR + context.joinCircleR); const joinCircleK = -bigCircleCos * (context.bigCircleR + context.joinCircleR); const coord1X = -bigCircleIntersectX; const coord1Y = -bigCircleIntersectY; const coord2X = bigCircleIntersectX; const coord2Y = -bigCircleIntersectY; const smallCircleTan = (context.smallCircleK - joinCircleK) / (context.smallCircleH - joinCircleH); const smallCircleAngle = Math.atan(smallCircleTan); const smallCircleIntersectX = joinCircleH - Math.cos(smallCircleAngle) * (context.joinCircleR); const smallCircleIntersectY = joinCircleK - Math.sin(smallCircleAngle) * (context.joinCircleR); const x = joinCircleH - context.joinCircleR <= 0 && context.smallCircleK < joinCircleK; const crossOverY = this.getCircleYForX( joinCircleH, context.joinCircleR, 0, ); const largeArcFlag = (joinCircleK < context.smallCircleK) ? 0 : 1; const isOverlap = (joinCircleH - context.joinCircleR <= 0 && context.smallCircleK < joinCircleK); const path = [ "M " + coord1X + " " + coord1Y + " A " + context.bigCircleR + " " + context.bigCircleR + " 0 1 0 " + coord2X + " " + coord2Y, (!!x) && "A " + context.joinCircleR + " " + context.joinCircleR + " 0 0 1 0 " + (joinCircleK + crossOverY), (!!x) && "m 0 -" + (crossOverY * 2), "A " + context.joinCircleR + " " + context.joinCircleR + " 0 0 1 " + smallCircleIntersectX + " " + smallCircleIntersectY, "a " + context.smallCircleR + " " + context.smallCircleR + " 0 " + largeArcFlag + " 0 " + (smallCircleIntersectX * -2) + " 0", (!!isOverlap) && "A " + context.joinCircleR + " " + context.joinCircleR + " 0 0 1 0 " + (joinCircleK - crossOverY), (!!isOverlap) && "m 0 " + (crossOverY * 2), "A " + context.joinCircleR + " " + context.joinCircleR + " 0 0 1 " + coord1X + " " + coord1Y, "A " + context.joinCircleR + " " + context.joinCircleR + " 0 0 1 " + coord1X + " " + coord1Y, ] .filter(e => !!e) .join(); return this.__getCallback() .updateBlob( blob.getId(), [ bigCircleH, bigCircleK, ], angle, path, mode, ); } __addEventListener(eventType, blob) { const existing = this.__getEventListeners[eventType] || []; if (existing.indexOf(blob) < 0) { return this.__setEventListeners( { ...this.__getEventListeners(), [eventType]: [ ...existing, blob, ], }, ); } throw new Error( `Blob "${blob.getId()} is already configured to listen to the ${eventType} event!"`, ); } __removeEventListener(eventType, blob) { const existing = this.__getEventListeners()[eventType]; if (existing) { return this.__setEventListeners( { ...this.__getEventListeners(), [eventType]: existing .filter( e => (e.getId() !== blob.getId()), ), }, ); } throw new Error( `Attempted to unregister a listener for ${eventType}, when none have been allocated.`, ); } __onPointerMovedDrag(x, y, activeBlob) { const activeContext = this.__getContext()[activeBlob.getId()]; const { bigCircleOriginH, bigCircleOriginK, pointerCoords, } = activeContext; Object.assign( activeContext, { bigCircleH: bigCircleOriginH + x - pointerCoords[0], bigCircleK: bigCircleOriginK + y - pointerCoords[1], }, ); const { bigCircleH, bigCircleK, bigCircleR, } = activeContext; const otherBlobs = this.__getBlobs() .filter(e => e.getId() !== activeBlob.getId()); for (let i = 0; i < otherBlobs.length; i += 1) { const otherBlob = otherBlobs[i]; const otherContext = this.__getContext()[otherBlob.getId()]; const distance = Math.sqrt( Math.pow( bigCircleH - otherContext.bigCircleH, 2, ) + Math.pow( bigCircleK - otherContext.bigCircleK, 2, ), ); if (distance < bigCircleR + otherContext.bigCircleR) { const bigCircleArea = Math.PI * Math.pow( otherContext.bigCircleR, 2, ); const smallCircleArea = Math.PI * Math.pow( bigCircleR, 2, ); const afterCircleArea = bigCircleArea + smallCircleArea; if (bigCircleR < otherContext.bigCircleR) { Object.assign( otherContext, { bigCircleRMin: otherContext.bigCircleR, bigCircleRMax: Math.sqrt( afterCircleArea / Math.PI, ), smallCircleR: activeContext.bigCircleR, smallCircleOriginH: activeContext.bigCircleOriginH, smallCircleOriginK: activeContext.bigCircleOriginK, pointerCoords, }, ); const distanceDiff = Math.max( distance - otherContext.bigCircleRMax + otherContext.smallCircleR, 1, ); this.render( otherBlob, distanceDiff, this.calculateAngle( [ otherContext.bigCircleH, otherContext.bigCircleK, ], [ activeContext.bigCircleH, activeContext.bigCircleK, ], ), 'join', ); this.__addEventListener( EVENT_TYPE_JOIN, // TODO needs to exist otherBlob, ); } else { Object.assign( otherContext, { bigCircleRMin: activeContext.bigCircleR, bigCircleRMax: Math.sqrt( afterCircleArea / Math.PI, ), smallCircleR: otherContext.bigCircleR, smallCircleOriginH: otherContext.bigCircleH, smallCircleOriginK: otherContext.bigCircleK, bigCircleR: activeContext.bigCircleR, bigCircleH: activeContext.bigCircleH, bigCircleK: activeContext.bigCircleK, bigCircleOriginH: activeContext.bigCircleOriginH, bigCircleOriginK: activeContext.bigCircleOriginK, pointerCoords, }, ); const distanceDiff = Math.max( distance - otherContext.bigCircleRMax + otherContext.smallCircleR, 1, ); this.render( otherBlob, distanceDiff, this.calculateAngle( [ otherContext.bigCircleH, otherContext.bigCircleK, ], [ otherContext.smallCircleOriginH, otherContext.smallCircleOriginK, ], ), 'join', ); this.__addEventListener( EVENT_TYPE_JOIN_ALT, otherBlob, ); } this.__shouldDeleteBlob( activeBlob, ); return; } } this.__doReset(activeBlob); } __onPointerMovedSeparate(x, y, activeBlob) { const activeContext = this.__getContext()[activeBlob.getId()]; const distance = Math.sqrt( Math.pow( x - activeContext.bigCircleH, 2, ) + Math.pow( y - activeContext.bigCircleK, 2, ), ); if (distance > activeContext.bigCircleR + activeContext.joinCircleR * 2 + activeContext.smallCircleR) { const detached = new Blob( uuidv4(), activeContext.smallCircleR, activeBlob.getViscosity(), activeBlob.getSmallestRadius(), ); this.putBlob( detached, x, y, ); this.__addEventListener( EVENT_TYPE_DRAG, detached, ); this.__removeEventListener( EVENT_TYPE_SEPARATE, activeBlob, ); Object.assign( activeContext, { bigCircleR: activeContext.bigCircleRMin, }, ); this.__doReset( activeBlob, ); } else { const distanceDiff = Math.max(distance - activeContext.originDistance, 1); this.render( activeBlob, distanceDiff, this.calculateAngle( [ activeContext.bigCircleH, activeContext.bigCircleK, ], [ x, y, ], ), 'separation', ); } } __onPointerMovedJoin(x, y, blob) { const context = this.__getContext()[blob.getId()]; const distance = Math.sqrt( Math.pow( context.smallCircleOriginH + x - context.pointerCoords[0] - context.bigCircleH, 2, ) + Math.pow( context.smallCircleOriginK + y - context.pointerCoords[1] - context.bigCircleK, 2, ), ); if (distance > context.bigCircleRMin + context.smallCircleR) { const detached = new Blob( uuidv4(), context.smallCircleR, blob.getViscosity(), blob.getSmallestRadius(), ); this.putBlob( detached, x, y, ); this.__addEventListener( EVENT_TYPE_DRAG, detached, ); this.__removeEventListener( EVENT_TYPE_JOIN, blob, ); Object.assign( context, { bigCircleR: context.bigCircleRMin, }, ); this.__doReset( blob, ); } else { const distanceDiff = Math.max( distance - context.bigCircleRMax + context.smallCircleR, 1, ); this.render( blob, distanceDiff, this.calculateAngle( [ context.bigCircleH, context.bigCircleK, ], [ context.smallCircleOriginH + x - context.pointerCoords[0], context.smallCircleOriginK + y - context.pointerCoords[1], ], ), 'join', ); } } __onPointerMovedJoinAlt(x, y, blob) { const context = this.__getContext()[blob.getId()]; Object.assign( context, { bigCircleH: context.bigCircleOriginH + x - context.pointerCoords[0], bigCircleK: context.bigCircleOriginK + y - context.pointerCoords[1], }, ); const distance = Math.sqrt( Math.pow( context.bigCircleH - context.smallCircleOriginH, 2, ) + Math.pow( context.bigCircleK - context.smallCircleOriginK, 2, ), ); if (distance > context.bigCircleRMin + context.smallCircleR) { const detached = new Blob( uuidv4(), context.smallCircleR, blob.getViscosity(), blob.getSmallestRadius(), ); this.putBlob( detached, context.smallCircleOriginH, context.smallCircleOriginK, ); this.__addEventListener( EVENT_TYPE_DRAG, blob, ); this.__removeEventListener( EVENT_TYPE_JOIN_ALT, blob, ); Object.assign( context, { bigCircleR: context.bigCircleRMin, }, ); this.__doReset( blob, ); } else { const distanceDiff = Math.max( distance - context.bigCircleRMax + context.smallCircleR, 1, ); this.render( blob, distanceDiff, this.calculateAngle( [ context.bigCircleH, context.bigCircleK, ], [ context.smallCircleOriginH, context.smallCircleOriginK, ], ), 'join', ); } } onPointerMoved(x, y) { const eventListeners = this.__getEventListeners(); (eventListeners[EVENT_TYPE_DRAG] || []) .map( (blob) => { this.__onPointerMovedDrag( x, y, blob, ); }, ); (eventListeners[EVENT_TYPE_SEPARATE] || []) .map( (blob) => { this.__onPointerMovedSeparate( x, y, blob, ); }, ); (eventListeners[EVENT_TYPE_JOIN] || []) .map( (blob) => { this.__onPointerMovedJoin( x, y, blob, ); }, ); (eventListeners[EVENT_TYPE_JOIN_ALT] || []) .map( (blob) => { this.__onPointerMovedJoinAlt( x, y, blob, ); }, ); } __doReset(activeBlob) { const activeContext = this.__getContext()[activeBlob.getId()]; const { transform, path, } = this.__getResetData( activeBlob.getId(), activeContext, ); this.__getCallback() .updateBlob( activeBlob.getId(), transform, null, path, undefined, ); } __shouldDeleteBlob(blob) { this.__setBlobs( this.__getBlobs() .filter( e => (e.getId() !== blob.getId()), ), ); Object.assign( this.__getContext(), { [blob.getId()]: null, }, ); this.__setEventListeners( Object.entries(this.__getEventListeners()) .reduce( (obj, [eventType, listeners]) => { return { ...obj, [eventType]: listeners .filter( e => (e.getId() !== blob.getId()), ), }; }, {}, ), ); this.__getCallback() .deleteBlob( blob .getId(), ); } __onPointerUpDrag(x, y, blob) { this.__removeEventListener( EVENT_TYPE_DRAG, blob, ); } __onPointerUpSeparate(x, y, blob) { this.__collapse( x, y, blob, ); this.__removeEventListener( EVENT_TYPE_SEPARATE, blob, ); } __onPointerUpJoin(x, y, blob) { this.__join( x, y, blob, ); this.__removeEventListener( EVENT_TYPE_JOIN, blob, ); } __onPointerUpJoinAlt(x, y, blob) { const context = this.__getContext()[blob.getId()]; this.__join( context.smallCircleOriginH, context.smallCircleOriginK, blob, ); this.__removeEventListener( EVENT_TYPE_JOIN_ALT, blob, ); } // TODO: provide the callback with an iterator __join(x, y, blob) { const context = this.__getContext()[blob.getId()]; const increment = 20; const newK = context.smallCircleK + increment; if (newK > -context.bigCircleR + context.smallCircleR - 1) { const { bigCircleRMax } = context; Object.assign( context, { bigCircleR: bigCircleRMax, }, ); this.__doReset( blob, ); } else { const distance = -newK - (context.bigCircleRMax - context.smallCircleR); const angle = this.calculateAngle( [ context.bigCircleH, context.bigCircleK, ], [ x, y, ], ); this.render( blob, distance, angle, 'join', ); setTimeout( () => this.__join(x, y, blob), 1, ); } } __collapse(x, y, blob) { const context = this.__getContext()[blob.getId()]; const increment = blob.getViscosity() / 4; const newK = context.smallCircleK + increment; if (newK > -context.bigCircleR + context.smallCircleR - 1) { const { bigCircleRMax } = context; Object.assign( context, { bigCircleR: bigCircleRMax, }, ); this.__doReset( blob, ); } else { const distance = -newK - (context.bigCircleRMax - context.smallCircleR); const angle = this.calculateAngle( [ context.bigCircleH, context.bigCircleK, ], [ x, y, ], ); this.render( blob, distance, angle, 'separation', ); setTimeout( () => this.__collapse(x, y, blob), 1, ); } } onPointerUp(x, y) { const eventListeners = this.__getEventListeners(); (eventListeners[EVENT_TYPE_DRAG] || []) .map( blob => this.__onPointerUpDrag(x, y, blob), ); (eventListeners[EVENT_TYPE_SEPARATE] || []) .map( blob => this.__onPointerUpSeparate(x, y, blob), ); (eventListeners[EVENT_TYPE_JOIN] || []) .map( blob => this.__onPointerUpJoin(x, y, blob), ); (eventListeners[EVENT_TYPE_JOIN_ALT] || []) .map( blob => this.__onPointerUpJoinAlt(x, y, blob), ); } putBlob(blob, x, y) { const id = blob .getId(); if (typeof id !== 'string') { throw new Error( `Expected string id, found ${typeof id}.`, ); } const available = this.__getBlobs() .reduce( (result, blob) => result && ( blob.getId() !== id ), true, ); if (available) { this.__setBlobs( [ ...this.__getBlobs(), blob, ], ); const blobContext = this.__createBlobContext( blob, x, y, ); this.__setContext( { ...this.__getContext(), [id]: blobContext, }, ); const { transform, path, } = this.__getResetData( id, blobContext, ); const { createBlob } = this.__getCallback(); return createBlob( id, transform, path, ); } throw new Error( `Attempted to allocate a blob with an existing identifier, "${id}".`, ); } __setBlobs(blobs) { this.blobs = blobs; } __getBlobs() { return this.blobs; } __createBlobContext(blob, x, y) { return { bigCircleR: blob.getRadius(), bigCircleH: x, bigCircleK: y, bigCircleOriginH: x, bigCircleOriginK: y, joinCircleR: blob.getViscosity(), smallCircleR: blob.getSmallestRadius(), smallCircleH: 0, smallCircleK: - blob.getRadius() + blob.getSmallestRadius() - 1, pointerCoords: [ x, y, ], }; } __getResetData(blobId, blobContext) { const { bigCircleH, bigCircleK, bigCircleR, } = blobContext; const transform = [ bigCircleH, bigCircleK, ]; const path = [ `m 0 ${-bigCircleR} A ${bigCircleR} ${bigCircleR} 0 1 1 0 ${bigCircleR}`, `A ${bigCircleR} ${bigCircleR} 0 1 1 0 ${-bigCircleR}`, ] .join(''); return { transform, path, }; } __setContext(context) { this.context = context; } __getContext() { return this.context; } __getCallback() { return this.callback; } __setEventListeners(eventListeners) { this.eventListeners = eventListeners; } __getEventListeners() { return this.eventListeners; } } module.exports = { default: Blobular, Blobular, Blob, };