sprite-factory 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. data/LICENSE +20 -0
  2. data/README.md +207 -0
  3. data/Rakefile +67 -0
  4. data/bin/sf +46 -0
  5. data/lib/sprite_factory.rb +51 -0
  6. data/lib/sprite_factory/layout.rb +89 -0
  7. data/lib/sprite_factory/library/chunky_png.rb +31 -0
  8. data/lib/sprite_factory/library/rmagick.rb +32 -0
  9. data/lib/sprite_factory/runner.rb +204 -0
  10. data/lib/sprite_factory/style.rb +58 -0
  11. data/sprite_factory.gemspec +24 -0
  12. data/test/images/custom/custom.css +4 -0
  13. data/test/images/custom/running.png +0 -0
  14. data/test/images/custom/stopped.png +0 -0
  15. data/test/images/empty/readme.txt +1 -0
  16. data/test/images/formats/alice.gif +0 -0
  17. data/test/images/formats/monkey.gif +0 -0
  18. data/test/images/formats/spies.jpg +0 -0
  19. data/test/images/formats/thief.png +0 -0
  20. data/test/images/irregular/irregular1.png +0 -0
  21. data/test/images/irregular/irregular2.png +0 -0
  22. data/test/images/irregular/irregular3.png +0 -0
  23. data/test/images/irregular/irregular4.png +0 -0
  24. data/test/images/irregular/irregular5.png +0 -0
  25. data/test/images/irregular/readme.txt +2 -0
  26. data/test/images/reference/custom.css +22 -0
  27. data/test/images/reference/custom.png +0 -0
  28. data/test/images/reference/formats.css +22 -0
  29. data/test/images/reference/formats.png +0 -0
  30. data/test/images/reference/index.html +135 -0
  31. data/test/images/reference/irregular.css +24 -0
  32. data/test/images/reference/irregular.fixed.css +24 -0
  33. data/test/images/reference/irregular.fixed.png +0 -0
  34. data/test/images/reference/irregular.horizontal.css +24 -0
  35. data/test/images/reference/irregular.horizontal.png +0 -0
  36. data/test/images/reference/irregular.padded.css +24 -0
  37. data/test/images/reference/irregular.padded.png +0 -0
  38. data/test/images/reference/irregular.png +0 -0
  39. data/test/images/reference/irregular.sassy.css +38 -0
  40. data/test/images/reference/irregular.sassy.png +0 -0
  41. data/test/images/reference/irregular.sassy.sass +40 -0
  42. data/test/images/reference/irregular.vertical.css +24 -0
  43. data/test/images/reference/irregular.vertical.png +0 -0
  44. data/test/images/reference/regular.css +24 -0
  45. data/test/images/reference/regular.custom.css +24 -0
  46. data/test/images/reference/regular.custom.png +0 -0
  47. data/test/images/reference/regular.fixed.css +24 -0
  48. data/test/images/reference/regular.fixed.png +0 -0
  49. data/test/images/reference/regular.horizontal.css +24 -0
  50. data/test/images/reference/regular.horizontal.png +0 -0
  51. data/test/images/reference/regular.padded.css +24 -0
  52. data/test/images/reference/regular.padded.png +0 -0
  53. data/test/images/reference/regular.png +0 -0
  54. data/test/images/reference/regular.sassy.css +38 -0
  55. data/test/images/reference/regular.sassy.png +0 -0
  56. data/test/images/reference/regular.sassy.sass +40 -0
  57. data/test/images/reference/regular.vertical.css +24 -0
  58. data/test/images/reference/regular.vertical.png +0 -0
  59. data/test/images/reference/s.gif +0 -0
  60. data/test/images/regular/regular1.png +0 -0
  61. data/test/images/regular/regular2.png +0 -0
  62. data/test/images/regular/regular3.png +0 -0
  63. data/test/images/regular/regular4.png +0 -0
  64. data/test/images/regular/regular5.png +0 -0
  65. data/test/integration_test.rb +100 -0
  66. data/test/layout_test.rb +228 -0
  67. data/test/library_test.rb +57 -0
  68. data/test/runner_test.rb +156 -0
  69. data/test/style_test.rb +64 -0
  70. data/test/test_case.rb +127 -0
  71. metadata +159 -0
@@ -0,0 +1,31 @@
1
+ require 'chunky_png'
2
+
3
+ module SpriteFactory
4
+ module Library
5
+ module ChunkyPng
6
+
7
+ VALID_EXTENSIONS = :png
8
+
9
+ def self.load(files)
10
+ files.map do |filename|
11
+ image = ChunkyPNG::Image.from_file(filename)
12
+ {
13
+ :filename => filename,
14
+ :image => image,
15
+ :width => image.width,
16
+ :height => image.height
17
+ }
18
+ end
19
+ end
20
+
21
+ def self.create(filename, images, width, height)
22
+ target = ChunkyPNG::Image.new(width, height, ChunkyPNG::Color::TRANSPARENT)
23
+ images.each do |image|
24
+ target.compose!(image[:image], image[:x], image[:y])
25
+ end
26
+ target.save(filename)
27
+ end
28
+
29
+ end # module ChunkyPng
30
+ end # module Library
31
+ end # module SpriteFactory
@@ -0,0 +1,32 @@
1
+ require 'RMagick'
2
+
3
+ module SpriteFactory
4
+ module Library
5
+ module RMagick
6
+
7
+ VALID_EXTENSIONS = [:png, :jpg, :jpeg, :gif]
8
+
9
+ def self.load(files)
10
+ files.map do |filename|
11
+ image = Magick::Image.read(filename)[0]
12
+ {
13
+ :filename => filename,
14
+ :image => image,
15
+ :width => image.columns,
16
+ :height => image.rows
17
+ }
18
+ end
19
+ end
20
+
21
+ def self.create(filename, images, width, height)
22
+ target = Magick::Image.new(width,height)
23
+ target.opacity = Magick::MaxRGB
24
+ images.each do |image|
25
+ target.composite!(image[:image], image[:x], image[:y], Magick::SrcOverCompositeOp)
26
+ end
27
+ target.write(filename)
28
+ end
29
+
30
+ end # module RMagick
31
+ end # module Library
32
+ end # module SpriteFactory
@@ -0,0 +1,204 @@
1
+ require 'pathname'
2
+
3
+ module SpriteFactory
4
+ class Runner
5
+
6
+ #----------------------------------------------------------------------------
7
+
8
+ attr :input
9
+ attr :config
10
+
11
+ def initialize(input, config = {})
12
+ @input = input.to_s[-1] == "/" ? input[0...-1] : input # gracefully ignore trailing slash on input directory name
13
+ @config = config
14
+ @config[:style] ||= SpriteFactory.style || :css
15
+ @config[:layout] ||= SpriteFactory.layout || :horizontal
16
+ @config[:library] ||= SpriteFactory.library || :rmagick
17
+ @config[:selector] ||= SpriteFactory.selector || 'img.'
18
+ @config[:csspath] ||= SpriteFactory.csspath
19
+ @config[:report] ||= SpriteFactory.report
20
+ end
21
+
22
+ #----------------------------------------------------------------------------
23
+
24
+ def run!(&block)
25
+
26
+ raise RuntimeError, "unknown layout #{layout_name}" if !Layout.respond_to?(layout_name)
27
+ raise RuntimeError, "unknown style #{style_name}" if !Style.respond_to?(style_name)
28
+ raise RuntimeError, "unknown library #{library_name}" if !Library.respond_to?(library_name)
29
+
30
+ raise RuntimeError, "input must be a single directory" if input.nil? || input.to_s.empty? || !File.directory?(input)
31
+ raise RuntimeError, "no output file specified" if output.nil? || output.to_s.empty?
32
+ raise RuntimeError, "no image files found" if image_files.empty?
33
+
34
+ raise RuntimeError, "set :width for fixed width, or :hpadding for horizontal padding, but not both." if width && !hpadding.zero?
35
+ raise RuntimeError, "set :height for fixed height, or :vpadding for vertical padding, but not both." if height && !vpadding.zero?
36
+
37
+ images = load_images
38
+ max = layout_images(images)
39
+ header = summary(images, max)
40
+
41
+ create_sprite(images, max[:width], max[:height])
42
+
43
+ css_file = File.open(output_style_file, "w+")
44
+ css_file.puts style_comment(header)
45
+ css_file.puts style(selector, css_path, images, &block)
46
+ css_file.puts IO.read(custom_style_file) if File.exists?(custom_style_file)
47
+ css_file.close
48
+
49
+ report(header)
50
+
51
+ end
52
+
53
+ #----------------------------------------------------------------------------
54
+
55
+ private
56
+
57
+ def output
58
+ config[:output] || input
59
+ end
60
+
61
+ def selector
62
+ config[:selector]
63
+ end
64
+
65
+ def style_name
66
+ config[:style]
67
+ end
68
+
69
+ def layout_name
70
+ config[:layout]
71
+ end
72
+
73
+ def library_name
74
+ config[:library]
75
+ end
76
+
77
+ def hpadding
78
+ config[:hpadding] || config[:padding] || 0
79
+ end
80
+
81
+ def vpadding
82
+ config[:vpadding] || config[:padding] || 0
83
+ end
84
+
85
+ def width
86
+ config[:width]
87
+ end
88
+
89
+ def height
90
+ config[:height]
91
+ end
92
+
93
+ def output_image_file
94
+ "#{output}.png" if output
95
+ end
96
+
97
+ def output_style_file
98
+ "#{output}.#{style_name}" if output and style_name
99
+ end
100
+
101
+ def custom_style_file
102
+ File.join(input, File.basename(input) + ".#{style_name}")
103
+ end
104
+
105
+ def css_path
106
+ base = File.basename(output_image_file)
107
+ custom = config[:csspath]
108
+ if custom
109
+ if custom.is_a?(Proc)
110
+ custom.call(base) # allow custom path using a lambda
111
+ elsif custom.include?('$IMAGE')
112
+ custom.sub('$IMAGE', base) # allow custom path with token replacement
113
+ else
114
+ File.join(custom, base) # allow custom path with simple prepend
115
+ end
116
+ else
117
+ base # otherwise, just default to basename of the output image
118
+ end
119
+ end
120
+
121
+ def image_files
122
+ return [] if input.nil?
123
+ valid_extensions = library::VALID_EXTENSIONS
124
+ expansions = Array(valid_extensions).map{|ext| File.join(input, "**", "*.#{ext}")}
125
+ Dir[*expansions].sort
126
+ end
127
+
128
+ #----------------------------------------------------------------------------
129
+
130
+ def library
131
+ @library ||= Library.send(library_name)
132
+ end
133
+
134
+ def load_images
135
+ images = library.load(image_files)
136
+ images.each do |i|
137
+ i[:name] = File.basename(i[:filename])
138
+ i[:ext] = File.extname(i[:name])
139
+ i[:name] = i[:name][0...-i[:ext].length] unless i[:ext].empty?
140
+
141
+ raise RuntimeError, "image #{i[:name]} does not fit within a fixed width of #{width}" if width && (width < i[:width])
142
+ raise RuntimeError, "image #{i[:name]} does not fit within a fixed height of #{height}" if height && (height < i[:height])
143
+ end
144
+ images
145
+ end
146
+
147
+ def create_sprite(images, width, height)
148
+ library.create(output_image_file, images, width, height)
149
+ end
150
+
151
+ #----------------------------------------------------------------------------
152
+
153
+ def layout_images(images)
154
+ Layout.send(layout_name, images, :width => width, :height => height, :hpadding => hpadding, :vpadding => vpadding)
155
+ end
156
+
157
+ #----------------------------------------------------------------------------
158
+
159
+ def style(selector, path, images, &block)
160
+ defaults = Style.generate(style_name, selector, path, images) # must call, even if custom block is given, because it stashes generated css style into image[:style] attributes
161
+ if block_given?
162
+ yield images.inject({}) {|h,i| h[i[:name].to_sym] = i; h} # provide custom rule builder a hash by image name
163
+ else
164
+ defaults
165
+ end
166
+ end
167
+
168
+ def style_comment(comment)
169
+ Style.comment(style_name, comment)
170
+ end
171
+
172
+ #----------------------------------------------------------------------------
173
+
174
+ def summary(images, max)
175
+ return <<-EOF
176
+
177
+ Creating a sprite from following images:
178
+ \n#{images.map{|i| " #{report_path(i[:filename])} (#{i[:width]}x#{i[:height]})" }.join("\n")}
179
+
180
+ Output files:
181
+ #{report_path(output_image_file)}
182
+ #{report_path(output_style_file)}
183
+
184
+ Output size:
185
+ #{max[:width]}x#{max[:height]}
186
+
187
+ EOF
188
+ end
189
+
190
+ def report(msg)
191
+ puts msg if config[:report]
192
+ end
193
+
194
+ def report_path(path) # always report paths relative to . to avoid machine specific information in report (to avoid DIFF issues in tests and version control)
195
+ @cwd ||= Pathname.new(File.expand_path('.'))
196
+ path = Pathname.new(path)
197
+ path = path.relative_path_from(@cwd) if path.absolute?
198
+ path.to_s
199
+ end
200
+
201
+ #----------------------------------------------------------------------------
202
+
203
+ end # class Runner
204
+ end # module SpriteFactory
@@ -0,0 +1,58 @@
1
+ module SpriteFactory
2
+ module Style
3
+
4
+ #----------------------------------------------------------------------------
5
+
6
+ def self.css(selector, name, attributes)
7
+ "#{selector}#{name} { #{css_style(attributes)}; }"
8
+ end
9
+
10
+ def self.css_style(attributes)
11
+ attributes.join("; ")
12
+ end
13
+
14
+ def self.css_comment(comment)
15
+ return "/*\n#{comment}\n*/"
16
+ end
17
+
18
+ #----------------------------------------------------------------------------
19
+
20
+ def self.sass(selector, name, attributes)
21
+ "#{selector}#{name}\n" + sass_style(attributes)
22
+ end
23
+
24
+ def self.sass_style(attributes)
25
+ attributes.map{|a| " #{a}"}.join("\n") + "\n"
26
+ end
27
+
28
+ def self.sass_comment(comment)
29
+ return "/* #{comment.rstrip} */" # SASS has peculiar indenting requirements in order to recognise closing block comment
30
+ end
31
+
32
+ #----------------------------------------------------------------------------
33
+
34
+ def self.generate(style_name, selector, path, images)
35
+ styles = []
36
+ images.each do |image|
37
+ attr = [
38
+ "width: #{image[:cssw]}px",
39
+ "height: #{image[:cssh]}px",
40
+ "background: url(#{path}) #{-image[:cssx]}px #{-image[:cssy]}px no-repeat"
41
+ ]
42
+ image[:style] = send("#{style_name}_style", attr) # make pure style available for (optional) custom rule generators (see usage of yield inside Runner#style)
43
+ styles << send(style_name, selector, image[:name], attr)
44
+ end
45
+ styles << ""
46
+ styles.join("\n")
47
+ end
48
+
49
+ #----------------------------------------------------------------------------
50
+
51
+ def self.comment(style_name, comment)
52
+ send("#{style_name}_comment", comment)
53
+ end
54
+
55
+ #----------------------------------------------------------------------------
56
+
57
+ end
58
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $LOAD_PATH.push File.expand_path("lib", File.dirname(__FILE__))
3
+ require 'sprite_factory'
4
+
5
+ Gem::Specification.new do |s|
6
+
7
+ s.name = "sprite-factory"
8
+ s.version = SpriteFactory::VERSION
9
+ s.platform = Gem::Platform::RUBY
10
+ s.authors = ["Jake Gordon"]
11
+ s.email = ["jake@codeincomplete.com"]
12
+ s.homepage = "https://github.com/jakesgordon/sprite-factory"
13
+ s.summary = SpriteFactory::SUMMARY
14
+ s.description = SpriteFactory::DESCRIPTION
15
+
16
+ s.add_development_dependency 'rmagick'
17
+ s.add_development_dependency 'chunky_png'
18
+
19
+ s.files = `git ls-files`.split("\n")
20
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
21
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
+ s.require_paths = ["lib"]
23
+
24
+ end
@@ -0,0 +1,4 @@
1
+ div.running span.running { display: inline; }
2
+ div.running span.stopped { display: none; }
3
+ div.stopped span.running { display: none; }
4
+ div.stopped span.stopped { display: inline; }
@@ -0,0 +1 @@
1
+ this file is only here because git can't deal with empty directories (uck)
@@ -0,0 +1,2 @@
1
+ this file is not an image file, but is in a directory that is going to be sprited to see if the SpriteFactory
2
+ can handle it without crashing. Lets hope so!
@@ -0,0 +1,22 @@
1
+ /*
2
+
3
+ Creating a sprite from following images:
4
+
5
+ test/images/custom/running.png (46x25)
6
+ test/images/custom/stopped.png (46x25)
7
+
8
+ Output files:
9
+ test/images/custom.png
10
+ test/images/custom.css
11
+
12
+ Output size:
13
+ 92x25
14
+
15
+
16
+ */
17
+ div.running img.button { cursor: pointer; width: 46px; height: 25px; background: url(custom.png) 0px 0px no-repeat }
18
+ div.stopped img.button { cursor: pointer; width: 46px; height: 25px; background: url(custom.png) -46px 0px no-repeat }
19
+ div.running span.running { display: inline; }
20
+ div.running span.stopped { display: none; }
21
+ div.stopped span.running { display: none; }
22
+ div.stopped span.stopped { display: inline; }
@@ -0,0 +1,22 @@
1
+ /*
2
+
3
+ Creating a sprite from following images:
4
+
5
+ test/images/formats/alice.gif (50x50)
6
+ test/images/formats/monkey.gif (50x50)
7
+ test/images/formats/spies.jpg (150x92)
8
+ test/images/formats/thief.png (50x50)
9
+
10
+ Output files:
11
+ test/images/formats.png
12
+ test/images/formats.css
13
+
14
+ Output size:
15
+ 300x92
16
+
17
+
18
+ */
19
+ img.alice { width: 50px; height: 50px; background: url(formats.png) 0px -21px no-repeat; }
20
+ img.monkey { width: 50px; height: 50px; background: url(formats.png) -50px -21px no-repeat; }
21
+ img.spies { width: 150px; height: 92px; background: url(formats.png) -100px 0px no-repeat; }
22
+ img.thief { width: 50px; height: 50px; background: url(formats.png) -250px -21px no-repeat; }
@@ -0,0 +1,135 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset='utf-8' />
5
+ <title>Test page for sprite factory</title>
6
+ <link href='regular.css' rel='stylesheet' type='text/css' media='screen'></link>
7
+ <link href='regular.horizontal.css' rel='stylesheet' type='text/css' media='screen'></link>
8
+ <link href='regular.vertical.css' rel='stylesheet' type='text/css' media='screen'></link>
9
+ <link href='regular.padded.css' rel='stylesheet' type='text/css' media='screen'></link>
10
+ <link href='regular.fixed.css' rel='stylesheet' type='text/css' media='screen'></link>
11
+ <link href='regular.sassy.css' rel='stylesheet' type='text/css' media='screen'></link>
12
+ <link href='irregular.css' rel='stylesheet' type='text/css' media='screen'></link>
13
+ <link href='irregular.horizontal.css' rel='stylesheet' type='text/css' media='screen'></link>
14
+ <link href='irregular.vertical.css' rel='stylesheet' type='text/css' media='screen'></link>
15
+ <link href='irregular.padded.css' rel='stylesheet' type='text/css' media='screen'></link>
16
+ <link href='irregular.fixed.css' rel='stylesheet' type='text/css' media='screen'></link>
17
+ <link href='irregular.sassy.css' rel='stylesheet' type='text/css' media='screen'></link>
18
+ <link href='custom.css' rel='stylesheet' type='text/css' media='screen'></link>
19
+ <link href='formats.css' rel='stylesheet' type='text/css' media='screen'></link>
20
+ <style>
21
+ img { border: 1px solid red; }
22
+ </style>
23
+ </head>
24
+ <body>
25
+
26
+ <h1>Regular (defaults)</h1>
27
+ <img src='s.gif' class='regular1'>
28
+ <img src='s.gif' class='regular2'>
29
+ <img src='s.gif' class='regular3'>
30
+ <img src='s.gif' class='regular4'>
31
+ <img src='s.gif' class='regular5'>
32
+
33
+ <h1>Regular (horizontal)</h1>
34
+ <img src='s.gif' class='horizontal_regular1'>
35
+ <img src='s.gif' class='horizontal_regular2'>
36
+ <img src='s.gif' class='horizontal_regular3'>
37
+ <img src='s.gif' class='horizontal_regular4'>
38
+ <img src='s.gif' class='horizontal_regular5'>
39
+
40
+ <h1>Regular (vertical)</h1>
41
+ <img src='s.gif' class='vertical_regular1'><br>
42
+ <img src='s.gif' class='vertical_regular2'><br>
43
+ <img src='s.gif' class='vertical_regular3'><br>
44
+ <img src='s.gif' class='vertical_regular4'><br>
45
+ <img src='s.gif' class='vertical_regular5'><br>
46
+
47
+ <h1>Regular (padded)</h1>
48
+ <img src='s.gif' class='padded_regular1'>
49
+ <img src='s.gif' class='padded_regular2'>
50
+ <img src='s.gif' class='padded_regular3'>
51
+ <img src='s.gif' class='padded_regular4'>
52
+ <img src='s.gif' class='padded_regular5'>
53
+
54
+ <h1>Regular (fixed)</h1>
55
+ <img src='s.gif' class='fixed_regular1'>
56
+ <img src='s.gif' class='fixed_regular2'>
57
+ <img src='s.gif' class='fixed_regular3'>
58
+ <img src='s.gif' class='fixed_regular4'>
59
+ <img src='s.gif' class='fixed_regular5'>
60
+
61
+ <h1>Regular (sassy)</h1>
62
+ <img src='s.gif' class='sassy_regular1'>
63
+ <img src='s.gif' class='sassy_regular2'>
64
+ <img src='s.gif' class='sassy_regular3'>
65
+ <img src='s.gif' class='sassy_regular4'>
66
+ <img src='s.gif' class='sassy_regular5'>
67
+
68
+ <h1>Irregular (defaults)</h1>
69
+ <img src='s.gif' class='irregular1'>
70
+ <img src='s.gif' class='irregular2'>
71
+ <img src='s.gif' class='irregular3'>
72
+ <img src='s.gif' class='irregular4'>
73
+ <img src='s.gif' class='irregular5'>
74
+
75
+ <h1>Irregular (horizontal)</h1>
76
+ <img src='s.gif' class='horizontal_irregular1'>
77
+ <img src='s.gif' class='horizontal_irregular2'>
78
+ <img src='s.gif' class='horizontal_irregular3'>
79
+ <img src='s.gif' class='horizontal_irregular4'>
80
+ <img src='s.gif' class='horizontal_irregular5'>
81
+
82
+ <h1>Irregular (vertical)</h1>
83
+ <img src='s.gif' class='vertical_irregular1'><br>
84
+ <img src='s.gif' class='vertical_irregular2'><br>
85
+ <img src='s.gif' class='vertical_irregular3'><br>
86
+ <img src='s.gif' class='vertical_irregular4'><br>
87
+ <img src='s.gif' class='vertical_irregular5'><br>
88
+
89
+ <h1>Irregular (padded)</h1>
90
+ <img src='s.gif' class='padded_irregular1'>
91
+ <img src='s.gif' class='padded_irregular2'>
92
+ <img src='s.gif' class='padded_irregular3'>
93
+ <img src='s.gif' class='padded_irregular4'>
94
+ <img src='s.gif' class='padded_irregular5'>
95
+
96
+ <h1>Irregular (fixed)</h1>
97
+ <img src='s.gif' class='fixed_irregular1'>
98
+ <img src='s.gif' class='fixed_irregular2'>
99
+ <img src='s.gif' class='fixed_irregular3'>
100
+ <img src='s.gif' class='fixed_irregular4'>
101
+ <img src='s.gif' class='fixed_irregular5'>
102
+
103
+ <h1>Irregular (sassy)</h1>
104
+ <img src='s.gif' class='sassy_irregular1'>
105
+ <img src='s.gif' class='sassy_irregular2'>
106
+ <img src='s.gif' class='sassy_irregular3'>
107
+ <img src='s.gif' class='sassy_irregular4'>
108
+ <img src='s.gif' class='sassy_irregular5'>
109
+
110
+ <h1>Custom</h1>
111
+ <div class='running' id='timer'>
112
+ <img src='s.gif' class='button' onclick="SpriteFactory.toggleTimer();">
113
+ <span class='running'>running</span>
114
+ <span class='stopped'>stopped</span>
115
+ </div>
116
+
117
+ <h1>Other Formats</h1>
118
+ <img src='s.gif' class='alice'>
119
+ <img src='s.gif' class='monkey'>
120
+ <img src='s.gif' class='spies'>
121
+ <img src='s.gif' class='thief'>
122
+
123
+ <script>
124
+ SpriteFactory = {
125
+ toggleTimer: function() {
126
+ var timer = document.getElementById('timer');
127
+ if (timer) {
128
+ timer.setAttribute('class', timer.getAttribute('class') == 'running' ? 'stopped' : 'running');
129
+ }
130
+ }
131
+ }
132
+ </script>
133
+
134
+ </body>
135
+ </html>