65.9K
CodeProject 正在变化。 阅读更多。
Home

虚幻的 XAML:重塑 Benham's Top

2018年4月1日

CPOL

12分钟阅读

viewsIcon

26464

downloadIcon

201

1895年发明的本汉陀螺的奥秘,以及费希纳色彩效应,至今仍未完全揭开。WPF和XAML极大地加速了研究进程。

警告:在电脑屏幕上运行和观察提供的应用程序可能会引发光敏感性癫痫症患者的癫痫发作。请谨慎观看。

引言

如果你在大象笼子上看到一个“水牛”标志,请不要相信你的眼睛

Kozma Prutkov

目录

引言

多年前,我曾对一本普及心理学的书中提到的一种玩具所产生的奇特视觉现象深感着迷。这是一种相当有名的发明,叫做本汉陀螺,或本汉圆盘,以其发明者——一位业余科学家兼玩具制造商的名字命名。

这个圆盘的黑白图案在旋转时,会产生各种微弱但清晰可见的色彩的幻觉效应,称为费希纳色彩。与其他许多人类视觉感知错觉不同,这种效应至今仍未完全被理解。

毫不奇怪,我立即用木棍、硬纸板、凡顿纸、中国墨水和工程绘图工具重现了这个物体,尝试了不同的图案。令我惊讶的是,它立即奏效,但……前提是我的图案必须非常接近原始的本汉陀螺。有时,微小的差异就足以完全破坏原本明显的效果。可能大多数时候我都走错了方向。后来,我看到了几位作者发表的不同设计的出版物,声称它们能产生类似的效果;更重要的是,有些人甚至称他们的图案为“本汉陀螺”。尽管有这些说法……但这些设计实际上都没有奏效。一点都没有。此类出版物和声明本身就构成了另一个谜。

那么,问题到底出在哪里?为什么有些图案有效而有些无效?这个问题很难回答。

最近,我决定花些时间创建一个相当方便的实验工具。很明显,创建和测试图案所需的时间是最主要的限制因素。为了使实验富有成效,我们需要大量的试错。同样显而易见的是,在实验初期,很难预测图案编辑器所需的功能。

这些考虑导致了一个想法:实验应用程序应该提供一些功能的组合。一开始,最好能提供渲染和旋转由某些可用外部编辑器创建的图案的功能。获得一些经验将使未来的计划更加清晰。当一些用于创建新图案的技术得以开发后,专门的图案编辑器将进一步加速研究,因为图案可以即时创建并逐步进行测试。

这就是“ColorTop”应用程序的诞生过程。在本文中,我将解释它的工作原理并报告一些基本的实验结果。

XAML作为矢量图形

似乎很明显,为了我们的目的,矢量图形是最佳选择。借助矢量图形,我们可以无缝地缩放图像,并且在几乎不损失质量的情况下进行渲染,最大限度地减少像素化,这得益于抗锯齿技术

让我们从简单的开始。首先,我想说明XAML作为矢量图形的强大功能。
本文附带源代码,展示了两个应用程序:主要的“ColorTop”和简单的入门应用程序“XamlDemo”。让我们先看看“XamlDemo”。

这个简单的应用程序展示了我对CodeProject吉祥物进行的矢量化处理。该图像可缩放,因此在缩放时几乎没有像素化。

此外,一个金色的星星被放置在背景中,并使用标准的WPF动画进行动画处理。

如果您查看提供的源代码(名为“XamlDemo”的项目),您会发现几乎什么都没有。甚至图像似乎也丢失了。实际上,一个单独的XAML元素Canvas,是这里唯一的此类型元素,它包含了所有内容。它可以在项目唯一的XAML文件“MainWindows.xaml”中找到。实际上,更一致的技术应该将矢量图形部分放在一个单独的文件中,定义一个资源字典或一组这样的字典,但我想要演示最简单的解决方案。

请注意,Canvas元素放置在Viewbox元素中,该元素定义了视图的缩放行为。通过设置一个属性:Stretch="Uniform",可以实现保持原始纵横比的正确缩放。

那么,Canvas的内容本身来自哪里?

首先,我需要为不完美的图像矢量化道歉。第一个问题是取自CodeProject网站的位图的像素尺寸和质量有限。理想情况下,矢量图像应该从头开始创建。我是如何获得矢量化图像的?哦,这得益于我的“秘密武器”,我最喜欢的应用程序之一,跨平台开源Inkscape。我强烈推荐这个应用程序给任何需要2D矢量图形,以及许多需要任何类型图形的人。

对于CodeProject徽标,我使用了Inkscape中简单而有效的“路径->跟踪位图”功能,从位图中提取基本的贝塞尔曲线,然后使用标准的矢量图形控件手动调整曲线。之后,我将图像保存为SVG和XAML格式。从矢量图形的角度来看,SVG格式比XAML功能更丰富,并且足够紧凑,因此在所有情况下都应更好地保留。

这就是动画的实现方式

public partial class MainWindow : Window {

    public MainWindow() {
        InitializeComponent();
    } //MainWindow

    void Animate(Canvas svg, Path star) {
        DoubleAnimation da = new DoubleAnimation();
        da.From = 0;
        da.To = 360;
        da.Duration = new Duration(TimeSpan.FromSeconds(5));
        da.RepeatBehavior = RepeatBehavior.Forever;
        RotateTransform rt = new RotateTransform();
        rt.CenterX = svg.Width / 2;
        rt.CenterY = svg.Height / 2;
        star.RenderTransform = rt;
        rt.BeginAnimation(RotateTransform.AngleProperty, da);
    } //Animate

    protected override void OnContentRendered(System.EventArgs e) {
        base.OnContentRendered(e);
        svg.Background = Brushes.Transparent;
        Animate(this.svg, this.star);
    } //OnContentRendered

} //class MainWindow

实际上,这构成了整个应用程序特有的代码部分。

这里的关键是理解对对象this.star的访问。它起源于Inkscape创建时的XAML。当我们想要操作源自SVG的矢量图形对象时,我们需要为相应的XAML对象属性生成一个Name属性值。Inkscape生成的XAML代码中的Name值是从SVG的XML id属性值复制而来的。由于属性id值是唯一的,生成的Name值也是唯一的,这是其中一个要求。

因此,配方包括以下步骤:使用Inkscape,应使用“对象属性”编辑SVG对象;其id值应赋予一个在文档范围内唯一且也是有效.NET变量/属性标识符,并且对代码开发者来说易于识别和方便的值。

动态加载XAML矢量图形

现在,让我们进入下一步。应用程序“ColorTop”提供加载和保存XAML文件的功能,而不是嵌入矢量图形。图形可以通过专业的方式进行编辑,有助于费希纳色彩效应的实验。

加载本身相当简单

using XamlReader = System.Windows.Markup.XamlReader;
using Canvas = System.Windows.Controls.Canvas;
using Viewbox = System.Windows.Controls.Viewbox;

// ...

public MainWindow() {
    // ...
    Action reload = () => {
        if (openDialog.ShowDialog() != true) return;
        using (XmlReader reader = XmlReader.Create(openDialog.FileName)) {
            DependencyObject top = XamlReader.Load(reader) as DependencyObject;
            Canvas canvas = findCanvas(top);
            if (canvas == null) throw new Main.NoCanvasException();
            Viewbox vb = top as Viewbox;
            if (vb == null) {
                vb = new Viewbox();
                vb.Child = canvas;
            } //if
            this.borderAnimationBackground.Child = vb;
            animationState.Animate(animatedElement, animation, canvas);
            string title = getTitle(vb);
            if (title == null)
                title = getTitle(canvas);
            if (title == null)
                title = System.IO.Path.GetFileName(openDialog.FileName);
            animationState.setTitle(this, title);
        } //using
    }; //reload
    // ...
}

动画与上面显示的代码类似。这段加载代码中唯一值得注意的部分是关于查找某些XAML元素的,首先是Canvas

using Canvas = System.Windows.Controls.Canvas;
using LogicalTreeHelper = System.Windows.LogicalTreeHelper;

// ...

static Canvas findCanvas(System.Windows.DependencyObject parent) {
    if (parent == null) return null;
    Canvas result = parent as Canvas;
    if (result != null) return result;
    DependencyObject current = parent;
    var children = LogicalTreeHelper.GetChildren(parent);
    foreach (var child in children) {
        result = child as Canvas;
        if (result != null)
            return result;
        else
            return findCanvas(child as System.Windows.DependencyObject);
    } //loop
    return result;
} //findCanvas

这样做是为了给XAML文件作者一些自由。Inkscape创建的XAML代码使用Viewbox元素作为根文档元素,但这并非强制规定。顶级元素可以是其他内容,尤其是Canvas。因此,应该找到顶层Canvas实例。如果它的父级是Viewbox,则可以使用它;如果不是,则应该创建一个Viewbox实例,正如在前面的代码示例中所显示的。

保存XAML矢量图形

保存矢量图形XAML会更简单,如果不是因为“ColorTop”应用程序的设计——它的主窗口功能被抽象出来,以便隔离图形编辑器。这是我基于实现某个接口的窗口类的方法。接口IGeneratorClient定义在一个用于定义部分类声明的文件中;这个部分类实现了这个接口。由于这种设计,打开和保存命令在不同的窗口中执行:主窗口菜单有“打开”命令,另一个窗口“图案编辑器”的菜单中有“保存矢量图形”项。尽管如此,这两个操作都由主窗口类方便地实现。

这种形式的部分类声明对于窗口特别方便,因为窗口的继承列表只包含已实现的接口,而不包含基类。这样,窗口类的不同独立方面被分隔到不同的文件中,大大提高了代码的可维护性。如下所示

using XamlWriter = System.Windows.Markup.XamlWriter;
using StreamWriter = System.IO.StreamWriter;
using Canvas = System.Windows.Controls.Canvas;
using Viewbox = System.Windows.Controls.Viewbox;

// ...

interface IGeneratorClient {
    void NewPattern(string title);
    void SavePattern(string title, string fileName);
    void Add(string title, Main.Sector sector);
    bool CanUndo();
    bool CanRedo();
    bool Undo();
    bool Redo();
}

public partial class MainWindow : IGeneratorClient {

    void IGeneratorClient.SavePattern(string title, string fileName) {
        Viewbox viewBox = (Viewbox)borderAnimationBackground.Child;
        viewBox.Tag = title;
        using (StreamWriter writer = new StreamWriter(fileName, false))
            XamlWriter.Save(viewBox, writer);
    } //IGeneratorClient.SavePattern

    // ...
}

玩转幻觉

对这种幻觉的研究不能自称是全面的。然而,对不同圆盘状图案进行的初步实验揭示了以下几点:

  • 有些人从未观察到任何色彩效果。有些人会开你的玩笑,声称你的演示是一个骗局。实际上,我甚至不知道这些人是否真的没有观察到颜色;我的一种假设是,这些人只是想惹我。

  • 明亮的屏幕有助于观察幻觉;同时,屏幕的亮度不应过高——观察应该是舒适的。

  • 效果在约 200-900 rpm 时出现;接近 1000 rpm 时,可能会逐渐衰减。

  • 旋转方向似乎没有明显区别。

  • 旋转图案时的任何径向振荡都会破坏效果。

  • 任何频闪效果也会破坏效果。

  • 如果图案的角尺寸至少有约 1/4 是实心黑色,则可以看到该效果。

  • 中间有一个小的实心白色圆形,用细黑圆环勾勒,会在其白色背景上产生额外的色彩效果。

基于这些观察的一些相当成功的样本图案被称为“Experimental1.xaml”、“Experimental2.xaml”和“Experimental3.xaml”。它们都是使用下面描述的图案编辑器创建的。

如果你不相信演示怎么办?

事实上,即使是纯单色演示在电脑屏幕上也不是真正单色的。毕竟,大多数现代显示器都显示彩色像素。您看到的“白色”或“黑色”是色彩子像素之间的颜色平衡以及个体色彩视觉特征的问题。此外,这些子像素还存在微妙的效应;特别是,很容易在特殊排列的“黑白”形状的边缘看到颜色。

因此,如果电脑演示似乎不够令人信服,您总是可以制作一个真实的陀螺,使用一块圆形的纸板和一个削尖的棍子。您可以随时以所需的比例打印SVG或XAML文件,然后将其粘贴到圆盘顶部。

如果您不信任打印机的黑色效果,您甚至可以购买一瓶印度(中国)墨水,并在打印好的图像顶部手动绘制图案。这种墨水(实际上是基于烟灰的颜料)以其良好的中性黑色渲染质量而闻名。用墨水进行精确绘图需要一种特殊的工具:钢笔(或称排线器)与绘图仪结合使用;这种钢笔是专门为此类墨水设计的,尤其是不容易堵塞,并且很容易清除干墨。

我的预测是:如果您能在这些计算机实验中看到费希纳色彩效应,那么只要使用明亮的灯光并改变旋转速度,您在自己的纸质陀螺上也能看到它。

图案编辑器

基于不同图案的效应观察如上所述,可以得出一个简单的实用规则:一大类潜在成功的图案可以设计成仅由一种类型的元素组成:一定厚度的弧形片段。在极坐标下,这种形状也可以被视为实心黑色的矩形形状的类似物。显然,这类形状包括实心圆形和圆环。

上述观察得出的一个结论是:图案中的一些随机性可能非常有用。这个结论得到了实验的充分证实。

所有这些图案都可以使用图案编辑器轻松即时创建,该编辑器显示在顶部图片中。

无需展示该编辑器的编程方面。只需注意,所有参数可以是一个数字,或者两个用空格分隔的数字。如果只有一个数字,则该参数是确定的。在有两个数字的情况下,形状是以参数的随机值生成的,在第一个和第二个数字之间的区间内具有均匀概率分布。显然,输入确定性值的另一种方式是输入一对相等的数字。

结论

如果我们能通过简单的幻觉(无论是否完全解释)产生广泛的颜色,那么谁还需要带有“真实”彩色像素的显示器或数字电视?

如果有人想反对视觉幻觉,我可以提供一个清醒的论点:所有成像和视频技术无论如何都是完全基于幻觉的。没有照片或视频技术能重现原始风景的全部光谱;相反,每个图像元素(像素或感光材料的颗粒)都是三种固定颜色的组合,原因仅仅是人类视觉基于三种具有三种明显不同响应光谱的视锥细胞。我甚至不想详细阐述所谓的“3D”成像,即使从视网膜运作的角度来看,它也是完全虚幻的。

与其浪费资源来渲染“逼真”的屏幕颜色,不如迈出更彻底的实用一步:回到单色成像,通过使用类似于费希纳色彩效应的效果来弥补色彩的不足。通过消除几乎无用的彩色子像素,我们可以专注于获得更有价值的屏幕特性:更好的对比度和屏幕分辨率,这些都可以通过纯灰度像素来实现。

在电视和互联网视频流领域,我们可以专注于纯矢量成像。即使实时矢量化是一个困难的技术问题,我们也能在可用带宽的使用以及图像和视频的质量方面获得巨大的收益。

© . All rights reserved.