PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.86
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.86
51.1.86 51.1.84 51.1.85 51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 All 40 releases
king-addons / includes / widgets / Product_360_Viewer / Product_360_Viewer.php

Product_360_Viewer.php in King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder 51.1.86, at includes/widgets/Product_360_Viewer/Product_360_Viewer.php

1,253 lines 41.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * 360 Product Viewer Widget (Free).
4 *
5 * @package King_Addons
6 */
7
8 namespace King_Addons;
9
10 use Elementor\Controls_Manager;
11 use Elementor\Group_Control_Border;
12 use Elementor\Group_Control_Box_Shadow;
13 use Elementor\Group_Control_Typography;
14 use Elementor\Repeater;
15 use Elementor\Utils;
16 use Elementor\Widget_Base;
17
18 if (!defined('ABSPATH')) {
19 exit;
20 }
21
22 /**
23 * Renders the 360 Product Viewer widget.
24 */
25 class Product_360_Viewer extends Widget_Base
26 {
27 private const FREE_MAX_FRAMES = 36;
28 private const DEFAULT_ASPECT_RATIO = 1.2;
29 private const DEFAULT_DRAG_SENSITIVITY = 8;
30 private const DEFAULT_AUTOPLAY_SPEED = 90;
31 private const DEFAULT_ZOOM_SCALE = 1.35;
32 private const DEFAULT_INERTIA = 0.08;
33
34 /**
35 * Widget slug.
36 *
37 * @return string
38 */
39 public function get_name(): string
40 {
41 return 'king-addons-product-360-viewer';
42 }
43
44 /**
45 * Widget title.
46 *
47 * @return string
48 */
49 public function get_title(): string
50 {
51 return esc_html__('360° Product Viewer', 'king-addons');
52 }
53
54 /**
55 * Widget icon.
56 *
57 * @return string
58 */
59 public function get_icon(): string
60 {
61 return 'king-addons-icon king-addons-product-360-viewer';
62 }
63
64 /**
65 * Script dependencies.
66 *
67 * @return array<int, string>
68 */
69 public function get_script_depends(): array
70 {
71 return [
72 KING_ADDONS_ASSETS_UNIQUE_KEY . '-product-360-viewer-script',
73 ];
74 }
75
76 /**
77 * Style dependencies.
78 *
79 * @return array<int, string>
80 */
81 public function get_style_depends(): array
82 {
83 return [
84 KING_ADDONS_ASSETS_UNIQUE_KEY . '-product-360-viewer-style',
85 ];
86 }
87
88 /**
89 * Widget categories.
90 *
91 * @return array<int, string>
92 */
93 public function get_categories(): array
94 {
95 return ['king-addons-woo'];
96 }
97
98 /**
99 * Widget keywords.
100 *
101 * @return array<int, string>
102 */
103 public function get_keywords(): array
104 {
105 return ['360', '3d', 'viewer', 'product', 'spin', 'rotate'];
106 }
107
108 public function get_custom_help_url()
109 {
110 return 'mailto:[email protected]?subject=Bug Report - King Addons&body=Please describe the issue';
111 }
112
113 /**
114 * Register Elementor controls.
115 *
116 * @return void
117 */
118 public function register_controls(): void
119 {
120 $this->register_content_frames_controls(false);
121 $this->register_behavior_controls(false);
122 $this->register_ui_controls(false);
123 $this->register_hotspot_controls(false);
124 $this->register_style_viewer_controls();
125 $this->register_style_loader_controls();
126 $this->register_style_hotspot_controls(false);
127 $this->register_pro_notice_controls();
128 }
129
130 /**
131 * Render widget output.
132 *
133 * @return void
134 */
135 public function render(): void
136 {
137 $settings = $this->get_settings_for_display();
138 $frames = $this->prepare_frames($settings, false);
139
140 if (empty($frames)) {
141 return;
142 }
143
144 $hotspots = $this->prepare_hotspots($settings, false, count($frames));
145
146 $this->render_output($settings, $frames, $hotspots, false);
147 }
148
149 /**
150 * Register frames repeater controls.
151 *
152 * @param bool $is_pro Whether Pro controls should be unlocked.
153 *
154 * @return void
155 */
156 public function register_content_frames_controls(bool $is_pro): void
157 {
158 $this->start_controls_section(
159 'kng_frames_section',
160 [
161 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Frames', 'king-addons'),
162 'tab' => Controls_Manager::TAB_CONTENT,
163 ]
164 );
165
166 $repeater = new Repeater();
167
168 $repeater->add_control(
169 'kng_frame_image',
170 [
171 'label' => esc_html__('Frame Image', 'king-addons'),
172 'type' => Controls_Manager::MEDIA,
173 'default' => [
174 'url' => Utils::get_placeholder_image_src(),
175 ],
176 ]
177 );
178
179 $repeater->add_control(
180 'kng_frame_label',
181 [
182 'label' => esc_html__('Label (optional)', 'king-addons'),
183 'type' => Controls_Manager::TEXT,
184 'label_block' => true,
185 'default' => esc_html__('Frame', 'king-addons'),
186 ]
187 );
188
189 $repeater->add_control(
190 'kng_frame_alt',
191 [
192 'label' => esc_html__('Alt Text (optional)', 'king-addons'),
193 'type' => Controls_Manager::TEXT,
194 'label_block' => true,
195 ]
196 );
197
198 $this->add_control(
199 'kng_frames',
200 [
201 'label' => esc_html__('Frames', 'king-addons'),
202 'type' => Controls_Manager::REPEATER,
203 'fields' => $repeater->get_controls(),
204 'default' => $this->get_default_frames(),
205 'title_field' => '{{ kng_frame_label }}',
206 ]
207 );
208
209 if (!$is_pro) {
210 $this->add_control(
211 'kng_frames_limit_notice',
212 [
213 'type' => Controls_Manager::RAW_HTML,
214 'raw' => sprintf(
215 /* translators: %d: maximum frames in the free version. */
216 esc_html__('Free version renders up to %d frames. Add more with Pro.', 'king-addons'),
217 self::FREE_MAX_FRAMES
218 ),
219 'content_classes' => 'king-addons-pro-notice',
220 ]
221 );
222 }
223
224 $this->end_controls_section();
225 }
226
227 /**
228 * Register behavior and motion controls.
229 *
230 * @param bool $is_pro Whether Pro controls should be unlocked.
231 *
232 * @return void
233 */
234 public function register_behavior_controls(bool $is_pro): void
235 {
236 $this->start_controls_section(
237 'kng_behavior_section',
238 [
239 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Viewer Behavior', 'king-addons'),
240 'tab' => Controls_Manager::TAB_CONTENT,
241 ]
242 );
243
244 $this->add_responsive_control(
245 'kng_aspect_ratio',
246 [
247 'label' => esc_html__('Aspect Ratio', 'king-addons'),
248 'type' => Controls_Manager::SLIDER,
249 'size_units' => ['px'],
250 'range' => [
251 'px' => [
252 'min' => 0.5,
253 'max' => 2.5,
254 'step' => 0.05,
255 ],
256 ],
257 'default' => [
258 'size' => self::DEFAULT_ASPECT_RATIO,
259 ],
260 ]
261 );
262
263 $this->add_control(
264 'kng_start_frame',
265 [
266 'label' => esc_html__('Start From Frame', 'king-addons'),
267 'type' => Controls_Manager::NUMBER,
268 'min' => 1,
269 'max' => 200,
270 'step' => 1,
271 'default' => 1,
272 ]
273 );
274
275 $this->add_control(
276 'kng_autoplay',
277 [
278 'label' => esc_html__('Autoplay', 'king-addons'),
279 'type' => Controls_Manager::SWITCHER,
280 'return_value' => 'yes',
281 'default' => 'yes',
282 ]
283 );
284
285 $this->add_control(
286 'kng_autoplay_speed',
287 [
288 'label' => esc_html__('Autoplay Speed (ms/frame)', 'king-addons'),
289 'type' => Controls_Manager::SLIDER,
290 'size_units' => ['px'],
291 'range' => [
292 'px' => [
293 'min' => 20,
294 'max' => 400,
295 'step' => 5,
296 ],
297 ],
298 'default' => [
299 'size' => self::DEFAULT_AUTOPLAY_SPEED,
300 ],
301 'condition' => [
302 'kng_autoplay' => 'yes',
303 ],
304 ]
305 );
306
307 $this->add_control(
308 'kng_autoplay_mode',
309 [
310 'label' => sprintf(
311 /* translators: %s: pro icon. */
312 esc_html__('Autoplay Mode %s', 'king-addons'),
313 '<i class="eicon-pro-icon"></i>'
314 ),
315 'type' => Controls_Manager::SELECT,
316 'options' => [
317 'loop' => esc_html__('Loop', 'king-addons'),
318 'pingpong' => esc_html__('Ping Pong (Pro)', 'king-addons'),
319 ],
320 'default' => 'loop',
321 'classes' => 'king-addons-pro-control no-distance',
322 ]
323 );
324
325 $this->add_control(
326 'kng_drag_sensitivity',
327 [
328 'label' => esc_html__('Drag Sensitivity (px/frame)', 'king-addons'),
329 'type' => Controls_Manager::SLIDER,
330 'size_units' => ['px'],
331 'range' => [
332 'px' => [
333 'min' => 2,
334 'max' => 40,
335 'step' => 1,
336 ],
337 ],
338 'default' => [
339 'size' => self::DEFAULT_DRAG_SENSITIVITY,
340 ],
341 ]
342 );
343
344 $this->add_control(
345 'kng_invert_drag',
346 [
347 'label' => esc_html__('Invert Drag Direction', 'king-addons'),
348 'type' => Controls_Manager::SWITCHER,
349 'return_value' => 'yes',
350 'default' => 'no',
351 ]
352 );
353
354 $this->add_control(
355 'kng_drag_axis',
356 [
357 'label' => sprintf(
358 /* translators: %s: pro icon. */
359 esc_html__('Drag Axis %s', 'king-addons'),
360 '<i class="eicon-pro-icon"></i>'
361 ),
362 'type' => Controls_Manager::SELECT,
363 'options' => [
364 'horizontal' => esc_html__('Horizontal', 'king-addons'),
365 'vertical' => esc_html__('Vertical (Pro)', 'king-addons'),
366 'both' => esc_html__('Both (Pro)', 'king-addons'),
367 ],
368 'default' => 'horizontal',
369 'classes' => 'king-addons-pro-control no-distance',
370 ]
371 );
372
373 $this->add_control(
374 'kng_scroll_wheel',
375 [
376 'label' => sprintf(
377 /* translators: %s: pro icon. */
378 esc_html__('Scroll to Rotate %s', 'king-addons'),
379 '<i class="eicon-pro-icon"></i>'
380 ),
381 'type' => Controls_Manager::SWITCHER,
382 'return_value' => 'yes',
383 'default' => 'no',
384 'classes' => 'king-addons-pro-control no-distance',
385 ]
386 );
387
388 $this->add_control(
389 'kng_inertia',
390 [
391 'label' => sprintf(
392 /* translators: %s: pro icon. */
393 esc_html__('Inertia %s', 'king-addons'),
394 '<i class="eicon-pro-icon"></i>'
395 ),
396 'type' => Controls_Manager::SLIDER,
397 'size_units' => ['px'],
398 'range' => [
399 'px' => [
400 'min' => 0,
401 'max' => 0.3,
402 'step' => 0.01,
403 ],
404 ],
405 'default' => [
406 'size' => self::DEFAULT_INERTIA,
407 ],
408 'classes' => 'king-addons-pro-control no-distance',
409 ]
410 );
411
412 $this->end_controls_section();
413 }
414
415 /**
416 * Register UI controls for overlays and toolbar.
417 *
418 * @param bool $is_pro Whether Pro controls should be unlocked.
419 *
420 * @return void
421 */
422 public function register_ui_controls(bool $is_pro): void
423 {
424 $this->start_controls_section(
425 'kng_ui_section',
426 [
427 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Viewer UI', 'king-addons'),
428 'tab' => Controls_Manager::TAB_CONTENT,
429 ]
430 );
431
432 $this->add_control(
433 'kng_show_hint',
434 [
435 'label' => esc_html__('Show Drag Hint', 'king-addons'),
436 'type' => Controls_Manager::SWITCHER,
437 'return_value' => 'yes',
438 'default' => 'yes',
439 ]
440 );
441
442 $this->add_control(
443 'kng_hint_text',
444 [
445 'label' => esc_html__('Hint Text', 'king-addons'),
446 'type' => Controls_Manager::TEXT,
447 'default' => esc_html__('Drag or swipe to rotate', 'king-addons'),
448 'label_block' => true,
449 'condition' => [
450 'kng_show_hint' => 'yes',
451 ],
452 ]
453 );
454
455 $this->add_control(
456 'kng_show_loader',
457 [
458 'label' => esc_html__('Show Loader', 'king-addons'),
459 'type' => Controls_Manager::SWITCHER,
460 'return_value' => 'yes',
461 'default' => 'yes',
462 ]
463 );
464
465 $this->add_control(
466 'kng_loader_text',
467 [
468 'label' => esc_html__('Loader Text', 'king-addons'),
469 'type' => Controls_Manager::TEXT,
470 'default' => esc_html__('Loading frames...', 'king-addons'),
471 'label_block' => true,
472 'condition' => [
473 'kng_show_loader' => 'yes',
474 ],
475 ]
476 );
477
478 $this->add_control(
479 'kng_show_progress',
480 [
481 'label' => esc_html__('Show Progress', 'king-addons'),
482 'type' => Controls_Manager::SWITCHER,
483 'return_value' => 'yes',
484 'default' => 'yes',
485 ]
486 );
487
488 $this->add_control(
489 'kng_show_zoom',
490 [
491 'label' => sprintf(
492 /* translators: %s: pro icon. */
493 esc_html__('Zoom Toggle %s', 'king-addons'),
494 '<i class="eicon-pro-icon"></i>'
495 ),
496 'type' => Controls_Manager::SWITCHER,
497 'return_value' => 'yes',
498 'default' => 'no',
499 'classes' => 'king-addons-pro-control no-distance',
500 ]
501 );
502
503 $this->add_control(
504 'kng_zoom_scale',
505 [
506 'label' => sprintf(
507 /* translators: %s: pro icon. */
508 esc_html__('Zoom Scale %s', 'king-addons'),
509 '<i class="eicon-pro-icon"></i>'
510 ),
511 'type' => Controls_Manager::SLIDER,
512 'size_units' => ['px'],
513 'range' => [
514 'px' => [
515 'min' => 1.05,
516 'max' => 2.5,
517 'step' => 0.05,
518 ],
519 ],
520 'default' => [
521 'size' => self::DEFAULT_ZOOM_SCALE,
522 ],
523 'classes' => 'king-addons-pro-control no-distance',
524 'condition' => [
525 'kng_show_zoom' => 'yes',
526 ],
527 'selectors' => [
528 '{{WRAPPER}} .king-addons-product-360-viewer' => '--ka-360-zoom-scale: {{SIZE}};',
529 ],
530 ]
531 );
532
533 $this->end_controls_section();
534 }
535
536 /**
537 * Register hotspot controls.
538 *
539 * @param bool $is_pro Whether Pro controls should be unlocked.
540 *
541 * @return void
542 */
543 public function register_hotspot_controls(bool $is_pro): void
544 {
545 $this->start_controls_section(
546 'kng_hotspots_section',
547 [
548 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Hotspots', 'king-addons'),
549 'tab' => Controls_Manager::TAB_CONTENT,
550 ]
551 );
552
553 if (!$is_pro) {
554 $this->add_control(
555 'kng_hotspots_notice',
556 [
557 'type' => Controls_Manager::RAW_HTML,
558 'raw' => esc_html__('Frame-based hotspots are available in the Pro version.', 'king-addons'),
559 'content_classes' => 'king-addons-pro-notice',
560 ]
561 );
562 $this->end_controls_section();
563 return;
564 }
565
566 $hotspot_repeater = new Repeater();
567
568 $hotspot_repeater->add_control(
569 'kng_hotspot_label',
570 [
571 'label' => esc_html__('Label', 'king-addons'),
572 'type' => Controls_Manager::TEXT,
573 'default' => esc_html__('Detail', 'king-addons'),
574 'label_block' => true,
575 ]
576 );
577
578 $hotspot_repeater->add_control(
579 'kng_hotspot_frame',
580 [
581 'label' => esc_html__('Visible On Frame', 'king-addons'),
582 'type' => Controls_Manager::NUMBER,
583 'min' => 1,
584 'max' => 500,
585 'step' => 1,
586 'default' => 1,
587 ]
588 );
589
590 $hotspot_repeater->add_control(
591 'kng_hotspot_horizontal',
592 [
593 'label' => esc_html__('Horizontal (%)', 'king-addons'),
594 'type' => Controls_Manager::SLIDER,
595 'size_units' => ['%'],
596 'range' => [
597 '%' => [
598 'min' => 0,
599 'max' => 100,
600 'step' => 1,
601 ],
602 ],
603 'default' => [
604 'size' => 50,
605 'unit' => '%',
606 ],
607 ]
608 );
609
610 $hotspot_repeater->add_control(
611 'kng_hotspot_vertical',
612 [
613 'label' => esc_html__('Vertical (%)', 'king-addons'),
614 'type' => Controls_Manager::SLIDER,
615 'size_units' => ['%'],
616 'range' => [
617 '%' => [
618 'min' => 0,
619 'max' => 100,
620 'step' => 1,
621 ],
622 ],
623 'default' => [
624 'size' => 50,
625 'unit' => '%',
626 ],
627 ]
628 );
629
630 $hotspot_repeater->add_control(
631 'kng_hotspot_link',
632 [
633 'label' => esc_html__('Link (optional)', 'king-addons'),
634 'type' => Controls_Manager::URL,
635 'label_block' => true,
636 'placeholder' => esc_html__('https://example.com', 'king-addons'),
637 'dynamic' => [
638 'active' => true,
639 ],
640 ]
641 );
642
643 $this->add_control(
644 'kng_hotspots',
645 [
646 'label' => esc_html__('Hotspots', 'king-addons'),
647 'type' => Controls_Manager::REPEATER,
648 'fields' => $hotspot_repeater->get_controls(),
649 'title_field' => '{{ kng_hotspot_label }}',
650 ]
651 );
652
653 $this->end_controls_section();
654 }
655
656 /**
657 * Register viewer style controls.
658 *
659 * @return void
660 */
661 public function register_style_viewer_controls(): void
662 {
663 $this->start_controls_section(
664 'kng_style_viewer_section',
665 [
666 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Viewer', 'king-addons'),
667 'tab' => Controls_Manager::TAB_STYLE,
668 ]
669 );
670
671 $this->add_responsive_control(
672 'kng_viewer_padding',
673 [
674 'label' => esc_html__('Padding', 'king-addons'),
675 'type' => Controls_Manager::DIMENSIONS,
676 'size_units' => ['px', '%', 'em'],
677 'selectors' => [
678 '{{WRAPPER}} .king-addons-product-360-viewer' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
679 ],
680 ]
681 );
682
683 $this->add_control(
684 'kng_viewer_background',
685 [
686 'label' => esc_html__('Background', 'king-addons'),
687 'type' => Controls_Manager::COLOR,
688 'selectors' => [
689 '{{WRAPPER}} .king-addons-product-360-viewer' => 'background-color: {{VALUE}};',
690 ],
691 ]
692 );
693
694 $this->add_responsive_control(
695 'kng_viewer_radius',
696 [
697 'label' => esc_html__('Border Radius', 'king-addons'),
698 'type' => Controls_Manager::SLIDER,
699 'size_units' => ['px', '%'],
700 'range' => [
701 'px' => [
702 'min' => 0,
703 'max' => 100,
704 'step' => 1,
705 ],
706 '%' => [
707 'min' => 0,
708 'max' => 50,
709 'step' => 1,
710 ],
711 ],
712 'selectors' => [
713 '{{WRAPPER}} .king-addons-product-360-viewer' => 'border-radius: {{SIZE}}{{UNIT}};',
714 ],
715 ]
716 );
717
718 $this->add_group_control(
719 Group_Control_Border::get_type(),
720 [
721 'name' => 'kng_viewer_border',
722 'selector' => '{{WRAPPER}} .king-addons-product-360-viewer',
723 ]
724 );
725
726 $this->add_group_control(
727 Group_Control_Box_Shadow::get_type(),
728 [
729 'name' => 'kng_viewer_shadow',
730 'selector' => '{{WRAPPER}} .king-addons-product-360-viewer',
731 ]
732 );
733
734 $this->add_control(
735 'kng_image_fit',
736 [
737 'label' => esc_html__('Image Fit', 'king-addons'),
738 'type' => Controls_Manager::SELECT,
739 'options' => [
740 'contain' => esc_html__('Contain', 'king-addons'),
741 'cover' => esc_html__('Cover', 'king-addons'),
742 ],
743 'default' => 'contain',
744 'selectors' => [
745 '{{WRAPPER}} .king-addons-product-360-viewer__frame' => 'object-fit: {{VALUE}};',
746 ],
747 ]
748 );
749
750 $this->end_controls_section();
751 }
752
753 /**
754 * Register style controls for overlays and text.
755 *
756 * @return void
757 */
758 public function register_style_loader_controls(): void
759 {
760 $this->start_controls_section(
761 'kng_style_overlay_section',
762 [
763 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Overlays & Toolbar', 'king-addons'),
764 'tab' => Controls_Manager::TAB_STYLE,
765 ]
766 );
767
768 $this->add_group_control(
769 Group_Control_Typography::get_type(),
770 [
771 'name' => 'kng_hint_typography',
772 'selector' => '{{WRAPPER}} .king-addons-product-360-viewer__hint, {{WRAPPER}} .king-addons-product-360-viewer__progress',
773 ]
774 );
775
776 $this->add_control(
777 'kng_hint_color',
778 [
779 'label' => esc_html__('Text Color', 'king-addons'),
780 'type' => Controls_Manager::COLOR,
781 'selectors' => [
782 '{{WRAPPER}} .king-addons-product-360-viewer__hint' => 'color: {{VALUE}};',
783 ],
784 ]
785 );
786
787 $this->add_control(
788 'kng_hint_background',
789 [
790 'label' => esc_html__('Hint Background', 'king-addons'),
791 'type' => Controls_Manager::COLOR,
792 'selectors' => [
793 '{{WRAPPER}} .king-addons-product-360-viewer__hint' => 'background-color: {{VALUE}};',
794 ],
795 ]
796 );
797
798 $this->add_control(
799 'kng_loader_color',
800 [
801 'label' => esc_html__('Loader Text Color', 'king-addons'),
802 'type' => Controls_Manager::COLOR,
803 'selectors' => [
804 '{{WRAPPER}} .king-addons-product-360-viewer__loader' => 'color: {{VALUE}};',
805 ],
806 ]
807 );
808
809 $this->add_control(
810 'kng_loader_background',
811 [
812 'label' => esc_html__('Loader Background', 'king-addons'),
813 'type' => Controls_Manager::COLOR,
814 'selectors' => [
815 '{{WRAPPER}} .king-addons-product-360-viewer__loader' => 'background-color: {{VALUE}};',
816 ],
817 ]
818 );
819
820 $this->add_responsive_control(
821 'kng_loader_padding',
822 [
823 'label' => esc_html__('Loader Padding', 'king-addons'),
824 'type' => Controls_Manager::DIMENSIONS,
825 'size_units' => ['px', 'em', '%'],
826 'selectors' => [
827 '{{WRAPPER}} .king-addons-product-360-viewer__loader' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
828 ],
829 ]
830 );
831
832 $this->add_control(
833 'kng_progress_background',
834 [
835 'label' => esc_html__('Progress Background', 'king-addons'),
836 'type' => Controls_Manager::COLOR,
837 'selectors' => [
838 '{{WRAPPER}} .king-addons-product-360-viewer__progress' => 'background-color: {{VALUE}};',
839 ],
840 ]
841 );
842
843 $this->add_control(
844 'kng_progress_color',
845 [
846 'label' => esc_html__('Progress Text Color', 'king-addons'),
847 'type' => Controls_Manager::COLOR,
848 'selectors' => [
849 '{{WRAPPER}} .king-addons-product-360-viewer__progress' => 'color: {{VALUE}};',
850 ],
851 ]
852 );
853
854 $this->add_control(
855 'kng_toolbar_radius',
856 [
857 'label' => esc_html__('Overlay Radius', 'king-addons'),
858 'type' => Controls_Manager::SLIDER,
859 'size_units' => ['px', '%'],
860 'range' => [
861 'px' => [
862 'min' => 0,
863 'max' => 40,
864 'step' => 1,
865 ],
866 '%' => [
867 'min' => 0,
868 'max' => 50,
869 'step' => 1,
870 ],
871 ],
872 'selectors' => [
873 '{{WRAPPER}} .king-addons-product-360-viewer__hint' => 'border-radius: {{SIZE}}{{UNIT}};',
874 '{{WRAPPER}} .king-addons-product-360-viewer__progress' => 'border-radius: {{SIZE}}{{UNIT}};',
875 '{{WRAPPER}} .king-addons-product-360-viewer__loader' => 'border-radius: {{SIZE}}{{UNIT}};',
876 ],
877 ]
878 );
879
880 $this->end_controls_section();
881 }
882
883 /**
884 * Register hotspot style controls.
885 *
886 * @param bool $is_pro Whether Pro controls should be unlocked.
887 *
888 * @return void
889 */
890 public function register_style_hotspot_controls(bool $is_pro): void
891 {
892 $this->start_controls_section(
893 'kng_style_hotspot_section',
894 [
895 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Hotspot Style', 'king-addons'),
896 'tab' => Controls_Manager::TAB_STYLE,
897 ]
898 );
899
900 if (!$is_pro) {
901 $this->add_control(
902 'kng_style_hotspot_notice',
903 [
904 'type' => Controls_Manager::RAW_HTML,
905 'raw' => esc_html__('Style controls unlock with Pro hotspots.', 'king-addons'),
906 'content_classes' => 'king-addons-pro-notice',
907 ]
908 );
909 $this->end_controls_section();
910 return;
911 }
912
913 $this->add_control(
914 'kng_hotspot_color',
915 [
916 'label' => esc_html__('Dot Color', 'king-addons'),
917 'type' => Controls_Manager::COLOR,
918 'selectors' => [
919 '{{WRAPPER}} .king-addons-product-360-viewer__hotspot-dot' => 'background-color: {{VALUE}};',
920 ],
921 ]
922 );
923
924 $this->add_control(
925 'kng_hotspot_label_color',
926 [
927 'label' => esc_html__('Label Text Color', 'king-addons'),
928 'type' => Controls_Manager::COLOR,
929 'selectors' => [
930 '{{WRAPPER}} .king-addons-product-360-viewer__hotspot-label' => 'color: {{VALUE}};',
931 ],
932 ]
933 );
934
935 $this->add_control(
936 'kng_hotspot_label_background',
937 [
938 'label' => esc_html__('Label Background', 'king-addons'),
939 'type' => Controls_Manager::COLOR,
940 'selectors' => [
941 '{{WRAPPER}} .king-addons-product-360-viewer__hotspot-label' => 'background-color: {{VALUE}};',
942 ],
943 ]
944 );
945
946 $this->add_responsive_control(
947 'kng_hotspot_size',
948 [
949 'label' => esc_html__('Dot Size', 'king-addons'),
950 'type' => Controls_Manager::SLIDER,
951 'size_units' => ['px'],
952 'range' => [
953 'px' => [
954 'min' => 8,
955 'max' => 28,
956 'step' => 1,
957 ],
958 ],
959 'selectors' => [
960 '{{WRAPPER}} .king-addons-product-360-viewer__hotspot' => '--ka-360-hotspot-size: {{SIZE}}{{UNIT}};',
961 ],
962 ]
963 );
964
965 $this->add_group_control(
966 Group_Control_Typography::get_type(),
967 [
968 'name' => 'kng_hotspot_typography',
969 'selector' => '{{WRAPPER}} .king-addons-product-360-viewer__hotspot-label',
970 ]
971 );
972
973 $this->end_controls_section();
974 }
975
976 /**
977 * Render the viewer markup.
978 *
979 * @param array<string, mixed> $settings Widget settings.
980 * @param array<int, array<string, mixed>> $frames Prepared frames.
981 * @param array<int, array<string, mixed>> $hotspots Prepared hotspots.
982 * @param bool $is_pro Whether Pro rendering is active.
983 *
984 * @return void
985 */
986 public function render_output(array $settings, array $frames, array $hotspots, bool $is_pro): void
987 {
988 $frame_count = count($frames);
989 if ($frame_count === 0) {
990 return;
991 }
992
993 $start_frame = $this->clamp_int((int) ($settings['kng_start_frame'] ?? 1), 1, $frame_count);
994 $start_index = $start_frame - 1;
995 $start_frame_data = $frames[$start_index] ?? $frames[0];
996
997 $aspect_ratio = $this->clamp_float((float) ($settings['kng_aspect_ratio']['size'] ?? self::DEFAULT_ASPECT_RATIO), 0.4, 3.5);
998 $autoplay_speed = $this->clamp_float((float) ($settings['kng_autoplay_speed']['size'] ?? self::DEFAULT_AUTOPLAY_SPEED), 10, 1000);
999 $drag_sensitivity = $this->clamp_float((float) ($settings['kng_drag_sensitivity']['size'] ?? self::DEFAULT_DRAG_SENSITIVITY), 2, 80);
1000 $inertia = $is_pro ? $this->clamp_float((float) ($settings['kng_inertia']['size'] ?? self::DEFAULT_INERTIA), 0, 1) : 0;
1001 $zoom_scale = $is_pro ? $this->clamp_float((float) ($settings['kng_zoom_scale']['size'] ?? self::DEFAULT_ZOOM_SCALE), 1.05, 3) : self::DEFAULT_ZOOM_SCALE;
1002
1003 $container_attributes = [
1004 'data-frames' => esc_attr(wp_json_encode($frames)),
1005 'data-start-frame' => esc_attr((string) $start_frame),
1006 'data-aspect-ratio' => esc_attr((string) $aspect_ratio),
1007 'data-autoplay' => esc_attr($settings['kng_autoplay'] ?? 'no'),
1008 'data-autoplay-speed' => esc_attr((string) $autoplay_speed),
1009 'data-autoplay-mode' => esc_attr($is_pro ? ($settings['kng_autoplay_mode'] ?? 'loop') : 'loop'),
1010 'data-drag-sensitivity' => esc_attr((string) $drag_sensitivity),
1011 'data-invert-drag' => esc_attr($settings['kng_invert_drag'] ?? 'no'),
1012 'data-drag-axis' => esc_attr($is_pro ? ($settings['kng_drag_axis'] ?? 'horizontal') : 'horizontal'),
1013 'data-enable-scroll' => esc_attr($is_pro ? ($settings['kng_scroll_wheel'] ?? 'no') : 'no'),
1014 'data-inertia' => esc_attr((string) $inertia),
1015 'data-show-progress' => esc_attr($settings['kng_show_progress'] ?? 'yes'),
1016 'data-show-zoom' => esc_attr($is_pro ? ($settings['kng_show_zoom'] ?? 'no') : 'no'),
1017 'data-zoom-scale' => esc_attr((string) $zoom_scale),
1018 'data-hotspots' => $is_pro ? esc_attr(wp_json_encode($hotspots)) : '[]',
1019 'data-is-pro' => $is_pro ? 'true' : 'false',
1020 ];
1021
1022 $wrapper_classes = ['king-addons-product-360-viewer'];
1023 if (($settings['kng_show_loader'] ?? 'yes') === 'yes') {
1024 $wrapper_classes[] = 'king-addons-product-360-viewer--loading';
1025 }
1026
1027 ?>
1028 <div class="<?php echo esc_attr(implode(' ', $wrapper_classes)); ?>" <?php echo $this->render_attributes_string($container_attributes); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
1029 <div class="king-addons-product-360-viewer__canvas" role="presentation" style="--ka-360-aspect: <?php echo esc_attr((string) $aspect_ratio); ?>;">
1030 <img
1031 class="king-addons-product-360-viewer__frame"
1032 src="<?php echo esc_url($start_frame_data['url']); ?>"
1033 alt="<?php echo esc_attr($start_frame_data['alt'] ?: $start_frame_data['label']); ?>"
1034 loading="lazy"
1035 />
1036 <?php if (($settings['kng_show_loader'] ?? 'yes') === 'yes') : ?>
1037 <div class="king-addons-product-360-viewer__loader" aria-live="polite">
1038 <?php echo esc_html($settings['kng_loader_text'] ?? esc_html__('Loading frames...', 'king-addons')); ?>
1039 </div>
1040 <?php endif; ?>
1041 <?php if (($settings['kng_show_hint'] ?? 'yes') === 'yes') : ?>
1042 <div class="king-addons-product-360-viewer__hint">
1043 <?php echo esc_html($settings['kng_hint_text'] ?? esc_html__('Drag or swipe to rotate', 'king-addons')); ?>
1044 </div>
1045 <?php endif; ?>
1046 <?php if (($settings['kng_show_progress'] ?? 'yes') === 'yes') : ?>
1047 <div class="king-addons-product-360-viewer__progress" aria-live="polite">
1048 <span class="king-addons-product-360-viewer__progress-value"><?php echo esc_html('1 / ' . $frame_count); ?></span>
1049 </div>
1050 <?php endif; ?>
1051 <?php if ($is_pro && ($settings['kng_show_zoom'] ?? 'no') === 'yes') : ?>
1052 <button type="button" class="king-addons-product-360-viewer__zoom" aria-pressed="false">
1053 <?php echo esc_html__('Zoom', 'king-addons'); ?>
1054 </button>
1055 <?php endif; ?>
1056 </div>
1057 <?php if ($is_pro && !empty($hotspots)) : ?>
1058 <div class="king-addons-product-360-viewer__hotspots" aria-hidden="true">
1059 <?php foreach ($hotspots as $hotspot) : ?>
1060 <button
1061 type="button"
1062 class="king-addons-product-360-viewer__hotspot"
1063 data-frame="<?php echo esc_attr((string) $hotspot['frame']); ?>"
1064 data-link="<?php echo esc_url($hotspot['link']); ?>"
1065 data-new-tab="<?php echo esc_attr($hotspot['is_external'] ? 'true' : 'false'); ?>"
1066 data-nofollow="<?php echo esc_attr($hotspot['nofollow'] ? 'true' : 'false'); ?>"
1067 style="left: <?php echo esc_attr((string) $hotspot['x']); ?>%; top: <?php echo esc_attr((string) $hotspot['y']); ?>%;"
1068 >
1069 <span class="king-addons-product-360-viewer__hotspot-dot" aria-hidden="true"></span>
1070 <span class="king-addons-product-360-viewer__hotspot-label"><?php echo esc_html($hotspot['label']); ?></span>
1071 </button>
1072 <?php endforeach; ?>
1073 </div>
1074 <?php endif; ?>
1075 </div>
1076 <?php
1077 }
1078
1079 /**
1080 * Prepare frames respecting free limits and required keys.
1081 *
1082 * @param array<string, mixed> $settings Widget settings.
1083 * @param bool $is_pro Whether Pro rendering is active.
1084 *
1085 * @return array<int, array<string, string>>
1086 */
1087 public function prepare_frames(array $settings, bool $is_pro): array
1088 {
1089 $frames = $settings['kng_frames'] ?? [];
1090 if (!$is_pro) {
1091 $frames = array_slice($frames, 0, self::FREE_MAX_FRAMES);
1092 }
1093
1094 $prepared = [];
1095 foreach ($frames as $frame) {
1096 $url = $frame['kng_frame_image']['url'] ?? '';
1097 if (empty($url)) {
1098 continue;
1099 }
1100
1101 $label = $frame['kng_frame_label'] ?? '';
1102 $alt = $frame['kng_frame_alt'] ?? '';
1103
1104 $prepared[] = [
1105 'url' => esc_url($url),
1106 'label' => sanitize_text_field($label),
1107 'alt' => sanitize_text_field($alt),
1108 ];
1109 }
1110
1111 return $prepared;
1112 }
1113
1114 /**
1115 * Prepare hotspot data.
1116 *
1117 * @param array<string, mixed> $settings Widget settings.
1118 * @param bool $is_pro Whether Pro rendering is active.
1119 * @param int $frame_count Total frame count.
1120 *
1121 * @return array<int, array<string, mixed>>
1122 */
1123 public function prepare_hotspots(array $settings, bool $is_pro, int $frame_count): array
1124 {
1125 if (!$is_pro) {
1126 return [];
1127 }
1128
1129 $hotspots = $settings['kng_hotspots'] ?? [];
1130 $prepared = [];
1131
1132 foreach ($hotspots as $hotspot) {
1133 $frame = $this->clamp_int((int) ($hotspot['kng_hotspot_frame'] ?? 1), 1, $frame_count);
1134 $label = sanitize_text_field($hotspot['kng_hotspot_label'] ?? '');
1135 $x = $this->clamp_float((float) ($hotspot['kng_hotspot_horizontal']['size'] ?? 50), 0, 100);
1136 $y = $this->clamp_float((float) ($hotspot['kng_hotspot_vertical']['size'] ?? 50), 0, 100);
1137
1138 $link = $hotspot['kng_hotspot_link']['url'] ?? '';
1139 $is_external = !empty($hotspot['kng_hotspot_link']['is_external']);
1140 $nofollow = !empty($hotspot['kng_hotspot_link']['nofollow']);
1141
1142 $prepared[] = [
1143 'frame' => $frame,
1144 'label' => $label,
1145 'x' => $x,
1146 'y' => $y,
1147 'link' => esc_url_raw($link),
1148 'is_external' => $is_external,
1149 'nofollow' => $nofollow,
1150 ];
1151 }
1152
1153 return $prepared;
1154 }
1155
1156 /**
1157 * Pro upsell notice.
1158 *
1159 * @return void
1160 */
1161 public function register_pro_notice_controls(): void
1162 {
1163 if (!king_addons_freemius()->can_use_premium_code__premium_only()) {
1164 Core::renderProFeaturesSection($this, '', Controls_Manager::RAW_HTML, 'product-360-viewer', [
1165 'Unlimited frames with ping-pong autoplay',
1166 'Scroll, inertia, and multi-axis drag controls',
1167 'Zoom toggle with adjustable scale',
1168 'Frame-specific hotspots with links and labels',
1169 ]);
1170 }
1171 }
1172
1173 /**
1174 * Provide default frames.
1175 *
1176 * @return array<int, array<string, mixed>>
1177 */
1178 public function get_default_frames(): array
1179 {
1180 $placeholder = Utils::get_placeholder_image_src();
1181 $defaults = [];
1182
1183 for ($i = 1; $i <= 8; $i++) {
1184 $defaults[] = [
1185 'kng_frame_image' => [
1186 'url' => $placeholder,
1187 ],
1188 'kng_frame_label' => sprintf(
1189 /* translators: %d: frame number. */
1190 esc_html__('Frame %d', 'king-addons'),
1191 $i
1192 ),
1193 'kng_frame_alt' => sprintf(
1194 /* translators: %d: frame number. */
1195 esc_html__('Frame %d', 'king-addons'),
1196 $i
1197 ),
1198 ];
1199 }
1200
1201 return $defaults;
1202 }
1203
1204 /**
1205 * Clamp integer values.
1206 *
1207 * @param int $value Value to clamp.
1208 * @param int $min Minimum.
1209 * @param int $max Maximum.
1210 *
1211 * @return int
1212 */
1213 public function clamp_int(int $value, int $min, int $max): int
1214 {
1215 return max($min, min($max, $value));
1216 }
1217
1218 /**
1219 * Clamp float values.
1220 *
1221 * @param float $value Value to clamp.
1222 * @param float $min Minimum.
1223 * @param float $max Maximum.
1224 *
1225 * @return float
1226 */
1227 public function clamp_float(float $value, float $min, float $max): float
1228 {
1229 return max($min, min($max, $value));
1230 }
1231
1232 /**
1233 * Render attributes as HTML string.
1234 *
1235 * @param array<string, string> $attributes Attributes to render.
1236 *
1237 * @return string
1238 */
1239 public function render_attributes_string(array $attributes): string
1240 {
1241 $rendered = [];
1242 foreach ($attributes as $name => $value) {
1243 $rendered[] = sprintf('%s="%s"', esc_attr($name), $value);
1244 }
1245
1246 return implode(' ', $rendered);
1247 }
1248 }
1249
1250
1251
1252
1253