├── .gitignore ├── screenshot.gif ├── spec ├── fixtures │ ├── sample.sass │ ├── sample.styl │ ├── sample.less │ ├── sample.scss │ └── sample.css ├── spec-helper.coffee ├── bezier-curve-editor-spec.coffee └── bezier-curve-editor-element-spec.coffee ├── resources ├── css-easing.woff ├── linear.svg ├── ease.svg ├── ease-in.svg ├── ease-in-out.svg └── ease-out.svg ├── README.md ├── keymaps └── bezier-curve-editor.cson ├── package.json ├── lib ├── bezier-timing-element.coffee ├── curve-control-element.coffee ├── bezier-functions.coffee ├── bezier-curve-editor.coffee ├── bezier-curve-editor-element.coffee └── curve-element.coffee ├── LICENSE.md ├── styles └── bezier-curve-editor.less └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abe33/atom-bezier-curve-editor/HEAD/screenshot.gif -------------------------------------------------------------------------------- /spec/fixtures/sample.sass: -------------------------------------------------------------------------------- 1 | 2 | .foo 3 | transition: all 0.3s cubic-bezier(0.35, 0.77, 0.62, 0.28) 4 | -------------------------------------------------------------------------------- /spec/fixtures/sample.styl: -------------------------------------------------------------------------------- 1 | 2 | .foo 3 | transition all 0.3s cubic-bezier(0.35, 0.77, 0.62, 0.28) 4 | -------------------------------------------------------------------------------- /spec/fixtures/sample.less: -------------------------------------------------------------------------------- 1 | 2 | .foo { 3 | transition: all 0.3s cubic-bezier(0.35, 0.77, 0.62, 0.28); 4 | } 5 | -------------------------------------------------------------------------------- /spec/fixtures/sample.scss: -------------------------------------------------------------------------------- 1 | 2 | .foo { 3 | transition: all 0.3s cubic-bezier(0.35, 0.77, 0.62, 0.28); 4 | } 5 | -------------------------------------------------------------------------------- /resources/css-easing.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abe33/atom-bezier-curve-editor/HEAD/resources/css-easing.woff -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bezier-curve-editor package 2 | 3 | Edit CSS Bezier curves directly in Atom. Either right click on a `cubic-bezier` declaration or trigger `ctrl + alt + B` when the cursor is in a declaration. 4 | 5 | ![Editor Demo](https://github.com/abe33/atom-bezier-curve-editor/blob/master/screenshot.gif?raw=true) 6 | -------------------------------------------------------------------------------- /keymaps/bezier-curve-editor.cson: -------------------------------------------------------------------------------- 1 | # Keybindings require three things to be fully defined: A selector that is 2 | # matched against the focused element, the keystroke and the command to 3 | # execute. 4 | # 5 | # Below is a basic keybinding which registers on all platforms by applying to 6 | # the root workspace element. 7 | 8 | # For more detailed documentation see 9 | # https://atom.io/docs/latest/advanced/keymaps 10 | 'atom-workspace': 11 | 'ctrl-alt-b': 'bezier-curve-editor:open' 12 | -------------------------------------------------------------------------------- /spec/fixtures/sample.css: -------------------------------------------------------------------------------- 1 | 2 | .foo { 3 | transition: all 0.3s cubic-bezier(0.35, 0.77, 0.62, 0.28); 4 | } 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | .foo { 56 | transition: all 0.3s cubic-bezier(0.35, 0.77, 0.62, 0.28); 57 | } 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bezier-curve-editor", 3 | "main": "./lib/bezier-curve-editor", 4 | "version": "0.7.2", 5 | "description": "A bezier curve editor with common curves built-in.", 6 | "repository": "https://github.com/abe33/atom-bezier-curve-editor", 7 | "license": "MIT", 8 | "engines": { 9 | "atom": ">0.50.0" 10 | }, 11 | "dependencies": { 12 | "delegato": "1.x", 13 | "mixto": "1.x", 14 | "emissary": "1.x", 15 | "prolix": "1.x", 16 | "atom-utils": "^0.5.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /spec/spec-helper.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | buildMouseEvent: (type, properties={}) -> 3 | defaults = {bubbles: true, cancelable: true} 4 | defaults[k] = v for k,v of properties 5 | properties.detail ?= 1 6 | event = new MouseEvent(type, properties) 7 | Object.defineProperty(event, 'which', get: -> properties.which) if properties.which? 8 | if properties.target? 9 | Object.defineProperty(event, 'target', get: -> properties.target) 10 | Object.defineProperty(event, 'srcObject', get: -> properties.target) 11 | event 12 | -------------------------------------------------------------------------------- /resources/linear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /resources/ease.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /resources/ease-in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /resources/ease-in-out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /resources/ease-out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /lib/bezier-timing-element.coffee: -------------------------------------------------------------------------------- 1 | {SpacePenDSL, EventsDelegation} = require 'atom-utils' 2 | 3 | module.exports = 4 | class BezierTimingElement extends HTMLElement 5 | SpacePenDSL.includeInto(this) 6 | 7 | @content: -> 8 | @div class: 'bezier-timing-preview', => 9 | @div class: 'bezier-timing-ramp', => 10 | @div outlet: 'dummy', class: 'bezier-timing-dummy' 11 | 12 | duration: 500 13 | 14 | setSpline: (@spline...) -> @updateTiming() 15 | 16 | setDuration: (@duration) -> @updateTiming() 17 | 18 | updateTiming: -> 19 | @dummy.style.cssText = '' 20 | setTimeout => 21 | @dummy.style.webkitAnimation = "preview #{@duration / 1000}s cubic-bezier(#{@spline.join ', '}) alternate both infinite" 22 | , 100 23 | 24 | getModel: -> {} 25 | 26 | module.exports = BezierTimingElement = document.registerElement 'bezier-curve-timing', prototype: BezierTimingElement.prototype 27 | -------------------------------------------------------------------------------- /spec/bezier-curve-editor-spec.coffee: -------------------------------------------------------------------------------- 1 | BezierCurveEditor = require '../lib/bezier-curve-editor' 2 | 3 | describe "BezierCurveEditor", -> 4 | [workspaceElement, editor, editorView] = [] 5 | 6 | afterEach -> BezierCurveEditor.deactivate() 7 | 8 | beforeEach -> 9 | waitsForPromise -> atom.workspace.open('sample.js') 10 | 11 | runs -> 12 | workspaceElement = atom.views.getView(atom.workspace) 13 | editor = atom.workspace.getActiveTextEditor() 14 | editorView = atom.views.getView(editor) 15 | 16 | editor.setText("cubic-bezier(0.3, 0, 0.7, 1)") 17 | editor.setCursorBufferPosition([4,0]) 18 | 19 | waitsForPromise -> 20 | atom.packages.activatePackage('bezier-curve-editor') 21 | 22 | describe 'triggering the open command', -> 23 | beforeEach -> 24 | runs -> 25 | atom.commands.dispatch workspaceElement, 'bezier-curve-editor:open' 26 | 27 | it 'should have opened the view', -> 28 | expect(editorView.querySelector('.bezier-curve-editor')).toBeDefined() 29 | -------------------------------------------------------------------------------- /spec/bezier-curve-editor-element-spec.coffee: -------------------------------------------------------------------------------- 1 | {buildMouseEvent} = require './spec-helper' 2 | 3 | describe "BezierCurveEditorElement", -> 4 | [workspaceElement, editor, editorView] = [] 5 | 6 | beforeEach -> 7 | waitsForPromise -> atom.workspace.open('sample.js') 8 | 9 | runs -> 10 | workspaceElement = atom.views.getView(atom.workspace) 11 | editor = atom.workspace.getActiveTextEditor() 12 | editorView = atom.views.getView(editor) 13 | 14 | editor.setText("cubic-bezier(0.3, 0, 0.7, 1)") 15 | editor.setCursorBufferPosition([4,0]) 16 | 17 | waitsForPromise -> 18 | atom.packages.activatePackage('bezier-curve-editor') 19 | 20 | runs -> 21 | atom.commands.dispatch(editorView, 'bezier-curve-editor:open') 22 | 23 | describe 'clicking outside the panel', -> 24 | beforeEach -> 25 | runs -> 26 | editorView.dispatchEvent(buildMouseEvent('mousedown')) 27 | 28 | it 'should have removed the view', -> 29 | expect(editorView.querySelector('bezier-curve-editor')).toBeDefined() 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/curve-control-element.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable, Emitter} = require 'atom' 2 | {EventsDelegation} = require 'atom-utils' 3 | 4 | module.exports = 5 | class CurveControlElement extends HTMLElement 6 | EventsDelegation.includeInto(this) 7 | 8 | createdCallback: -> 9 | @emitter = new Emitter 10 | @subscriptions = new CompositeDisposable 11 | 12 | initialize: ({@minX, @maxX, @minY, @maxY}) -> 13 | 14 | onDidStartDrag: (callback) -> 15 | @emitter.on 'did-start-drag', callback 16 | 17 | onDidDrag: (callback) -> 18 | @emitter.on 'did-drag', callback 19 | 20 | onDidEndDrag: (callback) -> 21 | @emitter.on 'did-end-drag', callback 22 | 23 | activate: -> 24 | @subscriptions.add @subscribeTo this, 'mousedown': (e) => 25 | @onMouseDown(e) 26 | 27 | @subscriptions.add @subscribeTo document.body, 28 | 'mousemove': (e) => @onMouseMove(e) 29 | 'mouseup': (e) => @onMouseUp(e) 30 | 31 | deactivate: -> 32 | @subscriptions.dispose() 33 | 34 | destroy: -> 35 | @deactivate() 36 | @detach() 37 | 38 | onMouseDown: (e) -> 39 | @dragging = true 40 | @emitter.emit('did-start-drag', @getMousePosition(e)) 41 | 42 | onMouseMove: (e) -> 43 | if @dragging 44 | @emitter.emit('did-drag', @getMousePosition(e)) 45 | 46 | onMouseUp: (e) -> 47 | @emitter.emit('did-end-drag', @getMousePosition(e)) if @dragging 48 | @dragging = false 49 | 50 | getMousePosition: (e) -> 51 | offset = @parentNode.getBoundingClientRect() 52 | x = e.pageX - offset.left 53 | y = e.pageY - offset.top 54 | difX = @maxX - @minX 55 | difY = @maxY - @minY 56 | 57 | x = Math.max(@minX, Math.min(@maxX, x)) 58 | 59 | { 60 | x: (x - @minX) / difX 61 | y: 1 - (y - @minY) / difY 62 | } 63 | 64 | module.exports = CurveControlElement = document.registerElement 'bezier-curve-control', prototype: CurveControlElement.prototype 65 | -------------------------------------------------------------------------------- /lib/bezier-functions.coffee: -------------------------------------------------------------------------------- 1 | class module.exports.UnitBezier 2 | constructor: (p1x, p1y, p2x, p2y) -> 3 | 4 | # pre-calculate the polynomial coefficients 5 | # First and last control points are implied to be (0,0) and (1.0, 1.0) 6 | @cx = 3.0 * p1x 7 | @bx = 3.0 * (p2x - p1x) - @cx 8 | @ax = 1.0 - @cx - @bx 9 | @cy = 3.0 * p1y 10 | @by = 3.0 * (p2y - p1y) - @cy 11 | @ay = 1.0 - @cy - @by 12 | epsilon: 1e-6 # Precision 13 | sampleCurveX: (t) -> 14 | ((@ax * t + @bx) * t + @cx) * t 15 | 16 | sampleCurveY: (t) -> 17 | ((@ay * t + @by) * t + @cy) * t 18 | 19 | sampleCurveDerivativeX: (t) -> 20 | (3.0 * @ax * t + 2.0 * @bx) * t + @cx 21 | 22 | solveCurveX: (x, epsilon) -> 23 | t0 = undefined 24 | t1 = undefined 25 | t2 = undefined 26 | x2 = undefined 27 | d2 = undefined 28 | i = undefined 29 | 30 | # First try a few iterations of Newton's method -- normally very fast. 31 | t2 = x 32 | i = 0 33 | 34 | while i < 8 35 | x2 = @sampleCurveX(t2) - x 36 | return t2 if Math.abs(x2) < epsilon 37 | d2 = @sampleCurveDerivativeX(t2) 38 | break if Math.abs(d2) < epsilon 39 | t2 = t2 - x2 / d2 40 | i++ 41 | 42 | # No solution found - use bi-section 43 | t0 = 0.0 44 | t1 = 1.0 45 | t2 = x 46 | return t0 if t2 < t0 47 | return t1 if t2 > t1 48 | while t0 < t1 49 | x2 = @sampleCurveX(t2) 50 | return t2 if Math.abs(x2 - x) < epsilon 51 | if x > x2 52 | t0 = t2 53 | else 54 | t1 = t2 55 | t2 = (t1 - t0) * .5 + t0 56 | 57 | # Give up 58 | t2 59 | 60 | 61 | # Find new T as a function of Y along curve X 62 | solve: (x, epsilon) -> 63 | @sampleCurveY @solveCurveX(x, epsilon) 64 | 65 | module.exports.easing = 66 | ease: [0.25, 0.1, 0.25, 1.0] 67 | linear: [0.00, 0.0, 1.00, 1.0] 68 | ease_in: [0.42, 0.0, 1.00, 1.0] 69 | ease_out: [0.00, 0.0, 0.58, 1.0] 70 | ease_in_out: [0.42, 0.0, 0.58, 1.0] 71 | 72 | module.exports.extraEasing = 73 | cubic: 74 | ease_in: [0.550, 0.055, 0.675, 0.190] 75 | ease_out: [0.215, 0.610, 0.355, 1.000] 76 | ease_in_out: [0.645, 0.045, 0.355, 1.000] 77 | 78 | circ: 79 | ease_in: [0.600, 0.040, 0.980, 0.335] 80 | ease_out: [0.075, 0.820, 0.165, 1.000] 81 | ease_in_out: [0.785, 0.135, 0.150, 0.860] 82 | 83 | expo: 84 | ease_in: [0.950, 0.050, 0.795, 0.035] 85 | ease_out: [0.190, 1.000, 0.220, 1.000] 86 | ease_in_out: [1.000, 0.000, 0.000, 1.000] 87 | 88 | quad: 89 | ease_in: [0.550, 0.085, 0.680, 0.530] 90 | ease_out: [0.250, 0.460, 0.450, 0.940] 91 | ease_in_out: [0.455, 0.030, 0.515, 0.955] 92 | 93 | quart: 94 | ease_in: [0.895, 0.030, 0.685, 0.220] 95 | ease_out: [0.165, 0.840, 0.440, 1.000] 96 | ease_in_out: [0.770, 0.000, 0.175, 1.000] 97 | 98 | quint: 99 | ease_in: [0.755, 0.050, 0.855, 0.060] 100 | ease_out: [0.230, 1.000, 0.320, 1.000] 101 | ease_in_out: [0.860, 0.000, 0.070, 1.000] 102 | 103 | sine: 104 | ease_in: [0.470, 0.000, 0.745, 0.715] 105 | ease_out: [0.390, 0.575, 0.565, 1.000] 106 | ease_in_out: [0.445, 0.050, 0.550, 0.950] 107 | 108 | back: 109 | ease_in: [0.600, -0.280, 0.735, 0.045] 110 | ease_out: [0.175, 0.885, 0.320, 1.275] 111 | ease_in_out: [0.680, -0.550, 0.265, 1.550] 112 | -------------------------------------------------------------------------------- /styles/bezier-curve-editor.less: -------------------------------------------------------------------------------- 1 | 2 | @import "ui-variables"; 3 | 4 | @font-face { 5 | font-family: 'css-easing'; 6 | src:url('atom://bezier-curve-editor/resources/css-easing.woff') format('woff'); 7 | font-weight: normal; 8 | font-style: normal; 9 | } 10 | 11 | @-webkit-keyframes preview { 12 | 0% { 13 | -webkit-transform: translate(0,0); 14 | } 15 | 100% { 16 | -webkit-transform: translate(90%,0); 17 | } 18 | } 19 | 20 | [class^="easing-"], [class*=" easing-"] { 21 | font-family: 'css-easing'; 22 | speak: none; 23 | font-style: normal; 24 | font-weight: normal; 25 | font-variant: normal; 26 | text-transform: none; 27 | line-height: 1; 28 | 29 | -webkit-font-smoothing: antialiased; 30 | } 31 | 32 | .easing-linear:before { 33 | content: "\e600"; 34 | } 35 | .easing-ease:before { 36 | content: "\e601"; 37 | } 38 | .easing-ease-out:before { 39 | content: "\e602"; 40 | } 41 | .easing-ease-in:before { 42 | content: "\e603"; 43 | } 44 | .easing-ease-in-out:before { 45 | content: "\e604"; 46 | } 47 | 48 | 49 | bezier-curve-editor { 50 | position: absolute; 51 | display: block; 52 | width: auto !important; 53 | padding: 0; 54 | margin-left: 0 !important; 55 | background: @app-background-color !important; 56 | -webkit-filter: drop-shadow(0 0 1px rgba(255, 255, 255, 0.07)); 57 | padding: 5px; 58 | z-index: 2; 59 | 60 | .block { 61 | // clear for top-margin on the select element 62 | padding: 1px 0 0 0; 63 | clear: both; 64 | } 65 | 66 | select.form-control { 67 | width: 100%; 68 | margin: 4px 0 0; 69 | background: @input-background-color; 70 | border: 1px solid @input-border-color; 71 | color: @text-color; 72 | font-family: @font-family; 73 | } 74 | 75 | .btn { 76 | font-family: 'Lucida Grande', 'Segoe UI', sans-serif; 77 | } 78 | 79 | .btn-group { 80 | &:after { 81 | content: ''; 82 | display: table; 83 | clear: both; 84 | } 85 | 86 | .btn-icon { 87 | height: 28px; 88 | padding: 3px 0 0; 89 | } 90 | } 91 | 92 | bezier-curve { 93 | position: relative; 94 | display: block; 95 | } 96 | 97 | bezier-curve-timing { 98 | display: block; 99 | margin: 0 4px; 100 | 101 | .bezier-timing-ramp { 102 | background: @base-background-color; 103 | border: 1px solid @base-border-color; 104 | height: 8px; 105 | box-sizing: border-box; 106 | 107 | .bezier-timing-dummy { 108 | height: 100%; 109 | 110 | &:before { 111 | content: ''; 112 | display: block; 113 | width: 10%; 114 | height: 100%; 115 | background: red; 116 | } 117 | } 118 | 119 | } 120 | } 121 | 122 | .patterns { 123 | display: block; 124 | margin: 4px; 125 | overflow: hidden; 126 | .btn-icon { 127 | width: 20%; 128 | 129 | & + .btn-icon { 130 | width: -webkit-calc(~'20% + 1px'); 131 | } 132 | 133 | i { 134 | font-size: 20px; 135 | font-weight: bold; 136 | color: red; 137 | border: 1px solid @text-color-subtle; 138 | } 139 | } 140 | } 141 | 142 | .actions { 143 | display: block; 144 | position: relative; 145 | z-index: 2; 146 | .btn { 147 | width: 50%; 148 | } 149 | } 150 | 151 | bezier-curve-control { 152 | position: absolute; 153 | width: 10px; 154 | height: 10px; 155 | display: block; 156 | -webkit-transform: translate(-50%, -50%); 157 | background: @text-color-highlight; 158 | border-radius: 50%; 159 | z-index: 3; 160 | cursor: -webkit-grab; 161 | 162 | &:active { 163 | cursor: -webkit-grabbing; 164 | } 165 | } 166 | 167 | canvas { 168 | width: 200px; 169 | height: 200px; 170 | display: block; 171 | position: relative; 172 | z-index: 2; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /lib/bezier-curve-editor.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable, Disposable} = require 'atom' 2 | BezierCurveEditorElement = require './bezier-curve-editor-element' 3 | 4 | module.exports= 5 | view: null 6 | match: null 7 | 8 | activate: -> 9 | @subscriptions = new CompositeDisposable 10 | @subscriptions.add atom.commands.add 'atom-workspace', 11 | 'bezier-curve-editor:open': => @open true 12 | 13 | @subscriptions.add atom.contextMenu.add '.editor': [{ 14 | label: 'Edit Bezier Curve', 15 | command: 'bezier-curve-editor:open', 16 | shouldDisplay: (event) => 17 | return true if @match = @getMatchAtCursor() 18 | }] 19 | 20 | @view = new BezierCurveEditorElement 21 | 22 | @subscriptions.add @view.onDidCancel => @view.close() 23 | @subscriptions.add @view.onDidConfirm => 24 | spline = @view.getSpline() 25 | @replaceMatch(spline) 26 | @view.close() 27 | 28 | deactivate: -> 29 | @view.destroy() 30 | @subscriptions.dispose() 31 | 32 | getMatchAtCursor: -> 33 | editor = atom.workspace.getActiveTextEditor() 34 | return unless editor? 35 | 36 | line = editor.getLastCursor().getCurrentBufferLine() 37 | cursorBuffer = editor.getCursorBufferPosition() 38 | cursorRow = cursorBuffer.row 39 | cursorColumn = cursorBuffer.column 40 | 41 | matches = @matchesOnLine(line, cursorRow) 42 | matchAtPos = @matchAtPosition cursorColumn, matches 43 | 44 | return matchAtPos 45 | 46 | matchesOnLine: (line, cursorRow) -> 47 | float = '(\\d+|\\d?\\.\\d+)' 48 | regex = /// 49 | cubic-bezier\s* 50 | \(\s* 51 | (#{float}) 52 | \s*,\s* 53 | (-?#{float}) 54 | \s*,\s* 55 | (#{float}) 56 | \s*,\s* 57 | (-?#{float}) 58 | \s*\) 59 | ///g 60 | 61 | filteredMatches = [] 62 | matches = line.match regex 63 | 64 | return unless matches? 65 | 66 | for match in matches 67 | continue if (index = line.indexOf match) is -1 68 | 69 | filteredMatches.push 70 | match: match 71 | regexMatch: match.match RegExp regex.source, 'i' 72 | index: index 73 | end: index + match.length 74 | row: cursorRow 75 | 76 | line = line.replace match, (Array match.length + 1).join ' ' 77 | 78 | return unless filteredMatches.length 79 | filteredMatches 80 | 81 | matchAtPosition: (column, matches) -> 82 | return unless column and matches 83 | 84 | for match in matches 85 | if match.index <= column and match.end >= column 86 | matchResults = match 87 | break 88 | 89 | return matchResults 90 | 91 | selectMatch: -> 92 | editor = atom.workspace.getActiveTextEditor() 93 | 94 | editor.clearSelections() 95 | editor.addSelectionForBufferRange 96 | start: 97 | column: @match.index 98 | row: @match.row 99 | end: 100 | column: @match.end 101 | row: @match.row 102 | 103 | replaceMatch: (spline) -> 104 | return unless @match? 105 | 106 | editor = atom.workspace.getActiveTextEditor() 107 | splineCSS = @getSplineCSS(spline) 108 | editor.replaceSelectedText null, -> splineCSS 109 | 110 | editor.clearSelections() 111 | editor.addSelectionForBufferRange 112 | start: 113 | column: @match.index 114 | row: @match.row 115 | end: 116 | column: @match.index + splineCSS.length 117 | row: @match.row 118 | 119 | getSplineCSS: (spline) -> 120 | precision = 100000 121 | spline = spline.map (n) -> Math.floor(n * precision) / precision 122 | 123 | "cubic-bezier(#{ spline.join ', ' })" 124 | 125 | open: (getMatch = false) -> 126 | 127 | @match = @getMatchAtCursor() if getMatch 128 | 129 | return unless @match? 130 | 131 | [m, a1, _, a2, _, a3, _, a4] = @match.regexMatch 132 | [a1, a2, a3, a4] = [ 133 | parseFloat a1 134 | parseFloat a2 135 | parseFloat a3 136 | parseFloat a4 137 | ] 138 | @selectMatch() 139 | 140 | @view.setSpline(a1, a2, a3, a4) 141 | @view.renderSpline() 142 | @view.open() 143 | -------------------------------------------------------------------------------- /lib/bezier-curve-editor-element.coffee: -------------------------------------------------------------------------------- 1 | {SpacePenDSL, EventsDelegation, AncestorsMethods} = require 'atom-utils' 2 | {CompositeDisposable, Emitter} = require 'atom' 3 | Delegator = require 'delegato' 4 | CurveElement = require './curve-element' 5 | BezierTimingElement = require './bezier-timing-element' 6 | {easing, extraEasing} = require './bezier-functions' 7 | 8 | humanize = (str) -> 9 | str 10 | .split('_') 11 | .map((s) -> s.replace(/^./, (m) -> m.toUpperCase())) 12 | .join(' ') 13 | 14 | module.exports = 15 | class BezierCurveEditorElement extends HTMLElement 16 | Delegator.includeInto(this) 17 | SpacePenDSL.includeInto(this) 18 | EventsDelegation.includeInto(this) 19 | 20 | @content: -> 21 | @div => 22 | @tag 'bezier-curve', outlet: 'curveView' 23 | @tag 'bezier-curve-timing', outlet: 'timingView' 24 | 25 | @div class: 'patterns btn-group', => 26 | for name,spline of easing 27 | @button outlet: name, class: 'btn btn-icon btn-sm', => 28 | @i class: 'easing-' + name.replace(/_/g, '-') 29 | 30 | @div class: 'block', => 31 | @tag 'select', class: 'form-control', outlet: 'easingSelect', => 32 | @tag 'option', value: '', 'More easings...' 33 | 34 | for group, splines of extraEasing 35 | @tag 'optgroup', label: humanize(group), => 36 | for name, spline of splines 37 | @tag 'option', value: group + ':' + name, humanize(group + ' ' + name) 38 | 39 | @div class: 'actions btn-group', => 40 | @button outlet: 'cancelButton', class: 'btn', 'Cancel' 41 | @button outlet: 'validateButton', class: 'btn', 'Validate' 42 | 43 | @delegatesMethods 'getSpline', 'setSpline', 'renderSpline', toProperty: 'curveView' 44 | 45 | createdCallback: -> 46 | @classList.add 'overlay' 47 | @classList.add 'native-key-bindings' 48 | @setAttribute('tabindex', -1) 49 | 50 | @subscriptions = new CompositeDisposable 51 | @emitter = new Emitter 52 | 53 | @subscriptions.add @curveView.onDidChangeSpline => 54 | @timingView.setSpline @curveView.getSpline() 55 | 56 | @subscriptions.add @subscribeTo @easingSelect, 'change': => 57 | value = @easingSelect.value 58 | if value isnt '' 59 | [group, name] = value.split(':') 60 | 61 | @setSpline.apply this, extraEasing[group][name] 62 | @timingView.setSpline extraEasing[group][name] 63 | @renderSpline() 64 | 65 | @subscriptions.add @subscribeTo @cancelButton, 'click': => 66 | @emitter.emit('did-cancel') 67 | 68 | @subscriptions.add @subscribeTo @validateButton, 'click': => 69 | @emitter.emit('did-confirm') 70 | 71 | Object.keys(easing).forEach (name) => 72 | button = @[name] 73 | # button.setTooltip(name.replace /_/g, '-') 74 | @subscriptions.add @subscribeTo button, 'click': => 75 | @setSpline.apply this, easing[name] 76 | @timingView.setSpline easing[name] 77 | @renderSpline() 78 | 79 | onDidCancel: (callback) -> 80 | @emitter.on 'did-cancel', callback 81 | 82 | onDidConfirm: (callback) -> 83 | @emitter.on 'did-confirm', callback 84 | 85 | open: -> 86 | @attach() 87 | 88 | @subscribeToOutsideEvent() 89 | 90 | @timingView.setSpline @getSpline() 91 | 92 | @curveView.dummy1.activate() 93 | @curveView.dummy2.activate() 94 | 95 | subscribeToOutsideEvent: -> 96 | @subscriptions.add @subscribeTo this, 'mousedown': (e) -> 97 | e.stopImmediatePropagation() 98 | @subscriptions.add @subscribeTo document.body, 'mousedown': (e) => 99 | @closeIfClickedOutside(e) 100 | 101 | closeIfClickedOutside: (e) -> 102 | if AncestorsMethods.parents(e.target, '.bezier-curve-editor').length is 0 103 | @close() 104 | 105 | attach: -> 106 | editor = @getActiveEditor() 107 | return if @active or not editor? 108 | @destroyOverlay() 109 | 110 | if marker = editor.getLastSelection()?.marker 111 | @overlayDecoration = editor.decorateMarker(marker, {type: 'overlay', item: this, position: 'tail'}) 112 | @active = true 113 | 114 | getActiveEditor: -> atom.workspace.getActiveTextEditor() 115 | 116 | close: -> 117 | @detach() 118 | @curveView.dummy1.deactivate() 119 | @curveView.dummy2.deactivate() 120 | 121 | destroyOverlay: -> 122 | @active = false 123 | @overlayDecoration?.destroy() 124 | 125 | detach: -> 126 | @destroyOverlay() 127 | 128 | destroy: -> 129 | @close() 130 | 131 | module.exports = BezierCurveEditorElement = document.registerElement 'bezier-curve-editor', prototype: BezierCurveEditorElement.prototype 132 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # v0.7.2 (2015-06-04) 3 | 4 | ## :bug: Bug Fixes 5 | 6 | - Fix broken overlay since v0.205.0 ([bdd658ee](https://github.com/abe33/atom-bezier-curve-editor/commit/bdd658ee5d3b6d6e930d85536ffb20fc9aa5c48f)) 7 | 8 | 9 | # v0.7.1 (2015-05-28) 10 | 11 | ## :bug: Bug Fixes 12 | 13 | - Fix latest deprecations ([7f63d91c](https://github.com/abe33/atom-bezier-curve-editor/commit/7f63d91c933086dc1fd85d4d35da9600711747bc), [#10](https://github.com/abe33/atom-bezier-curve-editor/issues/10)) 14 | 15 | 16 | # v0.7.0 (2015-05-20) 17 | 18 | ## :sparkles: Features 19 | 20 | - Implement all views as custom elements ([ccb867b4](https://github.com/abe33/atom-bezier-curve-editor/commit/ccb867b4041793d7a489ddd47e34d65665f07846)) 21 | 22 | 23 | # 0.6.6 (2015-03-05) 24 | 25 | ## :bug: Bug Fixes 26 | 27 | - Fix positioning issues ([a77686db](https://github.com/abe33/atom-bezier-curve-editor/commit/a77686db867aaee800667584c7703425f109e3b2)) 28 | 29 | 30 | # v0.6.5 (2015-02-06) 31 | 32 | ## :bug: Bug Fixes 33 | 34 | - Fix remaining deprecations 35 | 36 | 37 | # v0.6.4 (2015-01-26) 38 | 39 | ## :bug: Bug Fixes 40 | 41 | - Fix selectors deprecations ([adbbdbf4](https://github.com/abe33/atom-bezier-curve-editor/commit/adbbdbf4bd65c6a7744a1e17f23933e34eb4db5c)) 42 | 43 | 44 | 45 | # v0.6.3 (2015-01-26) 46 | 47 | ## :bug: Bug Fixes 48 | 49 | - Fix deprecations ([95608d01](https://github.com/abe33/atom-bezier-curve-editor/commit/95608d01008bed4038bca17440954482a91e0168)) 50 | 51 | 52 | 53 | # v0.6.2 (2014-12-08) 54 | 55 | ## :bug: Bug Fixes 56 | 57 | - Fix bad redraw of canvas background ([cdd5dbea](https://github.com/abe33/atom-bezier-curve-editor/commit/cdd5dbeae9e7fda02afbd4f3d35b9b2863332ebe)) 58 | - Fix deprecations and broken API access. 59 | 60 | 61 | # v0.6.1 (2014-10-09) 62 | 63 | ## :bug: Bug Fixes 64 | 65 | - Fix all deprecations ([cc507c6c](https://github.com/abe33/atom-bezier-curve-editor/commit/cc507c6c26c6639eac7068f09d9dc8d2e91abae9)) 66 | - Fix issue with floats without numerator ([289f9dc2](https://github.com/abe33/atom-bezier-curve-editor/commit/289f9dc2ec1f639cf9cb013f21374397f443d1d1), [#5](https://github.com/abe33/atom-bezier-curve-editor/issues/5)) 67 | 68 | 69 | # v0.6.0 (2014-10-01) 70 | 71 | ## :sparkles: Features 72 | 73 | - Implement support for the new context menu shouldDisplay option ([96880e80](https://github.com/abe33/atom-bezier-curve-editor/commit/96880e802fc374db892abcf3b05d6d614905701d)) 74 | 75 | 76 | # v0.5.0 (2014-05-29) 77 | 78 | ## :sparkles: Features 79 | 80 | - Add extra easing curves presets ([fe74b2b8](https://github.com/abe33/atom-bezier-curve-editor/commit/fe74b2b852baaa9aa18d7606cba85863367bf1f2), [#2](https://github.com/abe33/atom-bezier-curve-editor/issues/2)) 81 |
Includes: 82 | - Cubic 83 | - Circ 84 | - Expo 85 | - Quad 86 | - Quart 87 | - Quint 88 | - Sine 89 | - Back 90 | 91 | ## :bug: Bug Fixes 92 | 93 | - Fix placement of the popup when editor content is small ([244e7a25](https://github.com/abe33/atom-bezier-curve-editor/commit/244e7a256f5cfce45ace3fcf3d20a1b47869df4d), [#1](https://github.com/abe33/atom-bezier-curve-editor/issues/1)) 94 | - Fix typo leading to error in conditional context menu ([27c78c52](https://github.com/abe33/atom-bezier-curve-editor/commit/27c78c524daad005dc2f0666ed45848f139ff9ed)) 95 | 96 | 97 | # v0.4.0 (2014-05-29) 98 | 99 | ## :bug: Bug Fixes 100 | 101 | - Fixes weird behavior when using mouse to select text ([8baa136](https://github.com/abe33/atom-bezier-curve-editor/commit/8baa136cb134f05a129b209fc19aae2f2785c9ff)) 102 | 103 | 104 | # v0.3.0 (2014-05-29) 105 | 106 | ## :sparkles: Features 107 | 108 | - Adds an animated preview of the timing function ([f985edb0](https://github.com/abe33/atom-bezier-curve-editor/commit/f985edb060d743fad1faad88c3489b89036911fc)) 109 | 110 | 111 | 112 | # v0.2.0 (2014-05-29) 113 | 114 | ## :sparkles: Features 115 | 116 | - Adds proper icons for presets buttons ([e2169bdc](https://github.com/abe33/atom-bezier-curve-editor/commit/e2169bdcb1fcbe513a8c446f71788e6c5143e63b)) 117 | - Adds buttons to set spline using presets ([fa767067](https://github.com/abe33/atom-bezier-curve-editor/commit/fa7670674da77c9f76c385acc68b04ae9276b309)) 118 |
The UI is quite broken though 119 | - Adds a getModel method on the view ([21cbcad6](https://github.com/abe33/atom-bezier-curve-editor/commit/21cbcad6ff9f61ebc5796d37c40bd37e9d50cfef)) 120 |
Not used, but seems to be required for views in the editor. 121 | -------------------------------------------------------------------------------- /lib/curve-element.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable, Emitter} = require 'atom' 2 | {SpacePenDSL, AncestorsMethods} = require 'atom-utils' 3 | {UnitBezier} = require './bezier-functions' 4 | CurveControlElement = require './curve-control-element' 5 | 6 | module.exports = 7 | class CurveElement extends HTMLElement 8 | SpacePenDSL.includeInto(this) 9 | 10 | canvasWidth = 200 11 | canvasHeight = 200 12 | canvasPadding = 50 13 | 14 | @content: -> 15 | @div class: 'curve-view', => 16 | @tag 'canvas', { 17 | outlet: 'canvas' 18 | id: 'curve-canvas' 19 | width: canvasWidth 20 | height: canvasHeight 21 | } 22 | 23 | @tag 'bezier-curve-control', outlet: 'dummy1' 24 | @tag 'bezier-curve-control', outlet: 'dummy2' 25 | 26 | createdCallback: -> 27 | @subscriptions = new CompositeDisposable 28 | @emitter = new Emitter 29 | 30 | @context = @canvas.getContext('2d') 31 | @canvasWidth = canvasWidth 32 | @canvasHeight = canvasHeight 33 | @canvasPadding = canvasPadding 34 | @canvasWidthMinusPadding = @canvasWidth - @canvasPadding 35 | @canvasHeightMinusPadding = @canvasHeight - @canvasPadding 36 | @curveSpaceWidth = @canvasWidth - @canvasPadding * 2 37 | @curveSpaceHeight = @canvasHeight - @canvasPadding * 2 38 | @cleanCanvas() 39 | 40 | minX = @canvasPadding 41 | maxX = @canvasWidth - @canvasPadding 42 | minY = @canvasPadding 43 | maxY = @canvasHeight - @canvasPadding 44 | 45 | @dummy1.initialize {minX, maxX, minY, maxY} 46 | @dummy2.initialize {minX, maxX, minY, maxY} 47 | 48 | @subscriptions.add @dummy1.onDidDrag (e) => @control1Changed(e) 49 | @subscriptions.add @dummy2.onDidDrag (e) => @control2Changed(e) 50 | 51 | @dummy1.onDidEndDrag => @emitter.emit 'did-change-spline' 52 | @dummy2.onDidEndDrag => @emitter.emit 'did-change-spline' 53 | 54 | onDidChangeSpline: (callback) -> 55 | @emitter.on 'did-change-spline', callback 56 | 57 | getSpline: -> [@x1, @y1, @x2, @y2] 58 | 59 | setSpline: (@x1, @y1, @x2, @y2) -> 60 | 61 | cleanCanvas: -> 62 | styles = getComputedStyle(AncestorsMethods.parents(this, 'bezier-curve-editor')[0]) 63 | @context.fillStyle = styles.backgroundColor 64 | @context.fillRect(0, 0, @canvasWidth, @canvasWidth) 65 | 66 | @context.strokeStyle = styles.color 67 | @context.beginPath() 68 | @context.moveTo(@canvasPadding, @canvasPadding) 69 | @context.lineTo(@canvasPadding, @canvasHeightMinusPadding) 70 | @context.lineTo(@canvasWidthMinusPadding, @canvasHeightMinusPadding) 71 | @context.lineTo(@canvasWidthMinusPadding, @canvasPadding) 72 | @context.lineTo(@canvasPadding, @canvasPadding) 73 | @context.stroke() 74 | 75 | updateDummies: -> 76 | @dummy1.style.top = (@canvasHeightMinusPadding - @y1 * @curveSpaceHeight) + 'px' 77 | @dummy1.style.left = (@canvasPadding + @x1 * @curveSpaceWidth) + 'px' 78 | 79 | @dummy2.style.top = (@canvasHeightMinusPadding - @y2 * @curveSpaceHeight) + 'px' 80 | @dummy2.style.left = (@canvasPadding + @x2 * @curveSpaceWidth) + 'px' 81 | 82 | renderControls: -> 83 | panel = document.querySelector('atom-panel') 84 | return unless panel? 85 | 86 | @context.strokeStyle = getComputedStyle(panel).color 87 | 88 | @context.beginPath() 89 | @context.moveTo(@canvasPadding, @canvasHeightMinusPadding) 90 | @context.lineTo( 91 | @canvasPadding + @x1 * @curveSpaceWidth, 92 | @canvasHeightMinusPadding - @y1 * @curveSpaceHeight 93 | ) 94 | @context.stroke() 95 | 96 | @context.beginPath() 97 | @context.moveTo(@canvasWidthMinusPadding, @canvasPadding) 98 | @context.lineTo( 99 | @canvasPadding + @x2 * @curveSpaceWidth, 100 | @canvasHeightMinusPadding - @y2 * @curveSpaceHeight 101 | ) 102 | @context.stroke() 103 | 104 | renderSpline: -> 105 | requestAnimationFrame => 106 | @cleanCanvas() 107 | @updateDummies() 108 | @renderControls() 109 | 110 | @context.strokeStyle = '#ff0000' 111 | @context.beginPath() 112 | @context.moveTo(@canvasPadding, @canvasHeightMinusPadding) 113 | 114 | spline = new UnitBezier @x1, @y1, @x2, @y2 115 | 116 | for n in [0..50] 117 | r = n / 50 118 | x = @canvasPadding + r * @curveSpaceWidth 119 | curveY = spline.solve(r, spline.epsilon) 120 | y = @canvasHeightMinusPadding - curveY * @curveSpaceHeight 121 | @context.lineTo(x, y) 122 | 123 | @context.stroke() 124 | 125 | control1Changed: ({x, y}) => 126 | @x1 = x 127 | @y1 = y 128 | @renderSpline() 129 | 130 | control2Changed: ({x, y}) => 131 | @x2 = x 132 | @y2 = y 133 | @renderSpline() 134 | 135 | module.exports = CurveElement = document.registerElement 'bezier-curve', prototype: CurveElement.prototype 136 | --------------------------------------------------------------------------------