简体   繁体   中英

How to smoothly scroll repeating linear gradient on canvas 2D?

I'm using context.createLinearGradient to create gradients, and to make it scroll I'm animating the colorStops. But the issue is when a color reaches the end, if I wrap it around back to start the whole gradient changes.

In CSS I could avoid this using repeating-linear-gradient and it would work but I havent figured out a way to do this without the sudden color changes at the edges. I tried drawing it a little bit offscreen but It still off.

This is what I have so far:

 const colors = [ { color: "#FF0000", pos: 0 }, { color: "#FFFF00", pos: 1 / 5 }, { color: "#00FF00", pos: 2 / 5 }, { color: "#0000FF", pos: 3 / 5 }, { color: "#FF00FF", pos: 4 / 5 }, { color: "#FF0000", pos: 1 }, ]; const angleStep = 0.2; const linearStep = 0.001; function init() { const canvas = document.querySelector("canvas"); const context = canvas.getContext("2d"); const mw = canvas.width; const mh = canvas.height; let angle = 0; function drawScreen() { angle = (angle + angleStep) % 360; const [x1, y1, x2, y2] = angleToPoints(angle, mw, mh); const gradient = context.createLinearGradient(x1, y1, x2, y2); for (const colorStop of colors) { gradient.addColorStop(colorStop.pos, colorStop.color); colorStop.pos += linearStep; if (colorStop.pos > 1) colorStop.pos = 0; } context.fillStyle = gradient; context.fillRect(0, 0, canvas.width, canvas.height); } function loop() { drawScreen() window.requestAnimationFrame(loop); } loop(); } function angleToPoints(angle, width, height){ const rad = ((180 - angle) / 180) * Math.PI; // This computes the length such that the start/stop points will be at the corners const length = Math.abs(width * Math.sin(rad)) + Math.abs(height * Math.cos(rad)); // Compute the actual x,y points based on the angle, length of the gradient line and the center of the div const halfx = (Math.sin(rad) * length) / 2.0 const halfy = (Math.cos(rad) * length) / 2.0 const cx = width / 2.0 const cy = height / 2.0 const x1 = cx - halfx const y1 = cy - halfy const x2 = cx + halfx const y2 = cy + halfy return [x1, y1, x2, y2]; } init();
 html,body, canvas { width: 100%; height: 100%; margin: 0; padding: 0; }
 <canvas width="128" height="72"></canvas>

The problem is that the gradients you create don't usually have stops at 0 or 1 . When a gradient doesn't have those stops, the ends get filled out by whatever the color is of the closest stop.

To fill them in the way you want, you'd need to figure out what the color at the crossover point should be and add it to both ends.

Below, we determine the current end colors by sorting and then use linear interpolation (lerp) to get the crossover color. I've prefixed my meaningful changes with comments that start with // ### .

 // ### lerp for hexadecimal color strings function lerpColor(a, b, amount) { const ah = +a.replace('#', '0x'), ar = ah >> 16, ag = ah >> 8 & 0xff, ab = ah & 0xff, bh = +b.replace('#', '0x'), br = bh >> 16, bg = bh >> 8 & 0xff, bb = bh & 0xff, rr = ar + amount * (br - ar), rg = ag + amount * (bg - ag), rb = ab + amount * (bb - ab) ; return '#' + (0x1000000 + (rr << 16) + (rg << 8) + rb | 0).toString(16).slice(1); } const colors = [ { color: "#FF0000", pos: 0 }, { color: "#FFFF00", pos: 1 / 5 }, { color: "#00FF00", pos: 2 / 5 }, { color: "#0000FF", pos: 3 / 5 }, { color: "#FF00FF", pos: 4 / 5 }, { color: "#FF0000", pos: 1 }, ]; const angleStep = 0.2; const linearStep = 0.005; function init() { const canvas = document.querySelector("canvas"); const context = canvas.getContext("2d"); const mw = canvas.width; const mh = canvas.height; let angle = 0; function drawScreen() { angle = (angle + angleStep) % 360; const [x1, y1, x2, y2] = angleToPoints(angle, mw, mh); const gradient = context.createLinearGradient(x1, y1, x2, y2); for (const colorStop of colors) { gradient.addColorStop(colorStop.pos, colorStop.color); colorStop.pos += linearStep; // ### corrected error here if (colorStop.pos > 1) colorStop.pos -= 1; } // ### compute and set the gradient end stops const sortedStops = colors.sort((a,b) => a.pos - b.pos); const firstStop = sortedStops[0]; const lastStop = sortedStops.slice(-1)[0]; const endColor = lerpColor(firstStop.color, lastStop.color, firstStop.pos*5); gradient.addColorStop(0, endColor); gradient.addColorStop(1, endColor); context.fillStyle = gradient; context.fillRect(0, 0, canvas.width, canvas.height); } function loop() { drawScreen() requestAnimationFrame(loop) } loop(); } function angleToPoints(angle, width, height){ const rad = ((180 - angle) / 180) * Math.PI; // This computes the length such that the start/stop points will be at the corners const length = Math.abs(width * Math.sin(rad)) + Math.abs(height * Math.cos(rad)); // Compute the actual x,y points based on the angle, length of the gradient line and the center of the div const halfx = (Math.sin(rad) * length) / 2.0 const halfy = (Math.cos(rad) * length) / 2.0 const cx = width / 2.0 const cy = height / 2.0 const x1 = cx - halfx const y1 = cy - halfy const x2 = cx + halfx const y2 = cy + halfy return [x1, y1, x2, y2]; } init();
 html, body, canvas { width: 100%; height: 100%; margin: 0; padding: 0; }
 <canvas width="128" height="72"></canvas>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM