├── Hook.coffee ├── LICENSE ├── README.md ├── hook-example-gravity.framer ├── .gitignore ├── .preview.html ├── .viewer.html ├── app.coffee ├── framer │ ├── coffee-script.js │ ├── config.json │ ├── framer.generated.js │ ├── framer.init.js │ ├── framer.js │ ├── framer.js.map │ ├── framer.modules.js │ ├── images │ │ ├── cursor-active.png │ │ ├── cursor-active@2x.png │ │ ├── cursor.png │ │ ├── cursor@2x.png │ │ ├── icon-120.png │ │ ├── icon-152.png │ │ ├── icon-180.png │ │ ├── icon-192.png │ │ └── icon-76.png │ ├── manifest.txt │ ├── metadata.json │ ├── preview.png │ ├── style.css │ └── version ├── images │ └── .gitkeep ├── index.html └── modules │ └── Hook.coffee ├── hook-example-modulator.framer ├── .gitignore ├── .viewer.html ├── app.coffee ├── framer │ ├── coffee-script.js │ ├── config.json │ ├── framer.generated.js │ ├── framer.init.js │ ├── framer.js │ ├── framer.js.map │ ├── framer.modules.js │ ├── images │ │ ├── cursor-active.png │ │ ├── cursor-active@2x.png │ │ ├── cursor.png │ │ ├── cursor@2x.png │ │ ├── icon-120.png │ │ ├── icon-152.png │ │ ├── icon-180.png │ │ ├── icon-192.png │ │ └── icon-76.png │ ├── manifest.txt │ ├── preview.png │ ├── style.css │ └── version ├── images │ ├── .gitkeep │ └── spinner.svg ├── imported │ └── app@2x │ │ ├── images │ │ └── Layer-page-meneqzew.png │ │ ├── layers.json │ │ └── layers.json.js ├── index.html └── modules │ └── Hook.coffee └── hook-example-spring.framer ├── .gitignore ├── .viewer.html ├── app.coffee ├── framer ├── coffee-script.js ├── config.json ├── framer.generated.js ├── framer.init.js ├── framer.js ├── framer.js.map ├── framer.modules.js ├── images │ ├── cursor-active.png │ ├── cursor-active@2x.png │ ├── cursor.png │ ├── cursor@2x.png │ ├── icon-120.png │ ├── icon-152.png │ ├── icon-180.png │ ├── icon-192.png │ └── icon-76.png ├── manifest.txt ├── metadata.json ├── preview.png ├── style.css └── version ├── images └── .gitkeep ├── index.html ├── modules └── Hook.coffee ├── spring-example-720.gif └── spring-example.gif /Hook.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | -------------------------------------------------------------------------------- 3 | Hook module for Framer 4 | -------------------------------------------------------------------------------- 5 | 6 | by: Sigurd Mannsåker 7 | github: https://github.com/sigtm/framer-hook 8 | 9 | ················································································ 10 | 11 | 12 | The Hook module simply expands the Layer prototype, and lets you make any 13 | numeric Layer property follow another property - either its own or another 14 | object's - via a spring or gravity attraction. 15 | 16 | 17 | -------------------------------------------------------------------------------- 18 | Example: Layered animation (eased + spring) 19 | -------------------------------------------------------------------------------- 20 | 21 | myLayer = new Layer 22 | 23 | # Make our own custom property for the x property to follow 24 | myLayer.easedX = 0 25 | 26 | # Hook x to easedX via a spring 27 | myLayer.hook 28 | property: "x" 29 | targetProperty: "easedX" 30 | type: "spring(150, 15)" 31 | 32 | # Animate easedX 33 | myLayer.animate 34 | properties: 35 | easedX: 200 36 | time: 0.15 37 | curve: "cubic-bezier(0.2, 0, 0.4, 1)" 38 | 39 | NOTE: 40 | To attach both the x and y position, use "pos", "midPos" or "maxPos" as the 41 | property/targetProperty. 42 | 43 | 44 | -------------------------------------------------------------------------------- 45 | Example: Hooking property to another layer 46 | -------------------------------------------------------------------------------- 47 | 48 | target = new Layer 49 | hooked = new Layer 50 | 51 | hooked.hook 52 | property: "scale" 53 | to: target 54 | type: "spring(150, 15)" 55 | 56 | The "hooked" layer's scale will now continuously follow the target layer's scale 57 | with a spring animation. 58 | 59 | 60 | -------------------------------------------------------------------------------- 61 | layer.hook(options) 62 | -------------------------------------------------------------------------------- 63 | 64 | Options are passed as a single object, like you would for a new Layer. 65 | The options object takes the following properties: 66 | 67 | 68 | property [String] 69 | ----------------- 70 | The property you'd like to hook onto another object's property 71 | 72 | 73 | type [String] 74 | ------------- 75 | Either "spring(strength, friction)" or "gravity(strength, drag)". Only the last 76 | specified drag value is used for each property, since it is only applied to 77 | each property once (and only if it has a gravity hook applied to it.) 78 | 79 | 80 | to [Object] (Optional) 81 | ---------------------- 82 | The object to attach it to. Defaults to itself. 83 | 84 | 85 | targetProperty [String] (Optional) 86 | ---------------------------------- 87 | Specify the target object's property to follow, if you don't want to follow 88 | the same property that the hook is applied to. 89 | 90 | 91 | modulator [Function] (Optional) 92 | ------------------------------- 93 | The modulator function receives the target property's value, and lets you 94 | modify it before it is fed into the physics calculations. Useful for anything 95 | from standard Utils.modulate() type stuff to snapping and conditional values. 96 | 97 | 98 | zoom [Number] (Optional) 99 | ------------------------ 100 | This factor defines the distance that 1px represents in regards to gravity and 101 | drag calculations. Only one value is stored per layer, so specifying it 102 | overwrites its existing value. Default is 100. 103 | 104 | 105 | -------------------------------------------------------------------------------- 106 | layer.unHook(property, object) 107 | -------------------------------------------------------------------------------- 108 | 109 | This removes all hooks for a given property and target object. Example: 110 | 111 | # Hook it 112 | layer.hook 113 | property: "x" 114 | to: "otherlayer" 115 | targetProperty: "y" 116 | type: "spring(200,20)" 117 | 118 | # Unhook it 119 | layer.unHook "x", otherlayer 120 | 121 | 122 | -------------------------------------------------------------------------------- 123 | layer.onHookUpdate(delta) 124 | -------------------------------------------------------------------------------- 125 | 126 | After a layer is done applying accelerations to its hooked properties, it calls 127 | onHookUpdate() at the end of each frame, if it is defined. This is an easy way 128 | to animate or trigger other stuff, perhaps based on your layer's updated 129 | properties or velocities. 130 | 131 | The delta value from the Framer loop is passed on to onHookUpdate() as well, 132 | which is the time in seconds since the last animation frame. 133 | 134 | Note that if you unhook all your hooks, onHookUpdate() will of course no longer 135 | be called for that layer. 136 | 137 | ### 138 | 139 | 140 | # Since older versions of Safari seem to be missing String.prototype.includes() 141 | 142 | unless String.prototype.includes 143 | String::includes = (search, start) -> 144 | 'use strict' 145 | start = 0 if typeof start is 'number' 146 | 147 | if start + search.length > this.length 148 | return false; 149 | else 150 | return @indexOf(search, start) isnt -1 151 | 152 | # Expand layer 153 | 154 | Layer::hook = (config) -> 155 | 156 | throw new Error 'layer.hook() needs a property, a hook type and either a target object or target property to work' unless config.property and config.type and (config.to or config.targetProperty) 157 | 158 | # Single array for all hooks, as opposed to nested arrays per property, because performance 159 | @hooks ?= 160 | hooks: [] 161 | velocities: {} 162 | defs: 163 | zoom: 100 164 | getDrag: (velocity, drag, zoom) => 165 | velocity /= zoom 166 | # Dividing by 10 is unscientific, but it means a value of 2 equals roughly a 100g ball with 15cm radius in air 167 | drag = -(drag / 10) * velocity * velocity * velocity / Math.abs(velocity) 168 | if _.isNaN(drag) then return 0 else return drag 169 | getGravity: (strength, distance, zoom) => 170 | dist = Math.max(1, distance / zoom) 171 | return strength * zoom / (dist * dist) 172 | 173 | # Update the zoom value if given 174 | @hooks.zoom = config.zoom if config.zoom 175 | 176 | # Parse physics config string 177 | f = Utils.parseFunction config.type 178 | config.type = f.name 179 | config.strength = f.args[0] 180 | config.friction = f.args[1] or 0 181 | 182 | # Default to same targetProperty on same object (hopefully you've set at least one of these to something else) 183 | config.targetProperty ?= config.property 184 | config.to ?= @ 185 | 186 | # All position accelerations are added to a single 'pos' velocity. Store actual properties so we don't have to do it again every frame 187 | 188 | if config.property.toLowerCase().includes 'pos' 189 | config.prop = 'pos' 190 | 191 | if config.property.toLowerCase().includes 'mid' 192 | config.thisX = 'midX' 193 | config.thisY = 'midY' 194 | 195 | else if config.property.toLowerCase().includes 'max' 196 | config.thisX = 'maxX' 197 | config.thisY = 'maxY' 198 | 199 | else 200 | config.thisX = 'x' 201 | config.thisY = 'y' 202 | 203 | if config.targetProperty.toLowerCase().includes 'mid' 204 | config.toX = 'midX' 205 | config.toY = 'midY' 206 | 207 | else if config.targetProperty.toLowerCase().includes 'max' 208 | config.toX = 'maxX' 209 | config.toY = 'maxY' 210 | else 211 | config.toX = 'x' 212 | config.toY = 'y' 213 | 214 | else 215 | config.prop = config.property 216 | 217 | # Save hook to @hooks array 218 | @hooks.hooks.push(config) 219 | 220 | # Create velocity property if necessary 221 | @hooks.velocities[config.prop] ?= if config.prop is 'pos' then { x: 0, y: 0 } else 0 222 | 223 | # Use Framer's animation loop, slightly more robust than requestAnimationFrame directly 224 | # Save the returned AnimationLoop reference to make sure @hookLoop isn't added multiple times per layer 225 | @hooks.emitter ?= Framer.Loop.on('render', @hookLoop, this) 226 | 227 | Layer::unHook = (property, object) -> 228 | 229 | return unless @hooks 230 | 231 | prop = if property.toLowerCase().includes 'pos' then 'pos' else property 232 | 233 | # Remove all matches 234 | @hooks.hooks = @hooks.hooks.filter (hook) -> 235 | hook.to isnt object or hook.property isnt property 236 | 237 | # If there are no hooks left, shut it down 238 | if @hooks.hooks.length is 0 239 | delete @hooks 240 | Framer.Loop.removeListener 'render', @hookLoop 241 | return 242 | 243 | # Still here? Check if there are any remaining hooks affecting same velocity 244 | remaining = @hooks.hooks.filter (hook) -> 245 | prop is hook.prop 246 | 247 | # If not, delete velocity (otherwise it won't be reset if you make new hook for same property) 248 | delete @hooks.velocities[prop] if remaining.length is 0 249 | 250 | Layer::hookLoop = (delta) -> 251 | 252 | if @hooks 253 | 254 | # Multiple hooks can affect the same property. Add accelerations to temporary object so the property's velocity is the same for all calculations within the same animation frame 255 | acceleration = {} 256 | 257 | # Save drag for each property to this object, since only most recently specified value is used for each property 258 | drag = {} 259 | 260 | # Add accelerations 261 | for hook in @hooks.hooks 262 | 263 | if hook.prop is 'pos' 264 | 265 | acceleration.pos ?= { x: 0, y: 0 } 266 | 267 | target = { x: hook.to[hook.toX], y: hook.to[hook.toY] } 268 | 269 | target = hook.modulator(target) if hook.modulator 270 | 271 | vector = 272 | x: target.x - @[hook.thisX] 273 | y: target.y - @[hook.thisY] 274 | 275 | vLength = Math.sqrt((vector.x * vector.x) + (vector.y * vector.y)) 276 | 277 | if hook.type is 'spring' 278 | 279 | damper = 280 | x: -hook.friction * @hooks.velocities.pos.x 281 | y: -hook.friction * @hooks.velocities.pos.y 282 | 283 | vector.x *= hook.strength 284 | vector.y *= hook.strength 285 | 286 | acceleration.pos.x += (vector.x + damper.x) * delta 287 | acceleration.pos.y += (vector.y + damper.y) * delta 288 | 289 | else if hook.type is 'gravity' 290 | 291 | drag.pos = hook.friction 292 | 293 | gravity = @hooks.defs.getGravity(hook.strength, vLength, @hooks.defs.zoom) 294 | 295 | vector.x *= gravity / vLength 296 | vector.y *= gravity / vLength 297 | 298 | acceleration.pos.x += vector.x * delta 299 | acceleration.pos.y += vector.y * delta 300 | 301 | else 302 | 303 | acceleration[hook.prop] ?= 0 304 | 305 | target = hook.to[hook.targetProperty] 306 | 307 | target = hook.modulator(target) if hook.modulator 308 | 309 | vector = target - @[hook.prop] 310 | 311 | if hook.type is 'spring' 312 | 313 | force = vector * hook.strength 314 | damper = -hook.friction * @hooks.velocities[hook.prop] 315 | 316 | acceleration[hook.prop] += (force + damper) * delta 317 | 318 | 319 | else if hook.type is 'gravity' 320 | 321 | drag[hook.prop] = hook.friction 322 | 323 | force = @hooks.defs.getGravity(hook.strength, vector, @hooks.defs.zoom) 324 | 325 | acceleration[hook.prop] += force * delta 326 | 327 | 328 | # Add velocities to properties. Doing this at the end in case there are multiple hooks affecting the same velocity 329 | for prop, velocity of @hooks.velocities 330 | 331 | if prop is 'pos' 332 | 333 | # Add drag, if it exists 334 | if drag.pos 335 | velocity.x += @hooks.defs.getDrag(velocity.x, drag.pos, @hooks.defs.zoom) 336 | velocity.y += @hooks.defs.getDrag(velocity.y, drag.pos, @hooks.defs.zoom) 337 | 338 | # Add acceleration to velocity 339 | velocity.x += acceleration.pos.x 340 | velocity.y += acceleration.pos.y 341 | 342 | # Add velocity to position 343 | @x += velocity.x * delta 344 | @y += velocity.y * delta 345 | 346 | else 347 | 348 | # Add drag, if it exists 349 | if drag[prop] 350 | @hooks.velocities[prop] += @hooks.defs.getDrag(@hooks.velocities[prop], drag[prop], @hooks.defs.zoom) 351 | 352 | # Add acceleration to velocity 353 | @hooks.velocities[prop] += acceleration[prop] 354 | 355 | # Add velocity to property 356 | @[prop] += @hooks.velocities[prop] * delta 357 | 358 | @onHookUpdate?(delta) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 sigtm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hook module for Framer 2 | ![Spring example](http://www.sigurd.io/framer-hook/hook-example-spring.framer/spring-example-720.gif) 3 | 4 | The Hook module expands Framer's Layer prototype, and lets you make any numeric Layer property follow another property - either its own or another object's - via a spring or gravity attraction. 5 | 6 | Enough chat. Examples: 7 | 8 | [Example 1: Easing + spring](http://www.sigurd.io/framer-hook/hook-example-spring.framer/) 9 | 10 | [Example 2: Modulation from one property type to another](http://www.sigurd.io/framer-hook/hook-example-modulator.framer/) 11 | 12 | [Example 3: Gravity, too](http://www.sigurd.io/framer-hook/hook-example-gravity.framer/) 13 | 14 | The original use case was to layer a spring animation on top of an eased animation to give more control over the timing and feel of a transition, as seen in Example 1. You do not need two layers to achieve this though, as shown in the first code snippet below. 15 | 16 | I'm not a developer nor a mathematician, so much of this is improvised, particularly the physics. Please do let me know, or create a pull request, if you have any suggestions for improvements. 17 | 18 | For a more detailed documentation, check the comments at the top of Hook.coffee. 19 | 20 | 21 | ### Example: Layered animation (eased + spring) 22 | 23 | ``` 24 | myLayer = new Layer 25 | 26 | # Make our own custom property for the x property to follow 27 | myLayer.easedX = 0 28 | 29 | # Hook x to easedX via a spring 30 | myLayer.hook 31 | property: "x" 32 | targetProperty: "easedX" 33 | type: "spring(150, 15)" 34 | 35 | # Animate easedX 36 | myLayer.animate 37 | properties: 38 | easedX: 200 39 | time: 0.15 40 | curve: "cubic-bezier(0.2, 0, 0.4, 1)" 41 | ``` 42 | 43 | NOTE: 44 | To attach both the x and y position, use "pos", "midPos" or "maxPos" as the 45 | property/targetProperty. 46 | 47 | 48 | ### Example: Hooking property to another layer 49 | 50 | ``` 51 | target = new Layer 52 | hooked = new Layer 53 | 54 | hooked.hook 55 | property: "scale" 56 | to: target 57 | type: "spring(150, 15)" 58 | ``` 59 | 60 | The "hooked" layer's scale will now continuously follow the target layer's scale 61 | with a spring animation. 62 | 63 | 64 | ### Example: Adding a modulator function 65 | 66 | The modulator function allows you to do anything you want with the target property's value before it is applied. As a very basic example, let's say you want to convert one layer's y position into a corresponding scale value for another layer: 67 | 68 | ``` 69 | target = new Layer 70 | hooked = new Layer 71 | 72 | hooked.hook 73 | property: "scale" 74 | to: target 75 | targetProperty: "y" 76 | type: "spring(200,20)" 77 | modulator: (input) -> Utils.modulate(input, [0, 400], [0.5, 1]) 78 | ``` 79 | 80 | ### Documentation 81 | 82 | A more thorough documentation is included in the comments at the top of Hook.coffee. -------------------------------------------------------------------------------- /hook-example-gravity.framer/.gitignore: -------------------------------------------------------------------------------- 1 | # Framer Git Ignore 2 | 3 | # General OSX 4 | 5 | .DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | 9 | # Icon must end with two \r 10 | Icon 11 | 12 | 13 | # Thumbnails 14 | ._* 15 | 16 | # Files that might appear in the root of a volume 17 | .DocumentRevisions-V100 18 | .fseventsd 19 | .Spotlight-V100 20 | .TemporaryItems 21 | .Trashes 22 | .VolumeIcon.icns 23 | 24 | # Directories potentially created on remote AFP share 25 | .AppleDB 26 | .AppleDesktop 27 | Network Trash Folder 28 | Temporary Items 29 | .apdisk 30 | 31 | 32 | # Framer Specific 33 | .temp.html 34 | framer/*.old* 35 | framer/backup.coffee 36 | framer/backups/* 37 | framer/.*.hash 38 | -------------------------------------------------------------------------------- /hook-example-gravity.framer/.preview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /hook-example-gravity.framer/.viewer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /hook-example-gravity.framer/app.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | 3 | Hook example 4 | -------------- 5 | 6 | The Hook module simply expands the Layer prototype, and lets you make any 7 | numeric Layer property follow a property on another object via a spring or a 8 | gravity attraction. Check the comments in modules/Hook.coffee for a more thorough 9 | documentation. 10 | 11 | ### 12 | 13 | 14 | 15 | # Require Hook. No exports, it just adds methods to the Layer prototype 16 | # -------------------------------------------------------------------------------- 17 | 18 | require "Hook" 19 | 20 | 21 | 22 | # Settings 23 | # -------------------------------------------------------------------------------- 24 | 25 | gravity = 200 26 | drag = 2 27 | burstSize = 20 28 | 29 | colors = 30 | background: "#260355" 31 | orange: "#f90" 32 | blue: "#63ffff" 33 | primary: "white" 34 | secondary: "rgba(255,255,255,0.1)" 35 | 36 | easeInOut = "cubic-bezier(0.2, 0, 0.4, 1)" 37 | 38 | 39 | 40 | # Set background and defaults 41 | # -------------------------------------------------------------------------------- 42 | 43 | Framer.Device.viewport.backgroundColor = colors.background 44 | 45 | Framer.Defaults.Animation = 46 | curve: easeInOut 47 | time: 0.05 48 | 49 | 50 | 51 | # Set up example 52 | # -------------------------------------------------------------------------------- 53 | 54 | magnet = new Layer 55 | borderRadius: 100 56 | backgroundColor: colors.primary 57 | width: 80 58 | height: 80 59 | 60 | magnet.center() 61 | 62 | magnet.draggable.enabled = true 63 | 64 | magnet.onTouchStart -> 65 | 66 | @animate 67 | properties: 68 | scale: 0.95 69 | 70 | for i in [0...burstSize] 71 | 72 | bubble = new Layer 73 | x: @midX 74 | y: @midY 75 | index: 0 76 | borderRadius: 5 77 | backgroundColor: if (i % 2) then colors.blue else colors.orange 78 | width: 10 79 | height: 10 80 | 81 | bubble.hook 82 | property: 'midPos' 83 | to: magnet 84 | type: 'gravity(' + gravity + ',' + drag + ')' 85 | 86 | initialAngle = Math.random() * Math.PI * 2 87 | 88 | bubble.hooks.velocities.pos.x += Math.cos(initialAngle) * 3000 89 | bubble.hooks.velocities.pos.y += Math.sin(initialAngle) * 3000 90 | 91 | 92 | magnet.onTouchEnd -> 93 | @animate 94 | properties: 95 | scale: 1 96 | -------------------------------------------------------------------------------- /hook-example-gravity.framer/framer/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "propertyPanelToggleStates" : { 3 | 4 | }, 5 | "deviceOrientation" : 0, 6 | "sharedPrototype" : 0, 7 | "contentScale" : 1, 8 | "deviceType" : "apple-iphone-6s-silver", 9 | "selectedHand" : "", 10 | "updateDelay" : 0.3, 11 | "deviceScale" : "fit", 12 | "foldedCodeRanges" : [ 13 | 14 | ], 15 | "orientation" : 0, 16 | "fullScreen" : false 17 | } -------------------------------------------------------------------------------- /hook-example-gravity.framer/framer/framer.generated.js: -------------------------------------------------------------------------------- 1 | // This is autogenerated by Framer 2 | 3 | 4 | if (!window.Framer && window._bridge) {window._bridge('runtime.error', {message:'[framer.js] Framer library missing or corrupt. Select File → Update Framer Library.'})} 5 | if (DeviceComponent) {DeviceComponent.Devices["iphone-6-silver"].deviceImageJP2 = false}; 6 | if (window.Framer) {window.Framer.Defaults.DeviceView = {"deviceScale":"fit","selectedHand":"","deviceType":"apple-iphone-6s-silver","contentScale":1,"orientation":0}; 7 | } 8 | if (window.Framer) {window.Framer.Defaults.DeviceComponent = {"deviceScale":"fit","selectedHand":"","deviceType":"apple-iphone-6s-silver","contentScale":1,"orientation":0}; 9 | } 10 | window.FramerStudioInfo = {"deviceImagesUrl":"\/_server\/resources\/DeviceImages","documentTitle":"hook-example-gravity.framer"}; 11 | 12 | Framer.Device = new Framer.DeviceView(); 13 | Framer.Device.setupContext(); -------------------------------------------------------------------------------- /hook-example-gravity.framer/framer/framer.init.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | function isFileLoadingAllowed() { 4 | return (window.location.protocol.indexOf("file") == -1) 5 | } 6 | 7 | function isHomeScreened() { 8 | return ("standalone" in window.navigator) && window.navigator.standalone == true 9 | } 10 | 11 | function isCompatibleBrowser() { 12 | return Utils.isWebKit() 13 | } 14 | 15 | var alertNode; 16 | 17 | function dismissAlert() { 18 | alertNode.parentElement.removeChild(alertNode) 19 | loadProject() 20 | } 21 | 22 | function showAlert(html) { 23 | 24 | alertNode = document.createElement("div") 25 | 26 | alertNode.classList.add("framerAlertBackground") 27 | alertNode.innerHTML = html 28 | 29 | document.addEventListener("DOMContentLoaded", function(event) { 30 | document.body.appendChild(alertNode) 31 | }) 32 | 33 | window.dismissAlert = dismissAlert; 34 | } 35 | 36 | function showBrowserAlert() { 37 | var html = "" 38 | html += "
" 39 | html += "Error: Not A WebKit Browser" 40 | html += "Your browser is not supported.
Please use Safari or Chrome.
" 41 | html += "Try anyway" 42 | html += "
" 43 | 44 | showAlert(html) 45 | } 46 | 47 | function showFileLoadingAlert() { 48 | var html = "" 49 | html += "
" 50 | html += "Error: Local File Restrictions" 51 | html += "Preview this prototype with Framer Mirror or learn more about " 52 | html += "file restrictions.
" 53 | html += "Try anyway" 54 | html += "
" 55 | 56 | showAlert(html) 57 | } 58 | 59 | function loadProject() { 60 | CoffeeScript.load("app.coffee") 61 | } 62 | 63 | function setDefaultPageTitle() { 64 | // If no title was set we set it to the project folder name so 65 | // you get a nice name on iOS if you bookmark to desktop. 66 | document.addEventListener("DOMContentLoaded", function() { 67 | if (document.title == "") { 68 | if (window.FramerStudioInfo && window.FramerStudioInfo.documentTitle) { 69 | document.title = window.FramerStudioInfo.documentTitle 70 | } else { 71 | document.title = window.location.pathname.replace(/\//g, "") 72 | } 73 | } 74 | }) 75 | } 76 | 77 | function init() { 78 | 79 | if (Utils.isFramerStudio()) { 80 | return 81 | } 82 | 83 | setDefaultPageTitle() 84 | 85 | if (!isCompatibleBrowser()) { 86 | return showBrowserAlert() 87 | } 88 | 89 | if (!isFileLoadingAllowed()) { 90 | return showFileLoadingAlert() 91 | } 92 | 93 | loadProject() 94 | 95 | } 96 | 97 | init() 98 | 99 | })() 100 | -------------------------------------------------------------------------------- /hook-example-gravity.framer/framer/framer.modules.js: -------------------------------------------------------------------------------- 1 | require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o this.length) { 146 | return false; 147 | } else { 148 | return this.indexOf(search, start) !== -1; 149 | } 150 | }; 151 | } 152 | 153 | Layer.prototype.hook = function(config) { 154 | var base, base1, f, name; 155 | if (!(config.property && config.type && (config.to || config.targetProperty))) { 156 | throw new Error('layer.hook() needs a property, a hook type and either a target object or target property to work'); 157 | } 158 | if (this.hooks == null) { 159 | this.hooks = { 160 | hooks: [], 161 | velocities: {}, 162 | defs: { 163 | zoom: 100, 164 | getDrag: (function(_this) { 165 | return function(velocity, drag, zoom) { 166 | velocity /= zoom; 167 | drag = -(drag / 10) * velocity * velocity * velocity / Math.abs(velocity); 168 | if (_.isNaN(drag)) { 169 | return 0; 170 | } else { 171 | return drag; 172 | } 173 | }; 174 | })(this), 175 | getGravity: (function(_this) { 176 | return function(strength, distance, zoom) { 177 | var dist; 178 | dist = Math.max(1, distance / zoom); 179 | return strength * zoom / (dist * dist); 180 | }; 181 | })(this) 182 | } 183 | }; 184 | } 185 | if (config.zoom) { 186 | this.hooks.zoom = config.zoom; 187 | } 188 | f = Utils.parseFunction(config.type); 189 | config.type = f.name; 190 | config.strength = f.args[0]; 191 | config.friction = f.args[1] || 0; 192 | if (config.targetProperty == null) { 193 | config.targetProperty = config.property; 194 | } 195 | if (config.to == null) { 196 | config.to = this; 197 | } 198 | if (config.property.toLowerCase().includes('pos')) { 199 | config.prop = 'pos'; 200 | if (config.property.toLowerCase().includes('mid')) { 201 | config.thisX = 'midX'; 202 | config.thisY = 'midY'; 203 | } else if (config.property.toLowerCase().includes('max')) { 204 | config.thisX = 'maxX'; 205 | config.thisY = 'maxY'; 206 | } else { 207 | config.thisX = 'x'; 208 | config.thisY = 'y'; 209 | } 210 | if (config.targetProperty.toLowerCase().includes('mid')) { 211 | config.toX = 'midX'; 212 | config.toY = 'midY'; 213 | } else if (config.targetProperty.toLowerCase().includes('max')) { 214 | config.toX = 'maxX'; 215 | config.toY = 'maxY'; 216 | } else { 217 | config.toX = 'x'; 218 | config.toY = 'y'; 219 | } 220 | } else { 221 | config.prop = config.property; 222 | } 223 | this.hooks.hooks.push(config); 224 | if ((base = this.hooks.velocities)[name = config.prop] == null) { 225 | base[name] = config.prop === 'pos' ? { 226 | x: 0, 227 | y: 0 228 | } : 0; 229 | } 230 | return (base1 = this.hooks).emitter != null ? base1.emitter : base1.emitter = Framer.Loop.on('render', this.hookLoop, this); 231 | }; 232 | 233 | Layer.prototype.unHook = function(property, object) { 234 | var prop, remaining; 235 | if (!this.hooks) { 236 | return; 237 | } 238 | prop = property.toLowerCase().includes('pos') ? 'pos' : property; 239 | this.hooks.hooks = this.hooks.hooks.filter(function(hook) { 240 | return hook.to !== object || hook.property !== property; 241 | }); 242 | if (this.hooks.hooks.length === 0) { 243 | delete this.hooks; 244 | Framer.Loop.removeListener('render', this.hookLoop); 245 | return; 246 | } 247 | remaining = this.hooks.hooks.filter(function(hook) { 248 | return prop === hook.prop; 249 | }); 250 | if (remaining.length === 0) { 251 | return delete this.hooks.velocities[prop]; 252 | } 253 | }; 254 | 255 | Layer.prototype.hookLoop = function(delta) { 256 | var acceleration, damper, drag, force, gravity, hook, i, len, name, prop, ref, ref1, target, vLength, vector, velocity; 257 | if (this.hooks) { 258 | acceleration = {}; 259 | drag = {}; 260 | ref = this.hooks.hooks; 261 | for (i = 0, len = ref.length; i < len; i++) { 262 | hook = ref[i]; 263 | if (hook.prop === 'pos') { 264 | if (acceleration.pos == null) { 265 | acceleration.pos = { 266 | x: 0, 267 | y: 0 268 | }; 269 | } 270 | target = { 271 | x: hook.to[hook.toX], 272 | y: hook.to[hook.toY] 273 | }; 274 | if (hook.modulator) { 275 | target = hook.modulator(target); 276 | } 277 | vector = { 278 | x: target.x - this[hook.thisX], 279 | y: target.y - this[hook.thisY] 280 | }; 281 | vLength = Math.sqrt((vector.x * vector.x) + (vector.y * vector.y)); 282 | if (hook.type === 'spring') { 283 | damper = { 284 | x: -hook.friction * this.hooks.velocities.pos.x, 285 | y: -hook.friction * this.hooks.velocities.pos.y 286 | }; 287 | vector.x *= hook.strength; 288 | vector.y *= hook.strength; 289 | acceleration.pos.x += (vector.x + damper.x) * delta; 290 | acceleration.pos.y += (vector.y + damper.y) * delta; 291 | } else if (hook.type === 'gravity') { 292 | drag.pos = hook.friction; 293 | gravity = this.hooks.defs.getGravity(hook.strength, vLength, this.hooks.defs.zoom); 294 | vector.x *= gravity / vLength; 295 | vector.y *= gravity / vLength; 296 | acceleration.pos.x += vector.x * delta; 297 | acceleration.pos.y += vector.y * delta; 298 | } 299 | } else { 300 | if (acceleration[name = hook.prop] == null) { 301 | acceleration[name] = 0; 302 | } 303 | target = hook.to[hook.targetProperty]; 304 | if (hook.modulator) { 305 | target = hook.modulator(target); 306 | } 307 | vector = target - this[hook.prop]; 308 | if (hook.type === 'spring') { 309 | force = vector * hook.strength; 310 | damper = -hook.friction * this.hooks.velocities[hook.prop]; 311 | acceleration[hook.prop] += (force + damper) * delta; 312 | } else if (hook.type === 'gravity') { 313 | drag[hook.prop] = hook.friction; 314 | force = this.hooks.defs.getGravity(hook.strength, vector, this.hooks.defs.zoom); 315 | acceleration[hook.prop] += force * delta; 316 | } 317 | } 318 | } 319 | ref1 = this.hooks.velocities; 320 | for (prop in ref1) { 321 | velocity = ref1[prop]; 322 | if (prop === 'pos') { 323 | if (drag.pos) { 324 | velocity.x += this.hooks.defs.getDrag(velocity.x, drag.pos, this.hooks.defs.zoom); 325 | velocity.y += this.hooks.defs.getDrag(velocity.y, drag.pos, this.hooks.defs.zoom); 326 | } 327 | velocity.x += acceleration.pos.x; 328 | velocity.y += acceleration.pos.y; 329 | this.x += velocity.x * delta; 330 | this.y += velocity.y * delta; 331 | } else { 332 | if (drag[prop]) { 333 | this.hooks.velocities[prop] += this.hooks.defs.getDrag(this.hooks.velocities[prop], drag[prop], this.hooks.defs.zoom); 334 | } 335 | this.hooks.velocities[prop] += acceleration[prop]; 336 | this[prop] += this.hooks.velocities[prop] * delta; 337 | } 338 | } 339 | return typeof this.onHookUpdate === "function" ? this.onHookUpdate(delta) : void 0; 340 | } 341 | }; 342 | 343 | 344 | },{}]},{},[]) 345 | //# sourceMappingURL=data:application/json;charset:utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIm5vZGVfbW9kdWxlcy9icm93c2VyaWZ5L25vZGVfbW9kdWxlcy9icm93c2VyLXBhY2svX3ByZWx1ZGUuanMiLCIvVXNlcnMvc2lndXJkL1JlcG9zL2ZyYW1lci1ob29rL2hvb2stZXhhbXBsZS1ncmF2aXR5LmZyYW1lci9tb2R1bGVzL0hvb2suY29mZmVlIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztBQ0FBOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBNklBLElBQUEsQ0FBTyxNQUFNLENBQUMsU0FBUyxDQUFDLFFBQXhCO0VBQ0MsTUFBTSxDQUFBLFNBQUUsQ0FBQSxRQUFSLEdBQW1CLFNBQUMsTUFBRCxFQUFTLEtBQVQ7SUFDbEI7SUFDQSxJQUFhLE9BQU8sS0FBUCxLQUFnQixRQUE3QjtNQUFBLEtBQUEsR0FBUSxFQUFSOztJQUVBLElBQUcsS0FBQSxHQUFRLE1BQU0sQ0FBQyxNQUFmLEdBQXdCLElBQUksQ0FBQyxNQUFoQztBQUNDLGFBQU8sTUFEUjtLQUFBLE1BQUE7QUFHQyxhQUFPLElBQUMsQ0FBQSxPQUFELENBQVMsTUFBVCxFQUFpQixLQUFqQixDQUFBLEtBQTZCLENBQUMsRUFIdEM7O0VBSmtCLEVBRHBCOzs7QUFZQSxLQUFLLENBQUEsU0FBRSxDQUFBLElBQVAsR0FBYyxTQUFDLE1BQUQ7QUFFYixNQUFBO0VBQUEsSUFBQSxDQUFBLENBQTBILE1BQU0sQ0FBQyxRQUFQLElBQW9CLE1BQU0sQ0FBQyxJQUEzQixJQUFvQyxDQUFDLE1BQU0sQ0FBQyxFQUFQLElBQWEsTUFBTSxDQUFDLGNBQXJCLENBQTlKLENBQUE7QUFBQSxVQUFVLElBQUEsS0FBQSxDQUFNLGtHQUFOLEVBQVY7OztJQUdBLElBQUMsQ0FBQSxRQUNBO01BQUEsS0FB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QUssQ0FBQyxJQUFJLENBQUMsSUFBdEQsRUFGZjs7UUFLQSxRQUFRLENBQUMsQ0FBVCxJQUFjLFlBQVksQ0FBQyxHQUFHLENBQUM7UUFDL0IsUUFBUSxDQUFDLENBQVQsSUFBYyxZQUFZLENBQUMsR0FBRyxDQUFDO1FBRy9CLElBQUMsQ0FBQSxDQUFELElBQU0sUUFBUSxDQUFDLENBQVQsR0FBYTtRQUNuQixJQUFDLENBQUEsQ0FBRCxJQUFNLFFBQVEsQ0FBQyxDQUFULEdBQWEsTUFicEI7T0FBQSxNQUFBO1FBa0JDLElBQUcsSUFBSyxDQUFBLElBQUEsQ0FBUjtVQUNDLElBQUMsQ0FBQSxLQUFLLENBQUMsVUFBVyxDQUFBLElBQUEsQ0FBbEIsSUFBMkIsSUFBQyxDQUFBLEtBQUssQ0FBQyxJQUFJLENBQUMsT0FBWixDQUFvQixJQUFDLENBQUEsS0FBSyxDQUFDLFVBQVcsQ0FBQSxJQUFBLENBQXRDLEVBQTZDLElBQUssQ0FBQSxJQUFBLENBQWxELEVBQXlELElBQUMsQ0FBQSxLQUFLLENBQUMsSUFBSSxDQUFDLElBQXJFLEVBRDVCOztRQUlBLElBQUMsQ0FBQSxLQUFLLENBQUMsVUFBVyxDQUFBLElBQUEsQ0FBbEIsSUFBMkIsWUFBYSxDQUFBLElBQUE7UUFHeEMsSUFBRSxDQUFBLElBQUEsQ0FBRixJQUFXLElBQUMsQ0FBQSxLQUFLLENBQUMsVUFBVyxDQUFBLElBQUEsQ0FBbEIsR0FBMEIsTUF6QnRDOztBQUZEO3FEQTZCQSxJQUFDLENBQUEsYUFBYyxnQkExR2hCOztBQUZpQiIsImZpbGUiOiJnZW5lcmF0ZWQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlc0NvbnRlbnQiOlsiKGZ1bmN0aW9uIGUodCxuLHIpe2Z1bmN0aW9uIHMobyx1KXtpZighbltvXSl7aWYoIXRbb10pe3ZhciBhPXR5cGVvZiByZXF1aXJlPT1cImZ1bmN0aW9uXCImJnJlcXVpcmU7aWYoIXUmJmEpcmV0dXJuIGEobywhMCk7aWYoaSlyZXR1cm4gaShvLCEwKTt2YXIgZj1uZXcgRXJyb3IoXCJDYW5ub3QgZmluZCBtb2R1bGUgJ1wiK28rXCInXCIpO3Rocm93IGYuY29kZT1cIk1PRFVMRV9OT1RfRk9VTkRcIixmfXZhciBsPW5bb109e2V4cG9ydHM6e319O3Rbb11bMF0uY2FsbChsLmV4cG9ydHMsZnVuY3Rpb24oZSl7dmFyIG49dFtvXVsxXVtlXTtyZXR1cm4gcyhuP246ZSl9LGwsbC5leHBvcnRzLGUsdCxuLHIpfXJldHVybiBuW29dLmV4cG9ydHN9dmFyIGk9dHlwZW9mIHJlcXVpcmU9PVwiZnVuY3Rpb25cIiYmcmVxdWlyZTtmb3IodmFyIG89MDtvPHIubGVuZ3RoO28rKylzKHJbb10pO3JldHVybiBzfSkiLCIjIyNcbi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5Ib29rIG1vZHVsZSBmb3IgRnJhbWVyXG4tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLVxuXG5ieTogICAgICBTaWd1cmQgTWFubnPDpWtlclxuZ2l0aHViOiAgaHR0cHM6Ly9naXRodWIuY29tL3NpZ3RtL2ZyYW1lci1ob29rXG5cbsK3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrdcblxuXG5UaGUgSG9vayBtb2R1bGUgc2ltcGx5IGV4cGFuZHMgdGhlIExheWVyIHByb3RvdHlwZSwgYW5kIGxldHMgeW91IG1ha2UgYW55XG5udW1lcmljIExheWVyIHByb3BlcnR5IGZvbGxvdyBhbm90aGVyIHByb3BlcnR5IC0gZWl0aGVyIGl0cyBvd24gb3IgYW5vdGhlclxub2JqZWN0J3MgLSB2aWEgYSBzcHJpbmcgb3IgZ3Jhdml0eSBhdHRyYWN0aW9uLlxuXG5cbi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5FeGFtcGxlOiBMYXllcmVkIGFuaW1hdGlvbiAoZWFzZWQgKyBzcHJpbmcpXG4tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLVxuXG5teUxheWVyID0gbmV3IExheWVyXG5cbiMgTWFrZSBvdXIgb3duIGN1c3RvbSBwcm9wZXJ0eSBmb3IgdGhlIHggcHJvcGVydHkgdG8gZm9sbG93XG5teUxheWVyLmVhc2VkWCA9IDBcblxuIyBIb29rIHggdG8gZWFzZWRYIHZpYSBhIHNwcmluZ1xubXlMYXllci5ob29rXG5cdHByb3BlcnR5OiBcInhcIlxuXHR0YXJnZXRQcm9wZXJ0eTogXCJlYXNlZFhcIlxuXHR0eXBlOiBcInNwcmluZygxNTAsIDE1KVwiXG5cbiMgQW5pbWF0ZSBlYXNlZFhcbm15TGF5ZXIuYW5pbWF0ZVxuXHRwcm9wZXJ0aWVzOlxuXHRcdGVhc2VkWDogMjAwXG5cdHRpbWU6IDAuMTVcblx0Y3VydmU6IFwiY3ViaWMtYmV6aWVyKDAuMiwgMCwgMC40LCAxKVwiXG5cbk5PVEU6IFxuVG8gYXR0YWNoIGJvdGggdGhlIHggYW5kIHkgcG9zaXRpb24sIHVzZSBcInBvc1wiLCBcIm1pZFBvc1wiIG9yIFwibWF4UG9zXCIgYXMgdGhlXG5wcm9wZXJ0eS90YXJnZXRQcm9wZXJ0eS5cblxuXG4tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLVxuRXhhbXBsZTogSG9va2luZyBwcm9wZXJ0eSB0byBhbm90aGVyIGxheWVyXG4tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLVxuXG50YXJnZXQgPSBuZXcgTGF5ZXJcbmhvb2tlZCA9IG5ldyBMYXllclxuXG5ob29rZWQuaG9va1xuXHRwcm9wZXJ0eTogXCJzY2FsZVwiXG5cdHRvOiB0YXJnZXRcblx0dHlwZTogXCJzcHJpbmcoMTUwLCAxNSlcIlxuXG5UaGUgXCJob29rZWRcIiBsYXllcidzIHNjYWxlIHdpbGwgbm93IGNvbnRpbnVvdXNseSBmb2xsb3cgdGhlIHRhcmdldCBsYXllcidzIHNjYWxlXG53aXRoIGEgc3ByaW5nIGFuaW1hdGlvbi5cblxuXG4tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLVxubGF5ZXIuaG9vayhvcHRpb25zKVxuLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS1cblxuT3B0aW9ucyBhcmUgcGFzc2VkIGFzIGEgc2luZ2xlIG9iamVjdCwgbGlrZSB5b3Ugd291bGQgZm9yIGEgbmV3IExheWVyLlxuVGhlIG9wdGlvbnMgb2JqZWN0IHRha2VzIHRoZSBmb2xsb3dpbmcgcHJvcGVydGllczpcblxuXG5wcm9wZXJ0eSBbU3RyaW5nXVxuLS0tLS0tLS0tLS0tLS0tLS1cblRoZSBwcm9wZXJ0eSB5b3UnZCBsaWtlIHRvIGhvb2sgb250byBhbm90aGVyIG9iamVjdCdzIHByb3BlcnR5XG5cblxudHlwZSBbU3RyaW5nXVxuLS0tLS0tLS0tLS0tLVxuRWl0aGVyIFwic3ByaW5nKHN0cmVuZ3RoLCBmcmljdGlvbilcIiBvciBcImdyYXZpdHkoc3RyZW5ndGgsIGRyYWcpXCIuIE9ubHkgdGhlIGxhc3RcbnNwZWNpZmllZCBkcmFnIHZhbHVlIGlzIHVzZWQgZm9yIGVhY2ggcHJvcGVydHksIHNpbmNlIGl0IGlzIG9ubHkgYXBwbGllZCB0b1xuZWFjaCBwcm9wZXJ0eSBvbmNlIChhbmQgb25seSBpZiBpdCBoYXMgYSBncmF2aXR5IGhvb2sgYXBwbGllZCB0byBpdC4pXG5cblxudG8gW09iamVjdF0gKE9wdGlvbmFsKVxuLS0tLS0tLS0tLS0tLS0tLS0tLS0tLVxuVGhlIG9iamVjdCB0byBhdHRhY2ggaXQgdG8uIERlZmF1bHRzIHRvIGl0c2VsZi5cblxuXG50YXJnZXRQcm9wZXJ0eSBbU3RyaW5nXSAoT3B0aW9uYWwpXG4tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5TcGVjaWZ5IHRoZSB0YXJnZXQgb2JqZWN0J3MgcHJvcGVydHkgdG8gZm9sbG93LCBpZiB5b3UgZG9uJ3Qgd2FudCB0byBmb2xsb3dcbnRoZSBzYW1lIHByb3BlcnR5IHRoYXQgdGhlIGhvb2sgaXMgYXBwbGllZCB0by5cblxuXG5tb2R1bGF0b3IgW0Z1bmN0aW9uXSAoT3B0aW9uYWwpXG4tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5UaGUgbW9kdWxhdG9yIGZ1bmN0aW9uIHJlY2VpdmVzIHRoZSB0YXJnZXQgcHJvcGVydHkncyB2YWx1ZSwgYW5kIGxldHMgeW91XG5tb2RpZnkgaXQgYmVmb3JlIGl0IGlzIGZlZCBpbnRvIHRoZSBwaHlzaWNzIGNhbGN1bGF0aW9ucy4gVXNlZnVsIGZvciBhbnl0aGluZ1xuZnJvbSBzdGFuZGFyZCBVdGlscy5tb2R1bGF0ZSgpIHR5cGUgc3R1ZmYgdG8gc25hcHBpbmcgYW5kIGNvbmRpdGlvbmFsIHZhbHVlcy5cblxuXG56b29tIFtOdW1iZXJdIChPcHRpb25hbClcbi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLVxuVGhpcyBmYWN0b3IgZGVmaW5lcyB0aGUgZGlzdGFuY2UgdGhhdCAxcHggcmVwcmVzZW50cyBpbiByZWdhcmRzIHRvIGdyYXZpdHkgYW5kXG5kcmFnIGNhbGN1bGF0aW9ucy4gT25seSBvbmUgdmFsdWUgaXMgc3RvcmVkIHBlciBsYXllciwgc28gc3BlY2lmeWluZyBpdFxub3ZlcndyaXRlcyBpdHMgZXhpc3RpbmcgdmFsdWUuIERlZmF1bHQgaXMgMTAwLlxuXG5cbi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5sYXllci51bkhvb2socHJvcGVydHksIG9iamVjdClcbi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5cblRoaXMgcmVtb3ZlcyBhbGwgaG9va3MgZm9yIGEgZ2l2ZW4gcHJvcGVydHkgYW5kIHRhcmdldCBvYmplY3QuIEV4YW1wbGU6XG5cbiMgSG9vayBpdFxubGF5ZXIuaG9va1xuXHRwcm9wZXJ0eTogXCJ4XCJcblx0dG86IFwib3RoZXJsYXllclwiXG5cdHRhcmdldFByb3BlcnR5OiBcInlcIlxuXHR0eXBlOiBcInNwcmluZygyMDAsMjApXCJcblxuIyBVbmhvb2sgaXRcbmxheWVyLnVuSG9vayBcInhcIiwgb3RoZXJsYXllclxuXG5cbi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5sYXllci5vbkhvb2tVcGRhdGUoZGVsdGEpXG4tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLVxuXG5BZnRlciBhIGxheWVyIGlzIGRvbmUgYXBwbHlpbmcgYWNjZWxlcmF0aW9ucyB0byBpdHMgaG9va2VkIHByb3BlcnRpZXMsIGl0IGNhbGxzXG5vbkhvb2tVcGRhdGUoKSBhdCB0aGUgZW5kIG9mIGVhY2ggZnJhbWUsIGlmIGl0IGlzIGRlZmluZWQuIFRoaXMgaXMgYW4gZWFzeSB3YXlcbnRvIGFuaW1hdGUgb3IgdHJpZ2dlciBvdGhlciBzdHVmZiwgcGVyaGFwcyBiYXNlZCBvbiB5b3VyIGxheWVyJ3MgdXBkYXRlZFxucHJvcGVydGllcyBvciB2ZWxvY2l0aWVzLlxuXG5UaGUgZGVsdGEgdmFsdWUgZnJvbSB0aGUgRnJhbWVyIGxvb3AgaXMgcGFzc2VkIG9uIHRvIG9uSG9va1VwZGF0ZSgpIGFzIHdlbGwsXG53aGljaCBpcyB0aGUgdGltZSBpbiBzZWNvbmRzIHNpbmNlIHRoZSBsYXN0IGFuaW1hdGlvbiBmcmFtZS5cblxuTm90ZSB0aGF0IGlmIHlvdSB1bmhvb2sgYWxsIHlvdXIgaG9va3MsIG9uSG9va1VwZGF0ZSgpIHdpbGwgb2YgY291cnNlIG5vIGxvbmdlclxuYmUgY2FsbGVkIGZvciB0aGF0IGxheWVyLlxuXG4jIyNcblxuXG4jIFNpbmNlIG9sZGVyIHZlcnNpb25zIG9mIFNhZmFyaSBzZWVtIHRvIGJlIG1pc3NpbmcgU3RyaW5nLnByb3RvdHlwZS5pbmNsdWRlcygpXG5cbnVubGVzcyBTdHJpbmcucHJvdG90eXBlLmluY2x1ZGVzXG5cdFN0cmluZzo6aW5jbHVkZXMgPSAoc2VhcmNoLCBzdGFydCkgLT5cblx0XHQndXNlIHN0cmljdCdcblx0XHRzdGFydCA9IDAgaWYgdHlwZW9mIHN0YXJ0IGlzICdudW1iZXInXG5cblx0XHRpZiBzdGFydCArIHNlYXJjaC5sZW5ndGggPiB0aGlzLmxlbmd0aFxuXHRcdFx0cmV0dXJuIGZhbHNlO1xuXHRcdGVsc2Vcblx0XHRcdHJldHVybiBAaW5kZXhPZihzZWFyY2gsIHN0YXJ0KSBpc250IC0xXG5cbiMgRXhwYW5kIGxheWVyXG5cbkxheWVyOjpob29rID0gKGNvbmZpZykgLT5cblxuXHR0aHJvdyBuZXcgRXJyb3IgJ2xheWVyLmhvb2soKSBuZWVkcyBhIHByb3BlcnR5LCBhIGhvb2sgdHlwZSBhbmQgZWl0aGVyIGEgdGFyZ2V0IG9iamVjdCBvciB0YXJnZXQgcHJvcGVydHkgdG8gd29yaycgdW5sZXNzIGNvbmZpZy5wcm9wZXJ0eSBhbmQgY29uZmlnLnR5cGUgYW5kIChjb25maWcudG8gb3IgY29uZmlnLnRhcmdldFByb3BlcnR5KVxuXG5cdCMgU2luZ2xlIGFycmF5IGZvciBhbGwgaG9va3MsIGFzIG9wcG9zZWQgdG8gbmVzdGVkIGFycmF5cyBwZXIgcHJvcGVydHksIGJlY2F1c2UgcGVyZm9ybWFuY2Vcblx0QGhvb2tzID89XG5cdFx0aG9va3M6IFtdXG5cdFx0dmVsb2NpdGllczoge31cblx0XHRkZWZzOlxuXHRcdFx0em9vbTogMTAwXG5cdFx0XHRnZXREcmFnOiAodmVsb2NpdHksIGRyYWcsIHpvb20pID0+XG5cdFx0XHRcdHZlbG9jaXR5IC89IHpvb21cblx0XHRcdFx0IyBEaXZpZGluZyBieSAxMCBpcyB1bnNjaWVudGlmaWMsIGJ1dCBpdCBtZWFucyBhIHZhbHVlIG9mIDIgZXF1YWxzIHJvdWdobHkgYSAxMDBnIGJhbGwgd2l0aCAxNWNtIHJhZGl1cyBpbiBhaXJcblx0XHRcdFx0ZHJhZyA9IC0oZHJhZyAvIDEwKSAqIHZlbG9jaXR5ICogdmVsb2NpdHkgKiB2ZWxvY2l0eSAvIE1hdGguYWJzKHZlbG9jaXR5KVxuXHRcdFx0XHRpZiBfLmlzTmFOKGRyYWcpIHRoZW4gcmV0dXJuIDAgZWxzZSByZXR1cm4gZHJhZ1xuXHRcdFx0Z2V0R3Jhdml0eTogKHN0cmVuZ3RoLCBkaXN0YW5jZSwgem9vbSkgPT5cblx0XHRcdFx0ZGlzdCA9IE1hdGgubWF4KDEsIGRpc3RhbmNlIC8gem9vbSlcblx0XHRcdFx0cmV0dXJuIHN0cmVuZ3RoICogem9vbSAvIChkaXN0ICogZGlzdClcblxuXHQjIFVwZGF0ZSB0aGUgem9vbSB2YWx1ZSBpZiBnaXZlblxuXHRAaG9va3Muem9vbSA9IGNvbmZpZy56b29tIGlmIGNvbmZpZy56b29tXG5cblx0IyBQYXJzZSBwaHlzaWNzIGNvbmZpZyBzdHJpbmdcblx0ZiA9IFV0aWxzLnBhcnNlRnVuY3Rpb24gY29uZmlnLnR5cGVcblx0Y29uZmlnLnR5cGUgPSBmLm5hbWVcblx0Y29uZmlnLnN0cmVuZ3RoID0gZi5hcmdzWzBdXG5cdGNvbmZpZy5mcmljdGlvbiA9IGYuYXJnc1sxXSBvciAwXG5cblx0IyBEZWZhdWx0IHRvIHNhbWUgdGFyZ2V0UHJvcGVydHkgb24gc2FtZSBvYmplY3QgKGhvcGVmdWxseSB5b3UndmUgc2V0IGF0IGxlYXN0IG9uZSBvZiB0aGVzZSB0byBzb21ldGhpbmcgZWxzZSlcblx0Y29uZmlnLnRhcmdldFByb3BlcnR5ID89IGNvbmZpZy5wcm9wZXJ0eVxuXHRjb25maWcudG8gPz0gQFxuXG5cdCMgQWxsIHBvc2l0aW9uIGFjY2VsZXJhdGlvbnMgYXJlIGFkZGVkIHRvIGEgc2luZ2xlICdwb3MnIHZlbG9jaXR5LiBTdG9yZSBhY3R1YWwgcHJvcGVydGllcyBzbyB3ZSBkb24ndCBoYXZlIHRvIGRvIGl0IGFnYWluIGV2ZXJ5IGZyYW1lXG5cblx0aWYgY29uZmlnLnByb3BlcnR5LnRvTG93ZXJDYXNlKCkuaW5jbHVkZXMgJ3Bvcydcblx0XHRjb25maWcucHJvcCA9ICdwb3MnXG5cdFx0XG5cdFx0aWYgY29uZmlnLnByb3BlcnR5LnRvTG93ZXJDYXNlKCkuaW5jbHVkZXMgJ21pZCdcblx0XHRcdGNvbmZpZy50aGlzWCA9ICdtaWRYJ1xuXHRcdFx0Y29uZmlnLnRoaXNZID0gJ21pZFknXG5cdFx0XG5cdFx0ZWxzZSBpZiBjb25maWcucHJvcGVydHkudG9Mb3dlckNhc2UoKS5pbmNsdWRlcyAnbWF4J1xuXHRcdFx0Y29uZmlnLnRoaXNYID0gJ21heFgnXG5cdFx0XHRjb25maWcudGhpc1kgPSAnbWF4WSdcblx0XHRcblx0XHRlbHNlXG5cdFx0XHRjb25maWcudGhpc1ggPSAneCdcblx0XHRcdGNvbmZpZy50aGlzWSA9ICd5J1xuXHRcdFxuXHRcdGlmIGNvbmZpZy50YXJnZXRQcm9wZXJ0eS50b0xvd2VyQ2FzZSgpLmluY2x1ZGVzICdtaWQnXG5cdFx0XHRjb25maWcudG9YID0gJ21pZFgnXG5cdFx0XHRjb25maWcudG9ZID0gJ21pZFknXG5cdFx0XG5cdFx0ZWxzZSBpZiBjb25maWcudGFyZ2V0UHJvcGVydHkudG9Mb3dlckNhc2UoKS5pbmNsdWRlcyAnbWF4J1xuXHRcdFx0Y29uZmlnLnRvWCA9ICdtYXhYJ1xuXHRcdFx0Y29uZmlnLnRvWSA9ICdtYXhZJ1x0XHRcblx0XHRlbHNlXG5cdFx0XHRjb25maWcudG9YID0gJ3gnXG5cdFx0XHRjb25maWcudG9ZID0gJ3knXG5cdFx0XG5cdGVsc2Vcblx0XHRjb25maWcucHJvcCA9IGNvbmZpZy5wcm9wZXJ0eVxuXG5cdCMgU2F2ZSBob29rIHRvIEBob29rcyBhcnJheVx0XG5cdEBob29rcy5ob29rcy5wdXNoKGNvbmZpZylcblxuXHQjIENyZWF0ZSB2ZWxvY2l0eSBwcm9wZXJ0eSBpZiBuZWNlc3Nhcnlcblx0QGhvb2tzLnZlbG9jaXRpZXNbY29uZmlnLnByb3BdID89IGlmIGNvbmZpZy5wcm9wIGlzICdwb3MnIHRoZW4geyB4OiAwLCB5OiAwIH0gZWxzZSAwXG5cblx0IyBVc2UgRnJhbWVyJ3MgYW5pbWF0aW9uIGxvb3AsIHNsaWdodGx5IG1vcmUgcm9idXN0IHRoYW4gcmVxdWVzdEFuaW1hdGlvbkZyYW1lIGRpcmVjdGx5XG5cdCMgU2F2ZSB0aGUgcmV0dXJuZWQgQW5pbWF0aW9uTG9vcCByZWZlcmVuY2UgdG8gbWFrZSBzdXJlIEBob29rTG9vcCBpc24ndCBhZGRlZCBtdWx0aXBsZSB0aW1lcyBwZXIgbGF5ZXJcblx0QGhvb2tzLmVtaXR0ZXIgPz0gRnJhbWVyLkxvb3Aub24oJ3JlbmRlcicsIEBob29rTG9vcCwgdGhpcylcblxuTGF5ZXI6OnVuSG9vayA9IChwcm9wZXJ0eSwgb2JqZWN0KSAtPlxuXHRcblx0cmV0dXJuIHVubGVzcyBAaG9va3NcblxuXHRwcm9wID0gaWYgcHJvcGVydHkudG9Mb3dlckNhc2UoKS5pbmNsdWRlcyAncG9zJyB0aGVuICdwb3MnIGVsc2UgcHJvcGVydHlcblxuXHQjIFJlbW92ZSBhbGwgbWF0Y2hlc1xuXHRAaG9va3MuaG9va3MgPSBAaG9va3MuaG9va3MuZmlsdGVyIChob29rKSAtPlxuXHRcdGhvb2sudG8gaXNudCBvYmplY3Qgb3IgaG9vay5wcm9wZXJ0eSBpc250IHByb3BlcnR5XG5cblx0IyBJZiB0aGVyZSBhcmUgbm8gaG9va3MgbGVmdCwgc2h1dCBpdCBkb3duXG5cdGlmIEBob29rcy5ob29rcy5sZW5ndGggaXMgMFxuXHRcdGRlbGV0ZSBAaG9va3Ncblx0XHRGcmFtZXIuTG9vcC5yZW1vdmVMaXN0ZW5lciAncmVuZGVyJywgQGhvb2tMb29wXG5cdFx0cmV0dXJuXG5cblx0IyBTdGlsbCBoZXJlPyBDaGVjayBpZiB0aGVyZSBhcmUgYW55IHJlbWFpbmluZyBob29rcyBhZmZlY3Rpbmcgc2FtZSB2ZWxvY2l0eVxuXHRyZW1haW5pbmcgPSBAaG9va3MuaG9va3MuZmlsdGVyIChob29rKSAtPlxuXHRcdHByb3AgaXMgaG9vay5wcm9wXG5cdFx0XG5cdCMgSWYgbm90LCBkZWxldGUgdmVsb2NpdHkgKG90aGVyd2lzZSBpdCB3b24ndCBiZSByZXNldCBpZiB5b3UgbWFrZSBuZXcgaG9vayBmb3Igc2FtZSBwcm9wZXJ0eSlcblx0ZGVsZXRlIEBob29rcy52ZWxvY2l0aWVzW3Byb3BdIGlmIHJlbWFpbmluZy5sZW5ndGggaXMgMFxuXG5MYXllcjo6aG9va0xvb3AgPSAoZGVsdGEpIC0+XG5cblx0aWYgQGhvb2tzXG5cblx0XHQjIE11bHRpcGxlIGhvb2tzIGNhbiBhZmZlY3QgdGhlIHNhbWUgcHJvcGVydHkuIEFkZCBhY2NlbGVyYXRpb25zIHRvIHRlbXBvcmFyeSBvYmplY3Qgc28gdGhlIHByb3BlcnR5J3MgdmVsb2NpdHkgaXMgdGhlIHNhbWUgZm9yIGFsbCBjYWxjdWxhdGlvbnMgd2l0aGluIHRoZSBzYW1lIGFuaW1hdGlvbiBmcmFtZVxuXHRcdGFjY2VsZXJhdGlvbiA9IHt9XG5cdFx0XG5cdFx0IyBTYXZlIGRyYWcgZm9yIGVhY2ggcHJvcGVydHkgdG8gdGhpcyBvYmplY3QsIHNpbmNlIG9ubHkgbW9zdCByZWNlbnRseSBzcGVjaWZpZWQgdmFsdWUgaXMgdXNlZCBmb3IgZWFjaCBwcm9wZXJ0eVxuXHRcdGRyYWcgPSB7fVxuXHRcdFxuXHRcdCMgQWRkIGFjY2VsZXJhdGlvbnNcblx0XHRmb3IgaG9vayBpbiBAaG9va3MuaG9va3Ncblx0XHRcblx0XHRcdGlmIGhvb2sucHJvcCBpcyAncG9zJ1xuXG5cdFx0XHRcdGFjY2VsZXJhdGlvbi5wb3MgPz0geyB4OiAwLCB5OiAwIH1cblxuXHRcdFx0XHR0YXJnZXQgPSB7IHg6IGhvb2sudG9baG9vay50b1hdLCB5OiBob29rLnRvW2hvb2sudG9ZXSB9XG5cblx0XHRcdFx0dGFyZ2V0ID0gaG9vay5tb2R1bGF0b3IodGFyZ2V0KSBpZiBob29rLm1vZHVsYXRvclxuXG5cdFx0XHRcdHZlY3RvciA9XG5cdFx0XHRcdFx0eDogdGFyZ2V0LnggLSBAW2hvb2sudGhpc1hdXG5cdFx0XHRcdFx0eTogdGFyZ2V0LnkgLSBAW2hvb2sudGhpc1ldXG5cdFx0XHRcdFxuXHRcdFx0XHR2TGVuZ3RoID0gTWF0aC5zcXJ0KCh2ZWN0b3IueCAqIHZlY3Rvci54KSArICh2ZWN0b3IueSAqIHZlY3Rvci55KSlcblxuXHRcdFx0XHRpZiBob29rLnR5cGUgaXMgJ3NwcmluZydcblxuXHRcdFx0XHRcdGRhbXBlciA9XG5cdFx0XHRcdFx0XHR4OiAtaG9vay5mcmljdGlvbiAqIEBob29rcy52ZWxvY2l0aWVzLnBvcy54XG5cdFx0XHRcdFx0XHR5OiAtaG9vay5mcmljdGlvbiAqIEBob29rcy52ZWxvY2l0aWVzLnBvcy55XG5cblx0XHRcdFx0XHR2ZWN0b3IueCAqPSBob29rLnN0cmVuZ3RoXG5cdFx0XHRcdFx0dmVjdG9yLnkgKj0gaG9vay5zdHJlbmd0aFxuXHRcdFx0XHRcdFxuXHRcdFx0XHRcdGFjY2VsZXJhdGlvbi5wb3MueCArPSAodmVjdG9yLnggKyBkYW1wZXIueCkgKiBkZWx0YVxuXHRcdFx0XHRcdGFjY2VsZXJhdGlvbi5wb3MueSArPSAodmVjdG9yLnkgKyBkYW1wZXIueSkgKiBkZWx0YVxuXHRcdFx0XHRcblx0XHRcdFx0ZWxzZSBpZiBob29rLnR5cGUgaXMgJ2dyYXZpdHknXG5cdFx0XHRcdFxuXHRcdFx0XHRcdGRyYWcucG9zID0gaG9vay5mcmljdGlvblxuXHRcdFx0XHRcdFxuXHRcdFx0XHRcdGdyYXZpdHkgPSBAaG9va3MuZGVmcy5nZXRHcmF2aXR5KGhvb2suc3RyZW5ndGgsIHZMZW5ndGgsIEBob29rcy5kZWZzLnpvb20pXG5cblx0XHRcdFx0XHR2ZWN0b3IueCAqPSBncmF2aXR5IC8gdkxlbmd0aFxuXHRcdFx0XHRcdHZlY3Rvci55ICo9IGdyYXZpdHkgLyB2TGVuZ3RoXG5cdFx0XHRcdFx0XG5cdFx0XHRcdFx0YWNjZWxlcmF0aW9uLnBvcy54ICs9IHZlY3Rvci54ICogZGVsdGFcblx0XHRcdFx0XHRhY2NlbGVyYXRpb24ucG9zLnkgKz0gdmVjdG9yLnkgKiBkZWx0YVxuXHRcdFx0XHRcdFx0XHRcdFx0XG5cdFx0XHRlbHNlXG5cdFx0XHRcdFxuXHRcdFx0XHRhY2NlbGVyYXRpb25baG9vay5wcm9wXSA/PSAwXG5cblx0XHRcdFx0dGFyZ2V0ID0gaG9vay50b1tob29rLnRhcmdldFByb3BlcnR5XVxuXG5cdFx0XHRcdHRhcmdldCA9IGhvb2subW9kdWxhdG9yKHRhcmdldCkgaWYgaG9vay5tb2R1bGF0b3JcblxuXHRcdFx0XHR2ZWN0b3IgPSB0YXJnZXQgLSBAW2hvb2sucHJvcF1cblx0XHRcdFx0XG5cdFx0XHRcdGlmIGhvb2sudHlwZSBpcyAnc3ByaW5nJ1xuXG5cdFx0XHRcdFx0Zm9yY2UgPSB2ZWN0b3IgKiBob29rLnN0cmVuZ3RoXG5cdFx0XHRcdFx0ZGFtcGVyID0gLWhvb2suZnJpY3Rpb24gKiBAaG9va3MudmVsb2NpdGllc1tob29rLnByb3BdXG5cblx0XHRcdFx0XHRhY2NlbGVyYXRpb25baG9vay5wcm9wXSArPSAoZm9yY2UgKyBkYW1wZXIpICogZGVsdGFcblxuXHRcdFx0XHRcblx0XHRcdFx0ZWxzZSBpZiBob29rLnR5cGUgaXMgJ2dyYXZpdHknXG5cdFxuXHRcdFx0XHRcdGRyYWdbaG9vay5wcm9wXSA9IGhvb2suZnJpY3Rpb25cblx0XHRcdFx0XHRcblx0XHRcdFx0XHRmb3JjZSA9IEBob29rcy5kZWZzLmdldEdyYXZpdHkoaG9vay5zdHJlbmd0aCwgdmVjdG9yLCBAaG9va3MuZGVmcy56b29tKVxuXHRcdFx0XHRcdFxuXHRcdFx0XHRcdGFjY2VsZXJhdGlvbltob29rLnByb3BdICs9IGZvcmNlICogZGVsdGFcblx0XHRcblx0XHRcblx0XHQjIEFkZCB2ZWxvY2l0aWVzIHRvIHByb3BlcnRpZXMuIERvaW5nIHRoaXMgYXQgdGhlIGVuZCBpbiBjYXNlIHRoZXJlIGFyZSBtdWx0aXBsZSBob29rcyBhZmZlY3RpbmcgdGhlIHNhbWUgdmVsb2NpdHlcblx0XHRmb3IgcHJvcCwgdmVsb2NpdHkgb2YgQGhvb2tzLnZlbG9jaXRpZXNcblx0XHRcblx0XHRcdGlmIHByb3AgaXMgJ3BvcydcblxuXHRcdFx0XHQjIEFkZCBkcmFnLCBpZiBpdCBleGlzdHNcblx0XHRcdFx0aWYgZHJhZy5wb3Ncblx0XHRcdFx0XHR2ZWxvY2l0eS54ICs9IEBob29rcy5kZWZzLmdldERyYWcodmVsb2NpdHkueCwgZHJhZy5wb3MsIEBob29rcy5kZWZzLnpvb20pXG5cdFx0XHRcdFx0dmVsb2NpdHkueSArPSBAaG9va3MuZGVmcy5nZXREcmFnKHZlbG9jaXR5LnksIGRyYWcucG9zLCBAaG9va3MuZGVmcy56b29tKVxuXHRcdFx0XHRcdFxuXHRcdFx0XHQjIEFkZCBhY2NlbGVyYXRpb24gdG8gdmVsb2NpdHlcblx0XHRcdFx0dmVsb2NpdHkueCArPSBhY2NlbGVyYXRpb24ucG9zLnhcblx0XHRcdFx0dmVsb2NpdHkueSArPSBhY2NlbGVyYXRpb24ucG9zLnlcblx0XHRcdFx0XG5cdFx0XHRcdCMgQWRkIHZlbG9jaXR5IHRvIHBvc2l0aW9uXG5cdFx0XHRcdEB4ICs9IHZlbG9jaXR5LnggKiBkZWx0YVxuXHRcdFx0XHRAeSArPSB2ZWxvY2l0eS55ICogZGVsdGFcblx0XHRcdFxuXHRcdFx0ZWxzZVxuXHRcdFx0XG5cdFx0XHRcdCMgQWRkIGRyYWcsIGlmIGl0IGV4aXN0c1xuXHRcdFx0XHRpZiBkcmFnW3Byb3BdXG5cdFx0XHRcdFx0QGhvb2tzLnZlbG9jaXRpZXNbcHJvcF0gKz0gQGhvb2tzLmRlZnMuZ2V0RHJhZyhAaG9va3MudmVsb2NpdGllc1twcm9wXSwgZHJhZ1twcm9wXSwgQGhvb2tzLmRlZnMuem9vbSlcblx0XHRcdFx0XG5cdFx0XHRcdCMgQWRkIGFjY2VsZXJhdGlvbiB0byB2ZWxvY2l0eVxuXHRcdFx0XHRAaG9va3MudmVsb2NpdGllc1twcm9wXSArPSBhY2NlbGVyYXRpb25bcHJvcF1cblx0XHRcdFx0XG5cdFx0XHRcdCMgQWRkIHZlbG9jaXR5IHRvIHByb3BlcnR5XG5cdFx0XHRcdEBbcHJvcF0gKz0gQGhvb2tzLnZlbG9jaXRpZXNbcHJvcF0gKiBkZWx0YVxuXG5cdFx0QG9uSG9va1VwZGF0ZT8oZGVsdGEpIl19 346 | -------------------------------------------------------------------------------- /hook-example-gravity.framer/framer/images/cursor-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-gravity.framer/framer/images/cursor-active.png -------------------------------------------------------------------------------- /hook-example-gravity.framer/framer/images/cursor-active@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-gravity.framer/framer/images/cursor-active@2x.png -------------------------------------------------------------------------------- /hook-example-gravity.framer/framer/images/cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-gravity.framer/framer/images/cursor.png -------------------------------------------------------------------------------- /hook-example-gravity.framer/framer/images/cursor@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-gravity.framer/framer/images/cursor@2x.png -------------------------------------------------------------------------------- /hook-example-gravity.framer/framer/images/icon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-gravity.framer/framer/images/icon-120.png -------------------------------------------------------------------------------- /hook-example-gravity.framer/framer/images/icon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-gravity.framer/framer/images/icon-152.png -------------------------------------------------------------------------------- /hook-example-gravity.framer/framer/images/icon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-gravity.framer/framer/images/icon-180.png -------------------------------------------------------------------------------- /hook-example-gravity.framer/framer/images/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-gravity.framer/framer/images/icon-192.png -------------------------------------------------------------------------------- /hook-example-gravity.framer/framer/images/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-gravity.framer/framer/images/icon-76.png -------------------------------------------------------------------------------- /hook-example-gravity.framer/framer/manifest.txt: -------------------------------------------------------------------------------- 1 | app.coffee 2 | framer/coffee-script.js 3 | framer/config.json 4 | framer/framer.generated.js 5 | framer/framer.init.js 6 | framer/framer.js 7 | framer/framer.js.map 8 | framer/framer.modules.js 9 | framer/images/cursor-active.png 10 | framer/images/cursor-active@2x.png 11 | framer/images/cursor.png 12 | framer/images/cursor@2x.png 13 | framer/images/icon-120.png 14 | framer/images/icon-152.png 15 | framer/images/icon-180.png 16 | framer/images/icon-192.png 17 | framer/images/icon-76.png 18 | framer/manifest.txt 19 | framer/metadata.json 20 | framer/preview.png 21 | framer/style.css 22 | framer/version 23 | index.html 24 | modules/Hook.coffee -------------------------------------------------------------------------------- /hook-example-gravity.framer/framer/metadata.json: -------------------------------------------------------------------------------- 1 | {"title":"hook-example-gravity","date":"2016-09-09T12:28:41Z"} -------------------------------------------------------------------------------- /hook-example-gravity.framer/framer/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-gravity.framer/framer/preview.png -------------------------------------------------------------------------------- /hook-example-gravity.framer/framer/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | border: none; 5 | -webkit-user-select: none; 6 | -webkit-tap-highlight-color: rgba(0,0,0,0); 7 | } 8 | 9 | body { 10 | background-color: #fff; 11 | font: 28px/1em "Helvetica"; 12 | color: gray; 13 | overflow: hidden; 14 | } 15 | 16 | a { 17 | color: gray; 18 | } 19 | 20 | body { 21 | cursor: url('images/cursor.png') 32 32, auto; 22 | cursor: -webkit-image-set( 23 | url('images/cursor.png') 1x, 24 | url('images/cursor@2x.png') 2x 25 | ) 32 32, auto; 26 | } 27 | 28 | body:active { 29 | cursor: url('images/cursor-active.png') 32 32, auto; 30 | cursor: -webkit-image-set( 31 | url('images/cursor-active.png') 1x, 32 | url('images/cursor-active@2x.png') 2x 33 | ) 32 32, auto; 34 | } 35 | 36 | .framerAlertBackground { 37 | position: absolute; top:0px; left:0px; right:0px; bottom:0px; 38 | z-index: 1000; 39 | background-color: #fff; 40 | } 41 | 42 | .framerAlert { 43 | font:400 14px/1.4 "Helvetica Neue", Helvetica, Arial, sans-serif; 44 | -webkit-font-smoothing:antialiased; 45 | color:#616367; text-align:center; 46 | position: absolute; top:40%; left:50%; width:260px; margin-left:-130px; 47 | } 48 | .framerAlert strong { font-weight:500; color:#000; margin-bottom:8px; display:block; } 49 | .framerAlert a { color:#28AFFA; } 50 | .framerAlert .btn { 51 | font-weight:500; text-decoration:none; line-height:1; 52 | display:inline-block; padding:6px 12px 7px 12px; 53 | border-radius:3px; margin-top:12px; 54 | background:#28AFFA; color:#fff; 55 | } 56 | 57 | ::-webkit-scrollbar { 58 | display: none; 59 | } -------------------------------------------------------------------------------- /hook-example-gravity.framer/framer/version: -------------------------------------------------------------------------------- 1 | 4 -------------------------------------------------------------------------------- /hook-example-gravity.framer/images/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-gravity.framer/images/.gitkeep -------------------------------------------------------------------------------- /hook-example-gravity.framer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /hook-example-gravity.framer/modules/Hook.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | -------------------------------------------------------------------------------- 3 | Hook module for Framer 4 | -------------------------------------------------------------------------------- 5 | 6 | by: Sigurd Mannsåker 7 | github: https://github.com/sigtm/framer-hook 8 | 9 | ················································································ 10 | 11 | 12 | The Hook module simply expands the Layer prototype, and lets you make any 13 | numeric Layer property follow another property - either its own or another 14 | object's - via a spring or gravity attraction. 15 | 16 | 17 | -------------------------------------------------------------------------------- 18 | Example: Layered animation (eased + spring) 19 | -------------------------------------------------------------------------------- 20 | 21 | myLayer = new Layer 22 | 23 | # Make our own custom property for the x property to follow 24 | myLayer.easedX = 0 25 | 26 | # Hook x to easedX via a spring 27 | myLayer.hook 28 | property: "x" 29 | targetProperty: "easedX" 30 | type: "spring(150, 15)" 31 | 32 | # Animate easedX 33 | myLayer.animate 34 | properties: 35 | easedX: 200 36 | time: 0.15 37 | curve: "cubic-bezier(0.2, 0, 0.4, 1)" 38 | 39 | NOTE: 40 | To attach both the x and y position, use "pos", "midPos" or "maxPos" as the 41 | property/targetProperty. 42 | 43 | 44 | -------------------------------------------------------------------------------- 45 | Example: Hooking property to another layer 46 | -------------------------------------------------------------------------------- 47 | 48 | target = new Layer 49 | hooked = new Layer 50 | 51 | hooked.hook 52 | property: "scale" 53 | to: target 54 | type: "spring(150, 15)" 55 | 56 | The "hooked" layer's scale will now continuously follow the target layer's scale 57 | with a spring animation. 58 | 59 | 60 | -------------------------------------------------------------------------------- 61 | layer.hook(options) 62 | -------------------------------------------------------------------------------- 63 | 64 | Options are passed as a single object, like you would for a new Layer. 65 | The options object takes the following properties: 66 | 67 | 68 | property [String] 69 | ----------------- 70 | The property you'd like to hook onto another object's property 71 | 72 | 73 | type [String] 74 | ------------- 75 | Either "spring(strength, friction)" or "gravity(strength, drag)". Only the last 76 | specified drag value is used for each property, since it is only applied to 77 | each property once (and only if it has a gravity hook applied to it.) 78 | 79 | 80 | to [Object] (Optional) 81 | ---------------------- 82 | The object to attach it to. Defaults to itself. 83 | 84 | 85 | targetProperty [String] (Optional) 86 | ---------------------------------- 87 | Specify the target object's property to follow, if you don't want to follow 88 | the same property that the hook is applied to. 89 | 90 | 91 | modulator [Function] (Optional) 92 | ------------------------------- 93 | The modulator function receives the target property's value, and lets you 94 | modify it before it is fed into the physics calculations. Useful for anything 95 | from standard Utils.modulate() type stuff to snapping and conditional values. 96 | 97 | 98 | zoom [Number] (Optional) 99 | ------------------------ 100 | This factor defines the distance that 1px represents in regards to gravity and 101 | drag calculations. Only one value is stored per layer, so specifying it 102 | overwrites its existing value. Default is 100. 103 | 104 | 105 | -------------------------------------------------------------------------------- 106 | layer.unHook(property, object) 107 | -------------------------------------------------------------------------------- 108 | 109 | This removes all hooks for a given property and target object. Example: 110 | 111 | # Hook it 112 | layer.hook 113 | property: "x" 114 | to: "otherlayer" 115 | targetProperty: "y" 116 | type: "spring(200,20)" 117 | 118 | # Unhook it 119 | layer.unHook "x", otherlayer 120 | 121 | 122 | -------------------------------------------------------------------------------- 123 | layer.onHookUpdate(delta) 124 | -------------------------------------------------------------------------------- 125 | 126 | After a layer is done applying accelerations to its hooked properties, it calls 127 | onHookUpdate() at the end of each frame, if it is defined. This is an easy way 128 | to animate or trigger other stuff, perhaps based on your layer's updated 129 | properties or velocities. 130 | 131 | The delta value from the Framer loop is passed on to onHookUpdate() as well, 132 | which is the time in seconds since the last animation frame. 133 | 134 | Note that if you unhook all your hooks, onHookUpdate() will of course no longer 135 | be called for that layer. 136 | 137 | ### 138 | 139 | 140 | # Since older versions of Safari seem to be missing String.prototype.includes() 141 | 142 | unless String.prototype.includes 143 | String::includes = (search, start) -> 144 | 'use strict' 145 | start = 0 if typeof start is 'number' 146 | 147 | if start + search.length > this.length 148 | return false; 149 | else 150 | return @indexOf(search, start) isnt -1 151 | 152 | # Expand layer 153 | 154 | Layer::hook = (config) -> 155 | 156 | throw new Error 'layer.hook() needs a property, a hook type and either a target object or target property to work' unless config.property and config.type and (config.to or config.targetProperty) 157 | 158 | # Single array for all hooks, as opposed to nested arrays per property, because performance 159 | @hooks ?= 160 | hooks: [] 161 | velocities: {} 162 | defs: 163 | zoom: 100 164 | getDrag: (velocity, drag, zoom) => 165 | velocity /= zoom 166 | # Dividing by 10 is unscientific, but it means a value of 2 equals roughly a 100g ball with 15cm radius in air 167 | drag = -(drag / 10) * velocity * velocity * velocity / Math.abs(velocity) 168 | if _.isNaN(drag) then return 0 else return drag 169 | getGravity: (strength, distance, zoom) => 170 | dist = Math.max(1, distance / zoom) 171 | return strength * zoom / (dist * dist) 172 | 173 | # Update the zoom value if given 174 | @hooks.zoom = config.zoom if config.zoom 175 | 176 | # Parse physics config string 177 | f = Utils.parseFunction config.type 178 | config.type = f.name 179 | config.strength = f.args[0] 180 | config.friction = f.args[1] or 0 181 | 182 | # Default to same targetProperty on same object (hopefully you've set at least one of these to something else) 183 | config.targetProperty ?= config.property 184 | config.to ?= @ 185 | 186 | # All position accelerations are added to a single 'pos' velocity. Store actual properties so we don't have to do it again every frame 187 | 188 | if config.property.toLowerCase().includes 'pos' 189 | config.prop = 'pos' 190 | 191 | if config.property.toLowerCase().includes 'mid' 192 | config.thisX = 'midX' 193 | config.thisY = 'midY' 194 | 195 | else if config.property.toLowerCase().includes 'max' 196 | config.thisX = 'maxX' 197 | config.thisY = 'maxY' 198 | 199 | else 200 | config.thisX = 'x' 201 | config.thisY = 'y' 202 | 203 | if config.targetProperty.toLowerCase().includes 'mid' 204 | config.toX = 'midX' 205 | config.toY = 'midY' 206 | 207 | else if config.targetProperty.toLowerCase().includes 'max' 208 | config.toX = 'maxX' 209 | config.toY = 'maxY' 210 | else 211 | config.toX = 'x' 212 | config.toY = 'y' 213 | 214 | else 215 | config.prop = config.property 216 | 217 | # Save hook to @hooks array 218 | @hooks.hooks.push(config) 219 | 220 | # Create velocity property if necessary 221 | @hooks.velocities[config.prop] ?= if config.prop is 'pos' then { x: 0, y: 0 } else 0 222 | 223 | # Use Framer's animation loop, slightly more robust than requestAnimationFrame directly 224 | # Save the returned AnimationLoop reference to make sure @hookLoop isn't added multiple times per layer 225 | @hooks.emitter ?= Framer.Loop.on('render', @hookLoop, this) 226 | 227 | Layer::unHook = (property, object) -> 228 | 229 | return unless @hooks 230 | 231 | prop = if property.toLowerCase().includes 'pos' then 'pos' else property 232 | 233 | # Remove all matches 234 | @hooks.hooks = @hooks.hooks.filter (hook) -> 235 | hook.to isnt object or hook.property isnt property 236 | 237 | # If there are no hooks left, shut it down 238 | if @hooks.hooks.length is 0 239 | delete @hooks 240 | Framer.Loop.removeListener 'render', @hookLoop 241 | return 242 | 243 | # Still here? Check if there are any remaining hooks affecting same velocity 244 | remaining = @hooks.hooks.filter (hook) -> 245 | prop is hook.prop 246 | 247 | # If not, delete velocity (otherwise it won't be reset if you make new hook for same property) 248 | delete @hooks.velocities[prop] if remaining.length is 0 249 | 250 | Layer::hookLoop = (delta) -> 251 | 252 | if @hooks 253 | 254 | # Multiple hooks can affect the same property. Add accelerations to temporary object so the property's velocity is the same for all calculations within the same animation frame 255 | acceleration = {} 256 | 257 | # Save drag for each property to this object, since only most recently specified value is used for each property 258 | drag = {} 259 | 260 | # Add accelerations 261 | for hook in @hooks.hooks 262 | 263 | if hook.prop is 'pos' 264 | 265 | acceleration.pos ?= { x: 0, y: 0 } 266 | 267 | target = { x: hook.to[hook.toX], y: hook.to[hook.toY] } 268 | 269 | target = hook.modulator(target) if hook.modulator 270 | 271 | vector = 272 | x: target.x - @[hook.thisX] 273 | y: target.y - @[hook.thisY] 274 | 275 | vLength = Math.sqrt((vector.x * vector.x) + (vector.y * vector.y)) 276 | 277 | if hook.type is 'spring' 278 | 279 | damper = 280 | x: -hook.friction * @hooks.velocities.pos.x 281 | y: -hook.friction * @hooks.velocities.pos.y 282 | 283 | vector.x *= hook.strength 284 | vector.y *= hook.strength 285 | 286 | acceleration.pos.x += (vector.x + damper.x) * delta 287 | acceleration.pos.y += (vector.y + damper.y) * delta 288 | 289 | else if hook.type is 'gravity' 290 | 291 | drag.pos = hook.friction 292 | 293 | gravity = @hooks.defs.getGravity(hook.strength, vLength, @hooks.defs.zoom) 294 | 295 | vector.x *= gravity / vLength 296 | vector.y *= gravity / vLength 297 | 298 | acceleration.pos.x += vector.x * delta 299 | acceleration.pos.y += vector.y * delta 300 | 301 | else 302 | 303 | acceleration[hook.prop] ?= 0 304 | 305 | target = hook.to[hook.targetProperty] 306 | 307 | target = hook.modulator(target) if hook.modulator 308 | 309 | vector = target - @[hook.prop] 310 | 311 | if hook.type is 'spring' 312 | 313 | force = vector * hook.strength 314 | damper = -hook.friction * @hooks.velocities[hook.prop] 315 | 316 | acceleration[hook.prop] += (force + damper) * delta 317 | 318 | 319 | else if hook.type is 'gravity' 320 | 321 | drag[hook.prop] = hook.friction 322 | 323 | force = @hooks.defs.getGravity(hook.strength, vector, @hooks.defs.zoom) 324 | 325 | acceleration[hook.prop] += force * delta 326 | 327 | 328 | # Add velocities to properties. Doing this at the end in case there are multiple hooks affecting the same velocity 329 | for prop, velocity of @hooks.velocities 330 | 331 | if prop is 'pos' 332 | 333 | # Add drag, if it exists 334 | if drag.pos 335 | velocity.x += @hooks.defs.getDrag(velocity.x, drag.pos, @hooks.defs.zoom) 336 | velocity.y += @hooks.defs.getDrag(velocity.y, drag.pos, @hooks.defs.zoom) 337 | 338 | # Add acceleration to velocity 339 | velocity.x += acceleration.pos.x 340 | velocity.y += acceleration.pos.y 341 | 342 | # Add velocity to position 343 | @x += velocity.x * delta 344 | @y += velocity.y * delta 345 | 346 | else 347 | 348 | # Add drag, if it exists 349 | if drag[prop] 350 | @hooks.velocities[prop] += @hooks.defs.getDrag(@hooks.velocities[prop], drag[prop], @hooks.defs.zoom) 351 | 352 | # Add acceleration to velocity 353 | @hooks.velocities[prop] += acceleration[prop] 354 | 355 | # Add velocity to property 356 | @[prop] += @hooks.velocities[prop] * delta 357 | 358 | @onHookUpdate?(delta) -------------------------------------------------------------------------------- /hook-example-modulator.framer/.gitignore: -------------------------------------------------------------------------------- 1 | # Framer Git Ignore 2 | 3 | # General OSX 4 | 5 | .DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | 9 | # Icon must end with two \r 10 | Icon 11 | 12 | 13 | # Thumbnails 14 | ._* 15 | 16 | # Files that might appear in the root of a volume 17 | .DocumentRevisions-V100 18 | .fseventsd 19 | .Spotlight-V100 20 | .TemporaryItems 21 | .Trashes 22 | .VolumeIcon.icns 23 | 24 | # Directories potentially created on remote AFP share 25 | .AppleDB 26 | .AppleDesktop 27 | Network Trash Folder 28 | Temporary Items 29 | .apdisk 30 | 31 | 32 | # Framer Specific 33 | .temp.html 34 | framer/*.old* 35 | framer/backup.coffee 36 | framer/backups/* 37 | framer/.*.hash 38 | -------------------------------------------------------------------------------- /hook-example-modulator.framer/.viewer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /hook-example-modulator.framer/app.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | 3 | Hook example 4 | -------------- 5 | 6 | The Hook module simply expands the Layer prototype, and lets you make any 7 | numeric Layer property follow a property on another object via a spring or a 8 | gravity attraction. Check the comments in modules/Hook.coffee for a more thorough 9 | documentation. 10 | 11 | ### 12 | 13 | 14 | 15 | # Require Hook. No exports, it just adds methods to the Layer prototype 16 | # -------------------------------------------------------------------------------- 17 | 18 | require "Hook" 19 | 20 | 21 | 22 | # Import sketch file 23 | # -------------------------------------------------------------------------------- 24 | sketch = Framer.Importer.load("imported/app@2x") 25 | Utils.globalLayers(sketch) 26 | 27 | 28 | 29 | # Settings 30 | # -------------------------------------------------------------------------------- 31 | 32 | colors = 33 | background: "#260355" 34 | orange: "#f90" 35 | blue: "#63ffff" 36 | primary: "white" 37 | secondary: "rgba(255,255,255,0.1)" 38 | 39 | margin = 60 40 | 41 | easeInOut = "cubic-bezier(0.2, 0, 0.4, 1)" 42 | 43 | 44 | 45 | # Set background and defaults 46 | # -------------------------------------------------------------------------------- 47 | 48 | Framer.Device.viewport.backgroundColor = colors.background 49 | 50 | Framer.Defaults.Animation = 51 | curve: easeInOut 52 | time: 0.15 53 | 54 | 55 | 56 | # Set up example 57 | # -------------------------------------------------------------------------------- 58 | 59 | # Make the page layer draggable 60 | page.draggable.enabled = true 61 | page.draggable.horizontal = false 62 | page.draggable.constraints = { height: Screen.height } 63 | page.draggable.bounceOptions.tension = 400 64 | 65 | # Make the spinner layer 66 | spinner = new Layer 67 | index: 0 68 | x: Align.center 69 | midY: 40 70 | width: 96 71 | height: 96 72 | borderRadius: 40 73 | image: "images/spinner.svg" 74 | 75 | # Attach the spinner's scale to the page's y property 76 | spinner.hook 77 | property: "scale" 78 | to: page 79 | targetProperty: "y" 80 | type: "spring(200,10)" 81 | 82 | # Add a modulator function, since we don't want the scale to be the same as page.y 83 | modulator: (inputvalue) -> 84 | 85 | # If page.y is under 300, translate to a scale between 0.5 and 0.75 86 | if inputvalue < 300 87 | return Utils.modulate(inputvalue, [0, 400], [0.5, 0.75]) 88 | 89 | # Otherwise, snap to 1 90 | else 91 | return 1 92 | 93 | # Now attach spinner.midY to page.y with similar conditional modulation 94 | spinner.hook 95 | property: "midY" 96 | to: page 97 | targetProperty: "y" 98 | type: "spring(100,8)" 99 | modulator: (inputvalue) -> 100 | if inputvalue < 300 101 | inputvalue / 6 + 40 102 | else 103 | inputvalue / 2 104 | 105 | # onHookUpdate() is called at the end of each frame, after all of the layer's hooks are applied 106 | # It receives the delta value from the Framer loop, which is the time in seconds since the last frame 107 | spinner.onHookUpdate = (delta) -> 108 | if page.y < 300 109 | @doSpin = false 110 | @opacity = 0.2 111 | @rotation = page.y / 2 112 | else 113 | @doSpin = true 114 | @opacity = 1 115 | @rotation += delta * 300 if spinner.doSpin -------------------------------------------------------------------------------- /hook-example-modulator.framer/framer/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "propertyPanelToggleStates" : { 3 | "Filters" : false 4 | }, 5 | "deviceOrientation" : 0, 6 | "sharedPrototype" : 0, 7 | "contentScale" : 1, 8 | "deviceType" : "apple-iphone-6s-silver", 9 | "selectedHand" : "", 10 | "updateDelay" : 0.3, 11 | "deviceScale" : "fit", 12 | "foldedCodeRanges" : [ 13 | 14 | ], 15 | "orientation" : 0, 16 | "fullScreen" : false 17 | } -------------------------------------------------------------------------------- /hook-example-modulator.framer/framer/framer.generated.js: -------------------------------------------------------------------------------- 1 | // This is autogenerated by Framer 2 | 3 | 4 | if (!window.Framer && window._bridge) {window._bridge('runtime.error', {message:'[framer.js] Framer library missing or corrupt. Select File → Update Framer Library.'})} 5 | window.__imported__ = window.__imported__ || {}; 6 | window.__imported__["app@2x/layers.json.js"] = [ 7 | { 8 | "objectId": "0CDC1067-9F1A-4B22-8CF3-3B8FEE516032", 9 | "kind": "group", 10 | "name": "page", 11 | "maskFrame": null, 12 | "layerFrame": { 13 | "x": 0, 14 | "y": 0, 15 | "width": 375, 16 | "height": 667 17 | }, 18 | "visible": true, 19 | "metadata": { 20 | "opacity": 1 21 | }, 22 | "image": { 23 | "path": "images/Layer-page-meneqzew.png", 24 | "frame": { 25 | "x": 0, 26 | "y": 0, 27 | "width": 375, 28 | "height": 667 29 | } 30 | }, 31 | "children": [], 32 | "time": 123 33 | } 34 | ] 35 | if (DeviceComponent) {DeviceComponent.Devices["iphone-6-silver"].deviceImageJP2 = false}; 36 | if (window.Framer) {window.Framer.Defaults.DeviceView = {"deviceScale":"fit","selectedHand":"","deviceType":"apple-iphone-6s-silver","contentScale":1,"orientation":0}; 37 | } 38 | if (window.Framer) {window.Framer.Defaults.DeviceComponent = {"deviceScale":"fit","selectedHand":"","deviceType":"apple-iphone-6s-silver","contentScale":1,"orientation":0}; 39 | } 40 | window.FramerStudioInfo = {"deviceImagesUrl":"\/_server\/resources\/DeviceImages","documentTitle":"hook-example-modulator.framer"}; 41 | 42 | Framer.Device = new Framer.DeviceView(); 43 | Framer.Device.setupContext(); -------------------------------------------------------------------------------- /hook-example-modulator.framer/framer/framer.init.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | function isFileLoadingAllowed() { 4 | return (window.location.protocol.indexOf("file") == -1) 5 | } 6 | 7 | function isHomeScreened() { 8 | return ("standalone" in window.navigator) && window.navigator.standalone == true 9 | } 10 | 11 | function isCompatibleBrowser() { 12 | return Utils.isWebKit() 13 | } 14 | 15 | var alertNode; 16 | 17 | function dismissAlert() { 18 | alertNode.parentElement.removeChild(alertNode) 19 | loadProject() 20 | } 21 | 22 | function showAlert(html) { 23 | 24 | alertNode = document.createElement("div") 25 | 26 | alertNode.classList.add("framerAlertBackground") 27 | alertNode.innerHTML = html 28 | 29 | document.addEventListener("DOMContentLoaded", function(event) { 30 | document.body.appendChild(alertNode) 31 | }) 32 | 33 | window.dismissAlert = dismissAlert; 34 | } 35 | 36 | function showBrowserAlert() { 37 | var html = "" 38 | html += "
" 39 | html += "Error: Not A WebKit Browser" 40 | html += "Your browser is not supported.
Please use Safari or Chrome.
" 41 | html += "Try anyway" 42 | html += "
" 43 | 44 | showAlert(html) 45 | } 46 | 47 | function showFileLoadingAlert() { 48 | var html = "" 49 | html += "
" 50 | html += "Error: Local File Restrictions" 51 | html += "Preview this prototype with Framer Mirror or learn more about " 52 | html += "file restrictions.
" 53 | html += "Try anyway" 54 | html += "
" 55 | 56 | showAlert(html) 57 | } 58 | 59 | function loadProject() { 60 | CoffeeScript.load("app.coffee") 61 | } 62 | 63 | function setDefaultPageTitle() { 64 | // If no title was set we set it to the project folder name so 65 | // you get a nice name on iOS if you bookmark to desktop. 66 | document.addEventListener("DOMContentLoaded", function() { 67 | if (document.title == "") { 68 | if (window.FramerStudioInfo && window.FramerStudioInfo.documentTitle) { 69 | document.title = window.FramerStudioInfo.documentTitle 70 | } else { 71 | document.title = window.location.pathname.replace(/\//g, "") 72 | } 73 | } 74 | }) 75 | } 76 | 77 | function init() { 78 | 79 | if (Utils.isFramerStudio()) { 80 | return 81 | } 82 | 83 | setDefaultPageTitle() 84 | 85 | if (!isCompatibleBrowser()) { 86 | return showBrowserAlert() 87 | } 88 | 89 | if (!isFileLoadingAllowed()) { 90 | return showFileLoadingAlert() 91 | } 92 | 93 | loadProject() 94 | 95 | } 96 | 97 | init() 98 | 99 | })() 100 | -------------------------------------------------------------------------------- /hook-example-modulator.framer/framer/framer.modules.js: -------------------------------------------------------------------------------- 1 | require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o this.length) { 146 | return false; 147 | } else { 148 | return this.indexOf(search, start) !== -1; 149 | } 150 | }; 151 | } 152 | 153 | Layer.prototype.hook = function(config) { 154 | var base, base1, f, name; 155 | if (!(config.property && config.type && (config.to || config.targetProperty))) { 156 | throw new Error('layer.hook() needs a property, a hook type and either a target object or target property to work'); 157 | } 158 | if (this.hooks == null) { 159 | this.hooks = { 160 | hooks: [], 161 | velocities: {}, 162 | defs: { 163 | zoom: 100, 164 | getDrag: (function(_this) { 165 | return function(velocity, drag, zoom) { 166 | velocity /= zoom; 167 | drag = -(drag / 10) * velocity * velocity * velocity / Math.abs(velocity); 168 | if (_.isNaN(drag)) { 169 | return 0; 170 | } else { 171 | return drag; 172 | } 173 | }; 174 | })(this), 175 | getGravity: (function(_this) { 176 | return function(strength, distance, zoom) { 177 | var dist; 178 | dist = Math.max(1, distance / zoom); 179 | return strength * zoom / (dist * dist); 180 | }; 181 | })(this) 182 | } 183 | }; 184 | } 185 | if (config.zoom) { 186 | this.hooks.zoom = config.zoom; 187 | } 188 | f = Utils.parseFunction(config.type); 189 | config.type = f.name; 190 | config.strength = f.args[0]; 191 | config.friction = f.args[1] || 0; 192 | if (config.targetProperty == null) { 193 | config.targetProperty = config.property; 194 | } 195 | if (config.to == null) { 196 | config.to = this; 197 | } 198 | if (config.property.toLowerCase().includes('pos')) { 199 | config.prop = 'pos'; 200 | if (config.property.toLowerCase().includes('mid')) { 201 | config.thisX = 'midX'; 202 | config.thisY = 'midY'; 203 | } else if (config.property.toLowerCase().includes('max')) { 204 | config.thisX = 'maxX'; 205 | config.thisY = 'maxY'; 206 | } else { 207 | config.thisX = 'x'; 208 | config.thisY = 'y'; 209 | } 210 | if (config.targetProperty.toLowerCase().includes('mid')) { 211 | config.toX = 'midX'; 212 | config.toY = 'midY'; 213 | } else if (config.targetProperty.toLowerCase().includes('max')) { 214 | config.toX = 'maxX'; 215 | config.toY = 'maxY'; 216 | } else { 217 | config.toX = 'x'; 218 | config.toY = 'y'; 219 | } 220 | } else { 221 | config.prop = config.property; 222 | } 223 | this.hooks.hooks.push(config); 224 | if ((base = this.hooks.velocities)[name = config.prop] == null) { 225 | base[name] = config.prop === 'pos' ? { 226 | x: 0, 227 | y: 0 228 | } : 0; 229 | } 230 | return (base1 = this.hooks).emitter != null ? base1.emitter : base1.emitter = Framer.Loop.on('render', this.hookLoop, this); 231 | }; 232 | 233 | Layer.prototype.unHook = function(property, object) { 234 | var prop, remaining; 235 | if (!this.hooks) { 236 | return; 237 | } 238 | prop = property.toLowerCase().includes('pos') ? 'pos' : property; 239 | this.hooks.hooks = this.hooks.hooks.filter(function(hook) { 240 | return hook.to !== object || hook.property !== property; 241 | }); 242 | if (this.hooks.hooks.length === 0) { 243 | delete this.hooks; 244 | Framer.Loop.removeListener('render', this.hookLoop); 245 | return; 246 | } 247 | remaining = this.hooks.hooks.filter(function(hook) { 248 | return prop === hook.prop; 249 | }); 250 | if (remaining.length === 0) { 251 | return delete this.hooks.velocities[prop]; 252 | } 253 | }; 254 | 255 | Layer.prototype.hookLoop = function(delta) { 256 | var acceleration, damper, drag, force, gravity, hook, i, len, name, prop, ref, ref1, target, vLength, vector, velocity; 257 | if (this.hooks) { 258 | acceleration = {}; 259 | drag = {}; 260 | ref = this.hooks.hooks; 261 | for (i = 0, len = ref.length; i < len; i++) { 262 | hook = ref[i]; 263 | if (hook.prop === 'pos') { 264 | if (acceleration.pos == null) { 265 | acceleration.pos = { 266 | x: 0, 267 | y: 0 268 | }; 269 | } 270 | target = { 271 | x: hook.to[hook.toX], 272 | y: hook.to[hook.toY] 273 | }; 274 | if (hook.modulator) { 275 | target = hook.modulator(target); 276 | } 277 | vector = { 278 | x: target.x - this[hook.thisX], 279 | y: target.y - this[hook.thisY] 280 | }; 281 | vLength = Math.sqrt((vector.x * vector.x) + (vector.y * vector.y)); 282 | if (hook.type === 'spring') { 283 | damper = { 284 | x: -hook.friction * this.hooks.velocities.pos.x, 285 | y: -hook.friction * this.hooks.velocities.pos.y 286 | }; 287 | vector.x *= hook.strength; 288 | vector.y *= hook.strength; 289 | acceleration.pos.x += (vector.x + damper.x) * delta; 290 | acceleration.pos.y += (vector.y + damper.y) * delta; 291 | } else if (hook.type === 'gravity') { 292 | drag.pos = hook.friction; 293 | gravity = this.hooks.defs.getGravity(hook.strength, vLength, this.hooks.defs.zoom); 294 | vector.x *= gravity / vLength; 295 | vector.y *= gravity / vLength; 296 | acceleration.pos.x += vector.x * delta; 297 | acceleration.pos.y += vector.y * delta; 298 | } 299 | } else { 300 | if (acceleration[name = hook.prop] == null) { 301 | acceleration[name] = 0; 302 | } 303 | target = hook.to[hook.targetProperty]; 304 | if (hook.modulator) { 305 | target = hook.modulator(target); 306 | } 307 | vector = target - this[hook.prop]; 308 | if (hook.type === 'spring') { 309 | force = vector * hook.strength; 310 | damper = -hook.friction * this.hooks.velocities[hook.prop]; 311 | acceleration[hook.prop] += (force + damper) * delta; 312 | } else if (hook.type === 'gravity') { 313 | drag[hook.prop] = hook.friction; 314 | force = this.hooks.defs.getGravity(hook.strength, vector, this.hooks.defs.zoom); 315 | acceleration[hook.prop] += force * delta; 316 | } 317 | } 318 | } 319 | ref1 = this.hooks.velocities; 320 | for (prop in ref1) { 321 | velocity = ref1[prop]; 322 | if (prop === 'pos') { 323 | if (drag.pos) { 324 | velocity.x += this.hooks.defs.getDrag(velocity.x, drag.pos, this.hooks.defs.zoom); 325 | velocity.y += this.hooks.defs.getDrag(velocity.y, drag.pos, this.hooks.defs.zoom); 326 | } 327 | velocity.x += acceleration.pos.x; 328 | velocity.y += acceleration.pos.y; 329 | this.x += velocity.x * delta; 330 | this.y += velocity.y * delta; 331 | } else { 332 | if (drag[prop]) { 333 | this.hooks.velocities[prop] += this.hooks.defs.getDrag(this.hooks.velocities[prop], drag[prop], this.hooks.defs.zoom); 334 | } 335 | this.hooks.velocities[prop] += acceleration[prop]; 336 | this[prop] += this.hooks.velocities[prop] * delta; 337 | } 338 | } 339 | return typeof this.onHookUpdate === "function" ? this.onHookUpdate(delta) : void 0; 340 | } 341 | }; 342 | 343 | 344 | },{}]},{},[]) 345 | //# sourceMappingURL=data:application/json;charset:utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIm5vZGVfbW9kdWxlcy9icm93c2VyaWZ5L25vZGVfbW9kdWxlcy9icm93c2VyLXBhY2svX3ByZWx1ZGUuanMiLCIvVXNlcnMvc2lndXJkL1JlcG9zL2ZyYW1lci1ob29rL2hvb2stZXhhbXBsZS1tb2R1bGF0b3IuZnJhbWVyL21vZHVsZXMvSG9vay5jb2ZmZWUiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7O0FDQUE7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUE2SUEsSUFBQSxDQUFPLE1BQU0sQ0FBQyxTQUFTLENBQUMsUUFBeEI7RUFDQyxNQUFNLENBQUEsU0FBRSxDQUFBLFFBQVIsR0FBbUIsU0FBQyxNQUFELEVBQVMsS0FBVDtJQUNsQjtJQUNBLElBQWEsT0FBTyxLQUFQLEtBQWdCLFFBQTdCO01BQUEsS0FBQSxHQUFRLEVBQVI7O0lBRUEsSUFBRyxLQUFBLEdBQVEsTUFBTSxDQUFDLE1BQWYsR0FBd0IsSUFBSSxDQUFDLE1BQWhDO0FBQ0MsYUFBTyxNQURSO0tBQUEsTUFBQTtBQUdDLGFBQU8sSUFBQyxDQUFBLE9BQUQsQ0FBUyxNQUFULEVBQWlCLEtBQWpCLENBQUEsS0FBNkIsQ0FBQyxFQUh0Qzs7RUFKa0IsRUFEcEI7OztBQVlBLEtBQUssQ0FBQSxTQUFFLENBQUEsSUFBUCxHQUFjLFNBQUMsTUFBRDtBQUViLE1BQUE7RUFBQSxJQUFBLENBQUEsQ0FBMEgsTUFBTSxDQUFDLFFBQVAsSUFBb0IsTUFBTSxDQUFDLElBQTNCLElBQW9DLENBQUMsTUFBTSxDQUFDLEVBQVAsSUFBYSxNQUFNLENBQUMsY0FBckIsQ0FBOUosQ0FBQTtBQUFBLFVBQVUsSUFBQSxLQUFBLENBQU0sa0dBQU4sRUFBVjs7O0lBR0EsSUFBQyxDQUFBLFFBQ0E7TUFBQSxLQUFBLEVBQU8sRUFBUDtNQUNBLFVBQUEsRUFBWSxFQURaO01BRUEsSUFBQSxFQUNDO1FBQUEsSUFBQSxFQUFNLEdBQU47UUFDQSxPQUFBLEVBQVMsQ0FBQSxTQUFBLEtBQUE7aUJBQUEsU0FBQyxRQUFELEVBQVcsSUFBWCxFQUFpQixJQUFqQjtZQUNSLFFBQUEsSUFBWTtZQUVaLElBQUEsR0FBTyxDQUFDLENBQUMsSUFBQSxHQUFPLEVBQVIsQ0FBRCxHQUFlLFFBQWYsR0FBMEIsUUFBMUIsR0FBcUMsUUFBckMsR0FBZ0QsSUFBSSxDQUFDLEdBQUwsQ0FBUyxRQUFUO1lBQ3ZELElBQUcsQ0FBQyxDQUFDLEtBQUYsQ0FBUSxJQUFSLENBQUg7QUFBc0IscUJBQU8sRUFBN0I7YUFBQSxNQUFBO0FBQW9DLHFCQUFPLEtBQTNDOztVQUpRO1FBQUEsQ0FBQSxDQUFBLENBQUEsSUFBQSxDQURUO1FBTUEsVUFBQSxFQUFZLENBQUEsU0FBQSxLQUFBO2lCQUFBLFNBQUMsUUFBRCxFQUFXLFFBQVgsRUFBcUIsSUFBckI7QUFDWCxnQkFBQTtZQUFBLElBQUEsR0FBTyxJQUFJLENBQUMsR0FBTCxDQUFTLENBQVQsRUFBWSxRQUFBLEdBQVcsSUFBdkI7QUFDUCxtQkFBTyxRQUFBLEdBQVcsSUFBWCxHQUFrQixDQUFDLElBQUEsR0FBTyxJQUFSO1VBRmQ7UUFBQSxDQUFBLENBQUEsQ0FBQSxJQUFBLENBTlo7T0FIRDs7O0VBY0QsSUFBNkIsTUFBTSxDQUFDLElBQXBDO0lBQUEsSUFBQyxDQUFBLEtBQUssQ0FBQyxJQUFQLEdBQWMsTUFBTSxDQUFDLEtBQXJCOztFQUdBLENBQUEsR0FBSSxLQUFLLENBQUMsYUFBTixDQUFvQixNQUFNLENBQUMsSUFBM0I7RUFDSixNQUFNLENBQUMsSUFBUCxHQUFjLENBQUMsQ0FBQztFQUNoQixNQUFNLENBQUMsUUFBUCxHQUFrQixDQUFDLENBQUMsSUFBSyxDQUFBLENBQUE7RUFDekIsTUFBTSxDQUFDLFFBQVAsR0FBa0IsQ0FBQyxDQUFDLElBQUssQ0FBQSxDQUFBLENBQVAsSUFBYTs7SUFHL0IsTUFBTSxDQUFDLGlCQUFrQixNQUFNLENBQUM7OztJQUNoQyxNQUFNLENBQUMsS0FBTTs7RUFJYixJQUFHLE1BQU0sQ0FBQyxRQUFRLENBQUMsV0FBaEIsQ0FBQSxDQUE2QixDQUFDLFFBQTlCLENBQXVDLEtBQXZDLENBQUg7SUFDQyxNQUFNLENBQUMsSUFBUCxHQUFjO0lBRWQsSUFBRyxNQUFNLENBQUMsUUFBUSxDQUFDLFdBQWhCLENBQUEsQ0FBNkIsQ0FBQyxRQUE5QixDQUF1QyxLQUF2QyxDQUFIO01BQ0MsTUFBTSxDQUFDLEtBQVAsR0FBZTtNQUNmLE1BQU0sQ0FBQyxLQUFQLEdBQWUsT0FGaEI7S0FBQSxNQUlLLElBQUcsTUFBTSxDQUFDLFFBQVEsQ0FBQyxXQUFoQixDQUFBLENBQTZCLENBQUMsUUFBOUIsQ0FBdUMsS0FBdkMsQ0FBSDtNQUNKLE1BQU0sQ0FBQyxLQUFQLEdBQWU7TUFDZixNQUFNLENBQUMsS0FBUCxHQUFlLE9BRlg7S0FBQSxNQUFBO01BS0osTUFBTSxDQUFDLEtBQVAsR0FBZTtNQUNmLE1BQU0sQ0FBQyxLQUFQLEdBQWUsSUFOWDs7SUFRTCxJQUFHLE1BQU0sQ0FBQyxjQUFjLENBQUMsV0FBdEIsQ0FBQSxDQUFtQyxDQUFDLFFBQXBDLENBQTZDLEtBQTdDLENBQUg7TUFDQyxNQUFNLENBQUMsR0FBUCxHQUFhO01BQ2IsTUFBTSxDQUFDLEdBQVAsR0FBYSxPQUZkO0tBQUEsTUFJSyxJQUFHLE1BQU0sQ0FBQyxjQUFjLENBQUMsV0FBdEIsQ0FBQSxDQUFtQyxDQUFDLFFBQXBDLENBQTZDLEtBQTdDLENBQUg7TUFDSixNQUFNLENBQUMsR0FBUCxHQUFhO01BQ2IsTUFBTSxDQUFDLEdBQVAsR0FBYSxPQUZUO0tBQUEsTUFBQTtNQUlKLE1BQU0sQ0FBQyxHQUFQLEdBQWE7TUFDYixNQUFNLENBQUMsR0FBUCxHQUFhLElBTFQ7S0FuQk47R0FBQSxNQUFBO0lBMkJDLE1BQU0sQ0FBQyxJQUFQLEdBQWMsTUFBTSxDQUFDLFNBM0J0Qjs7RUE4QkEsSUFBQyxDQUFBLEtBQUssQ0FBQyxLQUFLLENBQUMsSUFBYixDQUFrQixNQUFsQjs7aUJBR3FDLE1BQU0sQ0FBQyxJQUFQLEtBQWUsS0FBbEIsR0FBNkI7TUFBRSxDQUFBLEVBQUcsQ0FBTDtNQUFRLENBQUEsRUFBRyxDQUFYO0tBQTdCLEdBQWlEOztxREFJN0UsQ0FBQyxlQUFELENBQUMsVUFBVyxNQUFNLENBQUMsSUFBSSxDQUFDLEVBQVosQ0FBZSxRQUFmLEVBQXlCLElBQUMsQ0FBQSxRQUExQixFQUFvQyxJQUFwQztBQXZFTDs7QUF5RWQsS0FBSyxDQUFBLFNBQUUsQ0FBQSxNQUFQLEdBQWdCLFNBQUMsUUFBRCxFQUFXLE1BQVg7QUFFZixNQUFBO0VBQUEsSUFBQSxDQUFjLElBQUMsQ0FBQSxLQUFmO0FBQUEsV0FBQTs7RUFFQSxJQUFBLEdBQVUsUUFBUSxDQUFDLFdBQVQsQ0FBQSxDQUFzQixDQUFDLFFBQXZCLENBQWdDLEtBQWhDLENBQUgsR0FBOEMsS0FBOUMsR0FBeUQ7RUFHaEUsSUFBQyxDQUFBLEtBQUssQ0FBQyxLQUFQLEdBQWUsSUFBQyxDQUFBLEtBQUssQ0FBQyxLQUFLLENBQUMsTUFBYixDQUFvQixTQUFDLElBQUQ7V0FDbEMsSUFBSSxDQUFDLEVBQUwsS0FBYSxNQUFiLElBQXVCLElBQUksQ0FBQyxRQUFMLEtBQW1CO0VBRFIsQ0FBcEI7RUFJZixJQUFHLElBQUMsQ0FBQSxLQUFLLENBQUMsS0FBSyxDQUFDLE1BQWIsS0FBdUIsQ0FBMUI7SUFDQyxPQUFPLElBQUMsQ0FBQTtJQUNSLE1BQU0sQ0FBQyxJQUFJLENBQUMsY0FBWixDQUEyQixRQUEzQixFQUFxQyxJQUFDLENBQUEsUUFBdEM7QUFDQSxXQUhEOztFQU1BLFNBQUEsR0FBWSxJQUFDLENBQUEsS0FBSyxDQUFDLEtBQUssQ0FBQyxNQUFiLENBQW9CLFNBQUMsSUFBRDtXQUMvQixJQUFBLEtBQVEsSUFBSSxDQUFDO0VBRGtCLENBQXBCO0VBSVosSUFBa0MsU0FBUyxDQUFDLE1BQVYsS0FBb0IsQ0FBdEQ7V0FBQSxPQUFPLElBQUMsQ0FBQSxLQUFLLENBQUMsVUFBVyxDQUFBLElBQUEsRUFBekI7O0FBckJlOztBQXVCaEIsS0FBSyxDQUFBLFNBQUUsQ0FBQSxRQUFQLEdBQWtCLFNBQUMsS0FBRDtBQUVqQixNQUFBO0VBQUEsSUFBRyxJQUFDLENBQUEsS0FBSjtJQUdDLFlBQUEsR0FBZTtJQUdmLElBQUEsR0FBTztBQUdQO0FBQUEsU0FBQSxxQ0FBQTs7TUFFQyxJQUFHLElBQUksQ0FBQyxJQUFMLEtBQWEsS0FBaEI7O1VBRUMsWUFBWSxDQUFDLE1BQU87WUFBRSxDQUFBLEVBQUcsQ0FBTDtZQUFRLENBQUEsRUFBRyxDQUFYOzs7UUFFcEIsTUFBQSxHQUFTO1VBQUUsQ0FBQSxFQUFHLElBQUksQ0FBQyxFQUFHLENBQUEsSUFBSSxDQUFDLEdBQUwsQ0FBYjtVQUF3QixDQUFBLEVBQUcsSUFBSSxDQUFDLEVBQUcsQ0FBQSxJQUFJLENBQUMsR0FBTCxDQUFuQzs7UUFFVCxJQUFtQyxJQUFJLENBQUMsU0FBeEM7VUFBQSxNQUFBLEdBQVMsSUFBSSxDQUFDLFNBQUwsQ0FBZSxNQUFmLEVBQVQ7O1FBRUEsTUFBQSxHQUNDO1VBQUEsQ0FBQSxFQUFHLE1BQU0sQ0FBQyxDQUFQLEdBQVcsSUFBRSxDQUFBLElBQUksQ0FBQyxLQUFMLENBQWhCO1VBQ0EsQ0FBQSxFQUFHLE1BQU0sQ0FBQyxDQUFQLEdBQVcsSUFBRSxDQUFBLElBQUksQ0FBQyxLQUFMLENBRGhCOztRQUdELE9BQUEsR0FBVSxJQUFJLENBQUMsSUFBTCxDQUFVLENBQUMsTUFBTSxDQUFDLENBQVAsR0FBVyxNQUFNLENBQUMsQ0FBbkIsQ0FBQSxHQUF3QixDQUFDLE1BQU0sQ0FBQyxDQUFQLEdBQVcsTUFBTSxDQUFDLENBQW5CLENBQWxDO1FBRVYsSUFBRyxJQUFJLENBQUMsSUFBTCxLQUFhLFFBQWhCO1VBRUMsTUFBQSxHQUNDO1lBQUEsQ0FBQSxFQUFHLENBQUMsSUFBSSxDQUFDLFFBQU4sR0FBaUIsSUFBQyxDQUFBLEtBQUssQ0FBQyxVQUFVLENBQUMsR0FBRyxDQUFDLENBQTFDO1lBQ0EsQ0FBQSxFQUFHLENBQUMsSUFBSSxDQUFDLFFBQU4sR0FBaUIsSUFBQyxDQUFBLEtBQUssQ0FBQyxVQUFVLENBQUMsR0FBRyxDQUFDLENBRDFDOztVQUdELE1BQU0sQ0FBQyxDQUFQLElBQVksSUFBSSxDQUFDO1VBQ2pCLE1BQU0sQ0FBQyxDQUFQLElBQVksSUFBSSxDQUFDO1VBRWpCLFlBQVksQ0FBQyxHQUFHLENBQUMsQ0FBakIsSUFBc0IsQ0FBQyxNQUFNLENBQUMsQ0FBUCxHQUFXLE1BQU0sQ0FBQyxDQUFuQixDQUFBLEdBQXdCO1VBQzlDLFlBQVksQ0FBQyxHQUFHLENBQUMsQ0FBakIsSUFBc0IsQ0FBQyxNQUFNLENBQUMsQ0FBUCxHQUFXLE1BQU0sQ0FBQyxDQUFuQixDQUFBLEdBQXdCLE1BVi9DO1NBQUEsTUFZSyxJQUFHLElBQUksQ0FBQyxJQUFMLEtBQWEsU0FBaEI7VUFFSixJQUFJLENBQUMsR0FBTCxHQUFXLElBQUksQ0FBQztVQUVoQixPQUFBLEdBQVUsSUFBQyxDQUFBLEtBQUssQ0FBQyxJQUFJLENBQUMsVUFBWixDQUF1QixJQUFJLENBQUMsUUFBNUIsRUFBc0MsT0FBdEMsRUFBK0MsSUFBQyxDQUFBLEtBQUssQ0FBQyxJQUFJLENBQUMsSUFBM0Q7VUFFVixNQUFNLENBQUMsQ0FBUCxJQUFZLE9BQUEsR0FBVTtVQUN0QixNQUFNLENBQUMsQ0FBUCxJQUFZLE9BQUEsR0FBVTtVQUV0QixZQUFZLENBQUMsR0FBRyxDQUFDLENBQWpCLElBQXNCLE1BQU0sQ0FBQyxDQUFQLEdBQVc7VUFDakMsWUFBWSxDQUFDLEdBQUcsQ0FBQyxDQUFqQixJQUFzQixNQUFNLENBQUMsQ0FBUCxHQUFXLE1BVjdCO1NBMUJOO09BQUEsTUFBQTs7VUF3Q0MscUJBQTJCOztRQUUzQixNQUFBLEdBQVMsSUFBSSxDQUFDLEVBQUcsQ0FBQSxJQUFJLENBQUMsY0FBTDtRQUVqQixJQUFtQyxJQUFJLENBQUMsU0FBeEM7VUFBQSxNQUFBLEdBQVMsSUFBSSxDQUFDLFNBQUwsQ0FBZSxNQUFmLEVBQVQ7O1FBRUEsTUFBQSxHQUFTLE1BQUEsR0FBUyxJQUFFLENBQUEsSUFBSSxDQUFDLElBQUw7UUFFcEIsSUFBRyxJQUFJLENBQUMsSUFBTCxLQUFhLFFBQWhCO1VBRUMsS0FBQSxHQUFRLE1BQUEsR0FBUyxJQUFJLENBQUM7VUFDdEIsTUFBQSxHQUFTLENBQUMsSUFBSSxDQUFDLFFBQU4sR0FBaUIsSUFBQyxDQUFBLEtBQUssQ0FBQyxVQUFXLENBQUEsSUFBSSxDQUFDLElBQUw7VUFFNUMsWUFBYSxDQUFBLElBQUksQ0FBQyxJQUFMLENBQWIsSUFBMkIsQ0FBQyxLQUFBLEdBQVEsTUFBVCxDQUFBLEdBQW1CLE1BTC9DO1NBQUEsTUFRSyxJQUFHLElBQUksQ0FBQyxJQUFMLEtBQWEsU0FBaEI7VUFFSixJQUFLLENBQUEsSUFBSSxDQUFDLElBQUwsQ0FBTCxHQUFrQixJQUFJLENBQUM7VUFFdkIsS0FBQSxHQUFRLElBQUMsQ0FBQSxLQUFLLENBQUMsSUFBSSxDQUFDLFVBQVosQ0FBdUIsSUFBSSxDQUFDLFFBQTVCLEVBQXNDLE1BQXRDLEVBQThDLElBQUMsQ0FBQSxLQUFLLENBQUMsSUFBSSxDQUFDLElBQTFEO1VBRVIsWUFBYSxDQUFBLElBQUksQ0FBQyxJQUFMLENBQWIsSUFBMkIsS0FBQSxHQUFRLE1BTi9CO1NBeEROOztBQUZEO0FBb0VBO0FBQUEsU0FBQSxZQUFBOztNQUVDLElBQUcsSUFBQSxLQUFRLEtBQVg7UUFHQyxJQUFHLElBQUksQ0FBQyxHQUFSO1VBQ0MsUUFBUSxDQUFDLENBQVQsSUFBYyxJQUFDLENBQUEsS0FBSyxDQUFDLElBQUksQ0FBQyxPQUFaLENBQW9CLFFBQVEsQ0FBQyxDQUE3QixFQUFnQyxJQUFJLENBQUMsR0FBckMsRUFBMEMsSUFBQyxDQUFBLEtBQUssQ0FBQyxJQUFJLENBQUMsSUFBdEQ7VUFDZCxRQUFRLENBQUMsQ0FBVCxJQUFjLElBQUMsQ0FBQSxLQUFLLENBQUMsSUFBSSxDQUFDLE9BQVosQ0FBb0IsUUFBUSxDQUFDLENBQTdCLEVBQWdDLElBQUksQ0FBQyxHQUFyQyxFQUEwQyxJQUFDLENBQUEsS0FBSyxDQUFDLElBQUksQ0FBQyxJQUF0RCxFQUZmOztRQUtBLFFBQVEsQ0FBQyxDQUFULElBQWMsWUFBWSxDQUFDLEdBQUcsQ0FBQztRQUMvQixRQUFRLENBQUMsQ0FBVCxJQUFjLFlBQVksQ0FBQyxHQUFHLENBQUM7UUFHL0IsSUFBQyxDQUFBLENBQUQsSUFBTSxRQUFRLENBQUMsQ0FBVCxHQUFhO1FBQ25CLElBQUMsQ0FBQSxDQUFELElBQU0sUUFBUSxDQUFDLENBQVQsR0FBYSxNQWJwQjtPQUFBLE1BQUE7UUFrQkMsSUFBRyxJQUFLLENBQUEsSUFBQSxDQUFSO1VBQ0MsSUFBQyxDQUFBLEtBQUssQ0FBQyxVQUFXLENBQUEsSUFBQSxDQUFsQixJQUEyQixJQUFDLENBQUEsS0FBSyxDQUFDLElBQUksQ0FBQyxPQUFaLENBQW9CLElBQUMsQ0FBQSxLQUFLLENBQUMsVUFBVyxDQUFBLElBQUEsQ0FBdEMsRUFBNkMsSUFBSyxDQUFBLElBQUEsQ0FBbEQsRUFBeUQsSUFBQyxDQUFBLEtBQUssQ0FBQyxJQUFJLENBQUMsSUFBckUsRUFENUI7O1FBSUEsSUFBQyxDQUFBLEtBQUssQ0FBQyxVQUFXLENBQUEsSUFBQSxDQUFsQixJQUEyQixZQUFhLENBQUEsSUFBQTtRQUd4QyxJQUFFLENBQUEsSUFBQSxDQUFGLElBQVcsSUFBQyxDQUFBLEtBQUssQ0FBQyxVQUFXLENBQUEsSUFBQSxDQUFsQixHQUEwQixNQXpCdEM7O0FBRkQ7cURBNkJBLElBQUMsQ0FBQSxhQUFjLGdCQTFHaEI7O0FBRmlCIiwiZmlsZSI6ImdlbmVyYXRlZC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzQ29udGVudCI6WyIoZnVuY3Rpb24gZSh0LG4scil7ZnVuY3Rpb24gcyhvLHUpe2lmKCFuW29dKXtpZighdFtvXSl7dmFyIGE9dHlwZW9mIHJlcXVpcmU9PVwiZnVuY3Rpb25cIiYmcmVxdWlyZTtpZighdSYmYSlyZXR1cm4gYShvLCEwKTtpZihpKXJldHVybiBpKG8sITApO3ZhciBmPW5ldyBFcnJvcihcIkNhbm5vdCBmaW5kIG1vZHVsZSAnXCIrbytcIidcIik7dGhyb3cgZi5jb2RlPVwiTU9EVUxFX05PVF9GT1VORFwiLGZ9dmFyIGw9bltvXT17ZXhwb3J0czp7fX07dFtvXVswXS5jYWxsKGwuZXhwb3J0cyxmdW5jdGlvbihlKXt2YXIgbj10W29dWzFdW2VdO3JldHVybiBzKG4/bjplKX0sbCxsLmV4cG9ydHMsZSx0LG4scil9cmV0dXJuIG5bb10uZXhwb3J0c312YXIgaT10eXBlb2YgcmVxdWlyZT09XCJmdW5jdGlvblwiJiZyZXF1aXJlO2Zvcih2YXIgbz0wO288ci5sZW5ndGg7bysrKXMocltvXSk7cmV0dXJuIHN9KSIsIiMjI1xuLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS1cbkhvb2sgbW9kdWxlIGZvciBGcmFtZXJcbi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5cbmJ5OiAgICAgIFNpZ3VyZCBNYW5uc8Ola2VyXG5naXRodWI6ICBodHRwczovL2dpdGh1Yi5jb20vc2lndG0vZnJhbWVyLWhvb2tcblxuwrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt1xuXG5cblRoZSBIb29rIG1vZHVsZSBzaW1wbHkgZXhwYW5kcyB0aGUgTGF5ZXIgcHJvdG90eXBlLCBhbmQgbGV0cyB5b3UgbWFrZSBhbnlcbm51bWVyaWMgTGF5ZXIgcHJvcGVydHkgZm9sbG93IGFub3RoZXIgcHJvcGVydHkgLSBlaXRoZXIgaXRzIG93biBvciBhbm90aGVyXG5vYmplY3QncyAtIHZpYSBhIHNwcmluZyBvciBncmF2aXR5IGF0dHJhY3Rpb24uXG5cblxuLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS1cbkV4YW1wbGU6IExheWVyZWQgYW5pbWF0aW9uIChlYXNlZCArIHNwcmluZylcbi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5cbm15TGF5ZXIgPSBuZXcgTGF5ZXJcblxuIyBNYWtlIG91ciBvd24gY3VzdG9tIHByb3BlcnR5IGZvciB0aGUgeCBwcm9wZXJ0eSB0byBmb2xsb3dcbm15TGF5ZXIuZWFzZWRYID0gMFxuXG4jIEhvb2sgeCB0byBlYXNlZFggdmlhIGEgc3ByaW5nXG5teUxheWVyLmhvb2tcblx0cHJvcGVydHk6IFwieFwiXG5cdHRhcmdldFByb3BlcnR5OiBcImVhc2VkWFwiXG5cdHR5cGU6IFwic3ByaW5nKDE1MCwgMTUpXCJcblxuIyBBbmltYXRlIGVhc2VkWFxubXlMYXllci5hbmltYXRlXG5cdHByb3BlcnRpZXM6XG5cdFx0ZWFzZWRYOiAyMDBcblx0dGltZTogMC4xNVxuXHRjdXJ2ZTogXCJjdWJpYy1iZXppZXIoMC4yLCAwLCAwLjQsIDEpXCJcblxuTk9URTogXG5UbyBhdHRhY2ggYm90aCB0aGUgeCBhbmQgeSBwb3NpdGlvbiwgdXNlIFwicG9zXCIsIFwibWlkUG9zXCIgb3IgXCJtYXhQb3NcIiBhcyB0aGVcbnByb3BlcnR5L3RhcmdldFByb3BlcnR5LlxuXG5cbi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5FeGFtcGxlOiBIb29raW5nIHByb3BlcnR5IHRvIGFub3RoZXIgbGF5ZXJcbi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5cbnRhcmdldCA9IG5ldyBMYXllclxuaG9va2VkID0gbmV3IExheWVyXG5cbmhvb2tlZC5ob29rXG5cdHByb3BlcnR5OiBcInNjYWxlXCJcblx0dG86IHRhcmdldFxuXHR0eXBlOiBcInNwcmluZygxNTAsIDE1KVwiXG5cblRoZSBcImhvb2tlZFwiIGxheWVyJ3Mgc2NhbGUgd2lsbCBub3cgY29udGludW91c2x5IGZvbGxvdyB0aGUgdGFyZ2V0IGxheWVyJ3Mgc2NhbGVcbndpdGggYSBzcHJpbmcgYW5pbWF0aW9uLlxuXG5cbi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5sYXllci5ob29rKG9wdGlvbnMpXG4tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLVxuXG5PcHRpb25zIGFyZSBwYXNzZWQgYXMgYSBzaW5nbGUgb2JqZWN0LCBsaWtlIHlvdSB3b3VsZCBmb3IgYSBuZXcgTGF5ZXIuXG5UaGUgb3B0aW9ucyBvYmplY3QgdGFrZXMgdGhlIGZvbGxvd2luZyBwcm9wZXJ0aWVzOlxuXG5cbnByb3BlcnR5IFtTdHJpbmddXG4tLS0tLS0tLS0tLS0tLS0tLVxuVGhlIHByb3BlcnR5IHlvdSdkIGxpa2UgdG8gaG9vayBvbnRvIGFub3RoZXIgb2JqZWN0J3MgcHJvcGVydHlcblxuXG50eXBlIFtTdHJpbmddXG4tLS0tLS0tLS0tLS0tXG5FaXRoZXIgXCJzcHJpbmcoc3RyZW5ndGgsIGZyaWN0aW9uKVwiIG9yIFwiZ3Jhdml0eShzdHJlbmd0aCwgZHJhZylcIi4gT25seSB0aGUgbGFzdFxuc3BlY2lmaWVkIGRyYWcgdmFsdWUgaXMgdXNlZCBmb3IgZWFjaCBwcm9wZXJ0eSwgc2luY2UgaXQgaXMgb25seSBhcHBsaWVkIHRvXG5lYWNoIHByb3BlcnR5IG9uY2UgKGFuZCBvbmx5IGlmIGl0IGhhcyBhIGdyYXZpdHkgaG9vayBhcHBsaWVkIHRvIGl0LilcblxuXG50byBbT2JqZWN0XSAoT3B0aW9uYWwpXG4tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5UaGUgb2JqZWN0IHRvIGF0dGFjaCBpdCB0by4gRGVmYXVsdHMgdG8gaXRzZWxmLlxuXG5cbnRhcmdldFByb3BlcnR5IFtTdHJpbmddIChPcHRpb25hbClcbi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS1cblNwZWNpZnkgdGhlIHRhcmdldCBvYmplY3QncyBwcm9wZXJ0eSB0byBmb2xsb3csIGlmIHlvdSBkb24ndCB3YW50IHRvIGZvbGxvd1xudGhlIHNhbWUgcHJvcGVydHkgdGhhdCB0aGUgaG9vayBpcyBhcHBsaWVkIHRvLlxuXG5cbm1vZHVsYXRvciBbRnVuY3Rpb25dIChPcHRpb25hbClcbi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS1cblRoZSBtb2R1bGF0b3IgZnVuY3Rpb24gcmVjZWl2ZXMgdGhlIHRhcmdldCBwcm9wZXJ0eSdzIHZhbHVlLCBhbmQgbGV0cyB5b3Vcbm1vZGlmeSBpdCBiZWZvcmUgaXQgaXMgZmVkIGludG8gdGhlIHBoeXNpY3MgY2FsY3VsYXRpb25zLiBVc2VmdWwgZm9yIGFueXRoaW5nXG5mcm9tIHN0YW5kYXJkIFV0aWxzLm1vZHVsYXRlKCkgdHlwZSBzdHVmZiB0byBzbmFwcGluZyBhbmQgY29uZGl0aW9uYWwgdmFsdWVzLlxuXG5cbnpvb20gW051bWJlcl0gKE9wdGlvbmFsKVxuLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5UaGlzIGZhY3RvciBkZWZpbmVzIHRoZSBkaXN0YW5jZSB0aGF0IDFweCByZXByZXNlbnRzIGluIHJlZ2FyZHMgdG8gZ3Jhdml0eSBhbmRcbmRyYWcgY2FsY3VsYXRpb25zLiBPbmx5IG9uZSB2YWx1ZSBpcyBzdG9yZWQgcGVyIGxheWVyLCBzbyBzcGVjaWZ5aW5nIGl0XG5vdmVyd3JpdGVzIGl0cyBleGlzdGluZyB2YWx1ZS4gRGVmYXVsdCBpcyAxMDAuXG5cblxuLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS1cbmxheWVyLnVuSG9vayhwcm9wZXJ0eSwgb2JqZWN0KVxuLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS1cblxuVGhpcyByZW1vdmVzIGFsbCBob29rcyBmb3IgYSBnaXZlbiBwcm9wZXJ0eSBhbmQgdGFyZ2V0IG9iamVjdC4gRXhhbXBsZTpcblxuIyBIb29rIGl0XG5sYXllci5ob29rXG5cdHByb3BlcnR5OiBcInhcIlxuXHR0bzogXCJvdGhlcmxheWVyXCJcblx0dGFyZ2V0UHJvcGVydHk6IFwieVwiXG5cdHR5cGU6IFwic3ByaW5nKDIwMCwyMClcIlxuXG4jIFVuaG9vayBpdFxubGF5ZXIudW5Ib29rIFwieFwiLCBvdGhlcmxheWVyXG5cblxuLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS1cbmxheWVyLm9uSG9va1VwZGF0ZShkZWx0YSlcbi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5cbkFmdGVyIGEgbGF5ZXIgaXMgZG9uZSBhcHBseWluZyBhY2NlbGVyYXRpb25zIHRvIGl0cyBob29rZWQgcHJvcGVydGllcywgaXQgY2FsbHNcbm9uSG9va1VwZGF0ZSgpIGF0IHRoZSBlbmQgb2YgZWFjaCBmcmFtZSwgaWYgaXQgaXMgZGVmaW5lZC4gVGhpcyBpcyBhbiBlYXN5IHdheVxudG8gYW5pbWF0ZSBvciB0cmlnZ2VyIG90aGVyIHN0dWZmLCBwZXJoYXBzIGJhc2VkIG9uIHlvdXIgbGF5ZXIncyB1cGRhdGVkXG5wcm9wZXJ0aWVzIG9yIHZlbG9jaXRpZXMuXG5cblRoZSBkZWx0YSB2YWx1ZSBmcm9tIHRoZSBGcmFtZXIgbG9vcCBpcyBwYXNzZWQgb24gdG8gb25Ib29rVXBkYXRlKCkgYXMgd2VsbCxcbndoaWNoIGlzIHRoZSB0aW1lIGluIHNlY29uZHMgc2luY2UgdGhlIGxhc3QgYW5pbWF0aW9uIGZyYW1lLlxuXG5Ob3RlIHRoYXQgaWYgeW91IHVuaG9vayBhbGwgeW91ciBob29rcywgb25Ib29rVXBkYXRlKCkgd2lsbCBvZiBjb3Vyc2Ugbm8gbG9uZ2VyXG5iZSBjYWxsZWQgZm9yIHRoYXQgbGF5ZXIuXG5cbiMjI1xuXG5cbiMgU2luY2Ugb2xkZXIgdmVyc2lvbnMgb2YgU2FmYXJpIHNlZW0gdG8gYmUgbWlzc2luZyBTdHJpbmcucHJvdG90eXBlLmluY2x1ZGVzKClcblxudW5sZXNzIFN0cmluZy5wcm90b3R5cGUuaW5jbHVkZXNcblx0U3RyaW5nOjppbmNsdWRlcyA9IChzZWFyY2gsIHN0YXJ0KSAtPlxuXHRcdCd1c2Ugc3RyaWN0J1xuXHRcdHN0YXJ0ID0gMCBpZiB0eXBlb2Ygc3RhcnQgaXMgJ251bWJlcidcblxuXHRcdGlmIHN0YXJ0ICsgc2VhcmNoLmxlbmd0aCA+IHRoaXMubGVuZ3RoXG5cdFx0XHRyZXR1cm4gZmFsc2U7XG5cdFx0ZWxzZVxuXHRcdFx0cmV0dXJuIEBpbmRleE9mKHNlYXJjaCwgc3RhcnQpIGlzbnQgLTFcblxuIyBFeHBhbmQgbGF5ZXJcblxuTGF5ZXI6Omhvb2sgPSAoY29uZmlnKSAtPlxuXG5cdHRocm93IG5ldyBFcnJvciAnbGF5ZXIuaG9vaygpIG5lZWRzIGEgcHJvcGVydHksIGEgaG9vayB0eXBlIGFuZCBlaXRoZXIgYSB0YXJnZXQgb2JqZWN0IG9yIHRhcmdldCBwcm9wZXJ0eSB0byB3b3JrJyB1bmxlc3MgY29uZmlnLnByb3BlcnR5IGFuZCBjb25maWcudHlwZSBhbmQgKGNvbmZpZy50byBvciBjb25maWcudGFyZ2V0UHJvcGVydHkpXG5cblx0IyBTaW5nbGUgYXJyYXkgZm9yIGFsbCBob29rcywgYXMgb3Bwb3NlZCB0byBuZXN0ZWQgYXJyYXlzIHBlciBwcm9wZXJ0eSwgYmVjYXVzZSBwZXJmb3JtYW5jZVxuXHRAaG9va3MgPz1cblx0XHRob29rczogW11cblx0XHR2ZWxvY2l0aWVzOiB7fVxuXHRcdGRlZnM6XG5cdFx0XHR6b29tOiAxMDBcblx0XHRcdGdldERyYWc6ICh2ZWxvY2l0eSwgZHJhZywgem9vbSkgPT5cblx0XHRcdFx0dmVsb2NpdHkgLz0gem9vbVxuXHRcdFx0XHQjIERpdmlkaW5nIGJ5IDEwIGlzIHVuc2NpZW50aWZpYywgYnV0IGl0IG1lYW5zIGEgdmFsdWUgb2YgMiBlcXVhbHMgcm91Z2hseSBhIDEwMGcgYmFsbCB3aXRoIDE1Y20gcmFkaXVzIGluIGFpclxuXHRcdFx0XHRkcmFnID0gLShkcmFnIC8gMTApICogdmVsb2NpdHkgKiB2ZWxvY2l0eSAqIHZlbG9jaXR5IC8gTWF0aC5hYnModmVsb2NpdHkpXG5cdFx0XHRcdGlmIF8uaXNOYU4oZHJhZykgdGhlbiByZXR1cm4gMCBlbHNlIHJldHVybiBkcmFnXG5cdFx0XHRnZXRHcmF2aXR5OiAoc3RyZW5ndGgsIGRpc3RhbmNlLCB6b29tKSA9PlxuXHRcdFx0XHRkaXN0ID0gTWF0aC5tYXgoMSwgZGlzdGFuY2UgLyB6b29tKVxuXHRcdFx0XHRyZXR1cm4gc3RyZW5ndGggKiB6b29tIC8gKGRpc3QgKiBkaXN0KVxuXG5cdCMgVXBkYXRlIHRoZSB6b29tIHZhbHVlIGlmIGdpdmVuXG5cdEBob29rcy56b29tID0gY29uZmlnLnpvb20gaWYgY29uZmlnLnpvb21cblxuXHQjIFBhcnNlIHBoeXNpY3MgY29uZmlnIHN0cmluZ1xuXHRmID0gVXRpbHMucGFyc2VGdW5jdGlvbiBjb25maWcudHlwZVxuXHRjb25maWcudHlwZSA9IGYubmFtZVxuXHRjb25maWcuc3RyZW5ndGggPSBmLmFyZ3NbMF1cblx0Y29uZmlnLmZyaWN0aW9uID0gZi5hcmdzWzFdIG9yIDBcblxuXHQjIERlZmF1bHQgdG8gc2FtZSB0YXJnZXRQcm9wZXJ0eSBvbiBzYW1lIG9iamVjdCAoaG9wZWZ1bGx5IHlvdSd2ZSBzZXQgYXQgbGVhc3Qgb25lIG9mIHRoZXNlIHRvIHNvbWV0aGluZyBlbHNlKVxuXHRjb25maWcudGFyZ2V0UHJvcGVydHkgPz0gY29uZmlnLnByb3BlcnR5XG5cdGNvbmZpZy50byA/PSBAXG5cblx0IyBBbGwgcG9zaXRpb24gYWNjZWxlcmF0aW9ucyBhcmUgYWRkZWQgdG8gYSBzaW5nbGUgJ3BvcycgdmVsb2NpdHkuIFN0b3JlIGFjdHVhbCBwcm9wZXJ0aWVzIHNvIHdlIGRvbid0IGhhdmUgdG8gZG8gaXQgYWdhaW4gZXZlcnkgZnJhbWVcblxuXHRpZiBjb25maWcucHJvcGVydHkudG9Mb3dlckNhc2UoKS5pbmNsdWRlcyAncG9zJ1xuXHRcdGNvbmZpZy5wcm9wID0gJ3Bvcydcblx0XHRcblx0XHRpZiBjb25maWcucHJvcGVydHkudG9Mb3dlckNhc2UoKS5pbmNsdWRlcyAnbWlkJ1xuXHRcdFx0Y29uZmlnLnRoaXNYID0gJ21pZFgnXG5cdFx0XHRjb25maWcudGhpc1kgPSAnbWlkWSdcblx0XHRcblx0XHRlbHNlIGlmIGNvbmZpZy5wcm9wZXJ0eS50b0xvd2VyQ2FzZSgpLmluY2x1ZGVzICdtYXgnXG5cdFx0XHRjb25maWcudGhpc1ggPSAnbWF4WCdcblx0XHRcdGNvbmZpZy50aGlzWSA9ICdtYXhZJ1xuXHRcdFxuXHRcdGVsc2Vcblx0XHRcdGNvbmZpZy50aGlzWCA9ICd4J1xuXHRcdFx0Y29uZmlnLnRoaXNZID0gJ3knXG5cdFx0XG5cdFx0aWYgY29uZmlnLnRhcmdldFByb3BlcnR5LnRvTG93ZXJDYXNlKCkuaW5jbHVkZXMgJ21pZCdcblx0XHRcdGNvbmZpZy50b1ggPSAnbWlkWCdcblx0XHRcdGNvbmZpZy50b1kgPSAnbWlkWSdcblx0XHRcblx0XHRlbHNlIGlmIGNvbmZpZy50YXJnZXRQcm9wZXJ0eS50b0xvd2VyQ2FzZSgpLmluY2x1ZGVzICdtYXgnXG5cdFx0XHRjb25maWcudG9YID0gJ21heFgnXG5cdFx0XHRjb25maWcudG9ZID0gJ21heFknXHRcdFxuXHRcdGVsc2Vcblx0XHRcdGNvbmZpZy50b1ggPSAneCdcblx0XHRcdGNvbmZpZy50b1kgPSAneSdcblx0XHRcblx0ZWxzZVxuXHRcdGNvbmZpZy5wcm9wID0gY29uZmlnLnByb3BlcnR5XG5cblx0IyBTYXZlIGhvb2sgdG8gQGhvb2tzIGFycmF5XHRcblx0QGhvb2tzLmhvb2tzLnB1c2goY29uZmlnKVxuXG5cdCMgQ3JlYXRlIHZlbG9jaXR5IHByb3BlcnR5IGlmIG5lY2Vzc2FyeVxuXHRAaG9va3MudmVsb2NpdGllc1tjb25maWcucHJvcF0gPz0gaWYgY29uZmlnLnByb3AgaXMgJ3BvcycgdGhlbiB7IHg6IDAsIHk6IDAgfSBlbHNlIDBcblxuXHQjIFVzZSBGcmFtZXIncyBhbmltYXRpb24gbG9vcCwgc2xpZ2h0bHkgbW9yZSByb2J1c3QgdGhhbiByZXF1ZXN0QW5pbWF0aW9uRnJhbWUgZGlyZWN0bHlcblx0IyBTYXZlIHRoZSByZXR1cm5lZCBBbmltYXRpb25Mb29wIHJlZmVyZW5jZSB0byBtYWtlIHN1cmUgQGhvb2tMb29wIGlzbid0IGFkZGVkIG11bHRpcGxlIHRpbWVzIHBlciBsYXllclxuXHRAaG9va3MuZW1pdHRlciA/PSBGcmFtZXIuTG9vcC5vbigncmVuZGVyJywgQGhvb2tMb29wLCB0aGlzKVxuXG5MYXllcjo6dW5Ib29rID0gKHByb3BlcnR5LCBvYmplY3QpIC0+XG5cdFxuXHRyZXR1cm4gdW5sZXNzIEBob29rc1xuXG5cdHByb3AgPSBpZiBwcm9wZXJ0eS50b0xvd2VyQ2FzZSgpLmluY2x1ZGVzICdwb3MnIHRoZW4gJ3BvcycgZWxzZSBwcm9wZXJ0eVxuXG5cdCMgUmVtb3ZlIGFsbCBtYXRjaGVzXG5cdEBob29rcy5ob29rcyA9IEBob29rcy5ob29rcy5maWx0ZXIgKGhvb2spIC0+XG5cdFx0aG9vay50byBpc250IG9iamVjdCBvciBob29rLnByb3BlcnR5IGlzbnQgcHJvcGVydHlcblxuXHQjIElmIHRoZXJlIGFyZSBubyBob29rcyBsZWZ0LCBzaHV0IGl0IGRvd25cblx0aWYgQGhvb2tzLmhvb2tzLmxlbmd0aCBpcyAwXG5cdFx0ZGVsZXRlIEBob29rc1xuXHRcdEZyYW1lci5Mb29wLnJlbW92ZUxpc3RlbmVyICdyZW5kZXInLCBAaG9va0xvb3Bcblx0XHRyZXR1cm5cblxuXHQjIFN0aWxsIGhlcmU/IENoZWNrIGlmIHRoZXJlIGFyZSBhbnkgcmVtYWluaW5nIGhvb2tzIGFmZmVjdGluZyBzYW1lIHZlbG9jaXR5XG5cdHJlbWFpbmluZyA9IEBob29rcy5ob29rcy5maWx0ZXIgKGhvb2spIC0+XG5cdFx0cHJvcCBpcyBob29rLnByb3Bcblx0XHRcblx0IyBJZiBub3QsIGRlbGV0ZSB2ZWxvY2l0eSAob3RoZXJ3aXNlIGl0IHdvbid0IGJlIHJlc2V0IGlmIHlvdSBtYWtlIG5ldyBob29rIGZvciBzYW1lIHByb3BlcnR5KVxuXHRkZWxldGUgQGhvb2tzLnZlbG9jaXRpZXNbcHJvcF0gaWYgcmVtYWluaW5nLmxlbmd0aCBpcyAwXG5cbkxheWVyOjpob29rTG9vcCA9IChkZWx0YSkgLT5cblxuXHRpZiBAaG9va3NcblxuXHRcdCMgTXVsdGlwbGUgaG9va3MgY2FuIGFmZmVjdCB0aGUgc2FtZSBwcm9wZXJ0eS4gQWRkIGFjY2VsZXJhdGlvbnMgdG8gdGVtcG9yYXJ5IG9iamVjdCBzbyB0aGUgcHJvcGVydHkncyB2ZWxvY2l0eSBpcyB0aGUgc2FtZSBmb3IgYWxsIGNhbGN1bGF0aW9ucyB3aXRoaW4gdGhlIHNhbWUgYW5pbWF0aW9uIGZyYW1lXG5cdFx0YWNjZWxlcmF0aW9uID0ge31cblx0XHRcblx0XHQjIFNhdmUgZHJhZyBmb3IgZWFjaCBwcm9wZXJ0eSB0byB0aGlzIG9iamVjdCwgc2luY2Ugb25seSBtb3N0IHJlY2VudGx5IHNwZWNpZmllZCB2YWx1ZSBpcyB1c2VkIGZvciBlYWNoIHByb3BlcnR5XG5cdFx0ZHJhZyA9IHt9XG5cdFx0XG5cdFx0IyBBZGQgYWNjZWxlcmF0aW9uc1xuXHRcdGZvciBob29rIGluIEBob29rcy5ob29rc1xuXHRcdFxuXHRcdFx0aWYgaG9vay5wcm9wIGlzICdwb3MnXG5cblx0XHRcdFx0YWNjZWxlcmF0aW9uLnBvcyA/PSB7IHg6IDAsIHk6IDAgfVxuXG5cdFx0XHRcdHRhcmdldCA9IHsgeDogaG9vay50b1tob29rLnRvWF0sIHk6IGhvb2sudG9baG9vay50b1ldIH1cblxuXHRcdFx0XHR0YXJnZXQgPSBob29rLm1vZHVsYXRvcih0YXJnZXQpIGlmIGhvb2subW9kdWxhdG9yXG5cblx0XHRcdFx0dmVjdG9yID1cblx0XHRcdFx0XHR4OiB0YXJnZXQueCAtIEBbaG9vay50aGlzWF1cblx0XHRcdFx0XHR5OiB0YXJnZXQueSAtIEBbaG9vay50aGlzWV1cblx0XHRcdFx0XG5cdFx0XHRcdHZMZW5ndGggPSBNYXRoLnNxcnQoKHZlY3Rvci54ICogdmVjdG9yLngpICsgKHZlY3Rvci55ICogdmVjdG9yLnkpKVxuXG5cdFx0XHRcdGlmIGhvb2sudHlwZSBpcyAnc3ByaW5nJ1xuXG5cdFx0XHRcdFx0ZGFtcGVyID1cblx0XHRcdFx0XHRcdHg6IC1ob29rLmZyaWN0aW9uICogQGhvb2tzLnZlbG9jaXRpZXMucG9zLnhcblx0XHRcdFx0XHRcdHk6IC1ob29rLmZyaWN0aW9uICogQGhvb2tzLnZlbG9jaXRpZXMucG9zLnlcblxuXHRcdFx0XHRcdHZlY3Rvci54ICo9IGhvb2suc3RyZW5ndGhcblx0XHRcdFx0XHR2ZWN0b3IueSAqPSBob29rLnN0cmVuZ3RoXG5cdFx0XHRcdFx0XG5cdFx0XHRcdFx0YWNjZWxlcmF0aW9uLnBvcy54ICs9ICh2ZWN0b3IueCArIGRhbXBlci54KSAqIGRlbHRhXG5cdFx0XHRcdFx0YWNjZWxlcmF0aW9uLnBvcy55ICs9ICh2ZWN0b3IueSArIGRhbXBlci55KSAqIGRlbHRhXG5cdFx0XHRcdFxuXHRcdFx0XHRlbHNlIGlmIGhvb2sudHlwZSBpcyAnZ3Jhdml0eSdcblx0XHRcdFx0XG5cdFx0XHRcdFx0ZHJhZy5wb3MgPSBob29rLmZyaWN0aW9uXG5cdFx0XHRcdFx0XG5cdFx0XHRcdFx0Z3Jhdml0eSA9IEBob29rcy5kZWZzLmdldEdyYXZpdHkoaG9vay5zdHJlbmd0aCwgdkxlbmd0aCwgQGhvb2tzLmRlZnMuem9vbSlcblxuXHRcdFx0XHRcdHZlY3Rvci54ICo9IGdyYXZpdHkgLyB2TGVuZ3RoXG5cdFx0XHRcdFx0dmVjdG9yLnkgKj0gZ3Jhdml0eSAvIHZMZW5ndGhcblx0XHRcdFx0XHRcblx0XHRcdFx0XHRhY2NlbGVyYXRpb24ucG9zLnggKz0gdmVjdG9yLnggKiBkZWx0YVxuXHRcdFx0XHRcdGFjY2VsZXJhdGlvbi5wb3MueSArPSB2ZWN0b3IueSAqIGRlbHRhXG5cdFx0XHRcdFx0XHRcdFx0XHRcblx0XHRcdGVsc2Vcblx0XHRcdFx0XG5cdFx0XHRcdGFjY2VsZXJhdGlvbltob29rLnByb3BdID89IDBcblxuXHRcdFx0XHR0YXJnZXQgPSBob29rLnRvW2hvb2sudGFyZ2V0UHJvcGVydHldXG5cblx0XHRcdFx0dGFyZ2V0ID0gaG9vay5tb2R1bGF0b3IodGFyZ2V0KSBpZiBob29rLm1vZHVsYXRvclxuXG5cdFx0XHRcdHZlY3RvciA9IHRhcmdldCAtIEBbaG9vay5wcm9wXVxuXHRcdFx0XHRcblx0XHRcdFx0aWYgaG9vay50eXBlIGlzICdzcHJpbmcnXG5cblx0XHRcdFx0XHRmb3JjZSA9IHZlY3RvciAqIGhvb2suc3RyZW5ndGhcblx0XHRcdFx0XHRkYW1wZXIgPSAtaG9vay5mcmljdGlvbiAqIEBob29rcy52ZWxvY2l0aWVzW2hvb2sucHJvcF1cblxuXHRcdFx0XHRcdGFjY2VsZXJhdGlvbltob29rLnByb3BdICs9IChmb3JjZSArIGRhbXBlcikgKiBkZWx0YVxuXG5cdFx0XHRcdFxuXHRcdFx0XHRlbHNlIGlmIGhvb2sudHlwZSBpcyAnZ3Jhdml0eSdcblx0XG5cdFx0XHRcdFx0ZHJhZ1tob29rLnByb3BdID0gaG9vay5mcmljdGlvblxuXHRcdFx0XHRcdFxuXHRcdFx0XHRcdGZvcmNlID0gQGhvb2tzLmRlZnMuZ2V0R3Jhdml0eShob29rLnN0cmVuZ3RoLCB2ZWN0b3IsIEBob29rcy5kZWZzLnpvb20pXG5cdFx0XHRcdFx0XG5cdFx0XHRcdFx0YWNjZWxlcmF0aW9uW2hvb2sucHJvcF0gKz0gZm9yY2UgKiBkZWx0YVxuXHRcdFxuXHRcdFxuXHRcdCMgQWRkIHZlbG9jaXRpZXMgdG8gcHJvcGVydGllcy4gRG9pbmcgdGhpcyBhdCB0aGUgZW5kIGluIGNhc2UgdGhlcmUgYXJlIG11bHRpcGxlIGhvb2tzIGFmZmVjdGluZyB0aGUgc2FtZSB2ZWxvY2l0eVxuXHRcdGZvciBwcm9wLCB2ZWxvY2l0eSBvZiBAaG9va3MudmVsb2NpdGllc1xuXHRcdFxuXHRcdFx0aWYgcHJvcCBpcyAncG9zJ1xuXG5cdFx0XHRcdCMgQWRkIGRyYWcsIGlmIGl0IGV4aXN0c1xuXHRcdFx0XHRpZiBkcmFnLnBvc1xuXHRcdFx0XHRcdHZlbG9jaXR5LnggKz0gQGhvb2tzLmRlZnMuZ2V0RHJhZyh2ZWxvY2l0eS54LCBkcmFnLnBvcywgQGhvb2tzLmRlZnMuem9vbSlcblx0XHRcdFx0XHR2ZWxvY2l0eS55ICs9IEBob29rcy5kZWZzLmdldERyYWcodmVsb2NpdHkueSwgZHJhZy5wb3MsIEBob29rcy5kZWZzLnpvb20pXG5cdFx0XHRcdFx0XG5cdFx0XHRcdCMgQWRkIGFjY2VsZXJhdGlvbiB0byB2ZWxvY2l0eVxuXHRcdFx0XHR2ZWxvY2l0eS54ICs9IGFjY2VsZXJhdGlvbi5wb3MueFxuXHRcdFx0XHR2ZWxvY2l0eS55ICs9IGFjY2VsZXJhdGlvbi5wb3MueVxuXHRcdFx0XHRcblx0XHRcdFx0IyBBZGQgdmVsb2NpdHkgdG8gcG9zaXRpb25cblx0XHRcdFx0QHggKz0gdmVsb2NpdHkueCAqIGRlbHRhXG5cdFx0XHRcdEB5ICs9IHZlbG9jaXR5LnkgKiBkZWx0YVxuXHRcdFx0XG5cdFx0XHRlbHNlXG5cdFx0XHRcblx0XHRcdFx0IyBBZGQgZHJhZywgaWYgaXQgZXhpc3RzXG5cdFx0XHRcdGlmIGRyYWdbcHJvcF1cblx0XHRcdFx0XHRAaG9va3MudmVsb2NpdGllc1twcm9wXSArPSBAaG9va3MuZGVmcy5nZXREcmFnKEBob29rcy52ZWxvY2l0aWVzW3Byb3BdLCBkcmFnW3Byb3BdLCBAaG9va3MuZGVmcy56b29tKVxuXHRcdFx0XHRcblx0XHRcdFx0IyBBZGQgYWNjZWxlcmF0aW9uIHRvIHZlbG9jaXR5XG5cdFx0XHRcdEBob29rcy52ZWxvY2l0aWVzW3Byb3BdICs9IGFjY2VsZXJhdGlvbltwcm9wXVxuXHRcdFx0XHRcblx0XHRcdFx0IyBBZGQgdmVsb2NpdHkgdG8gcHJvcGVydHlcblx0XHRcdFx0QFtwcm9wXSArPSBAaG9va3MudmVsb2NpdGllc1twcm9wXSAqIGRlbHRhXG5cblx0XHRAb25Ib29rVXBkYXRlPyhkZWx0YSkiXX0= 346 | -------------------------------------------------------------------------------- /hook-example-modulator.framer/framer/images/cursor-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-modulator.framer/framer/images/cursor-active.png -------------------------------------------------------------------------------- /hook-example-modulator.framer/framer/images/cursor-active@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-modulator.framer/framer/images/cursor-active@2x.png -------------------------------------------------------------------------------- /hook-example-modulator.framer/framer/images/cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-modulator.framer/framer/images/cursor.png -------------------------------------------------------------------------------- /hook-example-modulator.framer/framer/images/cursor@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-modulator.framer/framer/images/cursor@2x.png -------------------------------------------------------------------------------- /hook-example-modulator.framer/framer/images/icon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-modulator.framer/framer/images/icon-120.png -------------------------------------------------------------------------------- /hook-example-modulator.framer/framer/images/icon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-modulator.framer/framer/images/icon-152.png -------------------------------------------------------------------------------- /hook-example-modulator.framer/framer/images/icon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-modulator.framer/framer/images/icon-180.png -------------------------------------------------------------------------------- /hook-example-modulator.framer/framer/images/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-modulator.framer/framer/images/icon-192.png -------------------------------------------------------------------------------- /hook-example-modulator.framer/framer/images/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-modulator.framer/framer/images/icon-76.png -------------------------------------------------------------------------------- /hook-example-modulator.framer/framer/manifest.txt: -------------------------------------------------------------------------------- 1 | app.coffee 2 | framer/coffee-script.js 3 | framer/config.json 4 | framer/framer.generated.js 5 | framer/framer.init.js 6 | framer/framer.js 7 | framer/framer.js.map 8 | framer/framer.modules.js 9 | framer/images/cursor-active.png 10 | framer/images/cursor-active@2x.png 11 | framer/images/cursor.png 12 | framer/images/cursor@2x.png 13 | framer/images/icon-120.png 14 | framer/images/icon-152.png 15 | framer/images/icon-180.png 16 | framer/images/icon-192.png 17 | framer/images/icon-76.png 18 | framer/preview.png 19 | framer/style.css 20 | framer/version 21 | images/spinner.svg 22 | imported/app@2x/images/Layer-page-meneqzew.png 23 | imported/app@2x/layers.json 24 | imported/app@2x/layers.json.js 25 | index.html 26 | modules/Hook.coffee -------------------------------------------------------------------------------- /hook-example-modulator.framer/framer/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-modulator.framer/framer/preview.png -------------------------------------------------------------------------------- /hook-example-modulator.framer/framer/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | border: none; 5 | -webkit-user-select: none; 6 | -webkit-tap-highlight-color: rgba(0,0,0,0); 7 | } 8 | 9 | body { 10 | background-color: #fff; 11 | font: 28px/1em "Helvetica"; 12 | color: gray; 13 | overflow: hidden; 14 | } 15 | 16 | a { 17 | color: gray; 18 | } 19 | 20 | body { 21 | cursor: url('images/cursor.png') 32 32, auto; 22 | cursor: -webkit-image-set( 23 | url('images/cursor.png') 1x, 24 | url('images/cursor@2x.png') 2x 25 | ) 32 32, auto; 26 | } 27 | 28 | body:active { 29 | cursor: url('images/cursor-active.png') 32 32, auto; 30 | cursor: -webkit-image-set( 31 | url('images/cursor-active.png') 1x, 32 | url('images/cursor-active@2x.png') 2x 33 | ) 32 32, auto; 34 | } 35 | 36 | .framerAlertBackground { 37 | position: absolute; top:0px; left:0px; right:0px; bottom:0px; 38 | z-index: 1000; 39 | background-color: #fff; 40 | } 41 | 42 | .framerAlert { 43 | font:400 14px/1.4 "Helvetica Neue", Helvetica, Arial, sans-serif; 44 | -webkit-font-smoothing:antialiased; 45 | color:#616367; text-align:center; 46 | position: absolute; top:40%; left:50%; width:260px; margin-left:-130px; 47 | } 48 | .framerAlert strong { font-weight:500; color:#000; margin-bottom:8px; display:block; } 49 | .framerAlert a { color:#28AFFA; } 50 | .framerAlert .btn { 51 | font-weight:500; text-decoration:none; line-height:1; 52 | display:inline-block; padding:6px 12px 7px 12px; 53 | border-radius:3px; margin-top:12px; 54 | background:#28AFFA; color:#fff; 55 | } 56 | 57 | ::-webkit-scrollbar { 58 | display: none; 59 | } -------------------------------------------------------------------------------- /hook-example-modulator.framer/framer/version: -------------------------------------------------------------------------------- 1 | 4 -------------------------------------------------------------------------------- /hook-example-modulator.framer/images/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-modulator.framer/images/.gitkeep -------------------------------------------------------------------------------- /hook-example-modulator.framer/images/spinner.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hook-example-modulator.framer/imported/app@2x/images/Layer-page-meneqzew.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-modulator.framer/imported/app@2x/images/Layer-page-meneqzew.png -------------------------------------------------------------------------------- /hook-example-modulator.framer/imported/app@2x/layers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "objectId": "0CDC1067-9F1A-4B22-8CF3-3B8FEE516032", 4 | "kind": "group", 5 | "name": "page", 6 | "maskFrame": null, 7 | "layerFrame": { 8 | "x": 0, 9 | "y": 0, 10 | "width": 375, 11 | "height": 667 12 | }, 13 | "visible": true, 14 | "metadata": { 15 | "opacity": 1 16 | }, 17 | "image": { 18 | "path": "images/Layer-page-meneqzew.png", 19 | "frame": { 20 | "x": 0, 21 | "y": 0, 22 | "width": 375, 23 | "height": 667 24 | } 25 | }, 26 | "children": [], 27 | "time": 123 28 | } 29 | ] -------------------------------------------------------------------------------- /hook-example-modulator.framer/imported/app@2x/layers.json.js: -------------------------------------------------------------------------------- 1 | window.__imported__ = window.__imported__ || {}; 2 | window.__imported__["app@2x/layers.json.js"] = [ 3 | { 4 | "objectId": "0CDC1067-9F1A-4B22-8CF3-3B8FEE516032", 5 | "kind": "group", 6 | "name": "page", 7 | "maskFrame": null, 8 | "layerFrame": { 9 | "x": 0, 10 | "y": 0, 11 | "width": 375, 12 | "height": 667 13 | }, 14 | "visible": true, 15 | "metadata": { 16 | "opacity": 1 17 | }, 18 | "image": { 19 | "path": "images/Layer-page-meneqzew.png", 20 | "frame": { 21 | "x": 0, 22 | "y": 0, 23 | "width": 375, 24 | "height": 667 25 | } 26 | }, 27 | "children": [], 28 | "time": 123 29 | } 30 | ] -------------------------------------------------------------------------------- /hook-example-modulator.framer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /hook-example-modulator.framer/modules/Hook.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | -------------------------------------------------------------------------------- 3 | Hook module for Framer 4 | -------------------------------------------------------------------------------- 5 | 6 | by: Sigurd Mannsåker 7 | github: https://github.com/sigtm/framer-hook 8 | 9 | ················································································ 10 | 11 | 12 | The Hook module simply expands the Layer prototype, and lets you make any 13 | numeric Layer property follow another property - either its own or another 14 | object's - via a spring or gravity attraction. 15 | 16 | 17 | -------------------------------------------------------------------------------- 18 | Example: Layered animation (eased + spring) 19 | -------------------------------------------------------------------------------- 20 | 21 | myLayer = new Layer 22 | 23 | # Make our own custom property for the x property to follow 24 | myLayer.easedX = 0 25 | 26 | # Hook x to easedX via a spring 27 | myLayer.hook 28 | property: "x" 29 | targetProperty: "easedX" 30 | type: "spring(150, 15)" 31 | 32 | # Animate easedX 33 | myLayer.animate 34 | properties: 35 | easedX: 200 36 | time: 0.15 37 | curve: "cubic-bezier(0.2, 0, 0.4, 1)" 38 | 39 | NOTE: 40 | To attach both the x and y position, use "pos", "midPos" or "maxPos" as the 41 | property/targetProperty. 42 | 43 | 44 | -------------------------------------------------------------------------------- 45 | Example: Hooking property to another layer 46 | -------------------------------------------------------------------------------- 47 | 48 | target = new Layer 49 | hooked = new Layer 50 | 51 | hooked.hook 52 | property: "scale" 53 | to: target 54 | type: "spring(150, 15)" 55 | 56 | The "hooked" layer's scale will now continuously follow the target layer's scale 57 | with a spring animation. 58 | 59 | 60 | -------------------------------------------------------------------------------- 61 | layer.hook(options) 62 | -------------------------------------------------------------------------------- 63 | 64 | Options are passed as a single object, like you would for a new Layer. 65 | The options object takes the following properties: 66 | 67 | 68 | property [String] 69 | ----------------- 70 | The property you'd like to hook onto another object's property 71 | 72 | 73 | type [String] 74 | ------------- 75 | Either "spring(strength, friction)" or "gravity(strength, drag)". Only the last 76 | specified drag value is used for each property, since it is only applied to 77 | each property once (and only if it has a gravity hook applied to it.) 78 | 79 | 80 | to [Object] (Optional) 81 | ---------------------- 82 | The object to attach it to. Defaults to itself. 83 | 84 | 85 | targetProperty [String] (Optional) 86 | ---------------------------------- 87 | Specify the target object's property to follow, if you don't want to follow 88 | the same property that the hook is applied to. 89 | 90 | 91 | modulator [Function] (Optional) 92 | ------------------------------- 93 | The modulator function receives the target property's value, and lets you 94 | modify it before it is fed into the physics calculations. Useful for anything 95 | from standard Utils.modulate() type stuff to snapping and conditional values. 96 | 97 | 98 | zoom [Number] (Optional) 99 | ------------------------ 100 | This factor defines the distance that 1px represents in regards to gravity and 101 | drag calculations. Only one value is stored per layer, so specifying it 102 | overwrites its existing value. Default is 100. 103 | 104 | 105 | -------------------------------------------------------------------------------- 106 | layer.unHook(property, object) 107 | -------------------------------------------------------------------------------- 108 | 109 | This removes all hooks for a given property and target object. Example: 110 | 111 | # Hook it 112 | layer.hook 113 | property: "x" 114 | to: "otherlayer" 115 | targetProperty: "y" 116 | type: "spring(200,20)" 117 | 118 | # Unhook it 119 | layer.unHook "x", otherlayer 120 | 121 | 122 | -------------------------------------------------------------------------------- 123 | layer.onHookUpdate(delta) 124 | -------------------------------------------------------------------------------- 125 | 126 | After a layer is done applying accelerations to its hooked properties, it calls 127 | onHookUpdate() at the end of each frame, if it is defined. This is an easy way 128 | to animate or trigger other stuff, perhaps based on your layer's updated 129 | properties or velocities. 130 | 131 | The delta value from the Framer loop is passed on to onHookUpdate() as well, 132 | which is the time in seconds since the last animation frame. 133 | 134 | Note that if you unhook all your hooks, onHookUpdate() will of course no longer 135 | be called for that layer. 136 | 137 | ### 138 | 139 | 140 | # Since older versions of Safari seem to be missing String.prototype.includes() 141 | 142 | unless String.prototype.includes 143 | String::includes = (search, start) -> 144 | 'use strict' 145 | start = 0 if typeof start is 'number' 146 | 147 | if start + search.length > this.length 148 | return false; 149 | else 150 | return @indexOf(search, start) isnt -1 151 | 152 | # Expand layer 153 | 154 | Layer::hook = (config) -> 155 | 156 | throw new Error 'layer.hook() needs a property, a hook type and either a target object or target property to work' unless config.property and config.type and (config.to or config.targetProperty) 157 | 158 | # Single array for all hooks, as opposed to nested arrays per property, because performance 159 | @hooks ?= 160 | hooks: [] 161 | velocities: {} 162 | defs: 163 | zoom: 100 164 | getDrag: (velocity, drag, zoom) => 165 | velocity /= zoom 166 | # Dividing by 10 is unscientific, but it means a value of 2 equals roughly a 100g ball with 15cm radius in air 167 | drag = -(drag / 10) * velocity * velocity * velocity / Math.abs(velocity) 168 | if _.isNaN(drag) then return 0 else return drag 169 | getGravity: (strength, distance, zoom) => 170 | dist = Math.max(1, distance / zoom) 171 | return strength * zoom / (dist * dist) 172 | 173 | # Update the zoom value if given 174 | @hooks.zoom = config.zoom if config.zoom 175 | 176 | # Parse physics config string 177 | f = Utils.parseFunction config.type 178 | config.type = f.name 179 | config.strength = f.args[0] 180 | config.friction = f.args[1] or 0 181 | 182 | # Default to same targetProperty on same object (hopefully you've set at least one of these to something else) 183 | config.targetProperty ?= config.property 184 | config.to ?= @ 185 | 186 | # All position accelerations are added to a single 'pos' velocity. Store actual properties so we don't have to do it again every frame 187 | 188 | if config.property.toLowerCase().includes 'pos' 189 | config.prop = 'pos' 190 | 191 | if config.property.toLowerCase().includes 'mid' 192 | config.thisX = 'midX' 193 | config.thisY = 'midY' 194 | 195 | else if config.property.toLowerCase().includes 'max' 196 | config.thisX = 'maxX' 197 | config.thisY = 'maxY' 198 | 199 | else 200 | config.thisX = 'x' 201 | config.thisY = 'y' 202 | 203 | if config.targetProperty.toLowerCase().includes 'mid' 204 | config.toX = 'midX' 205 | config.toY = 'midY' 206 | 207 | else if config.targetProperty.toLowerCase().includes 'max' 208 | config.toX = 'maxX' 209 | config.toY = 'maxY' 210 | else 211 | config.toX = 'x' 212 | config.toY = 'y' 213 | 214 | else 215 | config.prop = config.property 216 | 217 | # Save hook to @hooks array 218 | @hooks.hooks.push(config) 219 | 220 | # Create velocity property if necessary 221 | @hooks.velocities[config.prop] ?= if config.prop is 'pos' then { x: 0, y: 0 } else 0 222 | 223 | # Use Framer's animation loop, slightly more robust than requestAnimationFrame directly 224 | # Save the returned AnimationLoop reference to make sure @hookLoop isn't added multiple times per layer 225 | @hooks.emitter ?= Framer.Loop.on('render', @hookLoop, this) 226 | 227 | Layer::unHook = (property, object) -> 228 | 229 | return unless @hooks 230 | 231 | prop = if property.toLowerCase().includes 'pos' then 'pos' else property 232 | 233 | # Remove all matches 234 | @hooks.hooks = @hooks.hooks.filter (hook) -> 235 | hook.to isnt object or hook.property isnt property 236 | 237 | # If there are no hooks left, shut it down 238 | if @hooks.hooks.length is 0 239 | delete @hooks 240 | Framer.Loop.removeListener 'render', @hookLoop 241 | return 242 | 243 | # Still here? Check if there are any remaining hooks affecting same velocity 244 | remaining = @hooks.hooks.filter (hook) -> 245 | prop is hook.prop 246 | 247 | # If not, delete velocity (otherwise it won't be reset if you make new hook for same property) 248 | delete @hooks.velocities[prop] if remaining.length is 0 249 | 250 | Layer::hookLoop = (delta) -> 251 | 252 | if @hooks 253 | 254 | # Multiple hooks can affect the same property. Add accelerations to temporary object so the property's velocity is the same for all calculations within the same animation frame 255 | acceleration = {} 256 | 257 | # Save drag for each property to this object, since only most recently specified value is used for each property 258 | drag = {} 259 | 260 | # Add accelerations 261 | for hook in @hooks.hooks 262 | 263 | if hook.prop is 'pos' 264 | 265 | acceleration.pos ?= { x: 0, y: 0 } 266 | 267 | target = { x: hook.to[hook.toX], y: hook.to[hook.toY] } 268 | 269 | target = hook.modulator(target) if hook.modulator 270 | 271 | vector = 272 | x: target.x - @[hook.thisX] 273 | y: target.y - @[hook.thisY] 274 | 275 | vLength = Math.sqrt((vector.x * vector.x) + (vector.y * vector.y)) 276 | 277 | if hook.type is 'spring' 278 | 279 | damper = 280 | x: -hook.friction * @hooks.velocities.pos.x 281 | y: -hook.friction * @hooks.velocities.pos.y 282 | 283 | vector.x *= hook.strength 284 | vector.y *= hook.strength 285 | 286 | acceleration.pos.x += (vector.x + damper.x) * delta 287 | acceleration.pos.y += (vector.y + damper.y) * delta 288 | 289 | else if hook.type is 'gravity' 290 | 291 | drag.pos = hook.friction 292 | 293 | gravity = @hooks.defs.getGravity(hook.strength, vLength, @hooks.defs.zoom) 294 | 295 | vector.x *= gravity / vLength 296 | vector.y *= gravity / vLength 297 | 298 | acceleration.pos.x += vector.x * delta 299 | acceleration.pos.y += vector.y * delta 300 | 301 | else 302 | 303 | acceleration[hook.prop] ?= 0 304 | 305 | target = hook.to[hook.targetProperty] 306 | 307 | target = hook.modulator(target) if hook.modulator 308 | 309 | vector = target - @[hook.prop] 310 | 311 | if hook.type is 'spring' 312 | 313 | force = vector * hook.strength 314 | damper = -hook.friction * @hooks.velocities[hook.prop] 315 | 316 | acceleration[hook.prop] += (force + damper) * delta 317 | 318 | 319 | else if hook.type is 'gravity' 320 | 321 | drag[hook.prop] = hook.friction 322 | 323 | force = @hooks.defs.getGravity(hook.strength, vector, @hooks.defs.zoom) 324 | 325 | acceleration[hook.prop] += force * delta 326 | 327 | 328 | # Add velocities to properties. Doing this at the end in case there are multiple hooks affecting the same velocity 329 | for prop, velocity of @hooks.velocities 330 | 331 | if prop is 'pos' 332 | 333 | # Add drag, if it exists 334 | if drag.pos 335 | velocity.x += @hooks.defs.getDrag(velocity.x, drag.pos, @hooks.defs.zoom) 336 | velocity.y += @hooks.defs.getDrag(velocity.y, drag.pos, @hooks.defs.zoom) 337 | 338 | # Add acceleration to velocity 339 | velocity.x += acceleration.pos.x 340 | velocity.y += acceleration.pos.y 341 | 342 | # Add velocity to position 343 | @x += velocity.x * delta 344 | @y += velocity.y * delta 345 | 346 | else 347 | 348 | # Add drag, if it exists 349 | if drag[prop] 350 | @hooks.velocities[prop] += @hooks.defs.getDrag(@hooks.velocities[prop], drag[prop], @hooks.defs.zoom) 351 | 352 | # Add acceleration to velocity 353 | @hooks.velocities[prop] += acceleration[prop] 354 | 355 | # Add velocity to property 356 | @[prop] += @hooks.velocities[prop] * delta 357 | 358 | @onHookUpdate?(delta) -------------------------------------------------------------------------------- /hook-example-spring.framer/.gitignore: -------------------------------------------------------------------------------- 1 | # Framer Git Ignore 2 | 3 | # General OSX 4 | 5 | .DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | 9 | # Icon must end with two \r 10 | Icon 11 | 12 | 13 | # Thumbnails 14 | ._* 15 | 16 | # Files that might appear in the root of a volume 17 | .DocumentRevisions-V100 18 | .fseventsd 19 | .Spotlight-V100 20 | .TemporaryItems 21 | .Trashes 22 | .VolumeIcon.icns 23 | 24 | # Directories potentially created on remote AFP share 25 | .AppleDB 26 | .AppleDesktop 27 | Network Trash Folder 28 | Temporary Items 29 | .apdisk 30 | 31 | 32 | # Framer Specific 33 | .temp.html 34 | framer/*.old* 35 | framer/backup.coffee 36 | framer/backups/* 37 | framer/.*.hash 38 | -------------------------------------------------------------------------------- /hook-example-spring.framer/.viewer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /hook-example-spring.framer/app.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | 3 | Hook example 4 | -------------- 5 | 6 | Note: This example uses two layers to illustrate the concept more clearly, but 7 | the separate layer for the eased animation is unnecessary. We could just as easily 8 | have created another property, say layer.easedX, and hooked the x property to that. 9 | Then you'd just run layer.animate() on the easedX property like you would any 10 | other property. 11 | 12 | The Hook module simply expands the Layer prototype, and lets you make any 13 | numeric Layer property follow a property on another object via a spring or a 14 | gravity attraction. Check the comments in modules/Hook.coffee for a more thorough 15 | documentation. 16 | 17 | ### 18 | 19 | 20 | 21 | # Require Hook. No exports, it just adds methods to the Layer prototype 22 | # -------------------------------------------------------------------------------- 23 | 24 | require "Hook" 25 | 26 | 27 | 28 | # Settings 29 | # -------------------------------------------------------------------------------- 30 | 31 | colors = 32 | background: "#260355" 33 | orange: "#f90" 34 | blue: "#63ffff" 35 | primary: "white" 36 | secondary: "rgba(255,255,255,0.1)" 37 | 38 | margin = 60 39 | 40 | easeInOut = "cubic-bezier(0.2, 0, 0.4, 1)" 41 | 42 | 43 | 44 | # Set background and defaults 45 | # -------------------------------------------------------------------------------- 46 | 47 | Framer.Device.viewport.backgroundColor = colors.background 48 | 49 | Framer.Defaults.Animation = 50 | curve: easeInOut 51 | time: 0.15 52 | 53 | 54 | 55 | # Set up example 56 | # -------------------------------------------------------------------------------- 57 | 58 | eased = new Layer 59 | x: margin 60 | y: 300 61 | width: 100 62 | height: 100 63 | backgroundColor: colors.blue 64 | borderRadius: 12 65 | 66 | eased.states.add 67 | right: 68 | maxX: Screen.width - margin 69 | 70 | hooked = new Layer 71 | x: eased.x 72 | y: eased.maxY + margin 73 | width: 100 74 | height: 100 75 | backgroundColor: colors.orange 76 | borderRadius: 12 77 | 78 | 79 | # We run hook() in the slider change callback 80 | runAnim = () -> 81 | 82 | # Remove any existing hooks, since we want updated spring values 83 | hooked.unHook 'x', eased 84 | 85 | # Attach the x property to the eased layer with the sliders' spring values 86 | hooked.hook 87 | property: 'x' 88 | to: eased 89 | type: 'spring(' + spring.value + ', ' + friction.value + ')' 90 | 91 | # Animate the eased layer 92 | eased.states.animationOptions = 93 | curve: easeInOut 94 | time: speed.value / 1000 95 | 96 | eased.states.next() 97 | 98 | 99 | 100 | # Styled slider class 101 | # -------------------------------------------------------------------------------- 102 | 103 | class Slider extends SliderComponent 104 | 105 | constructor: (config) -> 106 | super config 107 | 108 | # Basics 109 | @x = margin 110 | @width = Screen.width - margin * 2 111 | @height = 4 112 | @fill.backgroundColor = colors.primary 113 | @backgroundColor = colors.secondary 114 | @value = config.value 115 | @unitName = config.unit or '' 116 | 117 | @baseStyle = 118 | fontFamily: "Roboto" 119 | fontWeight: 500 120 | fontSize: "26px" 121 | 122 | # Knob 123 | @knobSize = 24 124 | @knob.backgroundColor = colors.primary 125 | @knob.borderRadius = 30 126 | @knob.shadowColor = colors.background 127 | @knob.shadowBlur = 0 128 | @knob.shadowSpread = 6 129 | 130 | # Name label 131 | @txtLabel = new Layer 132 | parent: @ 133 | name: "label" 134 | y: -50 135 | backgroundColor: "" 136 | html: config.label 137 | color: colors.primary 138 | style: @baseStyle 139 | 140 | @txtLabel.style.fontStyle = "italic" 141 | 142 | # Value label 143 | @txtValue = new Layer 144 | parent: @ 145 | name: "value" 146 | maxX: @width 147 | y: -50 148 | backgroundColor: "" 149 | html: Math.round(@value) + ' ' + @unitName 150 | color: config.valueColor or colors.orange 151 | style: @baseStyle 152 | 153 | @txtValue.style.textAlign = "right" 154 | 155 | # Events 156 | 157 | @onValueChange -> 158 | @txtValue.html = Math.round(@value) + ' ' + @unitName 159 | 160 | @onTouchStart -> 161 | @knob.animate 162 | properties: 163 | scale: 1.4 164 | 165 | @onTouchEnd -> 166 | runAnim() 167 | @knob.animate 168 | properties: 169 | scale: 1 170 | 171 | 172 | 173 | # Instantiate sliders 174 | # -------------------------------------------------------------------------------- 175 | 176 | friction = new Slider 177 | label: "Friction" 178 | maxY: Screen.height - margin * 1.5 179 | min: 0 180 | max: 50 181 | value: 18 182 | 183 | spring = new Slider 184 | label: "Spring" 185 | y: friction.y - margin * 2 186 | min: 0 187 | max: 200 188 | value: 150 189 | 190 | speed = new Slider 191 | label: "Time" 192 | unit: "ms" 193 | valueColor: colors.blue 194 | y: spring.y - margin * 2 195 | min: 0 196 | max: 1000 197 | value: 200 198 | 199 | 200 | -------------------------------------------------------------------------------- /hook-example-spring.framer/framer/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "propertyPanelToggleStates" : { 3 | 4 | }, 5 | "deviceOrientation" : 0, 6 | "sharedPrototype" : 0, 7 | "contentScale" : 1, 8 | "deviceType" : "apple-iphone-6s-silver", 9 | "selectedHand" : "", 10 | "updateDelay" : 0.3, 11 | "deviceScale" : "fit", 12 | "foldedCodeRanges" : [ 13 | 14 | ], 15 | "orientation" : 0, 16 | "fullScreen" : false 17 | } -------------------------------------------------------------------------------- /hook-example-spring.framer/framer/framer.generated.js: -------------------------------------------------------------------------------- 1 | // This is autogenerated by Framer 2 | 3 | 4 | if (!window.Framer && window._bridge) {window._bridge('runtime.error', {message:'[framer.js] Framer library missing or corrupt. Select File → Update Framer Library.'})} 5 | if (DeviceComponent) {DeviceComponent.Devices["iphone-6-silver"].deviceImageJP2 = false}; 6 | if (window.Framer) {window.Framer.Defaults.DeviceView = {"deviceScale":"fit","selectedHand":"","deviceType":"apple-iphone-6s-silver","contentScale":1,"orientation":0}; 7 | } 8 | if (window.Framer) {window.Framer.Defaults.DeviceComponent = {"deviceScale":"fit","selectedHand":"","deviceType":"apple-iphone-6s-silver","contentScale":1,"orientation":0}; 9 | } 10 | window.FramerStudioInfo = {"deviceImagesUrl":"\/_server\/resources\/DeviceImages","documentTitle":"hook-example-spring.framer"}; 11 | 12 | Framer.Device = new Framer.DeviceView(); 13 | Framer.Device.setupContext(); -------------------------------------------------------------------------------- /hook-example-spring.framer/framer/framer.init.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | function isFileLoadingAllowed() { 4 | return (window.location.protocol.indexOf("file") == -1) 5 | } 6 | 7 | function isHomeScreened() { 8 | return ("standalone" in window.navigator) && window.navigator.standalone == true 9 | } 10 | 11 | function isCompatibleBrowser() { 12 | return Utils.isWebKit() 13 | } 14 | 15 | var alertNode; 16 | 17 | function dismissAlert() { 18 | alertNode.parentElement.removeChild(alertNode) 19 | loadProject() 20 | } 21 | 22 | function showAlert(html) { 23 | 24 | alertNode = document.createElement("div") 25 | 26 | alertNode.classList.add("framerAlertBackground") 27 | alertNode.innerHTML = html 28 | 29 | document.addEventListener("DOMContentLoaded", function(event) { 30 | document.body.appendChild(alertNode) 31 | }) 32 | 33 | window.dismissAlert = dismissAlert; 34 | } 35 | 36 | function showBrowserAlert() { 37 | var html = "" 38 | html += "
" 39 | html += "Error: Not A WebKit Browser" 40 | html += "Your browser is not supported.
Please use Safari or Chrome.
" 41 | html += "Try anyway" 42 | html += "
" 43 | 44 | showAlert(html) 45 | } 46 | 47 | function showFileLoadingAlert() { 48 | var html = "" 49 | html += "
" 50 | html += "Error: Local File Restrictions" 51 | html += "Preview this prototype with Framer Mirror or learn more about " 52 | html += "file restrictions.
" 53 | html += "Try anyway" 54 | html += "
" 55 | 56 | showAlert(html) 57 | } 58 | 59 | function loadProject() { 60 | CoffeeScript.load("app.coffee") 61 | } 62 | 63 | function setDefaultPageTitle() { 64 | // If no title was set we set it to the project folder name so 65 | // you get a nice name on iOS if you bookmark to desktop. 66 | document.addEventListener("DOMContentLoaded", function() { 67 | if (document.title == "") { 68 | if (window.FramerStudioInfo && window.FramerStudioInfo.documentTitle) { 69 | document.title = window.FramerStudioInfo.documentTitle 70 | } else { 71 | document.title = window.location.pathname.replace(/\//g, "") 72 | } 73 | } 74 | }) 75 | } 76 | 77 | function init() { 78 | 79 | if (Utils.isFramerStudio()) { 80 | return 81 | } 82 | 83 | setDefaultPageTitle() 84 | 85 | if (!isCompatibleBrowser()) { 86 | return showBrowserAlert() 87 | } 88 | 89 | if (!isFileLoadingAllowed()) { 90 | return showFileLoadingAlert() 91 | } 92 | 93 | loadProject() 94 | 95 | } 96 | 97 | init() 98 | 99 | })() 100 | -------------------------------------------------------------------------------- /hook-example-spring.framer/framer/framer.modules.js: -------------------------------------------------------------------------------- 1 | require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o this.length) { 146 | return false; 147 | } else { 148 | return this.indexOf(search, start) !== -1; 149 | } 150 | }; 151 | } 152 | 153 | Layer.prototype.hook = function(config) { 154 | var base, base1, f, name; 155 | if (!(config.property && config.type && (config.to || config.targetProperty))) { 156 | throw new Error('layer.hook() needs a property, a hook type and either a target object or target property to work'); 157 | } 158 | if (this.hooks == null) { 159 | this.hooks = { 160 | hooks: [], 161 | velocities: {}, 162 | defs: { 163 | zoom: 100, 164 | getDrag: (function(_this) { 165 | return function(velocity, drag, zoom) { 166 | velocity /= zoom; 167 | drag = -(drag / 10) * velocity * velocity * velocity / Math.abs(velocity); 168 | if (_.isNaN(drag)) { 169 | return 0; 170 | } else { 171 | return drag; 172 | } 173 | }; 174 | })(this), 175 | getGravity: (function(_this) { 176 | return function(strength, distance, zoom) { 177 | var dist; 178 | dist = Math.max(1, distance / zoom); 179 | return strength * zoom / (dist * dist); 180 | }; 181 | })(this) 182 | } 183 | }; 184 | } 185 | if (config.zoom) { 186 | this.hooks.zoom = config.zoom; 187 | } 188 | f = Utils.parseFunction(config.type); 189 | config.type = f.name; 190 | config.strength = f.args[0]; 191 | config.friction = f.args[1] || 0; 192 | if (config.targetProperty == null) { 193 | config.targetProperty = config.property; 194 | } 195 | if (config.to == null) { 196 | config.to = this; 197 | } 198 | if (config.property.toLowerCase().includes('pos')) { 199 | config.prop = 'pos'; 200 | if (config.property.toLowerCase().includes('mid')) { 201 | config.thisX = 'midX'; 202 | config.thisY = 'midY'; 203 | } else if (config.property.toLowerCase().includes('max')) { 204 | config.thisX = 'maxX'; 205 | config.thisY = 'maxY'; 206 | } else { 207 | config.thisX = 'x'; 208 | config.thisY = 'y'; 209 | } 210 | if (config.targetProperty.toLowerCase().includes('mid')) { 211 | config.toX = 'midX'; 212 | config.toY = 'midY'; 213 | } else if (config.targetProperty.toLowerCase().includes('max')) { 214 | config.toX = 'maxX'; 215 | config.toY = 'maxY'; 216 | } else { 217 | config.toX = 'x'; 218 | config.toY = 'y'; 219 | } 220 | } else { 221 | config.prop = config.property; 222 | } 223 | this.hooks.hooks.push(config); 224 | if ((base = this.hooks.velocities)[name = config.prop] == null) { 225 | base[name] = config.prop === 'pos' ? { 226 | x: 0, 227 | y: 0 228 | } : 0; 229 | } 230 | return (base1 = this.hooks).emitter != null ? base1.emitter : base1.emitter = Framer.Loop.on('render', this.hookLoop, this); 231 | }; 232 | 233 | Layer.prototype.unHook = function(property, object) { 234 | var prop, remaining; 235 | if (!this.hooks) { 236 | return; 237 | } 238 | prop = property.toLowerCase().includes('pos') ? 'pos' : property; 239 | this.hooks.hooks = this.hooks.hooks.filter(function(hook) { 240 | return hook.to !== object || hook.property !== property; 241 | }); 242 | if (this.hooks.hooks.length === 0) { 243 | delete this.hooks; 244 | Framer.Loop.removeListener('render', this.hookLoop); 245 | return; 246 | } 247 | remaining = this.hooks.hooks.filter(function(hook) { 248 | return prop === hook.prop; 249 | }); 250 | if (remaining.length === 0) { 251 | return delete this.hooks.velocities[prop]; 252 | } 253 | }; 254 | 255 | Layer.prototype.hookLoop = function(delta) { 256 | var acceleration, damper, drag, force, gravity, hook, i, len, name, prop, ref, ref1, target, vLength, vector, velocity; 257 | if (this.hooks) { 258 | acceleration = {}; 259 | drag = {}; 260 | ref = this.hooks.hooks; 261 | for (i = 0, len = ref.length; i < len; i++) { 262 | hook = ref[i]; 263 | if (hook.prop === 'pos') { 264 | if (acceleration.pos == null) { 265 | acceleration.pos = { 266 | x: 0, 267 | y: 0 268 | }; 269 | } 270 | target = { 271 | x: hook.to[hook.toX], 272 | y: hook.to[hook.toY] 273 | }; 274 | if (hook.modulator) { 275 | target = hook.modulator(target); 276 | } 277 | vector = { 278 | x: target.x - this[hook.thisX], 279 | y: target.y - this[hook.thisY] 280 | }; 281 | vLength = Math.sqrt((vector.x * vector.x) + (vector.y * vector.y)); 282 | if (hook.type === 'spring') { 283 | damper = { 284 | x: -hook.friction * this.hooks.velocities.pos.x, 285 | y: -hook.friction * this.hooks.velocities.pos.y 286 | }; 287 | vector.x *= hook.strength; 288 | vector.y *= hook.strength; 289 | acceleration.pos.x += (vector.x + damper.x) * delta; 290 | acceleration.pos.y += (vector.y + damper.y) * delta; 291 | } else if (hook.type === 'gravity') { 292 | drag.pos = hook.friction; 293 | gravity = this.hooks.defs.getGravity(hook.strength, vLength, this.hooks.defs.zoom); 294 | vector.x *= gravity / vLength; 295 | vector.y *= gravity / vLength; 296 | acceleration.pos.x += vector.x * delta; 297 | acceleration.pos.y += vector.y * delta; 298 | } 299 | } else { 300 | if (acceleration[name = hook.prop] == null) { 301 | acceleration[name] = 0; 302 | } 303 | target = hook.to[hook.targetProperty]; 304 | if (hook.modulator) { 305 | target = hook.modulator(target); 306 | } 307 | vector = target - this[hook.prop]; 308 | if (hook.type === 'spring') { 309 | force = vector * hook.strength; 310 | damper = -hook.friction * this.hooks.velocities[hook.prop]; 311 | acceleration[hook.prop] += (force + damper) * delta; 312 | } else if (hook.type === 'gravity') { 313 | drag[hook.prop] = hook.friction; 314 | force = this.hooks.defs.getGravity(hook.strength, vector, this.hooks.defs.zoom); 315 | acceleration[hook.prop] += force * delta; 316 | } 317 | } 318 | } 319 | ref1 = this.hooks.velocities; 320 | for (prop in ref1) { 321 | velocity = ref1[prop]; 322 | if (prop === 'pos') { 323 | if (drag.pos) { 324 | velocity.x += this.hooks.defs.getDrag(velocity.x, drag.pos, this.hooks.defs.zoom); 325 | velocity.y += this.hooks.defs.getDrag(velocity.y, drag.pos, this.hooks.defs.zoom); 326 | } 327 | velocity.x += acceleration.pos.x; 328 | velocity.y += acceleration.pos.y; 329 | this.x += velocity.x * delta; 330 | this.y += velocity.y * delta; 331 | } else { 332 | if (drag[prop]) { 333 | this.hooks.velocities[prop] += this.hooks.defs.getDrag(this.hooks.velocities[prop], drag[prop], this.hooks.defs.zoom); 334 | } 335 | this.hooks.velocities[prop] += acceleration[prop]; 336 | this[prop] += this.hooks.velocities[prop] * delta; 337 | } 338 | } 339 | return typeof this.onHookUpdate === "function" ? this.onHookUpdate(delta) : void 0; 340 | } 341 | }; 342 | 343 | 344 | },{}]},{},[]) 345 | //# sourceMappingURL=data:application/json;charset:utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIm5vZGVfbW9kdWxlcy9icm93c2VyaWZ5L25vZGVfbW9kdWxlcy9icm93c2VyLXBhY2svX3ByZWx1ZGUuanMiLCIvVXNlcnMvc2lndXJkL1JlcG9zL2ZyYW1lci1ob29rL2hvb2stZXhhbXBsZS1zcHJpbmcuZnJhbWVyL21vZHVsZXMvSG9vay5jb2ZmZWUiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7O0FDQUE7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUE2SUEsSUFBQSxDQUFPLE1BQU0sQ0FBQyxTQUFTLENBQUMsUUFBeEI7RUFDQyxNQUFNLENBQUEsU0FBRSxDQUFBLFFBQVIsR0FBbUIsU0FBQyxNQUFELEVBQVMsS0FBVDtJQUNsQjtJQUNBLElBQWEsT0FBTyxLQUFQLEtBQWdCLFFBQTdCO01BQUEsS0FBQSxHQUFRLEVBQVI7O0lBRUEsSUFBRyxLQUFBLEdBQVEsTUFBTSxDQUFDLE1BQWYsR0FBd0IsSUFBSSxDQUFDLE1BQWhDO0FBQ0MsYUFBTyxNQURSO0tBQUEsTUFBQTtBQUdDLGFBQU8sSUFBQyxDQUFBLE9BQUQsQ0FBUyxNQUFULEVBQWlCLEtBQWpCLENBQUEsS0FBNkIsQ0FBQyxFQUh0Qzs7RUFKa0IsRUFEcEI7OztBQVlBLEtBQUssQ0FBQSxTQUFFLENBQUEsSUFBUCxHQUFjLFNBQUMsTUFBRDtBQUViLE1BQUE7RUFBQSxJQUFBLENBQUEsQ0FBMEgsTUFBTSxDQUFDLFFBQVAsSUFBb0IsTUFBTSxDQUFDLElBQTNCLElBQW9DLENBQUMsTUFBTSxDQUFDLEVBQVAsSUFBYSxNQUFNLENBQUMsY0FBckIsQ0FBOUosQ0FBQTtBQUFBLFVBQVUsSUFBQSxLQUFBLENBQU0sa0dBQU4sRUFBVjs7O0lBR0EsSUFBQyxDQUFBLFFBQ0E7TUFBQSxLQUFB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SyxDQUFDLElBQUksQ0FBQyxJQUF0RCxFQUZmOztRQUtBLFFBQVEsQ0FBQyxDQUFULElBQWMsWUFBWSxDQUFDLEdBQUcsQ0FBQztRQUMvQixRQUFRLENBQUMsQ0FBVCxJQUFjLFlBQVksQ0FBQyxHQUFHLENBQUM7UUFHL0IsSUFBQyxDQUFBLENBQUQsSUFBTSxRQUFRLENBQUMsQ0FBVCxHQUFhO1FBQ25CLElBQUMsQ0FBQSxDQUFELElBQU0sUUFBUSxDQUFDLENBQVQsR0FBYSxNQWJwQjtPQUFBLE1BQUE7UUFrQkMsSUFBRyxJQUFLLENBQUEsSUFBQSxDQUFSO1VBQ0MsSUFBQyxDQUFBLEtBQUssQ0FBQyxVQUFXLENBQUEsSUFBQSxDQUFsQixJQUEyQixJQUFDLENBQUEsS0FBSyxDQUFDLElBQUksQ0FBQyxPQUFaLENBQW9CLElBQUMsQ0FBQSxLQUFLLENBQUMsVUFBVyxDQUFBLElBQUEsQ0FBdEMsRUFBNkMsSUFBSyxDQUFBLElBQUEsQ0FBbEQsRUFBeUQsSUFBQyxDQUFBLEtBQUssQ0FBQyxJQUFJLENBQUMsSUFBckUsRUFENUI7O1FBSUEsSUFBQyxDQUFBLEtBQUssQ0FBQyxVQUFXLENBQUEsSUFBQSxDQUFsQixJQUEyQixZQUFhLENBQUEsSUFBQTtRQUd4QyxJQUFFLENBQUEsSUFBQSxDQUFGLElBQVcsSUFBQyxDQUFBLEtBQUssQ0FBQyxVQUFXLENBQUEsSUFBQSxDQUFsQixHQUEwQixNQXpCdEM7O0FBRkQ7cURBNkJBLElBQUMsQ0FBQSxhQUFjLGdCQTFHaEI7O0FBRmlCIiwiZmlsZSI6ImdlbmVyYXRlZC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzQ29udGVudCI6WyIoZnVuY3Rpb24gZSh0LG4scil7ZnVuY3Rpb24gcyhvLHUpe2lmKCFuW29dKXtpZighdFtvXSl7dmFyIGE9dHlwZW9mIHJlcXVpcmU9PVwiZnVuY3Rpb25cIiYmcmVxdWlyZTtpZighdSYmYSlyZXR1cm4gYShvLCEwKTtpZihpKXJldHVybiBpKG8sITApO3ZhciBmPW5ldyBFcnJvcihcIkNhbm5vdCBmaW5kIG1vZHVsZSAnXCIrbytcIidcIik7dGhyb3cgZi5jb2RlPVwiTU9EVUxFX05PVF9GT1VORFwiLGZ9dmFyIGw9bltvXT17ZXhwb3J0czp7fX07dFtvXVswXS5jYWxsKGwuZXhwb3J0cyxmdW5jdGlvbihlKXt2YXIgbj10W29dWzFdW2VdO3JldHVybiBzKG4/bjplKX0sbCxsLmV4cG9ydHMsZSx0LG4scil9cmV0dXJuIG5bb10uZXhwb3J0c312YXIgaT10eXBlb2YgcmVxdWlyZT09XCJmdW5jdGlvblwiJiZyZXF1aXJlO2Zvcih2YXIgbz0wO288ci5sZW5ndGg7bysrKXMocltvXSk7cmV0dXJuIHN9KSIsIiMjI1xuLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS1cbkhvb2sgbW9kdWxlIGZvciBGcmFtZXJcbi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5cbmJ5OiAgICAgIFNpZ3VyZCBNYW5uc8Ola2VyXG5naXRodWI6ICBodHRwczovL2dpdGh1Yi5jb20vc2lndG0vZnJhbWVyLWhvb2tcblxuwrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt8K3wrfCt1xuXG5cblRoZSBIb29rIG1vZHVsZSBzaW1wbHkgZXhwYW5kcyB0aGUgTGF5ZXIgcHJvdG90eXBlLCBhbmQgbGV0cyB5b3UgbWFrZSBhbnlcbm51bWVyaWMgTGF5ZXIgcHJvcGVydHkgZm9sbG93IGFub3RoZXIgcHJvcGVydHkgLSBlaXRoZXIgaXRzIG93biBvciBhbm90aGVyXG5vYmplY3QncyAtIHZpYSBhIHNwcmluZyBvciBncmF2aXR5IGF0dHJhY3Rpb24uXG5cblxuLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS1cbkV4YW1wbGU6IExheWVyZWQgYW5pbWF0aW9uIChlYXNlZCArIHNwcmluZylcbi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5cbm15TGF5ZXIgPSBuZXcgTGF5ZXJcblxuIyBNYWtlIG91ciBvd24gY3VzdG9tIHByb3BlcnR5IGZvciB0aGUgeCBwcm9wZXJ0eSB0byBmb2xsb3dcbm15TGF5ZXIuZWFzZWRYID0gMFxuXG4jIEhvb2sgeCB0byBlYXNlZFggdmlhIGEgc3ByaW5nXG5teUxheWVyLmhvb2tcblx0cHJvcGVydHk6IFwieFwiXG5cdHRhcmdldFByb3BlcnR5OiBcImVhc2VkWFwiXG5cdHR5cGU6IFwic3ByaW5nKDE1MCwgMTUpXCJcblxuIyBBbmltYXRlIGVhc2VkWFxubXlMYXllci5hbmltYXRlXG5cdHByb3BlcnRpZXM6XG5cdFx0ZWFzZWRYOiAyMDBcblx0dGltZTogMC4xNVxuXHRjdXJ2ZTogXCJjdWJpYy1iZXppZXIoMC4yLCAwLCAwLjQsIDEpXCJcblxuTk9URTogXG5UbyBhdHRhY2ggYm90aCB0aGUgeCBhbmQgeSBwb3NpdGlvbiwgdXNlIFwicG9zXCIsIFwibWlkUG9zXCIgb3IgXCJtYXhQb3NcIiBhcyB0aGVcbnByb3BlcnR5L3RhcmdldFByb3BlcnR5LlxuXG5cbi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5FeGFtcGxlOiBIb29raW5nIHByb3BlcnR5IHRvIGFub3RoZXIgbGF5ZXJcbi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5cbnRhcmdldCA9IG5ldyBMYXllclxuaG9va2VkID0gbmV3IExheWVyXG5cbmhvb2tlZC5ob29rXG5cdHByb3BlcnR5OiBcInNjYWxlXCJcblx0dG86IHRhcmdldFxuXHR0eXBlOiBcInNwcmluZygxNTAsIDE1KVwiXG5cblRoZSBcImhvb2tlZFwiIGxheWVyJ3Mgc2NhbGUgd2lsbCBub3cgY29udGludW91c2x5IGZvbGxvdyB0aGUgdGFyZ2V0IGxheWVyJ3Mgc2NhbGVcbndpdGggYSBzcHJpbmcgYW5pbWF0aW9uLlxuXG5cbi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5sYXllci5ob29rKG9wdGlvbnMpXG4tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLVxuXG5PcHRpb25zIGFyZSBwYXNzZWQgYXMgYSBzaW5nbGUgb2JqZWN0LCBsaWtlIHlvdSB3b3VsZCBmb3IgYSBuZXcgTGF5ZXIuXG5UaGUgb3B0aW9ucyBvYmplY3QgdGFrZXMgdGhlIGZvbGxvd2luZyBwcm9wZXJ0aWVzOlxuXG5cbnByb3BlcnR5IFtTdHJpbmddXG4tLS0tLS0tLS0tLS0tLS0tLVxuVGhlIHByb3BlcnR5IHlvdSdkIGxpa2UgdG8gaG9vayBvbnRvIGFub3RoZXIgb2JqZWN0J3MgcHJvcGVydHlcblxuXG50eXBlIFtTdHJpbmddXG4tLS0tLS0tLS0tLS0tXG5FaXRoZXIgXCJzcHJpbmcoc3RyZW5ndGgsIGZyaWN0aW9uKVwiIG9yIFwiZ3Jhdml0eShzdHJlbmd0aCwgZHJhZylcIi4gT25seSB0aGUgbGFzdFxuc3BlY2lmaWVkIGRyYWcgdmFsdWUgaXMgdXNlZCBmb3IgZWFjaCBwcm9wZXJ0eSwgc2luY2UgaXQgaXMgb25seSBhcHBsaWVkIHRvXG5lYWNoIHByb3BlcnR5IG9uY2UgKGFuZCBvbmx5IGlmIGl0IGhhcyBhIGdyYXZpdHkgaG9vayBhcHBsaWVkIHRvIGl0LilcblxuXG50byBbT2JqZWN0XSAoT3B0aW9uYWwpXG4tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5UaGUgb2JqZWN0IHRvIGF0dGFjaCBpdCB0by4gRGVmYXVsdHMgdG8gaXRzZWxmLlxuXG5cbnRhcmdldFByb3BlcnR5IFtTdHJpbmddIChPcHRpb25hbClcbi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS1cblNwZWNpZnkgdGhlIHRhcmdldCBvYmplY3QncyBwcm9wZXJ0eSB0byBmb2xsb3csIGlmIHlvdSBkb24ndCB3YW50IHRvIGZvbGxvd1xudGhlIHNhbWUgcHJvcGVydHkgdGhhdCB0aGUgaG9vayBpcyBhcHBsaWVkIHRvLlxuXG5cbm1vZHVsYXRvciBbRnVuY3Rpb25dIChPcHRpb25hbClcbi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS1cblRoZSBtb2R1bGF0b3IgZnVuY3Rpb24gcmVjZWl2ZXMgdGhlIHRhcmdldCBwcm9wZXJ0eSdzIHZhbHVlLCBhbmQgbGV0cyB5b3Vcbm1vZGlmeSBpdCBiZWZvcmUgaXQgaXMgZmVkIGludG8gdGhlIHBoeXNpY3MgY2FsY3VsYXRpb25zLiBVc2VmdWwgZm9yIGFueXRoaW5nXG5mcm9tIHN0YW5kYXJkIFV0aWxzLm1vZHVsYXRlKCkgdHlwZSBzdHVmZiB0byBzbmFwcGluZyBhbmQgY29uZGl0aW9uYWwgdmFsdWVzLlxuXG5cbnpvb20gW051bWJlcl0gKE9wdGlvbmFsKVxuLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5UaGlzIGZhY3RvciBkZWZpbmVzIHRoZSBkaXN0YW5jZSB0aGF0IDFweCByZXByZXNlbnRzIGluIHJlZ2FyZHMgdG8gZ3Jhdml0eSBhbmRcbmRyYWcgY2FsY3VsYXRpb25zLiBPbmx5IG9uZSB2YWx1ZSBpcyBzdG9yZWQgcGVyIGxheWVyLCBzbyBzcGVjaWZ5aW5nIGl0XG5vdmVyd3JpdGVzIGl0cyBleGlzdGluZyB2YWx1ZS4gRGVmYXVsdCBpcyAxMDAuXG5cblxuLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS1cbmxheWVyLnVuSG9vayhwcm9wZXJ0eSwgb2JqZWN0KVxuLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS1cblxuVGhpcyByZW1vdmVzIGFsbCBob29rcyBmb3IgYSBnaXZlbiBwcm9wZXJ0eSBhbmQgdGFyZ2V0IG9iamVjdC4gRXhhbXBsZTpcblxuIyBIb29rIGl0XG5sYXllci5ob29rXG5cdHByb3BlcnR5OiBcInhcIlxuXHR0bzogXCJvdGhlcmxheWVyXCJcblx0dGFyZ2V0UHJvcGVydHk6IFwieVwiXG5cdHR5cGU6IFwic3ByaW5nKDIwMCwyMClcIlxuXG4jIFVuaG9vayBpdFxubGF5ZXIudW5Ib29rIFwieFwiLCBvdGhlcmxheWVyXG5cblxuLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS1cbmxheWVyLm9uSG9va1VwZGF0ZShkZWx0YSlcbi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5cbkFmdGVyIGEgbGF5ZXIgaXMgZG9uZSBhcHBseWluZyBhY2NlbGVyYXRpb25zIHRvIGl0cyBob29rZWQgcHJvcGVydGllcywgaXQgY2FsbHNcbm9uSG9va1VwZGF0ZSgpIGF0IHRoZSBlbmQgb2YgZWFjaCBmcmFtZSwgaWYgaXQgaXMgZGVmaW5lZC4gVGhpcyBpcyBhbiBlYXN5IHdheVxudG8gYW5pbWF0ZSBvciB0cmlnZ2VyIG90aGVyIHN0dWZmLCBwZXJoYXBzIGJhc2VkIG9uIHlvdXIgbGF5ZXIncyB1cGRhdGVkXG5wcm9wZXJ0aWVzIG9yIHZlbG9jaXRpZXMuXG5cblRoZSBkZWx0YSB2YWx1ZSBmcm9tIHRoZSBGcmFtZXIgbG9vcCBpcyBwYXNzZWQgb24gdG8gb25Ib29rVXBkYXRlKCkgYXMgd2VsbCxcbndoaWNoIGlzIHRoZSB0aW1lIGluIHNlY29uZHMgc2luY2UgdGhlIGxhc3QgYW5pbWF0aW9uIGZyYW1lLlxuXG5Ob3RlIHRoYXQgaWYgeW91IHVuaG9vayBhbGwgeW91ciBob29rcywgb25Ib29rVXBkYXRlKCkgd2lsbCBvZiBjb3Vyc2Ugbm8gbG9uZ2VyXG5iZSBjYWxsZWQgZm9yIHRoYXQgbGF5ZXIuXG5cbiMjI1xuXG5cbiMgU2luY2Ugb2xkZXIgdmVyc2lvbnMgb2YgU2FmYXJpIHNlZW0gdG8gYmUgbWlzc2luZyBTdHJpbmcucHJvdG90eXBlLmluY2x1ZGVzKClcblxudW5sZXNzIFN0cmluZy5wcm90b3R5cGUuaW5jbHVkZXNcblx0U3RyaW5nOjppbmNsdWRlcyA9IChzZWFyY2gsIHN0YXJ0KSAtPlxuXHRcdCd1c2Ugc3RyaWN0J1xuXHRcdHN0YXJ0ID0gMCBpZiB0eXBlb2Ygc3RhcnQgaXMgJ251bWJlcidcblxuXHRcdGlmIHN0YXJ0ICsgc2VhcmNoLmxlbmd0aCA+IHRoaXMubGVuZ3RoXG5cdFx0XHRyZXR1cm4gZmFsc2U7XG5cdFx0ZWxzZVxuXHRcdFx0cmV0dXJuIEBpbmRleE9mKHNlYXJjaCwgc3RhcnQpIGlzbnQgLTFcblxuIyBFeHBhbmQgbGF5ZXJcblxuTGF5ZXI6Omhvb2sgPSAoY29uZmlnKSAtPlxuXG5cdHRocm93IG5ldyBFcnJvciAnbGF5ZXIuaG9vaygpIG5lZWRzIGEgcHJvcGVydHksIGEgaG9vayB0eXBlIGFuZCBlaXRoZXIgYSB0YXJnZXQgb2JqZWN0IG9yIHRhcmdldCBwcm9wZXJ0eSB0byB3b3JrJyB1bmxlc3MgY29uZmlnLnByb3BlcnR5IGFuZCBjb25maWcudHlwZSBhbmQgKGNvbmZpZy50byBvciBjb25maWcudGFyZ2V0UHJvcGVydHkpXG5cblx0IyBTaW5nbGUgYXJyYXkgZm9yIGFsbCBob29rcywgYXMgb3Bwb3NlZCB0byBuZXN0ZWQgYXJyYXlzIHBlciBwcm9wZXJ0eSwgYmVjYXVzZSBwZXJmb3JtYW5jZVxuXHRAaG9va3MgPz1cblx0XHRob29rczogW11cblx0XHR2ZWxvY2l0aWVzOiB7fVxuXHRcdGRlZnM6XG5cdFx0XHR6b29tOiAxMDBcblx0XHRcdGdldERyYWc6ICh2ZWxvY2l0eSwgZHJhZywgem9vbSkgPT5cblx0XHRcdFx0dmVsb2NpdHkgLz0gem9vbVxuXHRcdFx0XHQjIERpdmlkaW5nIGJ5IDEwIGlzIHVuc2NpZW50aWZpYywgYnV0IGl0IG1lYW5zIGEgdmFsdWUgb2YgMiBlcXVhbHMgcm91Z2hseSBhIDEwMGcgYmFsbCB3aXRoIDE1Y20gcmFkaXVzIGluIGFpclxuXHRcdFx0XHRkcmFnID0gLShkcmFnIC8gMTApICogdmVsb2NpdHkgKiB2ZWxvY2l0eSAqIHZlbG9jaXR5IC8gTWF0aC5hYnModmVsb2NpdHkpXG5cdFx0XHRcdGlmIF8uaXNOYU4oZHJhZykgdGhlbiByZXR1cm4gMCBlbHNlIHJldHVybiBkcmFnXG5cdFx0XHRnZXRHcmF2aXR5OiAoc3RyZW5ndGgsIGRpc3RhbmNlLCB6b29tKSA9PlxuXHRcdFx0XHRkaXN0ID0gTWF0aC5tYXgoMSwgZGlzdGFuY2UgLyB6b29tKVxuXHRcdFx0XHRyZXR1cm4gc3RyZW5ndGggKiB6b29tIC8gKGRpc3QgKiBkaXN0KVxuXG5cdCMgVXBkYXRlIHRoZSB6b29tIHZhbHVlIGlmIGdpdmVuXG5cdEBob29rcy56b29tID0gY29uZmlnLnpvb20gaWYgY29uZmlnLnpvb21cblxuXHQjIFBhcnNlIHBoeXNpY3MgY29uZmlnIHN0cmluZ1xuXHRmID0gVXRpbHMucGFyc2VGdW5jdGlvbiBjb25maWcudHlwZVxuXHRjb25maWcudHlwZSA9IGYubmFtZVxuXHRjb25maWcuc3RyZW5ndGggPSBmLmFyZ3NbMF1cblx0Y29uZmlnLmZyaWN0aW9uID0gZi5hcmdzWzFdIG9yIDBcblxuXHQjIERlZmF1bHQgdG8gc2FtZSB0YXJnZXRQcm9wZXJ0eSBvbiBzYW1lIG9iamVjdCAoaG9wZWZ1bGx5IHlvdSd2ZSBzZXQgYXQgbGVhc3Qgb25lIG9mIHRoZXNlIHRvIHNvbWV0aGluZyBlbHNlKVxuXHRjb25maWcudGFyZ2V0UHJvcGVydHkgPz0gY29uZmlnLnByb3BlcnR5XG5cdGNvbmZpZy50byA/PSBAXG5cblx0IyBBbGwgcG9zaXRpb24gYWNjZWxlcmF0aW9ucyBhcmUgYWRkZWQgdG8gYSBzaW5nbGUgJ3BvcycgdmVsb2NpdHkuIFN0b3JlIGFjdHVhbCBwcm9wZXJ0aWVzIHNvIHdlIGRvbid0IGhhdmUgdG8gZG8gaXQgYWdhaW4gZXZlcnkgZnJhbWVcblxuXHRpZiBjb25maWcucHJvcGVydHkudG9Mb3dlckNhc2UoKS5pbmNsdWRlcyAncG9zJ1xuXHRcdGNvbmZpZy5wcm9wID0gJ3Bvcydcblx0XHRcblx0XHRpZiBjb25maWcucHJvcGVydHkudG9Mb3dlckNhc2UoKS5pbmNsdWRlcyAnbWlkJ1xuXHRcdFx0Y29uZmlnLnRoaXNYID0gJ21pZFgnXG5cdFx0XHRjb25maWcudGhpc1kgPSAnbWlkWSdcblx0XHRcblx0XHRlbHNlIGlmIGNvbmZpZy5wcm9wZXJ0eS50b0xvd2VyQ2FzZSgpLmluY2x1ZGVzICdtYXgnXG5cdFx0XHRjb25maWcudGhpc1ggPSAnbWF4WCdcblx0XHRcdGNvbmZpZy50aGlzWSA9ICdtYXhZJ1xuXHRcdFxuXHRcdGVsc2Vcblx0XHRcdGNvbmZpZy50aGlzWCA9ICd4J1xuXHRcdFx0Y29uZmlnLnRoaXNZID0gJ3knXG5cdFx0XG5cdFx0aWYgY29uZmlnLnRhcmdldFByb3BlcnR5LnRvTG93ZXJDYXNlKCkuaW5jbHVkZXMgJ21pZCdcblx0XHRcdGNvbmZpZy50b1ggPSAnbWlkWCdcblx0XHRcdGNvbmZpZy50b1kgPSAnbWlkWSdcblx0XHRcblx0XHRlbHNlIGlmIGNvbmZpZy50YXJnZXRQcm9wZXJ0eS50b0xvd2VyQ2FzZSgpLmluY2x1ZGVzICdtYXgnXG5cdFx0XHRjb25maWcudG9YID0gJ21heFgnXG5cdFx0XHRjb25maWcudG9ZID0gJ21heFknXHRcdFxuXHRcdGVsc2Vcblx0XHRcdGNvbmZpZy50b1ggPSAneCdcblx0XHRcdGNvbmZpZy50b1kgPSAneSdcblx0XHRcblx0ZWxzZVxuXHRcdGNvbmZpZy5wcm9wID0gY29uZmlnLnByb3BlcnR5XG5cblx0IyBTYXZlIGhvb2sgdG8gQGhvb2tzIGFycmF5XHRcblx0QGhvb2tzLmhvb2tzLnB1c2goY29uZmlnKVxuXG5cdCMgQ3JlYXRlIHZlbG9jaXR5IHByb3BlcnR5IGlmIG5lY2Vzc2FyeVxuXHRAaG9va3MudmVsb2NpdGllc1tjb25maWcucHJvcF0gPz0gaWYgY29uZmlnLnByb3AgaXMgJ3BvcycgdGhlbiB7IHg6IDAsIHk6IDAgfSBlbHNlIDBcblxuXHQjIFVzZSBGcmFtZXIncyBhbmltYXRpb24gbG9vcCwgc2xpZ2h0bHkgbW9yZSByb2J1c3QgdGhhbiByZXF1ZXN0QW5pbWF0aW9uRnJhbWUgZGlyZWN0bHlcblx0IyBTYXZlIHRoZSByZXR1cm5lZCBBbmltYXRpb25Mb29wIHJlZmVyZW5jZSB0byBtYWtlIHN1cmUgQGhvb2tMb29wIGlzbid0IGFkZGVkIG11bHRpcGxlIHRpbWVzIHBlciBsYXllclxuXHRAaG9va3MuZW1pdHRlciA/PSBGcmFtZXIuTG9vcC5vbigncmVuZGVyJywgQGhvb2tMb29wLCB0aGlzKVxuXG5MYXllcjo6dW5Ib29rID0gKHByb3BlcnR5LCBvYmplY3QpIC0+XG5cdFxuXHRyZXR1cm4gdW5sZXNzIEBob29rc1xuXG5cdHByb3AgPSBpZiBwcm9wZXJ0eS50b0xvd2VyQ2FzZSgpLmluY2x1ZGVzICdwb3MnIHRoZW4gJ3BvcycgZWxzZSBwcm9wZXJ0eVxuXG5cdCMgUmVtb3ZlIGFsbCBtYXRjaGVzXG5cdEBob29rcy5ob29rcyA9IEBob29rcy5ob29rcy5maWx0ZXIgKGhvb2spIC0+XG5cdFx0aG9vay50byBpc250IG9iamVjdCBvciBob29rLnByb3BlcnR5IGlzbnQgcHJvcGVydHlcblxuXHQjIElmIHRoZXJlIGFyZSBubyBob29rcyBsZWZ0LCBzaHV0IGl0IGRvd25cblx0aWYgQGhvb2tzLmhvb2tzLmxlbmd0aCBpcyAwXG5cdFx0ZGVsZXRlIEBob29rc1xuXHRcdEZyYW1lci5Mb29wLnJlbW92ZUxpc3RlbmVyICdyZW5kZXInLCBAaG9va0xvb3Bcblx0XHRyZXR1cm5cblxuXHQjIFN0aWxsIGhlcmU/IENoZWNrIGlmIHRoZXJlIGFyZSBhbnkgcmVtYWluaW5nIGhvb2tzIGFmZmVjdGluZyBzYW1lIHZlbG9jaXR5XG5cdHJlbWFpbmluZyA9IEBob29rcy5ob29rcy5maWx0ZXIgKGhvb2spIC0+XG5cdFx0cHJvcCBpcyBob29rLnByb3Bcblx0XHRcblx0IyBJZiBub3QsIGRlbGV0ZSB2ZWxvY2l0eSAob3RoZXJ3aXNlIGl0IHdvbid0IGJlIHJlc2V0IGlmIHlvdSBtYWtlIG5ldyBob29rIGZvciBzYW1lIHByb3BlcnR5KVxuXHRkZWxldGUgQGhvb2tzLnZlbG9jaXRpZXNbcHJvcF0gaWYgcmVtYWluaW5nLmxlbmd0aCBpcyAwXG5cbkxheWVyOjpob29rTG9vcCA9IChkZWx0YSkgLT5cblxuXHRpZiBAaG9va3NcblxuXHRcdCMgTXVsdGlwbGUgaG9va3MgY2FuIGFmZmVjdCB0aGUgc2FtZSBwcm9wZXJ0eS4gQWRkIGFjY2VsZXJhdGlvbnMgdG8gdGVtcG9yYXJ5IG9iamVjdCBzbyB0aGUgcHJvcGVydHkncyB2ZWxvY2l0eSBpcyB0aGUgc2FtZSBmb3IgYWxsIGNhbGN1bGF0aW9ucyB3aXRoaW4gdGhlIHNhbWUgYW5pbWF0aW9uIGZyYW1lXG5cdFx0YWNjZWxlcmF0aW9uID0ge31cblx0XHRcblx0XHQjIFNhdmUgZHJhZyBmb3IgZWFjaCBwcm9wZXJ0eSB0byB0aGlzIG9iamVjdCwgc2luY2Ugb25seSBtb3N0IHJlY2VudGx5IHNwZWNpZmllZCB2YWx1ZSBpcyB1c2VkIGZvciBlYWNoIHByb3BlcnR5XG5cdFx0ZHJhZyA9IHt9XG5cdFx0XG5cdFx0IyBBZGQgYWNjZWxlcmF0aW9uc1xuXHRcdGZvciBob29rIGluIEBob29rcy5ob29rc1xuXHRcdFxuXHRcdFx0aWYgaG9vay5wcm9wIGlzICdwb3MnXG5cblx0XHRcdFx0YWNjZWxlcmF0aW9uLnBvcyA/PSB7IHg6IDAsIHk6IDAgfVxuXG5cdFx0XHRcdHRhcmdldCA9IHsgeDogaG9vay50b1tob29rLnRvWF0sIHk6IGhvb2sudG9baG9vay50b1ldIH1cblxuXHRcdFx0XHR0YXJnZXQgPSBob29rLm1vZHVsYXRvcih0YXJnZXQpIGlmIGhvb2subW9kdWxhdG9yXG5cblx0XHRcdFx0dmVjdG9yID1cblx0XHRcdFx0XHR4OiB0YXJnZXQueCAtIEBbaG9vay50aGlzWF1cblx0XHRcdFx0XHR5OiB0YXJnZXQueSAtIEBbaG9vay50aGlzWV1cblx0XHRcdFx0XG5cdFx0XHRcdHZMZW5ndGggPSBNYXRoLnNxcnQoKHZlY3Rvci54ICogdmVjdG9yLngpICsgKHZlY3Rvci55ICogdmVjdG9yLnkpKVxuXG5cdFx0XHRcdGlmIGhvb2sudHlwZSBpcyAnc3ByaW5nJ1xuXG5cdFx0XHRcdFx0ZGFtcGVyID1cblx0XHRcdFx0XHRcdHg6IC1ob29rLmZyaWN0aW9uICogQGhvb2tzLnZlbG9jaXRpZXMucG9zLnhcblx0XHRcdFx0XHRcdHk6IC1ob29rLmZyaWN0aW9uICogQGhvb2tzLnZlbG9jaXRpZXMucG9zLnlcblxuXHRcdFx0XHRcdHZlY3Rvci54ICo9IGhvb2suc3RyZW5ndGhcblx0XHRcdFx0XHR2ZWN0b3IueSAqPSBob29rLnN0cmVuZ3RoXG5cdFx0XHRcdFx0XG5cdFx0XHRcdFx0YWNjZWxlcmF0aW9uLnBvcy54ICs9ICh2ZWN0b3IueCArIGRhbXBlci54KSAqIGRlbHRhXG5cdFx0XHRcdFx0YWNjZWxlcmF0aW9uLnBvcy55ICs9ICh2ZWN0b3IueSArIGRhbXBlci55KSAqIGRlbHRhXG5cdFx0XHRcdFxuXHRcdFx0XHRlbHNlIGlmIGhvb2sudHlwZSBpcyAnZ3Jhdml0eSdcblx0XHRcdFx0XG5cdFx0XHRcdFx0ZHJhZy5wb3MgPSBob29rLmZyaWN0aW9uXG5cdFx0XHRcdFx0XG5cdFx0XHRcdFx0Z3Jhdml0eSA9IEBob29rcy5kZWZzLmdldEdyYXZpdHkoaG9vay5zdHJlbmd0aCwgdkxlbmd0aCwgQGhvb2tzLmRlZnMuem9vbSlcblxuXHRcdFx0XHRcdHZlY3Rvci54ICo9IGdyYXZpdHkgLyB2TGVuZ3RoXG5cdFx0XHRcdFx0dmVjdG9yLnkgKj0gZ3Jhdml0eSAvIHZMZW5ndGhcblx0XHRcdFx0XHRcblx0XHRcdFx0XHRhY2NlbGVyYXRpb24ucG9zLnggKz0gdmVjdG9yLnggKiBkZWx0YVxuXHRcdFx0XHRcdGFjY2VsZXJhdGlvbi5wb3MueSArPSB2ZWN0b3IueSAqIGRlbHRhXG5cdFx0XHRcdFx0XHRcdFx0XHRcblx0XHRcdGVsc2Vcblx0XHRcdFx0XG5cdFx0XHRcdGFjY2VsZXJhdGlvbltob29rLnByb3BdID89IDBcblxuXHRcdFx0XHR0YXJnZXQgPSBob29rLnRvW2hvb2sudGFyZ2V0UHJvcGVydHldXG5cblx0XHRcdFx0dGFyZ2V0ID0gaG9vay5tb2R1bGF0b3IodGFyZ2V0KSBpZiBob29rLm1vZHVsYXRvclxuXG5cdFx0XHRcdHZlY3RvciA9IHRhcmdldCAtIEBbaG9vay5wcm9wXVxuXHRcdFx0XHRcblx0XHRcdFx0aWYgaG9vay50eXBlIGlzICdzcHJpbmcnXG5cblx0XHRcdFx0XHRmb3JjZSA9IHZlY3RvciAqIGhvb2suc3RyZW5ndGhcblx0XHRcdFx0XHRkYW1wZXIgPSAtaG9vay5mcmljdGlvbiAqIEBob29rcy52ZWxvY2l0aWVzW2hvb2sucHJvcF1cblxuXHRcdFx0XHRcdGFjY2VsZXJhdGlvbltob29rLnByb3BdICs9IChmb3JjZSArIGRhbXBlcikgKiBkZWx0YVxuXG5cdFx0XHRcdFxuXHRcdFx0XHRlbHNlIGlmIGhvb2sudHlwZSBpcyAnZ3Jhdml0eSdcblx0XG5cdFx0XHRcdFx0ZHJhZ1tob29rLnByb3BdID0gaG9vay5mcmljdGlvblxuXHRcdFx0XHRcdFxuXHRcdFx0XHRcdGZvcmNlID0gQGhvb2tzLmRlZnMuZ2V0R3Jhdml0eShob29rLnN0cmVuZ3RoLCB2ZWN0b3IsIEBob29rcy5kZWZzLnpvb20pXG5cdFx0XHRcdFx0XG5cdFx0XHRcdFx0YWNjZWxlcmF0aW9uW2hvb2sucHJvcF0gKz0gZm9yY2UgKiBkZWx0YVxuXHRcdFxuXHRcdFxuXHRcdCMgQWRkIHZlbG9jaXRpZXMgdG8gcHJvcGVydGllcy4gRG9pbmcgdGhpcyBhdCB0aGUgZW5kIGluIGNhc2UgdGhlcmUgYXJlIG11bHRpcGxlIGhvb2tzIGFmZmVjdGluZyB0aGUgc2FtZSB2ZWxvY2l0eVxuXHRcdGZvciBwcm9wLCB2ZWxvY2l0eSBvZiBAaG9va3MudmVsb2NpdGllc1xuXHRcdFxuXHRcdFx0aWYgcHJvcCBpcyAncG9zJ1xuXG5cdFx0XHRcdCMgQWRkIGRyYWcsIGlmIGl0IGV4aXN0c1xuXHRcdFx0XHRpZiBkcmFnLnBvc1xuXHRcdFx0XHRcdHZlbG9jaXR5LnggKz0gQGhvb2tzLmRlZnMuZ2V0RHJhZyh2ZWxvY2l0eS54LCBkcmFnLnBvcywgQGhvb2tzLmRlZnMuem9vbSlcblx0XHRcdFx0XHR2ZWxvY2l0eS55ICs9IEBob29rcy5kZWZzLmdldERyYWcodmVsb2NpdHkueSwgZHJhZy5wb3MsIEBob29rcy5kZWZzLnpvb20pXG5cdFx0XHRcdFx0XG5cdFx0XHRcdCMgQWRkIGFjY2VsZXJhdGlvbiB0byB2ZWxvY2l0eVxuXHRcdFx0XHR2ZWxvY2l0eS54ICs9IGFjY2VsZXJhdGlvbi5wb3MueFxuXHRcdFx0XHR2ZWxvY2l0eS55ICs9IGFjY2VsZXJhdGlvbi5wb3MueVxuXHRcdFx0XHRcblx0XHRcdFx0IyBBZGQgdmVsb2NpdHkgdG8gcG9zaXRpb25cblx0XHRcdFx0QHggKz0gdmVsb2NpdHkueCAqIGRlbHRhXG5cdFx0XHRcdEB5ICs9IHZlbG9jaXR5LnkgKiBkZWx0YVxuXHRcdFx0XG5cdFx0XHRlbHNlXG5cdFx0XHRcblx0XHRcdFx0IyBBZGQgZHJhZywgaWYgaXQgZXhpc3RzXG5cdFx0XHRcdGlmIGRyYWdbcHJvcF1cblx0XHRcdFx0XHRAaG9va3MudmVsb2NpdGllc1twcm9wXSArPSBAaG9va3MuZGVmcy5nZXREcmFnKEBob29rcy52ZWxvY2l0aWVzW3Byb3BdLCBkcmFnW3Byb3BdLCBAaG9va3MuZGVmcy56b29tKVxuXHRcdFx0XHRcblx0XHRcdFx0IyBBZGQgYWNjZWxlcmF0aW9uIHRvIHZlbG9jaXR5XG5cdFx0XHRcdEBob29rcy52ZWxvY2l0aWVzW3Byb3BdICs9IGFjY2VsZXJhdGlvbltwcm9wXVxuXHRcdFx0XHRcblx0XHRcdFx0IyBBZGQgdmVsb2NpdHkgdG8gcHJvcGVydHlcblx0XHRcdFx0QFtwcm9wXSArPSBAaG9va3MudmVsb2NpdGllc1twcm9wXSAqIGRlbHRhXG5cblx0XHRAb25Ib29rVXBkYXRlPyhkZWx0YSkiXX0= 346 | -------------------------------------------------------------------------------- /hook-example-spring.framer/framer/images/cursor-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-spring.framer/framer/images/cursor-active.png -------------------------------------------------------------------------------- /hook-example-spring.framer/framer/images/cursor-active@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-spring.framer/framer/images/cursor-active@2x.png -------------------------------------------------------------------------------- /hook-example-spring.framer/framer/images/cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-spring.framer/framer/images/cursor.png -------------------------------------------------------------------------------- /hook-example-spring.framer/framer/images/cursor@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-spring.framer/framer/images/cursor@2x.png -------------------------------------------------------------------------------- /hook-example-spring.framer/framer/images/icon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-spring.framer/framer/images/icon-120.png -------------------------------------------------------------------------------- /hook-example-spring.framer/framer/images/icon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-spring.framer/framer/images/icon-152.png -------------------------------------------------------------------------------- /hook-example-spring.framer/framer/images/icon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-spring.framer/framer/images/icon-180.png -------------------------------------------------------------------------------- /hook-example-spring.framer/framer/images/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-spring.framer/framer/images/icon-192.png -------------------------------------------------------------------------------- /hook-example-spring.framer/framer/images/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-spring.framer/framer/images/icon-76.png -------------------------------------------------------------------------------- /hook-example-spring.framer/framer/manifest.txt: -------------------------------------------------------------------------------- 1 | app.coffee 2 | framer/backups/backup-2016-09-09 14.23.29.coffee 3 | framer/backups/backup-2016-09-09 14.27.29.coffee 4 | framer/coffee-script.js 5 | framer/config.json 6 | framer/framer.generated.js 7 | framer/framer.init.js 8 | framer/framer.js 9 | framer/framer.js.map 10 | framer/framer.modules.js 11 | framer/images/cursor-active.png 12 | framer/images/cursor-active@2x.png 13 | framer/images/cursor.png 14 | framer/images/cursor@2x.png 15 | framer/images/icon-120.png 16 | framer/images/icon-152.png 17 | framer/images/icon-180.png 18 | framer/images/icon-192.png 19 | framer/images/icon-76.png 20 | framer/manifest.txt 21 | framer/metadata.json 22 | framer/preview.png 23 | framer/style.css 24 | framer/version 25 | index.html 26 | modules/Hook.coffee 27 | spring-example-720.gif 28 | spring-example.gif -------------------------------------------------------------------------------- /hook-example-spring.framer/framer/metadata.json: -------------------------------------------------------------------------------- 1 | {"title":"hook-example-spring","date":"2016-09-09T12:30:12Z"} -------------------------------------------------------------------------------- /hook-example-spring.framer/framer/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-spring.framer/framer/preview.png -------------------------------------------------------------------------------- /hook-example-spring.framer/framer/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | border: none; 5 | -webkit-user-select: none; 6 | -webkit-tap-highlight-color: rgba(0,0,0,0); 7 | } 8 | 9 | body { 10 | background-color: #fff; 11 | font: 28px/1em "Helvetica"; 12 | color: gray; 13 | overflow: hidden; 14 | } 15 | 16 | a { 17 | color: gray; 18 | } 19 | 20 | body { 21 | cursor: url('images/cursor.png') 32 32, auto; 22 | cursor: -webkit-image-set( 23 | url('images/cursor.png') 1x, 24 | url('images/cursor@2x.png') 2x 25 | ) 32 32, auto; 26 | } 27 | 28 | body:active { 29 | cursor: url('images/cursor-active.png') 32 32, auto; 30 | cursor: -webkit-image-set( 31 | url('images/cursor-active.png') 1x, 32 | url('images/cursor-active@2x.png') 2x 33 | ) 32 32, auto; 34 | } 35 | 36 | .framerAlertBackground { 37 | position: absolute; top:0px; left:0px; right:0px; bottom:0px; 38 | z-index: 1000; 39 | background-color: #fff; 40 | } 41 | 42 | .framerAlert { 43 | font:400 14px/1.4 "Helvetica Neue", Helvetica, Arial, sans-serif; 44 | -webkit-font-smoothing:antialiased; 45 | color:#616367; text-align:center; 46 | position: absolute; top:40%; left:50%; width:260px; margin-left:-130px; 47 | } 48 | .framerAlert strong { font-weight:500; color:#000; margin-bottom:8px; display:block; } 49 | .framerAlert a { color:#28AFFA; } 50 | .framerAlert .btn { 51 | font-weight:500; text-decoration:none; line-height:1; 52 | display:inline-block; padding:6px 12px 7px 12px; 53 | border-radius:3px; margin-top:12px; 54 | background:#28AFFA; color:#fff; 55 | } 56 | 57 | ::-webkit-scrollbar { 58 | display: none; 59 | } -------------------------------------------------------------------------------- /hook-example-spring.framer/framer/version: -------------------------------------------------------------------------------- 1 | 4 -------------------------------------------------------------------------------- /hook-example-spring.framer/images/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-spring.framer/images/.gitkeep -------------------------------------------------------------------------------- /hook-example-spring.framer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /hook-example-spring.framer/modules/Hook.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | -------------------------------------------------------------------------------- 3 | Hook module for Framer 4 | -------------------------------------------------------------------------------- 5 | 6 | by: Sigurd Mannsåker 7 | github: https://github.com/sigtm/framer-hook 8 | 9 | ················································································ 10 | 11 | 12 | The Hook module simply expands the Layer prototype, and lets you make any 13 | numeric Layer property follow another property - either its own or another 14 | object's - via a spring or gravity attraction. 15 | 16 | 17 | -------------------------------------------------------------------------------- 18 | Example: Layered animation (eased + spring) 19 | -------------------------------------------------------------------------------- 20 | 21 | myLayer = new Layer 22 | 23 | # Make our own custom property for the x property to follow 24 | myLayer.easedX = 0 25 | 26 | # Hook x to easedX via a spring 27 | myLayer.hook 28 | property: "x" 29 | targetProperty: "easedX" 30 | type: "spring(150, 15)" 31 | 32 | # Animate easedX 33 | myLayer.animate 34 | properties: 35 | easedX: 200 36 | time: 0.15 37 | curve: "cubic-bezier(0.2, 0, 0.4, 1)" 38 | 39 | NOTE: 40 | To attach both the x and y position, use "pos", "midPos" or "maxPos" as the 41 | property/targetProperty. 42 | 43 | 44 | -------------------------------------------------------------------------------- 45 | Example: Hooking property to another layer 46 | -------------------------------------------------------------------------------- 47 | 48 | target = new Layer 49 | hooked = new Layer 50 | 51 | hooked.hook 52 | property: "scale" 53 | to: target 54 | type: "spring(150, 15)" 55 | 56 | The "hooked" layer's scale will now continuously follow the target layer's scale 57 | with a spring animation. 58 | 59 | 60 | -------------------------------------------------------------------------------- 61 | layer.hook(options) 62 | -------------------------------------------------------------------------------- 63 | 64 | Options are passed as a single object, like you would for a new Layer. 65 | The options object takes the following properties: 66 | 67 | 68 | property [String] 69 | ----------------- 70 | The property you'd like to hook onto another object's property 71 | 72 | 73 | type [String] 74 | ------------- 75 | Either "spring(strength, friction)" or "gravity(strength, drag)". Only the last 76 | specified drag value is used for each property, since it is only applied to 77 | each property once (and only if it has a gravity hook applied to it.) 78 | 79 | 80 | to [Object] (Optional) 81 | ---------------------- 82 | The object to attach it to. Defaults to itself. 83 | 84 | 85 | targetProperty [String] (Optional) 86 | ---------------------------------- 87 | Specify the target object's property to follow, if you don't want to follow 88 | the same property that the hook is applied to. 89 | 90 | 91 | modulator [Function] (Optional) 92 | ------------------------------- 93 | The modulator function receives the target property's value, and lets you 94 | modify it before it is fed into the physics calculations. Useful for anything 95 | from standard Utils.modulate() type stuff to snapping and conditional values. 96 | 97 | 98 | zoom [Number] (Optional) 99 | ------------------------ 100 | This factor defines the distance that 1px represents in regards to gravity and 101 | drag calculations. Only one value is stored per layer, so specifying it 102 | overwrites its existing value. Default is 100. 103 | 104 | 105 | -------------------------------------------------------------------------------- 106 | layer.unHook(property, object) 107 | -------------------------------------------------------------------------------- 108 | 109 | This removes all hooks for a given property and target object. Example: 110 | 111 | # Hook it 112 | layer.hook 113 | property: "x" 114 | to: "otherlayer" 115 | targetProperty: "y" 116 | type: "spring(200,20)" 117 | 118 | # Unhook it 119 | layer.unHook "x", otherlayer 120 | 121 | 122 | -------------------------------------------------------------------------------- 123 | layer.onHookUpdate(delta) 124 | -------------------------------------------------------------------------------- 125 | 126 | After a layer is done applying accelerations to its hooked properties, it calls 127 | onHookUpdate() at the end of each frame, if it is defined. This is an easy way 128 | to animate or trigger other stuff, perhaps based on your layer's updated 129 | properties or velocities. 130 | 131 | The delta value from the Framer loop is passed on to onHookUpdate() as well, 132 | which is the time in seconds since the last animation frame. 133 | 134 | Note that if you unhook all your hooks, onHookUpdate() will of course no longer 135 | be called for that layer. 136 | 137 | ### 138 | 139 | 140 | # Since older versions of Safari seem to be missing String.prototype.includes() 141 | 142 | unless String.prototype.includes 143 | String::includes = (search, start) -> 144 | 'use strict' 145 | start = 0 if typeof start is 'number' 146 | 147 | if start + search.length > this.length 148 | return false; 149 | else 150 | return @indexOf(search, start) isnt -1 151 | 152 | # Expand layer 153 | 154 | Layer::hook = (config) -> 155 | 156 | throw new Error 'layer.hook() needs a property, a hook type and either a target object or target property to work' unless config.property and config.type and (config.to or config.targetProperty) 157 | 158 | # Single array for all hooks, as opposed to nested arrays per property, because performance 159 | @hooks ?= 160 | hooks: [] 161 | velocities: {} 162 | defs: 163 | zoom: 100 164 | getDrag: (velocity, drag, zoom) => 165 | velocity /= zoom 166 | # Dividing by 10 is unscientific, but it means a value of 2 equals roughly a 100g ball with 15cm radius in air 167 | drag = -(drag / 10) * velocity * velocity * velocity / Math.abs(velocity) 168 | if _.isNaN(drag) then return 0 else return drag 169 | getGravity: (strength, distance, zoom) => 170 | dist = Math.max(1, distance / zoom) 171 | return strength * zoom / (dist * dist) 172 | 173 | # Update the zoom value if given 174 | @hooks.zoom = config.zoom if config.zoom 175 | 176 | # Parse physics config string 177 | f = Utils.parseFunction config.type 178 | config.type = f.name 179 | config.strength = f.args[0] 180 | config.friction = f.args[1] or 0 181 | 182 | # Default to same targetProperty on same object (hopefully you've set at least one of these to something else) 183 | config.targetProperty ?= config.property 184 | config.to ?= @ 185 | 186 | # All position accelerations are added to a single 'pos' velocity. Store actual properties so we don't have to do it again every frame 187 | 188 | if config.property.toLowerCase().includes 'pos' 189 | config.prop = 'pos' 190 | 191 | if config.property.toLowerCase().includes 'mid' 192 | config.thisX = 'midX' 193 | config.thisY = 'midY' 194 | 195 | else if config.property.toLowerCase().includes 'max' 196 | config.thisX = 'maxX' 197 | config.thisY = 'maxY' 198 | 199 | else 200 | config.thisX = 'x' 201 | config.thisY = 'y' 202 | 203 | if config.targetProperty.toLowerCase().includes 'mid' 204 | config.toX = 'midX' 205 | config.toY = 'midY' 206 | 207 | else if config.targetProperty.toLowerCase().includes 'max' 208 | config.toX = 'maxX' 209 | config.toY = 'maxY' 210 | else 211 | config.toX = 'x' 212 | config.toY = 'y' 213 | 214 | else 215 | config.prop = config.property 216 | 217 | # Save hook to @hooks array 218 | @hooks.hooks.push(config) 219 | 220 | # Create velocity property if necessary 221 | @hooks.velocities[config.prop] ?= if config.prop is 'pos' then { x: 0, y: 0 } else 0 222 | 223 | # Use Framer's animation loop, slightly more robust than requestAnimationFrame directly 224 | # Save the returned AnimationLoop reference to make sure @hookLoop isn't added multiple times per layer 225 | @hooks.emitter ?= Framer.Loop.on('render', @hookLoop, this) 226 | 227 | Layer::unHook = (property, object) -> 228 | 229 | return unless @hooks 230 | 231 | prop = if property.toLowerCase().includes 'pos' then 'pos' else property 232 | 233 | # Remove all matches 234 | @hooks.hooks = @hooks.hooks.filter (hook) -> 235 | hook.to isnt object or hook.property isnt property 236 | 237 | # If there are no hooks left, shut it down 238 | if @hooks.hooks.length is 0 239 | delete @hooks 240 | Framer.Loop.removeListener 'render', @hookLoop 241 | return 242 | 243 | # Still here? Check if there are any remaining hooks affecting same velocity 244 | remaining = @hooks.hooks.filter (hook) -> 245 | prop is hook.prop 246 | 247 | # If not, delete velocity (otherwise it won't be reset if you make new hook for same property) 248 | delete @hooks.velocities[prop] if remaining.length is 0 249 | 250 | Layer::hookLoop = (delta) -> 251 | 252 | if @hooks 253 | 254 | # Multiple hooks can affect the same property. Add accelerations to temporary object so the property's velocity is the same for all calculations within the same animation frame 255 | acceleration = {} 256 | 257 | # Save drag for each property to this object, since only most recently specified value is used for each property 258 | drag = {} 259 | 260 | # Add accelerations 261 | for hook in @hooks.hooks 262 | 263 | if hook.prop is 'pos' 264 | 265 | acceleration.pos ?= { x: 0, y: 0 } 266 | 267 | target = { x: hook.to[hook.toX], y: hook.to[hook.toY] } 268 | 269 | target = hook.modulator(target) if hook.modulator 270 | 271 | vector = 272 | x: target.x - @[hook.thisX] 273 | y: target.y - @[hook.thisY] 274 | 275 | vLength = Math.sqrt((vector.x * vector.x) + (vector.y * vector.y)) 276 | 277 | if hook.type is 'spring' 278 | 279 | damper = 280 | x: -hook.friction * @hooks.velocities.pos.x 281 | y: -hook.friction * @hooks.velocities.pos.y 282 | 283 | vector.x *= hook.strength 284 | vector.y *= hook.strength 285 | 286 | acceleration.pos.x += (vector.x + damper.x) * delta 287 | acceleration.pos.y += (vector.y + damper.y) * delta 288 | 289 | else if hook.type is 'gravity' 290 | 291 | drag.pos = hook.friction 292 | 293 | gravity = @hooks.defs.getGravity(hook.strength, vLength, @hooks.defs.zoom) 294 | 295 | vector.x *= gravity / vLength 296 | vector.y *= gravity / vLength 297 | 298 | acceleration.pos.x += vector.x * delta 299 | acceleration.pos.y += vector.y * delta 300 | 301 | else 302 | 303 | acceleration[hook.prop] ?= 0 304 | 305 | target = hook.to[hook.targetProperty] 306 | 307 | target = hook.modulator(target) if hook.modulator 308 | 309 | vector = target - @[hook.prop] 310 | 311 | if hook.type is 'spring' 312 | 313 | force = vector * hook.strength 314 | damper = -hook.friction * @hooks.velocities[hook.prop] 315 | 316 | acceleration[hook.prop] += (force + damper) * delta 317 | 318 | 319 | else if hook.type is 'gravity' 320 | 321 | drag[hook.prop] = hook.friction 322 | 323 | force = @hooks.defs.getGravity(hook.strength, vector, @hooks.defs.zoom) 324 | 325 | acceleration[hook.prop] += force * delta 326 | 327 | 328 | # Add velocities to properties. Doing this at the end in case there are multiple hooks affecting the same velocity 329 | for prop, velocity of @hooks.velocities 330 | 331 | if prop is 'pos' 332 | 333 | # Add drag, if it exists 334 | if drag.pos 335 | velocity.x += @hooks.defs.getDrag(velocity.x, drag.pos, @hooks.defs.zoom) 336 | velocity.y += @hooks.defs.getDrag(velocity.y, drag.pos, @hooks.defs.zoom) 337 | 338 | # Add acceleration to velocity 339 | velocity.x += acceleration.pos.x 340 | velocity.y += acceleration.pos.y 341 | 342 | # Add velocity to position 343 | @x += velocity.x * delta 344 | @y += velocity.y * delta 345 | 346 | else 347 | 348 | # Add drag, if it exists 349 | if drag[prop] 350 | @hooks.velocities[prop] += @hooks.defs.getDrag(@hooks.velocities[prop], drag[prop], @hooks.defs.zoom) 351 | 352 | # Add acceleration to velocity 353 | @hooks.velocities[prop] += acceleration[prop] 354 | 355 | # Add velocity to property 356 | @[prop] += @hooks.velocities[prop] * delta 357 | 358 | @onHookUpdate?(delta) -------------------------------------------------------------------------------- /hook-example-spring.framer/spring-example-720.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-spring.framer/spring-example-720.gif -------------------------------------------------------------------------------- /hook-example-spring.framer/spring-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigtm/framer-hook/ca54ec1268092b5af195518aeb0b3dbd90bd533f/hook-example-spring.framer/spring-example.gif --------------------------------------------------------------------------------