• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hosuaby / Leaflet.SmoothMarkerBouncing / #42

pending completion
#42

push

hosuaby
FIX #51: bouncing N times when exclisive=true

67 of 120 branches covered (55.83%)

Branch coverage included in aggregate %.

6 of 6 new or added lines in 2 files covered. (100.0%)

172 of 213 relevant lines covered (80.75%)

35.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

61.96
/src/BouncingMotionCss3.js
1
import {DomUtil} from 'leaflet';
3✔
2
import {calculateLine} from './line';
3✔
3
import './bouncing.css';
3✔
4
import BouncingOptions from './BouncingOptions';
3✔
5
import Styles from './Styles';
3✔
6

7
const animationNamePrefix = 'l-smooth-marker-bouncing-';
3✔
8
const moveAnimationName = animationNamePrefix + 'move';
3✔
9
const contractAnimationName = animationNamePrefix + 'contract';
3✔
10

11
/*
12
 * CSS3 animation runs faster than transform-based animation. We need to reduce speed in order
13
 * to be compatible with old API.
14
 */
15
const speedCoefficient = 0.8;
3✔
16

17
/**
18
 * Removes and then resets required classes on the HTML element.
19
 * Used as hack to restart CSS3 animation.
20
 *
21
 * @param element {HTMLElement}  HTML element
22
 * @param classes {string[]}  names of classes
23
 */
24
function resetClasses(element, classes) {
25
    classes.forEach((className) => DomUtil.removeClass(element, className));
8✔
26
    void element.offsetWidth;
8✔
27
    classes.forEach((className) => DomUtil.addClass(element, className));
8✔
28
}
29

30
export default class BouncingMotionCss3 {
31
    marker;
32
    position;
33
    bouncingOptions;
34
    isBouncing = false;
35
    iconStyles;
36
    shadowStyles;
37
    bouncingAnimationPlaying = false;
38
    onMotionEnd;
39
    #lastAnimationName = contractAnimationName;
40
    #classes = ['bouncing'];
41
    #eventCounter;
42
    #times;
43
    #listener = (event) => this.onAnimationEnd(event);
×
44

45
    /**
46
     * Constructor.
47
     *
48
     * @param marker {Marker}  marker
49
     * @param position {Point}  marker current position on the map canvas
50
     * @param bouncingOptions {BouncingOptions}  options of bouncing animation
51
     */
52
    constructor(marker, position, bouncingOptions) {
105✔
53
        this.marker = marker;
7✔
54
        this.position = position;
7✔
55
        this.updateBouncingOptions(bouncingOptions);
7✔
56
    }
57

58
    updateBouncingOptions(options) {
59
        this.bouncingOptions = options instanceof BouncingOptions
9✔
60
                ? options
9✔
61
                : this.bouncingOptions.override(options);
62

63
        if (this.bouncingOptions.elastic && this.bouncingOptions.contractHeight) {
9!
64
            this.#lastAnimationName = contractAnimationName;
9✔
65
            const index = this.#classes.indexOf('simple');
9✔
66
            if (index > -1) {
9!
67
                this.#classes.splice(index, 1);
×
68
            }
69

70
            if (this.marker._icon) {
9!
71
                DomUtil.removeClass(this.marker._icon, 'simple');
×
72
            }
73
        } else {
74
            this.#lastAnimationName = moveAnimationName;
×
75
            this.#classes.push('simple');
×
76
        }
77

78
        if (this.marker._icon) {
9!
79
            this.resetStyles(this.marker);
×
80
        }
81
    }
82

83
    onAnimationEnd(event) {
×
84
        if (event.animationName === this.#lastAnimationName) {
×
85
            this.#eventCounter++;
×
86
            this.#eventCounter %= 2;
×
87

88
            if (!this.#eventCounter) {
×
89
                if (this.isBouncing && (this.#times === null || --this.#times)) {
×
90
                    resetClasses(this.marker._icon, this.#classes);
×
91
                    if (this.marker._shadow && this.bouncingOptions.shadowAngle) {
×
92
                        resetClasses(this.marker._shadow, this.#classes);
×
93
                    }
94
                } else {
95
                    this.#classes.forEach((className) => {
×
96
                        DomUtil.removeClass(this.marker._icon, className);
×
97
                        if (this.marker._shadow) {
×
98
                            DomUtil.removeClass(this.marker._shadow, className);
×
99
                        }
100
                    });
101
                    this.bouncingAnimationPlaying = false;
×
102

103
                    if (this.onMotionEnd) {
×
104
                        this.onMotionEnd();
×
105
                        this.onMotionEnd = null;
×
106
                    }
107

108
                    this.marker.fire('bounceend');
×
109
                }
110
            }
111
        }
112
    }
113

114
    resetStyles(marker) {
14✔
115
        this.marker = marker;
14✔
116
        this.iconStyles = Styles.ofMarker(marker);
14✔
117

118
        if (marker._shadow) {
14!
119
            this.shadowStyles = Styles.parse(marker._shadow.style.cssText);
14✔
120
        }
121

122
        const iconHeight = this.marker.getIcon()?.options?.iconSize[1]
14!
123
                || this.marker?._iconObj?.options?.iconSize[1];
124

125
        const iconAnimationParams = BouncingMotionCss3.animationParams(
14✔
126
                this.position, this.bouncingOptions, iconHeight);
127

128
        this.iconStyles = this.iconStyles.withStyles(iconAnimationParams);
14✔
129
        this.marker._icon.style.cssText = this.iconStyles.toString();
14✔
130

131
        if (this.bouncingAnimationPlaying) {
14!
132
            resetClasses(this.marker._icon, this.#classes);
×
133
            this.marker._icon.addEventListener('animationend', this.#listener);
×
134
        }
135

136
        const {bounceHeight, contractHeight, shadowAngle} = this.bouncingOptions;
14✔
137

138
        if (this.marker._shadow) {
14!
139
            if (shadowAngle) {
14!
140
                const {x, y} = this.position;
14✔
141
                const points = calculateLine(x, y, shadowAngle, bounceHeight + 1);
14✔
142
                const [posXJump, posYJump] = points[bounceHeight];
42✔
143

144
                const shadowHeight = this.marker.getIcon()?.options?.shadowSize[1];
14!
145
                const shadowScaleContract = BouncingMotionCss3.contractScale(
14✔
146
                        shadowHeight, contractHeight);
147

148
                this.shadowStyles = this.shadowStyles
14✔
149
                        .withStyles(iconAnimationParams)
150
                        .withStyles({
151
                            '--pos-x-jump': `${posXJump}px`,
152
                            '--pos-y-jump': `${posYJump}px`,
153
                            '--scale-contract': shadowScaleContract,
154
                        });
155
                this.marker._shadow.style.cssText = this.shadowStyles.toString();
14✔
156

157
                if (this.bouncingAnimationPlaying) {
14!
158
                    resetClasses(this.marker._shadow, this.#classes);
×
159
                }
160
            } else {
161
                this.#classes.forEach(className => {
×
162
                    DomUtil.removeClass(this.marker._shadow, className);
×
163
                });
164
            }
165
        }
166
    }
167

168
    bounce(times = null) {
5!
169
        this.#times = times;
5✔
170
        this.isBouncing = true;
5✔
171

172
        if (this.bouncingAnimationPlaying) {
5✔
173
            return;
1✔
174
        }
175

176
        this.#eventCounter = 0;
4✔
177
        this.bouncingAnimationPlaying = true;
4✔
178

179
        resetClasses(this.marker._icon, this.#classes);
4✔
180
        if (this.marker._shadow && this.bouncingOptions.shadowAngle) {
4!
181
            resetClasses(this.marker._shadow, this.#classes);
4✔
182
        }
183

184
        this.marker._icon.addEventListener('animationend', this.#listener);
4✔
185
    }
186

187
    stopBouncing() {
188
        this.isBouncing = false;
4✔
189
    }
190

191
    /**
192
     * Calculates parameters of CSS3 animation of bouncing.
193
     *
194
     * @param position {Point}  marker current position on the map canvas
195
     * @param bouncingOptions {BouncingOptions}  options of bouncing animation
196
     * @param height {number}  icons height
197
     * @return {object} CSS3 animation parameters
198
     */
199
    static animationParams(position, bouncingOptions, height) {
200
        const {x, y} = position;
15✔
201
        const {bounceHeight, contractHeight, bounceSpeed, contractSpeed} = bouncingOptions;
15✔
202

203
        const scaleContract = BouncingMotionCss3.contractScale(height, contractHeight);
15✔
204
        const durationJump = BouncingMotionCss3.calculateDuration(bounceHeight, bounceSpeed);
15✔
205
        const durationContract = BouncingMotionCss3.calculateDuration(contractHeight, contractSpeed);
15✔
206

207
        const delays = [
15✔
208
            0,
209
            durationJump,
210
            durationJump * 2,
211
            durationJump * 2 + durationContract
212
        ];
213

214
        return {
15✔
215
            '--pos-x': `${x}px`,
216
            '--pos-y': `${y}px`,
217
            '--pos-y-jump': `${y - bounceHeight}px`,
218
            '--pos-y-contract': `${y + contractHeight}px`,
219
            '--scale-contract': scaleContract,
220
            '--duration-jump': `${durationJump}ms`,
221
            '--duration-contract': `${durationContract}ms`,
222
            '--delays': `0ms, ${delays[1]}ms, ${delays[2]}ms, ${delays[3]}ms`
223
        };
224
    }
225

226
    /**
227
     * Calculates scale of contracting.
228
     *
229
     * @param {number} height  original height
230
     * @param {number} contractHeight  how much it must contract
231
     * @return {number}  contracting scale between 0 and 1
232
     */
233
    static contractScale(height, contractHeight) {
234
        return (height - contractHeight) / height;
31✔
235
    }
236

237
    /**
238
     * Calculates duration of animation.
239
     *
240
     * @param height {number}  height of movement or resizing (px)
241
     * @param speed {number}  speed coefficient
242
     *
243
     * @return {number} duration of animation (ms)
244
     */
245
    static calculateDuration(height, speed) {
246
        if (height === 0) {
34✔
247
            return 0;
1✔
248
        }
249

250
        let duration = Math.round(speed * speedCoefficient);
33✔
251
        let i = height;
33✔
252

253
        while (--i) {
33✔
254
            duration += Math.round(speed / (height - i));
589✔
255
        }
256

257
        return duration;
33✔
258
    }
259
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc