1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "third_party/blink/renderer/core/inspector/inspector_animation_agent.h"
6
7 #include <memory>
8
9 #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
10 #include "third_party/blink/renderer/bindings/core/v8/v8_computed_effect_timing.h"
11 #include "third_party/blink/renderer/bindings/core/v8/v8_optional_effect_timing.h"
12 #include "third_party/blink/renderer/core/animation/animation.h"
13 #include "third_party/blink/renderer/core/animation/animation_effect.h"
14 #include "third_party/blink/renderer/core/animation/css/css_animation.h"
15 #include "third_party/blink/renderer/core/animation/css/css_animations.h"
16 #include "third_party/blink/renderer/core/animation/css/css_transition.h"
17 #include "third_party/blink/renderer/core/animation/document_timeline.h"
18 #include "third_party/blink/renderer/core/animation/effect_model.h"
19 #include "third_party/blink/renderer/core/animation/element_animations.h"
20 #include "third_party/blink/renderer/core/animation/keyframe_effect.h"
21 #include "third_party/blink/renderer/core/animation/keyframe_effect_model.h"
22 #include "third_party/blink/renderer/core/animation/string_keyframe.h"
23 #include "third_party/blink/renderer/core/css/css_keyframe_rule.h"
24 #include "third_party/blink/renderer/core/css/css_keyframes_rule.h"
25 #include "third_party/blink/renderer/core/css/css_rule_list.h"
26 #include "third_party/blink/renderer/core/css/css_style_rule.h"
27 #include "third_party/blink/renderer/core/css/resolver/style_resolver.h"
28 #include "third_party/blink/renderer/core/frame/local_frame.h"
29 #include "third_party/blink/renderer/core/inspector/identifiers_factory.h"
30 #include "third_party/blink/renderer/core/inspector/inspected_frames.h"
31 #include "third_party/blink/renderer/core/inspector/inspector_css_agent.h"
32 #include "third_party/blink/renderer/core/inspector/inspector_style_sheet.h"
33 #include "third_party/blink/renderer/core/inspector/v8_inspector_string.h"
34 #include "third_party/blink/renderer/platform/animation/timing_function.h"
35 #include "third_party/blink/renderer/platform/heap/heap.h"
36 #include "third_party/blink/renderer/platform/wtf/text/base64.h"
37
38 namespace blink {
39
40 namespace {
41
AnimationDisplayName(const Animation & animation)42 String AnimationDisplayName(const Animation& animation) {
43 if (!animation.id().IsEmpty())
44 return animation.id();
45 else if (auto* css_animation = DynamicTo<CSSAnimation>(animation))
46 return css_animation->animationName();
47 else if (auto* css_transition = DynamicTo<CSSTransition>(animation))
48 return css_transition->transitionProperty();
49 else
50 return animation.id();
51 }
52
53 } // namespace
54
55 using protocol::Response;
56
InspectorAnimationAgent(InspectedFrames * inspected_frames,InspectorCSSAgent * css_agent,v8_inspector::V8InspectorSession * v8_session)57 InspectorAnimationAgent::InspectorAnimationAgent(
58 InspectedFrames* inspected_frames,
59 InspectorCSSAgent* css_agent,
60 v8_inspector::V8InspectorSession* v8_session)
61 : inspected_frames_(inspected_frames),
62 css_agent_(css_agent),
63 v8_session_(v8_session),
64 is_cloning_(false),
65 enabled_(&agent_state_, /*default_value=*/false),
66 playback_rate_(&agent_state_, /*default_value=*/1.0) {}
67
Restore()68 void InspectorAnimationAgent::Restore() {
69 if (enabled_.Get()) {
70 instrumenting_agents_->AddInspectorAnimationAgent(this);
71 setPlaybackRate(playback_rate_.Get());
72 }
73 }
74
enable()75 Response InspectorAnimationAgent::enable() {
76 enabled_.Set(true);
77 instrumenting_agents_->AddInspectorAnimationAgent(this);
78 return Response::Success();
79 }
80
disable()81 Response InspectorAnimationAgent::disable() {
82 setPlaybackRate(1.0);
83 for (const auto& clone : id_to_animation_clone_.Values())
84 clone->cancel();
85 enabled_.Clear();
86 instrumenting_agents_->RemoveInspectorAnimationAgent(this);
87 id_to_animation_.clear();
88 id_to_animation_clone_.clear();
89 cleared_animations_.clear();
90 return Response::Success();
91 }
92
DidCommitLoadForLocalFrame(LocalFrame * frame)93 void InspectorAnimationAgent::DidCommitLoadForLocalFrame(LocalFrame* frame) {
94 if (frame == inspected_frames_->Root()) {
95 id_to_animation_.clear();
96 id_to_animation_clone_.clear();
97 cleared_animations_.clear();
98 }
99 setPlaybackRate(playback_rate_.Get());
100 }
101
102 static std::unique_ptr<protocol::Animation::AnimationEffect>
BuildObjectForAnimationEffect(KeyframeEffect * effect)103 BuildObjectForAnimationEffect(KeyframeEffect* effect) {
104 ComputedEffectTiming* computed_timing = effect->getComputedTiming();
105 double delay = computed_timing->delay();
106 double duration = computed_timing->duration().GetAsUnrestrictedDouble();
107 String easing = effect->SpecifiedTiming().timing_function->ToString();
108
109 std::unique_ptr<protocol::Animation::AnimationEffect> animation_object =
110 protocol::Animation::AnimationEffect::create()
111 .setDelay(delay)
112 .setEndDelay(computed_timing->endDelay())
113 .setIterationStart(computed_timing->iterationStart())
114 .setIterations(computed_timing->iterations())
115 .setDuration(duration)
116 .setDirection(computed_timing->direction())
117 .setFill(computed_timing->fill())
118 .setEasing(easing)
119 .build();
120 if (effect->EffectTarget()) {
121 animation_object->setBackendNodeId(
122 IdentifiersFactory::IntIdForNode(effect->EffectTarget()));
123 }
124 return animation_object;
125 }
126
127 static std::unique_ptr<protocol::Animation::KeyframeStyle>
BuildObjectForStringKeyframe(const StringKeyframe * keyframe,double computed_offset)128 BuildObjectForStringKeyframe(const StringKeyframe* keyframe,
129 double computed_offset) {
130 String offset = String::NumberToStringECMAScript(computed_offset * 100) + "%";
131
132 std::unique_ptr<protocol::Animation::KeyframeStyle> keyframe_object =
133 protocol::Animation::KeyframeStyle::create()
134 .setOffset(offset)
135 .setEasing(keyframe->Easing().ToString())
136 .build();
137 return keyframe_object;
138 }
139
140 static std::unique_ptr<protocol::Animation::KeyframesRule>
BuildObjectForAnimationKeyframes(const KeyframeEffect * effect)141 BuildObjectForAnimationKeyframes(const KeyframeEffect* effect) {
142 if (!effect || !effect->Model() || !effect->Model()->IsKeyframeEffectModel())
143 return nullptr;
144 const KeyframeEffectModelBase* model = effect->Model();
145 Vector<double> computed_offsets =
146 KeyframeEffectModelBase::GetComputedOffsets(model->GetFrames());
147 auto keyframes =
148 std::make_unique<protocol::Array<protocol::Animation::KeyframeStyle>>();
149
150 for (wtf_size_t i = 0; i < model->GetFrames().size(); i++) {
151 const Keyframe* keyframe = model->GetFrames().at(i);
152 // Ignore CSS Transitions
153 if (!keyframe->IsStringKeyframe())
154 continue;
155 const auto* string_keyframe = To<StringKeyframe>(keyframe);
156 keyframes->emplace_back(
157 BuildObjectForStringKeyframe(string_keyframe, computed_offsets.at(i)));
158 }
159 return protocol::Animation::KeyframesRule::create()
160 .setKeyframes(std::move(keyframes))
161 .build();
162 }
163
164 std::unique_ptr<protocol::Animation::Animation>
BuildObjectForAnimation(blink::Animation & animation)165 InspectorAnimationAgent::BuildObjectForAnimation(blink::Animation& animation) {
166 String animation_type = AnimationType::WebAnimation;
167 std::unique_ptr<protocol::Animation::AnimationEffect> animation_effect_object;
168
169 if (animation.effect()) {
170 animation_effect_object =
171 BuildObjectForAnimationEffect(To<KeyframeEffect>(animation.effect()));
172
173 if (IsA<CSSTransition>(animation)) {
174 animation_type = AnimationType::CSSTransition;
175 } else {
176 animation_effect_object->setKeyframesRule(
177 BuildObjectForAnimationKeyframes(
178 To<KeyframeEffect>(animation.effect())));
179
180 if (IsA<CSSAnimation>(animation))
181 animation_type = AnimationType::CSSAnimation;
182 }
183 }
184
185 String id = String::Number(animation.SequenceNumber());
186 id_to_animation_.Set(id, &animation);
187
188 std::unique_ptr<protocol::Animation::Animation> animation_object =
189 protocol::Animation::Animation::create()
190 .setId(id)
191 .setName(AnimationDisplayName(animation))
192 .setPausedState(animation.Paused())
193 .setPlayState(animation.PlayStateString())
194 .setPlaybackRate(animation.playbackRate())
195 .setStartTime(NormalizedStartTime(animation))
196 .setCurrentTime(animation.currentTime())
197 .setType(animation_type)
198 .build();
199 if (animation_type != AnimationType::WebAnimation)
200 animation_object->setCssId(CreateCSSId(animation));
201 if (animation_effect_object)
202 animation_object->setSource(std::move(animation_effect_object));
203 return animation_object;
204 }
205
getPlaybackRate(double * playback_rate)206 Response InspectorAnimationAgent::getPlaybackRate(double* playback_rate) {
207 *playback_rate = ReferenceTimeline().PlaybackRate();
208 return Response::Success();
209 }
210
setPlaybackRate(double playback_rate)211 Response InspectorAnimationAgent::setPlaybackRate(double playback_rate) {
212 for (LocalFrame* frame : *inspected_frames_)
213 frame->GetDocument()->Timeline().SetPlaybackRate(playback_rate);
214 playback_rate_.Set(playback_rate);
215 return Response::Success();
216 }
217
getCurrentTime(const String & id,double * current_time)218 Response InspectorAnimationAgent::getCurrentTime(const String& id,
219 double* current_time) {
220 blink::Animation* animation = nullptr;
221 Response response = AssertAnimation(id, animation);
222 if (!response.IsSuccess())
223 return response;
224 if (id_to_animation_clone_.at(id))
225 animation = id_to_animation_clone_.at(id);
226
227 if (animation->Paused() || !animation->timeline()->IsActive()) {
228 *current_time = animation->currentTime();
229 } else {
230 // Use startTime where possible since currentTime is limited.
231 base::Optional<double> timeline_time = animation->timeline()->CurrentTime();
232 // TODO(crbug.com/916117): Handle NaN values for scroll linked animations.
233 *current_time =
234 timeline_time ? timeline_time.value() -
235 animation->startTime().value_or(Timing::NullValue())
236 : Timing::NullValue();
237 }
238 return Response::Success();
239 }
240
setPaused(std::unique_ptr<protocol::Array<String>> animation_ids,bool paused)241 Response InspectorAnimationAgent::setPaused(
242 std::unique_ptr<protocol::Array<String>> animation_ids,
243 bool paused) {
244 for (const String& animation_id : *animation_ids) {
245 blink::Animation* animation = nullptr;
246 Response response = AssertAnimation(animation_id, animation);
247 if (!response.IsSuccess())
248 return response;
249 blink::Animation* clone = AnimationClone(animation);
250 if (!clone)
251 return Response::ServerError("Failed to clone detached animation");
252 if (paused && !clone->Paused()) {
253 // Ensure we restore a current time if the animation is limited.
254 double current_time = 0;
255 if (!clone->timeline()->IsActive()) {
256 current_time = clone->currentTime();
257 } else {
258 base::Optional<double> timeline_time = clone->timeline()->CurrentTime();
259 // TODO(crbug.com/916117): Handle NaN values.
260 current_time =
261 timeline_time ? timeline_time.value() -
262 clone->startTime().value_or(Timing::NullValue())
263 : Timing::NullValue();
264 }
265 clone->pause();
266 clone->setCurrentTime(current_time, false);
267 } else if (!paused && clone->Paused()) {
268 clone->Unpause();
269 }
270 }
271 return Response::Success();
272 }
273
AnimationClone(blink::Animation * animation)274 blink::Animation* InspectorAnimationAgent::AnimationClone(
275 blink::Animation* animation) {
276 const String id = String::Number(animation->SequenceNumber());
277 if (!id_to_animation_clone_.at(id)) {
278 auto* old_effect = To<KeyframeEffect>(animation->effect());
279 DCHECK(old_effect->Model()->IsKeyframeEffectModel());
280 KeyframeEffectModelBase* old_model = old_effect->Model();
281 KeyframeEffectModelBase* new_model = nullptr;
282 // Clone EffectModel.
283 // TODO(samli): Determine if this is an animations bug.
284 if (old_model->IsStringKeyframeEffectModel()) {
285 auto* old_string_keyframe_model =
286 To<StringKeyframeEffectModel>(old_model);
287 KeyframeVector old_keyframes = old_string_keyframe_model->GetFrames();
288 StringKeyframeVector new_keyframes;
289 for (auto& old_keyframe : old_keyframes)
290 new_keyframes.push_back(To<StringKeyframe>(*old_keyframe));
291 new_model =
292 MakeGarbageCollected<StringKeyframeEffectModel>(new_keyframes);
293 } else if (old_model->IsTransitionKeyframeEffectModel()) {
294 auto* old_transition_keyframe_model =
295 To<TransitionKeyframeEffectModel>(old_model);
296 KeyframeVector old_keyframes = old_transition_keyframe_model->GetFrames();
297 TransitionKeyframeVector new_keyframes;
298 for (auto& old_keyframe : old_keyframes)
299 new_keyframes.push_back(To<TransitionKeyframe>(*old_keyframe));
300 new_model =
301 MakeGarbageCollected<TransitionKeyframeEffectModel>(new_keyframes);
302 }
303
304 auto* new_effect = MakeGarbageCollected<KeyframeEffect>(
305 old_effect->EffectTarget(), new_model, old_effect->SpecifiedTiming());
306 is_cloning_ = true;
307 blink::Animation* clone =
308 blink::Animation::Create(new_effect, animation->timeline());
309 is_cloning_ = false;
310 id_to_animation_clone_.Set(id, clone);
311 id_to_animation_.Set(String::Number(clone->SequenceNumber()), clone);
312 clone->play();
313 clone->setStartTime(animation->startTime().value_or(Timing::NullValue()),
314 false);
315
316 animation->SetEffectSuppressed(true);
317 }
318 return id_to_animation_clone_.at(id);
319 }
320
seekAnimations(std::unique_ptr<protocol::Array<String>> animation_ids,double current_time)321 Response InspectorAnimationAgent::seekAnimations(
322 std::unique_ptr<protocol::Array<String>> animation_ids,
323 double current_time) {
324 for (const String& animation_id : *animation_ids) {
325 blink::Animation* animation = nullptr;
326 Response response = AssertAnimation(animation_id, animation);
327 if (!response.IsSuccess())
328 return response;
329 blink::Animation* clone = AnimationClone(animation);
330 if (!clone)
331 return Response::ServerError("Failed to clone a detached animation.");
332 if (!clone->Paused())
333 clone->play();
334 clone->setCurrentTime(current_time, false);
335 }
336 return Response::Success();
337 }
338
releaseAnimations(std::unique_ptr<protocol::Array<String>> animation_ids)339 Response InspectorAnimationAgent::releaseAnimations(
340 std::unique_ptr<protocol::Array<String>> animation_ids) {
341 for (const String& animation_id : *animation_ids) {
342 blink::Animation* animation = id_to_animation_.at(animation_id);
343 if (animation)
344 animation->SetEffectSuppressed(false);
345 blink::Animation* clone = id_to_animation_clone_.at(animation_id);
346 if (clone)
347 clone->cancel();
348 id_to_animation_clone_.erase(animation_id);
349 id_to_animation_.erase(animation_id);
350 cleared_animations_.insert(animation_id);
351 }
352 return Response::Success();
353 }
354
setTiming(const String & animation_id,double duration,double delay)355 Response InspectorAnimationAgent::setTiming(const String& animation_id,
356 double duration,
357 double delay) {
358 blink::Animation* animation = nullptr;
359 Response response = AssertAnimation(animation_id, animation);
360 if (!response.IsSuccess())
361 return response;
362
363 animation = AnimationClone(animation);
364 NonThrowableExceptionState exception_state;
365
366 OptionalEffectTiming* timing = OptionalEffectTiming::Create();
367 UnrestrictedDoubleOrString unrestricted_duration;
368 unrestricted_duration.SetUnrestrictedDouble(duration);
369 timing->setDuration(unrestricted_duration);
370 timing->setDelay(delay);
371 animation->effect()->updateTiming(timing, exception_state);
372 return Response::Success();
373 }
374
resolveAnimation(const String & animation_id,std::unique_ptr<v8_inspector::protocol::Runtime::API::RemoteObject> * result)375 Response InspectorAnimationAgent::resolveAnimation(
376 const String& animation_id,
377 std::unique_ptr<v8_inspector::protocol::Runtime::API::RemoteObject>*
378 result) {
379 blink::Animation* animation = nullptr;
380 Response response = AssertAnimation(animation_id, animation);
381 if (!response.IsSuccess())
382 return response;
383 if (id_to_animation_clone_.at(animation_id))
384 animation = id_to_animation_clone_.at(animation_id);
385 const Element* element =
386 To<KeyframeEffect>(animation->effect())->EffectTarget();
387 Document* document = element->ownerDocument();
388 LocalFrame* frame = document ? document->GetFrame() : nullptr;
389 ScriptState* script_state =
390 frame ? ToScriptStateForMainWorld(frame) : nullptr;
391 if (!script_state)
392 return Response::ServerError("Element not associated with a document.");
393
394 ScriptState::Scope scope(script_state);
395 static const char kAnimationObjectGroup[] = "animation";
396 v8_session_->releaseObjectGroup(
397 ToV8InspectorStringView(kAnimationObjectGroup));
398 *result = v8_session_->wrapObject(
399 script_state->GetContext(),
400 ToV8(animation, script_state->GetContext()->Global(),
401 script_state->GetIsolate()),
402 ToV8InspectorStringView(kAnimationObjectGroup),
403 false /* generatePreview */);
404 if (!*result)
405 return Response::ServerError("Element not associated with a document.");
406 return Response::Success();
407 }
408
CreateCSSId(blink::Animation & animation)409 String InspectorAnimationAgent::CreateCSSId(blink::Animation& animation) {
410 static const CSSProperty* g_animation_properties[] = {
411 &GetCSSPropertyAnimationDelay(),
412 &GetCSSPropertyAnimationDirection(),
413 &GetCSSPropertyAnimationDuration(),
414 &GetCSSPropertyAnimationFillMode(),
415 &GetCSSPropertyAnimationIterationCount(),
416 &GetCSSPropertyAnimationName(),
417 &GetCSSPropertyAnimationTimingFunction(),
418 };
419 static const CSSProperty* g_transition_properties[] = {
420 &GetCSSPropertyTransitionDelay(), &GetCSSPropertyTransitionDuration(),
421 &GetCSSPropertyTransitionProperty(),
422 &GetCSSPropertyTransitionTimingFunction(),
423 };
424
425 auto* effect = To<KeyframeEffect>(animation.effect());
426 Vector<const CSSProperty*> css_properties;
427 if (IsA<CSSAnimation>(animation)) {
428 for (const CSSProperty* property : g_animation_properties)
429 css_properties.push_back(property);
430 } else if (auto* css_transition = DynamicTo<CSSTransition>(animation)) {
431 for (const CSSProperty* property : g_transition_properties)
432 css_properties.push_back(property);
433 css_properties.push_back(&css_transition->TransitionCSSProperty());
434 } else {
435 NOTREACHED();
436 }
437
438 Element* element = effect->EffectTarget();
439 HeapVector<Member<CSSStyleDeclaration>> styles =
440 css_agent_->MatchingStyles(element);
441 Digestor digestor(kHashAlgorithmSha1);
442 digestor.UpdateUtf8(IsA<CSSTransition>(animation)
443 ? AnimationType::CSSTransition
444 : AnimationType::CSSAnimation);
445 digestor.UpdateUtf8(animation.id());
446 for (const CSSProperty* property : css_properties) {
447 CSSStyleDeclaration* style =
448 css_agent_->FindEffectiveDeclaration(*property, styles);
449 // Ignore inline styles.
450 if (!style || !style->ParentStyleSheet() || !style->parentRule() ||
451 style->parentRule()->type() != CSSRule::kStyleRule)
452 continue;
453 digestor.UpdateUtf8(property->GetPropertyNameString());
454 digestor.UpdateUtf8(css_agent_->StyleSheetId(style->ParentStyleSheet()));
455 digestor.UpdateUtf8(To<CSSStyleRule>(style->parentRule())->selectorText());
456 }
457 DigestValue digest_result;
458 digestor.Finish(digest_result);
459 DCHECK(!digestor.has_failed());
460 return Base64Encode(base::make_span(digest_result).first<10>());
461 }
462
DidCreateAnimation(unsigned sequence_number)463 void InspectorAnimationAgent::DidCreateAnimation(unsigned sequence_number) {
464 if (is_cloning_)
465 return;
466 GetFrontend()->animationCreated(String::Number(sequence_number));
467 }
468
AnimationPlayStateChanged(blink::Animation * animation,blink::Animation::AnimationPlayState old_play_state,blink::Animation::AnimationPlayState new_play_state)469 void InspectorAnimationAgent::AnimationPlayStateChanged(
470 blink::Animation* animation,
471 blink::Animation::AnimationPlayState old_play_state,
472 blink::Animation::AnimationPlayState new_play_state) {
473 const String& animation_id = String::Number(animation->SequenceNumber());
474
475 // We no longer care about animations that have been released.
476 if (cleared_animations_.Contains(animation_id))
477 return;
478
479 // Record newly starting animations only once, as |buildObjectForAnimation|
480 // constructs and caches our internal representation of the given |animation|.
481 if ((new_play_state == blink::Animation::kRunning ||
482 new_play_state == blink::Animation::kFinished) &&
483 !id_to_animation_.Contains(animation_id))
484 GetFrontend()->animationStarted(BuildObjectForAnimation(*animation));
485 else if (new_play_state == blink::Animation::kIdle ||
486 new_play_state == blink::Animation::kPaused)
487 GetFrontend()->animationCanceled(animation_id);
488 }
489
DidClearDocumentOfWindowObject(LocalFrame * frame)490 void InspectorAnimationAgent::DidClearDocumentOfWindowObject(
491 LocalFrame* frame) {
492 if (!enabled_.Get())
493 return;
494 DCHECK(frame->GetDocument());
495 frame->GetDocument()->Timeline().SetPlaybackRate(
496 ReferenceTimeline().PlaybackRate());
497 }
498
AssertAnimation(const String & id,blink::Animation * & result)499 Response InspectorAnimationAgent::AssertAnimation(const String& id,
500 blink::Animation*& result) {
501 result = id_to_animation_.at(id);
502 if (!result)
503 return Response::ServerError("Could not find animation with given id");
504 return Response::Success();
505 }
506
ReferenceTimeline()507 DocumentTimeline& InspectorAnimationAgent::ReferenceTimeline() {
508 return inspected_frames_->Root()->GetDocument()->Timeline();
509 }
510
NormalizedStartTime(blink::Animation & animation)511 double InspectorAnimationAgent::NormalizedStartTime(
512 blink::Animation& animation) {
513 double time_ms = animation.startTime().value_or(Timing::NullValue());
514 auto* document_timeline = DynamicTo<DocumentTimeline>(animation.timeline());
515 if (document_timeline) {
516 if (ReferenceTimeline().PlaybackRate() == 0) {
517 time_ms +=
518 ReferenceTimeline().currentTime() - document_timeline->currentTime();
519 } else {
520 time_ms +=
521 (document_timeline->ZeroTime() - ReferenceTimeline().ZeroTime())
522 .InMillisecondsF() *
523 ReferenceTimeline().PlaybackRate();
524 }
525 }
526 // Round to the closest microsecond.
527 return std::round(time_ms * 1000) / 1000;
528 }
529
Trace(Visitor * visitor)530 void InspectorAnimationAgent::Trace(Visitor* visitor) {
531 visitor->Trace(inspected_frames_);
532 visitor->Trace(css_agent_);
533 visitor->Trace(id_to_animation_);
534 visitor->Trace(id_to_animation_clone_);
535 InspectorBaseAgent::Trace(visitor);
536 }
537
538 } // namespace blink
539