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

hosuaby / Leaflet.SmoothMarkerBouncing / #55

15 Feb 2025 02:33PM UTC coverage: 72.152% (-0.05%) from 72.204%
#55

push

hosuaby
Release 3.1.1

56 of 104 branches covered (53.85%)

Branch coverage included in aggregate %.

172 of 212 relevant lines covered (81.13%)

50.39 hits per line

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

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

7
const animationNamePrefix = 'l-smooth-marker-bouncing-';
4✔
8
const moveAnimationName = animationNamePrefix + 'move';
4✔
9
const contractAnimationName = animationNamePrefix + 'contract';
4✔
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;
4✔
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));
10✔
26
    void element.offsetWidth;
10✔
27
    classes.forEach((className) => DomUtil.addClass(element, className));
10✔
28
}
29

30
export default class BouncingMotionCss3 {
31
    marker;
32
    position;
33
    bouncingOptions;
34
    isBouncing = false;
9✔
35
    iconStyles;
36
    shadowStyles;
37
    bouncingAnimationPlaying = false;
9✔
38
    onMotionEnd;
39
    #lastAnimationName = contractAnimationName;
9✔
40
    #classes = ['bouncing'];
9✔
41
    #eventCounter;
42
    #times;
43
    #listener = (event) => this.onAnimationEnd(event);
9✔
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) {
53
        this.marker = marker;
9✔
54
        this.position = position;
9✔
55
        this.updateBouncingOptions(bouncingOptions);
9✔
56
    }
57

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

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

70
            if (this.marker._icon) {
12!
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) {
12!
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._stopBouncingAnimation();
×
96
                }
97
            }
98
        }
99
    }
100

101
    resetStyles(marker) {
102
        this.marker = marker;
18✔
103
        this.iconStyles = Styles.ofMarker(marker);
18✔
104

105
        if (marker._shadow) {
18!
106
            this.shadowStyles = Styles.parse(marker._shadow.style.cssText);
18✔
107
        }
108

109
        const iconHeight = this.marker.getIcon()?.options?.iconSize[1]
18!
110
                || this.marker?._iconObj?.options?.iconSize[1];
111

112
        const iconAnimationParams = BouncingMotionCss3.animationParams(
18✔
113
                this.position, this.bouncingOptions, iconHeight);
114

115
        this.iconStyles = this.iconStyles.withStyles(iconAnimationParams);
18✔
116
        this.marker._icon.style.cssText = this.iconStyles.toString();
18✔
117

118
        if (this.bouncingAnimationPlaying) {
18!
119
            resetClasses(this.marker._icon, this.#classes);
×
120
            this.marker._icon.addEventListener('animationend', this.#listener);
×
121
        }
122

123
        const {bounceHeight, contractHeight, shadowAngle} = this.bouncingOptions;
18✔
124

125
        if (this.marker._shadow) {
18!
126
            if (shadowAngle) {
18!
127
                const {x, y} = this.position;
18✔
128
                const points = calculateLine(x, y, shadowAngle, bounceHeight + 1);
18✔
129
                const [posXJump, posYJump] = points[bounceHeight];
18✔
130

131
                const shadowHeight = this.marker.getIcon()?.options?.shadowSize[1];
18✔
132
                const shadowScaleContract = BouncingMotionCss3.contractScale(
18✔
133
                        shadowHeight, contractHeight);
134

135
                this.shadowStyles = this.shadowStyles
18✔
136
                        .withStyles(iconAnimationParams)
137
                        .withStyles({
138
                            '--pos-x-jump': `${posXJump}px`,
139
                            '--pos-y-jump': `${posYJump}px`,
140
                            '--scale-contract': shadowScaleContract,
141
                        });
142
                this.marker._shadow.style.cssText = this.shadowStyles.toString();
18✔
143

144
                if (this.bouncingAnimationPlaying) {
18!
145
                    resetClasses(this.marker._shadow, this.#classes);
×
146
                }
147
            } else {
148
                this.#classes.forEach(className => {
×
149
                    DomUtil.removeClass(this.marker._shadow, className);
×
150
                });
151
            }
152
        }
153
    }
154

155
    bounce(times = null) {
×
156
        this.#times = times;
6✔
157
        this.isBouncing = true;
6✔
158

159
        if (this.bouncingAnimationPlaying) {
6✔
160
            return;
1✔
161
        }
162

163
        this.#eventCounter = 0;
5✔
164
        this.bouncingAnimationPlaying = true;
5✔
165

166
        if (this.marker._icon) {
5!
167
            resetClasses(this.marker._icon, this.#classes);
5✔
168
            this.marker._icon.addEventListener('animationend', this.#listener);
5✔
169
        }
170

171
        if (this.marker._shadow && this.bouncingOptions.shadowAngle) {
5!
172
            resetClasses(this.marker._shadow, this.#classes);
5✔
173
        }
174
    }
175

176
    stopBouncing(immediate = false) {
×
177
        this.isBouncing = false;
5✔
178

179
        immediate ||= this.bouncingOptions.immediateStop;
5✔
180
        if (immediate) {
5!
181
            this._stopBouncingAnimation();
×
182
        }
183
    }
184

185
    _stopBouncingAnimation() {
186
        this.#classes.forEach((className) => {
×
187
            DomUtil.removeClass(this.marker._icon, className);
×
188
            if (this.marker._shadow) {
×
189
                DomUtil.removeClass(this.marker._shadow, className);
×
190
            }
191
        });
192
        this.bouncingAnimationPlaying = false;
×
193

194
        if (this.onMotionEnd) {
×
195
            this.onMotionEnd();
×
196
            this.onMotionEnd = null;
×
197
        }
198

199
        this.marker.fire('bounceend');
×
200
    }
201

202
    /**
203
     * Calculates parameters of CSS3 animation of bouncing.
204
     *
205
     * @param position {Point}  marker current position on the map canvas
206
     * @param bouncingOptions {BouncingOptions}  options of bouncing animation
207
     * @param height {number}  icons height
208
     * @return {object} CSS3 animation parameters
209
     */
210
    static animationParams(position, bouncingOptions, height) {
211
        const {x, y} = position;
19✔
212
        const {bounceHeight, contractHeight, bounceSpeed, contractSpeed} = bouncingOptions;
19✔
213

214
        const scaleContract = BouncingMotionCss3.contractScale(height, contractHeight);
19✔
215
        const durationJump = BouncingMotionCss3.calculateDuration(bounceHeight, bounceSpeed);
19✔
216
        const durationContract = BouncingMotionCss3.calculateDuration(contractHeight, contractSpeed);
19✔
217

218
        const delays = [
19✔
219
            0,
220
            durationJump,
221
            durationJump * 2,
222
            durationJump * 2 + durationContract
223
        ];
224

225
        return {
19✔
226
            '--pos-x': `${x}px`,
227
            '--pos-y': `${y}px`,
228
            '--pos-y-jump': `${y - bounceHeight}px`,
229
            '--pos-y-contract': `${y + contractHeight}px`,
230
            '--scale-contract': scaleContract,
231
            '--duration-jump': `${durationJump}ms`,
232
            '--duration-contract': `${durationContract}ms`,
233
            '--delays': `0ms, ${delays[1]}ms, ${delays[2]}ms, ${delays[3]}ms`
234
        };
235
    }
236

237
    /**
238
     * Calculates scale of contracting.
239
     *
240
     * @param {number} height  original height
241
     * @param {number} contractHeight  how much it must contract
242
     * @return {number}  contracting scale between 0 and 1
243
     */
244
    static contractScale(height, contractHeight) {
245
        return (height - contractHeight) / height;
39✔
246
    }
247

248
    /**
249
     * Calculates duration of animation.
250
     *
251
     * @param height {number}  height of movement or resizing (px)
252
     * @param speed {number}  speed coefficient
253
     *
254
     * @return {number} duration of animation (ms)
255
     */
256
    static calculateDuration(height, speed) {
257
        if (height === 0) {
42✔
258
            return 0;
1✔
259
        }
260

261
        let duration = Math.round(speed * speedCoefficient);
41✔
262
        let i = height;
41✔
263

264
        while (--i) {
41✔
265
            duration += Math.round(speed / (height - i));
859✔
266
        }
267

268
        return duration;
41✔
269
    }
270
}
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