Reputation: 3630
I have a big text to insert inside a canvas.
first I wrap the text, and now I'm trying to make it scrollable inside the canvas.
context.wrapText = function(text, x, y, maxWidth, lineHeight) {
var y_initial = y;
var cars = text.split("\n");
for (var ii = 0; ii < cars.length; ii++) {
var line = "";
var words = cars[ii].split(" ");
for (var n = 0; n < words.length; n++) {
var testLine = line + words[n] + " ";
var metrics = context.measureText(testLine);
var testWidth = metrics.width;
if (testWidth > maxWidth) {
context.fillText(line, x, y);
line = words[n] + " ";
y += lineHeight;
}
else {
line = testLine;
}
}
context.fillText(line, x, y);
y += lineHeight;
}
return y - y_initial;
}
context.wrapText("hello dear friends\nhere is\na sample of text\nthis is juste an example\nthanks\nbut need to be scrollable\n it works?\nlet's try.", 120, 80, 200, 15);
thank you for your help.
Upvotes: 0
Views: 3518
Reputation: 54128
The canvas is just a pixel store. It does not remember what is drawn except for the pixels. To do a scrollbox you need to render the whole scroll box every time you move it.
The context lets you set a clip area. You use that to make sure text that goes outside the bounds is not shown.
Use the ctx.setTransform to set the scroll position for rendering. If you scroll up the scroll position must go into the negative.
Demo is a very basic example of a text scroll box. It has not optimisations and the text fitting has no real smarts.
See code for details, it's quite basic code so not that many comments. There is not mouse or user interaction, that I leave up to you.
const ctx = canvas.getContext("2d");
const textScrollBox = {
dirty : true, // indicates that variouse setting need update
cleanit(dontFitText){
if(this.dirty){
this.setFont();
this.getTextPos();
this.dirty = false;
if(!dontFitText){
this.fitText();
}
}
},
scrollY : 0,
fontSize : 24,
font : "Arial",
align : "left",
background : "#999",
border : {
lineWidth : 4,
style : "black",
corner : "round",
},
scrollBox : {
width : 10,
background : "#568",
color : "#78a",
},
fontStyle : "black",
setOptions(options){
Object.keys(this).forEach((key) =>{
if(options[key] !== undefined){
this[key] = options[key];
this.dirty = true;
}
})
},
setFont(){
this.fontStr = this.fontSize + "px " + this.font;
this.textHeight = this.fontSize + Math.ceil(this.fontSize * 0.05);
},
getTextPos(){
if(this.align === "left"){ this.textPos = 0 }
else if(this.align === "right"){ this.textPos = Math.floor(this.width - this.scrollBox.width -this.fontSize / 4) }
else { this.textPos = Math.floor((this.width- - this.scrollBox.width) / 2) }
},
fitText(){
this.cleanit(true); // MUST PASS TRUE or will recurse to call stack overflow
ctx.font = this.fontStr;
ctx.textAlign = this.align;
ctx.textBaseline = "top";
var words = this.text.split(" ");
this.lines.length = 0;
var line = "";
var space = "";
while(words.length > 0){
var word = words.shift();
var width = ctx.measureText(line + space + word).width;
if(width < this.width - this.scrollBox.width - this.scrollBox.width){
line += space + word;
space = " ";
}else{
if(space === ""){ // if one word too big put it in anyways
line += word;
}else{
words.unshift(word);
}
this.lines.push(line);
space = "";
line = "";
}
}
if(line !== ""){
this.lines.push(line);
}
this.maxScroll = ((this.lines.length + 0.5) * this.textHeight) - this.height;
},
drawBorder(){
var bw = this.border.lineWidth / 2;
ctx.lineJoin = this.border.corner;
ctx.lineWidth = this.border.lineWidth;
ctx.strokeStyle = this.border.style;
ctx.strokeRect(this.x - bw,this.y - bw,this.width + 2 * bw,this.height + 2 * bw);
},
drawScrollBox(){
var displayHeight = this.height;
var scale = this.height / (this.lines.length * this.textHeight);
ctx.fillStyle = this.scrollBox.background;
ctx.fillRect(
this.x + this.width - this.scrollBox.width,
this.y,this.scrollBox.width,this.height
)
ctx.fillStyle = this.scrollBox.color;
ctx.fillRect(
this.x + this.width - this.scrollBox.width,
this.y - (this.scrollY * scale),
this.scrollBox.width,this.height * scale
)
},
scroll(pos){
this.cleanit();
this.scrollY = -pos;
if(this.scrollY > 0){
this.scrollY = 0;
}else if(this.scrollY < -this.maxScroll ){
this.scrollY = -this.maxScroll ;
}
},
render(){
this.cleanit();
ctx.font = this.fontStr;
ctx.textAlign = this.align;
this.drawBorder();
ctx.save(); // need this to reset the clip area
ctx.fillStyle = this.background;
ctx.fillRect(this.x,this.y,this.width,this.height);
this.drawScrollBox();
ctx.beginPath();
ctx.rect(this.x,this.y,this.width-this.scrollBox.width,this.height);
ctx.clip();
// Important text does not like being place at fractions of a pixel
// make sure you round the y pos;
ctx.setTransform(1,0,0,1,this.x, Math.floor(this.y + this.scrollY));
ctx.fillStyle = this.fontStyle;
for(var i = 0; i < this.lines.length; i ++){
// Important text does not like being place at fractions of a pixel
// make sure you round the y pos;
ctx.fillText(this.lines[i],this.textPos,Math.floor(i * this.textHeight));
}
ctx.restore(); // remove the clipping
}
}
function createScrollText( text, x, y, width, height, options = {} ){
return Object.assign({},
textScrollBox,{
text,x, y, width, height,
lines : [],
},
options
);
}
const text = "This is some random text added to the canvas via 2d API as a simple scroll box example at Stack Overflow. Happy rendering 😀👍"
var scrollText = createScrollText(text, 10, 10, 200, 180);
var scrollText1 = createScrollText(text, 220, 10, 270, 180);
scrollText1.setOptions({ // set only the properties you want to change
fontStyle : "white",
scrollBox : { // if you add scrollbox you must put all property in for it
width : 5,
background : "#DDD",
color : "#555",
},
border : { // if you add border you must put all property in for it
lineWidth : 4,
style : "black",
corner : "round",
},
font : "Comic Sans MS",
fontSize : 36,
align :"center",
})
function mainLoop(time){
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,canvas.width,canvas.height);
scrollText.scroll((Math.sin(time/3000) + 1) * scrollText.maxScroll * 0.5);
scrollText1.scroll((Math.sin(time/3462) + 1) * scrollText1.maxScroll * 0.5);
scrollText.render();
scrollText1.render();
requestAnimationFrame(mainLoop);
}
requestAnimationFrame(mainLoop);
canvas {
border : 2px solid black;
}
<canvas id="canvas" width=500 height=200></canvas>
Upvotes: 4