Skip to content

Instantly share code, notes, and snippets.

@appsforartists
Created November 24, 2016 06:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save appsforartists/39b83156dee562636d5ddcec200569eb to your computer and use it in GitHub Desktop.
Save appsforartists/39b83156dee562636d5ddcec200569eb to your computer and use it in GitHub Desktop.
Attempting drag in Cycle.js
/** @license
* Copyright 2016 - present The Material Motion Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy
* of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import pairwise from 'xstream/extra/pairwise'
import sampleCombine from 'xstream/extra/sampleCombine'
import { DOMSource } from '@cycle/dom/rxjs-typings';
import xs, { Stream } from 'xstream'
import { VNode } from '@cycle/dom';
import { html } from 'snabbdom-jsx';
export type Sources = {
DOM: DOMSource
}
export type Sinks = {
DOM: Stream<VNode>
}
// helpers moved to bottom of file for brevity
export function App(sources: Sources): Sinks {
const drag$ = getDrag$FromDOMSource(
sources.DOM.select('.draggable')
);
// This looks like the right way to implement mutual observation according to
// http://staltz.com/xstream/#imitate but it isn't working. Moreover, if I
// even subscribe to location$Proxy, vtree$ stops dispatching.
const location$Proxy = xs.create();
const location$ = xs.merge(
xs.of(
{
x: 0,
y: 0,
}
),
drag$.compose(
sampleCombine(location$Proxy)
).map(
addPoints
)
);
location$Proxy.imitate(location$);
const vtree$ = location$.map(
location => (
<div
className = 'draggable'
style = {
{
backgroundColor: '#8BC34A',
width: '56px',
height: '56px',
borderRadius: '28px',
boxShadow: `
0 3px 1px -2px rgba(0, 0, 0, 0.2),
0 2px 2px 0 rgba(0, 0, 0, 0.14),
0 1px 5px 0 rgba(0, 0, 0, 0.12)
`,
willChange: 'transform',
transform: `translate(${ location.x }px, ${ location.y }px)`,
}
}
/>
)
);
const sinks = {
DOM: vtree$
};
return sinks;
}
function getPointerLocationFromEvent(event: PointerEvent) {
return {
x: event.pageX,
y: event.pageY,
};
}
function getDistanceBetweenPoints([prev, next]) {
if (!prev || !next) {
return {
x: 0,
y: 0,
};
}
return {
x: next.x - prev.x,
y: next.y - prev.y,
};
}
function addPoints([prev, next]) {
if (!prev || !next) {
return {
x: 0,
y: 0,
};
}
return {
x: next.x + prev.x,
y: next.y + prev.y,
};
}
function getDrag$FromDOMSource(domSource) {
return domSource.events('pointerdown').map(
downEvent => {
downEvent.target.setPointerCapture(downEvent.pointerId);
return domSource.events('pointermove').endWhen(
domSource.events('pointerup').take()
).startWith(
downEvent
).map(
getPointerLocationFromEvent
).compose(pairwise).map(
getDistanceBetweenPoints
)
}
).flatten();
}
@appsforartists
Copy link
Author

It's because merge isn't a memorystream, so sampleCombine never gets the first point.

This is ugly, but it works:

export function App(sources: Sources): Sinks {
  const drag$ = getDrag$FromDOMSource(
    sources.DOM.select('.draggable')
  );

  const emptyPoint = xs.of(
      {
        x: 0,
        y: 0,
      }
    );

  const location$Proxy = xs.create();
  const location$ = xs.merge(
    emptyPoint,

    drag$.compose(
      sampleCombine(xs.merge(emptyPoint, location$Proxy))
    ).map(
      addPoints
    )
  );

  location$Proxy.imitate(location$);

  const vtree$ = location$.map(
    location => (
      <div
        className = 'draggable'
        style = {
          {
            backgroundColor: '#8BC34A',
            width: '56px',
            height: '56px',
            borderRadius: '28px',
            boxShadow: `
              0 3px 1px -2px rgba(0, 0, 0, 0.2),
              0 2px 2px 0 rgba(0, 0, 0, 0.14),
              0 1px 5px 0 rgba(0, 0, 0, 0.12)
            `,
            willChange: 'transform',
            transform: `translate(${ location.x }px, ${ location.y }px)`,
          }
        }
      />
    )
  );

  const sinks = {
    DOM: vtree$
  };

  return sinks;
}

@appsforartists
Copy link
Author

appsforartists commented Nov 24, 2016

And the cleaner solution:

export function App(sources: Sources): Sinks {
  const drag$ = getDrag$FromDOMSource(
    sources.DOM.select('.draggable')
  );

  // drags are relative to the last known location, but location$ doesn't exist
  // yet, so we define it in terms of a proxy that will be filled later with a
  // stream of (drag + last location).
  //
  // Proxying drag instead of location, because proxy.imitate doesn't work with
  // memory streams like startWith
  const combinedDrag$Proxy = xs.create();
  const location$ = xs.merge(
    combinedDrag$Proxy
  ).startWith(
    {
      x: 0,
      y: 0,
    }
  );

  combinedDrag$Proxy.imitate(
    drag$.compose(
      sampleCombine(location$)
    ).map(
      addPoints
    )
  );

  const vtree$ = location$.map(
    location => (
      <div
        className = 'draggable'
        style = {
          {
            backgroundColor: '#8BC34A',
            width: '56px',
            height: '56px',
            borderRadius: '28px',
            boxShadow: `
              0 3px 1px -2px rgba(0, 0, 0, 0.2),
              0 2px 2px 0 rgba(0, 0, 0, 0.14),
              0 1px 5px 0 rgba(0, 0, 0, 0.12)
            `,
            willChange: 'transform',
            transform: `translate(${ location.x }px, ${ location.y }px)`,
          }
        }
      />
    )
  );

  const sinks = {
    DOM: vtree$
  };

  return sinks;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment