Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(97)

Side by Side Diff: Source/core/animation/css/CSSAnimations.cpp

Issue 851693007: Prepare for responsive CSS animations. (Closed) Base URL: https://chromium.googlesource.com/chromium/blink.git@master
Patch Set: Attempt to re-snapshot only if needed Created 5 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 /* 1 /*
2 * Copyright (C) 2013 Google Inc. All rights reserved. 2 * Copyright (C) 2013 Google Inc. All rights reserved.
3 * 3 *
4 * Redistribution and use in source and binary forms, with or without 4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are 5 * modification, are permitted provided that the following conditions are
6 * met: 6 * met:
7 * 7 *
8 * * Redistributions of source code must retain the above copyright 8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer. 9 * notice, this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above 10 * * Redistributions in binary form must reproduce the above
(...skipping 25 matching lines...) Expand all
36 #include "core/animation/AnimationPlayer.h" 36 #include "core/animation/AnimationPlayer.h"
37 #include "core/animation/AnimationTimeline.h" 37 #include "core/animation/AnimationTimeline.h"
38 #include "core/animation/CompositorAnimations.h" 38 #include "core/animation/CompositorAnimations.h"
39 #include "core/animation/Interpolation.h" 39 #include "core/animation/Interpolation.h"
40 #include "core/animation/KeyframeEffectModel.h" 40 #include "core/animation/KeyframeEffectModel.h"
41 #include "core/animation/LegacyStyleInterpolation.h" 41 #include "core/animation/LegacyStyleInterpolation.h"
42 #include "core/animation/css/CSSAnimatableValueFactory.h" 42 #include "core/animation/css/CSSAnimatableValueFactory.h"
43 #include "core/animation/css/CSSPropertyEquality.h" 43 #include "core/animation/css/CSSPropertyEquality.h"
44 #include "core/css/CSSKeyframeRule.h" 44 #include "core/css/CSSKeyframeRule.h"
45 #include "core/css/CSSPropertyMetadata.h" 45 #include "core/css/CSSPropertyMetadata.h"
46 #include "core/css/RenderStyleCSSValueMapping.h"
46 #include "core/css/resolver/CSSToStyleMap.h" 47 #include "core/css/resolver/CSSToStyleMap.h"
47 #include "core/css/resolver/StyleResolver.h" 48 #include "core/css/resolver/StyleResolver.h"
48 #include "core/dom/Element.h" 49 #include "core/dom/Element.h"
49 #include "core/dom/PseudoElement.h" 50 #include "core/dom/PseudoElement.h"
50 #include "core/dom/StyleEngine.h" 51 #include "core/dom/StyleEngine.h"
51 #include "core/events/TransitionEvent.h" 52 #include "core/events/TransitionEvent.h"
52 #include "core/events/WebKitAnimationEvent.h" 53 #include "core/events/WebKitAnimationEvent.h"
53 #include "core/frame/UseCounter.h" 54 #include "core/frame/UseCounter.h"
54 #include "core/rendering/RenderLayer.h" 55 #include "core/rendering/RenderLayer.h"
55 #include "core/rendering/RenderObject.h" 56 #include "core/rendering/RenderObject.h"
(...skipping 23 matching lines...) Expand all
79 case CSSPropertyWebkitTransformOriginZ: 80 case CSSPropertyWebkitTransformOriginZ:
80 case CSSPropertyWebkitTransformOrigin: 81 case CSSPropertyWebkitTransformOrigin:
81 return CSSPropertyTransformOrigin; 82 return CSSPropertyTransformOrigin;
82 default: 83 default:
83 break; 84 break;
84 } 85 }
85 return property; 86 return property;
86 } 87 }
87 88
88 static void resolveKeyframes(StyleResolver* resolver, const Element* animatingEl ement, Element& element, const RenderStyle& style, RenderStyle* parentStyle, con st AtomicString& name, TimingFunction* defaultTimingFunction, 89 static void resolveKeyframes(StyleResolver* resolver, const Element* animatingEl ement, Element& element, const RenderStyle& style, RenderStyle* parentStyle, con st AtomicString& name, TimingFunction* defaultTimingFunction,
89 AnimatableValueKeyframeVector& keyframes) 90 StringKeyframeVector& keyframes)
90 { 91 {
91 // When the animating element is null, use its parent for scoping purposes. 92 // When the animating element is null, use its parent for scoping purposes.
92 const Element* elementForScoping = animatingElement ? animatingElement : &el ement; 93 const Element* elementForScoping = animatingElement ? animatingElement : &el ement;
93 const StyleRuleKeyframes* keyframesRule = resolver->findKeyframesRule(elemen tForScoping, name); 94 const StyleRuleKeyframes* keyframesRule = resolver->findKeyframesRule(elemen tForScoping, name);
94 if (!keyframesRule) 95 if (!keyframesRule)
95 return; 96 return;
96 97
97 const WillBeHeapVector<RefPtrWillBeMember<StyleRuleKeyframe>>& styleKeyframe s = keyframesRule->keyframes(); 98 const WillBeHeapVector<RefPtrWillBeMember<StyleRuleKeyframe>>& styleKeyframe s = keyframesRule->keyframes();
98 99
99 // Construct and populate the style for each keyframe 100 // Construct and populate the style for each keyframe
100 PropertySet specifiedPropertiesForUseCounter; 101 PropertySet specifiedPropertiesForUseCounter;
101 for (size_t i = 0; i < styleKeyframes.size(); ++i) { 102 for (size_t i = 0; i < styleKeyframes.size(); ++i) {
102 const StyleRuleKeyframe* styleKeyframe = styleKeyframes[i].get(); 103 const StyleRuleKeyframe* styleKeyframe = styleKeyframes[i].get();
103 RefPtr<RenderStyle> keyframeStyle = resolver->styleForKeyframe(element, style, parentStyle, styleKeyframe, name); 104 RefPtr<RenderStyle> keyframeStyle = resolver->styleForKeyframe(element, style, parentStyle, styleKeyframe, name);
104 RefPtrWillBeRawPtr<AnimatableValueKeyframe> keyframe = AnimatableValueKe yframe::create(); 105 RefPtrWillBeRawPtr<StringKeyframe> keyframe = StringKeyframe::create();
105 const Vector<double>& offsets = styleKeyframe->keys(); 106 const Vector<double>& offsets = styleKeyframe->keys();
106 ASSERT(!offsets.isEmpty()); 107 ASSERT(!offsets.isEmpty());
107 keyframe->setOffset(offsets[0]); 108 keyframe->setOffset(offsets[0]);
108 keyframe->setEasing(defaultTimingFunction); 109 keyframe->setEasing(defaultTimingFunction);
109 const StylePropertySet& properties = styleKeyframe->properties(); 110 const StylePropertySet& properties = styleKeyframe->properties();
110 for (unsigned j = 0; j < properties.propertyCount(); j++) { 111 for (unsigned j = 0; j < properties.propertyCount(); j++) {
111 specifiedPropertiesForUseCounter.add(properties.propertyAt(j).id()); 112 specifiedPropertiesForUseCounter.add(properties.propertyAt(j).id());
112 CSSPropertyID property = propertyForAnimation(properties.propertyAt( j).id()); 113 CSSPropertyID property = propertyForAnimation(properties.propertyAt( j).id());
113 if (property == CSSPropertyWebkitAnimationTimingFunction || property == CSSPropertyAnimationTimingFunction) { 114 if (property == CSSPropertyWebkitAnimationTimingFunction || property == CSSPropertyAnimationTimingFunction) {
114 CSSValue* value = properties.propertyAt(j).value(); 115 CSSValue* value = properties.propertyAt(j).value();
115 RefPtr<TimingFunction> timingFunction; 116 RefPtr<TimingFunction> timingFunction;
116 if (value->isInheritedValue() && parentStyle->animations()) { 117 if (value->isInheritedValue() && parentStyle->animations()) {
117 timingFunction = parentStyle->animations()->timingFunctionLi st()[0]; 118 timingFunction = parentStyle->animations()->timingFunctionLi st()[0];
118 } else if (value->isValueList()) { 119 } else if (value->isValueList()) {
119 timingFunction = CSSToStyleMap::mapAnimationTimingFunction(t oCSSValueList(value)->item(0)); 120 timingFunction = CSSToStyleMap::mapAnimationTimingFunction(t oCSSValueList(value)->item(0));
120 } else { 121 } else {
121 ASSERT(value->isInheritedValue() || value->isInitialValue() || value->isUnsetValue()); 122 ASSERT(value->isInheritedValue() || value->isInitialValue() || value->isUnsetValue());
122 timingFunction = CSSTimingData::initialTimingFunction(); 123 timingFunction = CSSTimingData::initialTimingFunction();
123 } 124 }
124 keyframe->setEasing(timingFunction.release()); 125 keyframe->setEasing(timingFunction.release());
125 } else if (CSSPropertyMetadata::isAnimatableProperty(property)) { 126 } else if (CSSPropertyMetadata::isAnimatableProperty(property)) {
126 keyframe->setPropertyValue(property, CSSAnimatableValueFactory:: create(property, *keyframeStyle).get()); 127 keyframe->setPropertyValue(property, properties.propertyAt(j).va lue());
128 if (property == CSSPropertyOpacity || property == CSSPropertyTra nsform)
129 keyframe->setPropertyAnimatableValue(property, CSSAnimatable ValueFactory::create(property, *keyframeStyle));
127 } 130 }
128 } 131 }
129 keyframes.append(keyframe); 132 keyframes.append(keyframe);
130 // The last keyframe specified at a given offset is used. 133 // The last keyframe specified at a given offset is used.
131 for (size_t j = 1; j < offsets.size(); ++j) { 134 for (size_t j = 1; j < offsets.size(); ++j) {
132 keyframes.append(toAnimatableValueKeyframe(keyframe->cloneWithOffset (offsets[j]).get())); 135 keyframes.append(toStringKeyframe(keyframe->cloneWithOffset(offsets[ j]).get()));
133 } 136 }
134 } 137 }
135 138
136 for (CSSPropertyID property : specifiedPropertiesForUseCounter) { 139 for (CSSPropertyID property : specifiedPropertiesForUseCounter) {
137 ASSERT(property != CSSPropertyInvalid); 140 ASSERT(property != CSSPropertyInvalid);
138 blink::Platform::current()->histogramSparse("WebCore.Animation.CSSProper ties", UseCounter::mapCSSPropertyIdToCSSSampleIdForHistogram(property)); 141 blink::Platform::current()->histogramSparse("WebCore.Animation.CSSProper ties", UseCounter::mapCSSPropertyIdToCSSSampleIdForHistogram(property));
139 } 142 }
140 143
141 // Merge duplicate keyframes. 144 // Merge duplicate keyframes.
142 std::stable_sort(keyframes.begin(), keyframes.end(), Keyframe::compareOffset s); 145 std::stable_sort(keyframes.begin(), keyframes.end(), Keyframe::compareOffset s);
143 size_t targetIndex = 0; 146 size_t targetIndex = 0;
144 for (size_t i = 1; i < keyframes.size(); i++) { 147 for (size_t i = 1; i < keyframes.size(); i++) {
145 if (keyframes[i]->offset() == keyframes[targetIndex]->offset()) { 148 if (keyframes[i]->offset() == keyframes[targetIndex]->offset()) {
146 for (CSSPropertyID property : keyframes[i]->properties()) 149 for (CSSPropertyID property : keyframes[i]->properties())
147 keyframes[targetIndex]->setPropertyValue(property, keyframes[i]- >propertyValue(property)); 150 keyframes[targetIndex]->setPropertyValue(property, keyframes[i]- >propertyValue(property));
148 } else { 151 } else {
149 targetIndex++; 152 targetIndex++;
150 keyframes[targetIndex] = keyframes[i]; 153 keyframes[targetIndex] = keyframes[i];
151 } 154 }
152 } 155 }
153 if (!keyframes.isEmpty()) 156 if (!keyframes.isEmpty())
154 keyframes.shrink(targetIndex + 1); 157 keyframes.shrink(targetIndex + 1);
155 158
156 // Add 0% and 100% keyframes if absent. 159 // Add 0% and 100% keyframes if absent.
157 RefPtrWillBeRawPtr<AnimatableValueKeyframe> startKeyframe = keyframes.isEmpt y() ? nullptr : keyframes[0]; 160 RefPtrWillBeRawPtr<StringKeyframe> startKeyframe = keyframes.isEmpty() ? nul lptr : keyframes[0];
158 if (!startKeyframe || keyframes[0]->offset() != 0) { 161 if (!startKeyframe || keyframes[0]->offset() != 0) {
159 startKeyframe = AnimatableValueKeyframe::create(); 162 startKeyframe = StringKeyframe::create();
160 startKeyframe->setOffset(0); 163 startKeyframe->setOffset(0);
161 startKeyframe->setEasing(defaultTimingFunction); 164 startKeyframe->setEasing(defaultTimingFunction);
162 keyframes.prepend(startKeyframe); 165 keyframes.prepend(startKeyframe);
163 } 166 }
164 RefPtrWillBeRawPtr<AnimatableValueKeyframe> endKeyframe = keyframes[keyframe s.size() - 1]; 167 RefPtrWillBeRawPtr<StringKeyframe> endKeyframe = keyframes[keyframes.size() - 1];
165 if (endKeyframe->offset() != 1) { 168 if (endKeyframe->offset() != 1) {
166 endKeyframe = AnimatableValueKeyframe::create(); 169 endKeyframe = StringKeyframe::create();
167 endKeyframe->setOffset(1); 170 endKeyframe->setOffset(1);
168 endKeyframe->setEasing(defaultTimingFunction); 171 endKeyframe->setEasing(defaultTimingFunction);
169 keyframes.append(endKeyframe); 172 keyframes.append(endKeyframe);
170 } 173 }
171 ASSERT(keyframes.size() >= 2); 174 ASSERT(keyframes.size() >= 2);
172 ASSERT(!keyframes.first()->offset()); 175 ASSERT(!keyframes.first()->offset());
173 ASSERT(keyframes.last()->offset() == 1); 176 ASSERT(keyframes.last()->offset() == 1);
174 177
175 // Snapshot current property values for 0% and 100% if missing. 178 // Explicitly create neutral keyframes for 0% and 100% if missing.
179 // FIXME: Remove this once neutral keyframes are implemented in StringKeyfra mes.
176 PropertySet allProperties; 180 PropertySet allProperties;
177 for (const auto& keyframe : keyframes) { 181 for (const auto& keyframe : keyframes) {
178 for (CSSPropertyID property : keyframe->properties()) 182 for (CSSPropertyID property : keyframe->properties())
179 allProperties.add(property); 183 allProperties.add(property);
180 } 184 }
181 const PropertySet& startKeyframeProperties = startKeyframe->properties(); 185 const PropertySet& startKeyframeProperties = startKeyframe->properties();
182 const PropertySet& endKeyframeProperties = endKeyframe->properties(); 186 const PropertySet& endKeyframeProperties = endKeyframe->properties();
183 bool missingStartValues = startKeyframeProperties.size() < allProperties.siz e(); 187 bool missingStartValues = startKeyframeProperties.size() < allProperties.siz e();
184 bool missingEndValues = endKeyframeProperties.size() < allProperties.size(); 188 bool missingEndValues = endKeyframeProperties.size() < allProperties.size();
185 if (missingStartValues || missingEndValues) { 189 if (missingStartValues || missingEndValues) {
186 for (CSSPropertyID property : allProperties) { 190 for (CSSPropertyID property : allProperties) {
187 bool startNeedsValue = missingStartValues && !startKeyframePropertie s.contains(property); 191 bool startNeedsValue = missingStartValues && !startKeyframePropertie s.contains(property);
188 bool endNeedsValue = missingEndValues && !endKeyframeProperties.cont ains(property); 192 bool endNeedsValue = missingEndValues && !endKeyframeProperties.cont ains(property);
189 if (!startNeedsValue && !endNeedsValue) 193 if (!startNeedsValue && !endNeedsValue)
190 continue; 194 continue;
191 RefPtrWillBeRawPtr<AnimatableValue> snapshotValue = CSSAnimatableVal ueFactory::create(property, style); 195
192 if (startNeedsValue) 196 bool isCompositableProperty = property == CSSPropertyOpacity || prop erty == CSSPropertyTransform;
193 startKeyframe->setPropertyValue(property, snapshotValue.get()); 197
194 if (endNeedsValue) 198 if (startNeedsValue) {
195 endKeyframe->setPropertyValue(property, snapshotValue.get()); 199 startKeyframe->setPropertyValue(property, nullptr);
196 if (property == CSSPropertyOpacity || property == CSSPropertyTransfo rm) 200 if (isCompositableProperty)
201 startKeyframe->setPropertyAnimatableValue(property, CSSAnima tableValueFactory::create(property, style));
202 }
203
204 if (endNeedsValue) {
205 endKeyframe->setPropertyValue(property, nullptr);
206 if (isCompositableProperty)
207 endKeyframe->setPropertyAnimatableValue(property, CSSAnimata bleValueFactory::create(property, style));
208 }
209
210 if (isCompositableProperty)
197 UseCounter::count(elementForScoping->document(), UseCounter::Syn theticKeyframesInCompositedCSSAnimation); 211 UseCounter::count(elementForScoping->document(), UseCounter::Syn theticKeyframesInCompositedCSSAnimation);
198 } 212 }
199 } 213 }
200 ASSERT(startKeyframe->properties().size() == allProperties.size()); 214 ASSERT(startKeyframe->properties().size() == allProperties.size());
201 ASSERT(endKeyframe->properties().size() == allProperties.size()); 215 ASSERT(endKeyframe->properties().size() == allProperties.size());
202 } 216 }
203 217
204 } // namespace 218 } // namespace
205 219
206 CSSAnimations::CSSAnimations() 220 CSSAnimations::CSSAnimations()
(...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after
258 RefPtr<TimingFunction> keyframeTimingFunction = timing.timingFunctio n; 272 RefPtr<TimingFunction> keyframeTimingFunction = timing.timingFunctio n;
259 timing.timingFunction = Timing::defaults().timingFunction; 273 timing.timingFunction = Timing::defaults().timingFunction;
260 274
261 if (cssAnimations) { 275 if (cssAnimations) {
262 AnimationMap::const_iterator existing(cssAnimations->m_animation s.find(animationName)); 276 AnimationMap::const_iterator existing(cssAnimations->m_animation s.find(animationName));
263 if (existing != cssAnimations->m_animations.end()) { 277 if (existing != cssAnimations->m_animations.end()) {
264 inactive.remove(animationName); 278 inactive.remove(animationName);
265 279
266 AnimationPlayer* player = existing->value.get(); 280 AnimationPlayer* player = existing->value.get();
267 281
282 if (!activeAnimations || !activeAnimations->isAnimationStyle Change()) {
Timothy Loh 2015/01/20 05:29:32 We refer to this so many times now, can we just pu
shend 2015/01/20 23:13:03 Done.
283 if (player->source()->isAnimation()) {
Timothy Loh 2015/01/20 05:29:32 Is this check is because players for CSS animation
shend 2015/01/20 23:13:03 :)
284 Animation* animation = toAnimation(player->source()) ;
Timothy Loh 2015/01/20 05:29:32 Just store the effect here?
shend 2015/01/20 23:13:03 Done.
285 if (animation->effect() && animation->effect()->isKe yframeEffectModel()) {
286 toKeyframeEffectModelBase(animation->effect())-> setDeferredInterpolationsOutdated(true);
287 }
288 }
289 }
290
268 // FIXME: Should handle changes in the timing function. 291 // FIXME: Should handle changes in the timing function.
269 if (timing != player->source()->specifiedTiming()) { 292 if (timing != player->source()->specifiedTiming()) {
270 ASSERT(!activeAnimations || !activeAnimations->isAnimati onStyleChange()); 293 ASSERT(!activeAnimations || !activeAnimations->isAnimati onStyleChange());
271 294
272 AnimatableValueKeyframeVector resolvedKeyframes; 295 StringKeyframeVector resolvedKeyframes;
273 resolveKeyframes(resolver, animatingElement, element, st yle, parentStyle, animationName, keyframeTimingFunction.get(), resolvedKeyframes ); 296 resolveKeyframes(resolver, animatingElement, element, st yle, parentStyle, animationName, keyframeTimingFunction.get(), resolvedKeyframes );
274 297
275 update->updateAnimationTiming(player, InertAnimation::cr eate(AnimatableValueKeyframeEffectModel::create(resolvedKeyframes), 298 auto effect = StringKeyframeEffectModel::create(resolved Keyframes);
276 timing, isPaused, player->currentTimeInternal()), ti ming); 299 effect->forceConversionsToAnimatableValues(&element); // FIXME: remove this once LegacyStyleInterpolation is removed from StringKeyframe
300 update->updateAnimationTiming(player, InertAnimation::cr eate(effect, timing, isPaused, player->currentTimeInternal()), timing);
277 } 301 }
278 302
279 if (isPaused != player->paused()) { 303 if (isPaused != player->paused()) {
280 ASSERT(!activeAnimations || !activeAnimations->isAnimati onStyleChange()); 304 ASSERT(!activeAnimations || !activeAnimations->isAnimati onStyleChange());
281 update->toggleAnimationPaused(animationName); 305 update->toggleAnimationPaused(animationName);
282 } 306 }
283 307
284 continue; 308 continue;
285 } 309 }
286 } 310 }
287 311
288 AnimatableValueKeyframeVector resolvedKeyframes; 312 StringKeyframeVector resolvedKeyframes;
289 resolveKeyframes(resolver, animatingElement, element, style, parentS tyle, animationName, keyframeTimingFunction.get(), resolvedKeyframes); 313 resolveKeyframes(resolver, animatingElement, element, style, parentS tyle, animationName, keyframeTimingFunction.get(), resolvedKeyframes);
290 if (!resolvedKeyframes.isEmpty()) { 314 if (!resolvedKeyframes.isEmpty()) {
291 ASSERT(!activeAnimations || !activeAnimations->isAnimationStyleC hange()); 315 ASSERT(!activeAnimations || !activeAnimations->isAnimationStyleC hange());
292 update->startAnimation(animationName, InertAnimation::create(Ani matableValueKeyframeEffectModel::create(resolvedKeyframes), timing, isPaused, 0) ); 316 auto effect = StringKeyframeEffectModel::create(resolvedKeyframe s);
317 effect->forceConversionsToAnimatableValues(&element); // FIXME: remove this once LegacyStyleInterpolation is removed from StringKeyframe
318 update->startAnimation(animationName, InertAnimation::create(eff ect, timing, isPaused, 0));
293 } 319 }
294 } 320 }
295 } 321 }
296 322
297 ASSERT(inactive.isEmpty() || cssAnimations); 323 ASSERT(inactive.isEmpty() || cssAnimations);
298 for (const AtomicString& animationName : inactive) { 324 for (const AtomicString& animationName : inactive) {
299 ASSERT(!activeAnimations || !activeAnimations->isAnimationStyleChange()) ; 325 ASSERT(!activeAnimations || !activeAnimations->isAnimationStyleChange()) ;
300 update->cancelAnimation(animationName, *cssAnimations->m_animations.get( animationName)); 326 update->cancelAnimation(animationName, *cssAnimations->m_animations.get( animationName));
301 } 327 }
302 } 328 }
(...skipping 447 matching lines...) Expand 10 before | Expand all | Expand 10 after
750 visitor->trace(m_newTransitions); 776 visitor->trace(m_newTransitions);
751 visitor->trace(m_activeInterpolationsForAnimations); 777 visitor->trace(m_activeInterpolationsForAnimations);
752 visitor->trace(m_activeInterpolationsForTransitions); 778 visitor->trace(m_activeInterpolationsForTransitions);
753 visitor->trace(m_newAnimations); 779 visitor->trace(m_newAnimations);
754 visitor->trace(m_suppressedAnimationPlayers); 780 visitor->trace(m_suppressedAnimationPlayers);
755 visitor->trace(m_animationsWithTimingUpdates); 781 visitor->trace(m_animationsWithTimingUpdates);
756 #endif 782 #endif
757 } 783 }
758 784
759 } // namespace blink 785 } // namespace blink
OLDNEW
« Source/core/animation/StringKeyframe.cpp ('K') | « Source/core/animation/StyleInterpolation.h ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698