SVG animation does not set attributes as expected - animation

I am trying to animate the changing of the viewport of an SVG element. When a particular ellipse in the SVG is clicked the viewport is changed so that it is zoomed in on the ellipse. From my interpretation of the W3C animate specification, when fill="freeze" is used, the value in the to attribute will stay. However, when I use console.log("after animation: "+canvas.getAttribute("viewBox")); the viewBox is the same as before (the animation does zoom in). For extra info, the <animate> is added when the ellipse is clicked. This is the code for how the animation is added to the <svg>;
var canvassnap=Snap(canvas);
var animation = '<animate id="smoothpan" attributeName="viewBox" begin="0s" dur="'+duration+'ms" from="0 0 1280 720" to="'+minX+' '+minY+' '+(maxX-minX)+' '+(maxY-minY)+'" fill="freeze" />';
var parse = Snap.parse(animation);
canvassnap.add(parse);
Do I not understand the specification properly?

SVG has two values for each attribute, the base value which you can get either with getAttribute or via element.viewBox.baseVal and the animated value which you can get via element.viewBox.animVal.
If an element is not the subject of SMIL animation then animVal == baseVal.
SMIL animation only affects the animVal and it's the animVal that is used for rendering.

Related

Animated Bar Chart with SVG and CSS (D3 & svelte)

I want to make an animated bar chart with scroller. I already made the scroll and the data interactivity, but I stuck at the transition.
I use the CSS transition on each <g> element. While it's animating and moving toward the right direction, they start at nowhere/or center(?), but not from the last position. I made the data key consectetur to be always the largest every time, so it is supposed to be always on top of the chart. But it's always animating from the center or the bottom, not staying on top.
<svg>
{#each data as d}
<!--translate Y reactive to data changes -->
<g class="gBar" transform="translate(0 {d.id * 10})">
<text>Lorem ipsum</text>
<!--rect width reactive to data changes -->
<rect x="0" y="0"
width={d.val/max * w * 0.5}
height="10"
fill="black"/>
</g>
{/each}
</svg>
<style>
/* add transition to the gBar class */
.gBar {
transition:transform 1s;
}
</style>
I tried the svelte transition directive, but a bit confused to trigger it with the index change by the scroller. (The example of transition directive is using boolean checkbox)
This is what I already made:
https://svelte.dev/repl/099ac842d7a54ab7a674545f8dc6d622?version=3.53.1

With canvas, ctx.lineTo draws a longer "Y" line than instructed (160 instead of 120) [duplicate]

I have 2 canvases, one uses HTML attributes width and height to size it, the other uses CSS:
<canvas id="compteur1" width="300" height="300" onmousedown="compteurClick(this.id);"></canvas>
<canvas id="compteur2" style="width: 300px; height: 300px;" onmousedown="compteurClick(this.id);"></canvas>
Compteur1 displays like it should, but not compteur2. The content is drawn using JavaScript on a 300x300 canvas.
Why is there a display difference?
It seems that the width and height attributes determine the width or height of the canvas’s coordinate system, whereas the CSS properties just determine the size of the box in which it will be shown.
This is explained in the HTML specification:
The canvas element has two attributes to control the size of the element’s bitmap: width and height. These attributes, when specified, must have values that are valid non-negative integers. The rules for parsing non-negative integers must be used to obtain their numeric values. If an attribute is missing, or if parsing its value returns an error, then the default value must be used instead. The width attribute defaults to 300, and the height attribute defaults to 150.
To set the width and height on a canvas, you may use:
canvasObject.setAttribute('width', '150');
canvasObject.setAttribute('height', '300');
For <canvas> elements, the CSS rules for width and height set the actual size of the canvas element that will be drawn to the page. On the other hand, the HTML attributes of width and height set the size of the coordinate system or 'grid' that the canvas API will use.
For example, consider this (jsfiddle):
var ctx = document.getElementById('canvas1').getContext('2d');
ctx.fillStyle = "red";
ctx.fillRect(10, 10, 30, 30);
var ctx2 = document.getElementById('canvas2').getContext('2d');
ctx2.fillStyle = "red";
ctx2.fillRect(10, 10, 30, 30);
canvas {
border: 1px solid black;
}
<canvas id="canvas1" style="width: 50px; height: 100px;" height="50" width="100"></canvas>
<canvas id="canvas2" style="width: 100px; height: 100px;" height="50" width="100"></canvas>
Both have had the same thing drawn on them relative to the internal coordinates of the canvas element. But in the second canvas, the red rectangle will be twice as wide because the canvas as a whole is being stretched across a bigger area by the CSS rules.
Note: If the CSS rules for width and/or height aren't specified then the browser will use the HTML attributes to size the element such that 1 unit of these values equals 1px on the page. If these attributes aren't specified then they will default to a width of 300 and a height of 150.
The canvas will be stretched if you set the width and height in your CSS. If you want to dynamically manipulate the dimension of the canvas you have to use JavaScript like so:
canvas = document.getElementById('canv');
canvas.setAttribute('width', '438');
canvas.setAttribute('height', '462');
The browser uses the css width and height, but the canvas element scales based on the canvas width and height. In javascript, read the css width and height and set the canvas width and height to that.
var myCanvas = $('#TheMainCanvas');
myCanvas[0].width = myCanvas.width();
myCanvas[0].height = myCanvas.height();
Shannimal correction
var el = $('#mycanvas');
el.attr('width', parseInt(el.css('width')))
el.attr('height', parseInt(el.css('height')))
Canvas renders image by buffer, so when you specify the width and height HTML attributes the buffer size and length changes, but when you use CSS, the buffer's size is unchanged. Making the image stretched.
Using HTML sizing.
Size of canvas is changed -> buffer size is changed -> rendered
Using CSS sizing
Size of canvas is changed -> rendered
Since the buffer length is kept unchanged, when the context renders the image,
the image is displayed in resized canvas (but rendered in unchanged buffer).
CSS sets the width and height of the canvas element so it affects the coordinate space leaving everything drawn skewed
Here's my way on how to set the width and height with Vanilla JavaScript
canvas.width = numberForWidth
canvas.height = numberForHeight
I believe CSS has much better machinery for specifying the size of the canvas and CSS must decide styling, not JavaScript or HTML. Having said that, setting width and height in HTML is important for working around the issue with canvas.
CSS has !important rule that allows to override other styling rules for the property, including those in HTML. Usually, its usage is frowned upon but here the use is a legitimate hack.
In Rust module for WebAssembly you can do the following:
fn update_buffer(canvas: &HtmlCanvasElement) {
canvas.set_width(canvas.client_width() as u32);
canvas.set_height(canvas.client_height() as u32);
}
//..
#[wasm_bindgen(start)]
pub fn start() -> Result<(), JsValue> {
// ...
let canvas: Rc<_> = document
.query_selector("canvas")
.unwrap()
.unwrap()
.dyn_into::<HtmlCanvasElement>()
.unwrap()
.into();
update_buffer(&canvas);
// ...
// create resizing handler for window
{
let on_resize = Closure::<dyn FnMut(_)>::new(move |_event: Event| {
let canvas = canvas.clone();
// ...
update_buffer(&canvas);
// ...
window.add_event_listener_with_callback("resize", on_resize.as_ref().unchecked_ref())?;
on_resize.forget();
}
}
There we update the canvas buffer once the WASM module is loaded and then whenever the window is resized. We do it by manually specifying width and height of canvas as values of clientWidth and clientHeight. Maybe there are better ways to update the buffer but I believe this solution is better than those suggested by #SamB, #CoderNaveed, #Anthony Gedeon, #Bluerain, #Ben Jackson, #Manolo, #XaviGuardia, #Russel Harkins, and #fermar because
The element is styled by CSS, not HTML.
Unlike elem.style.width & elem.style.height trick used by #Manolo or its JQuery equivalent used by #XaviGuardia, it will work for canvas whose size is specified by usage as flex or grid item.
Unlike the solution by #Russel Harkings, this also handles resizing. Though I like his answer because it is really clean and easy.
WASM is the future! Haha :D
P.S. there's a ton of .unwrap() because Rust explicitly handles possible failures.
P.P.S.
{
let on_resize = Closure::<dyn FnMut(_)>::new(move |_event: Event| {
let canvas = canvas.clone();
// ...
update_buffer(&canvas);
// ...
window.add_event_listener_with_callback("resize", on_resize.as_ref().unchecked_ref())?;
on_resize.forget();
}
can be done much cleaner with better libraries. E.g.
add_resize_handler(&window, move |e: ResizeEvent| {
let canvas = canvas.clone();
// ...
update_buffer(&canvas);
})
If you want a dynamic behaviour based on, e.g. CSS media queries, don't use canvas width and height attributes. Use CSS rules and then, before getting the canvas rendering context, assign to width and height attributes the CSS width and height styles:
var elem = document.getElementById("mycanvas");
elem.width = elem.style.width;
elem.height = elem.style.height;
var ctx1 = elem.getContext("2d");
...

d3.tree scrollbar instead of panning

I have a tree width d3.js. when tree overflows svg, it is hidden and I can view hidden parts by panning on tree. is there any way to add scrollbar for view hidden parts instead of panning?
for example in this sample http://bl.ocks.org/robschmuecker/7880033 I want to add scroll bar to this tree. this sample is not what I want: https://bl.ocks.org/CrandellWS/ca7e6626c9e6b1413963
because in this example when we collapse nodes, scrollbar size not change.
You can get the svg bounding box using svg.getBBox(), where svg is your svg node, e.g.
let svg = document.getElementsByTagName("svg")[0];
let box = svg.getBBox()
and then use box.x, .y, .width and .height to calculate the correct viewBox attribute value, width and height.

UWP - positioning ruler using matrix transform inside zoomable ScrollViewer

I have InkCanvas inside a zoomable ScrollViewer:
<ScrollViewer x:Name="ScrollViewer" ZoomMode="Enabled">
<Border Height="5000" Width="5000" BorderBrush="Black" BorderThickness="1">
<InkCanvas x:Name="inkCanvas" />
</Border>
</ScrollViewer>
I want to position ruler to the top left corner.
Ruler is positioned on the InkCanvas and state of ScrollViewer is defined by by HorizontalOffset, VerticalOffset and ZoomFactor
I've found this code (sample)
void OnBringIntoView(e)
{
// Set Ruler Origin to Scrollviewer Viewport origin.
// The purpose of this behavior is to allow the user to "grab" the
// ruler and bring it into view no matter where the scrollviewer viewport
// happens to be. Note that this is accomplished by a simple translation
// that adjusts to the zoom factor. The additional ZoomFactor term is to
// make ensure the scale of the InkPresenterRuler is invariant to Zoom.
Matrix3x2 viewportTransform =
Matrix3x2.CreateScale(ScrollViewer.ZoomFactor) *
Matrix3x2.CreateTranslation(
ScrollViewer.HorizontalOffset,
ScrollViewer.VerticalOffset) *
Matrix3x2.CreateScale(1.0f / ScrollViewer.ZoomFactor);
ruler.Transform = viewportTransform;
}
in short:
viewport = Scale(zoom) * Translate(offset) * Scale(1/zoom)
This works, but I'm a bit lost.
What does the first scale do and what does the second? Why can't I use juct TranslateTransform?
Why can't I use juct TranslateTransform?
Because the InkPresenterRuler.Transform property is designed to be Matrix3x2.
What does the first scale do and what does the second?
Scale(zoom)*Scale(1/zoom) aiming at letting the ruler be invariant to Zoom. No matter what your current zoom level is, the ruler will return to it's original size.
Translate(offset) changes the translation values(offsetX and offsetY) of the transform Matrix.
For details of the transformation theory you can refer to:
Remarks of Matrix Transform
Affine transformations section of Transformation matrix Wiki.

Hit testing against text shapes

I want to know whether a given point is inside or outside of a text shape. As you will notice in the sample I provided below, hitTest will return true as soon as the point is inside of the TextItem's bounds, and not only if the point is inside of the character itself. (You can experience this behavior best when you place your mouse pointer in the middle of the #)
Sample: Hit-testing against TextItem
I also tried drawing the character based on paths (as Raphaël is doing in their font samples) to use the paths itself for hit-testing but stumbled upon some quite strange behavior where (some) characters are not drawn correctly. (If you copy the path definition into a vector image software like Inkscape the text shapes are drawn correctly)
Sample: Drawing text as path
What is the most promising way to find out whether a given point is inside or outside of a text shape?
You can hit-test a text shape (or any other mathematically irregular shape) by texting whether the pixel under the mouse is transparent or not.
You can get the pixel-array for the entire canvas using:
var data=context.getImageData(0,0,canvas.width,canvas.height).data;
Then you can fetch the opacity (alpha) value for the pixel under the mouse like this:
var pixelIsTransparent = data[(mouseY*canvas.width+mouseX)*4+3]==0
If the pixel is not transparent then you're over the text shape.
If you have other non-text drawings on the canvas then those non-text drawings might give false-positives for your hit-tests. A workaround for that is to use a second in-memory canvas containing only your text-shape and then do hit testing against the pixels on that second canvas.
Here's example code and a Demo:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var $canvas=$("#canvas");
var canvasOffset=$canvas.offset();
var offsetX=canvasOffset.left;
var offsetY=canvasOffset.top;
ctx.strokeStyle='gray';
ctx.font='300px verdana';
var wasHit=false;
var isHit=false;
draw();
var data=ctx.getImageData(0,0,canvas.width,canvas.height).data;
$("#canvas").mousemove(function(e){handleMouseMove(e);});
function draw(){
ctx.fillStyle=(isHit)?'green':'lightgray';
ctx.fillText("M",25,250);
ctx.strokeText("M",25,250);
}
function handleMouseMove(e){
e.preventDefault();
e.stopPropagation();
mouseX=parseInt(e.clientX-offsetX);
mouseY=parseInt(e.clientY-offsetY);
isHit=(data[(mouseY*cw+mouseX)*4+3]>10)
if(!isHit==wasHit){
draw();
wasHit=isHit;
}
}
body{ background-color: ivory; }
#canvas{border:1px solid red;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<p>Hit test: Move mouse over letter</p>
<canvas id="canvas" width=300 height=300></canvas>
After spending quite some time debugging paper.js code I finally found the solution for this problem.
Instead of using Path you are supposed to use CompoundPath:
A compound path contains two or more paths, holes are drawn where the paths overlap. All the paths in a compound path take on the style of the backmost path and can be accessed through its item.children list.
I also updated the example from above:
http://jsfiddle.net/64v7s6L9/1/

Resources