sprite-factory 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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>