├── .gitignore ├── README.md ├── elm-package.json ├── webpack.config.js ├── package.json ├── index.html └── src ├── board-driver ├── index.js └── Board.elm ├── keyboard-driver └── index.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | elm-stuff 3 | elm.js 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Demo showing using Elm to draw stuff controlled by a Cycle.js driver 2 | 3 | Demo here: http://justinwoo.github.io/cycle-elm-etch-sketch 4 | 5 | I'll write up a post about this if there's interest (or I feel like it'd be fun to write) 6 | -------------------------------------------------------------------------------- /elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "." 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "elm-lang/core": "3.0.0 <= v < 4.0.0" 12 | }, 13 | "elm-version": "0.16.0 <= v < 0.17.0" 14 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: [ 6 | './src/index' 7 | ], 8 | output: { 9 | filename: 'build/index.js' 10 | }, 11 | module: { 12 | loaders: [{ 13 | test: /\.js$/, 14 | loaders: ['babel'], 15 | exclude: /node_modules/, 16 | include: __dirname 17 | }, { 18 | test: /\.elm$/, 19 | loaders: ['elm-simple-loader'], 20 | exclude: /node_modules/ 21 | }] 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cycleelmfun", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack -wdc --progress", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "@cycle/core": "^4.0.0", 14 | "@cycle/dom": "^6.0.0", 15 | "babel-core": "^5.8.25", 16 | "babel-loader": "^5.3.2", 17 | "elm-simple-loader": "^1.0.1", 18 | "webpack": "^1.12.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cycle-Elm fun 6 | 29 | 30 | 31 |
32 | Press Up/Down/Left/Right or H/J/K/L to move the cursor! Double click the screen to clear your picture! :) 33 |
34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/board-driver/index.js: -------------------------------------------------------------------------------- 1 | import Rx from 'rx'; 2 | 3 | import Elm from './Board.elm'; 4 | 5 | export default function makeBoardDriver() { 6 | return function boardDriver(model$) { 7 | let board; 8 | let screenClick$ = new Rx.Subject(); 9 | let requestScreenClear$ = screenClick$ 10 | .buffer(function() { 11 | return screenClick$.debounce(250); 12 | }) 13 | .map(function (events) { 14 | return events.length; 15 | }) 16 | .filter(function (clicks) { 17 | return clicks >= 2; 18 | }); 19 | 20 | model$.first().subscribe(function (model) { 21 | board = Elm.fullscreen(Elm.Board, { 22 | model 23 | }); 24 | 25 | board.ports.mouseClicks.subscribe(function () { 26 | screenClick$.onNext(); 27 | }); 28 | }); 29 | 30 | model$.subscribe(function (model) { 31 | board.ports.model.send(model); 32 | }); 33 | 34 | return { 35 | requestScreenClear$ 36 | }; 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/keyboard-driver/index.js: -------------------------------------------------------------------------------- 1 | import Rx from 'rx'; 2 | 3 | export const UP = 'up'; 4 | export const DOWN = 'down'; 5 | export const LEFT = 'left'; 6 | export const RIGHT = 'right'; 7 | 8 | const UP_INPUTS = [38, 75]; 9 | const DOWN_INPUTS = [40, 74]; 10 | const LEFT_INPUTS = [37, 72]; 11 | const RIGHT_INPUTS = [39, 76]; 12 | 13 | const MAPPINGS = [ 14 | [UP_INPUTS, UP], 15 | [DOWN_INPUTS, DOWN], 16 | [LEFT_INPUTS, LEFT], 17 | [RIGHT_INPUTS, RIGHT], 18 | ]; 19 | 20 | export default function makeKeyboardDriver() { 21 | return function keyboardDriver() { 22 | const directionInput$ = Rx.Observable.fromEvent(window, 'keydown') 23 | .map(function ({keyCode}) { 24 | for (let i = 0; i < MAPPINGS.length; i++) { 25 | const [inputs, direction] = MAPPINGS[i]; 26 | 27 | if (inputs.indexOf(keyCode) !== -1) { 28 | return direction; 29 | } 30 | } 31 | }); 32 | 33 | return { 34 | directionInput$ 35 | }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/board-driver/Board.elm: -------------------------------------------------------------------------------- 1 | module Board where 2 | 3 | import Color exposing (Color, rgb) 4 | import Graphics.Collage exposing (Form, collage, rect, filled, move) 5 | import Graphics.Element exposing (Element) 6 | import Window 7 | import Mouse 8 | 9 | -- CONSTANTS 10 | cellSize : Float 11 | cellSize = 16 12 | 13 | -- MODEL 14 | -- 2-dimensional coordinates here 15 | type alias Coordinates = (Int, Int) 16 | 17 | type alias Model = 18 | { 19 | points: List Coordinates, 20 | cursor: Coordinates 21 | } 22 | 23 | -- VIEW 24 | drawPoint : Color -> Coordinates -> Form 25 | drawPoint color coords = 26 | let 27 | (x, y) = coords 28 | in 29 | rect cellSize cellSize 30 | |> filled color 31 | |> move (toFloat x * cellSize, toFloat y * cellSize) 32 | 33 | view : (Int, Int) -> Model -> Element 34 | view (w, h) model = 35 | let 36 | points = 37 | List.map (drawPoint (rgb 50 50 50)) model.points 38 | cursor = 39 | drawPoint (rgb 0 0 0) model.cursor 40 | in 41 | List.append points [cursor] 42 | |> collage w h 43 | 44 | -- SIGNALS 45 | port mouseClicks : Signal () 46 | port mouseClicks = Mouse.clicks 47 | 48 | port model : Signal Model 49 | 50 | main : Signal Element 51 | main = 52 | Signal.map2 view Window.dimensions model 53 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Rx from 'rx'; 2 | import Cycle from '@cycle/core'; 3 | 4 | import makeBoardDriver from './board-driver'; 5 | import makeKeyboardDriver, { 6 | UP, DOWN, LEFT, RIGHT 7 | } from './keyboard-driver'; 8 | 9 | function deduplicatePoints(points) { 10 | let newPoints = []; 11 | const pointsObject = points.reduce(function (aggregate, point) { 12 | aggregate[point.join(',')] = point; 13 | return aggregate; 14 | }, {}); 15 | 16 | for (let key in pointsObject) { 17 | newPoints.push(pointsObject[key]); 18 | } 19 | 20 | return newPoints; 21 | } 22 | 23 | function addPoint(model) { 24 | let points = model.points.slice(); 25 | 26 | points.push(model.cursor); 27 | return points; 28 | } 29 | 30 | function main(drivers) { 31 | const INITIAL_STATE = { 32 | points: [], 33 | cursor: [0, 0] 34 | }; 35 | 36 | const moveCursor$ = drivers.keyboard.directionInput$ 37 | .map(function (direction) { 38 | return function (model) { 39 | if (!direction) return model; 40 | 41 | const points = deduplicatePoints(addPoint(model)); 42 | let [cursorX, cursorY] = model.cursor; 43 | 44 | switch (direction) { 45 | case UP: 46 | cursorY++; 47 | break; 48 | case DOWN: 49 | cursorY--; 50 | break; 51 | case LEFT: 52 | cursorX--; 53 | break; 54 | case RIGHT: 55 | cursorX++; 56 | break; 57 | } 58 | 59 | return { 60 | points, 61 | cursor: [cursorX, cursorY] 62 | }; 63 | }; 64 | }); 65 | 66 | const clearScreen$ = drivers.board.requestScreenClear$ 67 | .map(function () { 68 | return function (model) { 69 | return { 70 | points: [], 71 | cursor: model.cursor 72 | }; 73 | }; 74 | }); 75 | 76 | const state$ = Rx.Observable 77 | .merge( 78 | moveCursor$, 79 | clearScreen$ 80 | ) 81 | .startWith(INITIAL_STATE) 82 | .scan(function (state, mapper) { 83 | return mapper(state); 84 | }) 85 | 86 | return { 87 | board: state$ 88 | }; 89 | } 90 | 91 | let drivers = { 92 | keyboard: makeKeyboardDriver(), 93 | board: makeBoardDriver() 94 | }; 95 | 96 | Cycle.run(main, drivers); 97 | --------------------------------------------------------------------------------