Image Position Misplaced with the Custom controls after Dragging (interact.js integrating)

I have these script that integrate dragging with interact.js but after integrating i cannot get the position of the image right after dragging using interact.js.

Here is the screen recording gif of the problem:
Erorr Screen Recording

Here’s my custom controls code:

    // Function to update red dots
            function updateRedDots(offsetX, offsetY) {
                $(controls[0]).css({ left: offsetX + xOffset, top: offsetY + yOffset }).show();
                $(controls[1]).css({ left: offsetX + xOffset + imagePosition.width - $(controls[1]).width(), top: offsetY + yOffset }).show();
                $(controls[2]).css({ left: offsetX + xOffset + imagePosition.width - $(controls[2]).width(), top: offsetY + yOffset + imagePosition.height - $(controls[2]).height() }).show();
                $(controls[3]).css({ left: offsetX + xOffset, top: offsetY + yOffset + imagePosition.height - $(controls[3]).height() }).show();
            }

I want the image keep attached everytime dragging the image and use the custom controls (skew features).

And here’s my script:

<script>
        var controls = [];
        var canvas;
        var context;
        var image = new Image();
        var triangles = [];
        var dirtyTriangles = true;
        var opacitySlider = document.getElementById('opacityRange');
        var currentOpacity = 1.0;
        var isDragging = false;
        let imagePosition = { width: 0, height: 0 };
        var interactInstance;
        
        $(document).ready(function() {
            canvas = document.getElementById('canvas');
            context = canvas.getContext('2d');
        
            var container = $('#container');
            var containerWidth = container.width();
            var containerHeight = container.height();
        
            canvas.width = containerWidth;
            canvas.height = containerHeight;
            
            // Initialize red dots and blue lines
            for (var i = 0; i < 4; ++i) {
                var control = document.createElement('div');
                control.id = 'node' + i;
                $(control).addClass('node').hide();  // Hide red dots initially
                $('#container').append(control);
                controls.push(control);
            }
            
            // Initialize blue lines
            var lines = ['line1', 'line2', 'line3', 'line4'];
            lines.forEach(function(line) {
                var lineElement = document.createElement('div');
                lineElement.id = line;
                $(lineElement).addClass('blue-line').hide();  // Hide blue lines initially
                $('#container').append(lineElement);
            });
        
            for (var i = 0; i < 4; ++i) {
                var control = document.createElement('div');
                $(control).addClass('node');
                $('#container').append(control);
                controls.push(control);
            }
            
            // Function to update red dots
            function updateRedDots(offsetX, offsetY) {
                $(controls[0]).css({ left: offsetX + xOffset, top: offsetY + yOffset }).show();
                $(controls[1]).css({ left: offsetX + xOffset + imagePosition.width - $(controls[1]).width(), top: offsetY + yOffset }).show();
                $(controls[2]).css({ left: offsetX + xOffset + imagePosition.width - $(controls[2]).width(), top: offsetY + yOffset + imagePosition.height - $(controls[2]).height() }).show();
                $(controls[3]).css({ left: offsetX + xOffset, top: offsetY + yOffset + imagePosition.height - $(controls[3]).height() }).show();
            }
            
            $('#imageUpload').on('change', function(event) {
                var file = event.target.files[0];
                var reader = new FileReader();
                reader.onload = function(e) {
                    image.src = e.target.result;
                    image.onload = function() {
                        imagePosition.width = 120;
                        imagePosition.height = image.height * (120 / image.width);
            
                        xOffset = (canvas.width - imagePosition.width) / 2;
                        yOffset = (canvas.height - imagePosition.height) / 2;
            
                        // Clear canvas and redraw the image
                        context.clearRect(0, 0, canvas.width, canvas.height);
                        context.drawImage(image, xOffset, yOffset, imagePosition.width, imagePosition.height);
            
                        // Update red dots positions initially
                        updateRedDots(0, 0);
            
                        $('.opacity-slider').show(); // Show opacity slider
                        $('#downloadButton').show(); // Show download button
            
                        setInterval(draw, 500 / 30);
            
                        // Initialize interact.js instance for the canvas
                        if (interactInstance) {
                            interactInstance.unset();
                        }
            
                        interactInstance = interact('#canvas')
                            .draggable({
                                listeners: {
                                    start() {
                                        isDragging = true;
                                    },
                                    move(event) {
                                        if (isDragging) {
                                            const x = (parseFloat(canvas.getAttribute('data-x')) || 0) + event.dx;
                                            const y = (parseFloat(canvas.getAttribute('data-y')) || 0) + event.dy;
            
                                            canvas.style.transform = `translate(${x}px, ${y}px)`;
                                            canvas.setAttribute('data-x', x);
                                            canvas.setAttribute('data-y', y);
            
                                            // Update red dots' positions
                                            updateRedDots(x, y);
                                        }
                                    },
                                    end() {
                                        isDragging = false;
                                    }
                                }
                            })
                            .resizable({
                                edges: { left: true, right: true, bottom: true, top: true },
                                listeners: {
                                    start() {
                                        isDragging = true;
                                    },
                                    move(event) {
                                        if (isDragging) {
                                            const x = parseFloat(canvas.getAttribute('data-x')) || 0;
                                            const y = parseFloat(canvas.getAttribute('data-y')) || 0;
            
                                            imagePosition.width = event.rect.width;
                                            imagePosition.height = event.rect.height;
            
                                            const newX = x + event.deltaRect.left;
                                            const newY = y + event.deltaRect.top;
            
                                            canvas.style.transform = `translate(${newX}px, ${newY}px)`;
                                            canvas.setAttribute('data-x', newX);
                                            canvas.setAttribute('data-y', newY);
            
                                            // Clear canvas and redraw the image
                                            context.clearRect(0, 0, canvas.width, canvas.height);
                                            context.drawImage(image, xOffset, yOffset, imagePosition.width, imagePosition.height);
            
                                            // Update red dots' positions
                                            updateRedDots(newX, newY);
                                        }
                                    },
                                    end() {
                                        isDragging = false;
                                    }
                                }
                            });
            
                        // Update red dots' positions
                        updateRedDots(0, 0);
                    };
                };
                reader.readAsDataURL(file);
            });
            
            /*function draw() {
                context.clearRect(0, 0, canvas.width, canvas.height);
                var xOffset = (canvas.width - imagePosition.width) / 2;
                var yOffset = (canvas.height - imagePosition.height) / 2;
                context.drawImage(image, xOffset, yOffset, imagePosition.width, imagePosition.height);
                updateRedDots(xOffset, yOffset);
            }*/
            
            $('#dragButton').on('click', function() {
                enableDragMode();
            });
            
            $('#skewButton').on('click', function() {
                enableSkewMode();
            });
            
            function enableDragMode() {
                disableSkewMode();
                $('#dragButton').addClass('active');
                interactInstance = interact('#container')
                    .draggable({
                        listeners: {
                            move(event) {
                                const target = event.target;
                                const x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx;
                                const y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy;
            
                                target.style.transform = `translate(${x}px, ${y}px)`;
                                target.setAttribute('data-x', x);
                                target.setAttribute('data-y', y);
                            }
                        }
                    });
            }
            
            function enableSkewMode() {
                disableDragMode();
                $('#skewButton').addClass('active');
                interactInstance = interact('#container')
                    .resizable({
                        edges: { left: true, right: true, bottom: true, top: true },
                        listeners: {
                            move(event) {
                                const target = event.target;
                                let x = parseFloat(target.getAttribute('data-x')) || 0;
                                let y = parseFloat(target.getAttribute('data-y')) || 0;
            
                                target.style.width = `${event.rect.width}px`;
                                target.style.height = `${event.rect.height}px`;
            
                                x += event.deltaRect.left;
                                y += event.deltaRect.top;
            
                                target.style.transform = `translate(${x}px, ${y}px)`;
                                target.setAttribute('data-x', x);
                                target.setAttribute('data-y', y);
                            }
                        }
                    });
            }
            
            function disableDragMode() {
                $('#dragButton').removeClass('active');
                if (interactInstance) {
                    interactInstance.unset();
                }
            }
            
            function disableSkewMode() {
                $('#skewButton').removeClass('active');
                if (interactInstance) {
                    interactInstance.unset();
                }
            }
        
            // Function to handle the dragging of nodes
            function handleDrag(node, offsetX, offsetY) {
                $(document).on('mousemove touchmove', function(e) {
                    e.preventDefault(); // Prevent default scrolling behavior on touch devices
                    e.stopPropagation(); // Prevent event from propagating to parent elements
            
                    var x = (e.pageX || e.originalEvent.touches[0].pageX) - $('#container').offset().left - offsetX;
                    var y = (e.pageY || e.originalEvent.touches[0].pageY) - $('#container').offset().top - offsetY;
            
                    // Update node position
                    $(node).css({
                        'left': x + 'px',
                        'top': y + 'px'
                    });
            
                    dirtyTriangles = true;
                    drawLines();
                });
            
                $(document).on('mouseup touchend', function() {
                    $(document).off('mousemove touchmove');
                    $(document).off('mouseup touchend');
                });
            }
            
            // Mouse down event
            $('body').on('mousedown touchstart', function(e) {
                if ($(e.target).hasClass('node')) {
                    var node = e.target;
                    var offsetX = e.pageX - $(node).offset().left;
                    var offsetY = e.pageY - $(node).offset().top;
                    handleDrag(node, offsetX, offsetY);
                }
            });
            
            // CSS to prevent selection during dragging
            $('body').on('mousedown touchstart', function(e) {
                if ($(e.target).hasClass('node')) {
                    e.preventDefault(); // Prevent text selection and other default behaviors
                }
            });
        
            opacitySlider.addEventListener('input', function() {
                currentOpacity = opacitySlider.value / 100;
                draw();
            });
        
            document.getElementById('downloadButton').addEventListener('click', function() {
                downloadImage();
            });
            
            function drawLines() {
                var controlPositions = [
                    { x: parseInt($(controls[0]).css('left')), y: parseInt($(controls[0]).css('top')) },
                    { x: parseInt($(controls[1]).css('left')), y: parseInt($(controls[1]).css('top')) },
                    { x: parseInt($(controls[2]).css('left')), y: parseInt($(controls[2]).css('top')) },
                    { x: parseInt($(controls[3]).css('left')), y: parseInt($(controls[3]).css('top')) }
                ];
            
                var lineElements = ['line1', 'line2', 'line3', 'line4'];
            
                lineElements.forEach(function(line, index) {
                    var lineElement = document.getElementById(line);
                    var nextIndex = (index + 1) % 4;
                    var startPos = controlPositions[index];
                    var endPos = controlPositions[nextIndex];
            
                    var length = Math.sqrt(Math.pow(endPos.x - startPos.x, 2) + Math.pow(endPos.y - startPos.y, 2));
                    var angle = Math.atan2(endPos.y - startPos.y, endPos.x - startPos.x) * 180 / Math.PI;
            
                    $(lineElement).css({
                        width: length + 'px',
                        transform: 'rotate(' + angle + 'deg)',
                        transformOrigin: '0 0',  // Rotate around the start point
                        left: startPos.x + 'px',
                        top: startPos.y + 'px'
                    }).show();
                });
            }
            
            $(document).ready(function() {
                // Toggle instructions visibility
                $('#toggleInstructions').on('click', function() {
                    var instructions = $('#instructions');
                    if (instructions.is(':visible')) {
                        instructions.hide();
                        $(this).text('Lihat Petunjuk');
                    } else {
                        instructions.show();
                        $(this).text('Sembunyikan Petunjuk');
                    }
                });
            });
            
        });
        
        var draw = function() {
            context.clearRect(0, 0, canvas.width, canvas.height);
        
            var render = function(wireframe, image, tri) {
                if (wireframe) {
                    context.strokeStyle = "black";
                    context.beginPath();
                    context.moveTo(tri.p0.x, tri.p0.y);
                    context.lineTo(tri.p1.x, tri.p1.y);
                    context.lineTo(tri.p2.x, tri.p2.y);
                    context.lineTo(tri.p0.x, tri.p0.y);
                    context.stroke();
                    context.closePath();
                }
        
                if (image) {
                    context.globalAlpha = currentOpacity;
                    drawTriangle(context, image,
                        tri.p0.x, tri.p0.y,
                        tri.p1.x, tri.p1.y,
                        tri.p2.x, tri.p2.y,
                        tri.t0.u, tri.t0.v,
                        tri.t1.u, tri.t1.v,
                        tri.t2.u, tri.t2.v);
                }
            };
        
            if (dirtyTriangles) {
                dirtyTriangles = false;
                calculateGeometry();
            }
        
            for (triangle of triangles) {
                render(false, image, triangle);
            }
        };
        
        var calculateGeometry = function() {
            triangles = [];
        
            var subs = 5;
            var divs = 5;
        
            var p1 = new Point(parseInt($(controls[0]).css('left')), parseInt($(controls[0]).css('top')));
            var p2 = new Point(parseInt($(controls[1]).css('left')), parseInt($(controls[1]).css('top')));
            var p3 = new Point(parseInt($(controls[2]).css('left')), parseInt($(controls[2]).css('top')));
            var p4 = new Point(parseInt($(controls[3]).css('left')), parseInt($(controls[3]).css('top')));
        
            var dx1 = p4.x - p1.x;
            var dy1 = p4.y - p1.y;
            var dx2 = p3.x - p2.x;
            var dy2 = p3.y - p2.y;
        
            var imgW = image.width;
            var imgH = image.height;
        
            for (var sub = 0; sub < subs; ++sub) {
                var curRow = sub / subs;
                var nextRow = (sub + 1) / subs;
        
                var curRowX1 = p1.x + dx1 * curRow;
                var curRowY1 = p1.y + dy1 * curRow;
        
                var curRowX2 = p2.x + dx2 * curRow;
                var curRowY2 = p2.y + dy2 * curRow;
        
                var nextRowX1 = p1.x + dx1 * nextRow;
                var nextRowY1 = p1.y + dy1 * nextRow;
        
                var nextRowX2 = p2.x + dx2 * nextRow;
                var nextRowY2 = p2.y + dy2 * nextRow;
        
                for (var div = 0; div < divs; ++div) {
                    var curCol = div / divs;
                    var nextCol = (div + 1) / divs;
        
                    var dCurX = curRowX2 - curRowX1;
                    var dCurY = curRowY2 - curRowY1;
                    var dNextX = nextRowX2 - nextRowX1;
                    var dNextY = nextRowY2 - nextRowY1;
        
                    var p1x = curRowX1 + dCurX * curCol;
                    var p1y = curRowY1 + dCurY * curCol;
        
                    var p2x = curRowX1 + (curRowX2 - curRowX1) * nextCol;
                    var p2y = curRowY1 + (curRowY2 - curRowY1) * nextCol;
        
                    var p3x = nextRowX1 + dNextX * nextCol;
                    var p3y = nextRowY1 + dNextY * nextCol;
        
                    var p4x = nextRowX1 + dNextX * curCol;
                    var p4y = nextRowY1 + dNextY * curCol;
        
                    var u1 = curCol * imgW;
                    var u2 = nextCol * imgW;
                    var v1 = curRow * imgH;
                    var v2 = nextRow * imgH;
        
                    var triangle1 = new Triangle(
                        new Point(p1x-1, p1y),
                        new Point(p3x+2, p3y+1),
                        new Point(p4x-1, p4y+1),
                        new TextCoord(u1, v1),
                        new TextCoord(u2, v2),
                        new TextCoord(u1, v2)
                    );
                    
                    var triangle2 = new Triangle(
                        new Point(p1x-2, p1y),
                        new Point(p2x+1, p2y),
                        new Point(p3x+1, p3y+1),
                        new TextCoord(u1, v1),
                        new TextCoord(u2, v1),
                        new TextCoord(u2, v2)
                    );
        
                    triangles.push(triangle1);
                    triangles.push(triangle2);
                }
            }
        };
        
        var drawTriangle = function(ctx, im, x0, y0, x1, y1, x2, y2,
            sx0, sy0, sx1, sy1, sx2, sy2) {
            ctx.save();
        
            ctx.beginPath();
            ctx.moveTo(x0, y0);
            ctx.lineTo(x1, y1);
            ctx.lineTo(x2, y2);
            ctx.closePath();
            ctx.clip();
        
            var denom = sx0 * (sy2 - sy1) - sx1 * sy2 + sx2 * sy1 + (sx1 - sx2) * sy0;
            if (denom == 0) {
                return;
            }
            var m11 = -(sy0 * (x2 - x1) - sy1 * x2 + sy2 * x1 + (sy1 - sy2) * x0) / denom;
            var m12 = (sy1 * y2 + sy0 * (y1 - y2) - sy2 * y1 + (sy2 - sy1) * y0) / denom;
            var m21 = (sx0 * (x2 - x1) - sx1 * x2 + sx2 * x1 + (sx1 - sx2) * x0) / denom;
            var m22 = -(sx1 * y2 + sx0 * (y1 - y2) - sx2 * y1 + (sx2 - sx1) * y0) / denom;
            var dx = (sx0 * (sy2 * x1 - sy1 * x2) + sy0 * (sx1 * x2 - sx2 * x1) + (sx2 * sy1 - sx1 * sy2) * x0) / denom;
            var dy = (sx0 * (sy2 * y1 - sy1 * y2) + sy0 * (sx1 * y2 - sx2 * y1) + (sx2 * sy1 - sx1 * sy2) * y0) / denom;
        
            ctx.transform(m11, m12, m21, m22, dx, dy);
        
            ctx.drawImage(im, 0, 0);
            ctx.restore();
        };
        
        var Point = function(x,y) {
            this.x = x ? x : 0;
            this.y = y ? y : 0;
        }
        
        var TextCoord = function(u,v) {
            this.u = u ? u : 0;
            this.v = v ? v : 0;
        }
        
        var Triangle = function(p0, p1, p2, t0, t1, t2) {
            this.p0 = p0;
            this.p1 = p1;
            this.p2 = p2;
            this.t0 = t0;
            this.t1 = t1;
            this.t2 = t2;
        }
        
        function downloadImage() {
            // Create a new canvas to combine the container background and the canvas image
            var downloadCanvas = document.createElement('canvas');
            var downloadContext = downloadCanvas.getContext('2d');
            downloadCanvas.width = canvas.width;
            downloadCanvas.height = canvas.height;
        
            // Draw the container background
            var containerBg = new Image();
            containerBg.src = $('#container').css('background-image').replace(/url(['"]?(.*?)['"]?)/, '$1');
            containerBg.onload = function() {
                downloadContext.drawImage(containerBg, 0, 0, canvas.width, canvas.height);
        
                // Draw the canvas image
                downloadContext.drawImage(canvas, 0, 0);
        
                // Create a link element
                var link = document.createElement('a');
                link.download = 'mockup_hasil.png';
                link.href = downloadCanvas.toDataURL('image/png');
                link.click();
            };
        }
        
        /*ZOOM FUNCTION1*/
        let zoomFactor = 1; // Initial zoom factor
        let isZoomed = false; // Flag to track zoom state
        let skewX = 0; // Initial skew values
        let skewY = 0;
        
        // Function to handle zoom on mouse wheel
        function zoom(e) {
            e.preventDefault();
            const zoomAmount = 0.1; // Amount to zoom each level
        
            if (e.deltaY < 0) {
                zoomFactor += zoomAmount; // Zoom in
            } else {
                zoomFactor -= zoomAmount; // Zoom out
                if (zoomFactor < 1) zoomFactor = 1; // Prevent zooming out too far
            }
        
            updateTransform(e.clientX, e.clientY); // Pass the mouse coordinates to adjust zoom
        }
        
        // Function to update the transform CSS property
        function updateTransform(x, y) {
            const container = document.getElementById('container');
            const containerRect = container.getBoundingClientRect();
            
            // Calculate the position relative to the container
            const offsetX = (x - containerRect.left) / containerRect.width;
            const offsetY = (y - containerRect.top) / containerRect.height;
        
            container.style.transform = `scale(${zoomFactor}) skew(${skewX}deg, ${skewY}deg)`;
            container.style.transformOrigin = `${offsetX * 100}% ${offsetY * 100}%`; // Adjust transform origin based on the click position
        }
        
        // Function to toggle zoom
        function toggleZoom(e) {
            const container = document.getElementById('container');
            
            const rect = container.getBoundingClientRect();
            const x = e.clientX - rect.left; // Get click position relative to container
            const y = e.clientY - rect.top;
        
            if (isZoomed) {
                // Zoom out
                zoomFactor = 1;
                container.classList.remove('zoom-out-cursor');
                container.classList.add('zoom-in-cursor');
            } else {
                // Zoom in
                zoomFactor = 1.5;
                container.classList.remove('zoom-in-cursor');
                container.classList.add('zoom-out-cursor');
            }
            updateTransform(x + rect.left, y + rect.top); // Pass the adjusted coordinates
            isZoomed = !isZoomed; // Toggle zoom state
        }
        
        // Attach event listener to the canvas
        document.getElementById('canvas').addEventListener('click', toggleZoom);
        
        // Function to update skew values
        function updateSkew(newSkewX, newSkewY) {
            skewX = newSkewX;
            skewY = newSkewY;
            updateTransform();
        }
        
        // Attach zoom functionality to the container-wrapper
        document.getElementById('container-wrapper').addEventListener('wheel', zoom);
        
        // Toggle zoom on button click
        document.getElementById('zoom-button').addEventListener('click', function() {
            toggleZoom({ clientX: window.innerWidth / 2, clientY: window.innerHeight / 2 }); // Default to center if no event
        });
        
        // Click outside the container to close zoom
        document.addEventListener('click', function(event) {
            if (isZoomed && !event.target.closest('#container-wrapper, #zoom-button')) {
                // Reset zoom
                zoomFactor = 1;
                updateTransform();
                isZoomed = false;
            }
        });
        
        // Ensure the zoom is applied correctly when clicking inside the container-wrapper
        document.getElementById('container-wrapper').addEventListener('click', function(e) {
            e.stopPropagation(); // Prevent the click event from bubbling up to the document
        });
        
        // Example function to update skew and reapply transformations
        function setSkew(x, y) {
            updateSkew(x, y); // Update skew values and adjust transformations
        }
    </script>

I’ve tried to register the updateRedDots(x, y); to the interact.js dragging. But still no luck.

New contributor

setiawanfarlin is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật