Frontend Development 18 min read

Creating Laser‑Cutting Effects with Three.js, SVG Logos and Vite

This tutorial demonstrates how to use Three.js, TypeScript and Vite to load an SVG logo, extract its Bézier curve points, generate evenly spaced laser paths, animate multiple lasers with Line2, and finally separate the logo from a floor using shape holes, providing complete source code and visual results.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Creating Laser‑Cutting Effects with Three.js, SVG Logos and Vite

Creating Laser‑Cutting Effects with Three.js, SVG Logos and Vite

This article walks through a step‑by‑step implementation of a laser‑cutting visual effect in a WebGL scene. It covers preparation, SVG loading, curve point extraction, dynamic point distribution, floor creation, laser generation, animation, and logo separation.

Preparation

threejs

TypeScript (ts)

Vite

Start by obtaining an SVG file of a logo (e.g., a bird) and adding it to the scene.

Rendering the SVG

// 加载模型
const loadModel = async () => {
    svgLoader.load('./svg/logo.svg', (data) => {
        const material = new THREE.MeshBasicMaterial({
            color: '#000',
        });
        for (const path of data.paths) {
            const shapes = SVGLoader.createShapes(path);
            for (const shape of shapes) {
                const geometry = new THREE.ShapeGeometry(shape);
                const mesh = new THREE.Mesh(geometry, material);
                scene.add(mesh)
            }
        }
        renderer.setAnimationLoop(render)
    })
}
loadModel()

The loaded shape contains all key points of the logo. These points are later used to build laser motion paths.

Extracting Curve Points

The CubicBezierCurve class provides getPoints(divisions) to sample points along a curve. The default division count is 5.

.getPoints ( divisions : Integer ) : Array divisions -- Number of segments to divide the curve into. Default is 5.

To visualise the points, small cubes are created at each sampled position:

// 加载模型
const loadModel = async () => {
    ...
    for (const curve of shape.curves) {
        const points = curve.getPoints(100);
        console.log(points);
        for (const v2 of points) {
            const geometry = new THREE.BoxGeometry(10, 10, 10);
            const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
            const cube = new THREE.Mesh(geometry, material);
            cube.position.set(v2.x, v2.y, 0)
            scene.add(cube);
        }
    }
    ...
    renderer.setAnimationLoop(render)
}
loadModel()

Because point density varies with curve length, the number of points is adjusted dynamically using curve.getLength() :

const length = curve.getLength();
const points = curve.getPoints(Math.floor(length / 10));

Collecting Point Information

All extracted points are stored in arrays of THREE.Vector2 and later converted to THREE.Vector3 for further processing.

// 新建一个二维数组用于收集组成logo的点位信息
let divisionPoints: THREE.Vector2[] = []
let divisionPoints: THREE.Vector3[] = []
let list: THREE.Vector3[] = []
const length = curve.getLength();
const points = curve.getPoints(Math.floor(length / 20));
for (const v2 of points) {
    v2.divideScalar(20) // 缩小 logo
    const v3 = new THREE.Vector3(v2.x, 0, v2.y)
    list.push(v3)
    divisionPoints.push(v2)
}
paths.push(list)

Creating the Floor and Centering the Logo

const logoSize = new THREE.Vector2()
const logoCenter = new THREE.Vector2()
const floorHeight = 3
let floor: THREE.Mesh | null
let floorOffset = 8

Using the collected points, a bounding box is computed to size the floor and position the logo at the scene centre.

const handlePaths = () => {
    const box2 = new THREE.Box2();
    box2.setFromPoints(divisionPoints)
    box2.getSize(logoSize)
    box2.getCenter(logoCenter)
    createFloor()
}

Laser Generation

Four (or any number) lasers are created. Their start points are placed on a circular arc around the logo, and end points follow the sampled logo points.

// 激光组
const buiGroup = new THREE.Group()
const buiDivide = 3
const buiOffsetH = 30
const buiCount = 10

const createBui = () => {
    var R = Math.min(...logoSize.toArray()) / buiDivide; // 圆弧半径
    var N = buiCount * 10; // 圆弧点数
    const vertices: number[] = []
    for (var i = 0; i < N; i++) {
        var angle = 2 * Math.PI / N * i;
        var x = R * Math.sin(angle);
        var y = R * Math.cos(angle);
        vertices.push(x, buiOffsetH, y)
    }
    initArc(vertices)
    for (let i = 0; i < buiCount; i++) {
        const startPoint = new THREE.Vector3().fromArray(vertices, i * buiCount * 3)
        const endPoint = new THREE.Vector3()
        endPoint.copy(startPoint.clone().setY(-floorHeight))
        const color = new THREE.Color(Math.random() * 0xffffff)
        initCube(startPoint, color)
        initCube(endPoint, color)
    }
}

Using Line2 for Adjustable Width

Because the native linewidth property is limited to 1, the tutorial uses Line2 from three‑js examples.

import { Line2 } from "three/examples/jsm/lines/Line2.js";
import { LineMaterial } from "three/examples/jsm/lines/LineMaterial.js";
import { LineGeometry } from "three/examples/jsm/lines/LineGeometry.js";

const createLine2 = (linePoints: number[]) => {
    const geometry = new LineGeometry();
    geometry.setPositions(linePoints);
    const matLine = new LineMaterial({
        linewidth: 0.002,
        dashed: true,
        opacity: 0.5,
        color: 0x4cb2f8,
        vertexColors: false,
    });
    let biu = new Line2(geometry, matLine);
    biuGroup.add(biu);
}

Laser Animation

The biuAnimate function updates each laser’s endpoint by iterating over the divided logo points, splitting them among the lasers, and feeding the data to Line2 at a fixed interval.

const biuAnimate = () => {
    const allPoints = [...divisionPoints]
    const len = Math.ceil(allPoints.length / biuCount)
    for (let i = 0; i < biuCount; i++) {
        const points = allPoints.splice(0, len);
        const biu = biuGroup.children[i] as Line2;
        const biuStartPoint = biu.userData.startPoint
        let j = 0;
        const interval = setInterval(() => {
            if (j < points.length) {
                const point = points[j]
                const attrPosition = [...biuStartPoint.toArray(), ...new THREE.Vector3(point.x, floorHeight/2, point.y).add(getlogoPos()).toArray()]
                uploadBiuLine(biu, attrPosition)
                j++
            } else {
                clearInterval(interval)
            }
        }, 100)
    }
}

const uploadBiuLine = (line2: Line2, attrPosition) => {
    const geometry = new LineGeometry();
    line2.geometry.setPositions(attrPosition);
}

Drawing the Logo with Laser Paths

As each laser moves, a THREE.Line is updated with the visited points, gradually reconstructing the logo.

for (let i = 0; i < biuCount; i++) {
    const line = createLine()
    scene.add(line)
    const interval = setInterval(() => {
        if (j < points.length) {
            const point = points[j]
            const endArray = new THREE.Vector3(point.x, floorHeight/2, point.y).add(getlogoPos()).toArray()
            const attrPosition = [...biuStartPoint.toArray(), ...endArray]
            // update line geometry
            const logoLinePointArray = [...(line.geometry.attributes['position']?.array || [])];
            logoLinePointArray.push(...endArray)
            line.geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(logoLinePointArray), 3))
            j++
        } else {
            clearInterval(interval)
        }
    }, 100)
}

Logo Separation (Boolean / Shape Hole)

After the laser finishes, the logo is separated from the floor. For simple shapes, threeBSP can be used, but for complex logos the tutorial prefers creating a shape with a hole.

// Create shapes for logo and excess part
const logoShape = new THREE.Shape()
const moreShape = new THREE.Shape()

// In loadModel, build logoShape path
if (i === 0) {
    logoShape.moveTo(v2.x, v2.y)
} else {
    logoShape.lineTo(v2.x, v2.y)
}

// In createFloor, build floor shape and add logo hole
moreShape.moveTo(floorSize.x / 2, floorSize.y / 2)
moreShape.lineTo(-floorSize.x / 2, floorSize.y / 2)
moreShape.lineTo(-floorSize.x / 2, -floorSize.y / 2)
moreShape.lineTo(floorSize.x / 2, -floorSize.y / 2)

const path = new THREE.Path()
divisionPoints.forEach((point, i) => {
    point.add(logoCenter.clone().negate())
    if (i === 0) path.moveTo(point.x, point.y)
    else path.lineTo(point.x, point.y)
})
moreShape.holes.push(path)

// Extrude shapes to meshes
logoMesh = createLogoMesh(logoShape)
moreMesh = createLogoMesh(moreShape)
scene.add(logoMesh)
scene.add(moreMesh)

Final Results

The tutorial showcases the final animated GIFs for several logos (Twitter, Douyin, GitHub) and provides a link to the complete source code repository.

All code versions are referenced as v.logo.1.0.x and can be downloaded from the provided Gitee repository.

TypeScriptSVG3D renderingThree.jsWebGLvitelaser effect
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.