├── .gitignore ├── CNAME ├── COPYING ├── Makefile ├── README.md ├── app-src ├── Makefile ├── env │ ├── dev │ │ ├── clj │ │ │ └── user.clj │ │ └── cljs │ │ │ └── inkscape_animation_assistant │ │ │ └── dev.cljs │ └── prod │ │ └── cljs │ │ └── inkscape_animation_assistant │ │ └── prod.cljs ├── launch.sh ├── package.json ├── project.clj ├── public │ ├── icon.png │ ├── index.html │ ├── layers.png │ └── style.css ├── shims.js └── src │ └── inkscape_animation_assistant │ ├── animation.cljs │ └── core.cljs ├── launcher-src ├── .gitignore ├── Makefile ├── iaa.rc ├── icon.svg ├── launch.c ├── make-icon └── splash.svg ├── screens ├── layers.png ├── svg-animation-assistant.gif └── walk-cycle.gif ├── test-svgs ├── balltest.svg ├── detective-walk.svg ├── illustrator-cc-2019 │ ├── README.md │ ├── demo-custom-layer-name.svg │ ├── demo-default.svg │ ├── demo-layer-name-with-fps.svg │ └── layer-visibility │ │ ├── SVG-image--layers-off.png │ │ └── SVG-image--layers-off.svg ├── test-no-viewbox.svg └── walk-cycle-2.svg └── try.png /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | profiles.clj 3 | pom.xml 4 | pom.xml.asc 5 | *.jar 6 | *.class 7 | /.lein-* 8 | /.nrepl-port 9 | resources/public/js 10 | .rebel_readline_history 11 | js 12 | chrome-svg-animation-assistant 13 | /out 14 | /.repl 15 | *.log 16 | /.env 17 | .*.swp 18 | workspace 19 | /*.exe 20 | /*.zip 21 | node_modules 22 | package*.json 23 | .lein-env 24 | animate*.js 25 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | svgflipbook.com 2 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | SVG Animation Assistant 2 | Copyright (C) 2018 Chris McCormick 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | svg-animation-assistant.zip: svg-animation-assistant.exe lib/index.html 2 | zip -r $@ . -x '*workspace*' '*src*' '*.git/*' '.*.swp' Makefile .gitignore 3 | 4 | svg-animation-assistant.exe: 5 | $(MAKE) -C launcher-src 6 | 7 | lib/index.html: 8 | $(MAKE) -C app-src 9 | 10 | clean: 11 | rm -f svg-animation-assistant.* 12 | $(MAKE) -C app-src clean 13 | $(MAKE) -C launcher-src clean 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Tool to do flipbook-style SVG animation with layers in Inkscape and other SVG editors. 2 | 3 | ## [Try it out online](https://svgflipbook.com/) 4 | 5 | ![SVG Animation Assistant interface showing Inkscape and a walk cycle animation](./screens/walk-cycle.gif) 6 | 7 | This tool will cycle through the layers of your SVG allowing you to do basic flip-book style animation. Each layer in your SVG is one frame of the animation. 8 | 9 | The animation live-reloads in the assistant window whenever you hit save in Inkscape. 10 | 11 | ![SVG Animation Assistant interface showing live reloading](./screens/svg-animation-assistant.gif) 12 | 13 | Customise the frame time and behaviour by editing the layer name: 14 | 15 | ![Inkscape layers UI with customisation](./screens/layers.png) 16 | 17 | * Set the number of milliseconds to pause on each frame by entering a number in brackets in the layer name like (100) for a pause of 1/10th of a second. 18 | * Add static background frames by putting (static) in the layer name. 19 | -------------------------------------------------------------------------------- /app-src/Makefile: -------------------------------------------------------------------------------- 1 | STATIC=../lib/index.html ../lib/style.css ../lib/animate.min.js ../lib/icon.png ../lib/layers.png 2 | BUILD=../lib/js/app.js 3 | 4 | build: $(BUILD) $(STATIC) 5 | 6 | node_modules: 7 | pnpm i --shamefully-hoist 8 | 9 | animate.js: src/inkscape_animation_assistant/animation.cljs ./shims.js node_modules 10 | cat ./shims.js > $@ 11 | ./node_modules/.bin/wisp --no-map < src/inkscape_animation_assistant/animation.cljs >> $@ 12 | 13 | public/animate.min.js: animate.js node_modules 14 | ./node_modules/.bin/uglifyjs animate.js > $@ 15 | 16 | ../lib/%: public/% 17 | cp -v $< $@ 18 | 19 | $(BUILD): src/**/** project.clj 20 | lein clean 21 | lein package 22 | 23 | clean: 24 | lein clean 25 | rm -f $(STATIC) $(BUILD) 26 | -------------------------------------------------------------------------------- /app-src/env/dev/clj/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require [figwheel-sidecar.repl-api :as ra] 3 | [clojure.java.io :as io] 4 | [environ.core :refer [env]])) 5 | 6 | (import 'java.lang.Runtime) 7 | 8 | (println "Building animate.min.js") 9 | 10 | (let [proc (.exec (Runtime/getRuntime) "make public/animate.min.js")] 11 | (with-open [rdr (io/reader (.getInputStream proc))] 12 | (doseq [line (line-seq rdr)] 13 | (println line)))) 14 | 15 | (defn start-fw [] 16 | (ra/start-figwheel!)) 17 | 18 | (defn stop-fw [] 19 | (ra/stop-figwheel!)) 20 | 21 | (defn cljs [] 22 | (ra/cljs-repl)) 23 | -------------------------------------------------------------------------------- /app-src/env/dev/cljs/inkscape_animation_assistant/dev.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:figwheel-no-load inkscape-animation-assistant.dev 2 | (:require 3 | [inkscape-animation-assistant.core :as core] 4 | [devtools.core :as devtools])) 5 | 6 | 7 | (enable-console-print!) 8 | 9 | (devtools/install!) 10 | 11 | (core/init!) 12 | -------------------------------------------------------------------------------- /app-src/env/prod/cljs/inkscape_animation_assistant/prod.cljs: -------------------------------------------------------------------------------- 1 | (ns inkscape-animation-assistant.prod 2 | (:require 3 | [inkscape-animation-assistant.core :as core])) 4 | 5 | ;;ignore println statements in prod 6 | (set! *print-fn* (fn [& _])) 7 | 8 | (core/init!) 9 | -------------------------------------------------------------------------------- /app-src/launch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | chromium-browser --user-data-dir=./chrome-svg-animation-assistant --window-size=600,600 --allow-file-access-from-files --app=http://localhost:3449/ 4 | -------------------------------------------------------------------------------- /app-src/package.json: -------------------------------------------------------------------------------- 1 | {"dependencies":{"uglify-js":"^3.17.4","wisp":"^0.13.0"}} 2 | -------------------------------------------------------------------------------- /app-src/project.clj: -------------------------------------------------------------------------------- 1 | (defproject inkscape-animation-assistant "0.1.0-SNAPSHOT" 2 | :description "FIXME: write description" 3 | :url "http://example.com/FIXME" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | 7 | :dependencies [[org.clojure/clojure "1.10.1"] 8 | [org.clojure/clojurescript "1.10.520"] 9 | [reagent "0.8.1"] 10 | [environ "1.1.0"] 11 | [binaryage/oops "0.7.0"]] 12 | 13 | :plugins [[lein-cljsbuild "1.1.7"] 14 | [lein-figwheel "0.5.19"] 15 | [lein-environ "1.1.0"]] 16 | 17 | :clean-targets ^{:protect false} 18 | 19 | [:target-path 20 | [:cljsbuild :builds :app :compiler :output-dir] 21 | [:cljsbuild :builds :app :compiler :output-to]] 22 | 23 | :resource-paths ["public"] 24 | 25 | :figwheel {:http-server-root "." 26 | :nrepl-port 7002 27 | :nrepl-middleware [cider.piggieback/wrap-cljs-repl] 28 | :css-dirs ["public"]} 29 | 30 | :cljsbuild {:builds {:app 31 | {:source-paths ["src" "env/dev/cljs"] 32 | :compiler 33 | {:main "inkscape-animation-assistant.dev" 34 | :output-to "public/js/app.js" 35 | :output-dir "public/js/out" 36 | :asset-path "js/out" 37 | :source-map true 38 | :optimizations :none 39 | :pretty-print true} 40 | :figwheel 41 | {:on-jsload "inkscape-animation-assistant.core/mount-root"}} 42 | :release 43 | {:source-paths ["src" "env/prod/cljs"] 44 | :compiler 45 | {:output-to "../lib/js/app.js" 46 | :output-dir "public/js/release" 47 | :asset-path "js/out" 48 | :optimizations :advanced 49 | :pretty-print false}}}} 50 | 51 | :aliases {"package" ["with-profile" "prod" "do" "clean" ["cljsbuild" "once" "release"]]} 52 | 53 | :profiles {:dev {:source-paths ["src" "env/dev/clj"] 54 | :dependencies [[binaryage/devtools "0.9.10"] 55 | [figwheel-sidecar "0.5.19"] 56 | [nrepl "0.6.0"] 57 | [cider/piggieback "0.4.1"]] 58 | :env {:dev true}} 59 | :prod {:source-paths ["src" "env/dev/clj"] 60 | :dependencies [[binaryage/devtools "0.9.10"] 61 | [figwheel-sidecar "0.5.19"] 62 | [nrepl "0.6.0"] 63 | [cider/piggieback "0.4.1"]]}}) 64 | -------------------------------------------------------------------------------- /app-src/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/svg-flipbook/82c22fa9d0ed4412d01b347dbef8f63d6bf91a2f/app-src/public/icon.png -------------------------------------------------------------------------------- /app-src/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SVG Flipbook - Inkscape SVG flipbook layer animation 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 34 | 35 | 36 |
37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app-src/public/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/svg-flipbook/82c22fa9d0ed4412d01b347dbef8f63d6bf91a2f/app-src/public/layers.png -------------------------------------------------------------------------------- /app-src/public/style.css: -------------------------------------------------------------------------------- 1 | /* apply a natural box layout model to all elements, but allowing components to change */ 2 | html { 3 | box-sizing: border-box; 4 | } 5 | 6 | *, *:before, *:after { 7 | box-sizing: inherit; 8 | } 9 | 10 | html, body, #app, #container { 11 | height: 100%; 12 | width: 100%; 13 | overflow: hidden; 14 | } 15 | 16 | body { 17 | font-family: 'Helvetica Neue', Verdana, Helvetica, Arial, sans-serif; 18 | margin: 0 auto; 19 | -webkit-font-smoothing: antialiased; 20 | font-size: 1.125em; 21 | color: #333; 22 | background-color: #404040 23 | line-height: 1.5em; 24 | } 25 | 26 | #app { 27 | display: flex; 28 | justify-content: center; 29 | align-items: center; 30 | } 31 | 32 | h1, h2, h3 { 33 | color: #000; 34 | } 35 | 36 | h1 { 37 | font-size: 2.5em 38 | } 39 | 40 | h2 { 41 | font-size: 2em 42 | } 43 | 44 | h3 { 45 | font-size: 1.5em 46 | } 47 | 48 | a:hover { 49 | text-decoration: underline; 50 | } 51 | 52 | /*** SPINNER ***/ 53 | 54 | #spinner { 55 | display: inline-block; 56 | width: 128px; 57 | height: 128px; 58 | animation: spinner 0.3s linear infinite; 59 | } 60 | 61 | @keyframes spinner { 62 | 0% { 63 | transform: rotate(0deg); 64 | } 65 | 100% { 66 | transform: rotate(360deg); 67 | } 68 | } 69 | 70 | /*** ELEMENTS ***/ 71 | 72 | #choosefile input[type="file"] { 73 | display: none; 74 | } 75 | 76 | #choosefile label { 77 | cursor: pointer; 78 | } 79 | 80 | #container { 81 | display: block; 82 | } 83 | 84 | #interface { 85 | position: absolute; 86 | top: 0px; 87 | right: 0px; 88 | left: 0px; 89 | bottom: 0px; 90 | opacity: 0; 91 | } 92 | 93 | #interface:hover { 94 | opacity: 1; 95 | } 96 | 97 | #animation svg { 98 | border: 1px dashed silver; 99 | position: absolute; 100 | top: 50%; 101 | left: 50%; 102 | transform: translate(-50%, -50%); 103 | max-width: 95vw; 104 | max-height: 95vh; 105 | width: unset; 106 | height: unset; 107 | } 108 | 109 | /*** HELP ***/ 110 | 111 | #modal { 112 | background-color: #363636; 113 | width: 100%; 114 | color: white; 115 | padding: 5em 1em; 116 | } 117 | 118 | #modal > div { 119 | max-width: 600px; 120 | margin: auto; 121 | } 122 | 123 | #intro { 124 | top: 0px; 125 | left: 0px; 126 | right: 0px; 127 | bottom: 0px; 128 | position: absolute; 129 | width: 100%; 130 | height: 100%; 131 | background-color: #eee; 132 | padding: 75px; 133 | overflow-y: auto; 134 | display: flex; 135 | flex-direction: column; 136 | align-items: center; 137 | justify-content: center; 138 | } 139 | 140 | #intro > div { 141 | width: 600px; 142 | max-width: 95vw; 143 | } 144 | 145 | #intro li + li { 146 | margin-top: 0.5em; 147 | } 148 | 149 | #help-page { 150 | top: 0px; 151 | left: 0px; 152 | right: 0px; 153 | bottom: 0px; 154 | position: absolute; 155 | width: 100%; 156 | height: 100%; 157 | background-color: #eee; 158 | padding-top: 75px; 159 | overflow-y: auto; 160 | } 161 | 162 | #help-page > div { 163 | width: 500px; 164 | max-width: 100%; 165 | margin: auto; 166 | padding: 1em; 167 | } 168 | 169 | #help-page img { 170 | display: block; 171 | margin: 50px auto; 172 | border: 1px solid #333; 173 | border-radius: 3px; 174 | box-shadow: 0px 0px 10px #555; 175 | } 176 | 177 | #help-page svg.icon { 178 | fill: #333; 179 | width: 48px; 180 | height: 48px; 181 | cursor: pointer; 182 | vertical-align: middle; 183 | float: right; 184 | } 185 | 186 | #help-page button, #modal button { 187 | border: none; 188 | border-radius: 3px; 189 | background-color: #01C7C7; 190 | color: white; 191 | font-size: 1.5em; 192 | font-weight: bold; 193 | margin: 1em 0em; 194 | padding: 0.25em 1em; 195 | float: right; 196 | } 197 | 198 | #help-page a { 199 | color: #333; 200 | font-weight: bold; 201 | } 202 | 203 | /*** MENU ***/ 204 | 205 | #menu { 206 | display: block; 207 | background-color: #262626; 208 | position: absolute; 209 | top: 0px; 210 | right: 0px; 211 | width: 100%; 212 | padding: 0px; 213 | margin: 0px; 214 | color: white; 215 | font-weight: bold; 216 | text-align: center; 217 | display: flex; 218 | align-items: center; 219 | } 220 | 221 | #menu a { 222 | text-decoration: none; 223 | color: #fff; 224 | } 225 | 226 | #menu > span { 227 | flex-basis: 33%; 228 | } 229 | 230 | #menu > span > span { 231 | margin: 10px; 232 | display: inline-block; 233 | } 234 | 235 | #menu #buttons { 236 | text-align: left; 237 | } 238 | 239 | #menu #buttons > * { 240 | text-align: center; 241 | } 242 | 243 | #menu #filename { 244 | padding-right: 2em; 245 | } 246 | 247 | #menu #actions { 248 | text-align: right; 249 | vertical-align: middle; 250 | } 251 | 252 | #menu #actions > * { 253 | text-align: center; 254 | vertical-align: middle; 255 | } 256 | 257 | #menu #actions .menu > * + * { 258 | margin-left: 1em; 259 | } 260 | 261 | #menu svg.icon { 262 | fill: #fff; 263 | width: 1em; 264 | height: 1em; 265 | cursor: pointer; 266 | vertical-align: middle; 267 | } 268 | 269 | #menu #logo { 270 | width: 48px; 271 | margin-left: 20px; 272 | margin-right: 20px; 273 | vertical-align: middle; 274 | } 275 | 276 | #menu .button { 277 | color: white; 278 | background-color: #787878; 279 | border-radius: 3px; 280 | border: none; 281 | padding: 0.5em; 282 | font-weight: bold; 283 | width: 150px; 284 | display: inline-block; 285 | cursor: pointer; 286 | } 287 | 288 | #menu #pp.button { 289 | background-color: #01C7C7; 290 | } 291 | -------------------------------------------------------------------------------- /app-src/shims.js: -------------------------------------------------------------------------------- 1 | function count(x) { return x.length; } 2 | function doall(x) { return x; } 3 | function mapIndexed(f, a) { return a.map(function(l, i) { return f(i,l);}); } 4 | function isEqual(a, b) { return a == b; } 5 | function partial(fn) { 6 | var slice = Array.prototype.slice; 7 | var stored_args = slice.call(arguments, 1); 8 | return function () { 9 | var new_args = slice.call(arguments); 10 | var args = stored_args.concat(new_args); 11 | return fn.apply(null, args); 12 | }; 13 | } 14 | exports = {}; 15 | setTimeout(function() { animate() }, 0); 16 | -------------------------------------------------------------------------------- /app-src/src/inkscape_animation_assistant/animation.cljs: -------------------------------------------------------------------------------- 1 | (ns inkscape-animation-assistant.animation) 2 | 3 | (def inkscape-label-re #"\((\d+)\)") 4 | (def illustrator-label-re #"_x28_(\d+)_x29_") 5 | 6 | (defn layer-get-delay [layer] 7 | (let [default 100] 8 | (if layer 9 | (let [label (or (.getAttribute layer "inkscape:label") (.getAttribute layer "id")) 10 | delayparameter-inkscape (if label (.match label inkscape-label-re)) 11 | delayparameter-illustrator (if label (.match label illustrator-label-re)) 12 | delayparameter (or delayparameter-inkscape delayparameter-illustrator)] 13 | (if delayparameter (js/parseInt (aget delayparameter 1)) default)) 14 | default))) 15 | 16 | (defn layer-is-static [layer] 17 | (if layer 18 | (-> (or 19 | (.getAttribute layer "inkscape:label") 20 | (.getAttribute layer "id")) 21 | (or "") 22 | (.indexOf "tatic") 23 | (not= -1)))) 24 | 25 | (defn layers-get-all [container] 26 | (js/Array.from (.querySelectorAll js/document container))) 27 | 28 | (defn flip-layers [cb layers] 29 | (doall 30 | (map-indexed 31 | (fn [i l] 32 | (aset (.-style l) "display" 33 | (if (cb i l) 34 | "inline" 35 | "none"))) 36 | layers))) 37 | 38 | (defn animate! [fn-is-playing? frame container] 39 | (let [fn-is-playing? (or fn-is-playing? (fn [] true)) 40 | layers (layers-get-all (or container "svg > g")) 41 | length (count layers) 42 | current-frame (mod (or frame 0) length) 43 | layer (aget layers (or current-frame 0)) 44 | frame-time (layer-get-delay layer) 45 | static (layer-is-static layer)] 46 | (if (fn-is-playing?) 47 | (do 48 | (if (or (not static) (not frame)) 49 | (flip-layers (fn [i l] (or (= i current-frame) (layer-is-static l))) layers)) 50 | (js/setTimeout (partial animate! fn-is-playing? (+ current-frame 1) container) frame-time))))) 51 | -------------------------------------------------------------------------------- /app-src/src/inkscape_animation_assistant/core.cljs: -------------------------------------------------------------------------------- 1 | (ns inkscape-animation-assistant.core 2 | (:require 3 | [reagent.core :as r] 4 | [inkscape-animation-assistant.animation :refer [animate! flip-layers layers-get-all]] 5 | [goog.crypt :refer [byteArrayToHex]] 6 | [oops.core :refer [oget]]) 7 | (:import goog.crypt.Sha256)) 8 | 9 | (def initial-state 10 | {:playing false 11 | :svg nil 12 | :last nil 13 | :menu nil 14 | :file nil 15 | :modal false}) 16 | 17 | (def animation-layers-selector "#animation svg > g") 18 | 19 | (defn read-file [file cb] 20 | (let [reader (js/FileReader.)] 21 | (aset reader "onload" #(cb (.. % -target -result))) 22 | (aset reader "onerror" #(js/console.log "FileReader error" %)) 23 | (.readAsText reader file "utf-8"))) 24 | 25 | (defn get-file-time [file] 26 | (if file 27 | (-> file .-lastModified js/Date. .getTime) 28 | 0)) 29 | 30 | (defn sha256 [t] 31 | (let [h (Sha256.)] 32 | (.update h t) 33 | (-> 34 | (.digest h) 35 | (byteArrayToHex) 36 | (.substr 8)))) 37 | 38 | (defn filewatcher [state] 39 | (let [file (@state :file) 40 | last-mod (@state :last)] 41 | ; TODO: strip out \n")))] 74 | (if export-text 75 | (str "data:image/svg;charset=utf-8," (js/encodeURIComponent export-text)) 76 | "#loading"))) 77 | 78 | (defn hide-menu [state ev] 79 | (swap! state assoc :help true :menu false) 80 | (.preventDefault ev)) 81 | 82 | (defn fit-width-height [div] 83 | (js/setTimeout (fn [] 84 | (let [svg (.querySelector div "svg") 85 | width (and svg (.getAttribute svg "width")) 86 | height (and svg (.getAttribute svg "height")) 87 | viewBox (when svg (oget svg "viewBox" "baseVal"))] 88 | (js/console.log "fit width height:" svg width height viewBox) 89 | (when (and viewBox (or (nil? width) (> (.indexOf width "%") 0)) (or (nil? height) (> (.indexOf height "%") 0))) 90 | (.setAttribute svg "width" (- (aget viewBox "width") (aget viewBox "x"))) 91 | (.setAttribute svg "height" (- (aget viewBox "height") (aget viewBox "y")))) 92 | (when (and height width 93 | (not (> (.indexOf height "%") 0)) 94 | (not (> (.indexOf width "%") 0)) 95 | (or (nil? viewBox) 96 | (= 0 97 | (aget viewBox "x") 98 | (aget viewBox "x") 99 | (aget viewBox "width") 100 | (aget viewBox "height")))) 101 | (.setAttribute svg "viewBox" (str "0 0 " width " " height))))) 102 | 1)) 103 | 104 | ;; ------------------------- 105 | ;; Views 106 | 107 | (defn component-play-pause [state] 108 | [:span#pp.button {:on-click (partial (if (@state :playing) pause! play!) state)} 109 | [:svg#play-pause.icon {:viewBox "0 0 1792 1792"} 110 | [:path {:fill "#fff" 111 | :stroke "#fff" 112 | :stroke-linejoin "round" 113 | :stroke-width "200" 114 | :d 115 | (if (@state :playing) 116 | "M1664 192v1408q0 26-19 45t-45 19h-1408q-26 0-45-19t-19-45v-1408q0-26 19-45t45-19h1408q26 0 45 19t19 45z" 117 | "M1576 927l-1328 738q-23 13-39.5 3t-16.5-36v-1472q0-26 16.5-36t39.5 3l1328 738q23 13 23 31t-23 31z")}]]]) 118 | 119 | (defn component-choose-file [state] 120 | [:label#choosefile 121 | [:input#fileinput {:type "file" 122 | :accept ".svg" 123 | :on-change (partial file-selected! state)}] 124 | [:a.button "Open SVG"]]) 125 | 126 | (defn component-menu [state animation-script] 127 | [:nav#menu 128 | [:span#buttons 129 | [:span [component-choose-file state]] 130 | [:span [component-play-pause state]]] 131 | [:span#filename [:span [:img#logo {:src "icon.png"}] (when (@state :file) (.-name (@state :file)))]] 132 | [:span#actions 133 | (when (@state :file) 134 | [:span 135 | [:a#export {:href (make-export-url state animation-script) :download (.replace (.-name (@state :file)) ".svg" "-animated.svg") :id "exportbtn"} "Export"]]) 136 | [:span 137 | [:a#help {:href "https://github.com/chr15m/svg-animation-assistant" 138 | :target "_BLANK"} 139 | "Source"]] 140 | [:span.menu 141 | [:a#help {:href "#" 142 | :on-click (partial hide-menu state)} 143 | "Help"]]]]) 144 | 145 | (defn component-close [_state close-fn] 146 | [:svg#close.icon {:viewBox "0 0 1792 1792" 147 | :on-click close-fn} 148 | [:path {:d "M1277 1122q0-26-19-45l-181-181 181-181q19-19 19-45 0-27-19-46l-90-90q-19-19-46-19-26 0-45 19l-181 181-181-181q-19-19-45-19-27 0-46 19l-90 90q-19 19-19 46 0 26 19 45l181 181-181 181q-19 19-19 45 0 27 19 46l90 90q19 19 46 19 26 0 45-19l181-181 181 181q19 19 45 19 27 0 46-19l90-90q19-19 19-46zm387-226q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"}]]) 149 | 150 | (defn component-modal [state] 151 | [:div#modal 152 | [:div 153 | [:p "Now open " [:strong (when (@state :file) (.-name (@state :file)))] " in your vector graphics editor."] 154 | [:p "When you make changes and save your work, the animation will update here."] 155 | [:button {:on-click #(swap! state assoc :modal false)} "Ok"]]]) 156 | 157 | (defn component-help [state] 158 | [:div#help-page 159 | [:div 160 | [component-close state #(swap! state assoc :help nil)] 161 | [:h1 "Help"] 162 | [:p "You can customize frame timing and behaviour by editing the layer name in your SVG editor."] 163 | [:img {:src "layers.png"}] 164 | [:p "Add frame commands to the layer name in brackets like '(300)' and '(static)'. See below for frame command details."] 165 | [:p "Once you have edited the layer name save your SVG and the changes will appear in the SVG Flipbook app immediately."] 166 | [:h3 "Frame duration"] 167 | [:p "Set the frame duration by entering the number of milliseconds in brackets in the layer name, like `(100)` for a pause of 100ms, or 1/10th of a second."] 168 | [:h3 "Static background"] 169 | [:p "Set a layer as a static background which will always be visible, by adding the word `(static)` to the layer name."] 170 | [:h3 "Export"] 171 | [:p "Click 'Export' in the top right to export an animated version of your SVG. It uses JavaScript to animate your SVG."] 172 | [:h3 "Embed code"] 173 | [:p "Once you have exported your animated SVG you can embed it in a web page with this code:"] 174 | [:pre "\n\t\n"] 175 | [:h3 "Source code"] 176 | [:p "Get the " [:span 177 | [:a#help {:href "https://github.com/chr15m/svg-animation-assistant" 178 | :target "_BLANK"} 179 | "source code on GitHub"]] "."] 180 | [:h3 "Feedback"] 181 | [:p "Got questions or feedback? " [:a#feedback {:href "mailto:chris@svgflipbook.com?subject=SVGFlipbook%20feedback"} "Send me an email"] "."] 182 | [:button {:on-click #(swap! state assoc :help nil)} "Ok"]]]) 183 | 184 | (defn component-app [state animation-script] 185 | [:div#container 186 | (if (:svg @state) 187 | [:div#animation {:dangerouslySetInnerHTML {:__html (@state :svg)} 188 | :ref #(when % 189 | (fit-width-height %) 190 | (flip-layers (fn [i _l] (= i 0)) (layers-get-all animation-layers-selector)))}] 191 | [:div#intro 192 | [:div 193 | [:h3 "SVG Flipbook"] 194 | [:p "SVG Flipbook is an online app for creating flipbook style frame-by-frame animated SVGs."] 195 | [:ul 196 | [:li "Start by opening the SVG you want to animate in your favourite editor, such as Inkscape."] 197 | [:li "Open the same SVG in this app using the 'Open SVG' button to the top left."] 198 | [:li "Add layers to your SVG. When you hit save, the animation will update in this window."]]]]) 199 | [:div#interface 200 | (when (or (not (@state :file)) (@state :help)) 201 | {:style {:opacity 1}}) 202 | (when (@state :help) 203 | [component-help state]) 204 | [component-menu state animation-script] 205 | (when (@state :modal) 206 | [component-modal state])]]) 207 | 208 | ;; ------------------------- 209 | ;; Initialize app 210 | 211 | (defonce state (r/atom initial-state)) 212 | 213 | (defn mount-root [] 214 | (-> 215 | (js/fetch "animate.min.js") 216 | (.then #(.text %)) 217 | (.then (fn [animation-script] 218 | (r/render [component-app state animation-script] (.getElementById js/document "app")))))) 219 | 220 | (defn init! [] 221 | (js/setInterval (partial #'filewatcher state) 500) 222 | (mount-root)) 223 | -------------------------------------------------------------------------------- /launcher-src/.gitignore: -------------------------------------------------------------------------------- 1 | *.ico 2 | *.res 3 | icon.png 4 | splash.png 5 | -------------------------------------------------------------------------------- /launcher-src/Makefile: -------------------------------------------------------------------------------- 1 | all: ../lib/icon.png ../lib/App/AppInfo/Launcher/Splash.jpg ../lib/App/AppInfo/AppIcon.ico ../svg-animation-assistant.exe 2 | 3 | ../lib/icon.png: icon.png 4 | cp $< $@ 5 | 6 | ../lib/App/AppInfo/Launcher/Splash.jpg: splash.svg 7 | inkscape -z -e splash.png -w 416 -h 257 $< 8 | convert splash.png $@ 9 | 10 | ../svg-animation-assistant.exe: launch.c iaa.res 11 | i686-w64-mingw32-gcc -o $@ $^ -Wl,-subsystem,windows 12 | 13 | iaa.res: iaa.rc AppIcon.ico 14 | i686-w64-mingw32-windres $< -O coff -o $@ 15 | 16 | AppIcon.ico: icon.png 17 | convert -background transparent "icon.png" -define icon:auto-resize=16,24,32,48,64,72,96,128,256 "AppIcon.ico" 18 | 19 | ../lib/App/AppInfo/AppIcon.ico: AppIcon.ico 20 | cp $< $@ 21 | 22 | icon.png: icon.svg 23 | inkscape -z -e $@ -w 256 -h 256 $< 24 | 25 | clean: 26 | rm -f iaa.res icon.png AppIcon.ico ../lib/icon.png ../lib/App/AppInfo/Launcher/Splash.jpg splash.png 27 | -------------------------------------------------------------------------------- /launcher-src/iaa.rc: -------------------------------------------------------------------------------- 1 | id ICON "AppIcon.ico" 2 | -------------------------------------------------------------------------------- /launcher-src/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | 62 | 68 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /launcher-src/launch.c: -------------------------------------------------------------------------------- 1 | #define NOMINMAX 2 | #define UNICODE 3 | #include 4 | #include 5 | #include 6 | 7 | int main(void) 8 | { 9 | TCHAR cwd[MAX_PATH+1] = L""; 10 | DWORD len = GetCurrentDirectory(MAX_PATH, cwd); 11 | 12 | TCHAR homedir[MAX_PATH]; 13 | SHGetFolderPath(NULL, CSIDL_PERSONAL | CSIDL_FLAG_CREATE, NULL, 0, homedir); 14 | 15 | char cmd[4096]; 16 | sprintf(cmd, "lib\\ChromiumPortable.exe --user-data-dir=\"%S\\svg-animation-assistant-chrome\" --window-size=600,600 --allow-file-access-from-files --app=\"file:///%S\\lib\\index.html\"", homedir, cwd); 17 | 18 | return WinExec(cmd, 0); 19 | } 20 | -------------------------------------------------------------------------------- /launcher-src/make-icon: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | convert -background transparent "icon.png" -define icon:auto-resize=16,24,32,48,64,72,96,128,256 "AppIcon.ico" 4 | -------------------------------------------------------------------------------- /launcher-src/splash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 32 | 54 | 61 | 64 | 68 | 72 | 76 | 80 | 84 | 88 | 92 | 96 | 100 | 104 | 108 | 112 | 116 | 120 | 124 | 128 | 132 | 136 | 140 | 144 | 148 | 149 | 152 | 157 | 163 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /screens/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/svg-flipbook/82c22fa9d0ed4412d01b347dbef8f63d6bf91a2f/screens/layers.png -------------------------------------------------------------------------------- /screens/svg-animation-assistant.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/svg-flipbook/82c22fa9d0ed4412d01b347dbef8f63d6bf91a2f/screens/svg-animation-assistant.gif -------------------------------------------------------------------------------- /screens/walk-cycle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/svg-flipbook/82c22fa9d0ed4412d01b347dbef8f63d6bf91a2f/screens/walk-cycle.gif -------------------------------------------------------------------------------- /test-svgs/balltest.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 64 | 65 | 82 | 112 | 129 | 146 | 151 | 164 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /test-svgs/illustrator-cc-2019/README.md: -------------------------------------------------------------------------------- 1 | # Illustrator SVG Files 2 | 3 | ## Process 4 | 5 | 1. Create new file in illustrator 6 | 2. Create new layers 7 | 3. File > Save As... Format: SVG (default settings) 8 | 9 | ## Observations 10 | 11 | Illustrator names layers i the following format `Layer 1`, `Layer 2`, `Layer 3`. 12 | 13 | On save illustrator renderes each layer into a `` 14 | 15 | When re-opening the SVG file in illustrator all groups are nested under a parent layer node `Layer 1`. 16 | 17 | Layer 1 18 | ├── SVG Group 1 19 | ├── SVG Group 2 20 | └── SVG Group 3 21 | 22 | For simplicity new layers should be nested below the parent layer node. 23 | -------------------------------------------------------------------------------- /test-svgs/illustrator-cc-2019/demo-custom-layer-name.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /test-svgs/illustrator-cc-2019/demo-default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /test-svgs/illustrator-cc-2019/demo-layer-name-with-fps.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test-svgs/illustrator-cc-2019/layer-visibility/SVG-image--layers-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/svg-flipbook/82c22fa9d0ed4412d01b347dbef8f63d6bf91a2f/test-svgs/illustrator-cc-2019/layer-visibility/SVG-image--layers-off.png -------------------------------------------------------------------------------- /test-svgs/test-no-viewbox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 38 | 40 | 41 | 43 | image/svg+xml 44 | 46 | 47 | 48 | 49 | 50 | 54 | 63 | 64 | 68 | 78 | 79 | 83 | 93 | 94 | 98 | 108 | 109 | 113 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /test-svgs/walk-cycle-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 32 | 58 | 63 | 68 | 69 | 74 | 79 | 80 | 85 | 91 | 92 | 97 | 103 | 104 | 109 | 115 | 116 | 121 | 127 | 128 | 140 | 152 | 153 | -------------------------------------------------------------------------------- /try.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/svg-flipbook/82c22fa9d0ed4412d01b347dbef8f63d6bf91a2f/try.png --------------------------------------------------------------------------------