├── .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 | 
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 | 
12 |
13 | Customise the frame time and behaviour by editing the layer name:
14 |
15 | 
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 |
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 ""]
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 |
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 |
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 |
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 |
29 |
--------------------------------------------------------------------------------
/test-svgs/illustrator-cc-2019/demo-default.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
26 |
--------------------------------------------------------------------------------
/test-svgs/illustrator-cc-2019/demo-layer-name-with-fps.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
125 |
--------------------------------------------------------------------------------
/test-svgs/walk-cycle-2.svg:
--------------------------------------------------------------------------------
1 |
2 |
153 |
--------------------------------------------------------------------------------
/try.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chr15m/svg-flipbook/82c22fa9d0ed4412d01b347dbef8f63d6bf91a2f/try.png
--------------------------------------------------------------------------------